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

Algod: Add EvalTracer tests for StateDeltas #5368

Merged
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
272851e
Expose StateDeltas to EvalTracer
jasonpaulos Mar 28, 2023
dcbe0e9
doc
jasonpaulos Mar 28, 2023
2080a1b
Rename + doc
jasonpaulos Mar 28, 2023
bd327a4
Pass StateDelta directly, and add option to gather StateDeltas for in…
jasonpaulos Mar 28, 2023
04a8819
Commit to parent in granular eval even during errors, for idential be…
jasonpaulos Mar 28, 2023
de02048
Update docstring
jasonpaulos Mar 29, 2023
ba5368d
Merge branch 'master' into eval-tracer-state-deltas
jasonpaulos Mar 29, 2023
bfcbe93
Make updates fn private
jasonpaulos Mar 29, 2023
5af6522
Merge branch 'master' into eval-tracer-state-deltas
jasonpaulos Mar 31, 2023
c07ccd2
Reuse child cows in BlockEvaluator.TransactionGroup & remove Updates …
jasonpaulos Mar 31, 2023
2801129
WIP
jasonpaulos Apr 14, 2023
f7b7777
Merge branch 'master' into eval-tracer-state-deltas
jasonpaulos Apr 19, 2023
d7fb785
Test AfterTxnGroup deltas
jasonpaulos Apr 21, 2023
a492003
Start to test AfterTxn deltas (granular eval)
jasonpaulos Apr 21, 2023
ed8ff74
TestTransactionGroupWithTracer working
jasonpaulos Apr 21, 2023
32f43b5
Testing from simulation package
jasonpaulos Apr 24, 2023
36c5427
Merge branch 'master' into eval-tracer-state-deltas
jasonpaulos Apr 24, 2023
f57040b
Fix race and simplify test table
jasonpaulos Apr 24, 2023
106aed5
Document statedelta methods
jasonpaulos Apr 24, 2023
9cb70e1
Merge branch 'master' into eval-tracer-state-deltas
jasonpaulos May 3, 2023
7ca0a1d
Remove StateDelta from simulation.Result
jasonpaulos May 3, 2023
1652a5c
Use StateDelta.Hydrate()
jasonpaulos May 3, 2023
b2f9af1
Ledger unit tests
jasonpaulos May 4, 2023
67cb81a
Dehydrate deltas before comparison
jasonpaulos May 4, 2023
955dcea
Merge branch 'master' into eval-tracer-state-deltas
jasonpaulos May 4, 2023
08eb749
Unit tests for hydrate/dehydrate
jasonpaulos May 4, 2023
ca3f65e
Allocate new cow for granular eval in Perform
jasonpaulos May 4, 2023
8371075
Merge branch 'master' into eval-tracer-state-deltas
jasonpaulos May 8, 2023
f87545e
Undo individual txn deltas
jasonpaulos May 8, 2023
d7cc4b9
Use Environment struct in simulation eval tests
jasonpaulos May 8, 2023
85cffdc
Merge branch 'master' into eval-tracer-state-deltas-tests
jasonpaulos May 9, 2023
f544699
Fix test expected block header
jasonpaulos May 9, 2023
f39564c
Use pointers in MergeAccount loops
jasonpaulos May 11, 2023
358d504
Merge branch 'master' into eval-tracer-state-deltas-tests
jasonpaulos May 11, 2023
762966b
Remove JSON printing
jasonpaulos May 12, 2023
a867d5a
Merge branch 'master' into eval-tracer-state-deltas-tests
jasonpaulos May 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
289 changes: 258 additions & 31 deletions data/transactions/logic/mocktracer/scenarios.go

Large diffs are not rendered by default.

57 changes: 54 additions & 3 deletions data/transactions/logic/mocktracer/tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@
package mocktracer

