diff options
author | John Lee <64482439+algojohnlee@users.noreply.github.com> | 2023-01-25 10:55:52 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-25 10:55:52 -0500 |
commit | 39ea2c6078ddbefbb15b7e7e7ca2840c43aa20a3 (patch) | |
tree | 66fa710cfb84d4b9dd6ad32b606187a69e3c0ff3 | |
parent | afe5b36903268cc6b03e236919d782e9d33bb88d (diff) | |
parent | 8dd7febf3ecbb39deeed5ea63d706c7866814627 (diff) |
Merge pull request #5055 from Algo-devops-service/relbeta3.14.1v3.14.1-beta
40 files changed, 1230 insertions, 267 deletions
diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index e39a09167..ec1a85943 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -88,7 +88,6 @@ jobs: -c .golangci-warnings.yml \ --issues-exit-code 0 \ --allow-parallel-runners > temp_golangci-lint-cgo.txt - cat temp_golangci-lint-cgo.txt cat temp_golangci-lint-cgo.txt | reviewdog \ -f=golangci-lint \ @@ -102,4 +101,4 @@ jobs: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} run: | curl -X POST --data-urlencode "payload={\"text\": \"Reviewdog failed. ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} \"}" $SLACK_WEBHOOK - if: ${{ failure() && (contains(github.ref_name, 'rel/nightly') || contains(github.ref_name, 'rel/beta') || contains(github.ref_name, 'rel/stable') || contains(github.ref_name, 'master')) }}
\ No newline at end of file + if: ${{ failure() && (contains(github.ref_name, 'rel/nightly') || contains(github.ref_name, 'rel/beta') || contains(github.ref_name, 'rel/stable') || contains(github.ref_name, 'master')) }} diff --git a/.golangci-warnings.yml b/.golangci-warnings.yml index e3b8d22ff..f8d206347 100644 --- a/.golangci-warnings.yml +++ b/.golangci-warnings.yml @@ -9,10 +9,8 @@ linters: - partitiontest - structcheck - varcheck - - unconvert - unused - linters-settings: custom: partitiontest: @@ -55,7 +53,6 @@ issues: - deadcode - structcheck - varcheck - - unconvert - unused # Add all linters here -- Comment this block out for testing linters - path: test/linttest/lintissues\.go @@ -63,7 +60,6 @@ issues: - deadcode - structcheck - varcheck - - unconvert - unused - path: crypto/secp256k1/secp256_test\.go linters: diff --git a/agreement/events_test.go b/agreement/events_test.go index 1ea35fa80..c558f0067 100644 --- a/agreement/events_test.go +++ b/agreement/events_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/algorand/go-algorand/protocol" + "github.com/algorand/go-algorand/test/partitiontest" "github.com/stretchr/testify/require" ) @@ -28,6 +29,7 @@ import ( // properly decoded from ConsensusVersionView. // This test is only needed for agreement state serialization switch from reflection to msgp. func TestSerializableErrorBackwardCompatibility(t *testing.T) { + partitiontest.PartitionTest(t) encodedEmpty, err := base64.StdEncoding.DecodeString("gqNFcnLAp1ZlcnNpb26jdjEw") require.NoError(t, err) diff --git a/agreement/persistence_test.go b/agreement/persistence_test.go index ef5d5da47..fbd9323b0 100644 --- a/agreement/persistence_test.go +++ b/agreement/persistence_test.go @@ -240,6 +240,7 @@ func TestEmptyMapDeserialization(t *testing.T) { } func TestDecodeFailures(t *testing.T) { + partitiontest.PartitionTest(t) clock := timers.MakeMonotonicClock(time.Date(2015, 1, 2, 5, 6, 7, 8, time.UTC)) ce := clock.Encode() log := makeServiceLogger(logging.Base()) diff --git a/buildnumber.dat b/buildnumber.dat index 573541ac9..d00491fd7 100644 --- a/buildnumber.dat +++ b/buildnumber.dat @@ -1 +1 @@ -0 +1 diff --git a/cmd/goal/formatting_test.go b/cmd/goal/formatting_test.go index 58a8d6d0d..f368c54c9 100644 --- a/cmd/goal/formatting_test.go +++ b/cmd/goal/formatting_test.go @@ -72,6 +72,7 @@ func TestNewBoxRef(t *testing.T) { } func TestStringsToBoxRefs(t *testing.T) { + partitiontest.PartitionTest(t) brs := stringsToBoxRefs([]string{"77,str:hello", "55,int:6", "int:88"}) require.EqualValues(t, 77, brs[0].appID) require.EqualValues(t, 55, brs[1].appID) diff --git a/cmd/tealdbg/README.md b/cmd/tealdbg/README.md index b5a7bebbf..14967677f 100644 --- a/cmd/tealdbg/README.md +++ b/cmd/tealdbg/README.md @@ -9,6 +9,7 @@ - [Protocol](#protocol) - [Transaction and Transaction Group](#transaction-and-transaction-group) - [Balance records](#balance-records) + - [Indexer Support](#indexer-support) - [Execution mode](#execution-mode) - [Chrome DevTools Frontend Features](#chrome-devtools-frontend-features) - [Configure the Listener](#configure-the-listener) @@ -44,7 +45,7 @@ and balance records (see [Setting Debug Context](#setting-debug-context) for det Remote debugger might be useful for debugging unit tests for TEAL (currently in Golang only) or for hacking **algod** `eval` and breaking on any TEAL evaluation. The protocol consist of three REST endpoints and one data structure describing the evaluator state. -See `WebDebuggerHook` and `TestWebDebuggerManual` in [go-algorand sources](https://github.com/algorand/go-algorand/tree/master/data/transactions/logic) for more details. +See `WebDebugger` and `TestWebDebuggerManual` in [go-algorand sources](https://github.com/algorand/go-algorand/tree/master/data/transactions/logic) for more details. ### Frontends @@ -206,13 +207,18 @@ Refer to the [Chrome DevTools debugging](https://developers.google.com/web/tools The evaluator accepts a new `Debugger` parameter described as the interface: ```golang -type DebuggerHook interface { +// Debugger is an interface that supports the first version of AVM debuggers. +// It consists of a set of functions called by eval function during AVM program execution. +// +// Deprecated: This interface does not support non-app call or inner transactions. Use EvalTracer +// instead. +type Debugger interface { // Register is fired on program creation - Register(state *DebugState) error + Register(state *DebugState) // Update is fired on every step - Update(state *DebugState) error + Update(state *DebugState) // Complete is called when the program exits - Complete(state *DebugState) error + Complete(state *DebugState) } ``` If `Debugger` is set the evaluator calls `Register` on creation, `Update` on every step and `Complete` on exit. @@ -251,13 +257,13 @@ The core calls `SessionEnded` on `Complete` call. If one needs to debug TEAL in as much real environment as possible then do -1. Add `WebDebuggerHook` to `data/transactions/logic/eval.go`: +1. Add `WebDebugger` to `data/transactions/logic/eval.go`: ```golang cx.program = program // begin new code debugURL := os.Getenv("TEAL_DEBUGGER_URL") - cx.Debugger = &WebDebuggerHook{URL: debugURL} + cx.Debugger = &WebDebugger{URL: debugURL} // end new code if cx.Debugger != nil { diff --git a/cmd/tealdbg/debugger.go b/cmd/tealdbg/debugger.go index 38a51f69d..3639a41af 100644 --- a/cmd/tealdbg/debugger.go +++ b/cmd/tealdbg/debugger.go @@ -26,6 +26,7 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/logging" ) // Notification is sent to the client over their websocket connection @@ -528,7 +529,7 @@ func (d *Debugger) SaveProgram( } // Register setups new session and notifies frontends if any -func (d *Debugger) Register(state *logic.DebugState) error { +func (d *Debugger) Register(state *logic.DebugState) { sid := state.ExecID pcOffset := make(map[int]int, len(state.PCOffset)) for _, pco := range state.PCOffset { @@ -556,12 +557,17 @@ func (d *Debugger) Register(state *logic.DebugState) error { // Wait for acknowledgement <-s.acknowledged - - return nil } // Update process state update notifications: pauses or continues as needed -func (d *Debugger) Update(state *logic.DebugState) error { +func (d *Debugger) Update(state *logic.DebugState) { + err := d.update(state) + if err != nil { + logging.Base().Errorf("error in Update hook: %s", err.Error()) + } +} + +func (d *Debugger) update(state *logic.DebugState) error { sid := state.ExecID s, err := d.getSession(sid) if err != nil { @@ -596,7 +602,14 @@ func (d *Debugger) Update(state *logic.DebugState) error { } // Complete terminates session and notifies frontends if any -func (d *Debugger) Complete(state *logic.DebugState) error { +func (d *Debugger) Complete(state *logic.DebugState) { + err := d.complete(state) + if err != nil { + logging.Base().Errorf("error in Complete hook: %s", err.Error()) + } +} + +func (d *Debugger) complete(state *logic.DebugState) error { sid := state.ExecID s, err := d.getSession(sid) if err != nil { diff --git a/cmd/tealdbg/debugger_test.go b/cmd/tealdbg/debugger_test.go index f97754a51..a73fb3f4f 100644 --- a/cmd/tealdbg/debugger_test.go +++ b/cmd/tealdbg/debugger_test.go @@ -102,7 +102,7 @@ func TestDebuggerSimple(t *testing.T) { debugger.AddAdapter(da) ep := logic.NewEvalParams(make([]transactions.SignedTxnWithAD, 1), &proto, nil) - ep.Debugger = debugger + ep.Tracer = logic.MakeEvalTracerDebuggerAdaptor(debugger) ep.SigLedger = logic.NoHeaderLedger{} source := `int 0 diff --git a/cmd/tealdbg/dryrunRequest.go b/cmd/tealdbg/dryrunRequest.go index 1ff43981a..5f13aa6cf 100644 --- a/cmd/tealdbg/dryrunRequest.go +++ b/cmd/tealdbg/dryrunRequest.go @@ -47,23 +47,6 @@ func ddrFromParams(dp *DebugParams) (ddr v2.DryrunRequest, err error) { return } -func convertAccounts(accounts []model.Account) (records []basics.BalanceRecord, err error) { - for _, a := range accounts { - var addr basics.Address - addr, err = basics.UnmarshalChecksumAddress(a.Address) - if err != nil { - return - } - var ad basics.AccountData - ad, err = v2.AccountToAccountData(&a) - if err != nil { - return - } - records = append(records, basics.BalanceRecord{Addr: addr, AccountData: ad}) - } - return -} - func balanceRecordsFromDdr(ddr *v2.DryrunRequest) (records []basics.BalanceRecord, err error) { accounts := make(map[basics.Address]basics.AccountData) for _, a := range ddr.Accounts { diff --git a/cmd/tealdbg/local.go b/cmd/tealdbg/local.go index d5673d9ae..8e9d9b81f 100644 --- a/cmd/tealdbg/local.go +++ b/cmd/tealdbg/local.go @@ -536,7 +536,7 @@ func (r *LocalRunner) RunAll() error { // ep.Debugger = r.debugger // if ep.Debugger != nil // FALSE if r.debugger != nil { - ep.Debugger = r.debugger + ep.Tracer = logic.MakeEvalTracerDebuggerAdaptor(r.debugger) } } diff --git a/cmd/tealdbg/remote.go b/cmd/tealdbg/remote.go index 1f27b5130..0656e418d 100644 --- a/cmd/tealdbg/remote.go +++ b/cmd/tealdbg/remote.go @@ -26,7 +26,7 @@ import ( "github.com/algorand/go-algorand/protocol" ) -// RemoteHookAdapter provides HTTP transport for WebDebuggerHook +// RemoteHookAdapter provides HTTP transport for WebDebugger type RemoteHookAdapter struct { debugger *Debugger } @@ -38,7 +38,7 @@ func MakeRemoteHook(debugger *Debugger) *RemoteHookAdapter { return r } -// Setup adds HTTP handlers for remote WebDebuggerHook +// Setup adds HTTP handlers for remote WebDebugger func (rha *RemoteHookAdapter) Setup(router *mux.Router) { router.HandleFunc("/exec/register", rha.registerHandler).Methods("POST") router.HandleFunc("/exec/update", rha.updateHandler).Methods("POST") @@ -59,11 +59,7 @@ func (rha *RemoteHookAdapter) registerHandler(w http.ResponseWriter, r *http.Req } // Register, and wait for user to acknowledge registration - err = rha.debugger.Register(&state) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - return - } + rha.debugger.Register(&state) // Proceed! w.WriteHeader(http.StatusOK) @@ -78,7 +74,7 @@ func (rha *RemoteHookAdapter) updateHandler(w http.ResponseWriter, r *http.Reque } // Ask debugger to process and wait to continue - err = rha.debugger.Update(&state) + err = rha.debugger.update(&state) if err != nil { w.WriteHeader(http.StatusBadRequest) return @@ -96,7 +92,7 @@ func (rha *RemoteHookAdapter) completeHandler(w http.ResponseWriter, r *http.Req } // Ask debugger to process and wait to continue - err = rha.debugger.Complete(&state) + err = rha.debugger.complete(&state) if err != nil { w.WriteHeader(http.StatusBadRequest) return diff --git a/config/config_test.go b/config/config_test.go index ebddd5a57..c2ee070fc 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -582,6 +582,7 @@ func TestGetNonDefaultConfigValues(t *testing.T) { } func TestLocal_TxFiltering(t *testing.T) { + partitiontest.PartitionTest(t) cfg := GetDefaultLocal() // ensure the default diff --git a/daemon/algod/api/server/v2/dryrun.go b/daemon/algod/api/server/v2/dryrun.go index 84f1fe070..49a5a427d 100644 --- a/daemon/algod/api/server/v2/dryrun.go +++ b/daemon/algod/api/server/v2/dryrun.go @@ -181,24 +181,22 @@ func (ddr *dryrunDebugReceiver) stateToState(state *logic.DebugState) model.Dryr return st } -// Register is fired on program creation (DebuggerHook interface) -func (ddr *dryrunDebugReceiver) Register(state *logic.DebugState) error { +// Register is fired on program creation (logic.Debugger interface) +func (ddr *dryrunDebugReceiver) Register(state *logic.DebugState) { ddr.disassembly = state.Disassembly ddr.lines = strings.Split(state.Disassembly, "\n") - return nil } -// Update is fired on every step (DebuggerHook interface) -func (ddr *dryrunDebugReceiver) Update(state *logic.DebugState) error { +// Update is fired on every step (logic.Debugger interface) +func (ddr *dryrunDebugReceiver) Update(state *logic.DebugState) { st := ddr.stateToState(state) ddr.history = append(ddr.history, st) ddr.updateScratch() - return nil } -// Complete is called when the program exits (DebuggerHook interface) -func (ddr *dryrunDebugReceiver) Complete(state *logic.DebugState) error { - return ddr.Update(state) +// Complete is called when the program exits (logic.Debugger interface) +func (ddr *dryrunDebugReceiver) Complete(state *logic.DebugState) { + ddr.Update(state) } type dryrunLedger struct { @@ -421,7 +419,7 @@ func doDryrunRequest(dr *DryrunRequest, response *model.DryrunResponse) { var result model.DryrunTxnResult if len(stxn.Lsig.Logic) > 0 { var debug dryrunDebugReceiver - ep.Debugger = &debug + ep.Tracer = logic.MakeEvalTracerDebuggerAdaptor(&debug) ep.SigLedger = &dl pass, err := logic.EvalSignature(ti, ep) var messages []string @@ -505,7 +503,7 @@ func doDryrunRequest(dr *DryrunRequest, response *model.DryrunResponse) { messages[0] = fmt.Sprintf("uploaded state did not include app id %d referenced in txn[%d]", appIdx, ti) } else { var debug dryrunDebugReceiver - ep.Debugger = &debug + ep.Tracer = logic.MakeEvalTracerDebuggerAdaptor(&debug) var program []byte messages = make([]string, 1) if stxn.Txn.OnCompletion == transactions.ClearStateOC { diff --git a/daemon/algod/api/server/v2/errors.go b/daemon/algod/api/server/v2/errors.go index 2db2d51ed..947a38fa9 100644 --- a/daemon/algod/api/server/v2/errors.go +++ b/daemon/algod/api/server/v2/errors.go @@ -29,14 +29,12 @@ var ( errFailedRetrievingLatestBlockHeaderStatus = "failed retrieving latests block header" errFailedRetrievingSyncRound = "failed retrieving sync round from ledger" errFailedSettingSyncRound = "failed to set sync round on the ledger" - errSyncModeNotEnabled = "sync mode must be enabled" errFailedParsingFormatOption = "failed to parse the format option" errFailedToParseAddress = "failed to parse the address" errFailedToParseExclude = "failed to parse exclude" errFailedToParseTransaction = "failed to parse transaction" errFailedToParseBlock = "failed to parse block" errFailedToParseCert = "failed to parse cert" - errFailedToParseSourcemap = "failed to parse sourcemap" errFailedToEncodeResponse = "failed to encode response" errInternalFailure = "internal failure" errNoValidTxnSpecified = "no valid transaction ID was specified" diff --git a/data/transactions/logic/assembler.go b/data/transactions/logic/assembler.go index 9e8da08d1..45489918a 100644 --- a/data/transactions/logic/assembler.go +++ b/data/transactions/logic/assembler.go @@ -1907,7 +1907,7 @@ func (ops *OpStream) assemble(text string) error { if ok { ops.trace("%3d: %s\t", ops.sourceLine, opstring) ops.recordSourceLine() - if spec.Modes == modeApp { + if spec.Modes == ModeApp { ops.HasStatefulOps = true } args, returns := spec.Arg.Types, spec.Return.Types @@ -2803,7 +2803,7 @@ func disassembleInstrumented(program []byte, labels map[int]string) (text string return } op := opsByOpcode[version][program[dis.pc]] - if op.Modes == modeApp { + if op.Modes == ModeApp { ds.hasStatefulOps = true } if op.Name == "" { diff --git a/data/transactions/logic/debugger.go b/data/transactions/logic/debugger.go index f4682494b..0a8e013da 100644 --- a/data/transactions/logic/debugger.go +++ b/data/transactions/logic/debugger.go @@ -33,19 +33,80 @@ import ( "github.com/algorand/go-algorand/protocol" ) -// DebuggerHook functions are called by eval function during TEAL program execution -// if provided -type DebuggerHook interface { +// Debugger is an interface that supports the first version of AVM debuggers. +// It consists of a set of functions called by eval function during AVM program execution. +// +// Deprecated: This interface does not support non-app call or inner transactions. Use EvalTracer +// instead. +type Debugger interface { // Register is fired on program creation - Register(state *DebugState) error + Register(state *DebugState) // Update is fired on every step - Update(state *DebugState) error + Update(state *DebugState) // Complete is called when the program exits - Complete(state *DebugState) error + Complete(state *DebugState) +} + +type debuggerEvalTracerAdaptor struct { + NullEvalTracer + + debugger Debugger + txnDepth int + debugState *DebugState +} + +// MakeEvalTracerDebuggerAdaptor creates an adaptor that externally adheres to the EvalTracer +// interface, but drives a Debugger interface +// +// Warning: The output EvalTracer is specifically designed to be invoked under the exact same +// circumstances that the previous Debugger interface was invoked. This means that it will only work +// properly if you attach it directly to a logic.EvalParams and execute a program. If you attempt to +// run this EvalTracer under a different entry point (such as by attaching it to a BlockEvaluator), +// it WILL NOT work properly. +func MakeEvalTracerDebuggerAdaptor(debugger Debugger) EvalTracer { + return &debuggerEvalTracerAdaptor{debugger: debugger} +} + +// BeforeTxnGroup updates inner txn depth +func (a *debuggerEvalTracerAdaptor) BeforeTxnGroup(ep *EvalParams) { + a.txnDepth++ +} + +// AfterTxnGroup updates inner txn depth +func (a *debuggerEvalTracerAdaptor) AfterTxnGroup(ep *EvalParams) { + a.txnDepth-- +} + +// BeforeProgram invokes the debugger's Register hook +func (a *debuggerEvalTracerAdaptor) BeforeProgram(cx *EvalContext) { + if a.txnDepth > 0 { + // only report updates for top-level transactions, for backwards compatibility + return + } + a.debugState = makeDebugState(cx) + a.debugger.Register(a.refreshDebugState(cx, nil)) +} + +// BeforeOpcode invokes the debugger's Update hook +func (a *debuggerEvalTracerAdaptor) BeforeOpcode(cx *EvalContext) { + if a.txnDepth > 0 { + // only report updates for top-level transactions, for backwards compatibility + return + } + a.debugger.Update(a.refreshDebugState(cx, nil)) +} + +// AfterProgram invokes the debugger's Complete hook +func (a *debuggerEvalTracerAdaptor) AfterProgram(cx *EvalContext, evalError error) { + if a.txnDepth > 0 { + // only report updates for top-level transactions, for backwards compatibility + return + } + a.debugger.Complete(a.refreshDebugState(cx, evalError)) } -// WebDebuggerHook represents a connection to tealdbg -type WebDebuggerHook struct { +// WebDebugger represents a connection to tealdbg +type WebDebugger struct { URL string } @@ -115,7 +176,7 @@ func makeDebugState(cx *EvalContext) *DebugState { globals := make([]basics.TealValue, len(globalFieldSpecs)) for _, fs := range globalFieldSpecs { // Don't try to grab app only fields when evaluating a signature - if (cx.runModeFlags&modeSig) != 0 && fs.mode == modeApp { + if (cx.runModeFlags&ModeSig) != 0 && fs.mode == ModeApp { continue } sv, err := cx.globalFieldToValue(fs) @@ -126,7 +187,7 @@ func makeDebugState(cx *EvalContext) *DebugState { } ds.Globals = globals - if (cx.runModeFlags & modeApp) != 0 { + if (cx.runModeFlags & ModeApp) != 0 { ds.EvalDelta = cx.txn.EvalDelta } @@ -221,8 +282,8 @@ func (d *DebugState) parseCallstack(callstack []frame) []CallFrame { return callFrames } -func (cx *EvalContext) refreshDebugState(evalError error) *DebugState { - ds := cx.debugState +func (a *debuggerEvalTracerAdaptor) refreshDebugState(cx *EvalContext, evalError error) *DebugState { + ds := a.debugState // Update pc, line, error, stack, scratch space, callstack, // and opcode budget @@ -247,14 +308,14 @@ func (cx *EvalContext) refreshDebugState(evalError error) *DebugState { ds.OpcodeBudget = cx.remainingBudget() ds.CallStack = ds.parseCallstack(cx.callstack) - if (cx.runModeFlags & modeApp) != 0 { + if (cx.runModeFlags & ModeApp) != 0 { ds.EvalDelta = cx.txn.EvalDelta } return ds } -func (dbg *WebDebuggerHook) postState(state *DebugState, endpoint string) error { +func (dbg *WebDebugger) postState(state *DebugState, endpoint string) error { var body bytes.Buffer enc := protocol.NewJSONEncoder(&body) err := enc.Encode(state) @@ -285,7 +346,7 @@ func (dbg *WebDebuggerHook) postState(state *DebugState, endpoint string) error } // Register sends state to remote debugger -func (dbg *WebDebuggerHook) Register(state *DebugState) error { +func (dbg *WebDebugger) Register(state *DebugState) { u, err := url.Parse(dbg.URL) if err != nil { logging.Base().Errorf("Failed to parse url: %s", err.Error()) @@ -295,15 +356,24 @@ func (dbg *WebDebuggerHook) Register(state *DebugState) error { if h != "localhost" && h != "127.0.0.1" && h != "::1" { logging.Base().Warnf("Unsecured communication with non-local debugger: %s", h) } - return dbg.postState(state, "exec/register") + err = dbg.postState(state, "exec/register") + if err != nil { + logging.Base().Errorf("Failed to post state to exec/register: %s", err.Error()) + } } // Update sends state to remote debugger -func (dbg *WebDebuggerHook) Update(state *DebugState) error { - return dbg.postState(state, "exec/update") +func (dbg *WebDebugger) Update(state *DebugState) { + err := dbg.postState(state, "exec/update") + if err != nil { + logging.Base().Errorf("Failed to post state to exec/update: %s", err.Error()) + } } // Complete sends state to remote debugger -func (dbg *WebDebuggerHook) Complete(state *DebugState) error { - return dbg.postState(state, "exec/complete") +func (dbg *WebDebugger) Complete(state *DebugState) { + err := dbg.postState(state, "exec/complete") + if err != nil { + logging.Base().Errorf("Failed to post state to exec/complete: %s", err.Error()) + } } diff --git a/data/transactions/logic/debugger_test.go b/data/transactions/logic/debugger_test.go index 413bc7ea6..26bd940b7 100644 --- a/data/transactions/logic/debugger_test.go +++ b/data/transactions/logic/debugger_test.go @@ -26,7 +26,7 @@ import ( "github.com/stretchr/testify/require" ) -var testProgram string = `intcblock 0 1 1 1 1 5 100 +const debuggerTestProgram string = `intcblock 0 1 1 1 1 5 100 bytecblock 0x414c474f 0x1337 0x2001 0xdeadbeef 0x70077007 bytec 0 sha256 @@ -63,9 +63,8 @@ bytec 4 && ` -func TestWebDebuggerManual(t *testing.T) { +func TestWebDebuggerManual(t *testing.T) { //nolint:paralleltest // Manual test partitiontest.PartitionTest(t) - t.Parallel() debugURL := os.Getenv("TEAL_DEBUGGER_URL") if len(debugURL) == 0 { @@ -81,48 +80,79 @@ func TestWebDebuggerManual(t *testing.T) { tx.SelectionPK[:], tx.Note, } - ep.Debugger = &WebDebuggerHook{URL: debugURL} - testLogic(t, testProgram, AssemblerMaxVersion, ep) + ep.Tracer = MakeEvalTracerDebuggerAdaptor(&WebDebugger{URL: debugURL}) + testLogic(t, debuggerTestProgram, AssemblerMaxVersion, ep) } -type testDbgHook struct { +type testDebugger struct { register int update int complete int state *DebugState } -func (d *testDbgHook) Register(state *DebugState) error { +func (d *testDebugger) Register(state *DebugState) { d.register++ d.state = state - return nil } -func (d *testDbgHook) Update(state *DebugState) error { +func (d *testDebugger) Update(state *DebugState) { d.update++ d.state = state - return nil } -func (d *testDbgHook) Complete(state *DebugState) error { +func (d *testDebugger) Complete(state *DebugState) { d.complete++ d.state = state - return nil } -func TestDebuggerHook(t *testing.T) { +func TestDebuggerProgramEval(t *testing.T) { partitiontest.PartitionTest(t) t.Parallel() - testDbg := testDbgHook{} - ep := defaultEvalParams() - ep.Debugger = &testDbg - testLogic(t, testProgram, AssemblerMaxVersion, ep) - - require.Equal(t, 1, testDbg.register) - require.Equal(t, 1, testDbg.complete) - require.Greater(t, testDbg.update, 1) - require.Len(t, testDbg.state.Stack, 1) + t.Run("logicsig", func(t *testing.T) { + t.Parallel() + testDbg := testDebugger{} + ep := defaultEvalParams() + ep.Tracer = MakeEvalTracerDebuggerAdaptor(&testDbg) + testLogic(t, debuggerTestProgram, AssemblerMaxVersion, ep) + + require.Equal(t, 1, testDbg.register) + require.Equal(t, 1, testDbg.complete) + require.Equal(t, 35, testDbg.update) + require.Len(t, testDbg.state.Stack, 1) + }) + + t.Run("simple app", func(t *testing.T) { + t.Parallel() + testDbg := testDebugger{} + ep := defaultEvalParams() + ep.Tracer = MakeEvalTracerDebuggerAdaptor(&testDbg) + testApp(t, debuggerTestProgram, ep) + + require.Equal(t, 1, testDbg.register) + require.Equal(t, 1, testDbg.complete) + require.Equal(t, 35, testDbg.update) + require.Len(t, testDbg.state.Stack, 1) + }) + + t.Run("app with inner txns", func(t *testing.T) { + t.Parallel() + testDbg := testDebugger{} + ep, tx, ledger := MakeSampleEnv() + + // Establish 888 as the app id, and fund it. + ledger.NewApp(tx.Receiver, 888, basics.AppParams{}) + ledger.NewAccount(basics.AppIndex(888).Address(), 200000) + + ep.Tracer = MakeEvalTracerDebuggerAdaptor(&testDbg) + testApp(t, innerTxnTestProgram, ep) + + require.Equal(t, 1, testDbg.register) + require.Equal(t, 1, testDbg.complete) + require.Equal(t, 27, testDbg.update) + require.Len(t, testDbg.state.Stack, 1) + }) } func TestLineToPC(t *testing.T) { @@ -228,9 +258,9 @@ func TestCallStackUpdate(t *testing.T) { }, } - testDbg := testDbgHook{} + testDbg := testDebugger{} ep := defaultEvalParams() - ep.Debugger = &testDbg + ep.Tracer = MakeEvalTracerDebuggerAdaptor(&testDbg) testLogic(t, testCallStackProgram, AssemblerMaxVersion, ep) require.Equal(t, 1, testDbg.register) diff --git a/data/transactions/logic/eval.go b/data/transactions/logic/eval.go index f7bb12048..158ca6e08 100644 --- a/data/transactions/logic/eval.go +++ b/data/transactions/logic/eval.go @@ -282,8 +282,8 @@ type EvalParams struct { SigLedger LedgerForSignature Ledger LedgerForLogic - // optional debugger - Debugger DebuggerHook + // optional tracer + Tracer EvalTracer // MinAvmVersion is the minimum allowed AVM version of this program. // The program must reject if its version is less than this version. If @@ -455,7 +455,7 @@ func NewInnerEvalParams(txg []transactions.SignedTxnWithAD, caller *EvalContext) logger: caller.logger, SigLedger: caller.SigLedger, Ledger: caller.Ledger, - Debugger: nil, // See #4438, where this becomes caller.Debugger + Tracer: caller.Tracer, MinAvmVersion: &minAvmVersion, FeeCredit: caller.FeeCredit, Specials: caller.Specials, @@ -474,28 +474,31 @@ func NewInnerEvalParams(txg []transactions.SignedTxnWithAD, caller *EvalContext) type evalFunc func(cx *EvalContext) error type checkFunc func(cx *EvalContext) error -type runMode uint64 +// RunMode is a bitset of logic evaluation modes. +// There are currently two such modes: Signature and Application. +type RunMode uint64 const ( - // modeSig is LogicSig execution - modeSig runMode = 1 << iota + // ModeSig is LogicSig execution + ModeSig RunMode = 1 << iota - // modeApp is application/contract execution - modeApp + // ModeApp is application/contract execution + ModeApp // local constant, run in any mode - modeAny = modeSig | modeApp + modeAny = ModeSig | ModeApp ) -func (r runMode) Any() bool { +// Any checks if this mode bitset represents any evaluation mode +func (r RunMode) Any() bool { return r == modeAny } -func (r runMode) String() string { +func (r RunMode) String() string { switch r { - case modeSig: + case ModeSig: return "Signature" - case modeApp: + case ModeApp: return "Application" case modeAny: return "Any" @@ -547,7 +550,7 @@ type EvalContext struct { *EvalParams // determines eval mode: runModeSignature or runModeApplication - runModeFlags runMode + runModeFlags RunMode // the index of the transaction being evaluated groupIndex int @@ -591,9 +594,11 @@ type EvalContext struct { instructionStarts []bool programHashCached crypto.Digest +} - // Stores state & disassembly for the optional debugger - debugState *DebugState +// RunMode returns the evaluation context's mode (signature or application) +func (cx *EvalContext) RunMode() RunMode { + return cx.runModeFlags } // StackType describes the type of a value on the operand stack @@ -700,7 +705,7 @@ func EvalContract(program []byte, gi int, aid basics.AppIndex, params *EvalParam } cx := EvalContext{ EvalParams: params, - runModeFlags: modeApp, + runModeFlags: ModeApp, groupIndex: gi, txn: ¶ms.TxnGroup[gi], appID: aid, @@ -787,7 +792,7 @@ func EvalSignatureFull(gi int, params *EvalParams) (pass bool, pcx *EvalContext, } cx := EvalContext{ EvalParams: params, - runModeFlags: modeSig, + runModeFlags: ModeSig, groupIndex: gi, txn: ¶ms.TxnGroup[gi], } @@ -834,17 +839,11 @@ func eval(program []byte, cx *EvalContext) (pass bool, err error) { cx.txn.EvalDelta.GlobalDelta = basics.StateDelta{} cx.txn.EvalDelta.LocalDeltas = make(map[uint64]basics.StateDelta) - if cx.Debugger != nil { - cx.debugState = makeDebugState(cx) - if derr := cx.Debugger.Register(cx.refreshDebugState(err)); derr != nil { - return false, derr - } + if cx.Tracer != nil { + cx.Tracer.BeforeProgram(cx) defer func() { - // Ensure we update the debugger before exiting - errDbg := cx.Debugger.Complete(cx.refreshDebugState(err)) - if err == nil { - err = errDbg - } + // Ensure we update the tracer before exiting + cx.Tracer.AfterProgram(cx, err) }() } @@ -859,13 +858,15 @@ func eval(program []byte, cx *EvalContext) (pass bool, err error) { } for (err == nil) && (cx.pc < len(cx.program)) { - if cx.Debugger != nil { - if derr := cx.Debugger.Update(cx.refreshDebugState(err)); derr != nil { - return false, derr - } + if cx.Tracer != nil { + cx.Tracer.BeforeOpcode(cx) } err = cx.step() + + if cx.Tracer != nil { + cx.Tracer.AfterOpcode(cx, err) + } } if err != nil { if cx.Trace != nil { @@ -895,17 +896,17 @@ func eval(program []byte, cx *EvalContext) (pass bool, err error) { // these static checks include a cost estimate that must be low enough // (controlled by params.Proto). func CheckContract(program []byte, params *EvalParams) error { - return check(program, params, modeApp) + return check(program, params, ModeApp) } // CheckSignature should be faster than EvalSignature. It can perform static // checks and reject programs that are invalid. Prior to v4, these static checks // include a cost estimate that must be low enough (controlled by params.Proto). func CheckSignature(gi int, params *EvalParams) error { - return check(params.TxnGroup[gi].Lsig.Logic, params, modeSig) + return check(params.TxnGroup[gi].Lsig.Logic, params, ModeSig) } -func check(program []byte, params *EvalParams, mode runMode) (err error) { +func check(program []byte, params *EvalParams, mode RunMode) (err error) { defer func() { if x := recover(); x != nil { buf := make([]byte, 16*1024) @@ -1012,7 +1013,7 @@ func (cx *EvalContext) Cost() int { } func (cx *EvalContext) remainingBudget() int { - if cx.runModeFlags == modeSig { + if cx.runModeFlags == ModeSig { return int(cx.Proto.LogicSigMaxCost) - cx.cost } @@ -1842,7 +1843,14 @@ func opBytesLt(cx *EvalContext) error { rhs := nonzero(cx.stack[last].Bytes) lhs := nonzero(cx.stack[prev].Bytes) - cx.stack[prev] = boolToSV(len(lhs) < len(rhs) || bytes.Compare(lhs, rhs) < 0) + switch { + case len(lhs) < len(rhs): + cx.stack[prev] = boolToSV(true) + case len(lhs) > len(rhs): + cx.stack[prev] = boolToSV(false) + default: + cx.stack[prev] = boolToSV(bytes.Compare(lhs, rhs) < 0) + } cx.stack = cx.stack[:last] return nil @@ -2614,7 +2622,7 @@ func (cx *EvalContext) getTxID(txn *transactions.Transaction, groupIndex int, in func (cx *EvalContext) txnFieldToStack(stxn *transactions.SignedTxnWithAD, fs *txnFieldSpec, arrayFieldIdx uint64, groupIndex int, inner bool) (sv stackValue, err error) { if fs.effects { - if cx.runModeFlags == modeSig { + if cx.runModeFlags == ModeSig { return sv, fmt.Errorf("txn[%s] not allowed in current mode", fs.field) } if cx.version < txnEffectsVersion && !inner { @@ -2882,7 +2890,7 @@ func (cx *EvalContext) opTxnImpl(gi uint64, src txnSource, field TxnField, ai ui case srcGroup: if fs.effects && gi >= uint64(cx.groupIndex) { // Test mode so that error is clearer - if cx.runModeFlags == modeSig { + if cx.runModeFlags == ModeSig { return sv, fmt.Errorf("txn[%s] not allowed in current mode", fs.field) } return sv, fmt.Errorf("txn effects can only be read from past txns %d %d", gi, cx.groupIndex) @@ -5166,7 +5174,16 @@ func opItxnSubmit(cx *EvalContext) error { } ep := NewInnerEvalParams(cx.subtxns, cx) + + if ep.Tracer != nil { + ep.Tracer.BeforeTxnGroup(ep) + } + for i := range ep.TxnGroup { + if ep.Tracer != nil { + ep.Tracer.BeforeTxn(ep, i) + } + err := cx.Ledger.Perform(i, ep) if err != nil { return err @@ -5174,11 +5191,20 @@ func opItxnSubmit(cx *EvalContext) error { // This is mostly a no-op, because Perform does its work "in-place", but // RecordAD has some further responsibilities. ep.RecordAD(i, ep.TxnGroup[i].ApplyData) + + if ep.Tracer != nil { + ep.Tracer.AfterTxn(ep, i, ep.TxnGroup[i].ApplyData) + } } cx.txn.EvalDelta.InnerTxns = append(cx.txn.EvalDelta.InnerTxns, ep.TxnGroup...) cx.subtxns = nil // must clear the inner txid cache, otherwise prior inner txids will be returned for this group cx.innerTxidCache = nil + + if ep.Tracer != nil { + ep.Tracer.AfterTxnGroup(ep) + } + return nil } diff --git a/data/transactions/logic/evalStateful_test.go b/data/transactions/logic/evalStateful_test.go index 3b755e899..a2f33cc90 100644 --- a/data/transactions/logic/evalStateful_test.go +++ b/data/transactions/logic/evalStateful_test.go @@ -197,9 +197,9 @@ pop bytec_0 log ` - tests := map[runMode]string{ - modeSig: opcodesRunModeAny + opcodesRunModeSignature, - modeApp: opcodesRunModeAny + opcodesRunModeApplication, + tests := map[RunMode]string{ + ModeSig: opcodesRunModeAny + opcodesRunModeSignature, + ModeApp: opcodesRunModeAny + opcodesRunModeApplication, } for mode, test := range tests { @@ -237,7 +237,7 @@ log ledger.NewLocal(tx.Sender, 100, "ALGO", algoValue) ledger.NewAsset(tx.Sender, 5, params) - if mode == modeSig { + if mode == ModeSig { testLogic(t, test, AssemblerMaxVersion, ep) } else { testApp(t, test, ep) @@ -303,9 +303,9 @@ log "not allowed in current mode", "not allowed in current mode") } - require.Equal(t, runMode(1), modeSig) - require.Equal(t, runMode(2), modeApp) - require.True(t, modeAny == modeSig|modeApp) + require.Equal(t, RunMode(1), ModeSig) + require.Equal(t, RunMode(2), ModeApp) + require.True(t, modeAny == ModeSig|ModeApp) require.True(t, modeAny.Any()) } @@ -2442,7 +2442,7 @@ func TestReturnTypes(t *testing.T) { } byName := OpsByName[LogicVersion] - for _, m := range []runMode{modeSig, modeApp} { + for _, m := range []RunMode{ModeSig, ModeApp} { for name, spec := range byName { // Only try an opcode in its modes if (m & spec.Modes) == 0 { diff --git a/data/transactions/logic/eval_test.go b/data/transactions/logic/eval_test.go index f63eae7da..6731c1982 100644 --- a/data/transactions/logic/eval_test.go +++ b/data/transactions/logic/eval_test.go @@ -2753,7 +2753,7 @@ func TestGload(t *testing.T) { // for more complex group transaction cases type failureCase struct { firstTxn transactions.SignedTxn - runMode runMode + runMode RunMode errContains string } @@ -2763,7 +2763,7 @@ func TestGload(t *testing.T) { Type: protocol.PaymentTx, }, }, - runMode: modeApp, + runMode: ModeApp, errContains: "can't use gload on non-app call txn with index 0", } @@ -2773,7 +2773,7 @@ func TestGload(t *testing.T) { Type: protocol.ApplicationCallTx, }, }, - runMode: modeSig, + runMode: ModeSig, errContains: "gload not allowed in current mode", } @@ -2794,7 +2794,7 @@ func TestGload(t *testing.T) { program := testProg(t, "gload 0 0", AssemblerMaxVersion).Program switch failCase.runMode { - case modeApp: + case ModeApp: testAppBytes(t, program, ep, failCase.errContains) default: testLogicBytes(t, program, ep, failCase.errContains, failCase.errContains) @@ -4918,6 +4918,9 @@ func TestBytesCompare(t *testing.T) { testPanics(t, "byte 0x10; int 65; bzero; b>", 4) testAccepts(t, "byte 0x1010; byte 0x10; b<; !", 4) + testAccepts(t, "byte 0x2000; byte 0x70; b<; !", 4) + testAccepts(t, "byte 0x7000; byte 0x20; b<; !", 4) + // All zero input are interesting, because they lead to bytes.Compare being // called with nils. Show that is correct. testAccepts(t, "byte 0x10; byte 0x00; b<; !", 4) @@ -5038,7 +5041,7 @@ func TestLog(t *testing.T) { msg := strings.Repeat("a", 400) failCases := []struct { source string - runMode runMode + runMode RunMode errContains string // For cases where assembly errors, we manually put in the bytes assembledBytes []byte @@ -5046,44 +5049,44 @@ func TestLog(t *testing.T) { { source: fmt.Sprintf(`byte "%s"; log; int 1`, strings.Repeat("a", maxLogSize+1)), errContains: fmt.Sprintf("> %d bytes limit", maxLogSize), - runMode: modeApp, + runMode: ModeApp, }, { source: fmt.Sprintf(`byte "%s"; log; byte "%s"; log; byte "%s"; log; int 1`, msg, msg, msg), errContains: fmt.Sprintf("> %d bytes limit", maxLogSize), - runMode: modeApp, + runMode: ModeApp, }, { source: fmt.Sprintf(`%s; int 1`, strings.Repeat(`byte "a"; log; `, maxLogCalls+1)), errContains: "too many log calls", - runMode: modeApp, + runMode: ModeApp, }, { source: `int 1; loop: byte "a"; log; int 1; +; dup; int 35; <; bnz loop;`, errContains: "too many log calls", - runMode: modeApp, + runMode: ModeApp, }, { source: fmt.Sprintf(`int 1; loop: byte "%s"; log; int 1; +; dup; int 6; <; bnz loop;`, strings.Repeat(`a`, 400)), errContains: fmt.Sprintf("> %d bytes limit", maxLogSize), - runMode: modeApp, + runMode: ModeApp, }, { source: `load 0; log`, errContains: "log arg 0 wanted []byte but got uint64", - runMode: modeApp, + runMode: ModeApp, assembledBytes: []byte{byte(ep.Proto.LogicSigVersion), 0x34, 0x00, 0xb0}, }, { source: `byte "a logging message"; log; int 1`, errContains: "log not allowed in current mode", - runMode: modeSig, + runMode: ModeSig, }, } for _, c := range failCases { switch c.runMode { - case modeApp: + case ModeApp: if c.assembledBytes == nil { testApp(t, c.source, ep, c.errContains) } else { diff --git a/data/transactions/logic/fields.go b/data/transactions/logic/fields.go index dc9678b3d..0af2079a3 100644 --- a/data/transactions/logic/fields.go +++ b/data/transactions/logic/fields.go @@ -537,7 +537,7 @@ var GlobalFieldNames [invalidGlobalField]string type globalFieldSpec struct { field GlobalField ftype StackType - mode runMode + mode RunMode version uint64 doc string } @@ -556,7 +556,7 @@ func (fs globalFieldSpec) Version() uint64 { } func (fs globalFieldSpec) Note() string { note := fs.doc - if fs.mode == modeApp { + if fs.mode == ModeApp { note = addExtra(note, "Application mode only.") } // There are no Signature mode only globals @@ -572,21 +572,21 @@ var globalFieldSpecs = [...]globalFieldSpec{ {GroupSize, StackUint64, modeAny, 0, "Number of transactions in this atomic transaction group. At least 1"}, {LogicSigVersion, StackUint64, modeAny, 2, "Maximum supported version"}, - {Round, StackUint64, modeApp, 2, "Current round number"}, - {LatestTimestamp, StackUint64, modeApp, 2, + {Round, StackUint64, ModeApp, 2, "Current round number"}, + {LatestTimestamp, StackUint64, ModeApp, 2, "Last confirmed block UNIX timestamp. Fails if negative"}, - {CurrentApplicationID, StackUint64, modeApp, 2, "ID of current application executing"}, - {CreatorAddress, StackBytes, modeApp, 3, + {CurrentApplicationID, StackUint64, ModeApp, 2, "ID of current application executing"}, + {CreatorAddress, StackBytes, ModeApp, 3, "Address of the creator of the current application"}, - {CurrentApplicationAddress, StackBytes, modeApp, 5, + {CurrentApplicationAddress, StackBytes, ModeApp, 5, "Address that the current application controls"}, {GroupID, StackBytes, modeAny, 5, "ID of the transaction group. 32 zero bytes if the transaction is not part of a group."}, {OpcodeBudget, StackUint64, modeAny, 6, "The remaining cost that can be spent by opcodes in this program."}, - {CallerApplicationID, StackUint64, modeApp, 6, + {CallerApplicationID, StackUint64, ModeApp, 6, "The application ID of the application that called this application. 0 if this application is at the top-level."}, - {CallerApplicationAddress, StackBytes, modeApp, 6, + {CallerApplicationAddress, StackBytes, ModeApp, 6, "The application address of the application that called this application. ZeroAddress if this application is at the top-level."}, } diff --git a/data/transactions/logic/mocktracer/tracer.go b/data/transactions/logic/mocktracer/tracer.go new file mode 100644 index 000000000..967798ec9 --- /dev/null +++ b/data/transactions/logic/mocktracer/tracer.go @@ -0,0 +1,147 @@ +// Copyright (C) 2019-2023 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see <https://www.gnu.org/licenses/>. + +package mocktracer + +import ( + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/protocol" +) + +// EventType represents a type of logic.EvalTracer event +type EventType string + +const ( + // BeforeTxnGroupEvent represents the logic.EvalTracer.BeforeTxnGroup event + BeforeTxnGroupEvent EventType = "BeforeTxnGroup" + // AfterTxnGroupEvent represents the logic.EvalTracer.AfterTxnGroup event + AfterTxnGroupEvent EventType = "AfterTxnGroup" + // BeforeTxnEvent represents the logic.EvalTracer.BeforeTxn event + BeforeTxnEvent EventType = "BeforeTxn" + // AfterTxnEvent represents the logic.EvalTracer.AfterTxn event + AfterTxnEvent EventType = "AfterTxn" + // BeforeProgramEvent represents the logic.EvalTracer.BeforeProgram event + BeforeProgramEvent EventType = "BeforeProgram" + // AfterProgramEvent represents the logic.EvalTracer.AfterProgram event + AfterProgramEvent EventType = "AfterProgram" + // BeforeOpcodeEvent represents the logic.EvalTracer.BeforeOpcode event + BeforeOpcodeEvent EventType = "BeforeOpcode" + // AfterOpcodeEvent represents the logic.EvalTracer.AfterOpcode event + AfterOpcodeEvent EventType = "AfterOpcode" +) + +// Event represents a logic.EvalTracer event +type Event struct { + Type EventType + + // only for BeforeProgram and AfterProgram + LogicEvalMode logic.RunMode + + // only for BeforeTxn and AfterTxn + TxnType protocol.TxType + + // only for AfterTxn + TxnApplyData transactions.ApplyData + + // only for BeforeTxnGroup and AfterTxnGroup + GroupSize int +} + +// BeforeTxnGroup creates a new Event with the type BeforeTxnGroupEvent +func BeforeTxnGroup(groupSize int) Event { + return Event{Type: BeforeTxnGroupEvent, GroupSize: groupSize} +} + +// AfterTxnGroup creates a new Event with the type AfterTxnGroupEvent +func AfterTxnGroup(groupSize int) Event { + return Event{Type: AfterTxnGroupEvent, GroupSize: groupSize} +} + +// BeforeProgram creates a new Event with the type BeforeProgramEvent +func BeforeProgram(mode logic.RunMode) Event { + return Event{Type: BeforeProgramEvent, LogicEvalMode: mode} +} + +// BeforeTxn creates a new Event with the type BeforeTxnEvent +func BeforeTxn(txnType protocol.TxType) Event { + return Event{Type: BeforeTxnEvent, TxnType: txnType} +} + +// AfterTxn creates a new Event with the type AfterTxnEvent +func AfterTxn(txnType protocol.TxType, ad transactions.ApplyData) Event { + return Event{Type: AfterTxnEvent, TxnType: txnType, TxnApplyData: ad} +} + +// AfterProgram creates a new Event with the type AfterProgramEvent +func AfterProgram(mode logic.RunMode) Event { + return Event{Type: AfterProgramEvent, LogicEvalMode: mode} +} + +// BeforeOpcode creates a new Event with the type BeforeOpcodeEvent +func BeforeOpcode() Event { + return Event{Type: BeforeOpcodeEvent} +} + +// AfterOpcode creates a new Event with the type AfterOpcodeEvent +func AfterOpcode() Event { + return Event{Type: AfterOpcodeEvent} +} + +// Tracer is a mock tracer that implements logic.EvalTracer +type Tracer struct { + Events []Event +} + +// BeforeTxnGroup mocks the logic.EvalTracer.BeforeTxnGroup method +func (d *Tracer) BeforeTxnGroup(ep *logic.EvalParams) { + d.Events = append(d.Events, BeforeTxnGroup(len(ep.TxnGroup))) +} + +// AfterTxnGroup mocks the logic.EvalTracer.AfterTxnGroup method +func (d *Tracer) AfterTxnGroup(ep *logic.EvalParams) { + d.Events = append(d.Events, AfterTxnGroup(len(ep.TxnGroup))) +} + +// BeforeTxn mocks the logic.EvalTracer.BeforeTxn method +func (d *Tracer) BeforeTxn(ep *logic.EvalParams, groupIndex int) { + d.Events = append(d.Events, BeforeTxn(ep.TxnGroup[groupIndex].Txn.Type)) +} + +// AfterTxn mocks the logic.EvalTracer.AfterTxn method +func (d *Tracer) AfterTxn(ep *logic.EvalParams, groupIndex int, ad transactions.ApplyData) { + d.Events = append(d.Events, AfterTxn(ep.TxnGroup[groupIndex].Txn.Type, ad)) +} + +// BeforeProgram mocks the logic.EvalTracer.BeforeProgram method +func (d *Tracer) BeforeProgram(cx *logic.EvalContext) { + d.Events = append(d.Events, BeforeProgram(cx.RunMode())) +} + +// AfterProgram mocks the logic.EvalTracer.AfterProgram method +func (d *Tracer) AfterProgram(cx *logic.EvalContext, evalError error) { + d.Events = append(d.Events, AfterProgram(cx.RunMode())) +} + +// BeforeOpcode mocks the logic.EvalTracer.BeforeOpcode method +func (d *Tracer) BeforeOpcode(cx *logic.EvalContext) { + d.Events = append(d.Events, BeforeOpcode()) +} + +// AfterOpcode mocks the logic.EvalTracer.AfterOpcode method +func (d *Tracer) AfterOpcode(cx *logic.EvalContext, evalError error) { + d.Events = append(d.Events, AfterOpcode()) +} diff --git a/data/transactions/logic/opcodes.go b/data/transactions/logic/opcodes.go index ffce9cf3e..8f47b57ca 100644 --- a/data/transactions/logic/opcodes.go +++ b/data/transactions/logic/opcodes.go @@ -121,7 +121,7 @@ type OpDetails struct { check checkFunc // static check bytecode (and determine size) refine refineFunc // refine arg/return types based on ProgramKnowledge at assembly time - Modes runMode // all modes that opcode can run in. i.e (cx.mode & Modes) != 0 allows + Modes RunMode // all modes that opcode can run in. i.e (cx.mode & Modes) != 0 allows FullCost linearCost // if non-zero, the cost of the opcode, no immediates matter Size int // if non-zero, the known size of opcode. if 0, check() determines. @@ -221,13 +221,13 @@ func (d OpDetails) costs(cost int) OpDetails { return d } -func only(m runMode) OpDetails { +func only(m RunMode) OpDetails { d := detDefault() d.Modes = m return d } -func (d OpDetails) only(m runMode) OpDetails { +func (d OpDetails) only(m RunMode) OpDetails { d.Modes = m return d } @@ -419,7 +419,7 @@ var OpSpecs = []OpSpec{ {0x03, "sha512_256", opSHA512_256, proto("b:b"), 7, unlimitedStorage, costByLength(17, 5, 8)}, */ - {0x04, "ed25519verify", opEd25519Verify, proto("bbb:i"), 1, costly(1900).only(modeSig)}, + {0x04, "ed25519verify", opEd25519Verify, proto("bbb:i"), 1, costly(1900).only(ModeSig)}, {0x04, "ed25519verify", opEd25519Verify, proto("bbb:i"), 5, costly(1900)}, {0x05, "ecdsa_verify", opEcdsaVerify, proto("bbbbb:i"), 5, costByField("v", &EcdsaCurves, ecdsaVerifyCosts)}, @@ -463,11 +463,11 @@ var OpSpecs = []OpSpec{ {0x29, "bytec_1", opByteConst1, proto(":b"), 1, detDefault()}, {0x2a, "bytec_2", opByteConst2, proto(":b"), 1, detDefault()}, {0x2b, "bytec_3", opByteConst3, proto(":b"), 1, detDefault()}, - {0x2c, "arg", opArg, proto(":b"), 1, immediates("n").only(modeSig).assembler(asmArg)}, - {0x2d, "arg_0", opArg0, proto(":b"), 1, only(modeSig)}, - {0x2e, "arg_1", opArg1, proto(":b"), 1, only(modeSig)}, - {0x2f, "arg_2", opArg2, proto(":b"), 1, only(modeSig)}, - {0x30, "arg_3", opArg3, proto(":b"), 1, only(modeSig)}, + {0x2c, "arg", opArg, proto(":b"), 1, immediates("n").only(ModeSig).assembler(asmArg)}, + {0x2d, "arg_0", opArg0, proto(":b"), 1, only(ModeSig)}, + {0x2e, "arg_1", opArg1, proto(":b"), 1, only(ModeSig)}, + {0x2f, "arg_2", opArg2, proto(":b"), 1, only(ModeSig)}, + {0x30, "arg_3", opArg3, proto(":b"), 1, only(ModeSig)}, // txn, gtxn, and gtxns are also implemented as pseudoOps to choose // between scalar and array version based on number of immediates. {0x31, "txn", opTxn, proto(":a"), 1, field("f", &TxnScalarFields)}, @@ -481,11 +481,11 @@ var OpSpecs = []OpSpec{ {0x38, "gtxns", opGtxns, proto("i:a"), 3, immediates("f").field("f", &TxnScalarFields)}, {0x39, "gtxnsa", opGtxnsa, proto("i:a"), 3, immediates("f", "i").field("f", &TxnArrayFields)}, // Group scratch space access - {0x3a, "gload", opGload, proto(":a"), 4, immediates("t", "i").only(modeApp)}, - {0x3b, "gloads", opGloads, proto("i:a"), 4, immediates("i").only(modeApp)}, + {0x3a, "gload", opGload, proto(":a"), 4, immediates("t", "i").only(ModeApp)}, + {0x3b, "gloads", opGloads, proto("i:a"), 4, immediates("i").only(ModeApp)}, // Access creatable IDs (consider deprecating, as txn CreatedAssetID, CreatedApplicationID should be enough - {0x3c, "gaid", opGaid, proto(":i"), 4, immediates("t").only(modeApp)}, - {0x3d, "gaids", opGaids, proto("i:i"), 4, only(modeApp)}, + {0x3c, "gaid", opGaid, proto(":i"), 4, immediates("t").only(ModeApp)}, + {0x3d, "gaids", opGaids, proto("i:i"), 4, only(ModeApp)}, // Like load/store, but scratch slot taken from TOS instead of immediate {0x3e, "loads", opLoads, proto("i:a"), 5, typed(typeLoads)}, @@ -526,31 +526,31 @@ var OpSpecs = []OpSpec{ {0x5e, "base64_decode", opBase64Decode, proto("b:b"), fidoVersion, field("e", &Base64Encodings).costByLength(1, 1, 16, 0)}, {0x5f, "json_ref", opJSONRef, proto("bb:a"), fidoVersion, field("r", &JSONRefTypes).costByLength(25, 2, 7, 1)}, - {0x60, "balance", opBalance, proto("i:i"), 2, only(modeApp)}, - {0x60, "balance", opBalance, proto("a:i"), directRefEnabledVersion, only(modeApp)}, - {0x61, "app_opted_in", opAppOptedIn, proto("ii:i"), 2, only(modeApp)}, - {0x61, "app_opted_in", opAppOptedIn, proto("ai:i"), directRefEnabledVersion, only(modeApp)}, - {0x62, "app_local_get", opAppLocalGet, proto("ib:a"), 2, only(modeApp)}, - {0x62, "app_local_get", opAppLocalGet, proto("ab:a"), directRefEnabledVersion, only(modeApp)}, - {0x63, "app_local_get_ex", opAppLocalGetEx, proto("iib:ai"), 2, only(modeApp)}, - {0x63, "app_local_get_ex", opAppLocalGetEx, proto("aib:ai"), directRefEnabledVersion, only(modeApp)}, - {0x64, "app_global_get", opAppGlobalGet, proto("b:a"), 2, only(modeApp)}, - {0x65, "app_global_get_ex", opAppGlobalGetEx, proto("ib:ai"), 2, only(modeApp)}, - {0x66, "app_local_put", opAppLocalPut, proto("iba:"), 2, only(modeApp)}, - {0x66, "app_local_put", opAppLocalPut, proto("aba:"), directRefEnabledVersion, only(modeApp)}, - {0x67, "app_global_put", opAppGlobalPut, proto("ba:"), 2, only(modeApp)}, - {0x68, "app_local_del", opAppLocalDel, proto("ib:"), 2, only(modeApp)}, - {0x68, "app_local_del", opAppLocalDel, proto("ab:"), directRefEnabledVersion, only(modeApp)}, - {0x69, "app_global_del", opAppGlobalDel, proto("b:"), 2, only(modeApp)}, - - {0x70, "asset_holding_get", opAssetHoldingGet, proto("ii:ai"), 2, field("f", &AssetHoldingFields).only(modeApp)}, - {0x70, "asset_holding_get", opAssetHoldingGet, proto("ai:ai"), directRefEnabledVersion, field("f", &AssetHoldingFields).only(modeApp)}, - {0x71, "asset_params_get", opAssetParamsGet, proto("i:ai"), 2, field("f", &AssetParamsFields).only(modeApp)}, - {0x72, "app_params_get", opAppParamsGet, proto("i:ai"), 5, field("f", &AppParamsFields).only(modeApp)}, - {0x73, "acct_params_get", opAcctParamsGet, proto("a:ai"), 6, field("f", &AcctParamsFields).only(modeApp)}, - - {0x78, "min_balance", opMinBalance, proto("i:i"), 3, only(modeApp)}, - {0x78, "min_balance", opMinBalance, proto("a:i"), directRefEnabledVersion, only(modeApp)}, + {0x60, "balance", opBalance, proto("i:i"), 2, only(ModeApp)}, + {0x60, "balance", opBalance, proto("a:i"), directRefEnabledVersion, only(ModeApp)}, + {0x61, "app_opted_in", opAppOptedIn, proto("ii:i"), 2, only(ModeApp)}, + {0x61, "app_opted_in", opAppOptedIn, proto("ai:i"), directRefEnabledVersion, only(ModeApp)}, + {0x62, "app_local_get", opAppLocalGet, proto("ib:a"), 2, only(ModeApp)}, + {0x62, "app_local_get", opAppLocalGet, proto("ab:a"), directRefEnabledVersion, only(ModeApp)}, + {0x63, "app_local_get_ex", opAppLocalGetEx, proto("iib:ai"), 2, only(ModeApp)}, + {0x63, "app_local_get_ex", opAppLocalGetEx, proto("aib:ai"), directRefEnabledVersion, only(ModeApp)}, + {0x64, "app_global_get", opAppGlobalGet, proto("b:a"), 2, only(ModeApp)}, + {0x65, "app_global_get_ex", opAppGlobalGetEx, proto("ib:ai"), 2, only(ModeApp)}, + {0x66, "app_local_put", opAppLocalPut, proto("iba:"), 2, only(ModeApp)}, + {0x66, "app_local_put", opAppLocalPut, proto("aba:"), directRefEnabledVersion, only(ModeApp)}, + {0x67, "app_global_put", opAppGlobalPut, proto("ba:"), 2, only(ModeApp)}, + {0x68, "app_local_del", opAppLocalDel, proto("ib:"), 2, only(ModeApp)}, + {0x68, "app_local_del", opAppLocalDel, proto("ab:"), directRefEnabledVersion, only(ModeApp)}, + {0x69, "app_global_del", opAppGlobalDel, proto("b:"), 2, only(ModeApp)}, + + {0x70, "asset_holding_get", opAssetHoldingGet, proto("ii:ai"), 2, field("f", &AssetHoldingFields).only(ModeApp)}, + {0x70, "asset_holding_get", opAssetHoldingGet, proto("ai:ai"), directRefEnabledVersion, field("f", &AssetHoldingFields).only(ModeApp)}, + {0x71, "asset_params_get", opAssetParamsGet, proto("i:ai"), 2, field("f", &AssetParamsFields).only(ModeApp)}, + {0x72, "app_params_get", opAppParamsGet, proto("i:ai"), 5, field("f", &AppParamsFields).only(ModeApp)}, + {0x73, "acct_params_get", opAcctParamsGet, proto("a:ai"), 6, field("f", &AcctParamsFields).only(ModeApp)}, + + {0x78, "min_balance", opMinBalance, proto("i:i"), 3, only(ModeApp)}, + {0x78, "min_balance", opMinBalance, proto("a:i"), directRefEnabledVersion, only(ModeApp)}, // Immediate bytes and ints. Smaller code size for single use of constant. {0x80, "pushbytes", opPushBytes, proto(":b"), 3, constants(asmPushBytes, opPushBytes, "bytes", immBytes)}, @@ -607,33 +607,33 @@ var OpSpecs = []OpSpec{ {0xaf, "bzero", opBytesZero, proto("i:b"), 4, detDefault()}, // AVM "effects" - {0xb0, "log", opLog, proto("b:"), 5, only(modeApp)}, - {0xb1, "itxn_begin", opTxBegin, proto(":"), 5, only(modeApp)}, - {0xb2, "itxn_field", opItxnField, proto("a:"), 5, immediates("f").typed(typeTxField).field("f", &TxnFields).only(modeApp).assembler(asmItxnField)}, - {0xb3, "itxn_submit", opItxnSubmit, proto(":"), 5, only(modeApp)}, - {0xb4, "itxn", opItxn, proto(":a"), 5, field("f", &TxnScalarFields).only(modeApp).assembler(asmItxn)}, - {0xb5, "itxna", opItxna, proto(":a"), 5, immediates("f", "i").field("f", &TxnArrayFields).only(modeApp)}, - {0xb6, "itxn_next", opItxnNext, proto(":"), 6, only(modeApp)}, - {0xb7, "gitxn", opGitxn, proto(":a"), 6, immediates("t", "f").field("f", &TxnFields).only(modeApp).assembler(asmGitxn)}, - {0xb8, "gitxna", opGitxna, proto(":a"), 6, immediates("t", "f", "i").field("f", &TxnArrayFields).only(modeApp)}, + {0xb0, "log", opLog, proto("b:"), 5, only(ModeApp)}, + {0xb1, "itxn_begin", opTxBegin, proto(":"), 5, only(ModeApp)}, + {0xb2, "itxn_field", opItxnField, proto("a:"), 5, immediates("f").typed(typeTxField).field("f", &TxnFields).only(ModeApp).assembler(asmItxnField)}, + {0xb3, "itxn_submit", opItxnSubmit, proto(":"), 5, only(ModeApp)}, + {0xb4, "itxn", opItxn, proto(":a"), 5, field("f", &TxnScalarFields).only(ModeApp).assembler(asmItxn)}, + {0xb5, "itxna", opItxna, proto(":a"), 5, immediates("f", "i").field("f", &TxnArrayFields).only(ModeApp)}, + {0xb6, "itxn_next", opItxnNext, proto(":"), 6, only(ModeApp)}, + {0xb7, "gitxn", opGitxn, proto(":a"), 6, immediates("t", "f").field("f", &TxnFields).only(ModeApp).assembler(asmGitxn)}, + {0xb8, "gitxna", opGitxna, proto(":a"), 6, immediates("t", "f", "i").field("f", &TxnArrayFields).only(ModeApp)}, // Unlimited Global Storage - Boxes - {0xb9, "box_create", opBoxCreate, proto("bi:i"), boxVersion, only(modeApp)}, - {0xba, "box_extract", opBoxExtract, proto("bii:b"), boxVersion, only(modeApp)}, - {0xbb, "box_replace", opBoxReplace, proto("bib:"), boxVersion, only(modeApp)}, - {0xbc, "box_del", opBoxDel, proto("b:i"), boxVersion, only(modeApp)}, - {0xbd, "box_len", opBoxLen, proto("b:ii"), boxVersion, only(modeApp)}, - {0xbe, "box_get", opBoxGet, proto("b:bi"), boxVersion, only(modeApp)}, - {0xbf, "box_put", opBoxPut, proto("bb:"), boxVersion, only(modeApp)}, + {0xb9, "box_create", opBoxCreate, proto("bi:i"), boxVersion, only(ModeApp)}, + {0xba, "box_extract", opBoxExtract, proto("bii:b"), boxVersion, only(ModeApp)}, + {0xbb, "box_replace", opBoxReplace, proto("bib:"), boxVersion, only(ModeApp)}, + {0xbc, "box_del", opBoxDel, proto("b:i"), boxVersion, only(ModeApp)}, + {0xbd, "box_len", opBoxLen, proto("b:ii"), boxVersion, only(ModeApp)}, + {0xbe, "box_get", opBoxGet, proto("b:bi"), boxVersion, only(ModeApp)}, + {0xbf, "box_put", opBoxPut, proto("bb:"), boxVersion, only(ModeApp)}, // Dynamic indexing {0xc0, "txnas", opTxnas, proto("i:a"), 5, field("f", &TxnArrayFields)}, {0xc1, "gtxnas", opGtxnas, proto("i:a"), 5, immediates("t", "f").field("f", &TxnArrayFields)}, {0xc2, "gtxnsas", opGtxnsas, proto("ii:a"), 5, field("f", &TxnArrayFields)}, - {0xc3, "args", opArgs, proto("i:b"), 5, only(modeSig)}, - {0xc4, "gloadss", opGloadss, proto("ii:a"), 6, only(modeApp)}, - {0xc5, "itxnas", opItxnas, proto("i:a"), 6, field("f", &TxnArrayFields).only(modeApp)}, - {0xc6, "gitxnas", opGitxnas, proto("i:a"), 6, immediates("t", "f").field("f", &TxnArrayFields).only(modeApp)}, + {0xc3, "args", opArgs, proto("i:b"), 5, only(ModeSig)}, + {0xc4, "gloadss", opGloadss, proto("ii:a"), 6, only(ModeApp)}, + {0xc5, "itxnas", opItxnas, proto("i:a"), 6, field("f", &TxnArrayFields).only(ModeApp)}, + {0xc6, "gitxnas", opGitxnas, proto("i:a"), 6, immediates("t", "f").field("f", &TxnArrayFields).only(ModeApp)}, // randomness support {0xd0, "vrf_verify", opVrfVerify, proto("bbb:bi"), randomnessVersion, field("s", &VrfStandards).costs(5700)}, diff --git a/data/transactions/logic/tracer.go b/data/transactions/logic/tracer.go new file mode 100644 index 000000000..929d5cdb8 --- /dev/null +++ b/data/transactions/logic/tracer.go @@ -0,0 +1,160 @@ +// Copyright (C) 2019-2023 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see <https://www.gnu.org/licenses/>. + +package logic + +import "github.com/algorand/go-algorand/data/transactions" + +// EvalTracer functions are called by eval function during AVM program execution, if a tracer +// is provided. +// +// Refer to the lifecycle graph below for the sequence in which hooks are called. +// +// NOTE: Arguments given to Tracer hooks (EvalParams and EvalContext) are passed by reference, +// they are not copies. It is therefore the responsibility of the tracer implementation to NOT +// modify the state of the structs passed to them. Additionally, hooks are responsible for copying +// the information they need from the argument structs. No guarantees are made that the referenced +// state will not change between hook calls. This decision was made in an effort to reduce the +// performance impact of tracers. +// +// LOGICSIG LIFECYCLE GRAPH +// βββββββββββββββββββββββββββ +// β LogicSig Evaluation β +// βββββββββββββββββββββββββββ€ +// β > BeforeProgram β +// β β +// β βββββββββββββββββββββ β +// β β Teal Operation β β +// β βββββββββββββββββββββ€ β +// β β > BeforeOpcode β β +// β β β β +// β β > AfterOpcode β β +// β βββββββββββββββββββββ β +// | β β β β β β β β +// β β +// β > AfterProgram β +// βββββββββββββββββββββββββββ +// +// APP LIFECYCLE GRAPH +// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ +// β Transaction Evaluation β +// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ +// β > BeforeTxnGroup β +// β β +// β ββββββββββββββββββββββββββββββββββββββββββββββββββ β +// β β > BeforeTxn β β +// β β β β +// β β ββββββββββββββββββββββββββββββββββββββββββββ β β +// β β β ? App Call β β β +// β β ββββββββββββββββββββββββββββββββββββββββββββ€ β β +// β β β > BeforeProgram β β β +// β β β β β β +// β β β ββββββββββββββββββββββββββββββββββββββ β β β +// β β β β Teal Operation β β β β +// β β β ββββββββββββββββββββββββββββββββββββββ€ β β β +// β β β β > BeforeOpcode β β β β +// β β β β ββββββββββββββββββββββββββββββββ β β β β +// β β β β β ? Inner Transaction Group β β β β β +// β β β β ββββββββββββββββββββββββββββββββ€ β β β β +// β β β β β > BeforeTxnGroup β β β β β +// β β β β β ββββββββββββββββββββββββββ β β β β β +// β β β β β β Transaction Evaluation β β β β β β +// β β β β β ββββββββββββββββββββββββββ€ β β β β β +// β β β β β β ... β β β β β β +// β β β β β ββββββββββββββββββββββββββ β β β β β +// β β β β β β β β β β β β β β β β β β +// β β β β β β β β β β +// β β β β β > AfterTxnGroup β β β β β +// β β β β ββββββββββββββββββββββββββββββββ β β β β +// β β β β > AfterOpcode β β β β +// β β β ββββββββββββββββββββββββββββββββββββββ β β β +// β β β β β β β β β β β β β β β β β β +// β β β β β β +// β β β > AfterProgram β β β +// β β ββββββββββββββββββββββββββββββββββββββββββββ β β +// | | β β β β β β β β β β β β β β β | +// β β β β +// β β > AfterTxn β β +// β ββββββββββββββββββββββββββββββββββββββββββββββββββ β +// | β β β β β β β β β β β β β β β β | +// β β +// β > AfterTxnGroup β +// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ +type EvalTracer interface { + // BeforeTxnGroup is called before a transaction group is executed. This includes both top-level + // and inner transaction groups. The argument ep is the EvalParams object for the group; if the + // group is an inner group, this is the EvalParams object for the inner group. + // + // Each transaction within the group calls BeforeTxn and subsequent hooks, as described in the + // lifecycle diagram. + BeforeTxnGroup(ep *EvalParams) + + // AfterTxnGroup is called after a transaction group has been executed. This includes both + // top-level and inner transaction groups. The argument ep is the EvalParams object for the + // group; if the group is an inner group, this is the EvalParams object for the inner group. + AfterTxnGroup(ep *EvalParams) + + // BeforeTxn is called before a transaction is executed. + // + // groupIndex refers to the index of the transaction in the transaction group that will be executed. + BeforeTxn(ep *EvalParams, groupIndex int) + + // AfterTxn is called after a transaction has been executed. + // + // groupIndex refers to the index of the transaction in the transaction group that was just executed. + // ad is the ApplyData result of the transaction; prefer using this instead of + // ep.TxnGroup[groupIndex].ApplyData, since it may not be populated at this point. + AfterTxn(ep *EvalParams, groupIndex int, ad transactions.ApplyData) + + // BeforeProgram is called before an app or LogicSig program is evaluated. + BeforeProgram(cx *EvalContext) + + // AfterProgram is called after an app or LogicSig program is evaluated. + AfterProgram(cx *EvalContext, evalError error) + + // BeforeOpcode is called before the op is evaluated + BeforeOpcode(cx *EvalContext) + + // AfterOpcode is called after the op has been evaluated + AfterOpcode(cx *EvalContext, evalError error) +} + +// NullEvalTracer implements EvalTracer, but all of its hook methods do nothing +type NullEvalTracer struct{} + +// BeforeTxnGroup does nothing +func (n NullEvalTracer) BeforeTxnGroup(ep *EvalParams) {} + +// AfterTxnGroup does nothing +func (n NullEvalTracer) AfterTxnGroup(ep *EvalParams) {} + +// BeforeTxn does nothing +func (n NullEvalTracer) BeforeTxn(ep *EvalParams, groupIndex int) {} + +// AfterTxn does nothing +func (n NullEvalTracer) AfterTxn(ep *EvalParams, groupIndex int, ad transactions.ApplyData) {} + +// BeforeProgram does nothing +func (n NullEvalTracer) BeforeProgram(cx *EvalContext) {} + +// AfterProgram does nothing +func (n NullEvalTracer) AfterProgram(cx *EvalContext, evalError error) {} + +// BeforeOpcode does nothing +func (n NullEvalTracer) BeforeOpcode(cx *EvalContext) {} + +// AfterOpcode does nothing +func (n NullEvalTracer) AfterOpcode(cx *EvalContext, evalError error) {} diff --git a/data/transactions/logic/tracer_test.go b/data/transactions/logic/tracer_test.go new file mode 100644 index 000000000..c47f3d192 --- /dev/null +++ b/data/transactions/logic/tracer_test.go @@ -0,0 +1,195 @@ +// Copyright (C) 2019-2023 Algorand, Inc. +// This file is part of go-algorand +// +// go-algorand is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// go-algorand is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with go-algorand. If not, see <https://www.gnu.org/licenses/>. + +package logic + +import ( + "testing" + + "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/test/partitiontest" + "github.com/stretchr/testify/require" +) + +const innerTxnTestProgram string = `itxn_begin +int appl +itxn_field TypeEnum +int NoOp +itxn_field OnCompletion +byte 0x068101 // #pragma version 6; int 1; +dup +itxn_field ApprovalProgram +itxn_field ClearStateProgram +itxn_submit + +itxn_begin +int pay +itxn_field TypeEnum +int 1 +itxn_field Amount +global CurrentApplicationAddress +itxn_field Receiver +itxn_next +int pay +itxn_field TypeEnum +int 2 +itxn_field Amount +global CurrentApplicationAddress +itxn_field Receiver +itxn_submit + +int 1 +` + +// can't use mocktracer.Tracer because the import would be circular +type testEvalTracer struct { + beforeTxnGroupCalls int + afterTxnGroupCalls int + + beforeTxnCalls int + afterTxnCalls int + + beforeProgramCalls int + afterProgramCalls int + programModes []RunMode + + beforeOpcodeCalls int + afterOpcodeCalls int +} + +func (t *testEvalTracer) BeforeTxnGroup(ep *EvalParams) { + t.beforeTxnGroupCalls++ +} + +func (t *testEvalTracer) AfterTxnGroup(ep *EvalParams) { + t.afterTxnGroupCalls++ +} + +func (t *testEvalTracer) BeforeTxn(ep *EvalParams, groupIndex int) { + t.beforeTxnCalls++ +} + +func (t *testEvalTracer) AfterTxn(ep *EvalParams, groupIndex int, ad transactions.ApplyData) { + t.afterTxnCalls++ +} + +func (t *testEvalTracer) BeforeProgram(cx *EvalContext) { + t.beforeProgramCalls++ + t.programModes = append(t.programModes, cx.RunMode()) +} + +func (t *testEvalTracer) AfterProgram(cx *EvalContext, evalError error) { + t.afterProgramCalls++ +} + +func (t *testEvalTracer) BeforeOpcode(cx *EvalContext) { + t.beforeOpcodeCalls++ +} + +func (t *testEvalTracer) AfterOpcode(cx *EvalContext, evalError error) { + t.afterOpcodeCalls++ +} + +func TestEvalWithTracer(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + t.Run("logicsig", func(t *testing.T) { + t.Parallel() + testTracer := testEvalTracer{} + ep := defaultEvalParams() + ep.Tracer = &testTracer + testLogic(t, debuggerTestProgram, AssemblerMaxVersion, ep) + + // BeforeTxnGroup/AfterTxnGroup/BeforeTxn/AfterTxn are only called for the inner txns in + // this test, not the top-level ones + require.Zero(t, testTracer.beforeTxnGroupCalls) + require.Zero(t, testTracer.afterTxnGroupCalls) + require.Zero(t, testTracer.beforeTxnCalls) + require.Zero(t, testTracer.afterTxnCalls) + + require.Equal(t, 1, testTracer.beforeProgramCalls) + require.Equal(t, 1, testTracer.afterProgramCalls) + require.Equal(t, []RunMode{ModeSig}, testTracer.programModes) + + require.Equal(t, 35, testTracer.beforeOpcodeCalls) + require.Equal(t, testTracer.beforeOpcodeCalls, testTracer.afterOpcodeCalls) + }) + + t.Run("simple app", func(t *testing.T) { + t.Parallel() + testTracer := testEvalTracer{} + ep := defaultEvalParams() + ep.Tracer = &testTracer + testApp(t, debuggerTestProgram, ep) + + // BeforeTxnGroup/AfterTxnGroup/BeforeTxn/AfterTxn are only called for the inner txns in + // this test, not the top-level ones + require.Zero(t, testTracer.beforeTxnGroupCalls) + require.Zero(t, testTracer.afterTxnGroupCalls) + require.Zero(t, testTracer.beforeTxnCalls) + require.Zero(t, testTracer.afterTxnCalls) + + require.Equal(t, 1, testTracer.beforeProgramCalls) + require.Equal(t, 1, testTracer.afterProgramCalls) + require.Equal(t, []RunMode{ModeApp}, testTracer.programModes) + + require.Equal(t, 35, testTracer.beforeOpcodeCalls) + require.Equal(t, testTracer.beforeOpcodeCalls, testTracer.afterOpcodeCalls) + }) + + t.Run("app with inner txns", func(t *testing.T) { + t.Parallel() + testTracer := testEvalTracer{} + ep, tx, ledger := MakeSampleEnv() + + // Establish 888 as the app id, and fund it. + ledger.NewApp(tx.Receiver, 888, basics.AppParams{}) + ledger.NewAccount(basics.AppIndex(888).Address(), 200000) + + ep.Tracer = &testTracer + testApp(t, innerTxnTestProgram, ep) + + // BeforeTxnGroup/AfterTxnGroup/BeforeTxn/AfterTxn are only called for the inner txns in + // this test, not the top-level ones + + // two groups of inner txns were issued + require.Equal(t, 2, testTracer.beforeTxnGroupCalls) + require.Equal(t, 2, testTracer.afterTxnGroupCalls) + + // three total inner txns were issued + require.Equal(t, 3, testTracer.beforeTxnCalls) + require.Equal(t, 3, testTracer.afterTxnCalls) + + require.Equal(t, 2, testTracer.beforeProgramCalls) + require.Equal(t, 2, testTracer.afterProgramCalls) + require.Equal(t, []RunMode{ModeApp, ModeApp}, testTracer.programModes) + + appCallTealOps := 27 + innerAppCallTealOps := 1 + require.Equal(t, appCallTealOps+innerAppCallTealOps, testTracer.beforeOpcodeCalls) + require.Equal(t, testTracer.beforeOpcodeCalls, testTracer.afterOpcodeCalls) + }) +} + +func TestNullEvalTracerIsEvalTracer(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + var tracer EvalTracer = NullEvalTracer{} + require.NotNil(t, tracer) +} diff --git a/data/transactions/verify/txn.go b/data/transactions/verify/txn.go index 58249bc79..2edd69aa0 100644 --- a/data/transactions/verify/txn.go +++ b/data/transactions/verify/txn.go @@ -175,7 +175,7 @@ func (g *GroupContext) Equal(other *GroupContext) bool { // txnBatchPrep verifies a SignedTxn having no obviously inconsistent data. // Block-assembly time checks of LogicSig and accounting rules may still block the txn. // It is the caller responsibility to call batchVerifier.Verify(). -func txnBatchPrep(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext, verifier *crypto.BatchVerifier) *TxGroupError { +func txnBatchPrep(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext, verifier *crypto.BatchVerifier, evalTracer logic.EvalTracer) *TxGroupError { if !groupCtx.consensusParams.SupportRekeying && (s.AuthAddr != basics.Address{}) { return &TxGroupError{err: errRekeyingNotSupported, Reason: TxGroupErrorReasonGeneric} } @@ -184,14 +184,23 @@ func txnBatchPrep(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext, return &TxGroupError{err: err, Reason: TxGroupErrorReasonNotWellFormed} } - return stxnCoreChecks(s, txnIdx, groupCtx, verifier) + return stxnCoreChecks(s, txnIdx, groupCtx, verifier, evalTracer) } // TxnGroup verifies a []SignedTxn as being signed and having no obviously inconsistent data. func TxnGroup(stxs []transactions.SignedTxn, contextHdr *bookkeeping.BlockHeader, cache VerifiedTransactionCache, ledger logic.LedgerForSignature) (groupCtx *GroupContext, err error) { + return txnGroup(stxs, contextHdr, cache, ledger, nil) +} + +// TxnGroupWithTracer verifies a []SignedTxn as being signed and having no obviously inconsistent data, while using a tracer. +func TxnGroupWithTracer(stxs []transactions.SignedTxn, contextHdr *bookkeeping.BlockHeader, cache VerifiedTransactionCache, ledger logic.LedgerForSignature, evalTracer logic.EvalTracer) (groupCtx *GroupContext, err error) { + return txnGroup(stxs, contextHdr, cache, ledger, evalTracer) +} + +func txnGroup(stxs []transactions.SignedTxn, contextHdr *bookkeeping.BlockHeader, cache VerifiedTransactionCache, ledger logic.LedgerForSignature, evalTracer logic.EvalTracer) (groupCtx *GroupContext, err error) { batchVerifier := crypto.MakeBatchVerifier() - if groupCtx, err = txnGroupBatchPrep(stxs, contextHdr, ledger, batchVerifier); err != nil { + if groupCtx, err = txnGroupBatchPrep(stxs, contextHdr, ledger, batchVerifier, evalTracer); err != nil { return nil, err } @@ -208,7 +217,7 @@ func TxnGroup(stxs []transactions.SignedTxn, contextHdr *bookkeeping.BlockHeader // txnGroupBatchPrep verifies a []SignedTxn having no obviously inconsistent data. // it is the caller responsibility to call batchVerifier.Verify() -func txnGroupBatchPrep(stxs []transactions.SignedTxn, contextHdr *bookkeeping.BlockHeader, ledger logic.LedgerForSignature, verifier *crypto.BatchVerifier) (*GroupContext, error) { +func txnGroupBatchPrep(stxs []transactions.SignedTxn, contextHdr *bookkeeping.BlockHeader, ledger logic.LedgerForSignature, verifier *crypto.BatchVerifier, evalTracer logic.EvalTracer) (*GroupContext, error) { groupCtx, err := PrepareGroupContext(stxs, contextHdr, ledger) if err != nil { return nil, err @@ -217,7 +226,7 @@ func txnGroupBatchPrep(stxs []transactions.SignedTxn, contextHdr *bookkeeping.Bl minFeeCount := uint64(0) feesPaid := uint64(0) for i, stxn := range stxs { - prepErr := txnBatchPrep(&stxn, i, groupCtx, verifier) + prepErr := txnBatchPrep(&stxn, i, groupCtx, verifier, evalTracer) if prepErr != nil { // re-wrap the error with more details prepErr.err = fmt.Errorf("transaction %+v invalid : %w", stxn, prepErr.err) @@ -288,7 +297,7 @@ func checkTxnSigTypeCounts(s *transactions.SignedTxn) (sigType sigOrTxnType, err } // stxnCoreChecks runs signatures validity checks and enqueues signature into batchVerifier for verification. -func stxnCoreChecks(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext, batchVerifier *crypto.BatchVerifier) *TxGroupError { +func stxnCoreChecks(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext, batchVerifier *crypto.BatchVerifier, evalTracer logic.EvalTracer) *TxGroupError { sigType, err := checkTxnSigTypeCounts(s) if err != nil { return err @@ -318,7 +327,7 @@ func stxnCoreChecks(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContex return nil case logicSig: - if err := logicSigVerify(s, txnIdx, groupCtx); err != nil { + if err := logicSigVerify(s, txnIdx, groupCtx, evalTracer); err != nil { return &TxGroupError{err: err, Reason: TxGroupErrorReasonLogicSigFailed} } return nil @@ -428,7 +437,7 @@ func logicSigSanityCheckBatchPrep(txn *transactions.SignedTxn, groupIndex int, g } // logicSigVerify checks that the signature is valid, executing the program. -func logicSigVerify(txn *transactions.SignedTxn, groupIndex int, groupCtx *GroupContext) error { +func logicSigVerify(txn *transactions.SignedTxn, groupIndex int, groupCtx *GroupContext, evalTracer logic.EvalTracer) error { err := LogicSigSanityCheck(txn, groupIndex, groupCtx) if err != nil { return err @@ -442,6 +451,7 @@ func logicSigVerify(txn *transactions.SignedTxn, groupIndex int, groupCtx *Group TxnGroup: transactions.WrapSignedTxnsWithAD(groupCtx.signedGroupTxns), MinAvmVersion: &groupCtx.minAvmVersion, SigLedger: groupCtx.ledger, + Tracer: evalTracer, } pass, cx, err := logic.EvalSignatureFull(groupIndex, &ep) if err != nil { @@ -501,7 +511,7 @@ func PaysetGroups(ctx context.Context, payset [][]transactions.SignedTxn, blkHea batchVerifier := crypto.MakeBatchVerifierWithHint(len(payset)) for i, signTxnsGrp := range txnGroups { - groupCtxs[i], grpErr = txnGroupBatchPrep(signTxnsGrp, &blkHeader, ledger, batchVerifier) + groupCtxs[i], grpErr = txnGroupBatchPrep(signTxnsGrp, &blkHeader, ledger, batchVerifier, nil) // abort only if it's a non-cache error. if grpErr != nil { return grpErr @@ -851,7 +861,7 @@ func (sv *StreamVerifier) addVerificationTaskToThePoolNow(ue []*UnverifiedElemen // TODO: separate operations here, and get the sig verification inside the LogicSig to the batch here blockHeader := sv.nbw.getBlockHeader() for _, ue := range ue { - groupCtx, err := txnGroupBatchPrep(ue.TxnGroup, blockHeader, sv.ledger, batchVerifier) + groupCtx, err := txnGroupBatchPrep(ue.TxnGroup, blockHeader, sv.ledger, batchVerifier, nil) if err != nil { // verification failed, no need to add the sig to the batch, report the error sv.sendResult(ue.TxnGroup, ue.BacklogMessage, err) diff --git a/data/transactions/verify/txn_test.go b/data/transactions/verify/txn_test.go index 62cccce19..eb03917fb 100644 --- a/data/transactions/verify/txn_test.go +++ b/data/transactions/verify/txn_test.go @@ -36,6 +36,8 @@ import ( "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/data/transactions/logic/mocktracer" + "github.com/algorand/go-algorand/data/txntest" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/logging" "github.com/algorand/go-algorand/protocol" @@ -66,7 +68,7 @@ var spec = transactions.SpecialAddresses{ func verifyTxn(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext) error { batchVerifier := crypto.MakeBatchVerifier() - if err := txnBatchPrep(s, txnIdx, groupCtx, batchVerifier); err != nil { + if err := txnBatchPrep(s, txnIdx, groupCtx, batchVerifier, nil); err != nil { return err } return batchVerifier.Verify() @@ -360,6 +362,89 @@ func TestDecodeNil(t *testing.T) { } } +func TestTxnGroupWithTracer(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + proto := config.Consensus[protocol.ConsensusCurrentVersion] + + account := keypair() + accountAddr := basics.Address(account.SignatureVerifier) + + ops1, err := logic.AssembleString(`#pragma version 6 +pushint 1`) + require.NoError(t, err) + program1 := ops1.Program + program1Addr := basics.Address(logic.HashProgram(program1)) + + ops2, err := logic.AssembleString(`#pragma version 6 +pushbytes "test" +pop +pushint 1`) + require.NoError(t, err) + program2 := ops2.Program + program2Addr := basics.Address(logic.HashProgram(program2)) + + // this shouldn't be invoked during this test + appProgram := "err" + + lsigPay := txntest.Txn{ + Type: protocol.PaymentTx, + Sender: program1Addr, + Receiver: accountAddr, + Fee: proto.MinTxnFee, + } + + normalSigAppCall := txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: accountAddr, + ApprovalProgram: appProgram, + ClearStateProgram: appProgram, + Fee: proto.MinTxnFee, + } + + lsigAppCall := txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: program2Addr, + ApprovalProgram: appProgram, + ClearStateProgram: appProgram, + Fee: proto.MinTxnFee, + } + + txntest.Group(&lsigPay, &normalSigAppCall, &lsigAppCall) + + txgroup := []transactions.SignedTxn{ + { + Lsig: transactions.LogicSig{ + Logic: program1, + }, + Txn: lsigPay.Txn(), + }, + normalSigAppCall.Txn().Sign(account), + { + Lsig: transactions.LogicSig{ + Logic: program2, + }, + Txn: lsigAppCall.Txn(), + }, + } + + mockTracer := &mocktracer.Tracer{} + _, err = TxnGroupWithTracer(txgroup, blockHeader, nil, logic.NoHeaderLedger{}, mockTracer) + require.NoError(t, err) + + expectedEvents := []mocktracer.Event{ + mocktracer.BeforeProgram(logic.ModeSig), // first txn start + mocktracer.BeforeOpcode(), mocktracer.AfterOpcode(), // first txn LogicSig: 1 op + mocktracer.AfterProgram(logic.ModeSig), // first txn end + // nothing for second txn (not signed with a LogicSig) + mocktracer.BeforeProgram(logic.ModeSig), // third txn start + mocktracer.BeforeOpcode(), mocktracer.AfterOpcode(), mocktracer.BeforeOpcode(), mocktracer.AfterOpcode(), mocktracer.BeforeOpcode(), mocktracer.AfterOpcode(), // third txn LogicSig: 3 ops + mocktracer.AfterProgram(logic.ModeSig), // third txn end + } + require.Equal(t, expectedEvents, mockTracer.Events) +} + func TestPaysetGroups(t *testing.T) { partitiontest.PartitionTest(t) diff --git a/data/txDupCache.go b/data/txDupCache.go index 6e127e2db..96a426dc7 100644 --- a/data/txDupCache.go +++ b/data/txDupCache.go @@ -228,6 +228,12 @@ func (c *txSaltedCache) CheckAndPut(msg []byte) (*crypto.Digest, bool) { // already added to cache between RUnlock() and Lock(), return return d, found } + } else { + // Do another check to see if another copy of the transaction won the race to write it to the cache + // Only check current to save a lookup since swaps are rare and no need to re-hash + if _, found := c.cur[*d]; found { + return d, found + } } if len(c.cur) >= c.maxSize { diff --git a/data/txntest/defi.go b/data/txntest/defi.go index dcdaaaf40..635393a67 100644 --- a/data/txntest/defi.go +++ b/data/txntest/defi.go @@ -526,7 +526,7 @@ func CreateTinyManSignedTxGroup(tb testing.TB, txns []Txn) ([]transactions.Signe ops, err := logic.AssembleString(TmLsig) require.NoError(tb, err) - stxns := SignedTxns(&txns[0], &txns[1], &txns[2], &txns[3]) + stxns := Group(&txns[0], &txns[1], &txns[2], &txns[3]) stxns[1].Lsig.Logic = ops.Program stxns[3].Lsig.Logic = ops.Program diff --git a/data/txntest/txn.go b/data/txntest/txn.go index 8ed1107c6..8e9f699f8 100644 --- a/data/txntest/txn.go +++ b/data/txntest/txn.go @@ -287,10 +287,10 @@ func (tx Txn) SignedTxnWithAD() transactions.SignedTxnWithAD { return transactions.SignedTxnWithAD{SignedTxn: tx.SignedTxn()} } -// SignedTxns turns a list of Txns into a slice of SignedTxns with -// GroupIDs set properly to make them a transaction group. Maybe -// another name is more approrpriate -func SignedTxns(txns ...*Txn) []transactions.SignedTxn { +// Group turns a list of Txns into a slice of SignedTxns with +// GroupIDs set properly to make them a transaction group. The input +// Txns are modified with the calculated GroupID. +func Group(txns ...*Txn) []transactions.SignedTxn { txgroup := transactions.TxGroup{ TxGroupHashes: make([]crypto.Digest, len(txns)), } diff --git a/ledger/internal/eval.go b/ledger/internal/eval.go index ea5480b0b..1a1a1eea5 100644 --- a/ledger/internal/eval.go +++ b/ledger/internal/eval.go @@ -594,6 +594,8 @@ type BlockEvaluator struct { l LedgerForEvaluator maxTxnBytesPerBlock int + + Tracer logic.EvalTracer } // LedgerForEvaluator defines the ledger interface needed by the evaluator. @@ -925,14 +927,7 @@ func (eval *BlockEvaluator) Transaction(txn transactions.SignedTxn, ad transacti // TransactionGroup tentatively adds a new transaction group as part of this block evaluation. // If the transaction group cannot be added to the block without violating some constraints, // an error is returned and the block evaluator state is unchanged. -func (eval *BlockEvaluator) TransactionGroup(txads []transactions.SignedTxnWithAD) error { - return eval.transactionGroup(txads) -} - -// transactionGroup tentatively executes a group of transactions as part of this block evaluation. -// If the transaction group cannot be added to the block without violating some constraints, -// an error is returned and the block evaluator state is unchanged. -func (eval *BlockEvaluator) transactionGroup(txgroup []transactions.SignedTxnWithAD) error { +func (eval *BlockEvaluator) TransactionGroup(txgroup []transactions.SignedTxnWithAD) error { // Nothing to do if there are no transactions. if len(txgroup) == 0 { return nil @@ -953,17 +948,30 @@ func (eval *BlockEvaluator) transactionGroup(txgroup []transactions.SignedTxnWit defer cow.recycle() evalParams := logic.NewEvalParams(txgroup, &eval.proto, &eval.specials) + evalParams.Tracer = eval.Tracer + + if eval.Tracer != nil { + eval.Tracer.BeforeTxnGroup(evalParams) + } // Evaluate each transaction in the group txibs = make([]transactions.SignedTxnInBlock, 0, len(txgroup)) for gi, txad := range txgroup { var txib transactions.SignedTxnInBlock + if eval.Tracer != nil { + eval.Tracer.BeforeTxn(evalParams, gi) + } + err := eval.transaction(txad.SignedTxn, evalParams, gi, txad.ApplyData, cow, &txib) if err != nil { return err } + if eval.Tracer != nil { + eval.Tracer.AfterTxn(evalParams, gi, txib.ApplyData) + } + txibs = append(txibs, txib) if eval.validate { @@ -1010,6 +1018,10 @@ func (eval *BlockEvaluator) transactionGroup(txgroup []transactions.SignedTxnWit eval.blockTxBytes += groupTxBytes cow.commitToParent() + if eval.Tracer != nil { + eval.Tracer.AfterTxnGroup(evalParams) + } + return nil } diff --git a/ledger/internal/eval_test.go b/ledger/internal/eval_test.go index 67a29488a..5803a4d61 100644 --- a/ledger/internal/eval_test.go +++ b/ledger/internal/eval_test.go @@ -33,11 +33,14 @@ import ( "github.com/algorand/go-algorand/crypto/merklesignature" "github.com/algorand/go-algorand/crypto/stateproof" "github.com/algorand/go-algorand/data/basics" + basics_testing "github.com/algorand/go-algorand/data/basics/testing" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/stateproofmsg" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/logic" + "github.com/algorand/go-algorand/data/transactions/logic/mocktracer" "github.com/algorand/go-algorand/data/transactions/verify" + "github.com/algorand/go-algorand/data/txntest" "github.com/algorand/go-algorand/ledger/apply" "github.com/algorand/go-algorand/ledger/ledgercore" ledgertesting "github.com/algorand/go-algorand/ledger/testing" @@ -307,8 +310,203 @@ func TestPrivateTransactionGroup(t *testing.T) { require.Error(t, err) // too many } +func tealOpLogs(count int) []mocktracer.Event { + var log []mocktracer.Event + + for i := 0; i < count; i++ { + log = append(log, mocktracer.BeforeOpcode(), mocktracer.AfterOpcode()) + } + + return log +} + +func flatten(rows [][]mocktracer.Event) []mocktracer.Event { + var out []mocktracer.Event + for _, row := range rows { + out = append(out, row...) + } + return out +} + +const innerTxnTestProgram string = `#pragma version 6 +itxn_begin +int appl +itxn_field TypeEnum +int NoOp +itxn_field OnCompletion +byte 0x068101 // #pragma version 6; int 1; +dup +itxn_field ApprovalProgram +itxn_field ClearStateProgram +itxn_submit + +itxn_begin +int pay +itxn_field TypeEnum +int 1 +itxn_field Amount +global CurrentApplicationAddress +itxn_field Receiver +itxn_next +int pay +itxn_field TypeEnum +int 2 +itxn_field Amount +global CurrentApplicationAddress +itxn_field Receiver +itxn_submit + +int 1 +` + +func TestTransactionGroupWithTracer(t *testing.T) { + partitiontest.PartitionTest(t) + t.Parallel() + + genesisInitState, addrs, keys := ledgertesting.Genesis(10) + + innerAppID := 3 + innerAppAddress := basics.AppIndex(innerAppID).Address() + balances := genesisInitState.Accounts + balances[innerAppAddress] = basics_testing.MakeAccountData(basics.Offline, basics.MicroAlgos{Raw: 1000000}) + + genesisBalances := bookkeeping.GenesisBalances{ + Balances: genesisInitState.Accounts, + FeeSink: testSinkAddr, + RewardsPool: testPoolAddr, + Timestamp: 0, + } + l := newTestLedger(t, genesisBalances) + + blkHeader, err := l.BlockHdr(basics.Round(0)) + require.NoError(t, err) + newBlock := bookkeeping.MakeBlock(blkHeader) + eval, err := l.StartEvaluator(newBlock.BlockHeader, 0, 0) + require.NoError(t, err) + eval.validate = true + eval.generate = true + + basicProgram := `#pragma version 6 +byte "hello" +log +int 1` + + genHash := l.GenesisHash() + + // a basic app call + basicAppCallTxn := txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: addrs[0], + ApprovalProgram: basicProgram, + ClearStateProgram: basicProgram, + + FirstValid: newBlock.Round(), + LastValid: newBlock.Round() + 1000, + Fee: minFee, + GenesisHash: genHash, + } + + // a non-app call txn + payTxn := txntest.Txn{ + Type: protocol.PaymentTx, + Sender: addrs[1], + Receiver: addrs[2], + CloseRemainderTo: addrs[3], + Amount: 1_000_000, + + FirstValid: newBlock.Round(), + LastValid: newBlock.Round() + 1000, + Fee: minFee, + GenesisHash: genHash, + } + + // an app call that spawns inner txns + innerAppCallTxn := txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: addrs[0], + ApprovalProgram: innerTxnTestProgram, + ClearStateProgram: basicProgram, + + FirstValid: newBlock.Round(), + LastValid: newBlock.Round() + 1000, + Fee: minFee, + GenesisHash: genHash, + } + + txntest.Group(&basicAppCallTxn, &payTxn, &innerAppCallTxn) + + txgroup := transactions.WrapSignedTxnsWithAD([]transactions.SignedTxn{ + basicAppCallTxn.Txn().Sign(keys[0]), + payTxn.Txn().Sign(keys[1]), + innerAppCallTxn.Txn().Sign(keys[0]), + }) + + require.Len(t, eval.block.Payset, 0) + + tracer := &mocktracer.Tracer{} + eval.Tracer = tracer + err = eval.TransactionGroup(txgroup) + require.NoError(t, err) + + require.Len(t, eval.block.Payset, len(txgroup)) + + expectedADs := make([]transactions.ApplyData, len(txgroup)) + for i, txn := range eval.block.Payset { + expectedADs[i] = txn.ApplyData + } + + expectedEvents := flatten([][]mocktracer.Event{ + { + mocktracer.BeforeTxnGroup(3), + mocktracer.BeforeTxn(protocol.ApplicationCallTx), // start basicAppCallTxn + mocktracer.BeforeProgram(logic.ModeApp), + }, + tealOpLogs(3), + { + mocktracer.AfterProgram(logic.ModeApp), + mocktracer.AfterTxn(protocol.ApplicationCallTx, expectedADs[0]), // end basicAppCallTxn + mocktracer.BeforeTxn(protocol.PaymentTx), // start payTxn + mocktracer.AfterTxn(protocol.PaymentTx, expectedADs[1]), // end payTxn + mocktracer.BeforeTxn(protocol.ApplicationCallTx), // start innerAppCallTxn + mocktracer.BeforeProgram(logic.ModeApp), + }, + tealOpLogs(10), + { + mocktracer.BeforeOpcode(), + mocktracer.BeforeTxnGroup(1), // start first itxn group + mocktracer.BeforeTxn(protocol.ApplicationCallTx), + mocktracer.BeforeProgram(logic.ModeApp), + }, + tealOpLogs(1), + { + mocktracer.AfterProgram(logic.ModeApp), + mocktracer.AfterTxn(protocol.ApplicationCallTx, expectedADs[2].EvalDelta.InnerTxns[0].ApplyData), + mocktracer.AfterTxnGroup(1), // end first itxn group + mocktracer.AfterOpcode(), + }, + tealOpLogs(14), + { + mocktracer.BeforeOpcode(), + mocktracer.BeforeTxnGroup(2), // start second itxn group + mocktracer.BeforeTxn(protocol.PaymentTx), + mocktracer.AfterTxn(protocol.PaymentTx, expectedADs[2].EvalDelta.InnerTxns[1].ApplyData), + mocktracer.BeforeTxn(protocol.PaymentTx), + mocktracer.AfterTxn(protocol.PaymentTx, expectedADs[2].EvalDelta.InnerTxns[2].ApplyData), + mocktracer.AfterTxnGroup(2), // end second itxn group + mocktracer.AfterOpcode(), + }, + tealOpLogs(1), + { + mocktracer.AfterProgram(logic.ModeApp), + mocktracer.AfterTxn(protocol.ApplicationCallTx, expectedADs[2]), // end innerAppCallTxn + mocktracer.AfterTxnGroup(3), + }, + }) + require.Equal(t, expectedEvents, tracer.Events) +} + // BlockEvaluator.workaroundOverspentRewards() fixed a couple issues on testnet. -// This is now part of history and has to be re-created when running catchup on testnet. So, test to ensure it keeps happenning. +// This is now part of history and has to be re-created when running catchup on testnet. So, test to ensure it keeps happening. func TestTestnetFixup(t *testing.T) { partitiontest.PartitionTest(t) diff --git a/ledger/simple_test.go b/ledger/simple_test.go index 5389931aa..e934e3f01 100644 --- a/ledger/simple_test.go +++ b/ledger/simple_test.go @@ -116,7 +116,7 @@ func txgroup(t testing.TB, ledger *Ledger, eval *internal.BlockEvaluator, txns . for _, txn := range txns { fillDefaults(t, ledger, eval, txn) } - txgroup := txntest.SignedTxns(txns...) + txgroup := txntest.Group(txns...) return eval.TransactionGroup(transactions.WrapSignedTxnsWithAD(txgroup)) } diff --git a/ledger/simulation/simulator.go b/ledger/simulation/simulator.go index 8cd209df4..b559e989f 100644 --- a/ledger/simulation/simulator.go +++ b/ledger/simulation/simulator.go @@ -24,6 +24,7 @@ import ( "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/transactions" + "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/data/transactions/verify" "github.com/algorand/go-algorand/ledger/ledgercore" "github.com/algorand/go-algorand/protocol" @@ -55,6 +56,18 @@ func (l simulatorLedger) LookupLatest(addr basics.Address) (basics.AccountData, } // ============================== +// > Simulator Tracer +// ============================== + +type evalTracer struct { + logic.NullEvalTracer +} + +func makeTracer() logic.EvalTracer { + return &evalTracer{} +} + +// ============================== // > Simulator Errors // ============================== @@ -115,7 +128,7 @@ var proxySigner = crypto.PrivateKey{ // check verifies that the transaction is well-formed and has valid or missing signatures. // An invalid transaction group error is returned if the transaction is not well-formed or there are invalid signatures. // To make things easier, we support submitting unsigned transactions and will respond whether signatures are missing. -func (s Simulator) check(hdr bookkeeping.BlockHeader, txgroup []transactions.SignedTxn) (bool, error) { +func (s Simulator) check(hdr bookkeeping.BlockHeader, txgroup []transactions.SignedTxn, debugger logic.EvalTracer) (bool, error) { proxySignerSecrets, err := crypto.SecretKeyToSignatureSecrets(proxySigner) if err != nil { return false, err @@ -150,7 +163,7 @@ func (s Simulator) check(hdr bookkeeping.BlockHeader, txgroup []transactions.Sig } // Verify the signed transactions are well-formed and have valid signatures - _, err = verify.TxnGroup(txnsToVerify, &hdr, nil, s.ledger) + _, err = verify.TxnGroupWithTracer(txnsToVerify, &hdr, nil, s.ledger, debugger) if err != nil { return false, InvalidTxGroupError{SimulatorError{err}} } @@ -158,13 +171,14 @@ func (s Simulator) check(hdr bookkeeping.BlockHeader, txgroup []transactions.Sig return len(missingSigs) != 0, nil } -func (s Simulator) evaluate(hdr bookkeeping.BlockHeader, stxns []transactions.SignedTxn) (*ledgercore.ValidatedBlock, error) { +func (s Simulator) evaluate(hdr bookkeeping.BlockHeader, stxns []transactions.SignedTxn, tracer logic.EvalTracer) (*ledgercore.ValidatedBlock, error) { // s.ledger has 'StartEvaluator' because *data.Ledger is embedded in the simulatorLedger // and data.Ledger embeds *ledger.Ledger eval, err := s.ledger.StartEvaluator(hdr, len(stxns), 0) if err != nil { return nil, err } + eval.Tracer = tracer group := transactions.WrapSignedTxnsWithAD(stxns) @@ -190,13 +204,14 @@ func (s Simulator) Simulate(txgroup []transactions.SignedTxn) (*ledgercore.Valid } nextBlock := bookkeeping.MakeBlock(prevBlockHdr) hdr := nextBlock.BlockHeader + simulatorTracer := makeTracer() // check that the transaction is well-formed and mark whether signatures are missing - missingSignatures, err := s.check(hdr, txgroup) + missingSignatures, err := s.check(hdr, txgroup, simulatorTracer) if err != nil { return nil, false, err } - vb, err := s.evaluate(hdr, txgroup) + vb, err := s.evaluate(hdr, txgroup, simulatorTracer) return vb, missingSignatures, err } diff --git a/logging/telemetryspec/metric_test.go b/logging/telemetryspec/metric_test.go index 170b6a60c..85693bdba 100644 --- a/logging/telemetryspec/metric_test.go +++ b/logging/telemetryspec/metric_test.go @@ -56,6 +56,8 @@ func TestTransactionProcessingTimeDistributionFormatting(t *testing.T) { } func TestTransactionProcessingTimeDistributionPrint(t *testing.T) { + partitiontest.PartitionTest(t) + var decPT transactionProcessingTimeDistribution expected := "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38]" require.NoError(t, json.Unmarshal([]byte(expected), &decPT)) diff --git a/node/node_test.go b/node/node_test.go index 77dfc7772..f58471090 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -21,7 +21,9 @@ import ( "math/rand" "os" "path/filepath" + "runtime" "strconv" + "strings" "sync" "testing" "time" @@ -245,6 +247,11 @@ func TestInitialSync(t *testing.T) { t.Skip("Test takes ~25 seconds.") } + if (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64") && + strings.ToUpper(os.Getenv("CIRCLECI")) == "TRUE" { + t.Skip("Test is too heavy for amd64 builder running in parallel with other packages") + } + backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil) defer backlogPool.Shutdown() diff --git a/shared/pingpong/pingpong.go b/shared/pingpong/pingpong.go index 9e6dc27c2..32352bcfc 100644 --- a/shared/pingpong/pingpong.go +++ b/shared/pingpong/pingpong.go @@ -14,6 +14,9 @@ // You should have received a copy of the GNU Affero General Public License // along with go-algorand. If not, see <https://www.gnu.org/licenses/>. +// Package pingpong provides a transaction generating utility for performance testing. +// +//nolint:unused,structcheck,deadcode,varcheck // ignore unused pingpong code package pingpong import ( diff --git a/util/rateLimit.go b/util/rateLimit.go index 3fbd50c3f..c4e85c71e 100644 --- a/util/rateLimit.go +++ b/util/rateLimit.go @@ -65,9 +65,8 @@ type capacityQueue chan capacity // ErlCapacityGuard is the structure returned to clients so they can release the capacity when needed // they also inform the congestion manager of events type ErlCapacityGuard struct { - client ErlClient - cq capacityQueue - cm CongestionManager + cq capacityQueue + cm CongestionManager } // Release will put capacity back into the queue attached to this capacity guard diff --git a/util/tcpinfo_linux.go b/util/tcpinfo_linux.go index 5c332643e..8cf1687ae 100644 --- a/util/tcpinfo_linux.go +++ b/util/tcpinfo_linux.go @@ -56,6 +56,7 @@ func getConnTCPInfo(raw syscall.RawConn) (*TCPInfo, error) { // linuxTCPInfo is based on linux include/uapi/linux/tcp.h struct tcp_info //revive:disable:var-naming +//nolint:structcheck // complains about unused fields that are rqeuired to match C tcp_info struct type linuxTCPInfo struct { state uint8 ca_state uint8 |