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

feat(api/debug): support debug trace without blockId #905

Merged
merged 5 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
133 changes: 85 additions & 48 deletions api/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,8 @@ func New(
}
}

func (d *Debug) prepareClauseEnv(ctx context.Context, blockID thor.Bytes32, txIndex uint64, clauseIndex uint32) (*runtime.Runtime, *runtime.TransactionExecutor, thor.Bytes32, error) {
block, err := d.repo.GetBlock(blockID)
if err != nil {
if d.repo.IsNotFound(err) {
return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("block not found"))
}
return nil, nil, thor.Bytes32{}, err
}
txs := block.Transactions()
if txIndex >= uint64(len(txs)) {
return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("tx index out of range"))
}
txID := txs[txIndex].ID()
if clauseIndex >= uint32(len(txs[txIndex].Clauses())) {
return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("clause index out of range"))
}
// prepareClauseEnv prepares the runtime environment for the specified clause.
func (d *Debug) prepareClauseEnv(ctx context.Context, block *block.Block, txID thor.Bytes32, clauseIndex uint32) (*runtime.Runtime, *runtime.TransactionExecutor, thor.Bytes32, error) {
rt, err := consensus.New(
d.repo,
d.stater,
Expand All @@ -99,17 +85,29 @@ func (d *Debug) prepareClauseEnv(ctx context.Context, blockID thor.Bytes32, txIn
if err != nil {
return nil, nil, thor.Bytes32{}, err
}
for i, tx := range txs {
if uint64(i) > txIndex {
break

var found bool
txs := block.Transactions()
for _, tx := range txs {
if txID == tx.ID() {
found = true
if clauseIndex >= uint32(len(tx.Clauses())) {
return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("clause index out of range"))
}
}
}
if !found {
return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("transaction not found"))
}

for _, tx := range block.Transactions() {
txExec, err := rt.PrepareTransaction(tx)
if err != nil {
return nil, nil, thor.Bytes32{}, err
}
clauseCounter := uint32(0)
for txExec.HasNextClause() {
if txIndex == uint64(i) && clauseIndex == clauseCounter {
if tx.ID() == txID && clauseIndex == clauseCounter {
return rt, txExec, txID, nil
}
exec, _ := txExec.PrepareNext()
Expand All @@ -127,18 +125,27 @@ func (d *Debug) prepareClauseEnv(ctx context.Context, blockID thor.Bytes32, txIn
default:
}
}

// no env created, that means tx was reverted at an early clause
return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("early reverted"))
}

// trace an existed clause
func (d *Debug) traceClause(ctx context.Context, tracer tracers.Tracer, blockID thor.Bytes32, txIndex uint64, clauseIndex uint32) (interface{}, error) {
rt, txExec, txID, err := d.prepareClauseEnv(ctx, blockID, txIndex, clauseIndex)
func (d *Debug) traceClause(ctx context.Context, tracer tracers.Tracer, block *block.Block, txID thor.Bytes32, clauseIndex uint32) (interface{}, error) {
rt, txExec, txID, err := d.prepareClauseEnv(ctx, block, txID, clauseIndex)
if err != nil {
return nil, err
}

var txIndex uint64 = math.MaxUint64
for i, tx := range block.Transactions() {
if tx.ID() == txID {
txIndex = uint64(i)
break
}
}
tracer.SetContext(&tracers.Context{
BlockID: blockID,
BlockID: block.Header().ID(),
BlockTime: rt.Context().Time,
TxID: txID,
TxIndex: txIndex,
Expand Down Expand Up @@ -178,11 +185,11 @@ func (d *Debug) handleTraceClause(w http.ResponseWriter, req *http.Request) erro
return utils.Forbidden(err)
}

blockID, txIndex, clauseIndex, err := d.parseTarget(opt.Target)
block, txID, clauseIndex, err := d.parseTarget(opt.Target)
if err != nil {
return err
}
res, err := d.traceClause(req.Context(), tracer, blockID, txIndex, clauseIndex)
res, err := d.traceClause(req.Context(), tracer, block, txID, clauseIndex)
if err != nil {
return err
}
Expand Down Expand Up @@ -291,8 +298,8 @@ func (d *Debug) traceCall(ctx context.Context, tracer tracers.Tracer, header *bl
return tracer.GetResult()
}

func (d *Debug) debugStorage(ctx context.Context, contractAddress thor.Address, blockID thor.Bytes32, txIndex uint64, clauseIndex uint32, keyStart []byte, maxResult int) (*StorageRangeResult, error) {
rt, _, _, err := d.prepareClauseEnv(ctx, blockID, txIndex, clauseIndex)
func (d *Debug) debugStorage(ctx context.Context, contractAddress thor.Address, block *block.Block, txID thor.Bytes32, clauseIndex uint32, keyStart []byte, maxResult int) (*StorageRangeResult, error) {
rt, _, _, err := d.prepareClauseEnv(ctx, block, txID, clauseIndex)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -357,41 +364,71 @@ func (d *Debug) handleDebugStorage(w http.ResponseWriter, req *http.Request) err
return utils.WriteJSON(w, res)
}

func (d *Debug) parseTarget(target string) (blockID thor.Bytes32, txIndex uint64, clauseIndex uint32, err error) {
func (d *Debug) parseTarget(target string) (block *block.Block, txID thor.Bytes32, clauseIndex uint32, err error) {
// target can be `${blockID}/${txID|txIndex}/${clauseIndex}` or `${txID}/${clauseIndex}`
parts := strings.Split(target, "/")
if len(parts) != 3 {
return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.New("target:" + target + " unsupported"))
if len(parts) != 3 && len(parts) != 2 {
return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.New("target:" + target + " unsupported"))
}
blockID, err = thor.ParseBytes32(parts[0])
if err != nil {
return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.WithMessage(err, "target[0]"))
}
if len(parts[1]) == 64 || len(parts[1]) == 66 {
txID, err := thor.ParseBytes32(parts[1])

if len(parts) == 2 {
txID, err = thor.ParseBytes32(parts[0])
if err != nil {
return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.WithMessage(err, "target[1]"))
return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, "target([0]"))
}

txMeta, err := d.repo.NewChain(blockID).GetTransactionMeta(txID)
txMeta, err := d.repo.NewBestChain().GetTransactionMeta(txID)
if err != nil {
if d.repo.IsNotFound(err) {
return thor.Bytes32{}, 0, 0, utils.Forbidden(errors.New("transaction not found"))
return nil, thor.Bytes32{}, 0, utils.Forbidden(errors.New("transaction not found"))
}
return thor.Bytes32{}, 0, 0, err
return nil, thor.Bytes32{}, 0, err
}
block, err = d.repo.GetBlock(txMeta.BlockID)
if err != nil {
return nil, thor.Bytes32{}, 0, err
}
txIndex = txMeta.Index
} else {
i, err := strconv.ParseUint(parts[1], 0, 0)
blockID, err := thor.ParseBytes32(parts[0])
if err != nil {
return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, "target[0]"))
}
block, err = d.repo.GetBlock(blockID)
if err != nil {
return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.WithMessage(err, "target[1]"))
return nil, thor.Bytes32{}, 0, err
}
if len(parts[1]) == 64 || len(parts[1]) == 66 {
txID, err = thor.ParseBytes32(parts[1])
if err != nil {
return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, "target[1]"))
}

var found bool
for _, tx := range block.Transactions() {
if tx.ID() == txID {
found = true
break
}
}
if !found {
return nil, thor.Bytes32{}, 0, utils.Forbidden(errors.New("transaction not found"))
}
} else {
i, err := strconv.ParseUint(parts[1], 0, 0)
if err != nil {
return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, "target[1]"))
}
if i >= uint64(len(block.Transactions())) {
return nil, thor.Bytes32{}, 0, utils.Forbidden(errors.New("tx index out of range"))
}
txID = block.Transactions()[i].ID()
}
txIndex = i
}
i, err := strconv.ParseUint(parts[2], 0, 0)