import (
"testing"

"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/ledger/ledgercore"
"github.com/algorand/go-algorand/protocol"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// EventType represents a type of logic.EvalTracer event
Expand Down Expand Up @@ -64,6 +68,9 @@ type Event struct {
// only for AfterTxn
TxnApplyData transactions.ApplyData

// only for AfterTxnGroup and AfterTxn
Deltas *ledgercore.StateDelta

// only for BeforeTxnGroup and AfterTxnGroup
GroupSize int

Expand All @@ -85,8 +92,8 @@ func BeforeTxnGroup(groupSize int) Event {
}

// AfterTxnGroup creates a new Event with the type AfterTxnGroupEvent
func AfterTxnGroup(groupSize int, hasError bool) Event {
return Event{Type: AfterTxnGroupEvent, GroupSize: groupSize, HasError: hasError}
func AfterTxnGroup(groupSize int, deltas *ledgercore.StateDelta, hasError bool) Event {
return Event{Type: AfterTxnGroupEvent, GroupSize: groupSize, Deltas: copyDeltas(deltas), HasError: hasError}
}

// BeforeProgram creates a new Event with the type BeforeProgramEvent
Expand Down Expand Up @@ -163,7 +170,7 @@ func (d *Tracer) BeforeTxnGroup(ep *logic.EvalParams) {

// AfterTxnGroup mocks the logic.EvalTracer.AfterTxnGroup method
func (d *Tracer) AfterTxnGroup(ep *logic.EvalParams, deltas *ledgercore.StateDelta, evalError error) {
d.Events = append(d.Events, AfterTxnGroup(len(ep.TxnGroup), evalError != nil))
d.Events = append(d.Events, AfterTxnGroup(len(ep.TxnGroup), deltas, evalError != nil))
}

// BeforeTxn mocks the logic.EvalTracer.BeforeTxn method
Expand Down Expand Up @@ -200,3 +207,47 @@ func (d *Tracer) AfterOpcode(cx *logic.EvalContext, evalError error) {
func (d *Tracer) AfterBlock(hdr *bookkeeping.BlockHeader) {
d.Events = append(d.Events, AfterBlock(hdr.Round))
}

// copyDeltas makes a deep copy of the given ledgercore.StateDelta pointer, if it's not nil.
// This is inefficient, but it should only be used for testing.
func copyDeltas(deltas *ledgercore.StateDelta) *ledgercore.StateDelta {
if deltas == nil {
return nil
}
encoded := protocol.EncodeReflect(deltas)
var clone ledgercore.StateDelta
err := protocol.DecodeReflect(encoded, &clone)
if err != nil {
panic(err)
}
return &clone
}

// AssertEventsEqual asserts that two slices of Events are equal, taking into account complex
// equality of StateDeltas. The arguments will be modified in-place to normalize any StateDeltas.
func AssertEventsEqual(t *testing.T, expected, actual []Event) {
t.Helper()

// Dehydrate deltas for better comparison
for i := range expected {
if expected[i].Deltas != nil {
expected[i].Deltas.Dehydrate()
}
jannotti marked this conversation as resolved.
Show resolved Hide resolved
}
for i := range actual {
if actual[i].Deltas != nil {
actual[i].Deltas.Dehydrate()
}
}

// These extra checks are not necessary for correctness, but they provide more targeted information on failure
if assert.Equal(t, len(expected), len(actual)) {
for i := range expected {
jsonExpectedDelta := protocol.EncodeJSONStrict(expected[i].Deltas)
jsonActualDelta := protocol.EncodeJSONStrict(actual[i].Deltas)
assert.Equal(t, expected[i].Deltas, actual[i].Deltas, "StateDelta disagreement: i=%d, event type: (%v,%v)\n\nexpected: %s\n\nactual: %s", i, expected[i].Type, actual[i].Type, jsonExpectedDelta, jsonActualDelta)
jannotti marked this conversation as resolved.
Show resolved Hide resolved
}
}

require.Equal(t, expected, actual)
}
45 changes: 45 additions & 0 deletions ledger/eval/cow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,51 @@ func TestCowBalance(t *testing.T) {
checkCowByUpdate(t, c0, updates2)
}

// TestCowDeltasAfterCommit tests that deltas are still valid after committing to parent.
func TestCowDeltasAfterCommit(t *testing.T) {
partitiontest.PartitionTest(t)
t.Parallel()

accts0 := ledgertesting.RandomAccounts(20, true)
ml := mockLedger{balanceMap: accts0}

c0 := makeRoundCowState(
&ml, bookkeeping.BlockHeader{}, config.Consensus[protocol.ConsensusCurrentVersion],
0, ledgercore.AccountTotals{}, 0)
checkCow(t, c0, accts0)

c1 := c0.child(0)

acctUpdates, _, _ := ledgertesting.RandomDeltas(10, accts0, 0)
applyUpdates(c1, acctUpdates)
acctUpdates.Dehydrate() // Prep for comparison

c1.kvPut("key", []byte("value"))
expectedKvMods := map[string]ledgercore.KvValueDelta{
"key": {
Data: []byte("value"),
},
}

actualDeltas := c1.deltas()
actualDeltas.Dehydrate() // Prep for comparison
require.Equal(t, acctUpdates, actualDeltas.Accts)
require.Equal(t, expectedKvMods, actualDeltas.KvMods)

// Parent should now have deltas
c1.commitToParent()
actualDeltas = c0.deltas()
actualDeltas.Dehydrate() // Prep for comparison
require.Equal(t, acctUpdates, actualDeltas.Accts)
require.Equal(t, expectedKvMods, actualDeltas.KvMods)

// Deltas remain valid in child after commit
actualDeltas = c0.deltas()
actualDeltas.Dehydrate() // Prep for comparison
require.Equal(t, acctUpdates, actualDeltas.Accts)
require.Equal(t, expectedKvMods, actualDeltas.KvMods)
}

func BenchmarkCowChild(b *testing.B) {
b.ReportAllocs()
cow := makeRoundCowState(nil, bookkeeping.BlockHeader{}, config.ConsensusParams{}, 10000, ledgercore.AccountTotals{}, 16)
Expand Down
157 changes: 144 additions & 13 deletions ledger/eval/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,13 +275,14 @@ func TestTransactionGroupWithTracer(t *testing.T) {
t.Parallel()
genesisInitState, addrs, keys := ledgertesting.Genesis(10)

basicAppID := basics.AppIndex(1001)
innerAppID := basics.AppIndex(1003)
innerAppAddress := innerAppID.Address()
balances := genesisInitState.Accounts
balances[innerAppAddress] = basics_testing.MakeAccountData(basics.Offline, basics.MicroAlgos{Raw: 1_000_000})

genesisBalances := bookkeeping.GenesisBalances{
Balances: genesisInitState.Accounts,
Balances: balances,
FeeSink: testSinkAddr,
RewardsPool: testPoolAddr,
Timestamp: 0,
Expand Down Expand Up @@ -343,7 +344,7 @@ int 1`,
// an app call with inner txn
innerAppCallTxn := txntest.Txn{
Type: protocol.ApplicationCallTx,
Sender: addrs[0],
Sender: addrs[4],
ClearStateProgram: `#pragma version 6
int 1`,

Expand All @@ -352,19 +353,50 @@ int 1`,
Fee: minFee,
GenesisHash: genHash,
}

expectedFeeSinkDataForScenario := ledgercore.ToAccountData(balances[testSinkAddr])
expectedFeeSinkDataForScenario.MicroAlgos.Raw += basicAppCallTxn.Txn().Fee.Raw
if testCase.firstTxnBehavior == "approve" {
expectedFeeSinkDataForScenario.MicroAlgos.Raw += payTxn.Txn().Fee.Raw
}

scenario := testCase.innerAppCallScenario(mocktracer.TestScenarioInfo{
CallingTxn: innerAppCallTxn.Txn(),
MinFee: minFee,
CreatedAppID: innerAppID,
CallingTxn: innerAppCallTxn.Txn(),
SenderData: ledgercore.ToAccountData(balances[addrs[4]]),
AppAccountData: ledgercore.ToAccountData(balances[innerAppAddress]),
FeeSinkData: expectedFeeSinkDataForScenario,
FeeSinkAddr: testSinkAddr,
MinFee: minFee,
CreatedAppID: innerAppID,
BlockHeader: eval.block.BlockHeader,
PrevTimestamp: blkHeader.TimeStamp,
})
innerAppCallTxn.ApprovalProgram = scenario.Program

txntest.Group(&basicAppCallTxn, &payTxn, &innerAppCallTxn)

// Update the expected state delta to reflect the inner app call txid
scenarioTxidValue, ok := scenario.ExpectedStateDelta.Txids[transactions.Txid{}]
if ok {
delete(scenario.ExpectedStateDelta.Txids, transactions.Txid{})
scenario.ExpectedStateDelta.Txids[innerAppCallTxn.Txn().ID()] = scenarioTxidValue
}
for i := range scenario.ExpectedEvents {
deltas := scenario.ExpectedEvents[i].Deltas
if deltas == nil {
continue
}
txidValue, ok := deltas.Txids[transactions.Txid{}]
if ok {
delete(deltas.Txids, transactions.Txid{})
deltas.Txids[innerAppCallTxn.Txn().ID()] = txidValue
}
}

txgroup := transactions.WrapSignedTxnsWithAD([]transactions.SignedTxn{
basicAppCallTxn.Txn().Sign(keys[0]),
payTxn.Txn().Sign(keys[1]),
innerAppCallTxn.Txn().Sign(keys[0]),
innerAppCallTxn.Txn().Sign(keys[4]),
})

require.Len(t, eval.block.Payset, 0)
Expand All @@ -388,7 +420,7 @@ int 1`,
}

expectedBasicAppCallAD := transactions.ApplyData{
ApplicationID: 1001,
ApplicationID: basicAppID,
EvalDelta: transactions.EvalDelta{
GlobalDelta: basics.StateDelta{},
LocalDeltas: map[uint64]basics.StateDelta{},
Expand All @@ -402,10 +434,107 @@ int 1`,
},
}

var expectedEvents = []mocktracer.Event{mocktracer.BeforeBlock(eval.block.Round())}
expectedFeeSinkData := ledgercore.ToAccountData(balances[testSinkAddr])
expectedFeeSinkData.MicroAlgos.Raw += txgroup[0].Txn.Fee.Raw
expectedAcct0Data := ledgercore.ToAccountData(balances[addrs[0]])
expectedAcct0Data.MicroAlgos.Raw -= txgroup[0].Txn.Fee.Raw
expectedAcct0Data.TotalAppParams = 1

expectedBlockHeader := eval.block.BlockHeader
expectedBasicAppCallDelta := ledgercore.StateDelta{
Accts: ledgercore.AccountDeltas{
Accts: []ledgercore.BalanceRecord{
{
Addr: addrs[0],
AccountData: expectedAcct0Data,
},
{
Addr: testSinkAddr,
AccountData: expectedFeeSinkData,
},
},
AppResources: []ledgercore.AppResourceRecord{
{
Aidx: basicAppID,
Addr: addrs[0],
Params: ledgercore.AppParamsDelta{
Params: &basics.AppParams{
ApprovalProgram: txgroup[0].Txn.ApprovalProgram,
ClearStateProgram: txgroup[0].Txn.ClearStateProgram,
},
},
},
},
},
Creatables: map[basics.CreatableIndex]ledgercore.ModifiedCreatable{
basics.CreatableIndex(basicAppID): {
Ctype: basics.AppCreatable,
Created: true,
Creator: addrs[0],
},
},
Txids: map[transactions.Txid]ledgercore.IncludedTransactions{
txgroup[0].Txn.ID(): {
LastValid: txgroup[0].Txn.LastValid,
Intra: 0,
},
},
Hdr: &expectedBlockHeader,
PrevTimestamp: blkHeader.TimeStamp,
}
expectedBasicAppCallDelta.Hydrate()

expectedEvents := []mocktracer.Event{mocktracer.BeforeBlock(eval.block.Round())}
if testCase.firstTxnBehavior == "approve" {
err = eval.endOfBlock()
require.NoError(t, err)

expectedAcct1Data := ledgercore.AccountData{}
expectedAcct2Data := ledgercore.ToAccountData(balances[addrs[2]])
expectedAcct2Data.MicroAlgos.Raw += payTxn.Amount
expectedAcct3Data := ledgercore.ToAccountData(balances[addrs[3]])
expectedAcct3Data.MicroAlgos.Raw += expectedPayTxnAD.ClosingAmount.Raw
expectedFeeSinkData.MicroAlgos.Raw += txgroup[1].Txn.Fee.Raw

expectedPayTxnDelta := ledgercore.StateDelta{
Accts: ledgercore.AccountDeltas{
Accts: []ledgercore.BalanceRecord{
{
Addr: addrs[1],
AccountData: expectedAcct1Data,
},
{
Addr: testSinkAddr,
AccountData: expectedFeeSinkData,
},
{
Addr: addrs[2],
AccountData: expectedAcct2Data,
},
{
Addr: addrs[3],
AccountData: expectedAcct3Data,
},
},
},
Txids: map[transactions.Txid]ledgercore.IncludedTransactions{
txgroup[1].Txn.ID(): {
LastValid: txgroup[1].Txn.LastValid,
Intra: 0, // will be incremented once merged
},
},
Hdr: &expectedBlockHeader,
PrevTimestamp: blkHeader.TimeStamp,
}
expectedPayTxnDelta.Hydrate()

expectedDelta := mocktracer.MergeStateDeltas(expectedBasicAppCallDelta, expectedPayTxnDelta, scenario.ExpectedStateDelta)

// If the scenario failed, we expect the failed txn ID to be removed from the group state delta
if scenario.Outcome != mocktracer.ApprovalOutcome {
delete(expectedDelta.Txids, txgroup[2].ID())
}

expectedEvents = append(expectedEvents, mocktracer.FlattenEvents([][]mocktracer.Event{
{
mocktracer.BeforeTxnGroup(3),
Expand All @@ -421,13 +550,14 @@ int 1`,
},
scenario.ExpectedEvents,
{
mocktracer.AfterTxnGroup(3, scenario.Outcome != mocktracer.ApprovalOutcome),
},
{
mocktracer.AfterTxnGroup(3, &expectedDelta, scenario.Outcome != mocktracer.ApprovalOutcome),
mocktracer.AfterBlock(eval.block.Round()),
},
})...)
} else {
// Removed failed txid from expected state delta
delete(expectedBasicAppCallDelta.Txids, txgroup[0].Txn.ID())

hasError := testCase.firstTxnBehavior == "error"
// EvalDeltas are removed from failed app call transactions
expectedBasicAppCallAD.EvalDelta = transactions.EvalDelta{}
Expand All @@ -441,11 +571,12 @@ int 1`,
{
mocktracer.AfterProgram(logic.ModeApp, hasError),
mocktracer.AfterTxn(protocol.ApplicationCallTx, expectedBasicAppCallAD, true), // end basicAppCallTxn
mocktracer.AfterTxnGroup(3, true),
mocktracer.AfterTxnGroup(3, &expectedBasicAppCallDelta, true),
},
})...)
}
require.Equal(t, expectedEvents, mocktracer.StripInnerTxnGroupIDsFromEvents(tracer.Events))
actualEvents := mocktracer.StripInnerTxnGroupIDsFromEvents(tracer.Events)
mocktracer.AssertEventsEqual(t, expectedEvents, actualEvents)
})
}
}
Expand Down
Loading