diff --git a/daemon/algod/api/server/v2/test/handlers_test.go b/daemon/algod/api/server/v2/test/handlers_test.go index e2278c505b..ed9b8d83bc 100644 --- a/daemon/algod/api/server/v2/test/handlers_test.go +++ b/daemon/algod/api/server/v2/test/handlers_test.go @@ -2077,7 +2077,7 @@ func TestDeltasForTxnGroup(t *testing.T) { blk1 := bookkeeping.BlockHeader{Round: 1} blk2 := bookkeeping.BlockHeader{Round: 2} delta1 := ledgercore.StateDelta{Hdr: &blk1} - delta2 := ledgercore.StateDelta{Hdr: &blk2} + delta2 := ledgercore.StateDelta{Hdr: &blk2, KvMods: map[string]ledgercore.KvValueDelta{"bx1": {Data: []byte("foobar")}}} txn1 := transactions.SignedTxnWithAD{SignedTxn: transactions.SignedTxn{Txn: transactions.Transaction{Type: protocol.PaymentTx}}} groupID1, err := crypto.DigestFromString(crypto.Hash([]byte("hello")).String()) require.NoError(t, err) @@ -2160,6 +2160,7 @@ func TestDeltasForTxnGroup(t *testing.T) { require.NoError(t, err) err = json.Unmarshal(rec.Body.Bytes(), &groupResponse) require.NoError(t, err) + require.NotNil(t, groupResponse["KvMods"]) groupHdr, ok = groupResponse["Hdr"].(map[string]interface{}) require.True(t, ok) require.Equal(t, delta2.Hdr.Round, basics.Round(groupHdr["rnd"].(float64))) diff --git a/ledger/eval/eval_test.go b/ledger/eval/eval_test.go index aa16634d2f..84de779ec1 100644 --- a/ledger/eval/eval_test.go +++ b/ledger/eval/eval_test.go @@ -600,6 +600,7 @@ type evalTestLedger struct { rewardsPool basics.Address latestTotals ledgercore.AccountTotals tracer logic.EvalTracer + boxes map[string][]byte } // newTestLedger creates a in memory Ledger that is as realistic as @@ -611,6 +612,7 @@ func newTestLedger(t testing.TB, balances bookkeeping.GenesisBalances) *evalTest feeSink: balances.FeeSink, rewardsPool: balances.RewardsPool, tracer: nil, + boxes: make(map[string][]byte), } protoVersion := protocol.ConsensusFuture @@ -728,7 +730,9 @@ func (ledger *evalTestLedger) LookupAsset(rnd basics.Round, addr basics.Address, } func (ledger *evalTestLedger) LookupKv(rnd basics.Round, key string) ([]byte, error) { - panic("unimplemented") + // The test ledger only has one view of the value of a box--no rnd based retrieval is implemented currently + val, _ := ledger.boxes[key] + return val, nil } // GenesisHash returns the genesis hash for this ledger. diff --git a/ledger/eval/txntracer.go b/ledger/eval/txntracer.go index f1ce6b2e1d..036ad773de 100644 --- a/ledger/eval/txntracer.go +++ b/ledger/eval/txntracer.go @@ -48,12 +48,50 @@ type StateDeltaSubset struct { } func convertStateDelta(delta ledgercore.StateDelta) StateDeltaSubset { + // The StateDelta object returned through the EvalTracer has its values deleted between txn groups to avoid + // reallocation during evaluation. + // This means the map values need to be copied (to avoid deletion) since they are all passed by reference. + kvmods := make(map[string]ledgercore.KvValueDelta, len(delta.KvMods)) + for k1, v1 := range delta.KvMods { + kvmods[k1] = v1 + } + txids := make(map[transactions.Txid]ledgercore.IncludedTransactions, len(delta.Txids)) + for k2, v2 := range delta.Txids { + txids[k2] = v2 + } + txleases := make(map[ledgercore.Txlease]basics.Round, len(delta.Txleases)) + for k3, v3 := range delta.Txleases { + txleases[k3] = v3 + } + creatables := make(map[basics.CreatableIndex]ledgercore.ModifiedCreatable, len(delta.Creatables)) + for k4, v4 := range delta.Creatables { + creatables[k4] = v4 + } + var accR []ledgercore.BalanceRecord + var appR []ledgercore.AppResourceRecord + var assetR []ledgercore.AssetResourceRecord + if len(delta.Accts.Accts) > 0 { + accR = make([]ledgercore.BalanceRecord, len(delta.Accts.Accts)) + copy(accR, delta.Accts.Accts) + } + if len(delta.Accts.AppResources) > 0 { + appR = make([]ledgercore.AppResourceRecord, len(delta.Accts.AppResources)) + copy(appR, delta.Accts.AppResources) + } + if len(delta.Accts.AssetResources) > 0 { + assetR = make([]ledgercore.AssetResourceRecord, len(delta.Accts.AssetResources)) + copy(assetR, delta.Accts.AssetResources) + } return StateDeltaSubset{ - Accts: delta.Accts, - KvMods: delta.KvMods, - Txids: delta.Txids, - Txleases: delta.Txleases, - Creatables: delta.Creatables, + Accts: ledgercore.AccountDeltas{ + Accts: accR, + AppResources: appR, + AssetResources: assetR, + }, + KvMods: kvmods, + Txids: txids, + Txleases: txleases, + Creatables: creatables, Hdr: delta.Hdr, } } @@ -65,8 +103,8 @@ type TxnGroupDeltaTracer struct { lookback uint64 // no-op methods we don't care about logic.NullEvalTracer - // txnGroupDeltas stores the StateDelta objects for each round, indexed by all the IDs within the group - txnGroupDeltas map[basics.Round]map[crypto.Digest]*ledgercore.StateDelta + // txnGroupDeltas stores the StateDeltaSubset objects for each round, indexed by all the IDs within the group + txnGroupDeltas map[basics.Round]map[crypto.Digest]*StateDeltaSubset // latestRound is the most recent round seen via the BeforeBlock hdr latestRound basics.Round } @@ -75,7 +113,7 @@ type TxnGroupDeltaTracer struct { func MakeTxnGroupDeltaTracer(lookback uint64) *TxnGroupDeltaTracer { return &TxnGroupDeltaTracer{ lookback: lookback, - txnGroupDeltas: make(map[basics.Round]map[crypto.Digest]*ledgercore.StateDelta), + txnGroupDeltas: make(map[basics.Round]map[crypto.Digest]*StateDeltaSubset), } } @@ -87,7 +125,7 @@ func (tracer *TxnGroupDeltaTracer) BeforeBlock(hdr *bookkeeping.BlockHeader) { delete(tracer.txnGroupDeltas, hdr.Round-basics.Round(tracer.lookback)) tracer.latestRound = hdr.Round // Initialize the delta map for the round - tracer.txnGroupDeltas[tracer.latestRound] = make(map[crypto.Digest]*ledgercore.StateDelta) + tracer.txnGroupDeltas[tracer.latestRound] = make(map[crypto.Digest]*StateDeltaSubset) } // AfterTxnGroup implements the EvalTracer interface for txn group boundaries @@ -95,16 +133,17 @@ func (tracer *TxnGroupDeltaTracer) AfterTxnGroup(ep *logic.EvalParams, deltas *l if deltas == nil { return } + deltaSub := convertStateDelta(*deltas) tracer.deltasLock.Lock() defer tracer.deltasLock.Unlock() txnDeltaMap := tracer.txnGroupDeltas[tracer.latestRound] for _, txn := range ep.TxnGroup { // Add Group ID if !txn.Txn.Group.IsZero() { - txnDeltaMap[txn.Txn.Group] = deltas + txnDeltaMap[txn.Txn.Group] = &deltaSub } // Add Txn ID - txnDeltaMap[crypto.Digest(txn.ID())] = deltas + txnDeltaMap[crypto.Digest(txn.ID())] = &deltaSub } } @@ -117,7 +156,7 @@ func (tracer *TxnGroupDeltaTracer) GetDeltasForRound(rnd basics.Round) ([]TxnGro return nil, fmt.Errorf("round %d not found in txnGroupDeltaTracer", rnd) } // Dedupe entries in our map and collect Ids - var deltas = map[*ledgercore.StateDelta][]string{} + var deltas = map[*StateDeltaSubset][]string{} for id, delta := range rndEntries { if _, present := deltas[delta]; !present { deltas[delta] = append(deltas[delta], id.String()) @@ -127,19 +166,19 @@ func (tracer *TxnGroupDeltaTracer) GetDeltasForRound(rnd basics.Round) ([]TxnGro for delta, ids := range deltas { deltasForRound = append(deltasForRound, TxnGroupDeltaWithIds{ Ids: ids, - Delta: convertStateDelta(*delta), + Delta: *delta, }) } return deltasForRound, nil } -// GetDeltaForID retruns the StateDelta associated with the group of transaction executed for the supplied ID (txn or group) +// GetDeltaForID returns the StateDelta associated with the group of transaction executed for the supplied ID (txn or group) func (tracer *TxnGroupDeltaTracer) GetDeltaForID(id crypto.Digest) (StateDeltaSubset, error) { tracer.deltasLock.RLock() defer tracer.deltasLock.RUnlock() for _, deltasForRound := range tracer.txnGroupDeltas { if delta, exists := deltasForRound[id]; exists { - return convertStateDelta(*delta), nil + return *delta, nil } } return StateDeltaSubset{}, fmt.Errorf("unable to find delta for id: %s", id) diff --git a/ledger/eval/txntracer_test.go b/ledger/eval/txntracer_test.go index 8b60a0c090..4711709c68 100644 --- a/ledger/eval/txntracer_test.go +++ b/ledger/eval/txntracer_test.go @@ -27,6 +27,7 @@ import ( basics_testing "github.com/algorand/go-algorand/data/basics/testing" "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" @@ -75,8 +76,13 @@ func TestTransactionGroupWithDeltaTracer(t *testing.T) { innerAppID := basics.AppIndex(3) + offset innerAppAddress := innerAppID.Address() + appID := basics.AppIndex(1) + offset + appAddress := appID.Address() + innerBoxAppID := basics.AppIndex(7) + offset + innerBoxAppAddress := innerBoxAppID.Address() balances := genesisInitState.Accounts balances[innerAppAddress] = basics_testing.MakeAccountData(basics.Offline, basics.MicroAlgos{Raw: 1_000_000}) + balances[appAddress] = basics_testing.MakeAccountData(basics.Offline, basics.MicroAlgos{Raw: 1_000_000}) genesisBalances := bookkeeping.GenesisBalances{ Balances: genesisInitState.Accounts, @@ -95,24 +101,38 @@ func TestTransactionGroupWithDeltaTracer(t *testing.T) { eval.generate = true genHash := l.GenesisHash() + basicAppCallApproval := `#pragma version 8 +byte "hellobox" +int 10 +box_create +pop +int 1` + basicAppCallClear := `#pragma version 8 +int 1` + basicAppCallClearOps, err := logic.AssembleString(basicAppCallClear) + require.NoError(t, err) + basicAppCallApprovalOps, err := logic.AssembleString(basicAppCallApproval) + require.NoError(t, err) // a basic app call basicAppCallTxn := txntest.Txn{ - Type: protocol.ApplicationCallTx, - Sender: addrs[0], - ApprovalProgram: `#pragma version 6 -byte "hello" -log -int 1`, - ClearStateProgram: `#pragma version 6 -int 1`, - FirstValid: newBlock.Round(), - LastValid: newBlock.Round() + 1000, - Fee: minFee, - GenesisHash: genHash, - Note: []byte("one"), + Type: protocol.ApplicationCallTx, + Sender: addrs[0], + ApprovalProgram: basicAppCallApproval, + ClearStateProgram: basicAppCallClear, + FirstValid: newBlock.Round(), + LastValid: newBlock.Round() + 1000, + Fee: minFee, + GenesisHash: genHash, + Note: []byte("one"), + Boxes: []transactions.BoxRef{{ + Index: 0, + Name: []byte("hellobox"), + }}, } // a non-app call txn + var txnLease [32]byte + copy(txnLease[:], "txnLeaseTest") payTxn := txntest.Txn{ Type: protocol.PaymentTx, Sender: addrs[1], @@ -124,18 +144,22 @@ int 1`, Fee: minFee, GenesisHash: genHash, Note: []byte("two"), + Lease: txnLease, } // an app call with inner txn + v6Clear := `#pragma version 6 +int 1` + v6ClearOps, err := logic.AssembleString(v6Clear) + require.NoError(t, err) innerAppCallTxn := txntest.Txn{ - Type: protocol.ApplicationCallTx, - Sender: addrs[0], - ClearStateProgram: `#pragma version 6 -int 1`, - FirstValid: newBlock.Round(), - LastValid: newBlock.Round() + 1000, - Fee: minFee, - GenesisHash: genHash, - Note: []byte("three"), + Type: protocol.ApplicationCallTx, + Sender: addrs[0], + ClearStateProgram: v6Clear, + FirstValid: newBlock.Round(), + LastValid: newBlock.Round() + 1000, + Fee: minFee, + GenesisHash: genHash, + Note: []byte("three"), } scenario := testCase.innerAppCallScenario(mocktracer.TestScenarioInfo{ CallingTxn: innerAppCallTxn.Txn(), @@ -143,20 +167,70 @@ int 1`, CreatedAppID: innerAppID, }) innerAppCallTxn.ApprovalProgram = scenario.Program + innerAppCallApprovalOps, err := logic.AssembleString(scenario.Program) + require.NoError(t, err) - txntest.Group(&basicAppCallTxn, &payTxn, &innerAppCallTxn) + // inner txn with more box mods + innerAppCallBoxApproval := `#pragma version 8 +byte "goodbyebox" +int 10 +box_create +pop +byte "goodbyebox" +int 0 +byte "2" +box_replace +byte "goodbyebox" +box_del +pop +int 1` + innerAppCallBoxApprovalOps, err := logic.AssembleString(innerAppCallBoxApproval) + require.NoError(t, err) + innerAppCallBoxTxn := txntest.Txn{ + Type: protocol.ApplicationCallTx, + Sender: addrs[0], + ApprovalProgram: innerAppCallBoxApproval, + ClearStateProgram: basicAppCallClear, + FirstValid: newBlock.Round(), + LastValid: newBlock.Round() + 1000, + Fee: minFee, + GenesisHash: genHash, + Boxes: []transactions.BoxRef{{ + Index: 0, + Name: []byte("goodbyebox"), + }}, + } + + txntest.Group(&basicAppCallTxn, &payTxn, &innerAppCallTxn, &innerAppCallBoxTxn) txgroup := transactions.WrapSignedTxnsWithAD([]transactions.SignedTxn{ basicAppCallTxn.Txn().Sign(keys[0]), payTxn.Txn().Sign(keys[1]), innerAppCallTxn.Txn().Sign(keys[0]), + innerAppCallBoxTxn.Txn().Sign(keys[0]), }) require.Len(t, eval.block.Payset, 0) err = eval.TransactionGroup(txgroup) require.NoError(t, err) - require.Len(t, eval.block.Payset, 3) + require.Len(t, eval.block.Payset, 4) + + secondPayTxn := txntest.Txn{ + Type: protocol.PaymentTx, + Sender: addrs[2], + Receiver: addrs[1], + Amount: 100_000, + FirstValid: newBlock.Round(), + LastValid: newBlock.Round() + 1000, + Fee: minFee, + GenesisHash: genHash, + } + secondTxGroup := transactions.WrapSignedTxnsWithAD([]transactions.SignedTxn{ + secondPayTxn.Txn().Sign(keys[2]), + }) + err = eval.TransactionGroup(secondTxGroup) + require.NoError(t, err) expectedAccts := ledgercore.AccountDeltas{ Accts: []ledgercore.BalanceRecord{ @@ -164,8 +238,8 @@ int 1`, Addr: addrs[0], AccountData: ledgercore.AccountData{ AccountBaseData: ledgercore.AccountBaseData{ - MicroAlgos: basics.MicroAlgos{Raw: 1666666666664666}, - TotalAppParams: 2, + MicroAlgos: basics.MicroAlgos{Raw: 1666666666663666}, + TotalAppParams: 3, }, }, }, @@ -174,7 +248,17 @@ int 1`, AccountData: ledgercore.AccountData{ AccountBaseData: ledgercore.AccountBaseData{ Status: basics.Status(2), - MicroAlgos: basics.MicroAlgos{Raw: 1666666666672666}, + MicroAlgos: basics.MicroAlgos{Raw: 1666666666673666}, + }, + }, + }, + { + Addr: appAddress, + AccountData: ledgercore.AccountData{ + AccountBaseData: ledgercore.AccountBaseData{ + MicroAlgos: basics.MicroAlgos{Raw: 1000000}, + TotalBoxes: 1, + TotalBoxBytes: 18, }, }, }, @@ -206,6 +290,10 @@ int 1`, }, }, }, + { + Addr: innerBoxAppAddress, + AccountData: ledgercore.AccountData{}, + }, }, AppResources: []ledgercore.AppResourceRecord{ { @@ -213,8 +301,8 @@ int 1`, Addr: addrs[0], Params: ledgercore.AppParamsDelta{ Params: &basics.AppParams{ - ApprovalProgram: []byte{0x06, 0x80, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xb0, 0x81, 0x01}, - ClearStateProgram: []byte{0x06, 0x81, 0x01}, + ApprovalProgram: basicAppCallApprovalOps.Program, + ClearStateProgram: basicAppCallClearOps.Program, }, }, }, @@ -223,12 +311,8 @@ int 1`, Addr: addrs[0], Params: ledgercore.AppParamsDelta{ Params: &basics.AppParams{ - ApprovalProgram: []byte{0x06, 0x80, 0x01, 0x61, 0xb0, 0xb1, 0x81, 0x06, 0xb2, 0x10, 0x81, 0x00, 0xb2, 0x19, 0x80, 0x07, - 0x06, 0x80, 0x01, 0x78, 0xb0, 0x81, 0x01, 0xb2, 0x1e, 0x80, 0x03, 0x06, 0x81, 0x01, 0xb2, 0x1f, - 0xb3, 0x80, 0x01, 0x62, 0xb0, 0xb1, 0x81, 0x01, 0xb2, 0x10, 0x81, 0x01, 0xb2, 0x08, 0x32, 0x0a, - 0xb2, 0x07, 0xb6, 0x81, 0x01, 0xb2, 0x10, 0x81, 0x02, 0xb2, 0x08, 0x32, 0x0a, 0xb2, 0x07, 0xb3, - 0x80, 0x01, 0x63, 0xb0, 0x81, 0x01}, - ClearStateProgram: []byte{0x06, 0x81, 0x01}, + ApprovalProgram: innerAppCallApprovalOps.Program, + ClearStateProgram: v6ClearOps.Program, }, }, }, @@ -237,12 +321,35 @@ int 1`, Addr: innerAppAddress, Params: ledgercore.AppParamsDelta{ Params: &basics.AppParams{ - ApprovalProgram: []byte{0x06, 0x80, 0x01, 0x78, 0xb0, 0x81, 0x01}, - ClearStateProgram: []byte{0x06, 0x81, 0x01}, + ApprovalProgram: []byte{0x06, 0x80, 0x01, 0x78, 0xb0, 0x81, 0x01}, // #pragma version 6; pushbytes "x"; log; pushint 1 + ClearStateProgram: v6ClearOps.Program, }, }, }, + { + Aidx: innerBoxAppID, + Addr: addrs[0], + Params: ledgercore.AppParamsDelta{ + Params: &basics.AppParams{ + ApprovalProgram: innerAppCallBoxApprovalOps.Program, + ClearStateProgram: basicAppCallClearOps.Program, + }, + }, + }, + }, + } + expectedKvMods := map[string]ledgercore.KvValueDelta{ + "bx:\x00\x00\x00\x00\x00\x00\x03\xe9hellobox": { + OldData: []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Data: []uint8{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, }, + "bx:\x00\x00\x00\x00\x00\x00\x03\xefgoodbyebox": { + OldData: nil, + Data: nil, + }, + } + expectedLeases := map[ledgercore.Txlease]basics.Round{ + {Sender: payTxn.Sender, Lease: payTxn.Lease}: payTxn.LastValid, } actualDelta, err := tracer.GetDeltaForID(crypto.Digest(txgroup[0].ID())) @@ -253,11 +360,13 @@ int 1`, require.NoError(t, err) allDeltas, err := tracer.GetDeltasForRound(basics.Round(1)) require.NoError(t, err) - require.Len(t, allDeltas, 1) + require.Len(t, allDeltas, 2) require.Equal(t, expectedAccts.Accts, actualDelta.Accts.Accts) require.Equal(t, expectedAccts.AppResources, actualDelta.Accts.AppResources) require.Equal(t, expectedAccts.AssetResources, actualDelta.Accts.AssetResources) + require.Equal(t, expectedKvMods, actualDelta.KvMods) + require.Equal(t, expectedLeases, actualDelta.Txleases) }) } }