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

AVM: report structure txn failure info #5875

Merged
merged 7 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion daemon/algod/api/server/v2/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import (
"encoding/base64"
"errors"
"fmt"
"net/http"
"strings"
Expand All @@ -44,7 +45,12 @@
// returnError logs an internal message while returning the encoded response.
func returnError(ctx echo.Context, code int, internal error, external string, logger logging.Logger) error {
logger.Info(internal)
return ctx.JSON(code, model.ErrorResponse{Message: external})
var data *map[string]any
var se *basics.SError
if errors.As(internal, &se) {
data = &se.Attrs

Check warning on line 51 in daemon/algod/api/server/v2/utils.go

View check run for this annotation

Codecov / codecov/patch

daemon/algod/api/server/v2/utils.go#L48-L51

Added lines #L48 - L51 were not covered by tests
}
return ctx.JSON(code, model.ErrorResponse{Message: external, Data: data})

Check warning on line 53 in daemon/algod/api/server/v2/utils.go

View check run for this annotation

Codecov / codecov/patch

daemon/algod/api/server/v2/utils.go#L53

Added line #L53 was not covered by tests
}

func badRequest(ctx echo.Context, internal error, external string, log logging.Logger) error {
Expand Down
132 changes: 132 additions & 0 deletions data/basics/serr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (C) 2019-2024 Algorand, Inc.
// This file is part of go-algorand
//
// go-algorand is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// go-algorand is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with go-algorand. If not, see <https://www.gnu.org/licenses/>.

package basics

import (
"errors"
"strings"

"golang.org/x/exp/slog"
)

// SError is a structured error object. It contains a message and an arbitrary
// set of attributes. If the message contains "%A", it will be replaced by the
// attributes (in no guaranteed order), when SError() is called.
//
//msgp:ignore SError
type SError struct {
Msg string
Attrs map[string]any
Wrapped error
}

// New creates a new structured error object using the supplied message and
// attributes. If the message contains "%A", it will be replaced by the
// attributes when Error() is called.
func New(msg string, pairs ...any) *SError {
attrs := make(map[string]any, len(pairs)/2)
for i := 0; i < len(pairs); i += 2 {
attrs[pairs[i].(string)] = pairs[i+1]
}
return &SError{Msg: msg, Attrs: attrs}
}

// Error returns either the exact supplied message, or the serialized attributes if
// the supplied message was blank, or substituted for %A.
func (e *SError) Error() string {
if e.Msg == "" {
return e.AttributesAsString()
}
// imperfect because we replace \%A as well
if strings.Contains(e.Msg, "%A") {
return strings.Replace(e.Msg, "%A", e.AttributesAsString(), -1)
}
return e.Msg
}

// AttributesAsString returns the attributes the same way that slog serializes
// attributes to text in a log message, in no guaranteed order.
func (e *SError) AttributesAsString() string {
var buf strings.Builder
args := make([]any, 0, 2*len(e.Attrs))
for key, val := range e.Attrs {
args = append(args, key)
args = append(args, val)
}
l := slog.New(slog.NewTextHandler(&buf, nil))
l.Info("", args...)
return strings.TrimSuffix(strings.SplitN(buf.String(), " ", 4)[3], "\n")
}

// Annotate adds additional attributes to an existing error, even if the error
// is deep in the error chain. If the supplied error is nil, nil is returned so
// that callers can annotate errors without checking if they are non-nil. If
// the error is not a structured error, it is wrapped in one using its existing
// message and the new attributes. Just like append() for slices, callers should
// re-assign, like this `err = serr.Annotate(err, "x", 100)`
func Annotate(err error, pairs ...any) error {
if err == nil {
return nil

Check warning on line 83 in data/basics/serr.go

View check run for this annotation

Codecov / codecov/patch

data/basics/serr.go#L83

Added line #L83 was not covered by tests
}
var serr *SError
if ok := errors.As(err, &serr); ok {
for i := 0; i < len(pairs); i += 2 {
serr.Attrs[pairs[i].(string)] = pairs[i+1]
}
return err
}
// Since we don't have a structured error, we wrap the existing error in one.
serr = New(err.Error(), pairs...)
serr.Wrapped = err
return serr
}

// Wrap is used to "demote" an existing error to a field in a new structured
// error. The wrapped error message is added as $field-msg, and if the error is
// structured, the attributes are added under $field-attrs.
func Wrap(err error, msg string, field string, pairs ...any) error {
serr := New(msg, field+"-msg", err.Error())
for i := 0; i < len(pairs); i += 2 {
serr.Attrs[pairs[i].(string)] = pairs[i+1]
}
serr.Wrapped = err

var inner *SError
if ok := errors.As(err, &inner); ok {
attributes := make(map[string]any, len(inner.Attrs))
for key, val := range inner.Attrs {
attributes[key] = val
}
serr.Attrs[field+"-attrs"] = attributes
}

return serr
}

// Unwrap returns the inner error, if it exists.
func (e *SError) Unwrap() error {
return e.Wrapped

Check warning on line 122 in data/basics/serr.go

View check run for this annotation

Codecov / codecov/patch

data/basics/serr.go#L121-L122

Added lines #L121 - L122 were not covered by tests
}

// Attributes returns the attributes of a structured error, or nil/empty.
func Attributes(err error) map[string]any {
var se *SError
if errors.As(err, &se) {
return se.Attrs

Check warning on line 129 in data/basics/serr.go

View check run for this annotation

Codecov / codecov/patch

data/basics/serr.go#L126-L129

Added lines #L126 - L129 were not covered by tests
}
return nil

Check warning on line 131 in data/basics/serr.go

View check run for this annotation

Codecov / codecov/patch

data/basics/serr.go#L131

Added line #L131 was not covered by tests
}
135 changes: 135 additions & 0 deletions data/basics/serr_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright (C) 2019-2024 Algorand, Inc.
// This file is part of go-algorand
//
// go-algorand is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// go-algorand is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with go-algorand. If not, see <https://www.gnu.org/licenses/>.

package basics

import (
"errors"
"fmt"
"testing"

"github.com/algorand/go-algorand/test/partitiontest"
"github.com/stretchr/testify/assert"
)

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

err := New("test")
assert.Equal(t, "test", err.Error())
}

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

err := New("test", "a", 7, "b", []byte{3, 4})
assert.Equal(t, "test", err.Error())
assert.Equal(t, 7, err.Attrs["a"])
assert.Equal(t, []byte{3, 4}, err.Attrs["b"])

err.Msg = ""
assert.ErrorContains(t, err, `a=7`)
assert.ErrorContains(t, err, `b="\x03\x04"`)

err.Msg = "check it: %A"
assert.ErrorContains(t, err, ` a=7`)
assert.ErrorContains(t, err, ` b="\x03\x04"`)
assert.Equal(t, `check it: `, err.Error()[:10])

}

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

