diff --git a/cmd/tealdbg/local.go b/cmd/tealdbg/local.go index 6fe2d006a2..24b2e13307 100644 --- a/cmd/tealdbg/local.go +++ b/cmd/tealdbg/local.go @@ -524,20 +524,38 @@ func (r *LocalRunner) RunAll() error { failed := 0 start := time.Now() - for _, run := range r.runs { - r.debugger.SaveProgram(run.name, run.program, run.source, run.offsetToLine, run.states) - + pooledApplicationBudget := uint64(0) + credit, _ := transactions.FeeCredit(r.txnGroup, r.proto.MinTxnFee) + // ignore error since fees are not important for debugging in most cases + + evalParams := make([]logic.EvalParams, len(r.runs)) + for i, run := range r.runs { + if run.mode == modeStateful { + if r.proto.EnableAppCostPooling { + pooledApplicationBudget += uint64(r.proto.MaxAppProgramCost) + } else { + pooledApplicationBudget = uint64(r.proto.MaxAppProgramCost) + } + } ep := logic.EvalParams{ - Proto: &r.proto, - Debugger: r.debugger, - Txn: &r.txnGroup[groupIndex], - TxnGroup: r.txnGroup, - GroupIndex: run.groupIndex, - PastSideEffects: run.pastSideEffects, - Specials: &transactions.SpecialAddresses{}, + Proto: &r.proto, + Debugger: r.debugger, + Txn: &r.txnGroup[groupIndex], + TxnGroup: r.txnGroup, + GroupIndex: run.groupIndex, + PastSideEffects: run.pastSideEffects, + Specials: &transactions.SpecialAddresses{}, + FeeCredit: &credit, + PooledApplicationBudget: &pooledApplicationBudget, } + evalParams[i] = ep + } + + for i := range r.runs { + run := &r.runs[i] + r.debugger.SaveProgram(run.name, run.program, run.source, run.offsetToLine, run.states) - run.result.pass, run.result.err = run.eval(ep) + run.result.pass, run.result.err = run.eval(evalParams[i]) if run.result.err != nil { failed++ } @@ -555,25 +573,44 @@ func (r *LocalRunner) Run() (bool, error) { return false, fmt.Errorf("no program to debug") } - run := r.runs[0] + pooledApplicationBudget := uint64(0) + credit, _ := transactions.FeeCredit(r.txnGroup, r.proto.MinTxnFee) + // ignore error since fees are not important for debugging in most cases - ep := logic.EvalParams{ - Proto: &r.proto, - Txn: &r.txnGroup[groupIndex], - TxnGroup: r.txnGroup, - GroupIndex: run.groupIndex, - PastSideEffects: run.pastSideEffects, - Specials: &transactions.SpecialAddresses{}, - } + evalParams := make([]logic.EvalParams, len(r.runs)) + for i, run := range r.runs { + if run.mode == modeStateful { + if r.proto.EnableAppCostPooling { + pooledApplicationBudget += uint64(r.proto.MaxAppProgramCost) + } else { + pooledApplicationBudget = uint64(r.proto.MaxAppProgramCost) + } + } + ep := logic.EvalParams{ + Proto: &r.proto, + Txn: &r.txnGroup[groupIndex], + TxnGroup: r.txnGroup, + GroupIndex: run.groupIndex, + PastSideEffects: run.pastSideEffects, + Specials: &transactions.SpecialAddresses{}, + FeeCredit: &credit, + PooledApplicationBudget: &pooledApplicationBudget, + } - // Workaround for Go's nil/empty interfaces nil check after nil assignment, i.e. - // r.debugger = nil - // ep.Debugger = r.debugger - // if ep.Debugger != nil // FALSE - if r.debugger != nil { - r.debugger.SaveProgram(run.name, run.program, run.source, run.offsetToLine, run.states) - ep.Debugger = r.debugger + // Workaround for Go's nil/empty interfaces nil check after nil assignment, i.e. + // r.debugger = nil + // ep.Debugger = r.debugger + // if ep.Debugger != nil // FALSE + if r.debugger != nil { + r.debugger.SaveProgram(run.name, run.program, run.source, run.offsetToLine, run.states) + ep.Debugger = r.debugger + } + + evalParams[i] = ep } + run := r.runs[0] + ep := evalParams[0] + return run.eval(ep) } diff --git a/cmd/tealdbg/local_test.go b/cmd/tealdbg/local_test.go index d4d917a402..ad7faa605e 100644 --- a/cmd/tealdbg/local_test.go +++ b/cmd/tealdbg/local_test.go @@ -18,6 +18,7 @@ package main import ( "encoding/json" + "fmt" "net/http" "net/http/httptest" "reflect" @@ -226,14 +227,6 @@ func makeSampleBalanceRecord(addr basics.Address, assetIdx basics.AssetIndex, ap return br } -func makeSampleSerializedBalanceRecord(addr basics.Address, toJSON bool) []byte { - br := makeSampleBalanceRecord(addr, 50, 100) - if toJSON { - return protocol.EncodeJSON(&br) - } - return protocol.EncodeMsgp(&br) -} - func TestBalanceJSONInput(t *testing.T) { partitiontest.PartitionTest(t) a := require.New(t) @@ -325,7 +318,7 @@ func TestDebugEnvironment(t *testing.T) { Txn: transactions.Transaction{ Header: transactions.Header{ Sender: sender, - Fee: basics.MicroAlgos{Raw: 100}, + Fee: basics.MicroAlgos{Raw: 1000}, Note: []byte{1, 2, 3}, }, ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ @@ -1087,7 +1080,7 @@ func TestDebugTxSubmit(t *testing.T) { Type: protocol.ApplicationCallTx, Header: transactions.Header{ Sender: sender, - Fee: basics.MicroAlgos{Raw: 100}, + Fee: basics.MicroAlgos{Raw: 1000}, Note: []byte{1, 2, 3}, }, ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ @@ -1138,3 +1131,222 @@ int 1` a.NoError(err) a.True(pass) } + +func TestDebugFeePooling(t *testing.T) { + partitiontest.PartitionTest(t) + a := require.New(t) + + sender, err := basics.UnmarshalChecksumAddress("47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU") + a.NoError(err) + + source := `#pragma version 5 +itxn_begin +int pay +itxn_field TypeEnum +int 0 +itxn_field Amount +txn Sender +itxn_field Receiver +itxn_submit +int 1` + + ops, err := logic.AssembleString(source) + a.NoError(err) + prog := ops.Program + + stxn := transactions.SignedTxn{ + Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: transactions.Header{ + Sender: sender, + Note: []byte{1, 2, 3}, + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: 0, + ApprovalProgram: prog, + ClearStateProgram: prog, + }, + }, + } + + appIdx := basics.AppIndex(1) + br := basics.BalanceRecord{ + Addr: sender, + AccountData: basics.AccountData{ + MicroAlgos: basics.MicroAlgos{Raw: 5000000}, + AppParams: map[basics.AppIndex]basics.AppParams{ + appIdx: { + ApprovalProgram: prog, + ClearStateProgram: prog, + }, + }, + }, + } + balanceBlob := protocol.EncodeMsgp(&br) + + // two testcase: success with enough fees and fail otherwise + var tests = []struct { + pass bool + fee uint64 + }{ + {true, 2000}, + {false, 1500}, + } + for _, test := range tests { + t.Run(fmt.Sprintf("fee=%d", test.fee), func(t *testing.T) { + + stxn.Txn.Fee = basics.MicroAlgos{Raw: test.fee} + encoded := protocol.EncodeJSON(&stxn) + + ds := DebugParams{ + ProgramNames: []string{"test"}, + BalanceBlob: balanceBlob, + TxnBlob: encoded, + Proto: string(protocol.ConsensusCurrentVersion), + Round: 222, + LatestTimestamp: 333, + GroupIndex: 0, + RunMode: "application", + AppID: uint64(appIdx), + } + + local := MakeLocalRunner(nil) + err = local.Setup(&ds) + a.NoError(err) + + pass, err := local.Run() + if test.pass { + a.NoError(err) + a.True(pass) + } else { + a.Error(err) + a.False(pass) + } + }) + } +} + +func TestDebugCostPooling(t *testing.T) { + partitiontest.PartitionTest(t) + a := require.New(t) + + sender, err := basics.UnmarshalChecksumAddress("47YPQTIGQEO7T4Y4RWDYWEKV6RTR2UNBQXBABEEGM72ESWDQNCQ52OPASU") + a.NoError(err) + + // cost is 2000 (ecdsa_pk_recover) + 130 (keccak256) + 8 (rest opcodes) + // needs 4 app calls to pool together + source := `#pragma version 5 +byte "hello from ethereum" // msg +keccak256 +int 0 // v +byte 0x745e8f55ac6189ee89ed707c36694868e3903988fbf776c8096c45da2e60c638 // r +byte 0x30c8e4a9b5d2eb53ddc6294587dd00bed8afe2c45dd72f6b4cf752e46d5ba681 // s +ecdsa_pk_recover Secp256k1 +concat // convert public key X and Y to ethereum addr +keccak256 +substring 12 32 +byte 0x5ce9454909639d2d17a3f753ce7d93fa0b9ab12e // addr +== +` + ops, err := logic.AssembleString(source) + a.NoError(err) + prog := ops.Program + + ops, err = logic.AssembleString("#pragma version 2\nint 1") + a.NoError(err) + trivial := ops.Program + + stxn := transactions.SignedTxn{ + Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: transactions.Header{ + Sender: sender, + Fee: basics.MicroAlgos{Raw: 1000}, + Note: []byte{1, 2, 3}, + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: 0, + ApprovalProgram: prog, + ClearStateProgram: trivial, + }, + }, + } + + appIdx := basics.AppIndex(1) + trivialAppIdx := basics.AppIndex(2) + trivialStxn := transactions.SignedTxn{ + Txn: transactions.Transaction{ + Type: protocol.ApplicationCallTx, + Header: transactions.Header{ + Sender: sender, + Fee: basics.MicroAlgos{Raw: 1000}, + }, + ApplicationCallTxnFields: transactions.ApplicationCallTxnFields{ + ApplicationID: trivialAppIdx, + }, + }, + } + + br := basics.BalanceRecord{ + Addr: sender, + AccountData: basics.AccountData{ + MicroAlgos: basics.MicroAlgos{Raw: 5000000}, + AppParams: map[basics.AppIndex]basics.AppParams{ + appIdx: { + ApprovalProgram: prog, + ClearStateProgram: trivial, + }, + trivialAppIdx: { + ApprovalProgram: trivial, + ClearStateProgram: trivial, + }, + }, + }, + } + balanceBlob := protocol.EncodeMsgp(&br) + + var tests = []struct { + pass bool + additionalApps int + }{ + {false, 2}, + {true, 3}, + } + for _, test := range tests { + t.Run(fmt.Sprintf("txn-count=%d", test.additionalApps+1), func(t *testing.T) { + txnBlob := protocol.EncodeMsgp(&stxn) + for i := 0; i < test.additionalApps; i++ { + val, err := getRandomAddress() + a.NoError(err) + trivialStxn.Txn.Note = val[:] + txnBlob = append(txnBlob, protocol.EncodeMsgp(&trivialStxn)...) + } + + ds := DebugParams{ + ProgramNames: []string{"test"}, + BalanceBlob: balanceBlob, + TxnBlob: txnBlob, + Proto: string(protocol.ConsensusCurrentVersion), + Round: 222, + LatestTimestamp: 333, + GroupIndex: 0, + RunMode: "application", + AppID: uint64(appIdx), + } + + local := MakeLocalRunner(nil) + err = local.Setup(&ds) + a.NoError(err) + + pass, err := local.Run() + if test.pass { + a.NoError(err) + a.True(pass) + } else { + a.Error(err) + a.Contains(err.Error(), "dynamic cost budget exceeded") + a.False(pass) + } + }) + } +}