From 3a0bd721f5f042259f57ebea96b533e3e28b5559 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 7 Sep 2021 14:48:14 -0700 Subject: [PATCH 1/3] txnbuild: implement encoding.Text{M,Unm}arshaler in Transaction, FeeBumpTransaction, GenericTransaction (#3897) Implement `encoding.TextMarshaler` and `encoding.TextUnmarshaler` in `Transaction`, `FeeBumpTransaction`, `GenericTransaction`. Transaction, FeeBumpTransaction, and GenericTransaction all have well defined formats for serialization as strings. It would be convenient if these types were automatically serializable via encoders such as the `encoding/json` package. Constructors for GenericTransaction are added so that code building Transaction and FeeBumpTransaction can get a GenericTransaction for those built transactions, for setting in a field that can hold both, in a type supporting serialization. --- txnbuild/CHANGELOG.md | 3 + txnbuild/transaction.go | 86 +++++++++++++++++++++++ txnbuild/transaction_test.go | 132 +++++++++++++++++++++++++++++++++++ 3 files changed, 221 insertions(+) diff --git a/txnbuild/CHANGELOG.md b/txnbuild/CHANGELOG.md index 24467fced4..3c89431c46 100644 --- a/txnbuild/CHANGELOG.md +++ b/txnbuild/CHANGELOG.md @@ -6,6 +6,9 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +* GenericTransaction, Transaction, and FeeBumpTransaction now implement +encoding.TextMarshaler and encoding.TextUnmarshaler. + ## [v7.1.1](https://github.com/stellar/go/releases/tag/horizonclient-v7.1.1) - 2021-06-25 ### Bug Fixes diff --git a/txnbuild/transaction.go b/txnbuild/transaction.go index ec04549636..35777e767a 100644 --- a/txnbuild/transaction.go +++ b/txnbuild/transaction.go @@ -189,6 +189,17 @@ func marshallBase64(e xdr.TransactionEnvelope, signatures []xdr.DecoratedSignatu return base64.StdEncoding.EncodeToString(binary), nil } +func marshallBase64Bytes(e xdr.TransactionEnvelope, signatures []xdr.DecoratedSignature) ([]byte, error) { + binary, err := marshallBinary(e, signatures) + if err != nil { + return nil, errors.Wrap(err, "failed to get XDR bytestring") + } + + encoded := make([]byte, base64.StdEncoding.EncodedLen(len(binary))) + base64.StdEncoding.Encode(encoded, binary) + return encoded, nil +} + // Transaction represents a Stellar transaction. See // https://www.stellar.org/developers/guides/concepts/transactions.html // A Transaction may be wrapped by a FeeBumpTransaction in which case @@ -346,11 +357,36 @@ func (t *Transaction) MarshalBinary() ([]byte, error) { return marshallBinary(t.envelope, t.Signatures()) } +// MarshalText returns the base64 XDR representation of the transaction envelope. +func (t *Transaction) MarshalText() ([]byte, error) { + return marshallBase64Bytes(t.envelope, t.Signatures()) +} + +// UnmarshalText consumes into the value the base64 XDR representation of the +// transaction envelope. +func (t *Transaction) UnmarshalText(b []byte) error { + gtx, err := TransactionFromXDR(string(b)) + if err != nil { + return err + } + tx, ok := gtx.Transaction() + if !ok { + return errors.New("transaction envelope unmarshaled into FeeBumpTransaction is not a fee bump transaction") + } + *t = *tx + return nil +} + // Base64 returns the base 64 XDR representation of the transaction envelope. func (t *Transaction) Base64() (string, error) { return marshallBase64(t.envelope, t.Signatures()) } +// ToGenericTransaction creates a GenericTransaction containing the Transaction. +func (t *Transaction) ToGenericTransaction() *GenericTransaction { + return &GenericTransaction{simple: t} +} + // ClaimableBalanceID returns the claimable balance ID for the operation at the given index within the transaction. // given index (which should be a `CreateClaimableBalance` operation). func (t *Transaction) ClaimableBalanceID(operationIndex int) (string, error) { @@ -506,11 +542,38 @@ func (t *FeeBumpTransaction) MarshalBinary() ([]byte, error) { return marshallBinary(t.envelope, t.Signatures()) } +// MarshalText returns the base64 XDR representation of the transaction +// envelope. +func (t *FeeBumpTransaction) MarshalText() ([]byte, error) { + return marshallBase64Bytes(t.envelope, t.Signatures()) +} + +// UnmarshalText consumes into the value the base64 XDR representation of the +// transaction envelope. +func (t *FeeBumpTransaction) UnmarshalText(b []byte) error { + gtx, err := TransactionFromXDR(string(b)) + if err != nil { + return err + } + fbtx, ok := gtx.FeeBump() + if !ok { + return errors.New("transaction envelope unmarshaled into Transaction is not a transaction") + } + *t = *fbtx + return nil +} + // Base64 returns the base 64 XDR representation of the transaction envelope. func (t *FeeBumpTransaction) Base64() (string, error) { return marshallBase64(t.envelope, t.Signatures()) } +// ToGenericTransaction creates a GenericTransaction containing the +// FeeBumpTransaction. +func (t *FeeBumpTransaction) ToGenericTransaction() *GenericTransaction { + return &GenericTransaction{feeBump: t} +} + // InnerTransaction returns the Transaction which is wrapped by // this FeeBumpTransaction instance. func (t *FeeBumpTransaction) InnerTransaction() *Transaction { @@ -540,6 +603,29 @@ func (t GenericTransaction) FeeBump() (*FeeBumpTransaction, bool) { return t.feeBump, t.feeBump != nil } +// MarshalText returns the base64 XDR representation of the transaction +// envelope. +func (t *GenericTransaction) MarshalText() ([]byte, error) { + if tx, ok := t.Transaction(); ok { + return tx.MarshalText() + } + if fbtx, ok := t.FeeBump(); ok { + return fbtx.MarshalText() + } + return nil, errors.New("unable to marshal empty GenericTransaction") +} + +// UnmarshalText consumes into the value the base64 XDR representation of the +// transaction envelope. +func (t *GenericTransaction) UnmarshalText(b []byte) error { + gtx, err := TransactionFromXDR(string(b)) + if err != nil { + return err + } + *t = *gtx + return nil +} + type TransactionFromXDROption int const ( diff --git a/txnbuild/transaction_test.go b/txnbuild/transaction_test.go index 4a4e8cad45..f1b6274ec8 100644 --- a/txnbuild/transaction_test.go +++ b/txnbuild/transaction_test.go @@ -4417,3 +4417,135 @@ func TestClaimableBalanceIds(t *testing.T) { assert.Equal(t, actualBalanceId, calculatedBalanceId) } + +func TestTransaction_marshalUnmarshalText(t *testing.T) { + k := keypair.MustRandom() + + tx, err := NewTransaction( + TransactionParams{ + SourceAccount: &SimpleAccount{AccountID: k.Address(), Sequence: 1}, + IncrementSequenceNum: false, + BaseFee: MinBaseFee, + Timebounds: NewInfiniteTimeout(), + Operations: []Operation{&BumpSequence{BumpTo: 2}}, + }, + ) + assert.NoError(t, err) + tx, err = tx.Sign(network.TestNetworkPassphrase, k) + assert.NoError(t, err) + + b64, err := tx.Base64() + require.NoError(t, err) + t.Log("tx base64:", b64) + + marshaled, err := tx.MarshalText() + require.NoError(t, err) + t.Log("tx marshaled text:", string(marshaled)) + assert.Equal(t, b64, string(marshaled)) + + tx2 := &Transaction{} + err = tx2.UnmarshalText(marshaled) + require.NoError(t, err) + assert.Equal(t, tx, tx2) + + err = (&FeeBumpTransaction{}).UnmarshalText(marshaled) + assert.EqualError(t, err, "transaction envelope unmarshaled into Transaction is not a transaction") +} + +func TestFeeBumpTransaction_marshalUnmarshalText(t *testing.T) { + k := keypair.MustRandom() + + tx, err := NewTransaction( + TransactionParams{ + SourceAccount: &SimpleAccount{AccountID: k.Address(), Sequence: 1}, + IncrementSequenceNum: false, + BaseFee: MinBaseFee, + Timebounds: NewInfiniteTimeout(), + Operations: []Operation{&BumpSequence{BumpTo: 2}}, + }, + ) + require.NoError(t, err) + tx, err = tx.Sign(network.TestNetworkPassphrase, k) + require.NoError(t, err) + + fbtx, err := NewFeeBumpTransaction(FeeBumpTransactionParams{ + Inner: tx, + FeeAccount: k.Address(), + BaseFee: MinBaseFee, + }) + require.NoError(t, err) + + b64, err := fbtx.Base64() + require.NoError(t, err) + t.Log("tx base64:", b64) + + marshaled, err := fbtx.MarshalText() + require.NoError(t, err) + t.Log("tx marshaled text:", string(marshaled)) + assert.Equal(t, b64, string(marshaled)) + + fbtx2 := &FeeBumpTransaction{} + err = fbtx2.UnmarshalText(marshaled) + require.NoError(t, err) + assert.Equal(t, fbtx, fbtx2) + + err = (&Transaction{}).UnmarshalText(marshaled) + assert.EqualError(t, err, "transaction envelope unmarshaled into FeeBumpTransaction is not a fee bump transaction") +} + +func TestGenericTransaction_marshalUnmarshalText(t *testing.T) { + k := keypair.MustRandom() + + // GenericTransaction containing nothing. + gtx := &GenericTransaction{} + _, err := gtx.MarshalText() + assert.EqualError(t, err, "unable to marshal empty GenericTransaction") + + // GenericTransaction containing a Transaction. + tx, err := NewTransaction( + TransactionParams{ + SourceAccount: &SimpleAccount{AccountID: k.Address(), Sequence: 1}, + IncrementSequenceNum: false, + BaseFee: MinBaseFee, + Timebounds: NewInfiniteTimeout(), + Operations: []Operation{&BumpSequence{BumpTo: 2}}, + }, + ) + require.NoError(t, err) + tx, err = tx.Sign(network.TestNetworkPassphrase, k) + require.NoError(t, err) + txb64, err := tx.Base64() + require.NoError(t, err) + t.Log("tx base64:", txb64) + + gtx = tx.ToGenericTransaction() + marshaled, err := gtx.MarshalText() + require.NoError(t, err) + t.Log("tx marshaled text:", string(marshaled)) + assert.Equal(t, txb64, string(marshaled)) + gtx2 := &GenericTransaction{} + err = gtx2.UnmarshalText(marshaled) + require.NoError(t, err) + assert.Equal(t, gtx, gtx2) + + // GenericTransaction containing a FeeBumpTransaction. + fbtx, err := NewFeeBumpTransaction(FeeBumpTransactionParams{ + Inner: tx, + FeeAccount: k.Address(), + BaseFee: MinBaseFee, + }) + require.NoError(t, err) + fbtxb64, err := fbtx.Base64() + require.NoError(t, err) + t.Log("fbtx base64:", fbtxb64) + + fbgtx := fbtx.ToGenericTransaction() + marshaled, err = fbgtx.MarshalText() + require.NoError(t, err) + t.Log("fbtx marshaled text:", string(marshaled)) + assert.Equal(t, fbtxb64, string(marshaled)) + fbgtx2 := &GenericTransaction{} + err = fbgtx2.UnmarshalText(marshaled) + require.NoError(t, err) + assert.Equal(t, fbgtx, fbgtx2) +} From ea9fc73d96d0c60fef0a4278a52ca2aad26e0489 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 7 Sep 2021 15:46:00 -0700 Subject: [PATCH 2/3] clients/horizonclient: allow Fund on other networks and rely on server to indicate support (#3891) Allow the `Fund` function to submit a fund request to any horizon instance, not just the instance defined by `horizonclient.DefaultTestNetClient`, and rely on the horizon instance responding with a 404 as an indicator that funding is not supported. The only way to construct a `horizonclient.Client` that supports the `Fund` operation is to use and mutate the `horizonclient.DefaultTestNetClient`. This is unnecessarily restrictive because a developer may wish to use the `Fund` operation with a non-default client that they have constructed themselves, or they may wish to use the `Fund` operation with their own private or standalone test network. --- clients/horizonclient/CHANGELOG.md | 6 ++++++ clients/horizonclient/client.go | 6 +++--- clients/horizonclient/client_fund_test.go | 24 ++++++++++++++++++++++- clients/horizonclient/main.go | 2 -- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/clients/horizonclient/CHANGELOG.md b/clients/horizonclient/CHANGELOG.md index fc47bb2510..9db3a0fdb3 100644 --- a/clients/horizonclient/CHANGELOG.md +++ b/clients/horizonclient/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased + +* The restriction that `Fund` can only be called on the DefaultTestNetClient has +been removed. Any horizonclient.Client may now call Fund. Horizon instances not +supporting Fund will error with a resource not found error. + ## [v7.1.1](https://github.com/stellar/go/releases/tag/horizonclient-v7.1.1) - 2021-06-25 * Added transaction and operation result codes to the horizonclient.Error string for easy glancing at string only errors for underlying cause. diff --git a/clients/horizonclient/client.go b/clients/horizonclient/client.go index 3d0c9c56b2..5f9accc61b 100644 --- a/clients/horizonclient/client.go +++ b/clients/horizonclient/client.go @@ -575,11 +575,11 @@ func (c *Client) Trades(request TradeRequest) (tds hProtocol.TradesPage, err err // Fund creates a new account funded from friendbot. It only works on test networks. See // https://www.stellar.org/developers/guides/get-started/create-account.html for more information. func (c *Client) Fund(addr string) (tx hProtocol.Transaction, err error) { - if !c.isTestNet { - return tx, errors.New("can't fund account from friendbot on production network") - } friendbotURL := fmt.Sprintf("%sfriendbot?addr=%s", c.fixHorizonURL(), addr) err = c.sendGetRequest(friendbotURL, &tx) + if IsNotFoundError(err) { + return tx, errors.Wrap(err, "funding is only available on test networks and may not be supported by "+c.fixHorizonURL()) + } return } diff --git a/clients/horizonclient/client_fund_test.go b/clients/horizonclient/client_fund_test.go index dfcbad8981..528ba26f17 100644 --- a/clients/horizonclient/client_fund_test.go +++ b/clients/horizonclient/client_fund_test.go @@ -25,7 +25,6 @@ func TestFund(t *testing.T) { client := &Client{ HorizonURL: "https://localhost/", HTTP: hmock, - isTestNet: true, } hmock.On( @@ -37,3 +36,26 @@ func TestFund(t *testing.T) { assert.NoError(t, err) assert.Equal(t, int32(8269), tx.Ledger) } + +func TestFund_notSupported(t *testing.T) { + friendbotFundResponse := `{ + "type": "https://stellar.org/horizon-errors/not_found", + "title": "Resource Missing", + "status": 404, + "detail": "The resource at the url requested was not found. This usually occurs for one of two reasons: The url requested is not valid, or no data in our database could be found with the parameters provided." +}` + + hmock := httptest.NewClient() + client := &Client{ + HorizonURL: "https://localhost/", + HTTP: hmock, + } + + hmock.On( + "GET", + "https://localhost/friendbot?addr=GBLPP2W3X3PJQXYMC7EFWM5G2QCZL7HTCTFNMONS4ITGAYJ3GNNZIQ4V", + ).ReturnString(404, friendbotFundResponse) + + _, err := client.Fund("GBLPP2W3X3PJQXYMC7EFWM5G2QCZL7HTCTFNMONS4ITGAYJ3GNNZIQ4V") + assert.EqualError(t, err, "funding is only available on test networks and may not be supported by https://localhost/: horizon error: \"Resource Missing\" - check horizon.Error.Problem for more information") +} diff --git a/clients/horizonclient/main.go b/clients/horizonclient/main.go index c81572dd71..f3f51313af 100644 --- a/clients/horizonclient/main.go +++ b/clients/horizonclient/main.go @@ -141,7 +141,6 @@ type Client struct { // AppVersion is the version of the application using the horizonclient package AppVersion string horizonTimeout time.Duration - isTestNet bool // clock is a Clock returning the current time. clock *clock.Clock @@ -215,7 +214,6 @@ var DefaultTestNetClient = &Client{ HorizonURL: "https://horizon-testnet.stellar.org/", HTTP: http.DefaultClient, horizonTimeout: HorizonTimeout, - isTestNet: true, } // DefaultPublicNetClient is a default client to connect to public network. From e011c915474e7d742c5840827eaee3c481f44adf Mon Sep 17 00:00:00 2001 From: Bartek Nowotarski Date: Thu, 9 Sep 2021 11:28:19 +0200 Subject: [PATCH 3/3] services/horizon: Split App.UpdateLedgerState into Core and Horizon ledger update methods (#3900) This commit splits `UpdateLedgerState` into `UpdateCoreLedgerState` and `UpdateHorizonLedgerState` which update Core and Horizon ledger state separately. It also changes `Tick` frequency to 5 seconds. If Stellar-Core is unresponsive the call to Stellar-Core `/info` endpoint will be cancelled after 10 seconds but it will also return entire `UpdateLedgerState` method. This will make Horizon ledger stats out of date which can affect streaming (because streams are updated only when Horizon ledger in `ledger.Status` is incremented). Known limitations: with this change it's possible that Horizon root can report Core ledger sequence behind Horizon. --- services/horizon/cmd/db.go | 2 +- .../horizon/internal/actions_root_test.go | 5 +-- services/horizon/internal/app.go | 31 ++++++++++++++----- services/horizon/internal/httpt_test.go | 6 ++-- .../internal/ledger/ledger_source_test.go | 12 +++++-- services/horizon/internal/ledger/main.go | 24 +++++++++++++- services/horizon/internal/middleware_test.go | 8 +++-- .../internal/resourceadapter/root_test.go | 27 ++++++++++++++-- 8 files changed, 94 insertions(+), 21 deletions(-) diff --git a/services/horizon/cmd/db.go b/services/horizon/cmd/db.go index 40e88a4cac..024063a35f 100644 --- a/services/horizon/cmd/db.go +++ b/services/horizon/cmd/db.go @@ -206,7 +206,7 @@ var dbReapCmd = &cobra.Command{ return err } ctx := context.Background() - app.UpdateLedgerState(ctx) + app.UpdateHorizonLedgerState(ctx) return app.DeleteUnretainedHistory(ctx) }, } diff --git a/services/horizon/internal/actions_root_test.go b/services/horizon/internal/actions_root_test.go index f4d521627e..440276e637 100644 --- a/services/horizon/internal/actions_root_test.go +++ b/services/horizon/internal/actions_root_test.go @@ -30,7 +30,8 @@ func TestRootAction(t *testing.T) { ht.App.config.StellarCoreURL = server.URL ht.App.config.NetworkPassphrase = "test" assert.NoError(t, ht.App.UpdateStellarCoreInfo(ht.Ctx)) - ht.App.UpdateLedgerState(ht.Ctx) + ht.App.UpdateCoreLedgerState(ht.Ctx) + ht.App.UpdateHorizonLedgerState(ht.Ctx) w := ht.Get("/") @@ -95,7 +96,7 @@ func TestRootCoreClientInfoErrored(t *testing.T) { defer server.Close() ht.App.config.StellarCoreURL = server.URL - ht.App.UpdateLedgerState(ht.Ctx) + ht.App.UpdateCoreLedgerState(ht.Ctx) w := ht.Get("/") diff --git a/services/horizon/internal/app.go b/services/horizon/internal/app.go index 09a309bfad..929f3f16fb 100644 --- a/services/horizon/internal/app.go +++ b/services/horizon/internal/app.go @@ -66,7 +66,7 @@ func (a *App) GetCoreState() corestate.State { } const tickerMaxFrequency = 1 * time.Second -const tickerMaxDuration = 10 * time.Second +const tickerMaxDuration = 5 * time.Second // NewApp constructs an new App instance from the provided config. func NewApp(config Config) (*App, error) { @@ -208,10 +208,11 @@ func (a *App) HorizonSession() db.SessionInterface { return a.historyQ.SessionInterface.Clone() } -// UpdateLedgerState triggers a refresh of several metrics gauges, such as open -// db connections and ledger state -func (a *App) UpdateLedgerState(ctx context.Context) { - var next ledger.Status +// UpdateCoreLedgerState triggers a refresh of Stellar-Core ledger state. +// This is done separately from Horizon ledger state update to prevent issues +// in case Stellar-Core query timeout. +func (a *App) UpdateCoreLedgerState(ctx context.Context) { + var next ledger.CoreStatus logErr := func(err error, msg string) { log.WithStack(err).WithField("err", err.Error()).Error(msg) @@ -228,7 +229,20 @@ func (a *App) UpdateLedgerState(ctx context.Context) { return } next.CoreLatest = int32(coreInfo.Info.Ledger.Num) + a.ledgerState.SetCoreStatus(next) +} + +// UpdateHorizonLedgerState triggers a refresh of Horizon ledger state. +// This is done separately from Core ledger state update to prevent issues +// in case Stellar-Core query timeout. +func (a *App) UpdateHorizonLedgerState(ctx context.Context) { + var next ledger.HorizonStatus + logErr := func(err error, msg string) { + log.WithStack(err).WithField("err", err.Error()).Error(msg) + } + + var err error next.HistoryLatest, next.HistoryLatestClosedAt, err = a.HistoryQ().LatestLedgerSequenceClosedAt(ctx) if err != nil { @@ -248,7 +262,7 @@ func (a *App) UpdateLedgerState(ctx context.Context) { return } - a.ledgerState.SetStatus(next) + a.ledgerState.SetHorizonStatus(next) } // UpdateFeeStatsState triggers a refresh of several operation fee metrics. @@ -419,9 +433,10 @@ func (a *App) Tick(ctx context.Context) error { log.Debug("ticking app") // update ledger state, operation fee state, and stellar-core info in parallel - wg.Add(3) + wg.Add(4) var err error - go func() { a.UpdateLedgerState(ctx); wg.Done() }() + go func() { a.UpdateCoreLedgerState(ctx); wg.Done() }() + go func() { a.UpdateHorizonLedgerState(ctx); wg.Done() }() go func() { a.UpdateFeeStatsState(ctx); wg.Done() }() go func() { err = a.UpdateStellarCoreInfo(ctx); wg.Done() }() wg.Wait() diff --git a/services/horizon/internal/httpt_test.go b/services/horizon/internal/httpt_test.go index 1799822d5d..73924a7530 100644 --- a/services/horizon/internal/httpt_test.go +++ b/services/horizon/internal/httpt_test.go @@ -42,7 +42,8 @@ func startHTTPTest(t *testing.T, scenario string) *HTTPT { }`) ret.App.config.StellarCoreURL = ret.coreServer.URL - ret.App.UpdateLedgerState(context.Background()) + ret.App.UpdateCoreLedgerState(context.Background()) + ret.App.UpdateHorizonLedgerState(context.Background()) return ret } @@ -101,5 +102,6 @@ func (ht *HTTPT) ReapHistory(retention uint) { ht.App.reaper.RetentionCount = retention err := ht.App.DeleteUnretainedHistory(context.Background()) ht.Require.NoError(err) - ht.App.UpdateLedgerState(context.Background()) + ht.App.UpdateCoreLedgerState(context.Background()) + ht.App.UpdateHorizonLedgerState(context.Background()) } diff --git a/services/horizon/internal/ledger/ledger_source_test.go b/services/horizon/internal/ledger/ledger_source_test.go index fe540aca3a..f7eedaa1df 100644 --- a/services/horizon/internal/ledger/ledger_source_test.go +++ b/services/horizon/internal/ledger/ledger_source_test.go @@ -8,7 +8,11 @@ import ( func Test_HistoryDBLedgerSourceCurrentLedger(t *testing.T) { state := &State{ RWMutex: sync.RWMutex{}, - current: Status{ExpHistoryLatest: 3}, + current: Status{ + HorizonStatus: HorizonStatus{ + ExpHistoryLatest: 3, + }, + }, } ledgerSource := HistoryDBSource{ @@ -25,7 +29,11 @@ func Test_HistoryDBLedgerSourceCurrentLedger(t *testing.T) { func Test_HistoryDBLedgerSourceNextLedger(t *testing.T) { state := &State{ RWMutex: sync.RWMutex{}, - current: Status{ExpHistoryLatest: 3}, + current: Status{ + HorizonStatus: HorizonStatus{ + ExpHistoryLatest: 3, + }, + }, } ledgerSource := HistoryDBSource{ diff --git a/services/horizon/internal/ledger/main.go b/services/horizon/internal/ledger/main.go index 7890f97193..f63c347fc2 100644 --- a/services/horizon/internal/ledger/main.go +++ b/services/horizon/internal/ledger/main.go @@ -13,7 +13,15 @@ import ( // Status represents a snapshot of both horizon's and stellar-core's view of the // ledger. type Status struct { - CoreLatest int32 `db:"core_latest"` + CoreStatus + HorizonStatus +} + +type CoreStatus struct { + CoreLatest int32 `db:"core_latest"` +} + +type HorizonStatus struct { HistoryLatest int32 `db:"history_latest"` HistoryLatestClosedAt time.Time `db:"history_latest_closed_at"` HistoryElder int32 `db:"history_elder"` @@ -41,3 +49,17 @@ func (c *State) SetStatus(next Status) { defer c.Unlock() c.current = next } + +// SetCoreStatus updates the cached snapshot of the ledger state of Stellar-Core +func (c *State) SetCoreStatus(next CoreStatus) { + c.Lock() + defer c.Unlock() + c.current.CoreStatus = next +} + +// SetHorizonStatus updates the cached snapshot of the ledger state of Horizon +func (c *State) SetHorizonStatus(next HorizonStatus) { + c.Lock() + defer c.Unlock() + c.current.HorizonStatus = next +} diff --git a/services/horizon/internal/middleware_test.go b/services/horizon/internal/middleware_test.go index 92472c2ca9..b411b23e3d 100644 --- a/services/horizon/internal/middleware_test.go +++ b/services/horizon/internal/middleware_test.go @@ -347,8 +347,12 @@ func TestCheckHistoryStaleMiddleware(t *testing.T) { } { t.Run(testCase.name, func(t *testing.T) { state := ledger.Status{ - CoreLatest: testCase.coreLatest, - HistoryLatest: testCase.historyLatest, + CoreStatus: ledger.CoreStatus{ + CoreLatest: testCase.coreLatest, + }, + HorizonStatus: ledger.HorizonStatus{ + HistoryLatest: testCase.historyLatest, + }, } ledgerState := &ledger.State{} ledgerState.SetStatus(state) diff --git a/services/horizon/internal/resourceadapter/root_test.go b/services/horizon/internal/resourceadapter/root_test.go index 1e7dc8205f..23bc1c5024 100644 --- a/services/horizon/internal/resourceadapter/root_test.go +++ b/services/horizon/internal/resourceadapter/root_test.go @@ -22,7 +22,14 @@ func TestPopulateRoot(t *testing.T) { PopulateRoot(context.Background(), res, - ledger.Status{CoreLatest: 1, HistoryLatest: 3, HistoryElder: 2}, + ledger.Status{ + CoreStatus: ledger.CoreStatus{ + CoreLatest: 1, + }, + HorizonStatus: ledger.HorizonStatus{ + HistoryLatest: 3, HistoryElder: 2, + }, + }, "hVersion", "cVersion", "passphrase", @@ -44,7 +51,14 @@ func TestPopulateRoot(t *testing.T) { res = &horizon.Root{} PopulateRoot(context.Background(), res, - ledger.Status{CoreLatest: 1, HistoryLatest: 3, HistoryElder: 2}, + ledger.Status{ + CoreStatus: ledger.CoreStatus{ + CoreLatest: 1, + }, + HorizonStatus: ledger.HorizonStatus{ + HistoryLatest: 3, HistoryElder: 2, + }, + }, "hVersion", "cVersion", "passphrase", @@ -65,7 +79,14 @@ func TestPopulateRoot(t *testing.T) { res = &horizon.Root{} PopulateRoot(context.Background(), res, - ledger.Status{CoreLatest: 1, HistoryLatest: 3, HistoryElder: 2}, + ledger.Status{ + CoreStatus: ledger.CoreStatus{ + CoreLatest: 1, + }, + HorizonStatus: ledger.HorizonStatus{ + HistoryLatest: 3, HistoryElder: 2, + }, + }, "hVersion", "cVersion", "passphrase",