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

clients/stellarcore horizon: Obtain and expose diagnostic events in transaction endpoint #5148

Merged
merged 3 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
23 changes: 22 additions & 1 deletion clients/stellarcore/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
"net/http"
"testing"

"github.com/stretchr/testify/assert"

proto "github.com/stellar/go/protocols/stellarcore"
"github.com/stellar/go/support/http/httptest"
"github.com/stretchr/testify/assert"
)

func TestSubmitTransaction(t *testing.T) {
Expand All @@ -27,6 +28,26 @@
}
}

func TestSubmitTransactionError(t *testing.T) {
hmock := httptest.NewClient()
c := &Client{HTTP: hmock, URL: "http://localhost:11626"}

// happy path - new transaction
hmock.On("GET", "http://localhost:11626/tx?blob=foo").
ReturnString(
200,
`{"diagnostic_events":"AAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAFZXJyb3IAAAAAAAACAAAAAwAAAAUAAAAQAAAAAQAAAAMAAAAOAAAAU3RyYW5zYWN0aW9uIGBzb3JvYmFuRGF0YS5yZXNvdXJjZUZlZWAgaXMgbG93ZXIgdGhhbiB0aGUgYWN0dWFsIFNvcm9iYW4gcmVzb3VyY2UgZmVlAAAAAAUAAAAAAAEJcwAAAAUAAAAAAAG6fA==","error":"AAAAAAABCdf////vAAAAAA==","status":"ERROR"}`,

Check failure on line 39 in clients/stellarcore/client_test.go

View workflow job for this annotation

GitHub Actions / golangci

line is 330 characters (lll)
)

resp, err := c.SubmitTransaction(context.Background(), "foo")

if assert.NoError(t, err) {
assert.Equal(t, "ERROR", resp.Status)
assert.Equal(t, resp.Error, "AAAAAAABCdf////vAAAAAA==")
assert.Equal(t, "AAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAFZXJyb3IAAAAAAAACAAAAAwAAAAUAAAAQAAAAAQAAAAMAAAAOAAAAU3RyYW5zYWN0aW9uIGBzb3JvYmFuRGF0YS5yZXNvdXJjZUZlZWAgaXMgbG93ZXIgdGhhbiB0aGUgYWN0dWFsIFNvcm9iYW4gcmVzb3VyY2UgZmVlAAAAAAUAAAAAAAEJcwAAAAUAAAAAAAG6fA==", resp.DiagnosticEvents)

Check failure on line 47 in clients/stellarcore/client_test.go

View workflow job for this annotation

GitHub Actions / golangci

line is 292 characters (lll)
}
}

