Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Dryrun: Return EvalDeltas for failed executions in Dryrun #3929

Merged
merged 15 commits into from
May 13, 2022
3 changes: 3 additions & 0 deletions daemon/algod/api/server/v2/dryrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,9 @@ func doDryrunRequest(dr *DryrunRequest, response *generated.DryrunResponse) {
messages[0] = "ApprovalProgram"
}
pass, delta, err := ba.StatefulEval(ti, ep, appIdx, program)
if !pass {
delta = ep.TxnGroup[ti].EvalDelta
}
result.Disassembly = debug.lines
result.AppCallTrace = &debug.history
result.GlobalDelta = StateDeltaToStateDelta(delta.GlobalDelta)
Expand Down
158 changes: 156 additions & 2 deletions daemon/algod/api/server/v2/dryrun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"strconv"
"strings"
"testing"
Expand Down Expand Up @@ -374,7 +375,7 @@ func checkLogicSigPass(t *testing.T, response *generated.DryrunResponse) {
}
}

func checkAppCallPass(t *testing.T, response *generated.DryrunResponse) {
func checkAppCallResponse(t *testing.T, response *generated.DryrunResponse, msg string) {
if len(response.Txns) < 1 {
t.Error("no response txns")
} else if len(response.Txns) == 0 {
Expand All @@ -387,12 +388,20 @@ func checkAppCallPass(t *testing.T, response *generated.DryrunResponse) {
if response.Txns[idx].AppCallMessages != nil {
messages := *response.Txns[idx].AppCallMessages
assert.GreaterOrEqual(t, len(messages), 1)
assert.Equal(t, "PASS", messages[len(messages)-1])
assert.Equal(t, msg, messages[len(messages)-1])
}
}
}
}

func checkAppCallPass(t *testing.T, response *generated.DryrunResponse) {
checkAppCallResponse(t, response, "PASS")
}

func checkAppCallReject(t *testing.T, response *generated.DryrunResponse) {
checkAppCallResponse(t, response, "REJECT")
}

type expectedSlotType struct {
slot int
tt basics.TealType
Expand Down Expand Up @@ -1634,3 +1643,148 @@ int 1`)
logResponse(t, &response)
}
}

func checkStateDelta(t *testing.T,
response generated.StateDelta,
expectedDelta generated.StateDelta,
) {
for i, vd := range response {
assert.Equal(t, expectedDelta[i].Key, vd.Key)

// Pointer checks: make sure we don't try to derefence a nil.
if expectedDelta[i].Value.Bytes == nil {
assert.Nil(t, vd.Value.Bytes)
} else {
assert.NotNil(t, vd.Value.Bytes)
assert.Equal(t, *expectedDelta[i].Value.Bytes, *vd.Value.Bytes)
}
if expectedDelta[i].Value.Uint == nil {
assert.Nil(t, vd.Value.Uint)
} else {
assert.NotNil(t, vd.Value.Uint)
assert.Equal(t, *expectedDelta[i].Value.Uint, *vd.Value.Uint)
}
}
}

func checkEvalDelta(t *testing.T,
response generated.DryrunResponse,
expectedGlobalDelta generated.StateDelta,
expectedLocalDelta generated.AccountStateDelta,
) {
for _, rt := range response.Txns {
if rt.GlobalDelta != nil && len(*rt.GlobalDelta) > 0 {
assert.Equal(t, len(expectedGlobalDelta), len(*rt.GlobalDelta))
checkStateDelta(t, *rt.GlobalDelta, expectedGlobalDelta)
} else {
assert.Nil(t, expectedGlobalDelta)
}

if rt.LocalDeltas != nil {
for _, ld := range *rt.LocalDeltas {
assert.Equal(t, expectedLocalDelta.Address, ld.Address)
checkStateDelta(t, ld.Delta, expectedLocalDelta.Delta)
}
} else {
assert.Nil(t, expectedLocalDelta)
}
}
}

func TestDryrunCheckEvalDeltasReturned(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

var dr DryrunRequest
var response generated.DryrunResponse

// Expected responses.
expectedByte := b64("val")
expectedUint := uint64(1)
expectedGlobalDelta := generated.StateDelta{
{
Key: b64("key"),
Value: generated.EvalDelta{
Action: uint64(basics.SetBytesAction),
Bytes: &expectedByte,
},
},
}
expectedLocalDelta := generated.AccountStateDelta{
Address: basics.Address{}.String(),
Delta: generated.StateDelta{
{
Key: b64("key"),
Value: generated.EvalDelta{
Action: uint64(basics.SetUintAction),
Uint: &expectedUint,
},
},
},
}

// Test that a PASS and REJECT dryrun both return the dryrun evaldelta.
for i := range []int{0, 1} {
ops, _ := logic.AssembleString(fmt.Sprintf(`
#pragma version 6
txna ApplicationArgs 0
txna ApplicationArgs 1
app_global_put
int 0
txna ApplicationArgs 0
int %d
app_local_put
int %d`, expectedUint, i))
dr.ProtocolVersion = string(dryrunProtoVersion)

dr.Txns = []transactions.SignedTxn{
{
Txn: transactions.Transaction{
Type: protocol.ApplicationCallTx,
ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{
ApplicationID: 1,
ApplicationArgs: [][]byte{
[]byte("key"),
[]byte("val"),
},
},
},
},
}
dr.Apps = []generated.Application{
{
Id: 1,
Params: generated.ApplicationParams{
ApprovalProgram: ops.Program,
GlobalStateSchema: &generated.ApplicationStateSchema{
NumByteSlice: 1,
NumUint: 1,
},
LocalStateSchema: &generated.ApplicationStateSchema{
NumByteSlice: 1,
NumUint: 1,
},
},
},
}
dr.Accounts = []generated.Account{
{
Status: "Online",
Address: basics.Address{}.String(),
AppsLocalState: &[]generated.ApplicationLocalState{{Id: 1}},
},
}

doDryrunRequest(&dr, &response)
if i == 0 {
checkAppCallReject(t, &response)
} else {
checkAppCallPass(t, &response)
}
checkEvalDelta(t, response, expectedGlobalDelta, expectedLocalDelta)
if t.Failed() {
logResponse(t, &response)
}
}

}