i, err := strconv.ParseUint(parts[len(parts)-1], 0, 0)
if err != nil {
return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.WithMessage(err, "target[2]"))
return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, fmt.Sprintf("target[%d]", len(parts)-1)))
} else if i > math.MaxUint32 {
return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.New("invalid target[2]"))
return nil, thor.Bytes32{}, 0, utils.BadRequest(fmt.Errorf("invalid target[%d]", len(parts)-1))
}
clauseIndex = uint32(i)
return
Expand Down
30 changes: 26 additions & 4 deletions api/debug/debug_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
package debug

import (
"context"
"encoding/json"
"fmt"
"math/big"
Expand Down Expand Up @@ -62,6 +61,7 @@ func TestDebug(t *testing.T) {
"testTraceClauseWithClauseIndexOutOfBound": testTraceClauseWithClauseIndexOutOfBound,
"testTraceClauseWithCustomTracer": testTraceClauseWithCustomTracer,
"testTraceClause": testTraceClause,
"testTraceClauseWithoutBlockID": testTraceClauseWithoutBlockID,
} {
t.Run(name, tt)
}
Expand Down Expand Up @@ -176,9 +176,11 @@ func testTraceClauseWithBadBlockID(t *testing.T) {
}

func testTraceClauseWithNonExistingBlockID(t *testing.T) {
_, _, _, err := debug.prepareClauseEnv(context.Background(), datagen.RandomHash(), 1, 1)

assert.Error(t, err)
traceClauseOption := &TraceClauseOption{
Name: "structLogger",
Target: fmt.Sprintf("%s/x/x", datagen.RandomHash()),
}
httpPostAndCheckResponseStatus(t, "/debug/tracers", traceClauseOption, 500)
}

func testTraceClauseWithBadTxID(t *testing.T) {
Expand Down Expand Up @@ -265,6 +267,26 @@ func testTraceClause(t *testing.T) {
assert.Equal(t, expectedExecutionResult, parsedExecutionRes)
}

func testTraceClauseWithoutBlockID(t *testing.T) {
traceClauseOption := &TraceClauseOption{
Name: "structLogger",
Target: fmt.Sprintf("%s/1", transaction.ID()),
}
expectedExecutionResult := &logger.ExecutionResult{
Gas: 0,
Failed: false,
ReturnValue: "",
StructLogs: make([]logger.StructLogRes, 0),
}
res := httpPostAndCheckResponseStatus(t, "/debug/tracers", traceClauseOption, 200)

var parsedExecutionRes *logger.ExecutionResult
if err := json.Unmarshal([]byte(res), &parsedExecutionRes); err != nil {
t.Fatal(err)
}
assert.Equal(t, expectedExecutionResult, parsedExecutionRes)
}

func testTraceClauseWithTxIndexOutOfBound(t *testing.T) {
traceClauseOption := &TraceClauseOption{
Name: "structLogger",
Expand Down
9 changes: 5 additions & 4 deletions api/doc/thor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2104,11 +2104,12 @@ components:
The unified path of the target to be traced. Currently, only the clause is supported.

Format:
`blockID/(txIndex|txId)/clauseIndex`
`blockID/(txIndex|txId)/clauseIndex` or `txID/clauseIndex`


example: '0x010709463c1f0c9aa66a31182fb36d1977d99bfb6526bae0564a0eac4006c31a/0/0'
nullable: false
pattern: '^0x[0-9a-fA-F]{64}\/(0x[0-9a-fA-F]{64}|\d+)\/[0-9]+$'
pattern: '^0x[0-9a-fA-F]{64}(\/(0x[0-9a-fA-F]{64}|\d+))?\/[0-9]+$'
example:
target: '0x010709463c1f0c9aa66a31182fb36d1977d99bfb6526bae0564a0eac4006c31a/0/0'

Expand Down Expand Up @@ -2174,9 +2175,9 @@ components:
The unified path of the transaction clause.

Format:
`blockID/(txIndex|txId)/clauseIndex`
`blockID/(txIndex|txId)/clauseIndex` or `txID/clauseIndex`
nullable: false
pattern: '^0x[0-9a-fA-F]{64}\/(0x[0-9a-fA-F]{64}|\d+)\/[0-9]+$'
pattern: '^0x[0-9a-fA-F]{64}(\/(0x[0-9a-fA-F]{64}|\d+))?\/[0-9]+$'

StorageRange:
type: object
Expand Down
Loading