From 9bdc48a982723e44a9c5a09067ac844d4ad458a5 Mon Sep 17 00:00:00 2001 From: George Date: Thu, 15 Aug 2024 15:17:55 -0700 Subject: [PATCH 1/9] Add basic, untested support for new Core endpoints --- clients/stellarcore/client.go | 122 ++++++++++++++---- .../stellarcore/getledgerentries_response.go | 30 +++++ protocols/stellarcore/proof_response.go | 6 + services/horizon/internal/ingest/fsm.go | 5 + 4 files changed, 138 insertions(+), 25 deletions(-) create mode 100644 protocols/stellarcore/getledgerentries_response.go create mode 100644 protocols/stellarcore/proof_response.go diff --git a/clients/stellarcore/client.go b/clients/stellarcore/client.go index 931a076021..02ef4eec18 100644 --- a/clients/stellarcore/client.go +++ b/clients/stellarcore/client.go @@ -180,7 +180,7 @@ func (c *Client) SetCursor(ctx context.Context, id string, cursor int32) (err er } var raw []byte - raw, err = ioutil.ReadAll(hresp.Body) + raw, err = io.ReadAll(hresp.Body) if err != nil { return err } @@ -193,9 +193,23 @@ func (c *Client) SetCursor(ctx context.Context, id string, cursor int32) (err er return nil } +func (c *Client) GetLedgerEntries(ctx context.Context, ledgerSeq uint32, keys ...xdr.LedgerKey) (*proto.GetLedgerEntriesResponse, error) { + var resp *proto.GetLedgerEntriesResponse + return resp, c.makeLedgerKeyRequest(ctx, resp, "getledgerentries", ledgerSeq, keys...) +} + +func (c *Client) GetInvocationProof(ctx context.Context, ledgerSeq uint32, keys ...xdr.LedgerKey) (*proto.ProofResponse, error) { + var resp *proto.ProofResponse + return resp, c.makeLedgerKeyRequest(ctx, resp, "getinvocationproof", ledgerSeq, keys...) +} + +func (c *Client) GetRestorationProof(ctx context.Context, ledgerSeq uint32, keys ...xdr.LedgerKey) (*proto.ProofResponse, error) { + var resp *proto.ProofResponse + return resp, c.makeLedgerKeyRequest(ctx, resp, "getrestorationproof", ledgerSeq, keys...) +} + // SubmitTransaction calls the `tx` command on the connected stellar core with the provided envelope func (c *Client) SubmitTransaction(ctx context.Context, envelope string) (resp *proto.TXResponse, err error) { - q := url.Values{} q.Set("blob", envelope) @@ -207,20 +221,12 @@ func (c *Client) SubmitTransaction(ctx context.Context, envelope string) (resp * } var hresp *http.Response - hresp, err = c.http().Do(req) + hresp, err = c.getResponse(req) if err != nil { - err = errors.Wrap(err, "http request errored") - return - } - defer drainReponse(hresp, true, &err) //nolint:errcheck - - if !(hresp.StatusCode >= 200 && hresp.StatusCode < 300) { - err = errors.New("http request failed with non-200 status code") return } err = json.NewDecoder(hresp.Body).Decode(&resp) - if err != nil { err = errors.Wrap(err, "json decode failed") return @@ -232,7 +238,6 @@ func (c *Client) SubmitTransaction(ctx context.Context, envelope string) (resp * // WaitForNetworkSync continually polls the connected stellar-core until it // receives a response that indicated the node has synced with the network func (c *Client) WaitForNetworkSync(ctx context.Context) error { - for { info, err := c.Info(ctx) @@ -266,17 +271,9 @@ func (c *Client) ManualClose(ctx context.Context) (err error) { return } - var hresp *http.Response - hresp, err = c.http().Do(req) + hresp, err := c.getResponse(req) if err != nil { - err = errors.Wrap(err, "http request errored") - return - } - defer drainReponse(hresp, true, &err) //nolint:errcheck - - if !(hresp.StatusCode >= 200 && hresp.StatusCode < 300) { - err = errors.New("http request failed with non-200 status code") - return + return errors.Wrap(err, "http request errored") } // verify there wasn't an exception @@ -312,16 +309,27 @@ func (c *Client) simpleGet( newPath string, query url.Values, ) (*http.Request, error) { + q := "" + if query != nil { + q = query.Encode() + } + return c.rawGet(ctx, newPath, q) +} +// rawGet returns a new GET request to the connected stellar-core using the +// provided path and a raw query string to construct the result. +func (c *Client) rawGet( + ctx context.Context, + newPath string, + query string, +) (*http.Request, error) { u, err := url.Parse(c.URL) if err != nil { return nil, errors.Wrap(err, "unparseable url") } u.Path = path.Join(u.Path, newPath) - if query != nil { - u.RawQuery = query.Encode() - } + u.RawQuery = query newURL := u.String() var req *http.Request @@ -332,3 +340,67 @@ func (c *Client) simpleGet( return req, nil } + +// makeLedgerKeyRequest is a generic method to perform a request in the form +// `key=...&key=...&ledgerSeq=...` which is useful because three Stellar Core +// endpoints all use this request format. +func (c *Client) makeLedgerKeyRequest( + ctx context.Context, + target interface{}, + endpoint string, + ledgerSeq uint32, + keys ...xdr.LedgerKey) error { + q, err := buildMultiKeyRequest(keys...) + if err != nil { + return err + } else if ledgerSeq >= 2 { // optional param + q += fmt.Sprintf("ledgerSeq=%d", ledgerSeq) + } + + var req *http.Request + req, err = c.rawGet(ctx, endpoint, q) + if err != nil { + return err + } + + hresp, err := c.getResponse(req) + if err != nil { + return err + } + + // returns nil if the error is nil + return errors.Wrap(json.NewDecoder(hresp.Body).Decode(&target), "json decode failed") +} + +// getResponse is an abstraction method to perform a request and check for a +// non-2xx status code, returning the whole response +func (c *Client) getResponse(req *http.Request) (*http.Response, error) { + var hresp *http.Response + hresp, err := c.http().Do(req) + if err != nil { + return hresp, errors.Wrap(err, "http request errored") + } + defer drainReponse(hresp, true, &err) //nolint:errcheck + + if hresp.StatusCode < 200 || hresp.StatusCode >= 300 { + return hresp, fmt.Errorf("http request failed with non-200 status code (%d)", hresp.StatusCode) + } + + return hresp, nil +} + +func buildMultiKeyRequest(keys ...xdr.LedgerKey) (string, error) { + // Unfortunately, url.Values does not support multiple keys via Set(), so we + // have to build our URL parameters manually. + q := "" + for _, key := range keys { + keyB64, err := key.MarshalBinaryBase64() + if err != nil { + return q, errors.Wrapf(err, "failed to encode LedgerKey") + } + q += "key=" + url.QueryEscape(keyB64) + "&" + } + + q, _ = strings.CutSuffix(q, "&") + return q, nil +} diff --git a/protocols/stellarcore/getledgerentries_response.go b/protocols/stellarcore/getledgerentries_response.go new file mode 100644 index 0000000000..9eb8577d52 --- /dev/null +++ b/protocols/stellarcore/getledgerentries_response.go @@ -0,0 +1,30 @@ +package stellarcore + +const ( + // NewStateNoProof indicates the entry is new and does NOT require a proof + // of non-existence. + NewStateNoProof = "new_entry_no_proof" + + // NewStateNeedsProof indicates the entry is new and DOES require a proof of + // non-existence. + NewStateNeedsProof = "new_entry_proof" + + // ArchivedStateNoProof indicates the entry is archived and does NOT require + // a proof of existence. + ArchivedStateNoProof = "archived_no_proof" + + // ArchivedStateNeedsProof indicates the entry is archived and DOES require + // a proof of non-existence. + ArchivedStateNeedsProof = "archived_proof" +) + +// GetLedgerEntriesResponse is the response from Stellar Core for the getledgerentries endpoint +type GetLedgerEntriesResponse struct { + Ledger uint32 `json:"ledger"` + Entries []LedgerEntryResponse `json:"entries"` +} + +type LedgerEntryResponse struct { + Entry string `json:"e"` // base64-encoded xdr.LedgerEntry + State string `json:"state"` // one of: "live" | "new_entry_no_proof" | "new_entry_proof" | "archived_no_proof" | "archived_proof" +} diff --git a/protocols/stellarcore/proof_response.go b/protocols/stellarcore/proof_response.go new file mode 100644 index 0000000000..5b2bd6c659 --- /dev/null +++ b/protocols/stellarcore/proof_response.go @@ -0,0 +1,6 @@ +package stellarcore + +type ProofResponse struct { + Ledger uint32 `json:"ledger"` + Proof string `json:"proof,omitempty"` +} diff --git a/services/horizon/internal/ingest/fsm.go b/services/horizon/internal/ingest/fsm.go index c165faf371..8220942e73 100644 --- a/services/horizon/internal/ingest/fsm.go +++ b/services/horizon/internal/ingest/fsm.go @@ -765,6 +765,11 @@ func (v verifyRangeState) run(s *system) (transition, error) { "ledger": true, "commit": true, }).Info("Processing ledger") + + if sequence == 52885867 || sequence == 52885868 { + log.Error("We are in the problem zone.") + } + startTime := time.Now() if err = s.historyQ.Begin(s.ctx); err != nil { From f2f051e1f8e1c5aa4aa2d9ce64f6d9fdd2cc3716 Mon Sep 17 00:00:00 2001 From: George Date: Thu, 15 Aug 2024 15:45:09 -0700 Subject: [PATCH 2/9] Better string building is lit --- clients/stellarcore/client.go | 23 +++++++++++++------ .../stellarcore/getledgerentries_response.go | 2 +- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/clients/stellarcore/client.go b/clients/stellarcore/client.go index 02ef4eec18..15f8b37185 100644 --- a/clients/stellarcore/client.go +++ b/clients/stellarcore/client.go @@ -389,18 +389,27 @@ func (c *Client) getResponse(req *http.Request) (*http.Response, error) { return hresp, nil } +// buildMultiKeyRequest is a workaround helper because, unfortunately, +// url.Values does not support multiple keys via Set(), so we have to build our +// URL parameters manually. func buildMultiKeyRequest(keys ...xdr.LedgerKey) (string, error) { - // Unfortunately, url.Values does not support multiple keys via Set(), so we - // have to build our URL parameters manually. - q := "" + // The average ledger key length, according to a simple + // + // SELECT AVG(LENGTH(HEX(key))) / 2 FROM ledger_entries; + // + // is ~57.6. We can use this to preallocate a final string buffer for + // performance. + q := strings.Builder{} + q.Grow(50 * len(keys)) + for _, key := range keys { keyB64, err := key.MarshalBinaryBase64() if err != nil { - return q, errors.Wrapf(err, "failed to encode LedgerKey") + return q.String(), errors.Wrapf(err, "failed to encode LedgerKey") } - q += "key=" + url.QueryEscape(keyB64) + "&" + q.WriteString("key=" + url.QueryEscape(keyB64) + "&") } - q, _ = strings.CutSuffix(q, "&") - return q, nil + s, _ := strings.CutSuffix(q.String(), "&") // trim trailing & + return s, nil } diff --git a/protocols/stellarcore/getledgerentries_response.go b/protocols/stellarcore/getledgerentries_response.go index 9eb8577d52..b3f6322f8d 100644 --- a/protocols/stellarcore/getledgerentries_response.go +++ b/protocols/stellarcore/getledgerentries_response.go @@ -26,5 +26,5 @@ type GetLedgerEntriesResponse struct { type LedgerEntryResponse struct { Entry string `json:"e"` // base64-encoded xdr.LedgerEntry - State string `json:"state"` // one of: "live" | "new_entry_no_proof" | "new_entry_proof" | "archived_no_proof" | "archived_proof" + State string `json:"state"` // one of the above State constants } From 21159c0cac2896becadfe613f2322858eb122029 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 19 Aug 2024 13:41:19 -0700 Subject: [PATCH 3/9] Remove GetLedgerEntry to make breaking changes more visible --- clients/stellarcore/client.go | 59 ++++--------------- .../stellarcore/getledgerentries_response.go | 8 +++ .../stellarcore/getledgerentry_response.go | 18 ------ 3 files changed, 18 insertions(+), 67 deletions(-) delete mode 100644 protocols/stellarcore/getledgerentry_response.go diff --git a/clients/stellarcore/client.go b/clients/stellarcore/client.go index 15f8b37185..fafc3f4f21 100644 --- a/clients/stellarcore/client.go +++ b/clients/stellarcore/client.go @@ -82,47 +82,6 @@ func (c *Client) Upgrade(ctx context.Context, version int) (err error) { return nil } -// GetLedgerEntry submits a request to the stellar core instance to get the latest -// state of a given ledger entry. -func (c *Client) GetLedgerEntry(ctx context.Context, ledgerKey xdr.LedgerKey) (proto.GetLedgerEntryResponse, error) { - b64, err := xdr.MarshalBase64(ledgerKey) - if err != nil { - return proto.GetLedgerEntryResponse{}, errors.Wrap(err, "failed to marshal ledger key") - } - q := url.Values{} - q.Set("key", b64) - - req, err := c.simpleGet(ctx, "getledgerentry", q) - if err != nil { - return proto.GetLedgerEntryResponse{}, errors.Wrap(err, "failed to create request") - } - - hresp, err := c.http().Do(req) - if err != nil { - return proto.GetLedgerEntryResponse{}, errors.Wrap(err, "http request errored") - } - defer hresp.Body.Close() - - if !(hresp.StatusCode >= 200 && hresp.StatusCode < 300) { - if drainReponse(hresp, false, &err) != nil { - return proto.GetLedgerEntryResponse{}, err - } - return proto.GetLedgerEntryResponse{}, errors.New("http request failed with non-200 status code") - } - - responseBytes, err := io.ReadAll(hresp.Body) - if err != nil { - return proto.GetLedgerEntryResponse{}, errors.Wrap(err, "could not read response") - } - - var response proto.GetLedgerEntryResponse - if err = json.Unmarshal(responseBytes, &response); err != nil { - return proto.GetLedgerEntryResponse{}, errors.Wrap(err, "json decode failed: "+string(responseBytes)) - } - - return response, nil -} - // Info calls the `info` command on the connected stellar core and returns the // provided response func (c *Client) Info(ctx context.Context) (resp *proto.InfoResponse, err error) { @@ -198,14 +157,16 @@ func (c *Client) GetLedgerEntries(ctx context.Context, ledgerSeq uint32, keys .. return resp, c.makeLedgerKeyRequest(ctx, resp, "getledgerentries", ledgerSeq, keys...) } -func (c *Client) GetInvocationProof(ctx context.Context, ledgerSeq uint32, keys ...xdr.LedgerKey) (*proto.ProofResponse, error) { - var resp *proto.ProofResponse - return resp, c.makeLedgerKeyRequest(ctx, resp, "getinvocationproof", ledgerSeq, keys...) +//lint:ignore U1000 Ignore unused function until it's supported in Core +func (c *Client) getInvocationProof(ctx context.Context, ledgerSeq uint32, keys ...xdr.LedgerKey) (proto.ProofResponse, error) { + var resp proto.ProofResponse + return resp, c.makeLedgerKeyRequest(ctx, &resp, "getinvocationproof", ledgerSeq, keys...) } -func (c *Client) GetRestorationProof(ctx context.Context, ledgerSeq uint32, keys ...xdr.LedgerKey) (*proto.ProofResponse, error) { - var resp *proto.ProofResponse - return resp, c.makeLedgerKeyRequest(ctx, resp, "getrestorationproof", ledgerSeq, keys...) +//lint:ignore U1000 Ignore unused function until it's supported in Core +func (c *Client) getRestorationProof(ctx context.Context, ledgerSeq uint32, keys ...xdr.LedgerKey) (proto.ProofResponse, error) { + var resp proto.ProofResponse + return resp, c.makeLedgerKeyRequest(ctx, &resp, "getrestorationproof", ledgerSeq, keys...) } // SubmitTransaction calls the `tx` command on the connected stellar core with the provided envelope @@ -397,8 +358,8 @@ func buildMultiKeyRequest(keys ...xdr.LedgerKey) (string, error) { // // SELECT AVG(LENGTH(HEX(key))) / 2 FROM ledger_entries; // - // is ~57.6. We can use this to preallocate a final string buffer for - // performance. + // on a pubnet RPC instance is ~57.6. We can use this to preallocate a + // string buffer for performance. q := strings.Builder{} q.Grow(50 * len(keys)) diff --git a/protocols/stellarcore/getledgerentries_response.go b/protocols/stellarcore/getledgerentries_response.go index b3f6322f8d..71635f1eea 100644 --- a/protocols/stellarcore/getledgerentries_response.go +++ b/protocols/stellarcore/getledgerentries_response.go @@ -1,6 +1,14 @@ package stellarcore const ( + // LiveState represents the state value returned by stellar-core when a + // ledger entry is live + LiveState = "live" + + // DeadState represents the state value returned by stellar-core when a + // ledger entry is dead + DeadState = "dead" + // NewStateNoProof indicates the entry is new and does NOT require a proof // of non-existence. NewStateNoProof = "new_entry_no_proof" diff --git a/protocols/stellarcore/getledgerentry_response.go b/protocols/stellarcore/getledgerentry_response.go deleted file mode 100644 index d3289c6aa8..0000000000 --- a/protocols/stellarcore/getledgerentry_response.go +++ /dev/null @@ -1,18 +0,0 @@ -package stellarcore - -const ( - // LiveState represents the state value returned by stellar-core when a - // ledger entry is live - LiveState = "live" - - // DeadState represents the state value returned by stellar-core when a - // ledger entry is dead - DeadState = "dead" -) - -// GetLedgerEntryResponse is the response from Stellar Core for the getLedgerEntry endpoint -type GetLedgerEntryResponse struct { - State string `json:"state"` - Entry string `json:"entry"` - Ledger int64 `json:"ledger"` -} From 7c8a69335647bbc621051e4edfbde4d1b7e07c0f Mon Sep 17 00:00:00 2001 From: George Date: Mon, 19 Aug 2024 13:46:28 -0700 Subject: [PATCH 4/9] Add basic test on POST, JSON, error checks: It'd be nice if we had POST param unit tests but we don't :( --- clients/stellarcore/client.go | 111 +++++++++++++++--------- clients/stellarcore/client_test.go | 41 +++++++++ services/horizon/internal/ingest/fsm.go | 5 -- 3 files changed, 109 insertions(+), 48 deletions(-) diff --git a/clients/stellarcore/client.go b/clients/stellarcore/client.go index fafc3f4f21..0d4e4765da 100644 --- a/clients/stellarcore/client.go +++ b/clients/stellarcore/client.go @@ -1,11 +1,11 @@ package stellarcore import ( + "bytes" "context" "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "net/url" "path" @@ -21,7 +21,7 @@ import ( // Client represents a client that is capable of communicating with a // stellar-core server using HTTP type Client struct { - // HTTP is the client to use when communicating with stellar-core. If nil, + // HTTP is the client to use when communicating with stellar-core. If nil, // http.DefaultClient will be used. HTTP HTTP @@ -36,7 +36,7 @@ type Client struct { // in case an error was encountered during either the draining or closing of the // stream, that error would be returned. func drainReponse(hresp *http.Response, close bool, err *error) (outerror error) { - _, err2 := io.Copy(ioutil.Discard, hresp.Body) + _, err2 := io.Copy(io.Discard, hresp.Body) if err2 != nil { if err != nil && *err == nil { *err = errors.Wrap(err2, "unable to read excess data from response") @@ -75,9 +75,8 @@ func (c *Client) Upgrade(ctx context.Context, version int) (err error) { } defer drainReponse(hresp, true, &err) //nolint:errcheck - if !(hresp.StatusCode >= 200 && hresp.StatusCode < 300) { - err = errors.New("http request failed with non-200 status code") - return + if hresp.StatusCode < 200 || hresp.StatusCode >= 300 { + return errors.New("http request failed with non-200 status code") } return nil } @@ -152,9 +151,9 @@ func (c *Client) SetCursor(ctx context.Context, id string, cursor int32) (err er return nil } -func (c *Client) GetLedgerEntries(ctx context.Context, ledgerSeq uint32, keys ...xdr.LedgerKey) (*proto.GetLedgerEntriesResponse, error) { - var resp *proto.GetLedgerEntriesResponse - return resp, c.makeLedgerKeyRequest(ctx, resp, "getledgerentries", ledgerSeq, keys...) +func (c *Client) GetLedgerEntries(ctx context.Context, ledgerSeq uint32, keys ...xdr.LedgerKey) (proto.GetLedgerEntriesResponse, error) { + var resp proto.GetLedgerEntriesResponse + return resp, c.makeLedgerKeyRequest(ctx, &resp, "getledgerentry", ledgerSeq, keys...) } //lint:ignore U1000 Ignore unused function until it's supported in Core @@ -182,8 +181,15 @@ func (c *Client) SubmitTransaction(ctx context.Context, envelope string) (resp * } var hresp *http.Response - hresp, err = c.getResponse(req) + hresp, err = c.http().Do(req) if err != nil { + err = errors.Wrap(err, "http request errored") + return + } + defer drainReponse(hresp, true, &err) //nolint:errcheck + + if hresp.StatusCode < 200 || hresp.StatusCode >= 300 { + err = errors.New("http request failed with non-200 status code") return } @@ -222,7 +228,6 @@ func (c *Client) WaitForNetworkSync(ctx context.Context) error { // ManualClose closes a ledger when Core is running in `MANUAL_CLOSE` mode func (c *Client) ManualClose(ctx context.Context) (err error) { - q := url.Values{} var req *http.Request @@ -232,9 +237,17 @@ func (c *Client) ManualClose(ctx context.Context) (err error) { return } - hresp, err := c.getResponse(req) + var hresp *http.Response + hresp, err = c.http().Do(req) if err != nil { - return errors.Wrap(err, "http request errored") + err = errors.Wrap(err, "http request errored") + return + } + defer drainReponse(hresp, true, &err) //nolint:errcheck + + if hresp.StatusCode < 200 || hresp.StatusCode >= 300 { + err = errors.New("http request failed with non-200 status code") + return } // verify there wasn't an exception @@ -270,19 +283,33 @@ func (c *Client) simpleGet( newPath string, query url.Values, ) (*http.Request, error) { - q := "" + u, err := url.Parse(c.URL) + if err != nil { + return nil, errors.Wrap(err, "unparseable url") + } + + u.Path = path.Join(u.Path, newPath) if query != nil { - q = query.Encode() + u.RawQuery = query.Encode() } - return c.rawGet(ctx, newPath, q) + newURL := u.String() + + var req *http.Request + req, err = http.NewRequestWithContext(ctx, http.MethodGet, newURL, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to create request") + } + + return req, nil } -// rawGet returns a new GET request to the connected stellar-core using the -// provided path and a raw query string to construct the result. -func (c *Client) rawGet( +// rawPost returns a new POST request to the connected stellar-core using the +// provided path and the params values encoded as the request body to construct +// the result. +func (c *Client) rawPost( ctx context.Context, newPath string, - query string, + params string, ) (*http.Request, error) { u, err := url.Parse(c.URL) if err != nil { @@ -290,11 +317,14 @@ func (c *Client) rawGet( } u.Path = path.Join(u.Path, newPath) - u.RawQuery = query newURL := u.String() var req *http.Request - req, err = http.NewRequestWithContext(ctx, http.MethodGet, newURL, nil) + req, err = http.NewRequestWithContext( + ctx, + http.MethodPost, + newURL, + bytes.NewBuffer([]byte(params))) if err != nil { return nil, errors.Wrap(err, "failed to create request") } @@ -304,13 +334,14 @@ func (c *Client) rawGet( // makeLedgerKeyRequest is a generic method to perform a request in the form // `key=...&key=...&ledgerSeq=...` which is useful because three Stellar Core -// endpoints all use this request format. +// endpoints all use this request format. Be sure to pass `target` by reference. func (c *Client) makeLedgerKeyRequest( ctx context.Context, target interface{}, endpoint string, ledgerSeq uint32, - keys ...xdr.LedgerKey) error { + keys ...xdr.LedgerKey, +) error { q, err := buildMultiKeyRequest(keys...) if err != nil { return err @@ -319,35 +350,24 @@ func (c *Client) makeLedgerKeyRequest( } var req *http.Request - req, err = c.rawGet(ctx, endpoint, q) - if err != nil { - return err - } - - hresp, err := c.getResponse(req) + req, err = c.rawPost(ctx, endpoint, q) if err != nil { return err } - // returns nil if the error is nil - return errors.Wrap(json.NewDecoder(hresp.Body).Decode(&target), "json decode failed") -} - -// getResponse is an abstraction method to perform a request and check for a -// non-2xx status code, returning the whole response -func (c *Client) getResponse(req *http.Request) (*http.Response, error) { var hresp *http.Response - hresp, err := c.http().Do(req) + hresp, err = c.http().Do(req) if err != nil { - return hresp, errors.Wrap(err, "http request errored") + return errors.Wrap(err, "http request errored") } defer drainReponse(hresp, true, &err) //nolint:errcheck if hresp.StatusCode < 200 || hresp.StatusCode >= 300 { - return hresp, fmt.Errorf("http request failed with non-200 status code (%d)", hresp.StatusCode) + return fmt.Errorf("http request failed with non-200 status code (%d)", hresp.StatusCode) } - return hresp, nil + // wrap returns nil if the inner error is nil + return errors.Wrap(json.NewDecoder(hresp.Body).Decode(&target), "json decode failed") } // buildMultiKeyRequest is a workaround helper because, unfortunately, @@ -356,17 +376,22 @@ func (c *Client) getResponse(req *http.Request) (*http.Response, error) { func buildMultiKeyRequest(keys ...xdr.LedgerKey) (string, error) { // The average ledger key length, according to a simple // - // SELECT AVG(LENGTH(HEX(key))) / 2 FROM ledger_entries; + // SELECT AVG(LENGTH(HEX(key))) / 2 FROM ledger_entries; // // on a pubnet RPC instance is ~57.6. We can use this to preallocate a // string buffer for performance. + // + // We know that these endpoints will almost exclusively be used for + // ContractData and the like, so we could optimize the buffer further for + // that, but that data is harder to query since it'd involve parsing the XDR + // from the DB to check the key type. q := strings.Builder{} q.Grow(50 * len(keys)) for _, key := range keys { keyB64, err := key.MarshalBinaryBase64() if err != nil { - return q.String(), errors.Wrapf(err, "failed to encode LedgerKey") + return q.String(), errors.Wrap(err, "failed to encode LedgerKey") } q.WriteString("key=" + url.QueryEscape(keyB64) + "&") } diff --git a/clients/stellarcore/client_test.go b/clients/stellarcore/client_test.go index 6cfd01b210..aa068610b1 100644 --- a/clients/stellarcore/client_test.go +++ b/clients/stellarcore/client_test.go @@ -6,9 +6,12 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stellar/go/keypair" proto "github.com/stellar/go/protocols/stellarcore" "github.com/stellar/go/support/http/httptest" + "github.com/stellar/go/xdr" ) func TestSubmitTransaction(t *testing.T) { @@ -75,3 +78,41 @@ func TestManualClose_NotAvailable(t *testing.T) { assert.EqualError(t, err, "exception in response: Set MANUAL_CLOSE=true") } + +func TestGetLedgerEntries(t *testing.T) { + hmock := httptest.NewClient() + c := &Client{HTTP: hmock, URL: "http://localhost:11626"} + + // build a fake response body + mockResp := proto.GetLedgerEntriesResponse{ + Ledger: 1215, // checkpoint align on expected request + Entries: []proto.LedgerEntryResponse{ + { + Entry: "pretend this is XDR lol", + State: proto.DeadState, + }, + { + Entry: "pretend this is another XDR lol", + State: proto.ArchivedStateNoProof, + }, + }, + } + + // happy path - fetch an entry + hmock.On("POST", "http://localhost:11626/getledgerentry"). + ReturnJSON(http.StatusOK, &mockResp) + + var key xdr.LedgerKey + acc, err := xdr.AddressToAccountId(keypair.MustRandom().Address()) + require.NoError(t, err) + key.SetAccount(acc) + + resp, err := c.GetLedgerEntries(context.Background(), 1234, key) + require.NoError(t, err) + require.NotNil(t, resp) + + require.EqualValues(t, 1215, resp.Ledger) + require.Len(t, resp.Entries, 2) + require.Equal(t, resp.Entries[0].State, proto.DeadState) + require.Equal(t, resp.Entries[1].State, proto.ArchivedStateNoProof) +} diff --git a/services/horizon/internal/ingest/fsm.go b/services/horizon/internal/ingest/fsm.go index 8220942e73..c165faf371 100644 --- a/services/horizon/internal/ingest/fsm.go +++ b/services/horizon/internal/ingest/fsm.go @@ -765,11 +765,6 @@ func (v verifyRangeState) run(s *system) (transition, error) { "ledger": true, "commit": true, }).Info("Processing ledger") - - if sequence == 52885867 || sequence == 52885868 { - log.Error("We are in the problem zone.") - } - startTime := time.Now() if err = s.historyQ.Begin(s.ctx); err != nil { From 3694c35df79b252b7795971e97c0514810b111a8 Mon Sep 17 00:00:00 2001 From: George Date: Tue, 17 Sep 2024 11:01:41 -0700 Subject: [PATCH 5/9] Drop nonexisting endpoints and add /getledgerentryraw --- clients/stellarcore/client.go | 21 ++++--------------- clients/stellarcore/client_test.go | 14 ++++++------- .../stellarcore/getledgerentryraw_response.go | 11 ++++++++++ 3 files changed, 21 insertions(+), 25 deletions(-) create mode 100644 protocols/stellarcore/getledgerentryraw_response.go diff --git a/clients/stellarcore/client.go b/clients/stellarcore/client.go index 0d4e4765da..d954e5fcf6 100644 --- a/clients/stellarcore/client.go +++ b/clients/stellarcore/client.go @@ -150,22 +150,9 @@ func (c *Client) SetCursor(ctx context.Context, id string, cursor int32) (err er return nil } - -func (c *Client) GetLedgerEntries(ctx context.Context, ledgerSeq uint32, keys ...xdr.LedgerKey) (proto.GetLedgerEntriesResponse, error) { - var resp proto.GetLedgerEntriesResponse - return resp, c.makeLedgerKeyRequest(ctx, &resp, "getledgerentry", ledgerSeq, keys...) -} - -//lint:ignore U1000 Ignore unused function until it's supported in Core -func (c *Client) getInvocationProof(ctx context.Context, ledgerSeq uint32, keys ...xdr.LedgerKey) (proto.ProofResponse, error) { - var resp proto.ProofResponse - return resp, c.makeLedgerKeyRequest(ctx, &resp, "getinvocationproof", ledgerSeq, keys...) -} - -//lint:ignore U1000 Ignore unused function until it's supported in Core -func (c *Client) getRestorationProof(ctx context.Context, ledgerSeq uint32, keys ...xdr.LedgerKey) (proto.ProofResponse, error) { - var resp proto.ProofResponse - return resp, c.makeLedgerKeyRequest(ctx, &resp, "getrestorationproof", ledgerSeq, keys...) +func (c *Client) GetLedgerEntryRaw(ctx context.Context, ledgerSeq uint32, keys ...xdr.LedgerKey) (proto.GetLedgerEntryRawResponse, error) { + var resp proto.GetLedgerEntryRawResponse + return resp, c.makeLedgerKeyRequest(ctx, &resp, "getledgerentryraw", ledgerSeq, keys...) } // SubmitTransaction calls the `tx` command on the connected stellar core with the provided envelope @@ -333,7 +320,7 @@ func (c *Client) rawPost( } // makeLedgerKeyRequest is a generic method to perform a request in the form -// `key=...&key=...&ledgerSeq=...` which is useful because three Stellar Core +// `key=...&key=...&ledgerSeq=...` which is useful because several Stellar Core // endpoints all use this request format. Be sure to pass `target` by reference. func (c *Client) makeLedgerKeyRequest( ctx context.Context, diff --git a/clients/stellarcore/client_test.go b/clients/stellarcore/client_test.go index aa068610b1..7f6b01983a 100644 --- a/clients/stellarcore/client_test.go +++ b/clients/stellarcore/client_test.go @@ -84,22 +84,20 @@ func TestGetLedgerEntries(t *testing.T) { c := &Client{HTTP: hmock, URL: "http://localhost:11626"} // build a fake response body - mockResp := proto.GetLedgerEntriesResponse{ + mockResp := proto.GetLedgerEntryRawResponse{ Ledger: 1215, // checkpoint align on expected request - Entries: []proto.LedgerEntryResponse{ + Entries: []proto.RawLedgerEntryResponse{ { Entry: "pretend this is XDR lol", - State: proto.DeadState, }, { Entry: "pretend this is another XDR lol", - State: proto.ArchivedStateNoProof, }, }, } // happy path - fetch an entry - hmock.On("POST", "http://localhost:11626/getledgerentry"). + hmock.On("POST", "http://localhost:11626/getledgerentryraw"). ReturnJSON(http.StatusOK, &mockResp) var key xdr.LedgerKey @@ -107,12 +105,12 @@ func TestGetLedgerEntries(t *testing.T) { require.NoError(t, err) key.SetAccount(acc) - resp, err := c.GetLedgerEntries(context.Background(), 1234, key) + resp, err := c.GetLedgerEntryRaw(context.Background(), 1234, key) require.NoError(t, err) require.NotNil(t, resp) require.EqualValues(t, 1215, resp.Ledger) require.Len(t, resp.Entries, 2) - require.Equal(t, resp.Entries[0].State, proto.DeadState) - require.Equal(t, resp.Entries[1].State, proto.ArchivedStateNoProof) + require.Equal(t, "pretend this is XDR lol", resp.Entries[0].Entry) + require.Equal(t, "pretend this is another XDR lol", resp.Entries[1].Entry) } diff --git a/protocols/stellarcore/getledgerentryraw_response.go b/protocols/stellarcore/getledgerentryraw_response.go new file mode 100644 index 0000000000..f4d31a635f --- /dev/null +++ b/protocols/stellarcore/getledgerentryraw_response.go @@ -0,0 +1,11 @@ +package stellarcore + +// GetLedgerEntryRawResponse is the structure of Stellar Core's /getledgerentryraw +type GetLedgerEntryRawResponse struct { + Ledger uint32 `json:"ledger"` + Entries []RawLedgerEntryResponse `json:"entries"` +} + +type RawLedgerEntryResponse struct { + Entry string `json:"le"` // base64-encoded xdr.LedgerEntry +} From 144533aa7eff9a499eb31db1c56ea082cc4b40a9 Mon Sep 17 00:00:00 2001 From: George Date: Tue, 17 Sep 2024 11:06:59 -0700 Subject: [PATCH 6/9] Drop unused proof schemas --- .../stellarcore/getledgerentries_response.go | 38 ------------------- protocols/stellarcore/proof_response.go | 6 --- 2 files changed, 44 deletions(-) delete mode 100644 protocols/stellarcore/getledgerentries_response.go delete mode 100644 protocols/stellarcore/proof_response.go diff --git a/protocols/stellarcore/getledgerentries_response.go b/protocols/stellarcore/getledgerentries_response.go deleted file mode 100644 index 71635f1eea..0000000000 --- a/protocols/stellarcore/getledgerentries_response.go +++ /dev/null @@ -1,38 +0,0 @@ -package stellarcore - -const ( - // LiveState represents the state value returned by stellar-core when a - // ledger entry is live - LiveState = "live" - - // DeadState represents the state value returned by stellar-core when a - // ledger entry is dead - DeadState = "dead" - - // NewStateNoProof indicates the entry is new and does NOT require a proof - // of non-existence. - NewStateNoProof = "new_entry_no_proof" - - // NewStateNeedsProof indicates the entry is new and DOES require a proof of - // non-existence. - NewStateNeedsProof = "new_entry_proof" - - // ArchivedStateNoProof indicates the entry is archived and does NOT require - // a proof of existence. - ArchivedStateNoProof = "archived_no_proof" - - // ArchivedStateNeedsProof indicates the entry is archived and DOES require - // a proof of non-existence. - ArchivedStateNeedsProof = "archived_proof" -) - -// GetLedgerEntriesResponse is the response from Stellar Core for the getledgerentries endpoint -type GetLedgerEntriesResponse struct { - Ledger uint32 `json:"ledger"` - Entries []LedgerEntryResponse `json:"entries"` -} - -type LedgerEntryResponse struct { - Entry string `json:"e"` // base64-encoded xdr.LedgerEntry - State string `json:"state"` // one of the above State constants -} diff --git a/protocols/stellarcore/proof_response.go b/protocols/stellarcore/proof_response.go deleted file mode 100644 index 5b2bd6c659..0000000000 --- a/protocols/stellarcore/proof_response.go +++ /dev/null @@ -1,6 +0,0 @@ -package stellarcore - -type ProofResponse struct { - Ledger uint32 `json:"ledger"` - Proof string `json:"proof,omitempty"` -} From fe204eb2dc37f76a03822e1c856f92349eecec7c Mon Sep 17 00:00:00 2001 From: George Date: Mon, 23 Sep 2024 13:17:08 -0700 Subject: [PATCH 7/9] Feedback: better building, optional param fix --- clients/stellarcore/client.go | 36 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/clients/stellarcore/client.go b/clients/stellarcore/client.go index d954e5fcf6..80938da577 100644 --- a/clients/stellarcore/client.go +++ b/clients/stellarcore/client.go @@ -150,6 +150,7 @@ func (c *Client) SetCursor(ctx context.Context, id string, cursor int32) (err er return nil } + func (c *Client) GetLedgerEntryRaw(ctx context.Context, ledgerSeq uint32, keys ...xdr.LedgerKey) (proto.GetLedgerEntryRawResponse, error) { var resp proto.GetLedgerEntryRawResponse return resp, c.makeLedgerKeyRequest(ctx, &resp, "getledgerentryraw", ledgerSeq, keys...) @@ -296,7 +297,7 @@ func (c *Client) simpleGet( func (c *Client) rawPost( ctx context.Context, newPath string, - params string, + body string, ) (*http.Request, error) { u, err := url.Parse(c.URL) if err != nil { @@ -311,7 +312,7 @@ func (c *Client) rawPost( ctx, http.MethodPost, newURL, - bytes.NewBuffer([]byte(params))) + bytes.NewBuffer([]byte(body))) if err != nil { return nil, errors.Wrap(err, "failed to create request") } @@ -329,11 +330,16 @@ func (c *Client) makeLedgerKeyRequest( ledgerSeq uint32, keys ...xdr.LedgerKey, ) error { + if len(keys) == 0 { + return errors.New("no keys specified in request") + } + q, err := buildMultiKeyRequest(keys...) if err != nil { return err - } else if ledgerSeq >= 2 { // optional param - q += fmt.Sprintf("ledgerSeq=%d", ledgerSeq) + } + if ledgerSeq >= 2 { // optional param + q += fmt.Sprintf("&ledgerSeq=%d", ledgerSeq) } var req *http.Request @@ -361,28 +367,16 @@ func (c *Client) makeLedgerKeyRequest( // url.Values does not support multiple keys via Set(), so we have to build our // URL parameters manually. func buildMultiKeyRequest(keys ...xdr.LedgerKey) (string, error) { - // The average ledger key length, according to a simple - // - // SELECT AVG(LENGTH(HEX(key))) / 2 FROM ledger_entries; - // - // on a pubnet RPC instance is ~57.6. We can use this to preallocate a - // string buffer for performance. - // - // We know that these endpoints will almost exclusively be used for - // ContractData and the like, so we could optimize the buffer further for - // that, but that data is harder to query since it'd involve parsing the XDR - // from the DB to check the key type. - q := strings.Builder{} - q.Grow(50 * len(keys)) + stringKeys := make([]string, 0, len(keys)) for _, key := range keys { keyB64, err := key.MarshalBinaryBase64() if err != nil { - return q.String(), errors.Wrap(err, "failed to encode LedgerKey") + return "", errors.Wrap(err, "failed to encode LedgerKey") } - q.WriteString("key=" + url.QueryEscape(keyB64) + "&") + + stringKeys = append(stringKeys, "key="+url.QueryEscape(keyB64)) } - s, _ := strings.CutSuffix(q.String(), "&") // trim trailing & - return s, nil + return strings.Join(stringKeys, "&"), nil } From 4da6f4ce963926b3dd0c57e7b2cc0604bc52a680 Mon Sep 17 00:00:00 2001 From: George Date: Tue, 24 Sep 2024 12:12:34 -0700 Subject: [PATCH 8/9] Add test for request body --- clients/stellarcore/client_test.go | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/clients/stellarcore/client_test.go b/clients/stellarcore/client_test.go index 7f6b01983a..af7fe7e49c 100644 --- a/clients/stellarcore/client_test.go +++ b/clients/stellarcore/client_test.go @@ -2,9 +2,13 @@ package stellarcore import ( "context" + "fmt" + "io" "net/http" + "net/url" "testing" + "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -96,15 +100,32 @@ func TestGetLedgerEntries(t *testing.T) { }, } - // happy path - fetch an entry - hmock.On("POST", "http://localhost:11626/getledgerentryraw"). - ReturnJSON(http.StatusOK, &mockResp) - var key xdr.LedgerKey acc, err := xdr.AddressToAccountId(keypair.MustRandom().Address()) require.NoError(t, err) key.SetAccount(acc) + // happy path - fetch an entry + ce := hmock.On("POST", "http://localhost:11626/getledgerentryraw") + hmock.RegisterResponder( + "POST", + "http://localhost:11626/getledgerentryraw", + func(r *http.Request) (*http.Response, error) { + // Ensure the request has the correct POST body + requestData, err := io.ReadAll(r.Body) + require.NoError(t, err) + + keyB64, err := key.MarshalBinaryBase64() + require.NoError(t, err) + expected := fmt.Sprintf("key=%s&ledgerSeq=1234", url.QueryEscape(keyB64)) + require.Equal(t, expected, string(requestData)) + + resp, err := httpmock.NewJsonResponse(http.StatusOK, &mockResp) + require.NoError(t, err) + ce.Return(httpmock.ResponderFromResponse(resp)) + return resp, nil + }) + resp, err := c.GetLedgerEntryRaw(context.Background(), 1234, key) require.NoError(t, err) require.NotNil(t, resp) From 63d475692601fbb4460b9a51b7f6a33afe398135 Mon Sep 17 00:00:00 2001 From: George Date: Tue, 24 Sep 2024 12:30:32 -0700 Subject: [PATCH 9/9] Whiny ass go vet --- clients/stellarcore/client_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/clients/stellarcore/client_test.go b/clients/stellarcore/client_test.go index af7fe7e49c..47540b2464 100644 --- a/clients/stellarcore/client_test.go +++ b/clients/stellarcore/client_test.go @@ -112,16 +112,16 @@ func TestGetLedgerEntries(t *testing.T) { "http://localhost:11626/getledgerentryraw", func(r *http.Request) (*http.Response, error) { // Ensure the request has the correct POST body - requestData, err := io.ReadAll(r.Body) - require.NoError(t, err) + requestData, ierr := io.ReadAll(r.Body) + require.NoError(t, ierr) - keyB64, err := key.MarshalBinaryBase64() - require.NoError(t, err) + keyB64, ierr := key.MarshalBinaryBase64() + require.NoError(t, ierr) expected := fmt.Sprintf("key=%s&ledgerSeq=1234", url.QueryEscape(keyB64)) require.Equal(t, expected, string(requestData)) - resp, err := httpmock.NewJsonResponse(http.StatusOK, &mockResp) - require.NoError(t, err) + resp, ierr := httpmock.NewJsonResponse(http.StatusOK, &mockResp) + require.NoError(t, ierr) ce.Return(httpmock.ResponderFromResponse(resp)) return resp, nil })