func TestManualClose(t *testing.T) {
hmock := httptest.NewClient()
c := &Client{HTTP: hmock, URL: "http://localhost:11626"}
Expand Down
37 changes: 37 additions & 0 deletions protocols/stellarcore/tx_response.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package stellarcore

import (
"github.com/stellar/go/xdr"
)

const (
// TXStatusError represents the status value returned by stellar-core when an error occurred from
// submitting a transaction
Expand All @@ -25,9 +29,42 @@ type TXResponse struct {
Exception string `json:"exception"`
Error string `json:"error"`
Status string `json:"status"`
// DiagnosticEvents is an optional base64-encoded XDR Variable-Length Array of DiagnosticEvents
DiagnosticEvents string `json:"diagnostic_events,omitempty"`
}

// IsException returns true if the response represents an exception response from stellar-core
func (resp *TXResponse) IsException() bool {
return resp.Exception != ""
}

// DecodeDiagnosticEvents returns the decoded events
func DecodeDiagnosticEvents(events string) ([]xdr.DiagnosticEvent, error) {
var ret []xdr.DiagnosticEvent
if events == "" {
return ret, nil
}
err := xdr.SafeUnmarshalBase64(events, &ret)
if err != nil {
return nil, err
}
return ret, err
}

// DiagnosticEventsToSlice transforms the base64 diagnostic events into a slice of individual
// base64-encoded diagnostic events
func DiagnosticEventsToSlice(events string) ([]string, error) {
decoded, err := DecodeDiagnosticEvents(events)
if err != nil {
return nil, err
}
result := make([]string, len(decoded))
for i := 0; i < len(decoded); i++ {
encoded, err := xdr.MarshalBase64(decoded[i])
if err != nil {
return nil, err
}
result[0] = encoded
2opremio marked this conversation as resolved.
Show resolved Hide resolved
}
return result, nil
}
14 changes: 14 additions & 0 deletions protocols/stellarcore/tx_response_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package stellarcore

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestDiagnosticEventsToSlice(t *testing.T) {
events := "AAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAFZXJyb3IAAAAAAAACAAAAAwAAAAUAAAAQAAAAAQAAAAMAAAAOAAAAU3RyYW5zYWN0aW9uIGBzb3JvYmFuRGF0YS5yZXNvdXJjZUZlZWAgaXMgbG93ZXIgdGhhbiB0aGUgYWN0dWFsIFNvcm9iYW4gcmVzb3VyY2UgZmVlAAAAAAUAAAAAAAEJcwAAAAUAAAAAAAG6fA=="

Check failure on line 10 in protocols/stellarcore/tx_response_test.go

View workflow job for this annotation

GitHub Actions / golangci

line is 261 characters (lll)
slice, err := DiagnosticEventsToSlice(events)
assert.NoError(t, err)
assert.Len(t, slice, 1)
2opremio marked this conversation as resolved.
Show resolved Hide resolved
}
30 changes: 21 additions & 9 deletions services/horizon/internal/actions/submit_transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/stellar/go/network"
"github.com/stellar/go/protocols/horizon"
"github.com/stellar/go/protocols/stellarcore"
hProblem "github.com/stellar/go/services/horizon/internal/render/problem"
"github.com/stellar/go/services/horizon/internal/resourceadapter"
"github.com/stellar/go/services/horizon/internal/txsub"
Expand Down Expand Up @@ -95,15 +96,30 @@ func (handler SubmitTransactionHandler) response(r *http.Request, info envelopeI
return nil, &hProblem.ClientDisconnected
}

switch err := result.Err.(type) {
case *txsub.FailedTransactionError:
if failedErr, ok := result.Err.(*txsub.FailedTransactionError); ok {
rcr := horizon.TransactionResultCodes{}
resourceadapter.PopulateTransactionResultCodes(
err := resourceadapter.PopulateTransactionResultCodes(
r.Context(),
info.hash,
&rcr,
err,
failedErr,
)
if err != nil {
return nil, failedErr
}

extras := map[string]interface{}{
"envelope_xdr": info.raw,
"result_xdr": failedErr.ResultXDR,
"result_codes": rcr,
}
if failedErr.DiagnosticEvents != "" {
events, err := stellarcore.DiagnosticEventsToSlice(failedErr.DiagnosticEvents)
if err != nil {
return nil, err
}
extras["diagnostic_events"] = events
}

return nil, &problem.P{
Type: "transaction_failed",
Expand All @@ -113,11 +129,7 @@ func (handler SubmitTransactionHandler) response(r *http.Request, info envelopeI
"The `extras.result_codes` field on this response contains further " +
"details. Descriptions of each code can be found at: " +
"https://developers.stellar.org/api/errors/http-status-codes/horizon-specific/transaction-failed/",
Extras: map[string]interface{}{
"envelope_xdr": info.raw,
"result_xdr": err.ResultXDR,
"result_codes": rcr,
},
Extras: extras,
}
}

Expand Down
51 changes: 48 additions & 3 deletions services/horizon/internal/actions/submit_transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/stellar/go/network"
"github.com/stellar/go/services/horizon/internal/corestate"
hProblem "github.com/stellar/go/services/horizon/internal/render/problem"
"github.com/stellar/go/services/horizon/internal/txsub"
"github.com/stellar/go/support/render/problem"
"github.com/stellar/go/xdr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

func TestStellarCoreMalformedTx(t *testing.T) {
Expand Down Expand Up @@ -199,3 +200,47 @@
_, err = handler.GetResource(w, request)
assert.Equal(t, p, err)
}

func TestSubmissionSorobanDiagnosticEvents(t *testing.T) {
mockSubmitChannel := make(chan txsub.Result, 1)
mock := &coreStateGetterMock{}
mock.On("GetCoreState").Return(corestate.State{
Synced: true,
})

mockSubmitter := &networkSubmitterMock{}
mockSubmitter.On("Submit").Return(mockSubmitChannel)
mockSubmitChannel <- txsub.Result{
Err: &txsub.FailedTransactionError{
ResultXDR: "AAAAAAABCdf////vAAAAAA==",
DiagnosticEvents: "AAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAFZXJyb3IAAAAAAAACAAAAAwAAAAUAAAAQAAAAAQAAAAMAAAAOAAAAU3RyYW5zYWN0aW9uIGBzb3JvYmFuRGF0YS5yZXNvdXJjZUZlZWAgaXMgbG93ZXIgdGhhbiB0aGUgYWN0dWFsIFNvcm9iYW4gcmVzb3VyY2UgZmVlAAAAAAUAAAAAAAEJcwAAAAUAAAAAAAG6fA==",

Check failure on line 216 in services/horizon/internal/actions/submit_transaction_test.go

View workflow job for this annotation

GitHub Actions / golangci

line is 272 characters (lll)
},
}

handler := SubmitTransactionHandler{
Submitter: mockSubmitter,
NetworkPassphrase: network.PublicNetworkPassphrase,
CoreStateGetter: mock,
}

form := url.Values{}
form.Set("tx", "AAAAAAGUcmKO5465JxTSLQOQljwk2SfqAJmZSG6JH6wtqpwhAAABLAAAAAAAAAABAAAAAAAAAAEAAAALaGVsbG8gd29ybGQAAAAAAwAAAAAAAAAAAAAAABbxCy3mLg3hiTqX4VUEEp60pFOrJNxYM1JtxXTwXhY2AAAAAAvrwgAAAAAAAAAAAQAAAAAW8Qst5i4N4Yk6l+FVBBKetKRTqyTcWDNSbcV08F4WNgAAAAAN4Lazj4x61AAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABLaqcIQAAAEBKwqWy3TaOxoGnfm9eUjfTRBvPf34dvDA0Nf+B8z4zBob90UXtuCqmQqwMCyH+okOI3c05br3khkH0yP4kCwcE")

Check failure on line 227 in services/horizon/internal/actions/submit_transaction_test.go

View workflow job for this annotation

GitHub Actions / golangci

line is 435 characters (lll)

request, err := http.NewRequest(

Check failure on line 229 in services/horizon/internal/actions/submit_transaction_test.go

View workflow job for this annotation

GitHub Actions / golangci

should rewrite http.NewRequestWithContext or add (*Request).WithContext (noctx)
"POST",
"https://horizon.stellar.org/transactions",
strings.NewReader(form.Encode()),
)

require.NoError(t, err)
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")

w := httptest.NewRecorder()
_, err = handler.GetResource(w, request)
require.Error(t, err)
require.IsType(t, &problem.P{}, err)
require.Contains(t, err.(*problem.P).Extras, "diagnostic_events")
require.IsType(t, []string{}, err.(*problem.P).Extras["diagnostic_events"])
diagnosticEvents := err.(*problem.P).Extras["diagnostic_events"].([]string)
require.Equal(t, diagnosticEvents, []string{"AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAVlcnJvcgAAAAAAAAIAAAADAAAABQAAABAAAAABAAAAAwAAAA4AAABTdHJhbnNhY3Rpb24gYHNvcm9iYW5EYXRhLnJlc291cmNlRmVlYCBpcyBsb3dlciB0aGFuIHRoZSBhY3R1YWwgU29yb2JhbiByZXNvdXJjZSBmZWUAAAAABQAAAAAAAQlzAAAABQAAAAAAAbp8"})

Check failure on line 245 in services/horizon/internal/actions/submit_transaction_test.go

View workflow job for this annotation

GitHub Actions / golangci

line is 289 characters (lll)
}
2 changes: 2 additions & 0 deletions services/horizon/internal/codes/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ func String(code interface{}) (string, error) {
return "tx_bad_minseq_age_or_gap", nil
case xdr.TransactionResultCodeTxMalformed:
return "tx_malformed", nil
case xdr.TransactionResultCodeTxSorobanInvalid:
return "tx_soroban_invalid", nil
}
case xdr.OperationResultCode:
switch code {
Expand Down
5 changes: 3 additions & 2 deletions services/horizon/internal/txsub/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ var (

// ErrBadSequence is a canned error response for transactions whose sequence
// number is wrong.
ErrBadSequence = &FailedTransactionError{"AAAAAAAAAAD////7AAAAAA=="}
ErrBadSequence = &FailedTransactionError{"AAAAAAAAAAD////7AAAAAA==", ""}
)

// FailedTransactionError represent an error that occurred because
// stellar-core rejected the transaction. ResultXDR is a base64
// encoded TransactionResult struct
type FailedTransactionError struct {
ResultXDR string
ResultXDR string
DiagnosticEvents string
2opremio marked this conversation as resolved.
Show resolved Hide resolved
}

func (err *FailedTransactionError) Error() string {
Expand Down
2 changes: 1 addition & 1 deletion services/horizon/internal/txsub/submitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func (sub *submitter) Submit(ctx context.Context, env string) (result Submission

switch cresp.Status {
case proto.TXStatusError:
result.Err = &FailedTransactionError{cresp.Error}
result.Err = &FailedTransactionError{cresp.Error, cresp.DiagnosticEvents}
case proto.TXStatusPending, proto.TXStatusDuplicate, proto.TXStatusTryAgainLater:
//noop. A nil Err indicates success
default:
Expand Down
Loading