err := New("test", "a", 7, "b", []byte{3, 4})
assert.Equal(t, 7, err.Attrs["a"])
assert.Equal(t, nil, err.Attrs["c"])
Annotate(err, "c", true, "a", false)
assert.Equal(t, true, err.Attrs["c"])
assert.Equal(t, false, err.Attrs["a"])
}

func attribute(err error, name string) any {
var serr *SError
if ok := errors.As(err, &serr); ok {
return serr.Attrs[name]
}
return nil
}

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

err := errors.New("hello")
err = Annotate(err, "c", true, "a", false)
assert.Equal(t, true, attribute(err, "c"))
}

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

var err error
err = New("test", "a", 7, "b", []byte{3, 4})
err = fmt.Errorf("embed the above here %w", err)
assert.Equal(t, 7, attribute(err, "a"))
assert.Equal(t, nil, attribute(err, "c"))
Annotate(err, "c", true, "a", false)
assert.Equal(t, true, attribute(err, "c"))
assert.Equal(t, false, attribute(err, "a"))
// "b" is still visible. It would not be is we had _wrapped_ the fmt.Error
assert.Equal(t, []byte{3, 4}, attribute(err, "b"))
}

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

var err error
err = errors.New("inner thingy")
err = Wrap(err, "outer stuff", "xxx")
assert.Equal(t, "inner thingy", attribute(err, "xxx-msg"))
assert.Equal(t, nil, attribute(err, "xxx-attrs"))
}

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

var err error
err = New("test", "a", 7, "b", []byte{3, 4})
err = Wrap(err, "outer stuff", "yyy")
assert.Equal(t, "test", attribute(err, "yyy-msg"))
assert.NotNil(t, attribute(err, "yyy-attrs"))

// these are deeper now, not here
assert.Equal(t, nil, attribute(err, "a"))
assert.Equal(t, nil, attribute(err, "b"))

// here they are
attrs := attribute(err, "yyy-attrs").(map[string]any)
assert.Equal(t, 7, attrs["a"])
assert.Equal(t, []byte{3, 4}, attrs["b"])

// deeper, with a new attribute
err = Wrap(err, "further out", "again", "name", "jj")
assert.Nil(t, attribute(err, "yyy-msg"))
assert.Equal(t, "outer stuff", attribute(err, "again-msg"))
}