From 854a8db58348807699d8f45e8a4d2624fe1b481f Mon Sep 17 00:00:00 2001 From: urvisavla Date: Mon, 4 Dec 2023 13:50:33 -0800 Subject: [PATCH 001/234] services/horizon: Remove the optimization that uses COPY for liquidity pool insertion and revert to the previous method (#5135) --- .../liquidity_pool_batch_insert_builder.go | 36 --------- .../internal/db2/history/liquidity_pools.go | 6 +- ...ock_liquidity_pool_batch_insert_builder.go | 21 ------ .../db2/history/mock_q_liquidity_pools.go | 5 -- .../internal/ingest/processor_runner_test.go | 8 -- .../liquidity_pools_change_processor.go | 20 ++--- .../liquidity_pools_change_processor_test.go | 75 +++++++------------ 7 files changed, 35 insertions(+), 136 deletions(-) delete mode 100644 services/horizon/internal/db2/history/liquidity_pool_batch_insert_builder.go delete mode 100644 services/horizon/internal/db2/history/mock_liquidity_pool_batch_insert_builder.go diff --git a/services/horizon/internal/db2/history/liquidity_pool_batch_insert_builder.go b/services/horizon/internal/db2/history/liquidity_pool_batch_insert_builder.go deleted file mode 100644 index 65472fa657..0000000000 --- a/services/horizon/internal/db2/history/liquidity_pool_batch_insert_builder.go +++ /dev/null @@ -1,36 +0,0 @@ -package history - -import ( - "context" - - "github.com/stellar/go/support/db" -) - -type LiquidityPoolBatchInsertBuilder interface { - Add(liquidityPool LiquidityPool) error - Exec(ctx context.Context) error -} - -type liquidityPoolBatchInsertBuilder struct { - session db.SessionInterface - builder db.FastBatchInsertBuilder - table string -} - -func (q *Q) NewLiquidityPoolBatchInsertBuilder() LiquidityPoolBatchInsertBuilder { - return &liquidityPoolBatchInsertBuilder{ - session: q, - builder: db.FastBatchInsertBuilder{}, - table: "liquidity_pools", - } -} - -// Add adds a new liquidity pool to the batch -func (i *liquidityPoolBatchInsertBuilder) Add(liquidityPool LiquidityPool) error { - return i.builder.RowStruct(liquidityPool) -} - -// Exec writes the batch of liquidity pools to the database. -func (i *liquidityPoolBatchInsertBuilder) Exec(ctx context.Context) error { - return i.builder.Exec(ctx, i.session, i.table) -} diff --git a/services/horizon/internal/db2/history/liquidity_pools.go b/services/horizon/internal/db2/history/liquidity_pools.go index c4d0a6e6c9..46e6ba59d3 100644 --- a/services/horizon/internal/db2/history/liquidity_pools.go +++ b/services/horizon/internal/db2/history/liquidity_pools.go @@ -37,10 +37,7 @@ type LiquidityPool struct { type LiquidityPoolAssetReserves []LiquidityPoolAssetReserve func (c LiquidityPoolAssetReserves) Value() (driver.Value, error) { - // Convert the byte array into a string as a workaround to bypass buggy encoding in the pq driver - // (More info about this bug here https://github.com/stellar/go/issues/5086#issuecomment-1773215436). - val, err := json.Marshal(c) - return string(val), err + return json.Marshal(c) } func (c *LiquidityPoolAssetReserves) Scan(value interface{}) error { @@ -94,7 +91,6 @@ type QLiquidityPools interface { FindLiquidityPoolByID(ctx context.Context, liquidityPoolID string) (LiquidityPool, error) GetUpdatedLiquidityPools(ctx context.Context, newerThanSequence uint32) ([]LiquidityPool, error) CompactLiquidityPools(ctx context.Context, cutOffSequence uint32) (int64, error) - NewLiquidityPoolBatchInsertBuilder() LiquidityPoolBatchInsertBuilder } // UpsertLiquidityPools upserts a batch of liquidity pools in the liquidity_pools table. diff --git a/services/horizon/internal/db2/history/mock_liquidity_pool_batch_insert_builder.go b/services/horizon/internal/db2/history/mock_liquidity_pool_batch_insert_builder.go deleted file mode 100644 index d0685ea381..0000000000 --- a/services/horizon/internal/db2/history/mock_liquidity_pool_batch_insert_builder.go +++ /dev/null @@ -1,21 +0,0 @@ -package history - -import ( - "context" - - "github.com/stretchr/testify/mock" -) - -type MockLiquidityPoolBatchInsertBuilder struct { - mock.Mock -} - -func (m *MockLiquidityPoolBatchInsertBuilder) Add(liquidityPool LiquidityPool) error { - a := m.Called(liquidityPool) - return a.Error(0) -} - -func (m *MockLiquidityPoolBatchInsertBuilder) Exec(ctx context.Context) error { - a := m.Called(ctx) - return a.Error(0) -} diff --git a/services/horizon/internal/db2/history/mock_q_liquidity_pools.go b/services/horizon/internal/db2/history/mock_q_liquidity_pools.go index d9ae946cee..7b64b24126 100644 --- a/services/horizon/internal/db2/history/mock_q_liquidity_pools.go +++ b/services/horizon/internal/db2/history/mock_q_liquidity_pools.go @@ -45,8 +45,3 @@ func (m *MockQLiquidityPools) CompactLiquidityPools(ctx context.Context, cutOffS a := m.Called(ctx, cutOffSequence) return a.Get(0).(int64), a.Error(1) } - -func (m *MockQLiquidityPools) NewLiquidityPoolBatchInsertBuilder() LiquidityPoolBatchInsertBuilder { - a := m.Called() - return a.Get(0).(LiquidityPoolBatchInsertBuilder) -} diff --git a/services/horizon/internal/ingest/processor_runner_test.go b/services/horizon/internal/ingest/processor_runner_test.go index 66bb316d18..aa250d29ba 100644 --- a/services/horizon/internal/ingest/processor_runner_test.go +++ b/services/horizon/internal/ingest/processor_runner_test.go @@ -598,13 +598,6 @@ func mockChangeProcessorBatchBuilders(q *mockDBQ, ctx context.Context, mockExec q.MockQClaimableBalances.On("NewClaimableBalanceBatchInsertBuilder"). Return(mockClaimableBalanceBatchInsertBuilder).Twice() - mockLiquidityPoolBatchInsertBuilder := &history.MockLiquidityPoolBatchInsertBuilder{} - if mockExec { - mockLiquidityPoolBatchInsertBuilder.On("Exec", ctx).Return(nil).Once() - } - q.MockQLiquidityPools.On("NewLiquidityPoolBatchInsertBuilder"). - Return(mockLiquidityPoolBatchInsertBuilder).Twice() - mockOfferBatchInsertBuilder := &history.MockOffersBatchInsertBuilder{} if mockExec { mockOfferBatchInsertBuilder.On("Exec", ctx).Return(nil).Once() @@ -630,7 +623,6 @@ func mockChangeProcessorBatchBuilders(q *mockDBQ, ctx context.Context, mockExec mockAccountsBatchInsertBuilder, mockClaimableBalanceBatchInsertBuilder, mockClaimableBalanceClaimantBatchInsertBuilder, - mockLiquidityPoolBatchInsertBuilder, mockOfferBatchInsertBuilder, mockAccountDataBatchInsertBuilder, mockTrustLinesBatchInsertBuilder, diff --git a/services/horizon/internal/ingest/processors/liquidity_pools_change_processor.go b/services/horizon/internal/ingest/processors/liquidity_pools_change_processor.go index 2387f77baf..c5e5252280 100644 --- a/services/horizon/internal/ingest/processors/liquidity_pools_change_processor.go +++ b/services/horizon/internal/ingest/processors/liquidity_pools_change_processor.go @@ -10,10 +10,9 @@ import ( ) type LiquidityPoolsChangeProcessor struct { - qLiquidityPools history.QLiquidityPools - cache *ingest.ChangeCompactor - sequence uint32 - batchInsertBuilder history.LiquidityPoolBatchInsertBuilder + qLiquidityPools history.QLiquidityPools + cache *ingest.ChangeCompactor + sequence uint32 } func NewLiquidityPoolsChangeProcessor(Q history.QLiquidityPools, sequence uint32) *LiquidityPoolsChangeProcessor { @@ -27,7 +26,6 @@ func NewLiquidityPoolsChangeProcessor(Q history.QLiquidityPools, sequence uint32 func (p *LiquidityPoolsChangeProcessor) reset() { p.cache = ingest.NewChangeCompactor() - p.batchInsertBuilder = p.qLiquidityPools.NewLiquidityPoolBatchInsertBuilder() } func (p *LiquidityPoolsChangeProcessor) ProcessChange(ctx context.Context, change ingest.Change) error { @@ -45,13 +43,13 @@ func (p *LiquidityPoolsChangeProcessor) ProcessChange(ctx context.Context, chang if err != nil { return errors.Wrap(err, "error in Commit") } + p.reset() } return nil } func (p *LiquidityPoolsChangeProcessor) Commit(ctx context.Context) error { - defer p.reset() changes := p.cache.GetChanges() var lps []history.LiquidityPool @@ -59,10 +57,7 @@ func (p *LiquidityPoolsChangeProcessor) Commit(ctx context.Context) error { switch { case change.Pre == nil && change.Post != nil: // Created - err := p.batchInsertBuilder.Add(p.ledgerEntryToRow(change.Post)) - if err != nil { - return errors.Wrap(err, "error adding to LiquidityPoolsBatchInsertBuilder") - } + lps = append(lps, p.ledgerEntryToRow(change.Post)) case change.Pre != nil && change.Post == nil: // Removed lp := p.ledgerEntryToRow(change.Pre) @@ -75,11 +70,6 @@ func (p *LiquidityPoolsChangeProcessor) Commit(ctx context.Context) error { } } - err := p.batchInsertBuilder.Exec(ctx) - if err != nil { - return errors.Wrap(err, "error executing LiquidityPoolsBatchInsertBuilder") - } - if len(lps) > 0 { if err := p.qLiquidityPools.UpsertLiquidityPools(ctx, lps); err != nil { return errors.Wrap(err, "error upserting liquidity pools") diff --git a/services/horizon/internal/ingest/processors/liquidity_pools_change_processor_test.go b/services/horizon/internal/ingest/processors/liquidity_pools_change_processor_test.go index 47a47ee6c5..4e7383b1fe 100644 --- a/services/horizon/internal/ingest/processors/liquidity_pools_change_processor_test.go +++ b/services/horizon/internal/ingest/processors/liquidity_pools_change_processor_test.go @@ -19,22 +19,16 @@ func TestLiquidityPoolsChangeProcessorTestSuiteState(t *testing.T) { type LiquidityPoolsChangeProcessorTestSuiteState struct { suite.Suite - ctx context.Context - processor *LiquidityPoolsChangeProcessor - mockQ *history.MockQLiquidityPools - sequence uint32 - mockLiquidityPoolBatchInsertBuilder *history.MockLiquidityPoolBatchInsertBuilder + ctx context.Context + processor *LiquidityPoolsChangeProcessor + mockQ *history.MockQLiquidityPools + sequence uint32 } func (s *LiquidityPoolsChangeProcessorTestSuiteState) SetupTest() { s.ctx = context.Background() s.mockQ = &history.MockQLiquidityPools{} - s.mockLiquidityPoolBatchInsertBuilder = &history.MockLiquidityPoolBatchInsertBuilder{} - s.mockQ. - On("NewLiquidityPoolBatchInsertBuilder"). - Return(s.mockLiquidityPoolBatchInsertBuilder) - s.mockLiquidityPoolBatchInsertBuilder.On("Exec", s.ctx).Return(nil) s.sequence = 456 s.processor = NewLiquidityPoolsChangeProcessor(s.mockQ, s.sequence) } @@ -91,7 +85,7 @@ func (s *LiquidityPoolsChangeProcessorTestSuiteState) TestCreatesLiquidityPools( }, LastModifiedLedger: 123, } - s.mockLiquidityPoolBatchInsertBuilder.On("Add", lp).Return(nil).Once() + s.mockQ.On("UpsertLiquidityPools", s.ctx, []history.LiquidityPool{lp}).Return(nil).Once() s.mockQ.On("CompactLiquidityPools", s.ctx, s.sequence-100).Return(int64(0), nil).Once() @@ -115,23 +109,16 @@ func TestLiquidityPoolsChangeProcessorTestSuiteLedger(t *testing.T) { type LiquidityPoolsChangeProcessorTestSuiteLedger struct { suite.Suite - ctx context.Context - processor *LiquidityPoolsChangeProcessor - mockQ *history.MockQLiquidityPools - sequence uint32 - mockLiquidityPoolBatchInsertBuilder *history.MockLiquidityPoolBatchInsertBuilder + ctx context.Context + processor *LiquidityPoolsChangeProcessor + mockQ *history.MockQLiquidityPools + sequence uint32 } func (s *LiquidityPoolsChangeProcessorTestSuiteLedger) SetupTest() { s.ctx = context.Background() s.mockQ = &history.MockQLiquidityPools{} - s.mockLiquidityPoolBatchInsertBuilder = &history.MockLiquidityPoolBatchInsertBuilder{} - s.mockQ. - On("NewLiquidityPoolBatchInsertBuilder"). - Return(s.mockLiquidityPoolBatchInsertBuilder).Twice() - s.mockLiquidityPoolBatchInsertBuilder.On("Exec", s.ctx).Return(nil).Once() - s.sequence = 456 s.processor = NewLiquidityPoolsChangeProcessor(s.mockQ, s.sequence) } @@ -183,28 +170,6 @@ func (s *LiquidityPoolsChangeProcessorTestSuiteLedger) TestNewLiquidityPool() { }, }, } - - liquidityPool := history.LiquidityPool{ - PoolID: "cafebabedeadbeef000000000000000000000000000000000000000000000000", - Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, - Fee: 34, - TrustlineCount: 52115, - ShareCount: 412241, - AssetReserves: []history.LiquidityPoolAssetReserve{ - { - xdr.MustNewCreditAsset("USD", "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - 450, - }, - { - xdr.MustNewNativeAsset(), - 500, - }, - }, - LastModifiedLedger: 123, - } - s.mockLiquidityPoolBatchInsertBuilder.On("Add", liquidityPool). - Return(nil).Once() - err := s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeLiquidityPool, Pre: nil, @@ -226,7 +191,7 @@ func (s *LiquidityPoolsChangeProcessorTestSuiteLedger) TestNewLiquidityPool() { }, }, } - s.mockLiquidityPoolBatchInsertBuilder.On("Add", liquidityPool).Return(nil).Once() + pre.LastModifiedLedgerSeq = pre.LastModifiedLedgerSeq - 1 err = s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeLiquidityPool, @@ -235,7 +200,25 @@ func (s *LiquidityPoolsChangeProcessorTestSuiteLedger) TestNewLiquidityPool() { }) s.Assert().NoError(err) - s.mockLiquidityPoolBatchInsertBuilder.On("Add", liquidityPool).Return(nil).Once() + postLP := history.LiquidityPool{ + PoolID: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, + Fee: 34, + TrustlineCount: 52115, + ShareCount: 412241, + AssetReserves: []history.LiquidityPoolAssetReserve{ + { + xdr.MustNewCreditAsset("USD", "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + 450, + }, + { + xdr.MustNewNativeAsset(), + 500, + }, + }, + LastModifiedLedger: 123, + } + s.mockQ.On("UpsertLiquidityPools", s.ctx, []history.LiquidityPool{postLP}).Return(nil).Once() s.mockQ.On("CompactLiquidityPools", s.ctx, s.sequence-100).Return(int64(0), nil).Once() } From 84787e82007fd91f562d3036da8c56bc46ff7f58 Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 18 Sep 2023 15:55:02 +0100 Subject: [PATCH 002/234] Update changelog for 2.27.0 release (#5055) --- services/horizon/CHANGELOG.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index f9122401f9..1234e005ac 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -3,17 +3,28 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). - ## Unreleased +### Added + +- Add a deprecation warning for using command-line flags when running Horizon ([5051](https://github.com/stellar/go/pull/5051)) +- Deprecate configuration flags related to legacy non-captive core ingestion ([5100](https://github.com/stellar/go/pull/5100)) + +## 2.27.0 + +**Upgrading to this version from <= 2.26.1 will trigger a state rebuild. During this process (which will take at least 10 minutes), Horizon will not ingest new ledgers.** + +**This release adds support for Protocol 20** + ### Breaking Changes - The command line flag `--remote-captive-core-url` has been removed, as remote captive core functionality is now deprecated ([4940](https://github.com/stellar/go/pull/4940)). - The functionality of generating default captive core configuration based on the --network-passphrase is now deprecated. Use the --network command instead ([4949](https://github.com/stellar/go/pull/4949)). ### Added - Added new command-line flag `--network` to specify the Stellar network (pubnet or testnet), aiming at simplifying the configuration process by automatically configuring the following parameters based on the chosen network: `--history-archive-urls`, `--network-passphrase`, and `--captive-core-config-path` ([4949](https://github.com/stellar/go/pull/4949)). -- Add a deprecation warning for using command-line flags when running Horizon ([5051](https://github.com/stellar/go/pull/5051)) -- Deprecate configuration flags related to legacy non-captive core ingestion ([5100](https://github.com/stellar/go/pull/5100)) +- Added `contract_credited` and `contract_debited` effects which are emitted whenever a Soroban contracts sends or receives a Stellar asset ([4832](https://github.com/stellar/go/pull/4832)). +* Added `num_contracts` (total number of Soroban contracts which hold an asset) and `contracts_amount` (total amount of the asset held by all Soroban contracts) fields to asset stat summaries at `/assets` ([4805](https://github.com/stellar/go/pull/4805)). +* Added responses for new operations introduced in protocol 20: `invoke_host_function`, `bump_footprint_expiration`, and `restore_footprint` ([4905](https://github.com/stellar/go/pull/4905)). ### Fixed - The same slippage calculation from the [`v2.26.1`](#2261) hotfix now properly excludes spikes for smoother trade aggregation plots ([4999](https://github.com/stellar/go/pull/4999)). From ad3b941e80794e74c55d82590808f52bbe42e7ea Mon Sep 17 00:00:00 2001 From: tamirms Date: Tue, 19 Sep 2023 15:30:41 +0100 Subject: [PATCH 003/234] fix operations query --- services/horizon/internal/db2/history/operation.go | 3 ++- .../db2/history/operation_batch_insert_builder.go | 12 ++++++++---- .../horizon/internal/db2/history/operation_test.go | 8 ++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/services/horizon/internal/db2/history/operation.go b/services/horizon/internal/db2/history/operation.go index 803c19791f..04a6d00f50 100644 --- a/services/horizon/internal/db2/history/operation.go +++ b/services/horizon/internal/db2/history/operation.go @@ -8,6 +8,7 @@ import ( "text/template" sq "github.com/Masterminds/squirrel" + "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/support/errors" "github.com/stellar/go/toid" @@ -394,7 +395,7 @@ var selectOperation = sq.Select( "hop.details, " + "hop.source_account, " + "hop.source_account_muxed, " + - "hop.is_payment, " + + "COALESCE(hop.is_payment, false) as is_payment, " + "ht.transaction_hash, " + "ht.tx_result, " + "COALESCE(ht.successful, true) as transaction_successful"). diff --git a/services/horizon/internal/db2/history/operation_batch_insert_builder.go b/services/horizon/internal/db2/history/operation_batch_insert_builder.go index e786ec97f7..272a4171c4 100644 --- a/services/horizon/internal/db2/history/operation_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/operation_batch_insert_builder.go @@ -4,6 +4,7 @@ import ( "context" "github.com/guregu/null" + "github.com/stellar/go/support/db" "github.com/stellar/go/xdr" ) @@ -49,7 +50,7 @@ func (i *operationBatchInsertBuilder) Add( sourceAccountMuxed null.String, isPayment bool, ) error { - return i.builder.Row(map[string]interface{}{ + row := map[string]interface{}{ "id": id, "transaction_id": transactionID, "application_order": applicationOrder, @@ -57,9 +58,12 @@ func (i *operationBatchInsertBuilder) Add( "details": string(details), "source_account": sourceAccount, "source_account_muxed": sourceAccountMuxed, - "is_payment": isPayment, - }) - + "is_payment": nil, + } + if isPayment { + row["is_payment"] = true + } + return i.builder.Row(row) } func (i *operationBatchInsertBuilder) Exec(ctx context.Context, session db.SessionInterface) error { diff --git a/services/horizon/internal/db2/history/operation_test.go b/services/horizon/internal/db2/history/operation_test.go index f7533ee5f3..1d20a9cb10 100644 --- a/services/horizon/internal/db2/history/operation_test.go +++ b/services/horizon/internal/db2/history/operation_test.go @@ -168,7 +168,7 @@ func TestOperationQueryBuilder(t *testing.T) { tt.Assert.NoError(err) // Operations for account queries will use hopp.history_operation_id in their predicates. - want := "SELECT hop.id, hop.transaction_id, hop.application_order, hop.type, hop.details, hop.source_account, hop.source_account_muxed, hop.is_payment, ht.transaction_hash, ht.tx_result, COALESCE(ht.successful, true) as transaction_successful FROM history_operations hop LEFT JOIN history_transactions ht ON ht.id = hop.transaction_id JOIN history_operation_participants hopp ON hopp.history_operation_id = hop.id WHERE hopp.history_account_id = ? AND hopp.history_operation_id > ? ORDER BY hopp.history_operation_id asc LIMIT 10" + want := "SELECT hop.id, hop.transaction_id, hop.application_order, hop.type, hop.details, hop.source_account, hop.source_account_muxed, COALESCE(hop.is_payment, false) as is_payment, ht.transaction_hash, ht.tx_result, COALESCE(ht.successful, true) as transaction_successful FROM history_operations hop LEFT JOIN history_transactions ht ON ht.id = hop.transaction_id JOIN history_operation_participants hopp ON hopp.history_operation_id = hop.id WHERE hopp.history_account_id = ? AND hopp.history_operation_id > ? ORDER BY hopp.history_operation_id asc LIMIT 10" tt.Assert.EqualValues(want, got) opsQ = q.Operations().ForLedger(tt.Ctx, 2).Page(db2.PageQuery{Cursor: "8589938689", Order: "asc", Limit: 10}) @@ -177,7 +177,7 @@ func TestOperationQueryBuilder(t *testing.T) { tt.Assert.NoError(err) // Other operation queries will use hop.id in their predicates. - want = "SELECT hop.id, hop.transaction_id, hop.application_order, hop.type, hop.details, hop.source_account, hop.source_account_muxed, hop.is_payment, ht.transaction_hash, ht.tx_result, COALESCE(ht.successful, true) as transaction_successful FROM history_operations hop LEFT JOIN history_transactions ht ON ht.id = hop.transaction_id WHERE hop.id >= ? AND hop.id < ? AND hop.id > ? ORDER BY hop.id asc LIMIT 10" + want = "SELECT hop.id, hop.transaction_id, hop.application_order, hop.type, hop.details, hop.source_account, hop.source_account_muxed, COALESCE(hop.is_payment, false) as is_payment, ht.transaction_hash, ht.tx_result, COALESCE(ht.successful, true) as transaction_successful FROM history_operations hop LEFT JOIN history_transactions ht ON ht.id = hop.transaction_id WHERE hop.id >= ? AND hop.id < ? AND hop.id > ? ORDER BY hop.id asc LIMIT 10" tt.Assert.EqualValues(want, got) } @@ -239,7 +239,7 @@ func TestOperationIncludeFailed(t *testing.T) { sql, _, err := query.sql.ToSql() tt.Assert.NoError(err) - tt.Assert.Equal("SELECT hop.id, hop.transaction_id, hop.application_order, hop.type, hop.details, hop.source_account, hop.source_account_muxed, hop.is_payment, ht.transaction_hash, ht.tx_result, COALESCE(ht.successful, true) as transaction_successful FROM history_operations hop LEFT JOIN history_transactions ht ON ht.id = hop.transaction_id JOIN history_operation_participants hopp ON hopp.history_operation_id = hop.id WHERE hopp.history_account_id = ?", sql) + tt.Assert.Equal("SELECT hop.id, hop.transaction_id, hop.application_order, hop.type, hop.details, hop.source_account, hop.source_account_muxed, COALESCE(hop.is_payment, false) as is_payment, ht.transaction_hash, ht.tx_result, COALESCE(ht.successful, true) as transaction_successful FROM history_operations hop LEFT JOIN history_transactions ht ON ht.id = hop.transaction_id JOIN history_operation_participants hopp ON hopp.history_operation_id = hop.id WHERE hopp.history_account_id = ?", sql) } // TestPaymentsSuccessfulOnly tests if default query returns payments in @@ -302,7 +302,7 @@ func TestPaymentsIncludeFailed(t *testing.T) { sql, _, err := query.sql.ToSql() tt.Assert.NoError(err) - tt.Assert.Equal("SELECT hop.id, hop.transaction_id, hop.application_order, hop.type, hop.details, hop.source_account, hop.source_account_muxed, hop.is_payment, ht.transaction_hash, ht.tx_result, COALESCE(ht.successful, true) as transaction_successful FROM history_operations hop LEFT JOIN history_transactions ht ON ht.id = hop.transaction_id JOIN history_operation_participants hopp ON hopp.history_operation_id = hop.id WHERE (hop.type IN (?,?,?,?,?) OR hop.is_payment = ?) AND hopp.history_account_id = ?", sql) + tt.Assert.Equal("SELECT hop.id, hop.transaction_id, hop.application_order, hop.type, hop.details, hop.source_account, hop.source_account_muxed, COALESCE(hop.is_payment, false) as is_payment, ht.transaction_hash, ht.tx_result, COALESCE(ht.successful, true) as transaction_successful FROM history_operations hop LEFT JOIN history_transactions ht ON ht.id = hop.transaction_id JOIN history_operation_participants hopp ON hopp.history_operation_id = hop.id WHERE (hop.type IN (?,?,?,?,?) OR hop.is_payment = ?) AND hopp.history_account_id = ?", sql) } func TestExtraChecksOperationsTransactionSuccessfulTrueResultFalse(t *testing.T) { From 7a5875436fffe6f8f303853393a09ba1fbfb86ea Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 22 Sep 2023 15:57:33 +0100 Subject: [PATCH 004/234] horizon: Improve performance of migrations/64_add_payment_flag_history_ops.sql (#5056) Remove index_history_operations_on_is_payment because it is not actually used in the payments query. --- .../horizon/internal/db2/schema/bindata.go | 177 ++++++++++-------- .../64_add_payment_flag_history_ops.sql | 2 - .../migrations/65_drop_payment_index.sql | 7 + 3 files changed, 106 insertions(+), 80 deletions(-) create mode 100644 services/horizon/internal/db2/schema/migrations/65_drop_payment_index.sql diff --git a/services/horizon/internal/db2/schema/bindata.go b/services/horizon/internal/db2/schema/bindata.go index 46dc3ba3c3..3f1e470840 100644 --- a/services/horizon/internal/db2/schema/bindata.go +++ b/services/horizon/internal/db2/schema/bindata.go @@ -60,7 +60,8 @@ // migrations/61_trust_lines_by_account_type_code_issuer.sql (383B) // migrations/62_claimable_balance_claimants.sql (1.428kB) // migrations/63_add_contract_id_to_asset_stats.sql (153B) -// migrations/64_add_payment_flag_history_ops.sql (300B) +// migrations/64_add_payment_flag_history_ops.sql (145B) +// migrations/65_drop_payment_index.sql (260B) // migrations/65_remove_unused_indexes.sql (2.897kB) // migrations/6_create_assets_table.sql (366B) // migrations/7_modify_trades_table.sql (2.303kB) @@ -1335,7 +1336,7 @@ func migrations63_add_contract_id_to_asset_statsSql() (*asset, error) { return a, nil } -var _migrations64_add_payment_flag_history_opsSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x8f\x41\xca\xc2\x30\x10\x46\xf7\x73\x8a\xa1\xab\xff\x47\x7a\x82\xac\x62\x13\xa4\x50\x53\xa9\x2d\xb8\x0b\x29\x0e\x1a\xb0\x99\x92\x06\xb4\xb7\x17\xdc\xb4\xa0\x20\xee\xe7\xcd\xfb\x5e\x9e\xe3\x66\xf0\x97\xe8\x12\x61\x37\x02\xc8\xaa\xd5\x0d\xb6\x72\x5b\x69\xbc\xfa\x29\x71\x9c\x2d\x8f\x14\x5d\xf2\x1c\x26\x94\x4a\xa1\x9f\xec\xe8\xe6\x81\x42\xc2\x9e\xf9\x46\x2e\x08\x28\x1a\x2d\x5b\x8d\xa5\x51\xfa\x84\x99\x0f\x67\x7a\xd8\x77\xdc\x72\xb0\x0b\x9d\x61\x6d\x3e\x39\xba\x63\x69\x76\xd8\xa7\x48\x84\x7f\xcb\xf9\xbf\x00\x58\xaf\x55\x7c\x0f\x00\xaa\xa9\x0f\x3f\x6a\xc5\xb7\xc8\xd7\xcf\xa2\xae\xba\xbd\x59\xc5\x0a\x78\x06\x00\x00\xff\xff\xc0\xa1\x37\xd4\x2c\x01\x00\x00") +var _migrations64_add_payment_flag_history_opsSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd2\xd5\x55\xd0\xce\xcd\x4c\x2f\x4a\x2c\x49\x55\x08\x2d\xe0\xe2\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\xc8\xc8\x2c\x2e\xc9\x2f\xaa\x8c\xcf\x2f\x48\x2d\x4a\x2c\xc9\xcc\xcf\x2b\x56\x70\x74\x71\x51\xc8\x2c\x8e\x2f\x48\xac\xcc\x4d\xcd\x2b\x51\x48\xca\xcf\xcf\x49\x4d\xcc\xb3\xe6\xe2\x42\x36\xc7\x25\xbf\x3c\x8f\xa0\x49\x2e\x41\xfe\x01\x0a\xce\xfe\x3e\xa1\xbe\x7e\x48\x26\x5a\x73\x01\x02\x00\x00\xff\xff\xcc\xf9\x34\xcb\x91\x00\x00\x00") func migrations64_add_payment_flag_history_opsSqlBytes() ([]byte, error) { return bindataRead( @@ -1351,7 +1352,27 @@ func migrations64_add_payment_flag_history_opsSql() (*asset, error) { } info := bindataFileInfo{name: "migrations/64_add_payment_flag_history_ops.sql", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x7e, 0x2d, 0x87, 0xe0, 0xa7, 0x38, 0xdc, 0xb7, 0xfb, 0xda, 0xc5, 0xad, 0xfb, 0x70, 0x15, 0xde, 0xb, 0x27, 0x97, 0x87, 0xc, 0xdb, 0xd3, 0x4b, 0x6c, 0x51, 0x39, 0x3a, 0xb4, 0xd3, 0x20, 0x42}} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xa7, 0x6d, 0xb6, 0x2e, 0x50, 0x40, 0x71, 0x1f, 0x97, 0xd9, 0xd9, 0xfb, 0xcf, 0x45, 0x0, 0xd1, 0x93, 0x79, 0x5d, 0x70, 0xb3, 0x2e, 0x31, 0x44, 0x13, 0x63, 0xdf, 0x70, 0xd9, 0xc9, 0x6b, 0x43}} + return a, nil +} + +var _migrations65_drop_payment_indexSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\xcf\xc1\x4a\xc3\x40\x10\xc6\xf1\x7b\x9e\xe2\xa3\x57\x8d\xeb\xc1\x0a\xea\x35\x15\x72\x51\xb1\x0a\xbd\x2d\xdb\x66\x48\x06\x9a\x99\x65\x67\x96\x9a\xb7\x17\x5b\xf0\xdc\x07\xf8\x7e\x7c\xff\xb6\xc5\xcd\xcc\x63\x49\x4e\xf8\xce\x4d\xd3\xb6\x60\x19\xe8\x27\x4e\x6c\xae\x65\x89\x9a\xa9\x24\x67\x15\x8b\x2a\x91\x2d\xe6\xb4\xcc\x24\x8e\x53\x32\xa4\x61\xa0\x01\x2c\xb8\x10\xac\x82\xc7\x07\xec\xab\x83\x1d\x5e\x8b\x18\xb4\xfa\x1f\xea\x13\x5d\xe0\xf3\x4e\xd4\x21\x74\x20\xb3\x54\x96\x5b\x18\x11\x26\xf7\x6c\xcf\x21\x8c\xec\x53\xdd\xdf\x1d\x74\x0e\xe6\x74\x3c\xa6\x12\x46\x0d\x6c\x56\xc9\xc2\xfa\x7e\xfd\xd4\x74\x9f\xef\x1f\xe8\xdf\xba\xcd\x0e\xfd\x2b\x36\xbb\x7e\xfb\xb5\xc5\xea\xba\xd7\xab\x97\x73\xe2\x7f\x72\xa7\x27\x69\x7e\x03\x00\x00\xff\xff\x15\xa3\x47\xcb\x04\x01\x00\x00") + +func migrations65_drop_payment_indexSqlBytes() ([]byte, error) { + return bindataRead( + _migrations65_drop_payment_indexSql, + "migrations/65_drop_payment_index.sql", + ) +} + +func migrations65_drop_payment_indexSql() (*asset, error) { + bytes, err := migrations65_drop_payment_indexSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "migrations/65_drop_payment_index.sql", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xf2, 0xe4, 0xb0, 0xad, 0x98, 0x6a, 0x5, 0xb8, 0x1e, 0xbb, 0xf6, 0x3d, 0x63, 0x93, 0xd5, 0x45, 0x29, 0xd7, 0x23, 0x23, 0x9b, 0xfc, 0x27, 0xb5, 0x8f, 0x37, 0x20, 0x66, 0x6c, 0x97, 0x45, 0x4a}} return a, nil } @@ -1627,6 +1648,7 @@ var _bindata = map[string]func() (*asset, error){ "migrations/62_claimable_balance_claimants.sql": migrations62_claimable_balance_claimantsSql, "migrations/63_add_contract_id_to_asset_stats.sql": migrations63_add_contract_id_to_asset_statsSql, "migrations/64_add_payment_flag_history_ops.sql": migrations64_add_payment_flag_history_opsSql, + "migrations/65_drop_payment_index.sql": migrations65_drop_payment_indexSql, "migrations/65_remove_unused_indexes.sql": migrations65_remove_unused_indexesSql, "migrations/6_create_assets_table.sql": migrations6_create_assets_tableSql, "migrations/7_modify_trades_table.sql": migrations7_modify_trades_tableSql, @@ -1639,13 +1661,11 @@ var _bindata = map[string]func() (*asset, error){ // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: -// -// data/ -// foo.txt -// img/ -// a.png -// b.png -// +// data/ +// foo.txt +// img/ +// a.png +// b.png // then AssetDir("data") would return []string{"foo.txt", "img"}, // AssetDir("data/img") would return []string{"a.png", "b.png"}, // AssetDir("foo.txt") and AssetDir("notexist") would return an error, and @@ -1678,74 +1698,75 @@ type bintree struct { } var _bintree = &bintree{nil, map[string]*bintree{ - "migrations": {nil, map[string]*bintree{ - "10_add_trades_price.sql": {migrations10_add_trades_priceSql, map[string]*bintree{}}, - "11_add_trades_account_index.sql": {migrations11_add_trades_account_indexSql, map[string]*bintree{}}, - "12_asset_stats_amount_string.sql": {migrations12_asset_stats_amount_stringSql, map[string]*bintree{}}, - "13_trade_offer_ids.sql": {migrations13_trade_offer_idsSql, map[string]*bintree{}}, - "14_fix_asset_toml_field.sql": {migrations14_fix_asset_toml_fieldSql, map[string]*bintree{}}, - "15_ledger_failed_txs.sql": {migrations15_ledger_failed_txsSql, map[string]*bintree{}}, - "16_ingest_failed_transactions.sql": {migrations16_ingest_failed_transactionsSql, map[string]*bintree{}}, - "17_transaction_fee_paid.sql": {migrations17_transaction_fee_paidSql, map[string]*bintree{}}, - "18_account_for_signers.sql": {migrations18_account_for_signersSql, map[string]*bintree{}}, - "19_offers.sql": {migrations19_offersSql, map[string]*bintree{}}, - "1_initial_schema.sql": {migrations1_initial_schemaSql, map[string]*bintree{}}, - "20_account_for_signer_index.sql": {migrations20_account_for_signer_indexSql, map[string]*bintree{}}, - "21_trades_remove_zero_amount_constraints.sql": {migrations21_trades_remove_zero_amount_constraintsSql, map[string]*bintree{}}, - "22_trust_lines.sql": {migrations22_trust_linesSql, map[string]*bintree{}}, - "23_exp_asset_stats.sql": {migrations23_exp_asset_statsSql, map[string]*bintree{}}, - "24_accounts.sql": {migrations24_accountsSql, map[string]*bintree{}}, - "25_expingest_rename_columns.sql": {migrations25_expingest_rename_columnsSql, map[string]*bintree{}}, - "26_exp_history_ledgers.sql": {migrations26_exp_history_ledgersSql, map[string]*bintree{}}, - "27_exp_history_transactions.sql": {migrations27_exp_history_transactionsSql, map[string]*bintree{}}, - "28_exp_history_operations.sql": {migrations28_exp_history_operationsSql, map[string]*bintree{}}, - "29_exp_history_assets.sql": {migrations29_exp_history_assetsSql, map[string]*bintree{}}, - "2_index_participants_by_toid.sql": {migrations2_index_participants_by_toidSql, map[string]*bintree{}}, - "30_exp_history_trades.sql": {migrations30_exp_history_tradesSql, map[string]*bintree{}}, - "31_exp_history_effects.sql": {migrations31_exp_history_effectsSql, map[string]*bintree{}}, - "32_drop_exp_history_tables.sql": {migrations32_drop_exp_history_tablesSql, map[string]*bintree{}}, - "33_remove_unused.sql": {migrations33_remove_unusedSql, map[string]*bintree{}}, - "34_fee_bump_transactions.sql": {migrations34_fee_bump_transactionsSql, map[string]*bintree{}}, - "35_drop_participant_id.sql": {migrations35_drop_participant_idSql, map[string]*bintree{}}, - "36_deleted_offers.sql": {migrations36_deleted_offersSql, map[string]*bintree{}}, - "37_add_tx_set_operation_count_to_ledgers.sql": {migrations37_add_tx_set_operation_count_to_ledgersSql, map[string]*bintree{}}, - "38_add_constraints.sql": {migrations38_add_constraintsSql, map[string]*bintree{}}, - "39_claimable_balances.sql": {migrations39_claimable_balancesSql, map[string]*bintree{}}, - "39_history_trades_indices.sql": {migrations39_history_trades_indicesSql, map[string]*bintree{}}, - "3_use_sequence_in_history_accounts.sql": {migrations3_use_sequence_in_history_accountsSql, map[string]*bintree{}}, - "40_fix_inner_tx_max_fee_constraint.sql": {migrations40_fix_inner_tx_max_fee_constraintSql, map[string]*bintree{}}, - "41_add_sponsor_to_state_tables.sql": {migrations41_add_sponsor_to_state_tablesSql, map[string]*bintree{}}, - "42_add_num_sponsored_and_num_sponsoring_to_accounts.sql": {migrations42_add_num_sponsored_and_num_sponsoring_to_accountsSql, map[string]*bintree{}}, - "43_add_claimable_balances_flags.sql": {migrations43_add_claimable_balances_flagsSql, map[string]*bintree{}}, - "44_asset_stat_accounts_and_balances.sql": {migrations44_asset_stat_accounts_and_balancesSql, map[string]*bintree{}}, - "45_add_claimable_balances_history.sql": {migrations45_add_claimable_balances_historySql, map[string]*bintree{}}, - "46_add_muxed_accounts.sql": {migrations46_add_muxed_accountsSql, map[string]*bintree{}}, - "47_precompute_trade_aggregations.sql": {migrations47_precompute_trade_aggregationsSql, map[string]*bintree{}}, - "48_rebuild_trade_aggregations.sql": {migrations48_rebuild_trade_aggregationsSql, map[string]*bintree{}}, - "49_add_brin_index_trade_aggregations.sql": {migrations49_add_brin_index_trade_aggregationsSql, map[string]*bintree{}}, - "4_add_protocol_version.sql": {migrations4_add_protocol_versionSql, map[string]*bintree{}}, - "50_liquidity_pools.sql": {migrations50_liquidity_poolsSql, map[string]*bintree{}}, - "51_remove_ht_unused_indexes.sql": {migrations51_remove_ht_unused_indexesSql, map[string]*bintree{}}, - "52_add_trade_type_index.sql": {migrations52_add_trade_type_indexSql, map[string]*bintree{}}, - "53_add_trades_rounding_slippage.sql": {migrations53_add_trades_rounding_slippageSql, map[string]*bintree{}}, - "54_tx_preconditions_and_account_fields.sql": {migrations54_tx_preconditions_and_account_fieldsSql, map[string]*bintree{}}, - "55_filter_rules.sql": {migrations55_filter_rulesSql, map[string]*bintree{}}, - "56_txsub_read_only.sql": {migrations56_txsub_read_onlySql, map[string]*bintree{}}, - "57_trade_aggregation_autovac.sql": {migrations57_trade_aggregation_autovacSql, map[string]*bintree{}}, - "58_add_index_by_id_optimization.sql": {migrations58_add_index_by_id_optimizationSql, map[string]*bintree{}}, - "59_remove_foreign_key_constraints.sql": {migrations59_remove_foreign_key_constraintsSql, map[string]*bintree{}}, - "5_create_trades_table.sql": {migrations5_create_trades_tableSql, map[string]*bintree{}}, - "60_add_asset_id_indexes.sql": {migrations60_add_asset_id_indexesSql, map[string]*bintree{}}, - "61_trust_lines_by_account_type_code_issuer.sql": {migrations61_trust_lines_by_account_type_code_issuerSql, map[string]*bintree{}}, - "62_claimable_balance_claimants.sql": {migrations62_claimable_balance_claimantsSql, map[string]*bintree{}}, - "63_add_contract_id_to_asset_stats.sql": {migrations63_add_contract_id_to_asset_statsSql, map[string]*bintree{}}, - "64_add_payment_flag_history_ops.sql": {migrations64_add_payment_flag_history_opsSql, map[string]*bintree{}}, - "65_remove_unused_indexes.sql": {migrations65_remove_unused_indexesSql, map[string]*bintree{}}, - "6_create_assets_table.sql": {migrations6_create_assets_tableSql, map[string]*bintree{}}, - "7_modify_trades_table.sql": {migrations7_modify_trades_tableSql, map[string]*bintree{}}, - "8_add_aggregators.sql": {migrations8_add_aggregatorsSql, map[string]*bintree{}}, - "8_create_asset_stats_table.sql": {migrations8_create_asset_stats_tableSql, map[string]*bintree{}}, - "9_add_header_xdr.sql": {migrations9_add_header_xdrSql, map[string]*bintree{}}, + "migrations": &bintree{nil, map[string]*bintree{ + "10_add_trades_price.sql": &bintree{migrations10_add_trades_priceSql, map[string]*bintree{}}, + "11_add_trades_account_index.sql": &bintree{migrations11_add_trades_account_indexSql, map[string]*bintree{}}, + "12_asset_stats_amount_string.sql": &bintree{migrations12_asset_stats_amount_stringSql, map[string]*bintree{}}, + "13_trade_offer_ids.sql": &bintree{migrations13_trade_offer_idsSql, map[string]*bintree{}}, + "14_fix_asset_toml_field.sql": &bintree{migrations14_fix_asset_toml_fieldSql, map[string]*bintree{}}, + "15_ledger_failed_txs.sql": &bintree{migrations15_ledger_failed_txsSql, map[string]*bintree{}}, + "16_ingest_failed_transactions.sql": &bintree{migrations16_ingest_failed_transactionsSql, map[string]*bintree{}}, + "17_transaction_fee_paid.sql": &bintree{migrations17_transaction_fee_paidSql, map[string]*bintree{}}, + "18_account_for_signers.sql": &bintree{migrations18_account_for_signersSql, map[string]*bintree{}}, + "19_offers.sql": &bintree{migrations19_offersSql, map[string]*bintree{}}, + "1_initial_schema.sql": &bintree{migrations1_initial_schemaSql, map[string]*bintree{}}, + "20_account_for_signer_index.sql": &bintree{migrations20_account_for_signer_indexSql, map[string]*bintree{}}, + "21_trades_remove_zero_amount_constraints.sql": &bintree{migrations21_trades_remove_zero_amount_constraintsSql, map[string]*bintree{}}, + "22_trust_lines.sql": &bintree{migrations22_trust_linesSql, map[string]*bintree{}}, + "23_exp_asset_stats.sql": &bintree{migrations23_exp_asset_statsSql, map[string]*bintree{}}, + "24_accounts.sql": &bintree{migrations24_accountsSql, map[string]*bintree{}}, + "25_expingest_rename_columns.sql": &bintree{migrations25_expingest_rename_columnsSql, map[string]*bintree{}}, + "26_exp_history_ledgers.sql": &bintree{migrations26_exp_history_ledgersSql, map[string]*bintree{}}, + "27_exp_history_transactions.sql": &bintree{migrations27_exp_history_transactionsSql, map[string]*bintree{}}, + "28_exp_history_operations.sql": &bintree{migrations28_exp_history_operationsSql, map[string]*bintree{}}, + "29_exp_history_assets.sql": &bintree{migrations29_exp_history_assetsSql, map[string]*bintree{}}, + "2_index_participants_by_toid.sql": &bintree{migrations2_index_participants_by_toidSql, map[string]*bintree{}}, + "30_exp_history_trades.sql": &bintree{migrations30_exp_history_tradesSql, map[string]*bintree{}}, + "31_exp_history_effects.sql": &bintree{migrations31_exp_history_effectsSql, map[string]*bintree{}}, + "32_drop_exp_history_tables.sql": &bintree{migrations32_drop_exp_history_tablesSql, map[string]*bintree{}}, + "33_remove_unused.sql": &bintree{migrations33_remove_unusedSql, map[string]*bintree{}}, + "34_fee_bump_transactions.sql": &bintree{migrations34_fee_bump_transactionsSql, map[string]*bintree{}}, + "35_drop_participant_id.sql": &bintree{migrations35_drop_participant_idSql, map[string]*bintree{}}, + "36_deleted_offers.sql": &bintree{migrations36_deleted_offersSql, map[string]*bintree{}}, + "37_add_tx_set_operation_count_to_ledgers.sql": &bintree{migrations37_add_tx_set_operation_count_to_ledgersSql, map[string]*bintree{}}, + "38_add_constraints.sql": &bintree{migrations38_add_constraintsSql, map[string]*bintree{}}, + "39_claimable_balances.sql": &bintree{migrations39_claimable_balancesSql, map[string]*bintree{}}, + "39_history_trades_indices.sql": &bintree{migrations39_history_trades_indicesSql, map[string]*bintree{}}, + "3_use_sequence_in_history_accounts.sql": &bintree{migrations3_use_sequence_in_history_accountsSql, map[string]*bintree{}}, + "40_fix_inner_tx_max_fee_constraint.sql": &bintree{migrations40_fix_inner_tx_max_fee_constraintSql, map[string]*bintree{}}, + "41_add_sponsor_to_state_tables.sql": &bintree{migrations41_add_sponsor_to_state_tablesSql, map[string]*bintree{}}, + "42_add_num_sponsored_and_num_sponsoring_to_accounts.sql": &bintree{migrations42_add_num_sponsored_and_num_sponsoring_to_accountsSql, map[string]*bintree{}}, + "43_add_claimable_balances_flags.sql": &bintree{migrations43_add_claimable_balances_flagsSql, map[string]*bintree{}}, + "44_asset_stat_accounts_and_balances.sql": &bintree{migrations44_asset_stat_accounts_and_balancesSql, map[string]*bintree{}}, + "45_add_claimable_balances_history.sql": &bintree{migrations45_add_claimable_balances_historySql, map[string]*bintree{}}, + "46_add_muxed_accounts.sql": &bintree{migrations46_add_muxed_accountsSql, map[string]*bintree{}}, + "47_precompute_trade_aggregations.sql": &bintree{migrations47_precompute_trade_aggregationsSql, map[string]*bintree{}}, + "48_rebuild_trade_aggregations.sql": &bintree{migrations48_rebuild_trade_aggregationsSql, map[string]*bintree{}}, + "49_add_brin_index_trade_aggregations.sql": &bintree{migrations49_add_brin_index_trade_aggregationsSql, map[string]*bintree{}}, + "4_add_protocol_version.sql": &bintree{migrations4_add_protocol_versionSql, map[string]*bintree{}}, + "50_liquidity_pools.sql": &bintree{migrations50_liquidity_poolsSql, map[string]*bintree{}}, + "51_remove_ht_unused_indexes.sql": &bintree{migrations51_remove_ht_unused_indexesSql, map[string]*bintree{}}, + "52_add_trade_type_index.sql": &bintree{migrations52_add_trade_type_indexSql, map[string]*bintree{}}, + "53_add_trades_rounding_slippage.sql": &bintree{migrations53_add_trades_rounding_slippageSql, map[string]*bintree{}}, + "54_tx_preconditions_and_account_fields.sql": &bintree{migrations54_tx_preconditions_and_account_fieldsSql, map[string]*bintree{}}, + "55_filter_rules.sql": &bintree{migrations55_filter_rulesSql, map[string]*bintree{}}, + "56_txsub_read_only.sql": &bintree{migrations56_txsub_read_onlySql, map[string]*bintree{}}, + "57_trade_aggregation_autovac.sql": &bintree{migrations57_trade_aggregation_autovacSql, map[string]*bintree{}}, + "58_add_index_by_id_optimization.sql": &bintree{migrations58_add_index_by_id_optimizationSql, map[string]*bintree{}}, + "59_remove_foreign_key_constraints.sql": &bintree{migrations59_remove_foreign_key_constraintsSql, map[string]*bintree{}}, + "5_create_trades_table.sql": &bintree{migrations5_create_trades_tableSql, map[string]*bintree{}}, + "60_add_asset_id_indexes.sql": &bintree{migrations60_add_asset_id_indexesSql, map[string]*bintree{}}, + "61_trust_lines_by_account_type_code_issuer.sql": &bintree{migrations61_trust_lines_by_account_type_code_issuerSql, map[string]*bintree{}}, + "62_claimable_balance_claimants.sql": &bintree{migrations62_claimable_balance_claimantsSql, map[string]*bintree{}}, + "63_add_contract_id_to_asset_stats.sql": &bintree{migrations63_add_contract_id_to_asset_statsSql, map[string]*bintree{}}, + "64_add_payment_flag_history_ops.sql": &bintree{migrations64_add_payment_flag_history_opsSql, map[string]*bintree{}}, + "65_drop_payment_index.sql": &bintree{migrations65_drop_payment_indexSql, map[string]*bintree{}}, + "65_remove_unused_indexes.sql": &bintree{migrations65_remove_unused_indexesSql, map[string]*bintree{}}, + "6_create_assets_table.sql": &bintree{migrations6_create_assets_tableSql, map[string]*bintree{}}, + "7_modify_trades_table.sql": &bintree{migrations7_modify_trades_tableSql, map[string]*bintree{}}, + "8_add_aggregators.sql": &bintree{migrations8_add_aggregatorsSql, map[string]*bintree{}}, + "8_create_asset_stats_table.sql": &bintree{migrations8_create_asset_stats_tableSql, map[string]*bintree{}}, + "9_add_header_xdr.sql": &bintree{migrations9_add_header_xdrSql, map[string]*bintree{}}, }}, }} diff --git a/services/horizon/internal/db2/schema/migrations/64_add_payment_flag_history_ops.sql b/services/horizon/internal/db2/schema/migrations/64_add_payment_flag_history_ops.sql index f95192f669..ad19f441fe 100644 --- a/services/horizon/internal/db2/schema/migrations/64_add_payment_flag_history_ops.sql +++ b/services/horizon/internal/db2/schema/migrations/64_add_payment_flag_history_ops.sql @@ -1,9 +1,7 @@ -- +migrate Up ALTER TABLE history_operations ADD is_payment boolean; -CREATE INDEX "index_history_operations_on_is_payment" ON history_operations USING btree (is_payment); -- +migrate Down -DROP INDEX "index_history_operations_on_is_payment"; ALTER TABLE history_operations DROP COLUMN is_payment; diff --git a/services/horizon/internal/db2/schema/migrations/65_drop_payment_index.sql b/services/horizon/internal/db2/schema/migrations/65_drop_payment_index.sql new file mode 100644 index 0000000000..325719a284 --- /dev/null +++ b/services/horizon/internal/db2/schema/migrations/65_drop_payment_index.sql @@ -0,0 +1,7 @@ +-- +migrate Up + +-- index_history_operations_on_is_payment was added in migration 64 but it turns out +-- the index was not necessary, see https://github.com/stellar/go/issues/5059 +DROP INDEX IF EXISTS "index_history_operations_on_is_payment"; + +-- +migrate Down From 9faeb3f75df25c8c62067168d1feb920fa1bd1c7 Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 25 Sep 2023 17:11:29 +0100 Subject: [PATCH 005/234] updated changelog notes for 2.27.0-rc2 release (#5063) Co-authored-by: Shawn Reuland --- services/horizon/CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 1234e005ac..575c2994e8 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -10,7 +10,11 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). - Add a deprecation warning for using command-line flags when running Horizon ([5051](https://github.com/stellar/go/pull/5051)) - Deprecate configuration flags related to legacy non-captive core ingestion ([5100](https://github.com/stellar/go/pull/5100)) -## 2.27.0 +## 2.27.0-rc2 +### Fixed +- treat null is_payment values as equivalent to false values, avoid sql nil conversion errors([5060](https://github.com/stellar/go/pull/5060)). + +## 2.27.0-rc1 **Upgrading to this version from <= 2.26.1 will trigger a state rebuild. During this process (which will take at least 10 minutes), Horizon will not ingest new ledgers.** From 136d71bfa833a596e58597608556129a74f2fbc3 Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 6 Oct 2023 18:31:18 +0100 Subject: [PATCH 006/234] ingest: Extract ledger entry changes from Tx Meta in a deterministic order (#5070) --- ingest/change.go | 56 +++++ ingest/change_test.go | 215 ++++++++++++++++++ ingest/ledger_change_reader.go | 1 + services/horizon/CHANGELOG.md | 4 + .../ingest/processors/effects_processor.go | 19 +- .../transaction_operation_wrapper_test.go | 10 +- .../integration/liquidity_pool_test.go | 34 ++- xdr/ledger_close_meta.go | 4 +- 8 files changed, 317 insertions(+), 26 deletions(-) create mode 100644 ingest/change_test.go diff --git a/ingest/change.go b/ingest/change.go index 7d9b761db2..027b37b861 100644 --- a/ingest/change.go +++ b/ingest/change.go @@ -2,6 +2,7 @@ package ingest import ( "bytes" + "sort" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" @@ -20,6 +21,13 @@ type Change struct { Post *xdr.LedgerEntry } +func (c *Change) ledgerKey() (xdr.LedgerKey, error) { + if c.Pre != nil { + return c.Pre.LedgerKey() + } + return c.Post.LedgerKey() +} + // GetChangesFromLedgerEntryChanges transforms LedgerEntryChanges to []Change. // Each `update` and `removed` is preceded with `state` and `create` changes // are alone, without `state`. The transformation we're doing is to move each @@ -64,9 +72,57 @@ func GetChangesFromLedgerEntryChanges(ledgerEntryChanges xdr.LedgerEntryChanges) } } + sortChanges(changes) return changes } +type sortableChanges struct { + changes []Change + ledgerKeys [][]byte +} + +func newSortableChanges(changes []Change) sortableChanges { + ledgerKeys := make([][]byte, len(changes)) + for i, c := range changes { + lk, err := c.ledgerKey() + if err != nil { + panic(err) + } + lkBytes, err := lk.MarshalBinary() + if err != nil { + panic(err) + } + ledgerKeys[i] = lkBytes + } + return sortableChanges{ + changes: changes, + ledgerKeys: ledgerKeys, + } +} + +func (s sortableChanges) Len() int { + return len(s.changes) +} + +func (s sortableChanges) Less(i, j int) bool { + return bytes.Compare(s.ledgerKeys[i], s.ledgerKeys[j]) < 0 +} + +func (s sortableChanges) Swap(i, j int) { + s.changes[i], s.changes[j] = s.changes[j], s.changes[i] + s.ledgerKeys[i], s.ledgerKeys[j] = s.ledgerKeys[j], s.ledgerKeys[i] +} + +// sortChanges is applied on a list of changes to ensure that LedgerEntryChanges +// from Tx Meta are ingested in a deterministic order. +// The changes are sorted by ledger key. It is unexpected for there to be +// multiple changes with the same ledger key in a LedgerEntryChanges group, +// but if that is the case, we fall back to the original ordering of the changes +// by using a stable sorting algorithm. +func sortChanges(changes []Change) { + sort.Stable(newSortableChanges(changes)) +} + // LedgerEntryChangeType returns type in terms of LedgerEntryChangeType. func (c *Change) LedgerEntryChangeType() xdr.LedgerEntryChangeType { switch { diff --git a/ingest/change_test.go b/ingest/change_test.go new file mode 100644 index 0000000000..d8ae9492dc --- /dev/null +++ b/ingest/change_test.go @@ -0,0 +1,215 @@ +package ingest + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/xdr" +) + +func assertChangesAreEqual(t *testing.T, a, b Change) { + assert.Equal(t, a.Type, b.Type) + if a.Pre == nil { + assert.Nil(t, b.Pre) + } else { + aBytes, err := a.Pre.MarshalBinary() + assert.NoError(t, err) + bBytes, err := b.Pre.MarshalBinary() + assert.NoError(t, err) + assert.Equal(t, aBytes, bBytes) + } + if a.Post == nil { + assert.Nil(t, b.Post) + } else { + aBytes, err := a.Post.MarshalBinary() + assert.NoError(t, err) + bBytes, err := b.Post.MarshalBinary() + assert.NoError(t, err) + assert.Equal(t, aBytes, bBytes) + } +} + +func TestSortChanges(t *testing.T) { + for _, testCase := range []struct { + input []Change + expected []Change + }{ + {[]Change{}, []Change{}}, + { + []Change{ + { + Type: xdr.LedgerEntryTypeAccount, + Pre: nil, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 11, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + }, + }, + }, + }, + }, + []Change{ + { + Type: xdr.LedgerEntryTypeAccount, + Pre: nil, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 11, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + }, + }, + }, + }, + }, + }, + { + []Change{ + { + Type: xdr.LedgerEntryTypeAccount, + Pre: nil, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 11, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Balance: 25, + }, + }, + }, + }, + { + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 11, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GCMNSW2UZMSH3ZFRLWP6TW2TG4UX4HLSYO5HNIKUSFMLN2KFSF26JKWF"), + Balance: 20, + }, + }, + }, + Post: nil, + }, + { + Type: xdr.LedgerEntryTypeExpiration, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 11, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeExpiration, + Expiration: &xdr.ExpirationEntry{ + KeyHash: xdr.Hash{1}, + ExpirationLedgerSeq: 50, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 11, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeExpiration, + Expiration: &xdr.ExpirationEntry{ + KeyHash: xdr.Hash{1}, + ExpirationLedgerSeq: 100, + }, + }, + }, + }, + { + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 11, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Balance: 25, + }, + }, + }, + Post: nil, + }, + }, + []Change{ + { + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 11, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GCMNSW2UZMSH3ZFRLWP6TW2TG4UX4HLSYO5HNIKUSFMLN2KFSF26JKWF"), + Balance: 20, + }, + }, + }, + Post: nil, + }, + { + Type: xdr.LedgerEntryTypeAccount, + Pre: nil, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 11, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Balance: 25, + }, + }, + }, + }, + { + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 11, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Balance: 25, + }, + }, + }, + Post: nil, + }, + + { + Type: xdr.LedgerEntryTypeExpiration, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 11, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeExpiration, + Expiration: &xdr.ExpirationEntry{ + KeyHash: xdr.Hash{1}, + ExpirationLedgerSeq: 50, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 11, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeExpiration, + Expiration: &xdr.ExpirationEntry{ + KeyHash: xdr.Hash{1}, + ExpirationLedgerSeq: 100, + }, + }, + }, + }, + }, + }, + } { + sortChanges(testCase.input) + assert.Equal(t, len(testCase.input), len(testCase.expected)) + for i := range testCase.input { + assertChangesAreEqual(t, testCase.input[i], testCase.expected[i]) + } + } +} diff --git a/ingest/ledger_change_reader.go b/ingest/ledger_change_reader.go index a539e057ea..d09c579dbd 100644 --- a/ingest/ledger_change_reader.go +++ b/ingest/ledger_change_reader.go @@ -139,6 +139,7 @@ func (r *LedgerChangeReader) Read() (Change, error) { Post: nil, } } + sortChanges(changes) r.pending = append(r.pending, changes...) r.state++ return r.Read() diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 575c2994e8..c3c5705464 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -9,6 +9,10 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). - Add a deprecation warning for using command-line flags when running Horizon ([5051](https://github.com/stellar/go/pull/5051)) - Deprecate configuration flags related to legacy non-captive core ingestion ([5100](https://github.com/stellar/go/pull/5100)) +## 2.27.0 + +### Fixed +- Ordering of effects are now deterministic. Previously the order of some Horizon effects could vary upon reingestion but this issue has now been fixed ([5070](https://github.com/stellar/go/pull/5070)). ## 2.27.0-rc2 ### Fixed diff --git a/services/horizon/internal/ingest/processors/effects_processor.go b/services/horizon/internal/ingest/processors/effects_processor.go index 496c4bc9b5..b8f97836b9 100644 --- a/services/horizon/internal/ingest/processors/effects_processor.go +++ b/services/horizon/internal/ingest/processors/effects_processor.go @@ -1274,12 +1274,6 @@ func setTrustLineFlagDetails(flagDetails map[string]interface{}, flags xdr.Trust } } -type sortableClaimableBalanceEntries []*xdr.ClaimableBalanceEntry - -func (s sortableClaimableBalanceEntries) Len() int { return len(s) } -func (s sortableClaimableBalanceEntries) Less(i, j int) bool { return s[i].Asset.LessThan(s[j].Asset) } -func (s sortableClaimableBalanceEntries) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - func (e *effectsWrapper) addLiquidityPoolRevokedEffect() error { source := e.operation.SourceAccount() lp, delta, err := e.operation.getLiquidityPoolAndProductDelta(nil) @@ -1295,7 +1289,6 @@ func (e *effectsWrapper) addLiquidityPoolRevokedEffect() error { return err } assetToCBID := map[string]string{} - var cbs sortableClaimableBalanceEntries for _, change := range changes { if change.Type == xdr.LedgerEntryTypeClaimableBalance && change.Pre == nil && change.Post != nil { cb := change.Post.Data.ClaimableBalance @@ -1304,21 +1297,15 @@ func (e *effectsWrapper) addLiquidityPoolRevokedEffect() error { return err } assetToCBID[cb.Asset.StringCanonical()] = id - cbs = append(cbs, cb) + if err := e.addClaimableBalanceEntryCreatedEffects(source, cb); err != nil { + return err + } } } if len(assetToCBID) == 0 { // no claimable balances were created, and thus, no revocation happened return nil } - // Core's claimable balance metadata isn't ordered, so we order it ourselves - // so that effects are ordered consistently - sort.Sort(cbs) - for _, cb := range cbs { - if err := e.addClaimableBalanceEntryCreatedEffects(source, cb); err != nil { - return err - } - } reservesRevoked := make([]map[string]string, 0, 2) for _, aa := range []base.AssetAmount{ diff --git a/services/horizon/internal/ingest/processors/transaction_operation_wrapper_test.go b/services/horizon/internal/ingest/processors/transaction_operation_wrapper_test.go index a96d4efef9..97b83f8f1b 100644 --- a/services/horizon/internal/ingest/processors/transaction_operation_wrapper_test.go +++ b/services/horizon/internal/ingest/processors/transaction_operation_wrapper_test.go @@ -5,10 +5,11 @@ package processors import ( "testing" - "github.com/stellar/go/protocols/horizon/base" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/ingest" "github.com/stellar/go/services/horizon/internal/db2/history" . "github.com/stellar/go/services/horizon/internal/test/transactions" @@ -1521,6 +1522,13 @@ func getSponsoredSandwichWrappers() []*transactionOperationWrapper { { Type: xdr.LedgerEntryChangeTypeLedgerEntryCreated, Created: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY"), + Balance: 100, + }, + }, LastModifiedLedgerSeq: xdr.Uint32(ledgerSeq), Ext: xdr.LedgerEntryExt{ V: 1, diff --git a/services/horizon/internal/integration/liquidity_pool_test.go b/services/horizon/internal/integration/liquidity_pool_test.go index 3f792486ad..9106003179 100644 --- a/services/horizon/internal/integration/liquidity_pool_test.go +++ b/services/horizon/internal/integration/liquidity_pool_test.go @@ -606,27 +606,45 @@ func TestLiquidityPoolRevoke(t *testing.T) { tt.Equal(master.Address(), ef4.Asset.Issuer) tt.Equal(shareAccount.GetAccountID(), ef4.Trustor) + // the ordering of the claimable_balance_created effects depends on + // the ids of the claimable balances which can vary between test runs. + // we assert that there will be two claimable balances created, + // one holding 777 usd and another holding 400 xlm but + // we don't know the ordering since it depends on the claimable + // balance ids which we don't know ahead of time. + usdAsset := fmt.Sprintf("USD:%s", master.Address()) + expectedAmount := map[string]string{ + usdAsset: "777.0000000", + "native": "400.0000000", + } ef5 := (effs.Embedded.Records[4]).(effects.ClaimableBalanceCreated) tt.Equal("claimable_balance_created", ef5.Type) - tt.Equal("native", ef5.Asset) - tt.Equal("400.0000000", ef5.Amount) + var expectedNextAsset string + if ef5.Asset == usdAsset { + expectedNextAsset = "native" + } else if ef5.Asset == "native" { + expectedNextAsset = usdAsset + } else { + tt.Failf("unexpected asset %v", ef5.Asset) + } + tt.Equal(expectedAmount[ef5.Asset], ef5.Amount) ef6 := (effs.Embedded.Records[5]).(effects.ClaimableBalanceClaimantCreated) tt.Equal("claimable_balance_claimant_created", ef6.Type) - tt.Equal("native", ef6.Asset) - tt.Equal("400.0000000", ef6.Amount) + tt.Equal(ef5.Asset, ef6.Asset) + tt.Equal(ef5.Amount, ef6.Amount) tt.Equal(shareKeys.Address(), ef6.Account) tt.Equal(xdr.ClaimPredicateTypeClaimPredicateUnconditional, ef6.Predicate.Type) ef7 := (effs.Embedded.Records[6]).(effects.ClaimableBalanceCreated) tt.Equal("claimable_balance_created", ef7.Type) - tt.Equal(fmt.Sprintf("USD:%s", master.Address()), ef7.Asset) - tt.Equal("777.0000000", ef7.Amount) + tt.Equal(expectedNextAsset, ef7.Asset) + tt.Equal(expectedAmount[ef7.Asset], ef7.Amount) ef8 := (effs.Embedded.Records[7]).(effects.ClaimableBalanceClaimantCreated) tt.Equal("claimable_balance_claimant_created", ef8.Type) - tt.Equal(fmt.Sprintf("USD:%s", master.Address()), ef8.Asset) - tt.Equal("777.0000000", ef8.Amount) + tt.Equal(ef7.Asset, ef8.Asset) + tt.Equal(ef7.Amount, ef8.Amount) tt.Equal(shareKeys.Address(), ef8.Account) tt.Equal(xdr.ClaimPredicateTypeClaimPredicateUnconditional, ef8.Predicate.Type) diff --git a/xdr/ledger_close_meta.go b/xdr/ledger_close_meta.go index c8b85bae97..c2b352b613 100644 --- a/xdr/ledger_close_meta.go +++ b/xdr/ledger_close_meta.go @@ -1,6 +1,8 @@ package xdr -import "fmt" +import ( + "fmt" +) func (l LedgerCloseMeta) LedgerHeaderHistoryEntry() LedgerHeaderHistoryEntry { switch l.V { From 0292a74fffd433ab411f616403bca7e2619580a1 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Tue, 17 Oct 2023 20:20:19 +0200 Subject: [PATCH 007/234] all: Bump XDR for soroban-pubnet release (#5079) Update XDR to [`bdb81c3710ecb12f0fcc23268b211eb237500019`](https://github.com/stellar/stellar-xdr/tree/bdb81c3710ecb12f0fcc23268b211eb237500019) --- .github/workflows/horizon.yml | 10 +- Makefile | 2 +- gxdr/xdr_generated.go | 685 +++++----- ingest/change_test.go | 36 +- ingest/ledger_change_reader_test.go | 8 +- ingest/stats_change_processor.go | 20 +- ingest/stats_change_processor_test.go | 6 +- protocols/horizon/operations/main.go | 14 +- services/horizon/internal/codes/main.go | 18 +- services/horizon/internal/codes/main_test.go | 5 +- services/horizon/internal/ingest/main_test.go | 2 +- .../ingest/processors/contract_data.go | 2 +- .../ingest/processors/effects_processor.go | 2 +- .../ingest/processors/operations_processor.go | 9 +- .../processors/operations_processor_test.go | 2 +- .../stats_ledger_transaction_processor.go | 6 +- services/horizon/internal/ingest/verify.go | 8 +- .../horizon/internal/ingest/verify_test.go | 6 +- .../internal/integration/contracts/Cargo.lock | 176 +-- .../internal/integration/contracts/Cargo.toml | 2 +- .../horizon/internal/integration/db_test.go | 12 +- ...n_test.go => extend_footprint_ttl_test.go} | 19 +- .../integration/invokehostfunction_test.go | 18 +- .../horizon/internal/integration/sac_test.go | 2 +- .../integration/testdata/soroban_add_u64.wasm | Bin 631 -> 631 bytes .../testdata/soroban_increment_contract.wasm | Bin 701 -> 701 bytes .../testdata/soroban_sac_test.wasm | Bin 1924 -> 1924 bytes .../internal/integration/txsub_test.go | 4 +- .../internal/resourceadapter/operations.go | 4 +- .../internal/test/integration/integration.go | 4 +- txnbuild/bump_footprint_expiration.go | 59 - txnbuild/extend_footprint_ttl.go | 59 + txnbuild/invoke_host_function_test.go | 2 +- txnbuild/operation.go | 4 +- xdr/Stellar-contract-config-setting.x | 80 +- xdr/Stellar-contract.x | 4 +- xdr/Stellar-internal.x | 7 + xdr/Stellar-ledger-entries.x | 18 +- xdr/Stellar-ledger.x | 28 +- xdr/Stellar-transaction.x | 58 +- xdr/ledger_close_meta.go | 43 +- xdr/ledger_entry.go | 4 +- xdr/ledger_key.go | 18 +- xdr/scval.go | 2 +- xdr/xdr_commit_generated.txt | 2 +- xdr/xdr_generated.go | 1142 ++++++++--------- 46 files changed, 1195 insertions(+), 1417 deletions(-) rename services/horizon/internal/integration/{bump_footprint_expiration_test.go => extend_footprint_ttl_test.go} (82%) delete mode 100644 txnbuild/bump_footprint_expiration.go create mode 100644 txnbuild/extend_footprint_ttl.go diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index 3fd687e31b..6139ad6806 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -33,11 +33,11 @@ jobs: env: HORIZON_INTEGRATION_TESTS_ENABLED: true HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL: ${{ matrix.protocol-version }} - PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 19.13.1-1481.3acf6dd26.focal - PROTOCOL_20_CORE_DOCKER_IMG: stellar/stellar-core:19.13.1-1481.3acf6dd26.focal - PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:20.0.0-rc3-39 - PROTOCOL_19_CORE_DEBIAN_PKG_VERSION: 19.14.0-1500.5664eff4e.focal - PROTOCOL_19_CORE_DOCKER_IMG: stellar/stellar-core:19.14.0-1500.5664eff4e.focal + PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 19.14.1-1529.fcbbad4ce.focal + PROTOCOL_20_CORE_DOCKER_IMG: 2opremio/stellar-core:19.14.1-1529.fcbbad4ce.focal + PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:20.0.0-rc4pubnet-42 + PROTOCOL_19_CORE_DEBIAN_PKG_VERSION: 19.12.0-1378.2109a168a.focal + PROTOCOL_19_CORE_DOCKER_IMG: stellar/stellar-core:19.12.0-1378.2109a168a.focal PGHOST: localhost PGPORT: 5432 PGUSER: postgres diff --git a/Makefile b/Makefile index 063a1a6eb0..4266730900 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ xdr/Stellar-internal.x \ xdr/Stellar-contract-config-setting.x XDRGEN_COMMIT=a231a92475ac6154c0c2f46dc503809823985060 -XDR_COMMIT=9ac02641139e6717924fdad716f6e958d0168491 +XDR_COMMIT=6a620d160aab22609c982d54578ff6a63bfcdc01 .PHONY: xdr xdr-clean xdr-update diff --git a/gxdr/xdr_generated.go b/gxdr/xdr_generated.go index d1b8635d15..2c20497952 100644 --- a/gxdr/xdr_generated.go +++ b/gxdr/xdr_generated.go @@ -205,7 +205,7 @@ const ( CONTRACT_DATA LedgerEntryType = 6 CONTRACT_CODE LedgerEntryType = 7 CONFIG_SETTING LedgerEntryType = 8 - EXPIRATION LedgerEntryType = 9 + TTL LedgerEntryType = 9 ) type Signer struct { @@ -645,10 +645,10 @@ type ContractCodeEntry struct { Code []byte } -type ExpirationEntry struct { - // Hash of the LedgerKey that is associated with this ExpirationEntry - KeyHash Hash - ExpirationLedgerSeq Uint32 +type TTLEntry struct { + // Hash of the LedgerKey that is associated with this TTLEntry + KeyHash Hash + LiveUntilLedgerSeq Uint32 } type LedgerEntryExtensionV1 struct { @@ -689,8 +689,8 @@ type XdrAnon_LedgerEntry_Data struct { // ContractCode() *ContractCodeEntry // CONFIG_SETTING: // ConfigSetting() *ConfigSettingEntry - // EXPIRATION: - // Expiration() *ExpirationEntry + // TTL: + // Ttl() *TTLEntry Type LedgerEntryType _u interface{} } @@ -726,8 +726,8 @@ type LedgerKey struct { // ContractCode() *XdrAnon_LedgerKey_ContractCode // CONFIG_SETTING: // ConfigSetting() *XdrAnon_LedgerKey_ConfigSetting - // EXPIRATION: - // Expiration() *XdrAnon_LedgerKey_Expiration + // TTL: + // Ttl() *XdrAnon_LedgerKey_Ttl Type LedgerEntryType _u interface{} } @@ -763,8 +763,8 @@ type XdrAnon_LedgerKey_ContractCode struct { type XdrAnon_LedgerKey_ConfigSetting struct { ConfigSettingID ConfigSettingID } -type XdrAnon_LedgerKey_Expiration struct { - // Hash of the LedgerKey that is associated with this ExpirationEntry +type XdrAnon_LedgerKey_Ttl struct { + // Hash of the LedgerKey that is associated with this TTLEntry KeyHash Hash } @@ -1274,21 +1274,8 @@ type LedgerCloseMetaV0 struct { } type LedgerCloseMetaV1 struct { - LedgerHeader LedgerHeaderHistoryEntry - TxSet GeneralizedTransactionSet - // NB: transactions are sorted in apply order here - // fees for all transactions are processed first - // followed by applying transactions - TxProcessing []TransactionResultMeta - // upgrades are applied last - UpgradesProcessing []UpgradeEntryMeta - // other misc information attached to the ledger close - ScpInfo []SCPHistoryEntry -} - -type LedgerCloseMetaV2 struct { - // We forgot to add an ExtensionPoint in v1 but at least - // we can add one now in v2. + // We forgot to add an ExtensionPoint in v0 but at least + // we can add one now in v1. Ext ExtensionPoint LedgerHeader LedgerHeaderHistoryEntry TxSet GeneralizedTransactionSet @@ -1303,9 +1290,9 @@ type LedgerCloseMetaV2 struct { // Size in bytes of BucketList, to support downstream // systems calculating storage fees correctly. TotalByteSizeOfBucketList Uint64 - // Expired temp keys that are being evicted at this ledger. + // Temp keys that are being evicted at this ledger. EvictedTemporaryLedgerKeys []LedgerKey - // Expired restorable ledger entries that are being + // Archived restorable ledger entries that are being // evicted at this ledger. EvictedPersistentLedgerEntries []LedgerEntry } @@ -1316,8 +1303,6 @@ type LedgerCloseMeta struct { // V0() *LedgerCloseMetaV0 // 1: // V1() *LedgerCloseMetaV1 - // 2: - // V2() *LedgerCloseMetaV2 V int32 _u interface{} } @@ -1663,7 +1648,7 @@ const ( LIQUIDITY_POOL_DEPOSIT OperationType = 22 LIQUIDITY_POOL_WITHDRAW OperationType = 23 INVOKE_HOST_FUNCTION OperationType = 24 - BUMP_FOOTPRINT_EXPIRATION OperationType = 25 + EXTEND_FOOTPRINT_TTL OperationType = 25 RESTORE_FOOTPRINT OperationType = 26 ) @@ -2187,20 +2172,20 @@ type InvokeHostFunctionOp struct { } /* -Bump the expiration ledger of the entries specified in the readOnly footprint +Extend the TTL of the entries specified in the readOnly footprint - so they'll expire at least ledgersToExpire ledgers from lcl. + so they will live at least extendTo ledgers from lcl. Threshold: med - Result: BumpFootprintExpirationResult + Result: ExtendFootprintTTLResult */ -type BumpFootprintExpirationOp struct { - Ext ExtensionPoint - LedgersToExpire Uint32 +type ExtendFootprintTTLOp struct { + Ext ExtensionPoint + ExtendTo Uint32 } /* -Restore the expired or evicted entries specified in the readWrite footprint. +Restore the archived entries specified in the readWrite footprint. Threshold: med Result: RestoreFootprintOp @@ -2269,8 +2254,8 @@ type XdrAnon_Operation_Body struct { // LiquidityPoolWithdrawOp() *LiquidityPoolWithdrawOp // INVOKE_HOST_FUNCTION: // InvokeHostFunctionOp() *InvokeHostFunctionOp - // BUMP_FOOTPRINT_EXPIRATION: - // BumpFootprintExpirationOp() *BumpFootprintExpirationOp + // EXTEND_FOOTPRINT_TTL: + // ExtendFootprintTTLOp() *ExtendFootprintTTLOp // RESTORE_FOOTPRINT: // RestoreFootprintOp() *RestoreFootprintOp Type OperationType @@ -2421,8 +2406,16 @@ type SorobanResources struct { type SorobanTransactionData struct { Ext ExtensionPoint Resources SorobanResources - // Portion of transaction `fee` allocated to refundable fees. - RefundableFee Int64 + // Amount of the transaction `fee` allocated to the Soroban resource fees. + // The fraction of `resourceFee` corresponding to `resources` specified + // above is *not* refundable (i.e. fees for instructions, ledger I/O), as + // well as fees for the transaction size. + // The remaining part of the fee is refundable and the charged value is + // based on the actual consumption of refundable resources (events, ledger + // rent bumps). + // The `inclusionFee` used for prioritization of the transaction is defined + // as `tx.fee - resourceFee`. + ResourceFee Int64 } // TransactionV0 is a transaction with the AccountID discriminant stripped off, @@ -3320,7 +3313,7 @@ const ( INVOKE_HOST_FUNCTION_MALFORMED InvokeHostFunctionResultCode = -1 INVOKE_HOST_FUNCTION_TRAPPED InvokeHostFunctionResultCode = -2 INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED InvokeHostFunctionResultCode = -3 - INVOKE_HOST_FUNCTION_ENTRY_EXPIRED InvokeHostFunctionResultCode = -4 + INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED InvokeHostFunctionResultCode = -4 INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE InvokeHostFunctionResultCode = -5 ) @@ -3328,30 +3321,30 @@ type InvokeHostFunctionResult struct { // The union discriminant Code selects among the following arms: // INVOKE_HOST_FUNCTION_SUCCESS: // Success() *Hash - // INVOKE_HOST_FUNCTION_MALFORMED, INVOKE_HOST_FUNCTION_TRAPPED, INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED, INVOKE_HOST_FUNCTION_ENTRY_EXPIRED, INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE: + // INVOKE_HOST_FUNCTION_MALFORMED, INVOKE_HOST_FUNCTION_TRAPPED, INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED, INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED, INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE: // void Code InvokeHostFunctionResultCode _u interface{} } -type BumpFootprintExpirationResultCode int32 +type ExtendFootprintTTLResultCode int32 const ( // codes considered as "success" for the operation - BUMP_FOOTPRINT_EXPIRATION_SUCCESS BumpFootprintExpirationResultCode = 0 + EXTEND_FOOTPRINT_TTL_SUCCESS ExtendFootprintTTLResultCode = 0 // codes considered as "failure" for the operation - BUMP_FOOTPRINT_EXPIRATION_MALFORMED BumpFootprintExpirationResultCode = -1 - BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED BumpFootprintExpirationResultCode = -2 - BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE BumpFootprintExpirationResultCode = -3 + EXTEND_FOOTPRINT_TTL_MALFORMED ExtendFootprintTTLResultCode = -1 + EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED ExtendFootprintTTLResultCode = -2 + EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE ExtendFootprintTTLResultCode = -3 ) -type BumpFootprintExpirationResult struct { +type ExtendFootprintTTLResult struct { // The union discriminant Code selects among the following arms: - // BUMP_FOOTPRINT_EXPIRATION_SUCCESS: + // EXTEND_FOOTPRINT_TTL_SUCCESS: // void - // BUMP_FOOTPRINT_EXPIRATION_MALFORMED, BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED, BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE: + // EXTEND_FOOTPRINT_TTL_MALFORMED, EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED, EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE: // void - Code BumpFootprintExpirationResultCode + Code ExtendFootprintTTLResultCode _u interface{} } @@ -3457,8 +3450,8 @@ type XdrAnon_OperationResult_Tr struct { // LiquidityPoolWithdrawResult() *LiquidityPoolWithdrawResult // INVOKE_HOST_FUNCTION: // InvokeHostFunctionResult() *InvokeHostFunctionResult - // BUMP_FOOTPRINT_EXPIRATION: - // BumpFootprintExpirationResult() *BumpFootprintExpirationResult + // EXTEND_FOOTPRINT_TTL: + // ExtendFootprintTTLResult() *ExtendFootprintTTLResult // RESTORE_FOOTPRINT: // RestoreFootprintResult() *RestoreFootprintResult Type OperationType @@ -4050,15 +4043,15 @@ type Int256Parts struct { type ContractExecutableType int32 const ( - CONTRACT_EXECUTABLE_WASM ContractExecutableType = 0 - CONTRACT_EXECUTABLE_TOKEN ContractExecutableType = 1 + CONTRACT_EXECUTABLE_WASM ContractExecutableType = 0 + CONTRACT_EXECUTABLE_STELLAR_ASSET ContractExecutableType = 1 ) type ContractExecutable struct { // The union discriminant Type selects among the following arms: // CONTRACT_EXECUTABLE_WASM: // Wasm_hash() *Hash - // CONTRACT_EXECUTABLE_TOKEN: + // CONTRACT_EXECUTABLE_STELLAR_ASSET: // void Type ContractExecutableType _u interface{} @@ -4167,6 +4160,12 @@ type StoredTransactionSet struct { _u interface{} } +type StoredDebugTransactionSet struct { + TxSet StoredTransactionSet + LedgerSeq Uint32 + ScpValue StellarValue +} + type PersistedSCPStateV0 struct { ScpEnvelopes []SCPEnvelope QuorumSets []SCPQuorumSet @@ -4274,64 +4273,54 @@ type ContractCostType int32 const ( // Cost of running 1 wasm instruction WasmInsnExec ContractCostType = 0 - // Cost of growing wasm linear memory by 1 page - WasmMemAlloc ContractCostType = 1 - // Cost of allocating a chuck of host memory (in bytes) - HostMemAlloc ContractCostType = 2 - // Cost of copying a chuck of bytes into a pre-allocated host memory - HostMemCpy ContractCostType = 3 - // Cost of comparing two slices of host memory - HostMemCmp ContractCostType = 4 + // Cost of allocating a slice of memory (in bytes) + MemAlloc ContractCostType = 1 + // Cost of copying a slice of bytes into a pre-allocated memory + MemCpy ContractCostType = 2 + // Cost of comparing two slices of memory + MemCmp ContractCostType = 3 // Cost of a host function dispatch, not including the actual work done by // the function nor the cost of VM invocation machinary - DispatchHostFunction ContractCostType = 5 + DispatchHostFunction ContractCostType = 4 // Cost of visiting a host object from the host object storage. Exists to // make sure some baseline cost coverage, i.e. repeatly visiting objects // by the guest will always incur some charges. - VisitObject ContractCostType = 6 + VisitObject ContractCostType = 5 // Cost of serializing an xdr object to bytes - ValSer ContractCostType = 7 + ValSer ContractCostType = 6 // Cost of deserializing an xdr object from bytes - ValDeser ContractCostType = 8 + ValDeser ContractCostType = 7 // Cost of computing the sha256 hash from bytes - ComputeSha256Hash ContractCostType = 9 + ComputeSha256Hash ContractCostType = 8 // Cost of computing the ed25519 pubkey from bytes - ComputeEd25519PubKey ContractCostType = 10 - // Cost of accessing an entry in a Map. - MapEntry ContractCostType = 11 - // Cost of accessing an entry in a Vec - VecEntry ContractCostType = 12 + ComputeEd25519PubKey ContractCostType = 9 // Cost of verifying ed25519 signature of a payload. - VerifyEd25519Sig ContractCostType = 13 - // Cost of reading a slice of vm linear memory - VmMemRead ContractCostType = 14 - // Cost of writing to a slice of vm linear memory - VmMemWrite ContractCostType = 15 + VerifyEd25519Sig ContractCostType = 10 // Cost of instantiation a VM from wasm bytes code. - VmInstantiation ContractCostType = 16 + VmInstantiation ContractCostType = 11 // Cost of instantiation a VM from a cached state. - VmCachedInstantiation ContractCostType = 17 + VmCachedInstantiation ContractCostType = 12 // Cost of invoking a function on the VM. If the function is a host function, // additional cost will be covered by `DispatchHostFunction`. - InvokeVmFunction ContractCostType = 18 + InvokeVmFunction ContractCostType = 13 // Cost of computing a keccak256 hash from bytes. - ComputeKeccak256Hash ContractCostType = 19 - // Cost of computing an ECDSA secp256k1 pubkey from bytes. - ComputeEcdsaSecp256k1Key ContractCostType = 20 + ComputeKeccak256Hash ContractCostType = 14 // Cost of computing an ECDSA secp256k1 signature from bytes. - ComputeEcdsaSecp256k1Sig ContractCostType = 21 + ComputeEcdsaSecp256k1Sig ContractCostType = 15 // Cost of recovering an ECDSA secp256k1 key from a signature. - RecoverEcdsaSecp256k1Key ContractCostType = 22 + RecoverEcdsaSecp256k1Key ContractCostType = 16 // Cost of int256 addition (`+`) and subtraction (`-`) operations - Int256AddSub ContractCostType = 23 + Int256AddSub ContractCostType = 17 // Cost of int256 multiplication (`*`) operation - Int256Mul ContractCostType = 24 + Int256Mul ContractCostType = 18 // Cost of int256 division (`/`) operation - Int256Div ContractCostType = 25 + Int256Div ContractCostType = 19 // Cost of int256 power (`exp`) operation - Int256Pow ContractCostType = 26 + Int256Pow ContractCostType = 20 // Cost of int256 shift (`shl`, `shr`) operation - Int256Shift ContractCostType = 27 + Int256Shift ContractCostType = 21 + // Cost of drawing random bytes using a ChaCha20 PRNG + ChaCha20DrawBytes ContractCostType = 22 ) type ContractCostParamEntry struct { @@ -4341,15 +4330,15 @@ type ContractCostParamEntry struct { LinearTerm Int64 } -type StateExpirationSettings struct { - MaxEntryExpiration Uint32 - MinTempEntryExpiration Uint32 - MinPersistentEntryExpiration Uint32 +type StateArchivalSettings struct { + MaxEntryTTL Uint32 + MinTemporaryTTL Uint32 + MinPersistentTTL Uint32 // rent_fee = wfee_rate_average / rent_rate_denominator_for_type PersistentRentRateDenominator Int64 TempRentRateDenominator Int64 - // max number of entries that emit expiration meta in a single ledger - MaxEntriesToExpire Uint32 + // max number of entries that emit archival meta in a single ledger + MaxEntriesToArchive Uint32 // Number of snapshots to use when calculating average BucketList size BucketListSizeWindowSampleSize Uint32 // Maximum number of bytes that we scan for eviction per ledger @@ -4383,7 +4372,7 @@ const ( CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES ConfigSettingID = 7 CONFIG_SETTING_CONTRACT_DATA_KEY_SIZE_BYTES ConfigSettingID = 8 CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES ConfigSettingID = 9 - CONFIG_SETTING_STATE_EXPIRATION ConfigSettingID = 10 + CONFIG_SETTING_STATE_ARCHIVAL ConfigSettingID = 10 CONFIG_SETTING_CONTRACT_EXECUTION_LANES ConfigSettingID = 11 CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW ConfigSettingID = 12 CONFIG_SETTING_EVICTION_ITERATOR ConfigSettingID = 13 @@ -4411,8 +4400,8 @@ type ConfigSettingEntry struct { // ContractDataKeySizeBytes() *Uint32 // CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES: // ContractDataEntrySizeBytes() *Uint32 - // CONFIG_SETTING_STATE_EXPIRATION: - // StateExpirationSettings() *StateExpirationSettings + // CONFIG_SETTING_STATE_ARCHIVAL: + // StateArchivalSettings() *StateArchivalSettings // CONFIG_SETTING_CONTRACT_EXECUTION_LANES: // ContractExecutionLanes() *ConfigSettingContractExecutionLanesV0 // CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW: @@ -5497,7 +5486,7 @@ var _XdrNames_LedgerEntryType = map[int32]string{ int32(CONTRACT_DATA): "CONTRACT_DATA", int32(CONTRACT_CODE): "CONTRACT_CODE", int32(CONFIG_SETTING): "CONFIG_SETTING", - int32(EXPIRATION): "EXPIRATION", + int32(TTL): "TTL", } var _XdrValues_LedgerEntryType = map[string]int32{ "ACCOUNT": int32(ACCOUNT), @@ -5509,7 +5498,7 @@ var _XdrValues_LedgerEntryType = map[string]int32{ "CONTRACT_DATA": int32(CONTRACT_DATA), "CONTRACT_CODE": int32(CONTRACT_CODE), "CONFIG_SETTING": int32(CONFIG_SETTING), - "EXPIRATION": int32(EXPIRATION), + "TTL": int32(TTL), } func (LedgerEntryType) XdrEnumNames() map[int32]string { @@ -7950,20 +7939,20 @@ func (v *ContractCodeEntry) XdrRecurse(x XDR, name string) { } func XDR_ContractCodeEntry(v *ContractCodeEntry) *ContractCodeEntry { return v } -type XdrType_ExpirationEntry = *ExpirationEntry +type XdrType_TTLEntry = *TTLEntry -func (v *ExpirationEntry) XdrPointer() interface{} { return v } -func (ExpirationEntry) XdrTypeName() string { return "ExpirationEntry" } -func (v ExpirationEntry) XdrValue() interface{} { return v } -func (v *ExpirationEntry) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -func (v *ExpirationEntry) XdrRecurse(x XDR, name string) { +func (v *TTLEntry) XdrPointer() interface{} { return v } +func (TTLEntry) XdrTypeName() string { return "TTLEntry" } +func (v TTLEntry) XdrValue() interface{} { return v } +func (v *TTLEntry) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *TTLEntry) XdrRecurse(x XDR, name string) { if name != "" { name = x.Sprintf("%s.", name) } x.Marshal(x.Sprintf("%skeyHash", name), XDR_Hash(&v.KeyHash)) - x.Marshal(x.Sprintf("%sexpirationLedgerSeq", name), XDR_Uint32(&v.ExpirationLedgerSeq)) + x.Marshal(x.Sprintf("%sliveUntilLedgerSeq", name), XDR_Uint32(&v.LiveUntilLedgerSeq)) } -func XDR_ExpirationEntry(v *ExpirationEntry) *ExpirationEntry { return v } +func XDR_TTLEntry(v *TTLEntry) *TTLEntry { return v } var _XdrTags_XdrAnon_LedgerEntryExtensionV1_Ext = map[int32]bool{ XdrToI32(0): true, @@ -8048,7 +8037,7 @@ var _XdrTags_XdrAnon_LedgerEntry_Data = map[int32]bool{ XdrToI32(CONTRACT_DATA): true, XdrToI32(CONTRACT_CODE): true, XdrToI32(CONFIG_SETTING): true, - XdrToI32(EXPIRATION): true, + XdrToI32(TTL): true, } func (_ XdrAnon_LedgerEntry_Data) XdrValidTags() map[int32]bool { @@ -8189,24 +8178,24 @@ func (u *XdrAnon_LedgerEntry_Data) ConfigSetting() *ConfigSettingEntry { return nil } } -func (u *XdrAnon_LedgerEntry_Data) Expiration() *ExpirationEntry { +func (u *XdrAnon_LedgerEntry_Data) Ttl() *TTLEntry { switch u.Type { - case EXPIRATION: - if v, ok := u._u.(*ExpirationEntry); ok { + case TTL: + if v, ok := u._u.(*TTLEntry); ok { return v } else { - var zero ExpirationEntry + var zero TTLEntry u._u = &zero return &zero } default: - XdrPanic("XdrAnon_LedgerEntry_Data.Expiration accessed when Type == %v", u.Type) + XdrPanic("XdrAnon_LedgerEntry_Data.Ttl accessed when Type == %v", u.Type) return nil } } func (u XdrAnon_LedgerEntry_Data) XdrValid() bool { switch u.Type { - case ACCOUNT, TRUSTLINE, OFFER, DATA, CLAIMABLE_BALANCE, LIQUIDITY_POOL, CONTRACT_DATA, CONTRACT_CODE, CONFIG_SETTING, EXPIRATION: + case ACCOUNT, TRUSTLINE, OFFER, DATA, CLAIMABLE_BALANCE, LIQUIDITY_POOL, CONTRACT_DATA, CONTRACT_CODE, CONFIG_SETTING, TTL: return true } return false @@ -8237,8 +8226,8 @@ func (u *XdrAnon_LedgerEntry_Data) XdrUnionBody() XdrType { return XDR_ContractCodeEntry(u.ContractCode()) case CONFIG_SETTING: return XDR_ConfigSettingEntry(u.ConfigSetting()) - case EXPIRATION: - return XDR_ExpirationEntry(u.Expiration()) + case TTL: + return XDR_TTLEntry(u.Ttl()) } return nil } @@ -8262,8 +8251,8 @@ func (u *XdrAnon_LedgerEntry_Data) XdrUnionBodyName() string { return "ContractCode" case CONFIG_SETTING: return "ConfigSetting" - case EXPIRATION: - return "Expiration" + case TTL: + return "Ttl" } return "" } @@ -8307,8 +8296,8 @@ func (u *XdrAnon_LedgerEntry_Data) XdrRecurse(x XDR, name string) { case CONFIG_SETTING: x.Marshal(x.Sprintf("%sconfigSetting", name), XDR_ConfigSettingEntry(u.ConfigSetting())) return - case EXPIRATION: - x.Marshal(x.Sprintf("%sexpiration", name), XDR_ExpirationEntry(u.Expiration())) + case TTL: + x.Marshal(x.Sprintf("%sttl", name), XDR_TTLEntry(u.Ttl())) return } XdrPanic("invalid Type (%v) in XdrAnon_LedgerEntry_Data", u.Type) @@ -8553,21 +8542,19 @@ func XDR_XdrAnon_LedgerKey_ConfigSetting(v *XdrAnon_LedgerKey_ConfigSetting) *Xd return v } -type XdrType_XdrAnon_LedgerKey_Expiration = *XdrAnon_LedgerKey_Expiration +type XdrType_XdrAnon_LedgerKey_Ttl = *XdrAnon_LedgerKey_Ttl -func (v *XdrAnon_LedgerKey_Expiration) XdrPointer() interface{} { return v } -func (XdrAnon_LedgerKey_Expiration) XdrTypeName() string { return "XdrAnon_LedgerKey_Expiration" } -func (v XdrAnon_LedgerKey_Expiration) XdrValue() interface{} { return v } -func (v *XdrAnon_LedgerKey_Expiration) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -func (v *XdrAnon_LedgerKey_Expiration) XdrRecurse(x XDR, name string) { +func (v *XdrAnon_LedgerKey_Ttl) XdrPointer() interface{} { return v } +func (XdrAnon_LedgerKey_Ttl) XdrTypeName() string { return "XdrAnon_LedgerKey_Ttl" } +func (v XdrAnon_LedgerKey_Ttl) XdrValue() interface{} { return v } +func (v *XdrAnon_LedgerKey_Ttl) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *XdrAnon_LedgerKey_Ttl) XdrRecurse(x XDR, name string) { if name != "" { name = x.Sprintf("%s.", name) } x.Marshal(x.Sprintf("%skeyHash", name), XDR_Hash(&v.KeyHash)) } -func XDR_XdrAnon_LedgerKey_Expiration(v *XdrAnon_LedgerKey_Expiration) *XdrAnon_LedgerKey_Expiration { - return v -} +func XDR_XdrAnon_LedgerKey_Ttl(v *XdrAnon_LedgerKey_Ttl) *XdrAnon_LedgerKey_Ttl { return v } var _XdrTags_LedgerKey = map[int32]bool{ XdrToI32(ACCOUNT): true, @@ -8579,7 +8566,7 @@ var _XdrTags_LedgerKey = map[int32]bool{ XdrToI32(CONTRACT_DATA): true, XdrToI32(CONTRACT_CODE): true, XdrToI32(CONFIG_SETTING): true, - XdrToI32(EXPIRATION): true, + XdrToI32(TTL): true, } func (_ LedgerKey) XdrValidTags() map[int32]bool { @@ -8720,24 +8707,24 @@ func (u *LedgerKey) ConfigSetting() *XdrAnon_LedgerKey_ConfigSetting { return nil } } -func (u *LedgerKey) Expiration() *XdrAnon_LedgerKey_Expiration { +func (u *LedgerKey) Ttl() *XdrAnon_LedgerKey_Ttl { switch u.Type { - case EXPIRATION: - if v, ok := u._u.(*XdrAnon_LedgerKey_Expiration); ok { + case TTL: + if v, ok := u._u.(*XdrAnon_LedgerKey_Ttl); ok { return v } else { - var zero XdrAnon_LedgerKey_Expiration + var zero XdrAnon_LedgerKey_Ttl u._u = &zero return &zero } default: - XdrPanic("LedgerKey.Expiration accessed when Type == %v", u.Type) + XdrPanic("LedgerKey.Ttl accessed when Type == %v", u.Type) return nil } } func (u LedgerKey) XdrValid() bool { switch u.Type { - case ACCOUNT, TRUSTLINE, OFFER, DATA, CLAIMABLE_BALANCE, LIQUIDITY_POOL, CONTRACT_DATA, CONTRACT_CODE, CONFIG_SETTING, EXPIRATION: + case ACCOUNT, TRUSTLINE, OFFER, DATA, CLAIMABLE_BALANCE, LIQUIDITY_POOL, CONTRACT_DATA, CONTRACT_CODE, CONFIG_SETTING, TTL: return true } return false @@ -8768,8 +8755,8 @@ func (u *LedgerKey) XdrUnionBody() XdrType { return XDR_XdrAnon_LedgerKey_ContractCode(u.ContractCode()) case CONFIG_SETTING: return XDR_XdrAnon_LedgerKey_ConfigSetting(u.ConfigSetting()) - case EXPIRATION: - return XDR_XdrAnon_LedgerKey_Expiration(u.Expiration()) + case TTL: + return XDR_XdrAnon_LedgerKey_Ttl(u.Ttl()) } return nil } @@ -8793,8 +8780,8 @@ func (u *LedgerKey) XdrUnionBodyName() string { return "ContractCode" case CONFIG_SETTING: return "ConfigSetting" - case EXPIRATION: - return "Expiration" + case TTL: + return "Ttl" } return "" } @@ -8838,8 +8825,8 @@ func (u *LedgerKey) XdrRecurse(x XDR, name string) { case CONFIG_SETTING: x.Marshal(x.Sprintf("%sconfigSetting", name), XDR_XdrAnon_LedgerKey_ConfigSetting(u.ConfigSetting())) return - case EXPIRATION: - x.Marshal(x.Sprintf("%sexpiration", name), XDR_XdrAnon_LedgerKey_Expiration(u.Expiration())) + case TTL: + x.Marshal(x.Sprintf("%sttl", name), XDR_XdrAnon_LedgerKey_Ttl(u.Ttl())) return } XdrPanic("invalid Type (%v) in LedgerKey", u.Type) @@ -12355,24 +12342,6 @@ func (v *LedgerCloseMetaV0) XdrRecurse(x XDR, name string) { } func XDR_LedgerCloseMetaV0(v *LedgerCloseMetaV0) *LedgerCloseMetaV0 { return v } -type XdrType_LedgerCloseMetaV1 = *LedgerCloseMetaV1 - -func (v *LedgerCloseMetaV1) XdrPointer() interface{} { return v } -func (LedgerCloseMetaV1) XdrTypeName() string { return "LedgerCloseMetaV1" } -func (v LedgerCloseMetaV1) XdrValue() interface{} { return v } -func (v *LedgerCloseMetaV1) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -func (v *LedgerCloseMetaV1) XdrRecurse(x XDR, name string) { - if name != "" { - name = x.Sprintf("%s.", name) - } - x.Marshal(x.Sprintf("%sledgerHeader", name), XDR_LedgerHeaderHistoryEntry(&v.LedgerHeader)) - x.Marshal(x.Sprintf("%stxSet", name), XDR_GeneralizedTransactionSet(&v.TxSet)) - x.Marshal(x.Sprintf("%stxProcessing", name), (*_XdrVec_unbounded_TransactionResultMeta)(&v.TxProcessing)) - x.Marshal(x.Sprintf("%supgradesProcessing", name), (*_XdrVec_unbounded_UpgradeEntryMeta)(&v.UpgradesProcessing)) - x.Marshal(x.Sprintf("%sscpInfo", name), (*_XdrVec_unbounded_SCPHistoryEntry)(&v.ScpInfo)) -} -func XDR_LedgerCloseMetaV1(v *LedgerCloseMetaV1) *LedgerCloseMetaV1 { return v } - type _XdrVec_unbounded_LedgerKey []LedgerKey func (_XdrVec_unbounded_LedgerKey) XdrBound() uint32 { @@ -12487,13 +12456,13 @@ func (v *_XdrVec_unbounded_LedgerEntry) XdrPointer() interface{} { return func (v _XdrVec_unbounded_LedgerEntry) XdrValue() interface{} { return ([]LedgerEntry)(v) } func (v *_XdrVec_unbounded_LedgerEntry) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -type XdrType_LedgerCloseMetaV2 = *LedgerCloseMetaV2 +type XdrType_LedgerCloseMetaV1 = *LedgerCloseMetaV1 -func (v *LedgerCloseMetaV2) XdrPointer() interface{} { return v } -func (LedgerCloseMetaV2) XdrTypeName() string { return "LedgerCloseMetaV2" } -func (v LedgerCloseMetaV2) XdrValue() interface{} { return v } -func (v *LedgerCloseMetaV2) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -func (v *LedgerCloseMetaV2) XdrRecurse(x XDR, name string) { +func (v *LedgerCloseMetaV1) XdrPointer() interface{} { return v } +func (LedgerCloseMetaV1) XdrTypeName() string { return "LedgerCloseMetaV1" } +func (v LedgerCloseMetaV1) XdrValue() interface{} { return v } +func (v *LedgerCloseMetaV1) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *LedgerCloseMetaV1) XdrRecurse(x XDR, name string) { if name != "" { name = x.Sprintf("%s.", name) } @@ -12507,12 +12476,11 @@ func (v *LedgerCloseMetaV2) XdrRecurse(x XDR, name string) { x.Marshal(x.Sprintf("%sevictedTemporaryLedgerKeys", name), (*_XdrVec_unbounded_LedgerKey)(&v.EvictedTemporaryLedgerKeys)) x.Marshal(x.Sprintf("%sevictedPersistentLedgerEntries", name), (*_XdrVec_unbounded_LedgerEntry)(&v.EvictedPersistentLedgerEntries)) } -func XDR_LedgerCloseMetaV2(v *LedgerCloseMetaV2) *LedgerCloseMetaV2 { return v } +func XDR_LedgerCloseMetaV1(v *LedgerCloseMetaV1) *LedgerCloseMetaV1 { return v } var _XdrTags_LedgerCloseMeta = map[int32]bool{ XdrToI32(0): true, XdrToI32(1): true, - XdrToI32(2): true, } func (_ LedgerCloseMeta) XdrValidTags() map[int32]bool { @@ -12548,24 +12516,9 @@ func (u *LedgerCloseMeta) V1() *LedgerCloseMetaV1 { return nil } } -func (u *LedgerCloseMeta) V2() *LedgerCloseMetaV2 { - switch u.V { - case 2: - if v, ok := u._u.(*LedgerCloseMetaV2); ok { - return v - } else { - var zero LedgerCloseMetaV2 - u._u = &zero - return &zero - } - default: - XdrPanic("LedgerCloseMeta.V2 accessed when V == %v", u.V) - return nil - } -} func (u LedgerCloseMeta) XdrValid() bool { switch u.V { - case 0, 1, 2: + case 0, 1: return true } return false @@ -12582,8 +12535,6 @@ func (u *LedgerCloseMeta) XdrUnionBody() XdrType { return XDR_LedgerCloseMetaV0(u.V0()) case 1: return XDR_LedgerCloseMetaV1(u.V1()) - case 2: - return XDR_LedgerCloseMetaV2(u.V2()) } return nil } @@ -12593,8 +12544,6 @@ func (u *LedgerCloseMeta) XdrUnionBodyName() string { return "V0" case 1: return "V1" - case 2: - return "V2" } return "" } @@ -12617,9 +12566,6 @@ func (u *LedgerCloseMeta) XdrRecurse(x XDR, name string) { case 1: x.Marshal(x.Sprintf("%sv1", name), XDR_LedgerCloseMetaV1(u.V1())) return - case 2: - x.Marshal(x.Sprintf("%sv2", name), XDR_LedgerCloseMetaV2(u.V2())) - return } XdrPanic("invalid V (%v) in LedgerCloseMeta", u.V) } @@ -14418,7 +14364,7 @@ var _XdrNames_OperationType = map[int32]string{ int32(LIQUIDITY_POOL_DEPOSIT): "LIQUIDITY_POOL_DEPOSIT", int32(LIQUIDITY_POOL_WITHDRAW): "LIQUIDITY_POOL_WITHDRAW", int32(INVOKE_HOST_FUNCTION): "INVOKE_HOST_FUNCTION", - int32(BUMP_FOOTPRINT_EXPIRATION): "BUMP_FOOTPRINT_EXPIRATION", + int32(EXTEND_FOOTPRINT_TTL): "EXTEND_FOOTPRINT_TTL", int32(RESTORE_FOOTPRINT): "RESTORE_FOOTPRINT", } var _XdrValues_OperationType = map[string]int32{ @@ -14447,7 +14393,7 @@ var _XdrValues_OperationType = map[string]int32{ "LIQUIDITY_POOL_DEPOSIT": int32(LIQUIDITY_POOL_DEPOSIT), "LIQUIDITY_POOL_WITHDRAW": int32(LIQUIDITY_POOL_WITHDRAW), "INVOKE_HOST_FUNCTION": int32(INVOKE_HOST_FUNCTION), - "BUMP_FOOTPRINT_EXPIRATION": int32(BUMP_FOOTPRINT_EXPIRATION), + "EXTEND_FOOTPRINT_TTL": int32(EXTEND_FOOTPRINT_TTL), "RESTORE_FOOTPRINT": int32(RESTORE_FOOTPRINT), } @@ -16259,20 +16205,20 @@ func (v *InvokeHostFunctionOp) XdrRecurse(x XDR, name string) { } func XDR_InvokeHostFunctionOp(v *InvokeHostFunctionOp) *InvokeHostFunctionOp { return v } -type XdrType_BumpFootprintExpirationOp = *BumpFootprintExpirationOp +type XdrType_ExtendFootprintTTLOp = *ExtendFootprintTTLOp -func (v *BumpFootprintExpirationOp) XdrPointer() interface{} { return v } -func (BumpFootprintExpirationOp) XdrTypeName() string { return "BumpFootprintExpirationOp" } -func (v BumpFootprintExpirationOp) XdrValue() interface{} { return v } -func (v *BumpFootprintExpirationOp) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -func (v *BumpFootprintExpirationOp) XdrRecurse(x XDR, name string) { +func (v *ExtendFootprintTTLOp) XdrPointer() interface{} { return v } +func (ExtendFootprintTTLOp) XdrTypeName() string { return "ExtendFootprintTTLOp" } +func (v ExtendFootprintTTLOp) XdrValue() interface{} { return v } +func (v *ExtendFootprintTTLOp) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *ExtendFootprintTTLOp) XdrRecurse(x XDR, name string) { if name != "" { name = x.Sprintf("%s.", name) } x.Marshal(x.Sprintf("%sext", name), XDR_ExtensionPoint(&v.Ext)) - x.Marshal(x.Sprintf("%sledgersToExpire", name), XDR_Uint32(&v.LedgersToExpire)) + x.Marshal(x.Sprintf("%sextendTo", name), XDR_Uint32(&v.ExtendTo)) } -func XDR_BumpFootprintExpirationOp(v *BumpFootprintExpirationOp) *BumpFootprintExpirationOp { return v } +func XDR_ExtendFootprintTTLOp(v *ExtendFootprintTTLOp) *ExtendFootprintTTLOp { return v } type XdrType_RestoreFootprintOp = *RestoreFootprintOp @@ -16314,7 +16260,7 @@ var _XdrTags_XdrAnon_Operation_Body = map[int32]bool{ XdrToI32(LIQUIDITY_POOL_DEPOSIT): true, XdrToI32(LIQUIDITY_POOL_WITHDRAW): true, XdrToI32(INVOKE_HOST_FUNCTION): true, - XdrToI32(BUMP_FOOTPRINT_EXPIRATION): true, + XdrToI32(EXTEND_FOOTPRINT_TTL): true, XdrToI32(RESTORE_FOOTPRINT): true, } @@ -16666,18 +16612,18 @@ func (u *XdrAnon_Operation_Body) InvokeHostFunctionOp() *InvokeHostFunctionOp { return nil } } -func (u *XdrAnon_Operation_Body) BumpFootprintExpirationOp() *BumpFootprintExpirationOp { +func (u *XdrAnon_Operation_Body) ExtendFootprintTTLOp() *ExtendFootprintTTLOp { switch u.Type { - case BUMP_FOOTPRINT_EXPIRATION: - if v, ok := u._u.(*BumpFootprintExpirationOp); ok { + case EXTEND_FOOTPRINT_TTL: + if v, ok := u._u.(*ExtendFootprintTTLOp); ok { return v } else { - var zero BumpFootprintExpirationOp + var zero ExtendFootprintTTLOp u._u = &zero return &zero } default: - XdrPanic("XdrAnon_Operation_Body.BumpFootprintExpirationOp accessed when Type == %v", u.Type) + XdrPanic("XdrAnon_Operation_Body.ExtendFootprintTTLOp accessed when Type == %v", u.Type) return nil } } @@ -16698,7 +16644,7 @@ func (u *XdrAnon_Operation_Body) RestoreFootprintOp() *RestoreFootprintOp { } func (u XdrAnon_Operation_Body) XdrValid() bool { switch u.Type { - case CREATE_ACCOUNT, PAYMENT, PATH_PAYMENT_STRICT_RECEIVE, MANAGE_SELL_OFFER, CREATE_PASSIVE_SELL_OFFER, SET_OPTIONS, CHANGE_TRUST, ALLOW_TRUST, ACCOUNT_MERGE, INFLATION, MANAGE_DATA, BUMP_SEQUENCE, MANAGE_BUY_OFFER, PATH_PAYMENT_STRICT_SEND, CREATE_CLAIMABLE_BALANCE, CLAIM_CLAIMABLE_BALANCE, BEGIN_SPONSORING_FUTURE_RESERVES, END_SPONSORING_FUTURE_RESERVES, REVOKE_SPONSORSHIP, CLAWBACK, CLAWBACK_CLAIMABLE_BALANCE, SET_TRUST_LINE_FLAGS, LIQUIDITY_POOL_DEPOSIT, LIQUIDITY_POOL_WITHDRAW, INVOKE_HOST_FUNCTION, BUMP_FOOTPRINT_EXPIRATION, RESTORE_FOOTPRINT: + case CREATE_ACCOUNT, PAYMENT, PATH_PAYMENT_STRICT_RECEIVE, MANAGE_SELL_OFFER, CREATE_PASSIVE_SELL_OFFER, SET_OPTIONS, CHANGE_TRUST, ALLOW_TRUST, ACCOUNT_MERGE, INFLATION, MANAGE_DATA, BUMP_SEQUENCE, MANAGE_BUY_OFFER, PATH_PAYMENT_STRICT_SEND, CREATE_CLAIMABLE_BALANCE, CLAIM_CLAIMABLE_BALANCE, BEGIN_SPONSORING_FUTURE_RESERVES, END_SPONSORING_FUTURE_RESERVES, REVOKE_SPONSORSHIP, CLAWBACK, CLAWBACK_CLAIMABLE_BALANCE, SET_TRUST_LINE_FLAGS, LIQUIDITY_POOL_DEPOSIT, LIQUIDITY_POOL_WITHDRAW, INVOKE_HOST_FUNCTION, EXTEND_FOOTPRINT_TTL, RESTORE_FOOTPRINT: return true } return false @@ -16761,8 +16707,8 @@ func (u *XdrAnon_Operation_Body) XdrUnionBody() XdrType { return XDR_LiquidityPoolWithdrawOp(u.LiquidityPoolWithdrawOp()) case INVOKE_HOST_FUNCTION: return XDR_InvokeHostFunctionOp(u.InvokeHostFunctionOp()) - case BUMP_FOOTPRINT_EXPIRATION: - return XDR_BumpFootprintExpirationOp(u.BumpFootprintExpirationOp()) + case EXTEND_FOOTPRINT_TTL: + return XDR_ExtendFootprintTTLOp(u.ExtendFootprintTTLOp()) case RESTORE_FOOTPRINT: return XDR_RestoreFootprintOp(u.RestoreFootprintOp()) } @@ -16820,8 +16766,8 @@ func (u *XdrAnon_Operation_Body) XdrUnionBodyName() string { return "LiquidityPoolWithdrawOp" case INVOKE_HOST_FUNCTION: return "InvokeHostFunctionOp" - case BUMP_FOOTPRINT_EXPIRATION: - return "BumpFootprintExpirationOp" + case EXTEND_FOOTPRINT_TTL: + return "ExtendFootprintTTLOp" case RESTORE_FOOTPRINT: return "RestoreFootprintOp" } @@ -16913,8 +16859,8 @@ func (u *XdrAnon_Operation_Body) XdrRecurse(x XDR, name string) { case INVOKE_HOST_FUNCTION: x.Marshal(x.Sprintf("%sinvokeHostFunctionOp", name), XDR_InvokeHostFunctionOp(u.InvokeHostFunctionOp())) return - case BUMP_FOOTPRINT_EXPIRATION: - x.Marshal(x.Sprintf("%sbumpFootprintExpirationOp", name), XDR_BumpFootprintExpirationOp(u.BumpFootprintExpirationOp())) + case EXTEND_FOOTPRINT_TTL: + x.Marshal(x.Sprintf("%sextendFootprintTTLOp", name), XDR_ExtendFootprintTTLOp(u.ExtendFootprintTTLOp())) return case RESTORE_FOOTPRINT: x.Marshal(x.Sprintf("%srestoreFootprintOp", name), XDR_RestoreFootprintOp(u.RestoreFootprintOp())) @@ -17960,7 +17906,7 @@ func (v *SorobanTransactionData) XdrRecurse(x XDR, name string) { } x.Marshal(x.Sprintf("%sext", name), XDR_ExtensionPoint(&v.Ext)) x.Marshal(x.Sprintf("%sresources", name), XDR_SorobanResources(&v.Resources)) - x.Marshal(x.Sprintf("%srefundableFee", name), XDR_Int64(&v.RefundableFee)) + x.Marshal(x.Sprintf("%sresourceFee", name), XDR_Int64(&v.ResourceFee)) } func XDR_SorobanTransactionData(v *SorobanTransactionData) *SorobanTransactionData { return v } @@ -22563,7 +22509,7 @@ var _XdrNames_InvokeHostFunctionResultCode = map[int32]string{ int32(INVOKE_HOST_FUNCTION_MALFORMED): "INVOKE_HOST_FUNCTION_MALFORMED", int32(INVOKE_HOST_FUNCTION_TRAPPED): "INVOKE_HOST_FUNCTION_TRAPPED", int32(INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED): "INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED", - int32(INVOKE_HOST_FUNCTION_ENTRY_EXPIRED): "INVOKE_HOST_FUNCTION_ENTRY_EXPIRED", + int32(INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED): "INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED", int32(INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE): "INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE", } var _XdrValues_InvokeHostFunctionResultCode = map[string]int32{ @@ -22571,7 +22517,7 @@ var _XdrValues_InvokeHostFunctionResultCode = map[string]int32{ "INVOKE_HOST_FUNCTION_MALFORMED": int32(INVOKE_HOST_FUNCTION_MALFORMED), "INVOKE_HOST_FUNCTION_TRAPPED": int32(INVOKE_HOST_FUNCTION_TRAPPED), "INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED": int32(INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED), - "INVOKE_HOST_FUNCTION_ENTRY_EXPIRED": int32(INVOKE_HOST_FUNCTION_ENTRY_EXPIRED), + "INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED": int32(INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED), "INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE": int32(INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE), } @@ -22627,7 +22573,7 @@ var _XdrTags_InvokeHostFunctionResult = map[int32]bool{ XdrToI32(INVOKE_HOST_FUNCTION_MALFORMED): true, XdrToI32(INVOKE_HOST_FUNCTION_TRAPPED): true, XdrToI32(INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED): true, - XdrToI32(INVOKE_HOST_FUNCTION_ENTRY_EXPIRED): true, + XdrToI32(INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED): true, XdrToI32(INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE): true, } @@ -22653,7 +22599,7 @@ func (u *InvokeHostFunctionResult) Success() *Hash { } func (u InvokeHostFunctionResult) XdrValid() bool { switch u.Code { - case INVOKE_HOST_FUNCTION_SUCCESS, INVOKE_HOST_FUNCTION_MALFORMED, INVOKE_HOST_FUNCTION_TRAPPED, INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED, INVOKE_HOST_FUNCTION_ENTRY_EXPIRED, INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE: + case INVOKE_HOST_FUNCTION_SUCCESS, INVOKE_HOST_FUNCTION_MALFORMED, INVOKE_HOST_FUNCTION_TRAPPED, INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED, INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED, INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE: return true } return false @@ -22668,7 +22614,7 @@ func (u *InvokeHostFunctionResult) XdrUnionBody() XdrType { switch u.Code { case INVOKE_HOST_FUNCTION_SUCCESS: return XDR_Hash(u.Success()) - case INVOKE_HOST_FUNCTION_MALFORMED, INVOKE_HOST_FUNCTION_TRAPPED, INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED, INVOKE_HOST_FUNCTION_ENTRY_EXPIRED, INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE: + case INVOKE_HOST_FUNCTION_MALFORMED, INVOKE_HOST_FUNCTION_TRAPPED, INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED, INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED, INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE: return nil } return nil @@ -22677,7 +22623,7 @@ func (u *InvokeHostFunctionResult) XdrUnionBodyName() string { switch u.Code { case INVOKE_HOST_FUNCTION_SUCCESS: return "Success" - case INVOKE_HOST_FUNCTION_MALFORMED, INVOKE_HOST_FUNCTION_TRAPPED, INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED, INVOKE_HOST_FUNCTION_ENTRY_EXPIRED, INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE: + case INVOKE_HOST_FUNCTION_MALFORMED, INVOKE_HOST_FUNCTION_TRAPPED, INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED, INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED, INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE: return "" } return "" @@ -22698,141 +22644,135 @@ func (u *InvokeHostFunctionResult) XdrRecurse(x XDR, name string) { case INVOKE_HOST_FUNCTION_SUCCESS: x.Marshal(x.Sprintf("%ssuccess", name), XDR_Hash(u.Success())) return - case INVOKE_HOST_FUNCTION_MALFORMED, INVOKE_HOST_FUNCTION_TRAPPED, INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED, INVOKE_HOST_FUNCTION_ENTRY_EXPIRED, INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE: + case INVOKE_HOST_FUNCTION_MALFORMED, INVOKE_HOST_FUNCTION_TRAPPED, INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED, INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED, INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE: return } XdrPanic("invalid Code (%v) in InvokeHostFunctionResult", u.Code) } func XDR_InvokeHostFunctionResult(v *InvokeHostFunctionResult) *InvokeHostFunctionResult { return v } -var _XdrNames_BumpFootprintExpirationResultCode = map[int32]string{ - int32(BUMP_FOOTPRINT_EXPIRATION_SUCCESS): "BUMP_FOOTPRINT_EXPIRATION_SUCCESS", - int32(BUMP_FOOTPRINT_EXPIRATION_MALFORMED): "BUMP_FOOTPRINT_EXPIRATION_MALFORMED", - int32(BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED): "BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED", - int32(BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE): "BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE", +var _XdrNames_ExtendFootprintTTLResultCode = map[int32]string{ + int32(EXTEND_FOOTPRINT_TTL_SUCCESS): "EXTEND_FOOTPRINT_TTL_SUCCESS", + int32(EXTEND_FOOTPRINT_TTL_MALFORMED): "EXTEND_FOOTPRINT_TTL_MALFORMED", + int32(EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED): "EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED", + int32(EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE): "EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE", } -var _XdrValues_BumpFootprintExpirationResultCode = map[string]int32{ - "BUMP_FOOTPRINT_EXPIRATION_SUCCESS": int32(BUMP_FOOTPRINT_EXPIRATION_SUCCESS), - "BUMP_FOOTPRINT_EXPIRATION_MALFORMED": int32(BUMP_FOOTPRINT_EXPIRATION_MALFORMED), - "BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED": int32(BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED), - "BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE": int32(BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE), +var _XdrValues_ExtendFootprintTTLResultCode = map[string]int32{ + "EXTEND_FOOTPRINT_TTL_SUCCESS": int32(EXTEND_FOOTPRINT_TTL_SUCCESS), + "EXTEND_FOOTPRINT_TTL_MALFORMED": int32(EXTEND_FOOTPRINT_TTL_MALFORMED), + "EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED": int32(EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED), + "EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE": int32(EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE), } -func (BumpFootprintExpirationResultCode) XdrEnumNames() map[int32]string { - return _XdrNames_BumpFootprintExpirationResultCode +func (ExtendFootprintTTLResultCode) XdrEnumNames() map[int32]string { + return _XdrNames_ExtendFootprintTTLResultCode } -func (v BumpFootprintExpirationResultCode) String() string { - if s, ok := _XdrNames_BumpFootprintExpirationResultCode[int32(v)]; ok { +func (v ExtendFootprintTTLResultCode) String() string { + if s, ok := _XdrNames_ExtendFootprintTTLResultCode[int32(v)]; ok { return s } - return fmt.Sprintf("BumpFootprintExpirationResultCode#%d", v) + return fmt.Sprintf("ExtendFootprintTTLResultCode#%d", v) } -func (v *BumpFootprintExpirationResultCode) Scan(ss fmt.ScanState, _ rune) error { +func (v *ExtendFootprintTTLResultCode) Scan(ss fmt.ScanState, _ rune) error { if tok, err := ss.Token(true, XdrSymChar); err != nil { return err } else { stok := string(tok) - if val, ok := _XdrValues_BumpFootprintExpirationResultCode[stok]; ok { - *v = BumpFootprintExpirationResultCode(val) + if val, ok := _XdrValues_ExtendFootprintTTLResultCode[stok]; ok { + *v = ExtendFootprintTTLResultCode(val) return nil - } else if stok == "BumpFootprintExpirationResultCode" { + } else if stok == "ExtendFootprintTTLResultCode" { if n, err := fmt.Fscanf(ss, "#%d", (*int32)(v)); n == 1 && err == nil { return nil } } - return XdrError(fmt.Sprintf("%s is not a valid BumpFootprintExpirationResultCode.", stok)) + return XdrError(fmt.Sprintf("%s is not a valid ExtendFootprintTTLResultCode.", stok)) } } -func (v BumpFootprintExpirationResultCode) GetU32() uint32 { return uint32(v) } -func (v *BumpFootprintExpirationResultCode) SetU32(n uint32) { - *v = BumpFootprintExpirationResultCode(n) -} -func (v *BumpFootprintExpirationResultCode) XdrPointer() interface{} { return v } -func (BumpFootprintExpirationResultCode) XdrTypeName() string { - return "BumpFootprintExpirationResultCode" -} -func (v BumpFootprintExpirationResultCode) XdrValue() interface{} { return v } -func (v *BumpFootprintExpirationResultCode) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v ExtendFootprintTTLResultCode) GetU32() uint32 { return uint32(v) } +func (v *ExtendFootprintTTLResultCode) SetU32(n uint32) { *v = ExtendFootprintTTLResultCode(n) } +func (v *ExtendFootprintTTLResultCode) XdrPointer() interface{} { return v } +func (ExtendFootprintTTLResultCode) XdrTypeName() string { return "ExtendFootprintTTLResultCode" } +func (v ExtendFootprintTTLResultCode) XdrValue() interface{} { return v } +func (v *ExtendFootprintTTLResultCode) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -type XdrType_BumpFootprintExpirationResultCode = *BumpFootprintExpirationResultCode +type XdrType_ExtendFootprintTTLResultCode = *ExtendFootprintTTLResultCode -func XDR_BumpFootprintExpirationResultCode(v *BumpFootprintExpirationResultCode) *BumpFootprintExpirationResultCode { +func XDR_ExtendFootprintTTLResultCode(v *ExtendFootprintTTLResultCode) *ExtendFootprintTTLResultCode { return v } -var _XdrComments_BumpFootprintExpirationResultCode = map[int32]string{ - int32(BUMP_FOOTPRINT_EXPIRATION_SUCCESS): "codes considered as \"success\" for the operation", - int32(BUMP_FOOTPRINT_EXPIRATION_MALFORMED): "codes considered as \"failure\" for the operation", +var _XdrComments_ExtendFootprintTTLResultCode = map[int32]string{ + int32(EXTEND_FOOTPRINT_TTL_SUCCESS): "codes considered as \"success\" for the operation", + int32(EXTEND_FOOTPRINT_TTL_MALFORMED): "codes considered as \"failure\" for the operation", } -func (e BumpFootprintExpirationResultCode) XdrEnumComments() map[int32]string { - return _XdrComments_BumpFootprintExpirationResultCode +func (e ExtendFootprintTTLResultCode) XdrEnumComments() map[int32]string { + return _XdrComments_ExtendFootprintTTLResultCode } -var _XdrTags_BumpFootprintExpirationResult = map[int32]bool{ - XdrToI32(BUMP_FOOTPRINT_EXPIRATION_SUCCESS): true, - XdrToI32(BUMP_FOOTPRINT_EXPIRATION_MALFORMED): true, - XdrToI32(BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED): true, - XdrToI32(BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE): true, +var _XdrTags_ExtendFootprintTTLResult = map[int32]bool{ + XdrToI32(EXTEND_FOOTPRINT_TTL_SUCCESS): true, + XdrToI32(EXTEND_FOOTPRINT_TTL_MALFORMED): true, + XdrToI32(EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED): true, + XdrToI32(EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE): true, } -func (_ BumpFootprintExpirationResult) XdrValidTags() map[int32]bool { - return _XdrTags_BumpFootprintExpirationResult +func (_ ExtendFootprintTTLResult) XdrValidTags() map[int32]bool { + return _XdrTags_ExtendFootprintTTLResult } -func (u BumpFootprintExpirationResult) XdrValid() bool { +func (u ExtendFootprintTTLResult) XdrValid() bool { switch u.Code { - case BUMP_FOOTPRINT_EXPIRATION_SUCCESS, BUMP_FOOTPRINT_EXPIRATION_MALFORMED, BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED, BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE: + case EXTEND_FOOTPRINT_TTL_SUCCESS, EXTEND_FOOTPRINT_TTL_MALFORMED, EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED, EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE: return true } return false } -func (u *BumpFootprintExpirationResult) XdrUnionTag() XdrNum32 { - return XDR_BumpFootprintExpirationResultCode(&u.Code) +func (u *ExtendFootprintTTLResult) XdrUnionTag() XdrNum32 { + return XDR_ExtendFootprintTTLResultCode(&u.Code) } -func (u *BumpFootprintExpirationResult) XdrUnionTagName() string { +func (u *ExtendFootprintTTLResult) XdrUnionTagName() string { return "Code" } -func (u *BumpFootprintExpirationResult) XdrUnionBody() XdrType { +func (u *ExtendFootprintTTLResult) XdrUnionBody() XdrType { switch u.Code { - case BUMP_FOOTPRINT_EXPIRATION_SUCCESS: + case EXTEND_FOOTPRINT_TTL_SUCCESS: return nil - case BUMP_FOOTPRINT_EXPIRATION_MALFORMED, BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED, BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE: + case EXTEND_FOOTPRINT_TTL_MALFORMED, EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED, EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE: return nil } return nil } -func (u *BumpFootprintExpirationResult) XdrUnionBodyName() string { +func (u *ExtendFootprintTTLResult) XdrUnionBodyName() string { switch u.Code { - case BUMP_FOOTPRINT_EXPIRATION_SUCCESS: + case EXTEND_FOOTPRINT_TTL_SUCCESS: return "" - case BUMP_FOOTPRINT_EXPIRATION_MALFORMED, BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED, BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE: + case EXTEND_FOOTPRINT_TTL_MALFORMED, EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED, EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE: return "" } return "" } -type XdrType_BumpFootprintExpirationResult = *BumpFootprintExpirationResult +type XdrType_ExtendFootprintTTLResult = *ExtendFootprintTTLResult -func (v *BumpFootprintExpirationResult) XdrPointer() interface{} { return v } -func (BumpFootprintExpirationResult) XdrTypeName() string { return "BumpFootprintExpirationResult" } -func (v BumpFootprintExpirationResult) XdrValue() interface{} { return v } -func (v *BumpFootprintExpirationResult) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -func (u *BumpFootprintExpirationResult) XdrRecurse(x XDR, name string) { +func (v *ExtendFootprintTTLResult) XdrPointer() interface{} { return v } +func (ExtendFootprintTTLResult) XdrTypeName() string { return "ExtendFootprintTTLResult" } +func (v ExtendFootprintTTLResult) XdrValue() interface{} { return v } +func (v *ExtendFootprintTTLResult) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (u *ExtendFootprintTTLResult) XdrRecurse(x XDR, name string) { if name != "" { name = x.Sprintf("%s.", name) } - XDR_BumpFootprintExpirationResultCode(&u.Code).XdrMarshal(x, x.Sprintf("%scode", name)) + XDR_ExtendFootprintTTLResultCode(&u.Code).XdrMarshal(x, x.Sprintf("%scode", name)) switch u.Code { - case BUMP_FOOTPRINT_EXPIRATION_SUCCESS: + case EXTEND_FOOTPRINT_TTL_SUCCESS: return - case BUMP_FOOTPRINT_EXPIRATION_MALFORMED, BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED, BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE: + case EXTEND_FOOTPRINT_TTL_MALFORMED, EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED, EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE: return } - XdrPanic("invalid Code (%v) in BumpFootprintExpirationResult", u.Code) -} -func XDR_BumpFootprintExpirationResult(v *BumpFootprintExpirationResult) *BumpFootprintExpirationResult { - return v + XdrPanic("invalid Code (%v) in ExtendFootprintTTLResult", u.Code) } +func XDR_ExtendFootprintTTLResult(v *ExtendFootprintTTLResult) *ExtendFootprintTTLResult { return v } var _XdrNames_RestoreFootprintResultCode = map[int32]string{ int32(RESTORE_FOOTPRINT_SUCCESS): "RESTORE_FOOTPRINT_SUCCESS", @@ -23052,7 +22992,7 @@ var _XdrTags_XdrAnon_OperationResult_Tr = map[int32]bool{ XdrToI32(LIQUIDITY_POOL_DEPOSIT): true, XdrToI32(LIQUIDITY_POOL_WITHDRAW): true, XdrToI32(INVOKE_HOST_FUNCTION): true, - XdrToI32(BUMP_FOOTPRINT_EXPIRATION): true, + XdrToI32(EXTEND_FOOTPRINT_TTL): true, XdrToI32(RESTORE_FOOTPRINT): true, } @@ -23434,18 +23374,18 @@ func (u *XdrAnon_OperationResult_Tr) InvokeHostFunctionResult() *InvokeHostFunct return nil } } -func (u *XdrAnon_OperationResult_Tr) BumpFootprintExpirationResult() *BumpFootprintExpirationResult { +func (u *XdrAnon_OperationResult_Tr) ExtendFootprintTTLResult() *ExtendFootprintTTLResult { switch u.Type { - case BUMP_FOOTPRINT_EXPIRATION: - if v, ok := u._u.(*BumpFootprintExpirationResult); ok { + case EXTEND_FOOTPRINT_TTL: + if v, ok := u._u.(*ExtendFootprintTTLResult); ok { return v } else { - var zero BumpFootprintExpirationResult + var zero ExtendFootprintTTLResult u._u = &zero return &zero } default: - XdrPanic("XdrAnon_OperationResult_Tr.BumpFootprintExpirationResult accessed when Type == %v", u.Type) + XdrPanic("XdrAnon_OperationResult_Tr.ExtendFootprintTTLResult accessed when Type == %v", u.Type) return nil } } @@ -23466,7 +23406,7 @@ func (u *XdrAnon_OperationResult_Tr) RestoreFootprintResult() *RestoreFootprintR } func (u XdrAnon_OperationResult_Tr) XdrValid() bool { switch u.Type { - case CREATE_ACCOUNT, PAYMENT, PATH_PAYMENT_STRICT_RECEIVE, MANAGE_SELL_OFFER, CREATE_PASSIVE_SELL_OFFER, SET_OPTIONS, CHANGE_TRUST, ALLOW_TRUST, ACCOUNT_MERGE, INFLATION, MANAGE_DATA, BUMP_SEQUENCE, MANAGE_BUY_OFFER, PATH_PAYMENT_STRICT_SEND, CREATE_CLAIMABLE_BALANCE, CLAIM_CLAIMABLE_BALANCE, BEGIN_SPONSORING_FUTURE_RESERVES, END_SPONSORING_FUTURE_RESERVES, REVOKE_SPONSORSHIP, CLAWBACK, CLAWBACK_CLAIMABLE_BALANCE, SET_TRUST_LINE_FLAGS, LIQUIDITY_POOL_DEPOSIT, LIQUIDITY_POOL_WITHDRAW, INVOKE_HOST_FUNCTION, BUMP_FOOTPRINT_EXPIRATION, RESTORE_FOOTPRINT: + case CREATE_ACCOUNT, PAYMENT, PATH_PAYMENT_STRICT_RECEIVE, MANAGE_SELL_OFFER, CREATE_PASSIVE_SELL_OFFER, SET_OPTIONS, CHANGE_TRUST, ALLOW_TRUST, ACCOUNT_MERGE, INFLATION, MANAGE_DATA, BUMP_SEQUENCE, MANAGE_BUY_OFFER, PATH_PAYMENT_STRICT_SEND, CREATE_CLAIMABLE_BALANCE, CLAIM_CLAIMABLE_BALANCE, BEGIN_SPONSORING_FUTURE_RESERVES, END_SPONSORING_FUTURE_RESERVES, REVOKE_SPONSORSHIP, CLAWBACK, CLAWBACK_CLAIMABLE_BALANCE, SET_TRUST_LINE_FLAGS, LIQUIDITY_POOL_DEPOSIT, LIQUIDITY_POOL_WITHDRAW, INVOKE_HOST_FUNCTION, EXTEND_FOOTPRINT_TTL, RESTORE_FOOTPRINT: return true } return false @@ -23529,8 +23469,8 @@ func (u *XdrAnon_OperationResult_Tr) XdrUnionBody() XdrType { return XDR_LiquidityPoolWithdrawResult(u.LiquidityPoolWithdrawResult()) case INVOKE_HOST_FUNCTION: return XDR_InvokeHostFunctionResult(u.InvokeHostFunctionResult()) - case BUMP_FOOTPRINT_EXPIRATION: - return XDR_BumpFootprintExpirationResult(u.BumpFootprintExpirationResult()) + case EXTEND_FOOTPRINT_TTL: + return XDR_ExtendFootprintTTLResult(u.ExtendFootprintTTLResult()) case RESTORE_FOOTPRINT: return XDR_RestoreFootprintResult(u.RestoreFootprintResult()) } @@ -23588,8 +23528,8 @@ func (u *XdrAnon_OperationResult_Tr) XdrUnionBodyName() string { return "LiquidityPoolWithdrawResult" case INVOKE_HOST_FUNCTION: return "InvokeHostFunctionResult" - case BUMP_FOOTPRINT_EXPIRATION: - return "BumpFootprintExpirationResult" + case EXTEND_FOOTPRINT_TTL: + return "ExtendFootprintTTLResult" case RESTORE_FOOTPRINT: return "RestoreFootprintResult" } @@ -23683,8 +23623,8 @@ func (u *XdrAnon_OperationResult_Tr) XdrRecurse(x XDR, name string) { case INVOKE_HOST_FUNCTION: x.Marshal(x.Sprintf("%sinvokeHostFunctionResult", name), XDR_InvokeHostFunctionResult(u.InvokeHostFunctionResult())) return - case BUMP_FOOTPRINT_EXPIRATION: - x.Marshal(x.Sprintf("%sbumpFootprintExpirationResult", name), XDR_BumpFootprintExpirationResult(u.BumpFootprintExpirationResult())) + case EXTEND_FOOTPRINT_TTL: + x.Marshal(x.Sprintf("%sextendFootprintTTLResult", name), XDR_ExtendFootprintTTLResult(u.ExtendFootprintTTLResult())) return case RESTORE_FOOTPRINT: x.Marshal(x.Sprintf("%srestoreFootprintResult", name), XDR_RestoreFootprintResult(u.RestoreFootprintResult())) @@ -26988,12 +26928,12 @@ func (v *Int256Parts) XdrRecurse(x XDR, name string) { func XDR_Int256Parts(v *Int256Parts) *Int256Parts { return v } var _XdrNames_ContractExecutableType = map[int32]string{ - int32(CONTRACT_EXECUTABLE_WASM): "CONTRACT_EXECUTABLE_WASM", - int32(CONTRACT_EXECUTABLE_TOKEN): "CONTRACT_EXECUTABLE_TOKEN", + int32(CONTRACT_EXECUTABLE_WASM): "CONTRACT_EXECUTABLE_WASM", + int32(CONTRACT_EXECUTABLE_STELLAR_ASSET): "CONTRACT_EXECUTABLE_STELLAR_ASSET", } var _XdrValues_ContractExecutableType = map[string]int32{ - "CONTRACT_EXECUTABLE_WASM": int32(CONTRACT_EXECUTABLE_WASM), - "CONTRACT_EXECUTABLE_TOKEN": int32(CONTRACT_EXECUTABLE_TOKEN), + "CONTRACT_EXECUTABLE_WASM": int32(CONTRACT_EXECUTABLE_WASM), + "CONTRACT_EXECUTABLE_STELLAR_ASSET": int32(CONTRACT_EXECUTABLE_STELLAR_ASSET), } func (ContractExecutableType) XdrEnumNames() map[int32]string { @@ -27033,8 +26973,8 @@ type XdrType_ContractExecutableType = *ContractExecutableType func XDR_ContractExecutableType(v *ContractExecutableType) *ContractExecutableType { return v } var _XdrTags_ContractExecutable = map[int32]bool{ - XdrToI32(CONTRACT_EXECUTABLE_WASM): true, - XdrToI32(CONTRACT_EXECUTABLE_TOKEN): true, + XdrToI32(CONTRACT_EXECUTABLE_WASM): true, + XdrToI32(CONTRACT_EXECUTABLE_STELLAR_ASSET): true, } func (_ ContractExecutable) XdrValidTags() map[int32]bool { @@ -27057,7 +26997,7 @@ func (u *ContractExecutable) Wasm_hash() *Hash { } func (u ContractExecutable) XdrValid() bool { switch u.Type { - case CONTRACT_EXECUTABLE_WASM, CONTRACT_EXECUTABLE_TOKEN: + case CONTRACT_EXECUTABLE_WASM, CONTRACT_EXECUTABLE_STELLAR_ASSET: return true } return false @@ -27072,7 +27012,7 @@ func (u *ContractExecutable) XdrUnionBody() XdrType { switch u.Type { case CONTRACT_EXECUTABLE_WASM: return XDR_Hash(u.Wasm_hash()) - case CONTRACT_EXECUTABLE_TOKEN: + case CONTRACT_EXECUTABLE_STELLAR_ASSET: return nil } return nil @@ -27081,7 +27021,7 @@ func (u *ContractExecutable) XdrUnionBodyName() string { switch u.Type { case CONTRACT_EXECUTABLE_WASM: return "Wasm_hash" - case CONTRACT_EXECUTABLE_TOKEN: + case CONTRACT_EXECUTABLE_STELLAR_ASSET: return "" } return "" @@ -27102,7 +27042,7 @@ func (u *ContractExecutable) XdrRecurse(x XDR, name string) { case CONTRACT_EXECUTABLE_WASM: x.Marshal(x.Sprintf("%swasm_hash", name), XDR_Hash(u.Wasm_hash())) return - case CONTRACT_EXECUTABLE_TOKEN: + case CONTRACT_EXECUTABLE_STELLAR_ASSET: return } XdrPanic("invalid Type (%v) in ContractExecutable", u.Type) @@ -28158,6 +28098,22 @@ func (u *StoredTransactionSet) XdrRecurse(x XDR, name string) { } func XDR_StoredTransactionSet(v *StoredTransactionSet) *StoredTransactionSet { return v } +type XdrType_StoredDebugTransactionSet = *StoredDebugTransactionSet + +func (v *StoredDebugTransactionSet) XdrPointer() interface{} { return v } +func (StoredDebugTransactionSet) XdrTypeName() string { return "StoredDebugTransactionSet" } +func (v StoredDebugTransactionSet) XdrValue() interface{} { return v } +func (v *StoredDebugTransactionSet) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *StoredDebugTransactionSet) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%stxSet", name), XDR_StoredTransactionSet(&v.TxSet)) + x.Marshal(x.Sprintf("%sledgerSeq", name), XDR_Uint32(&v.LedgerSeq)) + x.Marshal(x.Sprintf("%sscpValue", name), XDR_StellarValue(&v.ScpValue)) +} +func XDR_StoredDebugTransactionSet(v *StoredDebugTransactionSet) *StoredDebugTransactionSet { return v } + type _XdrVec_unbounded_StoredTransactionSet []StoredTransactionSet func (_XdrVec_unbounded_StoredTransactionSet) XdrBound() uint32 { @@ -28469,26 +28425,20 @@ func XDR_ConfigSettingContractBandwidthV0(v *ConfigSettingContractBandwidthV0) * var _XdrNames_ContractCostType = map[int32]string{ int32(WasmInsnExec): "WasmInsnExec", - int32(WasmMemAlloc): "WasmMemAlloc", - int32(HostMemAlloc): "HostMemAlloc", - int32(HostMemCpy): "HostMemCpy", - int32(HostMemCmp): "HostMemCmp", + int32(MemAlloc): "MemAlloc", + int32(MemCpy): "MemCpy", + int32(MemCmp): "MemCmp", int32(DispatchHostFunction): "DispatchHostFunction", int32(VisitObject): "VisitObject", int32(ValSer): "ValSer", int32(ValDeser): "ValDeser", int32(ComputeSha256Hash): "ComputeSha256Hash", int32(ComputeEd25519PubKey): "ComputeEd25519PubKey", - int32(MapEntry): "MapEntry", - int32(VecEntry): "VecEntry", int32(VerifyEd25519Sig): "VerifyEd25519Sig", - int32(VmMemRead): "VmMemRead", - int32(VmMemWrite): "VmMemWrite", int32(VmInstantiation): "VmInstantiation", int32(VmCachedInstantiation): "VmCachedInstantiation", int32(InvokeVmFunction): "InvokeVmFunction", int32(ComputeKeccak256Hash): "ComputeKeccak256Hash", - int32(ComputeEcdsaSecp256k1Key): "ComputeEcdsaSecp256k1Key", int32(ComputeEcdsaSecp256k1Sig): "ComputeEcdsaSecp256k1Sig", int32(RecoverEcdsaSecp256k1Key): "RecoverEcdsaSecp256k1Key", int32(Int256AddSub): "Int256AddSub", @@ -28496,29 +28446,24 @@ var _XdrNames_ContractCostType = map[int32]string{ int32(Int256Div): "Int256Div", int32(Int256Pow): "Int256Pow", int32(Int256Shift): "Int256Shift", + int32(ChaCha20DrawBytes): "ChaCha20DrawBytes", } var _XdrValues_ContractCostType = map[string]int32{ "WasmInsnExec": int32(WasmInsnExec), - "WasmMemAlloc": int32(WasmMemAlloc), - "HostMemAlloc": int32(HostMemAlloc), - "HostMemCpy": int32(HostMemCpy), - "HostMemCmp": int32(HostMemCmp), + "MemAlloc": int32(MemAlloc), + "MemCpy": int32(MemCpy), + "MemCmp": int32(MemCmp), "DispatchHostFunction": int32(DispatchHostFunction), "VisitObject": int32(VisitObject), "ValSer": int32(ValSer), "ValDeser": int32(ValDeser), "ComputeSha256Hash": int32(ComputeSha256Hash), "ComputeEd25519PubKey": int32(ComputeEd25519PubKey), - "MapEntry": int32(MapEntry), - "VecEntry": int32(VecEntry), "VerifyEd25519Sig": int32(VerifyEd25519Sig), - "VmMemRead": int32(VmMemRead), - "VmMemWrite": int32(VmMemWrite), "VmInstantiation": int32(VmInstantiation), "VmCachedInstantiation": int32(VmCachedInstantiation), "InvokeVmFunction": int32(InvokeVmFunction), "ComputeKeccak256Hash": int32(ComputeKeccak256Hash), - "ComputeEcdsaSecp256k1Key": int32(ComputeEcdsaSecp256k1Key), "ComputeEcdsaSecp256k1Sig": int32(ComputeEcdsaSecp256k1Sig), "RecoverEcdsaSecp256k1Key": int32(RecoverEcdsaSecp256k1Key), "Int256AddSub": int32(Int256AddSub), @@ -28526,6 +28471,7 @@ var _XdrValues_ContractCostType = map[string]int32{ "Int256Div": int32(Int256Div), "Int256Pow": int32(Int256Pow), "Int256Shift": int32(Int256Shift), + "ChaCha20DrawBytes": int32(ChaCha20DrawBytes), } func (ContractCostType) XdrEnumNames() map[int32]string { @@ -28566,26 +28512,20 @@ func XDR_ContractCostType(v *ContractCostType) *ContractCostType { return v } var _XdrComments_ContractCostType = map[int32]string{ int32(WasmInsnExec): "Cost of running 1 wasm instruction", - int32(WasmMemAlloc): "Cost of growing wasm linear memory by 1 page", - int32(HostMemAlloc): "Cost of allocating a chuck of host memory (in bytes)", - int32(HostMemCpy): "Cost of copying a chuck of bytes into a pre-allocated host memory", - int32(HostMemCmp): "Cost of comparing two slices of host memory", + int32(MemAlloc): "Cost of allocating a slice of memory (in bytes)", + int32(MemCpy): "Cost of copying a slice of bytes into a pre-allocated memory", + int32(MemCmp): "Cost of comparing two slices of memory", int32(DispatchHostFunction): "Cost of a host function dispatch, not including the actual work done by the function nor the cost of VM invocation machinary", int32(VisitObject): "Cost of visiting a host object from the host object storage. Exists to make sure some baseline cost coverage, i.e. repeatly visiting objects by the guest will always incur some charges.", int32(ValSer): "Cost of serializing an xdr object to bytes", int32(ValDeser): "Cost of deserializing an xdr object from bytes", int32(ComputeSha256Hash): "Cost of computing the sha256 hash from bytes", int32(ComputeEd25519PubKey): "Cost of computing the ed25519 pubkey from bytes", - int32(MapEntry): "Cost of accessing an entry in a Map.", - int32(VecEntry): "Cost of accessing an entry in a Vec", int32(VerifyEd25519Sig): "Cost of verifying ed25519 signature of a payload.", - int32(VmMemRead): "Cost of reading a slice of vm linear memory", - int32(VmMemWrite): "Cost of writing to a slice of vm linear memory", int32(VmInstantiation): "Cost of instantiation a VM from wasm bytes code.", int32(VmCachedInstantiation): "Cost of instantiation a VM from a cached state.", int32(InvokeVmFunction): "Cost of invoking a function on the VM. If the function is a host function, additional cost will be covered by `DispatchHostFunction`.", int32(ComputeKeccak256Hash): "Cost of computing a keccak256 hash from bytes.", - int32(ComputeEcdsaSecp256k1Key): "Cost of computing an ECDSA secp256k1 pubkey from bytes.", int32(ComputeEcdsaSecp256k1Sig): "Cost of computing an ECDSA secp256k1 signature from bytes.", int32(RecoverEcdsaSecp256k1Key): "Cost of recovering an ECDSA secp256k1 key from a signature.", int32(Int256AddSub): "Cost of int256 addition (`+`) and subtraction (`-`) operations", @@ -28593,6 +28533,7 @@ var _XdrComments_ContractCostType = map[int32]string{ int32(Int256Div): "Cost of int256 division (`/`) operation", int32(Int256Pow): "Cost of int256 power (`exp`) operation", int32(Int256Shift): "Cost of int256 shift (`shl`, `shr`) operation", + int32(ChaCha20DrawBytes): "Cost of drawing random bytes using a ChaCha20 PRNG", } func (e ContractCostType) XdrEnumComments() map[int32]string { @@ -28615,27 +28556,27 @@ func (v *ContractCostParamEntry) XdrRecurse(x XDR, name string) { } func XDR_ContractCostParamEntry(v *ContractCostParamEntry) *ContractCostParamEntry { return v } -type XdrType_StateExpirationSettings = *StateExpirationSettings +type XdrType_StateArchivalSettings = *StateArchivalSettings -func (v *StateExpirationSettings) XdrPointer() interface{} { return v } -func (StateExpirationSettings) XdrTypeName() string { return "StateExpirationSettings" } -func (v StateExpirationSettings) XdrValue() interface{} { return v } -func (v *StateExpirationSettings) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -func (v *StateExpirationSettings) XdrRecurse(x XDR, name string) { +func (v *StateArchivalSettings) XdrPointer() interface{} { return v } +func (StateArchivalSettings) XdrTypeName() string { return "StateArchivalSettings" } +func (v StateArchivalSettings) XdrValue() interface{} { return v } +func (v *StateArchivalSettings) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *StateArchivalSettings) XdrRecurse(x XDR, name string) { if name != "" { name = x.Sprintf("%s.", name) } - x.Marshal(x.Sprintf("%smaxEntryExpiration", name), XDR_Uint32(&v.MaxEntryExpiration)) - x.Marshal(x.Sprintf("%sminTempEntryExpiration", name), XDR_Uint32(&v.MinTempEntryExpiration)) - x.Marshal(x.Sprintf("%sminPersistentEntryExpiration", name), XDR_Uint32(&v.MinPersistentEntryExpiration)) + x.Marshal(x.Sprintf("%smaxEntryTTL", name), XDR_Uint32(&v.MaxEntryTTL)) + x.Marshal(x.Sprintf("%sminTemporaryTTL", name), XDR_Uint32(&v.MinTemporaryTTL)) + x.Marshal(x.Sprintf("%sminPersistentTTL", name), XDR_Uint32(&v.MinPersistentTTL)) x.Marshal(x.Sprintf("%spersistentRentRateDenominator", name), XDR_Int64(&v.PersistentRentRateDenominator)) x.Marshal(x.Sprintf("%stempRentRateDenominator", name), XDR_Int64(&v.TempRentRateDenominator)) - x.Marshal(x.Sprintf("%smaxEntriesToExpire", name), XDR_Uint32(&v.MaxEntriesToExpire)) + x.Marshal(x.Sprintf("%smaxEntriesToArchive", name), XDR_Uint32(&v.MaxEntriesToArchive)) x.Marshal(x.Sprintf("%sbucketListSizeWindowSampleSize", name), XDR_Uint32(&v.BucketListSizeWindowSampleSize)) x.Marshal(x.Sprintf("%sevictionScanSize", name), XDR_Uint64(&v.EvictionScanSize)) x.Marshal(x.Sprintf("%sstartingEvictionScanLevel", name), XDR_Uint32(&v.StartingEvictionScanLevel)) } -func XDR_StateExpirationSettings(v *StateExpirationSettings) *StateExpirationSettings { return v } +func XDR_StateArchivalSettings(v *StateArchivalSettings) *StateArchivalSettings { return v } type XdrType_EvictionIterator = *EvictionIterator @@ -28735,7 +28676,7 @@ var _XdrNames_ConfigSettingID = map[int32]string{ int32(CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES): "CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES", int32(CONFIG_SETTING_CONTRACT_DATA_KEY_SIZE_BYTES): "CONFIG_SETTING_CONTRACT_DATA_KEY_SIZE_BYTES", int32(CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES): "CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES", - int32(CONFIG_SETTING_STATE_EXPIRATION): "CONFIG_SETTING_STATE_EXPIRATION", + int32(CONFIG_SETTING_STATE_ARCHIVAL): "CONFIG_SETTING_STATE_ARCHIVAL", int32(CONFIG_SETTING_CONTRACT_EXECUTION_LANES): "CONFIG_SETTING_CONTRACT_EXECUTION_LANES", int32(CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW): "CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW", int32(CONFIG_SETTING_EVICTION_ITERATOR): "CONFIG_SETTING_EVICTION_ITERATOR", @@ -28751,7 +28692,7 @@ var _XdrValues_ConfigSettingID = map[string]int32{ "CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES": int32(CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES), "CONFIG_SETTING_CONTRACT_DATA_KEY_SIZE_BYTES": int32(CONFIG_SETTING_CONTRACT_DATA_KEY_SIZE_BYTES), "CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES": int32(CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES), - "CONFIG_SETTING_STATE_EXPIRATION": int32(CONFIG_SETTING_STATE_EXPIRATION), + "CONFIG_SETTING_STATE_ARCHIVAL": int32(CONFIG_SETTING_STATE_ARCHIVAL), "CONFIG_SETTING_CONTRACT_EXECUTION_LANES": int32(CONFIG_SETTING_CONTRACT_EXECUTION_LANES), "CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW": int32(CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW), "CONFIG_SETTING_EVICTION_ITERATOR": int32(CONFIG_SETTING_EVICTION_ITERATOR), @@ -28861,7 +28802,7 @@ var _XdrTags_ConfigSettingEntry = map[int32]bool{ XdrToI32(CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES): true, XdrToI32(CONFIG_SETTING_CONTRACT_DATA_KEY_SIZE_BYTES): true, XdrToI32(CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES): true, - XdrToI32(CONFIG_SETTING_STATE_EXPIRATION): true, + XdrToI32(CONFIG_SETTING_STATE_ARCHIVAL): true, XdrToI32(CONFIG_SETTING_CONTRACT_EXECUTION_LANES): true, XdrToI32(CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW): true, XdrToI32(CONFIG_SETTING_EVICTION_ITERATOR): true, @@ -29020,18 +28961,18 @@ func (u *ConfigSettingEntry) ContractDataEntrySizeBytes() *Uint32 { return nil } } -func (u *ConfigSettingEntry) StateExpirationSettings() *StateExpirationSettings { +func (u *ConfigSettingEntry) StateArchivalSettings() *StateArchivalSettings { switch u.ConfigSettingID { - case CONFIG_SETTING_STATE_EXPIRATION: - if v, ok := u._u.(*StateExpirationSettings); ok { + case CONFIG_SETTING_STATE_ARCHIVAL: + if v, ok := u._u.(*StateArchivalSettings); ok { return v } else { - var zero StateExpirationSettings + var zero StateArchivalSettings u._u = &zero return &zero } default: - XdrPanic("ConfigSettingEntry.StateExpirationSettings accessed when ConfigSettingID == %v", u.ConfigSettingID) + XdrPanic("ConfigSettingEntry.StateArchivalSettings accessed when ConfigSettingID == %v", u.ConfigSettingID) return nil } } @@ -29082,7 +29023,7 @@ func (u *ConfigSettingEntry) EvictionIterator() *EvictionIterator { } func (u ConfigSettingEntry) XdrValid() bool { switch u.ConfigSettingID { - case CONFIG_SETTING_CONTRACT_MAX_SIZE_BYTES, CONFIG_SETTING_CONTRACT_COMPUTE_V0, CONFIG_SETTING_CONTRACT_LEDGER_COST_V0, CONFIG_SETTING_CONTRACT_HISTORICAL_DATA_V0, CONFIG_SETTING_CONTRACT_EVENTS_V0, CONFIG_SETTING_CONTRACT_BANDWIDTH_V0, CONFIG_SETTING_CONTRACT_COST_PARAMS_CPU_INSTRUCTIONS, CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES, CONFIG_SETTING_CONTRACT_DATA_KEY_SIZE_BYTES, CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES, CONFIG_SETTING_STATE_EXPIRATION, CONFIG_SETTING_CONTRACT_EXECUTION_LANES, CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW, CONFIG_SETTING_EVICTION_ITERATOR: + case CONFIG_SETTING_CONTRACT_MAX_SIZE_BYTES, CONFIG_SETTING_CONTRACT_COMPUTE_V0, CONFIG_SETTING_CONTRACT_LEDGER_COST_V0, CONFIG_SETTING_CONTRACT_HISTORICAL_DATA_V0, CONFIG_SETTING_CONTRACT_EVENTS_V0, CONFIG_SETTING_CONTRACT_BANDWIDTH_V0, CONFIG_SETTING_CONTRACT_COST_PARAMS_CPU_INSTRUCTIONS, CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES, CONFIG_SETTING_CONTRACT_DATA_KEY_SIZE_BYTES, CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES, CONFIG_SETTING_STATE_ARCHIVAL, CONFIG_SETTING_CONTRACT_EXECUTION_LANES, CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW, CONFIG_SETTING_EVICTION_ITERATOR: return true } return false @@ -29115,8 +29056,8 @@ func (u *ConfigSettingEntry) XdrUnionBody() XdrType { return XDR_Uint32(u.ContractDataKeySizeBytes()) case CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES: return XDR_Uint32(u.ContractDataEntrySizeBytes()) - case CONFIG_SETTING_STATE_EXPIRATION: - return XDR_StateExpirationSettings(u.StateExpirationSettings()) + case CONFIG_SETTING_STATE_ARCHIVAL: + return XDR_StateArchivalSettings(u.StateArchivalSettings()) case CONFIG_SETTING_CONTRACT_EXECUTION_LANES: return XDR_ConfigSettingContractExecutionLanesV0(u.ContractExecutionLanes()) case CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW: @@ -29148,8 +29089,8 @@ func (u *ConfigSettingEntry) XdrUnionBodyName() string { return "ContractDataKeySizeBytes" case CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES: return "ContractDataEntrySizeBytes" - case CONFIG_SETTING_STATE_EXPIRATION: - return "StateExpirationSettings" + case CONFIG_SETTING_STATE_ARCHIVAL: + return "StateArchivalSettings" case CONFIG_SETTING_CONTRACT_EXECUTION_LANES: return "ContractExecutionLanes" case CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW: @@ -29202,8 +29143,8 @@ func (u *ConfigSettingEntry) XdrRecurse(x XDR, name string) { case CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES: x.Marshal(x.Sprintf("%scontractDataEntrySizeBytes", name), XDR_Uint32(u.ContractDataEntrySizeBytes())) return - case CONFIG_SETTING_STATE_EXPIRATION: - x.Marshal(x.Sprintf("%sstateExpirationSettings", name), XDR_StateExpirationSettings(u.StateExpirationSettings())) + case CONFIG_SETTING_STATE_ARCHIVAL: + x.Marshal(x.Sprintf("%sstateArchivalSettings", name), XDR_StateArchivalSettings(u.StateArchivalSettings())) return case CONFIG_SETTING_CONTRACT_EXECUTION_LANES: x.Marshal(x.Sprintf("%scontractExecutionLanes", name), XDR_ConfigSettingContractExecutionLanesV0(u.ContractExecutionLanes())) diff --git a/ingest/change_test.go b/ingest/change_test.go index d8ae9492dc..03af4f2cb4 100644 --- a/ingest/change_test.go +++ b/ingest/change_test.go @@ -99,24 +99,24 @@ func TestSortChanges(t *testing.T) { Post: nil, }, { - Type: xdr.LedgerEntryTypeExpiration, + Type: xdr.LedgerEntryTypeTtl, Pre: &xdr.LedgerEntry{ LastModifiedLedgerSeq: 11, Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeExpiration, - Expiration: &xdr.ExpirationEntry{ - KeyHash: xdr.Hash{1}, - ExpirationLedgerSeq: 50, + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: xdr.Hash{1}, + LiveUntilLedgerSeq: 50, }, }, }, Post: &xdr.LedgerEntry{ LastModifiedLedgerSeq: 11, Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeExpiration, - Expiration: &xdr.ExpirationEntry{ - KeyHash: xdr.Hash{1}, - ExpirationLedgerSeq: 100, + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: xdr.Hash{1}, + LiveUntilLedgerSeq: 100, }, }, }, @@ -181,24 +181,24 @@ func TestSortChanges(t *testing.T) { }, { - Type: xdr.LedgerEntryTypeExpiration, + Type: xdr.LedgerEntryTypeTtl, Pre: &xdr.LedgerEntry{ LastModifiedLedgerSeq: 11, Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeExpiration, - Expiration: &xdr.ExpirationEntry{ - KeyHash: xdr.Hash{1}, - ExpirationLedgerSeq: 50, + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: xdr.Hash{1}, + LiveUntilLedgerSeq: 50, }, }, }, Post: &xdr.LedgerEntry{ LastModifiedLedgerSeq: 11, Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeExpiration, - Expiration: &xdr.ExpirationEntry{ - KeyHash: xdr.Hash{1}, - ExpirationLedgerSeq: 100, + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: xdr.Hash{1}, + LiveUntilLedgerSeq: 100, }, }, }, diff --git a/ingest/ledger_change_reader_test.go b/ingest/ledger_change_reader_test.go index dea3794cb5..0b077007e5 100644 --- a/ingest/ledger_change_reader_test.go +++ b/ingest/ledger_change_reader_test.go @@ -394,8 +394,8 @@ func TestLedgerChangeLedgerCloseMetaV2(t *testing.T) { ContractId: &contractID, } ledger := xdr.LedgerCloseMeta{ - V: 2, - V2: &xdr.LedgerCloseMetaV2{ + V: 1, + V1: &xdr.LedgerCloseMetaV1{ LedgerHeader: xdr.LedgerHeaderHistoryEntry{Header: xdr.LedgerHeader{LedgerVersion: 10}}, TxSet: xdr.GeneralizedTransactionSet{ V: 1, @@ -605,8 +605,8 @@ func TestLedgerChangeLedgerCloseMetaV2Empty(t *testing.T) { baseFee := xdr.Int64(100) ledger := xdr.LedgerCloseMeta{ - V: 2, - V2: &xdr.LedgerCloseMetaV2{ + V: 1, + V1: &xdr.LedgerCloseMetaV1{ LedgerHeader: xdr.LedgerHeaderHistoryEntry{Header: xdr.LedgerHeader{LedgerVersion: 10}}, TxSet: xdr.GeneralizedTransactionSet{ V: 1, diff --git a/ingest/stats_change_processor.go b/ingest/stats_change_processor.go index 01f33da466..43a82011cb 100644 --- a/ingest/stats_change_processor.go +++ b/ingest/stats_change_processor.go @@ -51,9 +51,9 @@ type StatsChangeProcessorResults struct { ConfigSettingsUpdated int64 ConfigSettingsRemoved int64 - ExpirationCreated int64 - ExpirationUpdated int64 - ExpirationRemoved int64 + TtlCreated int64 + TtlUpdated int64 + TtlRemoved int64 } func (p *StatsChangeProcessor) ProcessChange(ctx context.Context, change Change) error { @@ -139,14 +139,14 @@ func (p *StatsChangeProcessor) ProcessChange(ctx context.Context, change Change) case xdr.LedgerEntryChangeTypeLedgerEntryRemoved: p.results.ConfigSettingsRemoved++ } - case xdr.LedgerEntryTypeExpiration: + case xdr.LedgerEntryTypeTtl: switch change.LedgerEntryChangeType() { case xdr.LedgerEntryChangeTypeLedgerEntryCreated: - p.results.ExpirationCreated++ + p.results.TtlCreated++ case xdr.LedgerEntryChangeTypeLedgerEntryUpdated: - p.results.ExpirationUpdated++ + p.results.TtlUpdated++ case xdr.LedgerEntryChangeTypeLedgerEntryRemoved: - p.results.ExpirationRemoved++ + p.results.TtlRemoved++ } default: return fmt.Errorf("unsupported ledger entry type: %s", change.Type.String()) @@ -197,8 +197,8 @@ func (stats *StatsChangeProcessorResults) Map() map[string]interface{} { "stats_config_settings_updated": stats.ConfigSettingsUpdated, "stats_config_settings_removed": stats.ConfigSettingsRemoved, - "stats_expiration_created": stats.ExpirationCreated, - "stats_expiration_updated": stats.ExpirationUpdated, - "stats_expiration_removed": stats.ExpirationRemoved, + "stats_ttl_created": stats.TtlCreated, + "stats_ttl_updated": stats.TtlUpdated, + "stats_ttl_removed": stats.TtlRemoved, } } diff --git a/ingest/stats_change_processor_test.go b/ingest/stats_change_processor_test.go index fe14af56e3..95cb6f1a85 100644 --- a/ingest/stats_change_processor_test.go +++ b/ingest/stats_change_processor_test.go @@ -45,7 +45,7 @@ func TestStatsChangeProcessor(t *testing.T) { assert.Equal(t, int64(1), results.ContractDataCreated) assert.Equal(t, int64(1), results.ContractCodeCreated) assert.Equal(t, int64(1), results.ConfigSettingsCreated) - assert.Equal(t, int64(1), results.ExpirationCreated) + assert.Equal(t, int64(1), results.TtlCreated) assert.Equal(t, int64(1), results.AccountsUpdated) assert.Equal(t, int64(1), results.ClaimableBalancesUpdated) @@ -56,7 +56,7 @@ func TestStatsChangeProcessor(t *testing.T) { assert.Equal(t, int64(1), results.ContractDataUpdated) assert.Equal(t, int64(1), results.ContractCodeUpdated) assert.Equal(t, int64(1), results.ConfigSettingsUpdated) - assert.Equal(t, int64(1), results.ExpirationUpdated) + assert.Equal(t, int64(1), results.TtlUpdated) assert.Equal(t, int64(1), results.AccountsRemoved) assert.Equal(t, int64(1), results.ClaimableBalancesRemoved) @@ -67,7 +67,7 @@ func TestStatsChangeProcessor(t *testing.T) { assert.Equal(t, int64(1), results.ContractCodeRemoved) assert.Equal(t, int64(1), results.ContractDataRemoved) assert.Equal(t, int64(1), results.ConfigSettingsRemoved) - assert.Equal(t, int64(1), results.ExpirationRemoved) + assert.Equal(t, int64(1), results.TtlRemoved) assert.Equal(t, len(xdr.LedgerEntryTypeMap)*3, len(results.Map())) } diff --git a/protocols/horizon/operations/main.go b/protocols/horizon/operations/main.go index e56bbc3649..3e4af748b8 100644 --- a/protocols/horizon/operations/main.go +++ b/protocols/horizon/operations/main.go @@ -39,7 +39,7 @@ var TypeNames = map[xdr.OperationType]string{ xdr.OperationTypeLiquidityPoolDeposit: "liquidity_pool_deposit", xdr.OperationTypeLiquidityPoolWithdraw: "liquidity_pool_withdraw", xdr.OperationTypeInvokeHostFunction: "invoke_host_function", - xdr.OperationTypeBumpFootprintExpiration: "bump_footprint_expiration", + xdr.OperationTypeExtendFootprintTtl: "extend_footprint_ttl", xdr.OperationTypeRestoreFootprint: "restore_footprint", } @@ -373,11 +373,11 @@ type HostFunctionParameter struct { Type string `json:"type"` } -// BumpFootprintExpiration is the json resource representing a single BumpFootprintExpirationOp. -// The model for BumpFootprintExpiration assimilates BumpFootprintExpirationOp, but is simplified. -type BumpFootprintExpiration struct { +// ExtendFootprintTtl is the json resource representing a single ExtendFootprintTtlOp. +// The model for ExtendFootprintTtl assimilates ExtendFootprintTtlOp, but is simplified. +type ExtendFootprintTtl struct { Base - LedgersToExpire uint32 `json:"ledgers_to_expire"` + ExtendTo uint32 `json:"extend_to"` } // RestoreFootprint is the json resource representing a single RestoreFootprint. @@ -642,8 +642,8 @@ func UnmarshalOperation(operationTypeID int32, dataString []byte) (ops Operation return } ops = op - case xdr.OperationTypeBumpFootprintExpiration: - var op BumpFootprintExpiration + case xdr.OperationTypeExtendFootprintTtl: + var op ExtendFootprintTtl if err = json.Unmarshal(dataString, &op); err != nil { return } diff --git a/services/horizon/internal/codes/main.go b/services/horizon/internal/codes/main.go index d2574f2e22..5af63bed1a 100644 --- a/services/horizon/internal/codes/main.go +++ b/services/horizon/internal/codes/main.go @@ -502,20 +502,20 @@ func String(code interface{}) (string, error) { return "function_trapped", nil case xdr.InvokeHostFunctionResultCodeInvokeHostFunctionResourceLimitExceeded: return "resource_limit_exceeded", nil - case xdr.InvokeHostFunctionResultCodeInvokeHostFunctionEntryExpired: - return "entry_expired", nil + case xdr.InvokeHostFunctionResultCodeInvokeHostFunctionEntryArchived: + return "entry_archived", nil case xdr.InvokeHostFunctionResultCodeInvokeHostFunctionInsufficientRefundableFee: return "insufficient_refundable_fee", nil } - case xdr.BumpFootprintExpirationResultCode: + case xdr.ExtendFootprintTtlResultCode: switch code { - case xdr.BumpFootprintExpirationResultCodeBumpFootprintExpirationSuccess: + case xdr.ExtendFootprintTtlResultCodeExtendFootprintTtlSuccess: return OpSuccess, nil - case xdr.BumpFootprintExpirationResultCodeBumpFootprintExpirationMalformed: + case xdr.ExtendFootprintTtlResultCodeExtendFootprintTtlMalformed: return OpMalformed, nil - case xdr.BumpFootprintExpirationResultCodeBumpFootprintExpirationResourceLimitExceeded: + case xdr.ExtendFootprintTtlResultCodeExtendFootprintTtlResourceLimitExceeded: return "resource_limit_exceeded", nil - case xdr.BumpFootprintExpirationResultCodeBumpFootprintExpirationInsufficientRefundableFee: + case xdr.ExtendFootprintTtlResultCodeExtendFootprintTtlInsufficientRefundableFee: return "insufficient_refundable_fee", nil } case xdr.RestoreFootprintResultCode: @@ -595,8 +595,8 @@ func ForOperationResult(opr xdr.OperationResult) (string, error) { ic = ir.MustLiquidityPoolWithdrawResult().Code case xdr.OperationTypeInvokeHostFunction: ic = ir.MustInvokeHostFunctionResult().Code - case xdr.OperationTypeBumpFootprintExpiration: - ic = ir.MustBumpFootprintExpirationResult().Code + case xdr.OperationTypeExtendFootprintTtl: + ic = ir.MustExtendFootprintTtlResult().Code case xdr.OperationTypeRestoreFootprint: ic = ir.MustRestoreFootprintResult().Code } diff --git a/services/horizon/internal/codes/main_test.go b/services/horizon/internal/codes/main_test.go index 51593b91c3..88c622440a 100644 --- a/services/horizon/internal/codes/main_test.go +++ b/services/horizon/internal/codes/main_test.go @@ -5,8 +5,9 @@ import ( "reflect" "testing" - "github.com/stellar/go/xdr" "github.com/stretchr/testify/assert" + + "github.com/stellar/go/xdr" ) func TestForOperationResultCoversForAllOpTypes(t *testing.T) { @@ -51,7 +52,7 @@ func TestForOperationResultCoversForAllOpTypes(t *testing.T) { xdr.OperationTypeLiquidityPoolDeposit: reflect.TypeOf(xdr.LiquidityPoolDepositResultCode(0)), xdr.OperationTypeLiquidityPoolWithdraw: reflect.TypeOf(xdr.LiquidityPoolWithdrawResultCode(0)), xdr.OperationTypeInvokeHostFunction: reflect.TypeOf(xdr.InvokeHostFunctionResultCode(0)), - xdr.OperationTypeBumpFootprintExpiration: reflect.TypeOf(xdr.BumpFootprintExpirationResultCode(0)), + xdr.OperationTypeExtendFootprintTtl: reflect.TypeOf(xdr.ExtendFootprintTtlResultCode(0)), xdr.OperationTypeRestoreFootprint: reflect.TypeOf(xdr.RestoreFootprintResultCode(0)), } // If this is not equal it means one or more result struct is missing in resultTypes map. diff --git a/services/horizon/internal/ingest/main_test.go b/services/horizon/internal/ingest/main_test.go index 3258db4899..55860eeaff 100644 --- a/services/horizon/internal/ingest/main_test.go +++ b/services/horizon/internal/ingest/main_test.go @@ -131,7 +131,7 @@ func TestStateMachineTransition(t *testing.T) { } historyQ.On("GetTx").Return(nil).Once() - historyQ.On("Begin", mock.AnythingOfType("*context.emptyCtx")).Return(errors.New("my error")).Once() + historyQ.On("Begin", mock.Anything).Return(errors.New("my error")).Once() historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() assert.PanicsWithValue(t, "unexpected transaction", func() { diff --git a/services/horizon/internal/ingest/processors/contract_data.go b/services/horizon/internal/ingest/processors/contract_data.go index 08ac0484d0..dec9199747 100644 --- a/services/horizon/internal/ingest/processors/contract_data.go +++ b/services/horizon/internal/ingest/processors/contract_data.go @@ -437,7 +437,7 @@ func AssetToContractData(isNative bool, code, issuer string, contractID [32]byte Type: xdr.ScValTypeScvContractInstance, Instance: &xdr.ScContractInstance{ Executable: xdr.ContractExecutable{ - Type: xdr.ContractExecutableTypeContractExecutableToken, + Type: xdr.ContractExecutableTypeContractExecutableStellarAsset, }, Storage: storageMap, }, diff --git a/services/horizon/internal/ingest/processors/effects_processor.go b/services/horizon/internal/ingest/processors/effects_processor.go index b8f97836b9..34e9f9169a 100644 --- a/services/horizon/internal/ingest/processors/effects_processor.go +++ b/services/horizon/internal/ingest/processors/effects_processor.go @@ -150,7 +150,7 @@ func (operation *transactionOperationWrapper) ingestEffects(accountLoader *histo // For now, the only effects are related to the events themselves. // Possible add'l work: https://github.com/stellar/go/issues/4585 err = wrapper.addInvokeHostFunctionEffects(filterEvents(diagnosticEvents)) - case xdr.OperationTypeBumpFootprintExpiration, xdr.OperationTypeRestoreFootprint: + case xdr.OperationTypeExtendFootprintTtl, xdr.OperationTypeRestoreFootprint: // do not produce effects for these operations as horizon only provides // limited visibility into soroban operations default: diff --git a/services/horizon/internal/ingest/processors/operations_processor.go b/services/horizon/internal/ingest/processors/operations_processor.go index c8ae1a9585..b9a23229d5 100644 --- a/services/horizon/internal/ingest/processors/operations_processor.go +++ b/services/horizon/internal/ingest/processors/operations_processor.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "github.com/guregu/null" "github.com/stellar/go/amount" @@ -715,9 +716,9 @@ func (operation *transactionOperationWrapper) Details() (map[string]interface{}, default: panic(fmt.Errorf("unknown host function type: %s", op.HostFunction.Type)) } - case xdr.OperationTypeBumpFootprintExpiration: - op := operation.operation.Body.MustBumpFootprintExpirationOp() - details["ledgers_to_expire"] = op.LedgersToExpire + case xdr.OperationTypeExtendFootprintTtl: + op := operation.operation.Body.MustExtendFootprintTtlOp() + details["extend_to"] = op.ExtendTo case xdr.OperationTypeRestoreFootprint: default: panic(fmt.Errorf("unknown operation type: %s", operation.OperationType())) @@ -1013,7 +1014,7 @@ func (operation *transactionOperationWrapper) Participants() ([]xdr.AccountId, e // the only direct participant is the source_account case xdr.OperationTypeInvokeHostFunction: // the only direct participant is the source_account - case xdr.OperationTypeBumpFootprintExpiration: + case xdr.OperationTypeExtendFootprintTtl: // the only direct participant is the source_account case xdr.OperationTypeRestoreFootprint: // the only direct participant is the source_account diff --git a/services/horizon/internal/ingest/processors/operations_processor_test.go b/services/horizon/internal/ingest/processors/operations_processor_test.go index 83ef1636a8..4b5fb376cd 100644 --- a/services/horizon/internal/ingest/processors/operations_processor_test.go +++ b/services/horizon/internal/ingest/processors/operations_processor_test.go @@ -209,7 +209,7 @@ func (s *OperationsProcessorTestSuiteLedger) TestOperationTypeInvokeHostFunction }, }, Executable: xdr.ContractExecutable{ - Type: xdr.ContractExecutableTypeContractExecutableToken, + Type: xdr.ContractExecutableTypeContractExecutableStellarAsset, }, }, }, diff --git a/services/horizon/internal/ingest/processors/stats_ledger_transaction_processor.go b/services/horizon/internal/ingest/processors/stats_ledger_transaction_processor.go index 8fa886ab52..83188e02f8 100644 --- a/services/horizon/internal/ingest/processors/stats_ledger_transaction_processor.go +++ b/services/horizon/internal/ingest/processors/stats_ledger_transaction_processor.go @@ -55,7 +55,7 @@ type StatsLedgerTransactionProcessorResults struct { OperationsLiquidityPoolDeposit int64 OperationsLiquidityPoolWithdraw int64 OperationsInvokeHostFunction int64 - OperationsBumpFootprintExpiration int64 + OperationsExtendFootprintTtl int64 OperationsRestoreFootprint int64 } @@ -129,8 +129,8 @@ func (p *StatsLedgerTransactionProcessor) ProcessTransaction(lcm xdr.LedgerClose p.results.OperationsLiquidityPoolWithdraw++ case xdr.OperationTypeInvokeHostFunction: p.results.OperationsInvokeHostFunction++ - case xdr.OperationTypeBumpFootprintExpiration: - p.results.OperationsBumpFootprintExpiration++ + case xdr.OperationTypeExtendFootprintTtl: + p.results.OperationsExtendFootprintTtl++ case xdr.OperationTypeRestoreFootprint: p.results.OperationsRestoreFootprint++ default: diff --git a/services/horizon/internal/ingest/verify.go b/services/horizon/internal/ingest/verify.go index ddacbb43c0..c7d6c8f0d6 100644 --- a/services/horizon/internal/ingest/verify.go +++ b/services/horizon/internal/ingest/verify.go @@ -242,13 +242,13 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { return errors.Wrap(err, "Error running assetStats.AddContractData") } totalByType["contract_data"]++ - case xdr.LedgerEntryTypeExpiration: - // we don't store expiration entries in the db, + case xdr.LedgerEntryTypeTtl: + // we don't store ttl entries in the db, // so there is nothing to verify in that case. if err = verifier.Write(entry); err != nil { return err } - totalByType["expiration"]++ + totalByType["ttl"]++ default: return errors.New("GetLedgerEntries return unexpected type") } @@ -322,7 +322,7 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { err = verifier.Verify( countAccounts + countData + countOffers + countTrustLines + countClaimableBalances + - countLiquidityPools + int(totalByType["contract_data"]) + int(totalByType["expiration"]), + countLiquidityPools + int(totalByType["contract_data"]) + int(totalByType["ttl"]), ) if err != nil { return errors.Wrap(err, "verifier.Verify failed") diff --git a/services/horizon/internal/ingest/verify_test.go b/services/horizon/internal/ingest/verify_test.go index 1fbb6b8f8c..dbff198de4 100644 --- a/services/horizon/internal/ingest/verify_test.go +++ b/services/horizon/internal/ingest/verify_test.go @@ -174,14 +174,14 @@ func genContractCode(tt *test.T, gen randxdr.Generator) xdr.LedgerEntryChange { return change } -func genExpiration(tt *test.T, gen randxdr.Generator) xdr.LedgerEntryChange { +func genTTL(tt *test.T, gen randxdr.Generator) xdr.LedgerEntryChange { change := xdr.LedgerEntryChange{} shape := &gxdr.LedgerEntryChange{} gen.Next( shape, []randxdr.Preset{ {randxdr.FieldEquals("type"), randxdr.SetU32(gxdr.LEDGER_ENTRY_CREATED.GetU32())}, - {randxdr.FieldEquals("created.data.type"), randxdr.SetU32(gxdr.EXPIRATION.GetU32())}, + {randxdr.FieldEquals("created.data.type"), randxdr.SetU32(gxdr.TTL.GetU32())}, }, ) tt.Assert.NoError(gxdr.Convert(shape, &change)) @@ -346,7 +346,7 @@ func TestStateVerifier(t *testing.T) { genAccountData(tt, gen), genContractCode(tt, gen), genConfigSetting(tt, gen), - genExpiration(tt, gen), + genTTL(tt, gen), ) changes = append(changes, genAssetContractMetadata(tt, gen)...) } diff --git a/services/horizon/internal/integration/contracts/Cargo.lock b/services/horizon/internal/integration/contracts/Cargo.lock index ec405876da..417d3ff74f 100644 --- a/services/horizon/internal/integration/contracts/Cargo.lock +++ b/services/horizon/internal/integration/contracts/Cargo.lock @@ -34,9 +34,9 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" +checksum = "a2e1373abdaa212b704512ec2bd8b26bd0b7d5c3f70117411a5d9a451383c859" dependencies = [ "derive_arbitrary", ] @@ -82,9 +82,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.3" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "base64ct" @@ -103,9 +103,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytes-lit" @@ -136,9 +136,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.28" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ed24df0632f708f5f6d8082675bef2596f7084dee3dd55f632290bf35bfe0f" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", @@ -181,9 +181,9 @@ dependencies = [ [[package]] name = "crypto-bigint" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15" +checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" dependencies = [ "generic-array", "rand_core", @@ -203,9 +203,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f34ba9a9bcb8645379e9de8cb3ecfcf4d1c85ba66d90deb3259206fa5aa193b" +checksum = "37e366bff8cd32dd8754b0991fb66b279dc48f598c3a18914852a6673deef583" dependencies = [ "quote", "syn", @@ -213,9 +213,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f711ade317dd348950a9910f81c5947e3d8907ebd2b83f76203ff1807e6a2bc2" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" dependencies = [ "cfg-if", "cpufeatures", @@ -286,10 +286,11 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" dependencies = [ + "powerfmt", "serde", ] @@ -338,9 +339,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "2.2.2" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", "signature", @@ -368,9 +369,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" -version = "0.13.5" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" dependencies = [ "base16ct", "crypto-bigint", @@ -409,9 +410,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.1.20" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" [[package]] name = "fnv" @@ -468,9 +469,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" [[package]] name = "hex" @@ -532,12 +533,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.1", "serde", ] @@ -596,15 +597,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.147" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "libm" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "log" @@ -614,9 +615,9 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" -version = "2.6.2" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "miniz_oxide" @@ -640,9 +641,9 @@ dependencies = [ [[package]] name = "num-derive" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e" +checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" dependencies = [ "proc-macro2", "quote", @@ -661,18 +662,18 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] [[package]] name = "object" -version = "0.32.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] @@ -705,6 +706,12 @@ version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -713,9 +720,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", "syn", @@ -723,9 +730,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -816,24 +823,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ "proc-macro2", "quote", @@ -842,9 +849,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", @@ -857,11 +864,11 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" dependencies = [ - "base64 0.21.3", + "base64 0.21.4", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.0.0", + "indexmap 2.0.2", "serde", "serde_json", "serde_with_macros", @@ -882,9 +889,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -913,14 +920,14 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "soroban-env-common" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-env?rev=8c63bff68a15d79aca3a705ee6916a68db57b7e6#8c63bff68a15d79aca3a705ee6916a68db57b7e6" +source = "git+https://github.com/stellar/rs-soroban-env?rev=91f44778389490ad863d61a8a90ac9875ba6d8fd#91f44778389490ad863d61a8a90ac9875ba6d8fd" dependencies = [ "arbitrary", "crate-git-revision", @@ -937,7 +944,7 @@ dependencies = [ [[package]] name = "soroban-env-guest" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-env?rev=8c63bff68a15d79aca3a705ee6916a68db57b7e6#8c63bff68a15d79aca3a705ee6916a68db57b7e6" +source = "git+https://github.com/stellar/rs-soroban-env?rev=91f44778389490ad863d61a8a90ac9875ba6d8fd#91f44778389490ad863d61a8a90ac9875ba6d8fd" dependencies = [ "soroban-env-common", "static_assertions", @@ -946,7 +953,7 @@ dependencies = [ [[package]] name = "soroban-env-host" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-env?rev=8c63bff68a15d79aca3a705ee6916a68db57b7e6#8c63bff68a15d79aca3a705ee6916a68db57b7e6" +source = "git+https://github.com/stellar/rs-soroban-env?rev=91f44778389490ad863d61a8a90ac9875ba6d8fd#91f44778389490ad863d61a8a90ac9875ba6d8fd" dependencies = [ "backtrace", "ed25519-dalek", @@ -969,7 +976,7 @@ dependencies = [ [[package]] name = "soroban-env-macros" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-env?rev=8c63bff68a15d79aca3a705ee6916a68db57b7e6#8c63bff68a15d79aca3a705ee6916a68db57b7e6" +source = "git+https://github.com/stellar/rs-soroban-env?rev=91f44778389490ad863d61a8a90ac9875ba6d8fd#91f44778389490ad863d61a8a90ac9875ba6d8fd" dependencies = [ "itertools", "proc-macro2", @@ -990,7 +997,7 @@ dependencies = [ [[package]] name = "soroban-ledger-snapshot" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-sdk?rev=0992413f9b05e5bfb1f872bce99e89d9129b2e61#0992413f9b05e5bfb1f872bce99e89d9129b2e61" +source = "git+https://github.com/stellar/rs-soroban-sdk?rev=729ed3ac5fe8600a3245d5816eadd3c95ab2eb54#729ed3ac5fe8600a3245d5816eadd3c95ab2eb54" dependencies = [ "serde", "serde_json", @@ -1002,7 +1009,7 @@ dependencies = [ [[package]] name = "soroban-native-sdk-macros" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-env?rev=8c63bff68a15d79aca3a705ee6916a68db57b7e6#8c63bff68a15d79aca3a705ee6916a68db57b7e6" +source = "git+https://github.com/stellar/rs-soroban-env?rev=91f44778389490ad863d61a8a90ac9875ba6d8fd#91f44778389490ad863d61a8a90ac9875ba6d8fd" dependencies = [ "itertools", "proc-macro2", @@ -1020,7 +1027,7 @@ dependencies = [ [[package]] name = "soroban-sdk" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-sdk?rev=0992413f9b05e5bfb1f872bce99e89d9129b2e61#0992413f9b05e5bfb1f872bce99e89d9129b2e61" +source = "git+https://github.com/stellar/rs-soroban-sdk?rev=729ed3ac5fe8600a3245d5816eadd3c95ab2eb54#729ed3ac5fe8600a3245d5816eadd3c95ab2eb54" dependencies = [ "arbitrary", "bytes-lit", @@ -1037,7 +1044,7 @@ dependencies = [ [[package]] name = "soroban-sdk-macros" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-sdk?rev=0992413f9b05e5bfb1f872bce99e89d9129b2e61#0992413f9b05e5bfb1f872bce99e89d9129b2e61" +source = "git+https://github.com/stellar/rs-soroban-sdk?rev=729ed3ac5fe8600a3245d5816eadd3c95ab2eb54#729ed3ac5fe8600a3245d5816eadd3c95ab2eb54" dependencies = [ "crate-git-revision", "darling", @@ -1056,7 +1063,7 @@ dependencies = [ [[package]] name = "soroban-spec" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-sdk?rev=0992413f9b05e5bfb1f872bce99e89d9129b2e61#0992413f9b05e5bfb1f872bce99e89d9129b2e61" +source = "git+https://github.com/stellar/rs-soroban-sdk?rev=729ed3ac5fe8600a3245d5816eadd3c95ab2eb54#729ed3ac5fe8600a3245d5816eadd3c95ab2eb54" dependencies = [ "base64 0.13.1", "stellar-xdr", @@ -1067,7 +1074,7 @@ dependencies = [ [[package]] name = "soroban-spec-rust" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-sdk?rev=0992413f9b05e5bfb1f872bce99e89d9129b2e61#0992413f9b05e5bfb1f872bce99e89d9129b2e61" +source = "git+https://github.com/stellar/rs-soroban-sdk?rev=729ed3ac5fe8600a3245d5816eadd3c95ab2eb54#729ed3ac5fe8600a3245d5816eadd3c95ab2eb54" dependencies = [ "prettyplease", "proc-macro2", @@ -1132,7 +1139,7 @@ dependencies = [ [[package]] name = "stellar-xdr" version = "20.0.0-rc1" -source = "git+https://github.com/stellar/rs-stellar-xdr?rev=d5ce0c9e7aa83461773a6e81662067f35d39e4c1#d5ce0c9e7aa83461773a6e81662067f35d39e4c1" +source = "git+https://github.com/stellar/rs-stellar-xdr?rev=9c97e4fa909a0b6455547a4f4a95800696b2a69a#9c97e4fa909a0b6455547a4f4a95800696b2a69a" dependencies = [ "arbitrary", "base64 0.13.1", @@ -1156,9 +1163,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "2.0.29" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", @@ -1167,18 +1174,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.47" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", @@ -1187,12 +1194,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa", + "powerfmt", "serde", "time-core", "time-macros", @@ -1200,30 +1208,30 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "version_check" diff --git a/services/horizon/internal/integration/contracts/Cargo.toml b/services/horizon/internal/integration/contracts/Cargo.toml index 1c65ebeb4d..f7e3b81aed 100644 --- a/services/horizon/internal/integration/contracts/Cargo.toml +++ b/services/horizon/internal/integration/contracts/Cargo.toml @@ -24,4 +24,4 @@ lto = true [workspace.dependencies.soroban-sdk] version = "20.0.0-rc2" git = "https://github.com/stellar/rs-soroban-sdk" -rev = "0992413f9b05e5bfb1f872bce99e89d9129b2e61" +rev = "729ed3ac5fe8600a3245d5816eadd3c95ab2eb54" diff --git a/services/horizon/internal/integration/db_test.go b/services/horizon/internal/integration/db_test.go index 2d48e1deb8..f54ae04568 100644 --- a/services/horizon/internal/integration/db_test.go +++ b/services/horizon/internal/integration/db_test.go @@ -167,18 +167,18 @@ func submitSorobanOps(itest *integration.Test, tt *assert.Assertions) (submitted installContractOp := assembleInstallContractCodeOp(itest.CurrentTest(), itest.Master().Address(), add_u64_contract) itest.MustSubmitOperations(itest.MasterAccount(), itest.Master(), installContractOp) - bumpFootprintExpirationOp := &txnbuild.BumpFootprintExpiration{ - LedgersToExpire: 100, - SourceAccount: itest.Master().Address(), + extendFootprintTtlOp := &txnbuild.ExtendFootprintTtl{ + ExtendTo: 100, + SourceAccount: itest.Master().Address(), } - itest.MustSubmitOperations(itest.MasterAccount(), itest.Master(), bumpFootprintExpirationOp) + itest.MustSubmitOperations(itest.MasterAccount(), itest.Master(), extendFootprintTtlOp) restoreFootprintOp := &txnbuild.RestoreFootprint{ SourceAccount: itest.Master().Address(), } txResp := itest.MustSubmitOperations(itest.MasterAccount(), itest.Master(), restoreFootprintOp) - return []txnbuild.Operation{installContractOp, bumpFootprintExpirationOp, restoreFootprintOp}, txResp.Ledger + return []txnbuild.Operation{installContractOp, extendFootprintTtlOp, restoreFootprintOp}, txResp.Ledger } func submitSponsorshipOps(itest *integration.Test, tt *assert.Assertions) (submittedOperations []txnbuild.Operation, lastLedger int32) { @@ -441,7 +441,7 @@ func initializeDBIntegrationTest(t *testing.T) (*integration.Test, int32) { submitters = append(submitters, submitSorobanOps) } else { delete(allOpTypes, xdr.OperationTypeInvokeHostFunction) - delete(allOpTypes, xdr.OperationTypeBumpFootprintExpiration) + delete(allOpTypes, xdr.OperationTypeExtendFootprintTtl) delete(allOpTypes, xdr.OperationTypeRestoreFootprint) } diff --git a/services/horizon/internal/integration/bump_footprint_expiration_test.go b/services/horizon/internal/integration/extend_footprint_ttl_test.go similarity index 82% rename from services/horizon/internal/integration/bump_footprint_expiration_test.go rename to services/horizon/internal/integration/extend_footprint_ttl_test.go index 314dc26d40..a9de3b5f69 100644 --- a/services/horizon/internal/integration/bump_footprint_expiration_test.go +++ b/services/horizon/internal/integration/extend_footprint_ttl_test.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestBumpFootPrintExpiration(t *testing.T) { +func TestExtendFootprintTtl(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } @@ -30,7 +30,7 @@ func TestBumpFootPrintExpiration(t *testing.T) { installContractOp := assembleInstallContractCodeOp(t, itest.Master().Address(), add_u64_contract) preFlightOp, minFee := itest.PreflightHostFunctions(&sourceAccount, *installContractOp) - tx := itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp) + tx := itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) _, err = itest.Client().TransactionDetail(tx.Hash) require.NoError(t, err) @@ -40,29 +40,30 @@ func TestBumpFootPrintExpiration(t *testing.T) { }) require.NoError(t, err) - bumpFootPrint := txnbuild.BumpFootprintExpiration{ - LedgersToExpire: 10000, - SourceAccount: "", + bumpFootPrint := txnbuild.ExtendFootprintTtl{ + ExtendTo: 10000, + SourceAccount: "", Ext: xdr.TransactionExt{ V: 1, SorobanData: &xdr.SorobanTransactionData{ + Ext: xdr.ExtensionPoint{}, Resources: xdr.SorobanResources{ Footprint: xdr.LedgerFootprint{ ReadOnly: preFlightOp.Ext.SorobanData.Resources.Footprint.ReadWrite, ReadWrite: nil, }, }, - RefundableFee: 0, + ResourceFee: 0, }, }, } bumpFootPrint, minFee = itest.PreflightBumpFootprintExpiration(&sourceAccount, bumpFootPrint) - tx = itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &bumpFootPrint) + tx = itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &bumpFootPrint) ops, err := itest.Client().Operations(horizonclient.OperationRequest{ForTransaction: tx.Hash}) require.NoError(t, err) require.Len(t, ops.Embedded.Records, 1) - op := ops.Embedded.Records[0].(operations.BumpFootprintExpiration) - require.Equal(t, uint32(10000), op.LedgersToExpire) + op := ops.Embedded.Records[0].(operations.ExtendFootprintTtl) + require.Equal(t, uint32(10000), op.ExtendTo) } diff --git a/services/horizon/internal/integration/invokehostfunction_test.go b/services/horizon/internal/integration/invokehostfunction_test.go index 54fd2fba1f..275f0de23b 100644 --- a/services/horizon/internal/integration/invokehostfunction_test.go +++ b/services/horizon/internal/integration/invokehostfunction_test.go @@ -42,7 +42,7 @@ func TestContractInvokeHostFunctionInstallContract(t *testing.T) { installContractOp := assembleInstallContractCodeOp(t, itest.Master().Address(), add_u64_contract) preFlightOp, minFee := itest.PreflightHostFunctions(&sourceAccount, *installContractOp) - tx := itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp) + tx := itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) clientTx, err := itest.Client().TransactionDetail(tx.Hash) require.NoError(t, err) @@ -93,12 +93,12 @@ func TestContractInvokeHostFunctionCreateContractByAddress(t *testing.T) { // Install the contract installContractOp := assembleInstallContractCodeOp(t, itest.Master().Address(), add_u64_contract) preFlightOp, minFee := itest.PreflightHostFunctions(&sourceAccount, *installContractOp) - itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp) + itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) // Create the contract createContractOp := assembleCreateContractOp(t, itest.Master().Address(), add_u64_contract, "a1", itest.GetPassPhrase()) preFlightOp, minFee = itest.PreflightHostFunctions(&sourceAccount, *createContractOp) - tx, err := itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp) + tx, err := itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) require.NoError(t, err) clientTx, err := itest.Client().TransactionDetail(tx.Hash) @@ -147,12 +147,12 @@ func TestContractInvokeHostFunctionInvokeStatelessContractFn(t *testing.T) { // Install the contract installContractOp := assembleInstallContractCodeOp(t, itest.Master().Address(), add_u64_contract) preFlightOp, minFee := itest.PreflightHostFunctions(&sourceAccount, *installContractOp) - itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp) + itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) // Create the contract createContractOp := assembleCreateContractOp(t, itest.Master().Address(), add_u64_contract, "a1", itest.GetPassPhrase()) preFlightOp, minFee = itest.PreflightHostFunctions(&sourceAccount, *createContractOp) - tx, err := itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp) + tx, err := itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) require.NoError(t, err) // contract has been deployed, now invoke a simple 'add' fn on the contract @@ -191,7 +191,7 @@ func TestContractInvokeHostFunctionInvokeStatelessContractFn(t *testing.T) { } preFlightOp, minFee = itest.PreflightHostFunctions(&sourceAccount, *invokeHostFunctionOp) - tx, err = itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp) + tx, err = itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) require.NoError(t, err) clientTx, err := itest.Client().TransactionDetail(tx.Hash) @@ -257,13 +257,13 @@ func TestContractInvokeHostFunctionInvokeStatefulContractFn(t *testing.T) { installContractOp := assembleInstallContractCodeOp(t, itest.Master().Address(), increment_contract) preFlightOp, minFee := itest.PreflightHostFunctions(&sourceAccount, *installContractOp) - itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp) + itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) // Create the contract createContractOp := assembleCreateContractOp(t, itest.Master().Address(), increment_contract, "a1", itest.GetPassPhrase()) preFlightOp, minFee = itest.PreflightHostFunctions(&sourceAccount, *createContractOp) - tx, err := itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp) + tx, err := itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) require.NoError(t, err) // contract has been deployed, now invoke a simple 'add' fn on the contract @@ -287,7 +287,7 @@ func TestContractInvokeHostFunctionInvokeStatefulContractFn(t *testing.T) { } preFlightOp, minFee = itest.PreflightHostFunctions(&sourceAccount, *invokeHostFunctionOp) - tx, err = itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp) + tx, err = itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) require.NoError(t, err) clientTx, err := itest.Client().TransactionDetail(tx.Hash) diff --git a/services/horizon/internal/integration/sac_test.go b/services/horizon/internal/integration/sac_test.go index 18a167969a..06bca5a86d 100644 --- a/services/horizon/internal/integration/sac_test.go +++ b/services/horizon/internal/integration/sac_test.go @@ -894,7 +894,7 @@ func createSAC(itest *integration.Test, sourceAccount string, asset xdr.Asset) * FromAsset: &asset, }, Executable: xdr.ContractExecutable{ - Type: xdr.ContractExecutableTypeContractExecutableToken, + Type: xdr.ContractExecutableTypeContractExecutableStellarAsset, WasmHash: nil, }, }, diff --git a/services/horizon/internal/integration/testdata/soroban_add_u64.wasm b/services/horizon/internal/integration/testdata/soroban_add_u64.wasm index d8707674daa77dd316b9e6e83461a27278c61213..9aac34ea654e19ee731b56ced4e60fd08bd60c89 100755 GIT binary patch delta 69 zcmey)@||VFG{(u=Og@vRGRiZ$Oy0_s Zim8R6S!!ZRigB`~X=0L5YLcl50{{U~6o>!- delta 95 zcmey)@||VFG)8$j!Q}kBlA^@qlGMDi+|-i9G6Mz(5CKw_lbM(_7>y=tGbw8tSXvsH t7#gQpCK;HfnkJ15Fi4iTqa*;)L=B8{D)Cl u)7;21HN`kF*)%QH!py)R(b&kuG{w}y&@44ECB-<|(ljy2C^gB{gaH6beHz37 delta 69 zcmdnXx|ems7RJe%Omd8tlRcO;7>yMehnqr)o mY?_v8VP;^EXl!I+nqq2UXqK9ol46`}X_}a1l$vB}!T3C-bvwFd9v^U{}^Ou(UKXF*HuIOfoP{ mHBCxOGEB2DH%dxQwX{sNuuQQuG_p)GN;NZN*!+@}jS&EfI2FPG diff --git a/services/horizon/internal/integration/txsub_test.go b/services/horizon/internal/integration/txsub_test.go index 90266768ee..2f6b98e905 100644 --- a/services/horizon/internal/integration/txsub_test.go +++ b/services/horizon/internal/integration/txsub_test.go @@ -75,7 +75,7 @@ func TestTxSubLimitsBodySize(t *testing.T) { installContractOp := assembleInstallContractCodeOp(t, itest.Master().Address(), "soroban_sac_test.wasm") preFlightOp, minFee := itest.PreflightHostFunctions(&sourceAccount, *installContractOp) - _, err = itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp) + _, err = itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) assert.EqualError( t, err, "horizon error: \"Transaction Malformed\" - check horizon.Error.Problem for more information", @@ -88,7 +88,7 @@ func TestTxSubLimitsBodySize(t *testing.T) { installContractOp = assembleInstallContractCodeOp(t, itest.Master().Address(), "soroban_add_u64.wasm") preFlightOp, minFee = itest.PreflightHostFunctions(&sourceAccount, *installContractOp) - tx, err := itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee, &preFlightOp) + tx, err := itest.SubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) require.NoError(t, err) require.True(t, tx.Successful) } diff --git a/services/horizon/internal/resourceadapter/operations.go b/services/horizon/internal/resourceadapter/operations.go index fdd083782c..2f995fb395 100644 --- a/services/horizon/internal/resourceadapter/operations.go +++ b/services/horizon/internal/resourceadapter/operations.go @@ -150,8 +150,8 @@ func NewOperation( e := operations.InvokeHostFunction{Base: base} err = operationRow.UnmarshalDetails(&e) result = e - case xdr.OperationTypeBumpFootprintExpiration: - e := operations.BumpFootprintExpiration{Base: base} + case xdr.OperationTypeExtendFootprintTtl: + e := operations.ExtendFootprintTtl{Base: base} err = operationRow.UnmarshalDetails(&e) result = e case xdr.OperationTypeRestoreFootprint: diff --git a/services/horizon/internal/test/integration/integration.go b/services/horizon/internal/test/integration/integration.go index 9ef5798487..7c06a187dd 100644 --- a/services/horizon/internal/test/integration/integration.go +++ b/services/horizon/internal/test/integration/integration.go @@ -701,8 +701,8 @@ func (i *Test) syncWithSorobanRPC(ledgerToWaitFor uint32) { } func (i *Test) PreflightBumpFootprintExpiration( - sourceAccount txnbuild.Account, bumpFootprint txnbuild.BumpFootprintExpiration, -) (txnbuild.BumpFootprintExpiration, int64) { + sourceAccount txnbuild.Account, bumpFootprint txnbuild.ExtendFootprintTtl, +) (txnbuild.ExtendFootprintTtl, int64) { result, transactionData := i.simulateTransaction(sourceAccount, &bumpFootprint) bumpFootprint.Ext = xdr.TransactionExt{ V: 1, diff --git a/txnbuild/bump_footprint_expiration.go b/txnbuild/bump_footprint_expiration.go deleted file mode 100644 index 169c3068af..0000000000 --- a/txnbuild/bump_footprint_expiration.go +++ /dev/null @@ -1,59 +0,0 @@ -package txnbuild - -import ( - "github.com/stellar/go/support/errors" - "github.com/stellar/go/xdr" -) - -type BumpFootprintExpiration struct { - LedgersToExpire uint32 - SourceAccount string - Ext xdr.TransactionExt -} - -func (f *BumpFootprintExpiration) BuildXDR() (xdr.Operation, error) { - xdrOp := xdr.BumpFootprintExpirationOp{ - Ext: xdr.ExtensionPoint{ - V: 0, - }, - LedgersToExpire: xdr.Uint32(f.LedgersToExpire), - } - - body, err := xdr.NewOperationBody(xdr.OperationTypeBumpFootprintExpiration, xdrOp) - if err != nil { - return xdr.Operation{}, errors.Wrap(err, "failed to build XDR Operation") - } - - op := xdr.Operation{Body: body} - - SetOpSourceAccount(&op, f.SourceAccount) - return op, nil -} - -func (f *BumpFootprintExpiration) FromXDR(xdrOp xdr.Operation) error { - result, ok := xdrOp.Body.GetBumpFootprintExpirationOp() - if !ok { - return errors.New("error parsing invoke host function operation from xdr") - } - f.SourceAccount = accountFromXDR(xdrOp.SourceAccount) - f.LedgersToExpire = uint32(result.LedgersToExpire) - return nil -} - -func (f *BumpFootprintExpiration) Validate() error { - if f.SourceAccount != "" { - _, err := xdr.AddressToMuxedAccount(f.SourceAccount) - if err != nil { - return NewValidationError("SourceAccount", err.Error()) - } - } - return nil -} - -func (f *BumpFootprintExpiration) GetSourceAccount() string { - return f.SourceAccount -} - -func (f *BumpFootprintExpiration) BuildTransactionExt() (xdr.TransactionExt, error) { - return f.Ext, nil -} diff --git a/txnbuild/extend_footprint_ttl.go b/txnbuild/extend_footprint_ttl.go new file mode 100644 index 0000000000..f28dee6e15 --- /dev/null +++ b/txnbuild/extend_footprint_ttl.go @@ -0,0 +1,59 @@ +package txnbuild + +import ( + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +type ExtendFootprintTtl struct { + ExtendTo uint32 + SourceAccount string + Ext xdr.TransactionExt +} + +func (f *ExtendFootprintTtl) BuildXDR() (xdr.Operation, error) { + xdrOp := xdr.ExtendFootprintTtlOp{ + Ext: xdr.ExtensionPoint{ + V: 0, + }, + ExtendTo: xdr.Uint32(f.ExtendTo), + } + + body, err := xdr.NewOperationBody(xdr.OperationTypeExtendFootprintTtl, xdrOp) + if err != nil { + return xdr.Operation{}, errors.Wrap(err, "failed to build XDR Operation") + } + + op := xdr.Operation{Body: body} + + SetOpSourceAccount(&op, f.SourceAccount) + return op, nil +} + +func (f *ExtendFootprintTtl) FromXDR(xdrOp xdr.Operation) error { + result, ok := xdrOp.Body.GetExtendFootprintTtlOp() + if !ok { + return errors.New("error parsing invoke host function operation from xdr") + } + f.SourceAccount = accountFromXDR(xdrOp.SourceAccount) + f.ExtendTo = uint32(result.ExtendTo) + return nil +} + +func (f *ExtendFootprintTtl) Validate() error { + if f.SourceAccount != "" { + _, err := xdr.AddressToMuxedAccount(f.SourceAccount) + if err != nil { + return NewValidationError("SourceAccount", err.Error()) + } + } + return nil +} + +func (f *ExtendFootprintTtl) GetSourceAccount() string { + return f.SourceAccount +} + +func (f *ExtendFootprintTtl) BuildTransactionExt() (xdr.TransactionExt, error) { + return f.Ext, nil +} diff --git a/txnbuild/invoke_host_function_test.go b/txnbuild/invoke_host_function_test.go index 25c6d177ea..8fd5e6e48e 100644 --- a/txnbuild/invoke_host_function_test.go +++ b/txnbuild/invoke_host_function_test.go @@ -143,7 +143,7 @@ func TestInvokeHostFunctionRoundTrip(t *testing.T) { ReadBytes: 0, WriteBytes: 0, }, - RefundableFee: 1, + ResourceFee: 1, Ext: xdr.ExtensionPoint{ V: 0, }, diff --git a/txnbuild/operation.go b/txnbuild/operation.go index 8182ada8a8..a436271592 100644 --- a/txnbuild/operation.go +++ b/txnbuild/operation.go @@ -78,8 +78,8 @@ func operationFromXDR(xdrOp xdr.Operation) (Operation, error) { newOp = &LiquidityPoolWithdraw{} case xdr.OperationTypeInvokeHostFunction: newOp = &InvokeHostFunction{} - case xdr.OperationTypeBumpFootprintExpiration: - newOp = &BumpFootprintExpiration{} + case xdr.OperationTypeExtendFootprintTtl: + newOp = &ExtendFootprintTtl{} case xdr.OperationTypeRestoreFootprint: newOp = &RestoreFootprint{} default: diff --git a/xdr/Stellar-contract-config-setting.x b/xdr/Stellar-contract-config-setting.x index 9512f0c4d6..b187a18c5a 100644 --- a/xdr/Stellar-contract-config-setting.x +++ b/xdr/Stellar-contract-config-setting.x @@ -92,64 +92,54 @@ struct ConfigSettingContractBandwidthV0 enum ContractCostType { // Cost of running 1 wasm instruction WasmInsnExec = 0, - // Cost of growing wasm linear memory by 1 page - WasmMemAlloc = 1, - // Cost of allocating a chuck of host memory (in bytes) - HostMemAlloc = 2, - // Cost of copying a chuck of bytes into a pre-allocated host memory - HostMemCpy = 3, - // Cost of comparing two slices of host memory - HostMemCmp = 4, + // Cost of allocating a slice of memory (in bytes) + MemAlloc = 1, + // Cost of copying a slice of bytes into a pre-allocated memory + MemCpy = 2, + // Cost of comparing two slices of memory + MemCmp = 3, // Cost of a host function dispatch, not including the actual work done by // the function nor the cost of VM invocation machinary - DispatchHostFunction = 5, + DispatchHostFunction = 4, // Cost of visiting a host object from the host object storage. Exists to // make sure some baseline cost coverage, i.e. repeatly visiting objects // by the guest will always incur some charges. - VisitObject = 6, + VisitObject = 5, // Cost of serializing an xdr object to bytes - ValSer = 7, + ValSer = 6, // Cost of deserializing an xdr object from bytes - ValDeser = 8, + ValDeser = 7, // Cost of computing the sha256 hash from bytes - ComputeSha256Hash = 9, + ComputeSha256Hash = 8, // Cost of computing the ed25519 pubkey from bytes - ComputeEd25519PubKey = 10, - // Cost of accessing an entry in a Map. - MapEntry = 11, - // Cost of accessing an entry in a Vec - VecEntry = 12, + ComputeEd25519PubKey = 9, // Cost of verifying ed25519 signature of a payload. - VerifyEd25519Sig = 13, - // Cost of reading a slice of vm linear memory - VmMemRead = 14, - // Cost of writing to a slice of vm linear memory - VmMemWrite = 15, + VerifyEd25519Sig = 10, // Cost of instantiation a VM from wasm bytes code. - VmInstantiation = 16, + VmInstantiation = 11, // Cost of instantiation a VM from a cached state. - VmCachedInstantiation = 17, + VmCachedInstantiation = 12, // Cost of invoking a function on the VM. If the function is a host function, // additional cost will be covered by `DispatchHostFunction`. - InvokeVmFunction = 18, + InvokeVmFunction = 13, // Cost of computing a keccak256 hash from bytes. - ComputeKeccak256Hash = 19, - // Cost of computing an ECDSA secp256k1 pubkey from bytes. - ComputeEcdsaSecp256k1Key = 20, + ComputeKeccak256Hash = 14, // Cost of computing an ECDSA secp256k1 signature from bytes. - ComputeEcdsaSecp256k1Sig = 21, + ComputeEcdsaSecp256k1Sig = 15, // Cost of recovering an ECDSA secp256k1 key from a signature. - RecoverEcdsaSecp256k1Key = 22, + RecoverEcdsaSecp256k1Key = 16, // Cost of int256 addition (`+`) and subtraction (`-`) operations - Int256AddSub = 23, + Int256AddSub = 17, // Cost of int256 multiplication (`*`) operation - Int256Mul = 24, + Int256Mul = 18, // Cost of int256 division (`/`) operation - Int256Div = 25, + Int256Div = 19, // Cost of int256 power (`exp`) operation - Int256Pow = 26, + Int256Pow = 20, // Cost of int256 shift (`shl`, `shr`) operation - Int256Shift = 27 + Int256Shift = 21, + // Cost of drawing random bytes using a ChaCha20 PRNG + ChaCha20DrawBytes = 22 }; struct ContractCostParamEntry { @@ -160,17 +150,17 @@ struct ContractCostParamEntry { int64 linearTerm; }; -struct StateExpirationSettings { - uint32 maxEntryExpiration; - uint32 minTempEntryExpiration; - uint32 minPersistentEntryExpiration; +struct StateArchivalSettings { + uint32 maxEntryTTL; + uint32 minTemporaryTTL; + uint32 minPersistentTTL; // rent_fee = wfee_rate_average / rent_rate_denominator_for_type int64 persistentRentRateDenominator; int64 tempRentRateDenominator; - // max number of entries that emit expiration meta in a single ledger - uint32 maxEntriesToExpire; + // max number of entries that emit archival meta in a single ledger + uint32 maxEntriesToArchive; // Number of snapshots to use when calculating average BucketList size uint32 bucketListSizeWindowSampleSize; @@ -206,7 +196,7 @@ enum ConfigSettingID CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES = 7, CONFIG_SETTING_CONTRACT_DATA_KEY_SIZE_BYTES = 8, CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES = 9, - CONFIG_SETTING_STATE_EXPIRATION = 10, + CONFIG_SETTING_STATE_ARCHIVAL = 10, CONFIG_SETTING_CONTRACT_EXECUTION_LANES = 11, CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW = 12, CONFIG_SETTING_EVICTION_ITERATOR = 13 @@ -234,8 +224,8 @@ case CONFIG_SETTING_CONTRACT_DATA_KEY_SIZE_BYTES: uint32 contractDataKeySizeBytes; case CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES: uint32 contractDataEntrySizeBytes; -case CONFIG_SETTING_STATE_EXPIRATION: - StateExpirationSettings stateExpirationSettings; +case CONFIG_SETTING_STATE_ARCHIVAL: + StateArchivalSettings stateArchivalSettings; case CONFIG_SETTING_CONTRACT_EXECUTION_LANES: ConfigSettingContractExecutionLanesV0 contractExecutionLanes; case CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW: diff --git a/xdr/Stellar-contract.x b/xdr/Stellar-contract.x index 7c7469c7e8..511300562e 100644 --- a/xdr/Stellar-contract.x +++ b/xdr/Stellar-contract.x @@ -165,14 +165,14 @@ struct Int256Parts { enum ContractExecutableType { CONTRACT_EXECUTABLE_WASM = 0, - CONTRACT_EXECUTABLE_TOKEN = 1 + CONTRACT_EXECUTABLE_STELLAR_ASSET = 1 }; union ContractExecutable switch (ContractExecutableType type) { case CONTRACT_EXECUTABLE_WASM: Hash wasm_hash; -case CONTRACT_EXECUTABLE_TOKEN: +case CONTRACT_EXECUTABLE_STELLAR_ASSET: void; }; diff --git a/xdr/Stellar-internal.x b/xdr/Stellar-internal.x index 73684db7ac..02f1b81e8e 100644 --- a/xdr/Stellar-internal.x +++ b/xdr/Stellar-internal.x @@ -17,6 +17,13 @@ case 1: GeneralizedTransactionSet generalizedTxSet; }; +struct StoredDebugTransactionSet +{ + StoredTransactionSet txSet; + uint32 ledgerSeq; + StellarValue scpValue; +}; + struct PersistedSCPStateV0 { SCPEnvelope scpEnvelopes<>; diff --git a/xdr/Stellar-ledger-entries.x b/xdr/Stellar-ledger-entries.x index f066484001..8a8784e2bb 100644 --- a/xdr/Stellar-ledger-entries.x +++ b/xdr/Stellar-ledger-entries.x @@ -101,7 +101,7 @@ enum LedgerEntryType CONTRACT_DATA = 6, CONTRACT_CODE = 7, CONFIG_SETTING = 8, - EXPIRATION = 9 + TTL = 9 }; struct Signer @@ -515,10 +515,10 @@ struct ContractCodeEntry { opaque code<>; }; -struct ExpirationEntry { - // Hash of the LedgerKey that is associated with this ExpirationEntry +struct TTLEntry { + // Hash of the LedgerKey that is associated with this TTLEntry Hash keyHash; - uint32 expirationLedgerSeq; + uint32 liveUntilLedgerSeq; }; struct LedgerEntryExtensionV1 @@ -557,8 +557,8 @@ struct LedgerEntry ContractCodeEntry contractCode; case CONFIG_SETTING: ConfigSettingEntry configSetting; - case EXPIRATION: - ExpirationEntry expiration; + case TTL: + TTLEntry ttl; } data; @@ -630,12 +630,12 @@ case CONFIG_SETTING: { ConfigSettingID configSettingID; } configSetting; -case EXPIRATION: +case TTL: struct { - // Hash of the LedgerKey that is associated with this ExpirationEntry + // Hash of the LedgerKey that is associated with this TTLEntry Hash keyHash; - } expiration; + } ttl; }; // list of all envelope types used in the application diff --git a/xdr/Stellar-ledger.x b/xdr/Stellar-ledger.x index a1bbac4b64..b18a3a0d57 100644 --- a/xdr/Stellar-ledger.x +++ b/xdr/Stellar-ledger.x @@ -486,26 +486,8 @@ struct LedgerCloseMetaV0 struct LedgerCloseMetaV1 { - LedgerHeaderHistoryEntry ledgerHeader; - - GeneralizedTransactionSet txSet; - - // NB: transactions are sorted in apply order here - // fees for all transactions are processed first - // followed by applying transactions - TransactionResultMeta txProcessing<>; - - // upgrades are applied last - UpgradeEntryMeta upgradesProcessing<>; - - // other misc information attached to the ledger close - SCPHistoryEntry scpInfo<>; -}; - -struct LedgerCloseMetaV2 -{ - // We forgot to add an ExtensionPoint in v1 but at least - // we can add one now in v2. + // We forgot to add an ExtensionPoint in v0 but at least + // we can add one now in v1. ExtensionPoint ext; LedgerHeaderHistoryEntry ledgerHeader; @@ -527,10 +509,10 @@ struct LedgerCloseMetaV2 // systems calculating storage fees correctly. uint64 totalByteSizeOfBucketList; - // Expired temp keys that are being evicted at this ledger. + // Temp keys that are being evicted at this ledger. LedgerKey evictedTemporaryLedgerKeys<>; - // Expired restorable ledger entries that are being + // Archived restorable ledger entries that are being // evicted at this ledger. LedgerEntry evictedPersistentLedgerEntries<>; }; @@ -541,7 +523,5 @@ case 0: LedgerCloseMetaV0 v0; case 1: LedgerCloseMetaV1 v1; -case 2: - LedgerCloseMetaV2 v2; }; } diff --git a/xdr/Stellar-transaction.x b/xdr/Stellar-transaction.x index 2e3c22b318..c7f0f5e276 100644 --- a/xdr/Stellar-transaction.x +++ b/xdr/Stellar-transaction.x @@ -63,7 +63,7 @@ enum OperationType LIQUIDITY_POOL_DEPOSIT = 22, LIQUIDITY_POOL_WITHDRAW = 23, INVOKE_HOST_FUNCTION = 24, - BUMP_FOOTPRINT_EXPIRATION = 25, + EXTEND_FOOTPRINT_TTL = 25, RESTORE_FOOTPRINT = 26 }; @@ -585,19 +585,19 @@ struct InvokeHostFunctionOp SorobanAuthorizationEntry auth<>; }; -/* Bump the expiration ledger of the entries specified in the readOnly footprint - so they'll expire at least ledgersToExpire ledgers from lcl. +/* Extend the TTL of the entries specified in the readOnly footprint + so they will live at least extendTo ledgers from lcl. Threshold: med - Result: BumpFootprintExpirationResult + Result: ExtendFootprintTTLResult */ -struct BumpFootprintExpirationOp +struct ExtendFootprintTTLOp { ExtensionPoint ext; - uint32 ledgersToExpire; + uint32 extendTo; }; -/* Restore the expired or evicted entries specified in the readWrite footprint. +/* Restore the archived entries specified in the readWrite footprint. Threshold: med Result: RestoreFootprintOp @@ -667,8 +667,8 @@ struct Operation LiquidityPoolWithdrawOp liquidityPoolWithdrawOp; case INVOKE_HOST_FUNCTION: InvokeHostFunctionOp invokeHostFunctionOp; - case BUMP_FOOTPRINT_EXPIRATION: - BumpFootprintExpirationOp bumpFootprintExpirationOp; + case EXTEND_FOOTPRINT_TTL: + ExtendFootprintTTLOp extendFootprintTTLOp; case RESTORE_FOOTPRINT: RestoreFootprintOp restoreFootprintOp; } @@ -821,8 +821,16 @@ struct SorobanTransactionData { ExtensionPoint ext; SorobanResources resources; - // Portion of transaction `fee` allocated to refundable fees. - int64 refundableFee; + // Amount of the transaction `fee` allocated to the Soroban resource fees. + // The fraction of `resourceFee` corresponding to `resources` specified + // above is *not* refundable (i.e. fees for instructions, ledger I/O), as + // well as fees for the transaction size. + // The remaining part of the fee is refundable and the charged value is + // based on the actual consumption of refundable resources (events, ledger + // rent bumps). + // The `inclusionFee` used for prioritization of the transaction is defined + // as `tx.fee - resourceFee`. + int64 resourceFee; }; // TransactionV0 is a transaction with the AccountID discriminant stripped off, @@ -1789,7 +1797,7 @@ enum InvokeHostFunctionResultCode INVOKE_HOST_FUNCTION_MALFORMED = -1, INVOKE_HOST_FUNCTION_TRAPPED = -2, INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED = -3, - INVOKE_HOST_FUNCTION_ENTRY_EXPIRED = -4, + INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED = -4, INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE = -5 }; @@ -1800,29 +1808,29 @@ case INVOKE_HOST_FUNCTION_SUCCESS: case INVOKE_HOST_FUNCTION_MALFORMED: case INVOKE_HOST_FUNCTION_TRAPPED: case INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED: -case INVOKE_HOST_FUNCTION_ENTRY_EXPIRED: +case INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED: case INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE: void; }; -enum BumpFootprintExpirationResultCode +enum ExtendFootprintTTLResultCode { // codes considered as "success" for the operation - BUMP_FOOTPRINT_EXPIRATION_SUCCESS = 0, + EXTEND_FOOTPRINT_TTL_SUCCESS = 0, // codes considered as "failure" for the operation - BUMP_FOOTPRINT_EXPIRATION_MALFORMED = -1, - BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED = -2, - BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE = -3 + EXTEND_FOOTPRINT_TTL_MALFORMED = -1, + EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED = -2, + EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE = -3 }; -union BumpFootprintExpirationResult switch (BumpFootprintExpirationResultCode code) +union ExtendFootprintTTLResult switch (ExtendFootprintTTLResultCode code) { -case BUMP_FOOTPRINT_EXPIRATION_SUCCESS: +case EXTEND_FOOTPRINT_TTL_SUCCESS: void; -case BUMP_FOOTPRINT_EXPIRATION_MALFORMED: -case BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED: -case BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE: +case EXTEND_FOOTPRINT_TTL_MALFORMED: +case EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED: +case EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE: void; }; @@ -1915,8 +1923,8 @@ case opINNER: LiquidityPoolWithdrawResult liquidityPoolWithdrawResult; case INVOKE_HOST_FUNCTION: InvokeHostFunctionResult invokeHostFunctionResult; - case BUMP_FOOTPRINT_EXPIRATION: - BumpFootprintExpirationResult bumpFootprintExpirationResult; + case EXTEND_FOOTPRINT_TTL: + ExtendFootprintTTLResult extendFootprintTTLResult; case RESTORE_FOOTPRINT: RestoreFootprintResult restoreFootprintResult; } diff --git a/xdr/ledger_close_meta.go b/xdr/ledger_close_meta.go index c2b352b613..2290f3bee1 100644 --- a/xdr/ledger_close_meta.go +++ b/xdr/ledger_close_meta.go @@ -10,8 +10,6 @@ func (l LedgerCloseMeta) LedgerHeaderHistoryEntry() LedgerHeaderHistoryEntry { return l.MustV0().LedgerHeader case 1: return l.MustV1().LedgerHeader - case 2: - return l.MustV2().LedgerHeader default: panic(fmt.Sprintf("Unsupported LedgerCloseMeta.V: %d", l.V)) } @@ -43,8 +41,7 @@ func (l LedgerCloseMeta) CountTransactions() int { return len(l.MustV0().TxProcessing) case 1: return len(l.MustV1().TxProcessing) - case 2: - return len(l.MustV2().TxProcessing) + default: panic(fmt.Sprintf("Unsupported LedgerCloseMeta.V: %d", l.V)) } @@ -54,15 +51,9 @@ func (l LedgerCloseMeta) TransactionEnvelopes() []TransactionEnvelope { switch l.V { case 0: return l.MustV0().TxSet.Txs - case 1, 2: + case 1: var envelopes = make([]TransactionEnvelope, 0, l.CountTransactions()) - var phases []TransactionPhase - if l.V == 1 { - phases = l.MustV1().TxSet.V1TxSet.Phases - } else { - phases = l.MustV2().TxSet.V1TxSet.Phases - } - for _, phase := range phases { + for _, phase := range l.MustV1().TxSet.V1TxSet.Phases { for _, component := range *phase.V0Components { envelopes = append(envelopes, component.TxsMaybeDiscountedFee.Txs...) } @@ -80,8 +71,6 @@ func (l LedgerCloseMeta) TransactionHash(i int) Hash { return l.MustV0().TxProcessing[i].Result.TransactionHash case 1: return l.MustV1().TxProcessing[i].Result.TransactionHash - case 2: - return l.MustV2().TxProcessing[i].Result.TransactionHash default: panic(fmt.Sprintf("Unsupported LedgerCloseMeta.V: %d", l.V)) } @@ -94,8 +83,6 @@ func (l LedgerCloseMeta) TransactionResultPair(i int) TransactionResultPair { return l.MustV0().TxProcessing[i].Result case 1: return l.MustV1().TxProcessing[i].Result - case 2: - return l.MustV2().TxProcessing[i].Result default: panic(fmt.Sprintf("Unsupported LedgerCloseMeta.V: %d", l.V)) } @@ -108,8 +95,6 @@ func (l LedgerCloseMeta) FeeProcessing(i int) LedgerEntryChanges { return l.MustV0().TxProcessing[i].FeeProcessing case 1: return l.MustV1().TxProcessing[i].FeeProcessing - case 2: - return l.MustV2().TxProcessing[i].FeeProcessing default: panic(fmt.Sprintf("Unsupported LedgerCloseMeta.V: %d", l.V)) } @@ -121,12 +106,10 @@ func (l LedgerCloseMeta) TxApplyProcessing(i int) TransactionMeta { case 0: return l.MustV0().TxProcessing[i].TxApplyProcessing case 1: - return l.MustV1().TxProcessing[i].TxApplyProcessing - case 2: - if l.MustV2().TxProcessing[i].TxApplyProcessing.V != 3 { - panic("TransactionResult unavailable because LedgerCloseMeta.V = 2 and TransactionMeta.V != 3") + if l.MustV1().TxProcessing[i].TxApplyProcessing.V != 3 { + panic("TransactionResult unavailable because LedgerCloseMeta.V = 1 and TransactionMeta.V != 3") } - return l.MustV2().TxProcessing[i].TxApplyProcessing + return l.MustV1().TxProcessing[i].TxApplyProcessing default: panic(fmt.Sprintf("Unsupported LedgerCloseMeta.V: %d", l.V)) } @@ -139,8 +122,6 @@ func (l LedgerCloseMeta) UpgradesProcessing() []UpgradeEntryMeta { return l.MustV0().UpgradesProcessing case 1: return l.MustV1().UpgradesProcessing - case 2: - return l.MustV2().UpgradesProcessing default: panic(fmt.Sprintf("Unsupported LedgerCloseMeta.V: %d", l.V)) } @@ -150,10 +131,10 @@ func (l LedgerCloseMeta) UpgradesProcessing() []UpgradeEntryMeta { // temporary ledger entries that have been evicted in this ledger. func (l LedgerCloseMeta) EvictedTemporaryLedgerKeys() ([]LedgerKey, error) { switch l.V { - case 0, 1: + case 0: return nil, nil - case 2: - return l.MustV2().EvictedTemporaryLedgerKeys, nil + case 1: + return l.MustV1().EvictedTemporaryLedgerKeys, nil default: panic(fmt.Sprintf("Unsupported LedgerCloseMeta.V: %d", l.V)) } @@ -163,10 +144,10 @@ func (l LedgerCloseMeta) EvictedTemporaryLedgerKeys() ([]LedgerKey, error) { // which have been evicted in this ledger. func (l LedgerCloseMeta) EvictedPersistentLedgerEntries() ([]LedgerEntry, error) { switch l.V { - case 0, 1: + case 0: return nil, nil - case 2: - return l.MustV2().EvictedPersistentLedgerEntries, nil + case 1: + return l.MustV1().EvictedPersistentLedgerEntries, nil default: panic(fmt.Sprintf("Unsupported LedgerCloseMeta.V: %d", l.V)) } diff --git a/xdr/ledger_entry.go b/xdr/ledger_entry.go index 8df379bffe..c04defb693 100644 --- a/xdr/ledger_entry.go +++ b/xdr/ledger_entry.go @@ -173,8 +173,8 @@ func (data *LedgerEntryData) LedgerKey() (LedgerKey, error) { if err := key.SetConfigSetting(data.ConfigSetting.ConfigSettingId); err != nil { return key, err } - case LedgerEntryTypeExpiration: - if err := key.SetExpiration(data.Expiration.KeyHash); err != nil { + case LedgerEntryTypeTtl: + if err := key.SetTtl(data.Ttl.KeyHash); err != nil { return key, err } default: diff --git a/xdr/ledger_key.go b/xdr/ledger_key.go index df4d01e9fc..88ba800cf2 100644 --- a/xdr/ledger_key.go +++ b/xdr/ledger_key.go @@ -53,9 +53,9 @@ func (key *LedgerKey) Equals(other LedgerKey) bool { l := key.MustClaimableBalance() r := other.MustClaimableBalance() return l.BalanceId.MustV0() == r.BalanceId.MustV0() - case LedgerEntryTypeExpiration: - l := key.MustExpiration() - r := other.MustExpiration() + case LedgerEntryTypeTtl: + l := key.MustTtl() + r := other.MustTtl() return l.KeyHash == r.KeyHash default: panic(fmt.Errorf("unknown ledger key type: %v", key.Type)) @@ -188,13 +188,13 @@ func (key *LedgerKey) SetConfigSetting(configSettingID ConfigSettingId) error { return nil } -// SetExpiration mutates `key` such that it represents the identity of an +// SetTtl mutates `key` such that it represents the identity of an // expiration entry. -func (key *LedgerKey) SetExpiration(keyHash Hash) error { - data := LedgerKeyExpiration{ +func (key *LedgerKey) SetTtl(keyHash Hash) error { + data := LedgerKeyTtl{ KeyHash: keyHash, } - nkey, err := NewLedgerKey(LedgerEntryTypeExpiration, data) + nkey, err := NewLedgerKey(LedgerEntryTypeTtl, data) if err != nil { return err } @@ -259,8 +259,8 @@ func (e *EncodingBuffer) ledgerKeyCompressEncodeTo(key LedgerKey) error { return err case LedgerEntryTypeConfigSetting: return key.ConfigSetting.ConfigSettingId.EncodeTo(e.encoder) - case LedgerEntryTypeExpiration: - return key.Expiration.KeyHash.EncodeTo(e.encoder) + case LedgerEntryTypeTtl: + return key.Ttl.KeyHash.EncodeTo(e.encoder) default: panic("Unknown type") } diff --git a/xdr/scval.go b/xdr/scval.go index 559941c27a..4648a6ab82 100644 --- a/xdr/scval.go +++ b/xdr/scval.go @@ -34,7 +34,7 @@ func (s ContractExecutable) Equals(o ContractExecutable) bool { return false } switch s.Type { - case ContractExecutableTypeContractExecutableToken: + case ContractExecutableTypeContractExecutableStellarAsset: return true case ContractExecutableTypeContractExecutableWasm: return s.MustWasmHash().Equals(o.MustWasmHash()) diff --git a/xdr/xdr_commit_generated.txt b/xdr/xdr_commit_generated.txt index c5e549e9c1..3c6d18d90d 100644 --- a/xdr/xdr_commit_generated.txt +++ b/xdr/xdr_commit_generated.txt @@ -1 +1 @@ -9ac02641139e6717924fdad716f6e958d0168491 \ No newline at end of file +6a620d160aab22609c982d54578ff6a63bfcdc01 \ No newline at end of file diff --git a/xdr/xdr_generated.go b/xdr/xdr_generated.go index db8fcf56aa..f4f59553cc 100644 --- a/xdr/xdr_generated.go +++ b/xdr/xdr_generated.go @@ -32,16 +32,16 @@ import ( // XdrFilesSHA256 is the SHA256 hashes of source files. var XdrFilesSHA256 = map[string]string{ "xdr/Stellar-SCP.x": "8f32b04d008f8bc33b8843d075e69837231a673691ee41d8b821ca229a6e802a", - "xdr/Stellar-contract-config-setting.x": "fd8709d1bcc36a90a1f7b1fd8cb4407f7733bec5ca06494cac9b6a99b942ef99", + "xdr/Stellar-contract-config-setting.x": "e466c4dfae1d5d181afbd990b91f26c5d8ed84a7fa987875f8d643cf97e34a77", "xdr/Stellar-contract-env-meta.x": "928a30de814ee589bc1d2aadd8dd81c39f71b7e6f430f56974505ccb1f49654b", "xdr/Stellar-contract-meta.x": "f01532c11ca044e19d9f9f16fe373e9af64835da473be556b9a807ee3319ae0d", "xdr/Stellar-contract-spec.x": "c7ffa21d2e91afb8e666b33524d307955426ff553a486d670c29217ed9888d49", - "xdr/Stellar-contract.x": "234d2adf0c9bdf7c42ea64a2650884d8e36ed31cd1cbe13fb8d12b335fb4e5c3", - "xdr/Stellar-internal.x": "368706dd6e2efafd16a8f63daf3374845b791d097b15c502aa7653a412b68b68", - "xdr/Stellar-ledger-entries.x": "73b467bce654c5b19d0fba24008c9ccae77b439320a4c9eef9128e1818fdd76d", - "xdr/Stellar-ledger.x": "247d1b486d546f5c37f3d8a719b195e3331106302bcdc54cd1f52a6f94a9a7ed", + "xdr/Stellar-contract.x": "7f665e4103e146a88fcdabce879aaaacd3bf9283feb194cc47ff986264c1e315", + "xdr/Stellar-internal.x": "227835866c1b2122d1eaf28839ba85ea7289d1cb681dda4ca619c2da3d71fe00", + "xdr/Stellar-ledger-entries.x": "4f8f2324f567a40065f54f696ea1428740f043ea4154f5986d9f499ad00ac333", + "xdr/Stellar-ledger.x": "2c842f3fe6e269498af5467f849cf6818554e90babc845f34c87cda471298d0f", "xdr/Stellar-overlay.x": "de3957c58b96ae07968b3d3aebea84f83603e95322d1fa336360e13e3aba737a", - "xdr/Stellar-transaction.x": "ce8194511afb4cbb165921c720d057381bcd4829999027d42753c11d5dcaa7f8", + "xdr/Stellar-transaction.x": "c5dd8507bc84e10b67bf3bc74c8f716a660b425814b015025c6f6b6f20cb70e7", "xdr/Stellar-types.x": "6e3b13f0d3e360b09fa5e2b0e55d43f4d974a769df66afb34e8aecbb329d3f15", } @@ -2768,7 +2768,7 @@ var _ xdrType = (*ThresholdIndexes)(nil) // CONTRACT_DATA = 6, // CONTRACT_CODE = 7, // CONFIG_SETTING = 8, -// EXPIRATION = 9 +// TTL = 9 // }; type LedgerEntryType int32 @@ -2782,7 +2782,7 @@ const ( LedgerEntryTypeContractData LedgerEntryType = 6 LedgerEntryTypeContractCode LedgerEntryType = 7 LedgerEntryTypeConfigSetting LedgerEntryType = 8 - LedgerEntryTypeExpiration LedgerEntryType = 9 + LedgerEntryTypeTtl LedgerEntryType = 9 ) var ledgerEntryTypeMap = map[int32]string{ @@ -2795,7 +2795,7 @@ var ledgerEntryTypeMap = map[int32]string{ 6: "LedgerEntryTypeContractData", 7: "LedgerEntryTypeContractCode", 8: "LedgerEntryTypeConfigSetting", - 9: "LedgerEntryTypeExpiration", + 9: "LedgerEntryTypeTtl", } // ValidEnum validates a proposed value for this enum. Implements @@ -8028,36 +8028,36 @@ func (s ContractCodeEntry) xdrType() {} var _ xdrType = (*ContractCodeEntry)(nil) -// ExpirationEntry is an XDR Struct defines as: +// TtlEntry is an XDR Struct defines as: // -// struct ExpirationEntry { -// // Hash of the LedgerKey that is associated with this ExpirationEntry +// struct TTLEntry { +// // Hash of the LedgerKey that is associated with this TTLEntry // Hash keyHash; -// uint32 expirationLedgerSeq; +// uint32 liveUntilLedgerSeq; // }; -type ExpirationEntry struct { - KeyHash Hash - ExpirationLedgerSeq Uint32 +type TtlEntry struct { + KeyHash Hash + LiveUntilLedgerSeq Uint32 } // EncodeTo encodes this value using the Encoder. -func (s *ExpirationEntry) EncodeTo(e *xdr.Encoder) error { +func (s *TtlEntry) EncodeTo(e *xdr.Encoder) error { var err error if err = s.KeyHash.EncodeTo(e); err != nil { return err } - if err = s.ExpirationLedgerSeq.EncodeTo(e); err != nil { + if err = s.LiveUntilLedgerSeq.EncodeTo(e); err != nil { return err } return nil } -var _ decoderFrom = (*ExpirationEntry)(nil) +var _ decoderFrom = (*TtlEntry)(nil) // DecodeFrom decodes this value using the Decoder. -func (s *ExpirationEntry) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { +func (s *TtlEntry) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { if maxDepth == 0 { - return 0, fmt.Errorf("decoding ExpirationEntry: %w", ErrMaxDecodingDepthReached) + return 0, fmt.Errorf("decoding TtlEntry: %w", ErrMaxDecodingDepthReached) } maxDepth -= 1 var err error @@ -8067,7 +8067,7 @@ func (s *ExpirationEntry) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) if err != nil { return n, fmt.Errorf("decoding Hash: %w", err) } - nTmp, err = s.ExpirationLedgerSeq.DecodeFrom(d, maxDepth) + nTmp, err = s.LiveUntilLedgerSeq.DecodeFrom(d, maxDepth) n += nTmp if err != nil { return n, fmt.Errorf("decoding Uint32: %w", err) @@ -8076,7 +8076,7 @@ func (s *ExpirationEntry) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } // MarshalBinary implements encoding.BinaryMarshaler. -func (s ExpirationEntry) MarshalBinary() ([]byte, error) { +func (s TtlEntry) MarshalBinary() ([]byte, error) { b := bytes.Buffer{} e := xdr.NewEncoder(&b) err := s.EncodeTo(e) @@ -8084,7 +8084,7 @@ func (s ExpirationEntry) MarshalBinary() ([]byte, error) { } // UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *ExpirationEntry) UnmarshalBinary(inp []byte) error { +func (s *TtlEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) d := xdr.NewDecoder(r) _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) @@ -8092,15 +8092,15 @@ func (s *ExpirationEntry) UnmarshalBinary(inp []byte) error { } var ( - _ encoding.BinaryMarshaler = (*ExpirationEntry)(nil) - _ encoding.BinaryUnmarshaler = (*ExpirationEntry)(nil) + _ encoding.BinaryMarshaler = (*TtlEntry)(nil) + _ encoding.BinaryUnmarshaler = (*TtlEntry)(nil) ) // xdrType signals that this type is an type representing // representing XDR values defined by this package. -func (s ExpirationEntry) xdrType() {} +func (s TtlEntry) xdrType() {} -var _ xdrType = (*ExpirationEntry)(nil) +var _ xdrType = (*TtlEntry)(nil) // LedgerEntryExtensionV1Ext is an XDR NestedUnion defines as: // @@ -8320,8 +8320,8 @@ var _ xdrType = (*LedgerEntryExtensionV1)(nil) // ContractCodeEntry contractCode; // case CONFIG_SETTING: // ConfigSettingEntry configSetting; -// case EXPIRATION: -// ExpirationEntry expiration; +// case TTL: +// TTLEntry ttl; // } type LedgerEntryData struct { Type LedgerEntryType @@ -8334,7 +8334,7 @@ type LedgerEntryData struct { ContractData *ContractDataEntry ContractCode *ContractCodeEntry ConfigSetting *ConfigSettingEntry - Expiration *ExpirationEntry + Ttl *TtlEntry } // SwitchFieldName returns the field name in which this union's @@ -8365,8 +8365,8 @@ func (u LedgerEntryData) ArmForSwitch(sw int32) (string, bool) { return "ContractCode", true case LedgerEntryTypeConfigSetting: return "ConfigSetting", true - case LedgerEntryTypeExpiration: - return "Expiration", true + case LedgerEntryTypeTtl: + return "Ttl", true } return "-", false } @@ -8438,13 +8438,13 @@ func NewLedgerEntryData(aType LedgerEntryType, value interface{}) (result Ledger return } result.ConfigSetting = &tv - case LedgerEntryTypeExpiration: - tv, ok := value.(ExpirationEntry) + case LedgerEntryTypeTtl: + tv, ok := value.(TtlEntry) if !ok { - err = errors.New("invalid value, must be ExpirationEntry") + err = errors.New("invalid value, must be TtlEntry") return } - result.Expiration = &tv + result.Ttl = &tv } return } @@ -8674,25 +8674,25 @@ func (u LedgerEntryData) GetConfigSetting() (result ConfigSettingEntry, ok bool) return } -// MustExpiration retrieves the Expiration value from the union, +// MustTtl retrieves the Ttl value from the union, // panicing if the value is not set. -func (u LedgerEntryData) MustExpiration() ExpirationEntry { - val, ok := u.GetExpiration() +func (u LedgerEntryData) MustTtl() TtlEntry { + val, ok := u.GetTtl() if !ok { - panic("arm Expiration is not set") + panic("arm Ttl is not set") } return val } -// GetExpiration retrieves the Expiration value from the union, +// GetTtl retrieves the Ttl value from the union, // returning ok if the union's switch indicated the value is valid. -func (u LedgerEntryData) GetExpiration() (result ExpirationEntry, ok bool) { +func (u LedgerEntryData) GetTtl() (result TtlEntry, ok bool) { armName, _ := u.ArmForSwitch(int32(u.Type)) - if armName == "Expiration" { - result = *u.Expiration + if armName == "Ttl" { + result = *u.Ttl ok = true } @@ -8751,8 +8751,8 @@ func (u LedgerEntryData) EncodeTo(e *xdr.Encoder) error { return err } return nil - case LedgerEntryTypeExpiration: - if err = (*u.Expiration).EncodeTo(e); err != nil { + case LedgerEntryTypeTtl: + if err = (*u.Ttl).EncodeTo(e); err != nil { return err } return nil @@ -8848,12 +8848,12 @@ func (u *LedgerEntryData) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding ConfigSettingEntry: %w", err) } return n, nil - case LedgerEntryTypeExpiration: - u.Expiration = new(ExpirationEntry) - nTmp, err = (*u.Expiration).DecodeFrom(d, maxDepth) + case LedgerEntryTypeTtl: + u.Ttl = new(TtlEntry) + nTmp, err = (*u.Ttl).DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding ExpirationEntry: %w", err) + return n, fmt.Errorf("decoding TtlEntry: %w", err) } return n, nil } @@ -9064,8 +9064,8 @@ var _ xdrType = (*LedgerEntryExt)(nil) // ContractCodeEntry contractCode; // case CONFIG_SETTING: // ConfigSettingEntry configSetting; -// case EXPIRATION: -// ExpirationEntry expiration; +// case TTL: +// TTLEntry ttl; // } // data; // @@ -9781,19 +9781,19 @@ func (s LedgerKeyConfigSetting) xdrType() {} var _ xdrType = (*LedgerKeyConfigSetting)(nil) -// LedgerKeyExpiration is an XDR NestedStruct defines as: +// LedgerKeyTtl is an XDR NestedStruct defines as: // // struct // { -// // Hash of the LedgerKey that is associated with this ExpirationEntry +// // Hash of the LedgerKey that is associated with this TTLEntry // Hash keyHash; // } -type LedgerKeyExpiration struct { +type LedgerKeyTtl struct { KeyHash Hash } // EncodeTo encodes this value using the Encoder. -func (s *LedgerKeyExpiration) EncodeTo(e *xdr.Encoder) error { +func (s *LedgerKeyTtl) EncodeTo(e *xdr.Encoder) error { var err error if err = s.KeyHash.EncodeTo(e); err != nil { return err @@ -9801,12 +9801,12 @@ func (s *LedgerKeyExpiration) EncodeTo(e *xdr.Encoder) error { return nil } -var _ decoderFrom = (*LedgerKeyExpiration)(nil) +var _ decoderFrom = (*LedgerKeyTtl)(nil) // DecodeFrom decodes this value using the Decoder. -func (s *LedgerKeyExpiration) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { +func (s *LedgerKeyTtl) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { if maxDepth == 0 { - return 0, fmt.Errorf("decoding LedgerKeyExpiration: %w", ErrMaxDecodingDepthReached) + return 0, fmt.Errorf("decoding LedgerKeyTtl: %w", ErrMaxDecodingDepthReached) } maxDepth -= 1 var err error @@ -9820,7 +9820,7 @@ func (s *LedgerKeyExpiration) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, er } // MarshalBinary implements encoding.BinaryMarshaler. -func (s LedgerKeyExpiration) MarshalBinary() ([]byte, error) { +func (s LedgerKeyTtl) MarshalBinary() ([]byte, error) { b := bytes.Buffer{} e := xdr.NewEncoder(&b) err := s.EncodeTo(e) @@ -9828,7 +9828,7 @@ func (s LedgerKeyExpiration) MarshalBinary() ([]byte, error) { } // UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *LedgerKeyExpiration) UnmarshalBinary(inp []byte) error { +func (s *LedgerKeyTtl) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) d := xdr.NewDecoder(r) _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) @@ -9836,15 +9836,15 @@ func (s *LedgerKeyExpiration) UnmarshalBinary(inp []byte) error { } var ( - _ encoding.BinaryMarshaler = (*LedgerKeyExpiration)(nil) - _ encoding.BinaryUnmarshaler = (*LedgerKeyExpiration)(nil) + _ encoding.BinaryMarshaler = (*LedgerKeyTtl)(nil) + _ encoding.BinaryUnmarshaler = (*LedgerKeyTtl)(nil) ) // xdrType signals that this type is an type representing // representing XDR values defined by this package. -func (s LedgerKeyExpiration) xdrType() {} +func (s LedgerKeyTtl) xdrType() {} -var _ xdrType = (*LedgerKeyExpiration)(nil) +var _ xdrType = (*LedgerKeyTtl)(nil) // LedgerKey is an XDR Union defines as: // @@ -9905,12 +9905,12 @@ var _ xdrType = (*LedgerKeyExpiration)(nil) // { // ConfigSettingID configSettingID; // } configSetting; -// case EXPIRATION: +// case TTL: // struct // { -// // Hash of the LedgerKey that is associated with this ExpirationEntry +// // Hash of the LedgerKey that is associated with this TTLEntry // Hash keyHash; -// } expiration; +// } ttl; // }; type LedgerKey struct { Type LedgerEntryType @@ -9923,7 +9923,7 @@ type LedgerKey struct { ContractData *LedgerKeyContractData ContractCode *LedgerKeyContractCode ConfigSetting *LedgerKeyConfigSetting - Expiration *LedgerKeyExpiration + Ttl *LedgerKeyTtl } // SwitchFieldName returns the field name in which this union's @@ -9954,8 +9954,8 @@ func (u LedgerKey) ArmForSwitch(sw int32) (string, bool) { return "ContractCode", true case LedgerEntryTypeConfigSetting: return "ConfigSetting", true - case LedgerEntryTypeExpiration: - return "Expiration", true + case LedgerEntryTypeTtl: + return "Ttl", true } return "-", false } @@ -10027,13 +10027,13 @@ func NewLedgerKey(aType LedgerEntryType, value interface{}) (result LedgerKey, e return } result.ConfigSetting = &tv - case LedgerEntryTypeExpiration: - tv, ok := value.(LedgerKeyExpiration) + case LedgerEntryTypeTtl: + tv, ok := value.(LedgerKeyTtl) if !ok { - err = errors.New("invalid value, must be LedgerKeyExpiration") + err = errors.New("invalid value, must be LedgerKeyTtl") return } - result.Expiration = &tv + result.Ttl = &tv } return } @@ -10263,25 +10263,25 @@ func (u LedgerKey) GetConfigSetting() (result LedgerKeyConfigSetting, ok bool) { return } -// MustExpiration retrieves the Expiration value from the union, +// MustTtl retrieves the Ttl value from the union, // panicing if the value is not set. -func (u LedgerKey) MustExpiration() LedgerKeyExpiration { - val, ok := u.GetExpiration() +func (u LedgerKey) MustTtl() LedgerKeyTtl { + val, ok := u.GetTtl() if !ok { - panic("arm Expiration is not set") + panic("arm Ttl is not set") } return val } -// GetExpiration retrieves the Expiration value from the union, +// GetTtl retrieves the Ttl value from the union, // returning ok if the union's switch indicated the value is valid. -func (u LedgerKey) GetExpiration() (result LedgerKeyExpiration, ok bool) { +func (u LedgerKey) GetTtl() (result LedgerKeyTtl, ok bool) { armName, _ := u.ArmForSwitch(int32(u.Type)) - if armName == "Expiration" { - result = *u.Expiration + if armName == "Ttl" { + result = *u.Ttl ok = true } @@ -10340,8 +10340,8 @@ func (u LedgerKey) EncodeTo(e *xdr.Encoder) error { return err } return nil - case LedgerEntryTypeExpiration: - if err = (*u.Expiration).EncodeTo(e); err != nil { + case LedgerEntryTypeTtl: + if err = (*u.Ttl).EncodeTo(e); err != nil { return err } return nil @@ -10437,12 +10437,12 @@ func (u *LedgerKey) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding LedgerKeyConfigSetting: %w", err) } return n, nil - case LedgerEntryTypeExpiration: - u.Expiration = new(LedgerKeyExpiration) - nTmp, err = (*u.Expiration).DecodeFrom(d, maxDepth) + case LedgerEntryTypeTtl: + u.Ttl = new(LedgerKeyTtl) + nTmp, err = (*u.Ttl).DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding LedgerKeyExpiration: %w", err) + return n, fmt.Errorf("decoding LedgerKeyTtl: %w", err) } return n, nil } @@ -17133,170 +17133,8 @@ var _ xdrType = (*LedgerCloseMetaV0)(nil) // // struct LedgerCloseMetaV1 // { -// LedgerHeaderHistoryEntry ledgerHeader; -// -// GeneralizedTransactionSet txSet; -// -// // NB: transactions are sorted in apply order here -// // fees for all transactions are processed first -// // followed by applying transactions -// TransactionResultMeta txProcessing<>; -// -// // upgrades are applied last -// UpgradeEntryMeta upgradesProcessing<>; -// -// // other misc information attached to the ledger close -// SCPHistoryEntry scpInfo<>; -// }; -type LedgerCloseMetaV1 struct { - LedgerHeader LedgerHeaderHistoryEntry - TxSet GeneralizedTransactionSet - TxProcessing []TransactionResultMeta - UpgradesProcessing []UpgradeEntryMeta - ScpInfo []ScpHistoryEntry -} - -// EncodeTo encodes this value using the Encoder. -func (s *LedgerCloseMetaV1) EncodeTo(e *xdr.Encoder) error { - var err error - if err = s.LedgerHeader.EncodeTo(e); err != nil { - return err - } - if err = s.TxSet.EncodeTo(e); err != nil { - return err - } - if _, err = e.EncodeUint(uint32(len(s.TxProcessing))); err != nil { - return err - } - for i := 0; i < len(s.TxProcessing); i++ { - if err = s.TxProcessing[i].EncodeTo(e); err != nil { - return err - } - } - if _, err = e.EncodeUint(uint32(len(s.UpgradesProcessing))); err != nil { - return err - } - for i := 0; i < len(s.UpgradesProcessing); i++ { - if err = s.UpgradesProcessing[i].EncodeTo(e); err != nil { - return err - } - } - if _, err = e.EncodeUint(uint32(len(s.ScpInfo))); err != nil { - return err - } - for i := 0; i < len(s.ScpInfo); i++ { - if err = s.ScpInfo[i].EncodeTo(e); err != nil { - return err - } - } - return nil -} - -var _ decoderFrom = (*LedgerCloseMetaV1)(nil) - -// DecodeFrom decodes this value using the Decoder. -func (s *LedgerCloseMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { - if maxDepth == 0 { - return 0, fmt.Errorf("decoding LedgerCloseMetaV1: %w", ErrMaxDecodingDepthReached) - } - maxDepth -= 1 - var err error - var n, nTmp int - nTmp, err = s.LedgerHeader.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding LedgerHeaderHistoryEntry: %w", err) - } - nTmp, err = s.TxSet.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding GeneralizedTransactionSet: %w", err) - } - var l uint32 - l, nTmp, err = d.DecodeUint() - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding TransactionResultMeta: %w", err) - } - s.TxProcessing = nil - if l > 0 { - s.TxProcessing = make([]TransactionResultMeta, l) - for i := uint32(0); i < l; i++ { - nTmp, err = s.TxProcessing[i].DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding TransactionResultMeta: %w", err) - } - } - } - l, nTmp, err = d.DecodeUint() - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding UpgradeEntryMeta: %w", err) - } - s.UpgradesProcessing = nil - if l > 0 { - s.UpgradesProcessing = make([]UpgradeEntryMeta, l) - for i := uint32(0); i < l; i++ { - nTmp, err = s.UpgradesProcessing[i].DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding UpgradeEntryMeta: %w", err) - } - } - } - l, nTmp, err = d.DecodeUint() - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding ScpHistoryEntry: %w", err) - } - s.ScpInfo = nil - if l > 0 { - s.ScpInfo = make([]ScpHistoryEntry, l) - for i := uint32(0); i < l; i++ { - nTmp, err = s.ScpInfo[i].DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding ScpHistoryEntry: %w", err) - } - } - } - return n, nil -} - -// MarshalBinary implements encoding.BinaryMarshaler. -func (s LedgerCloseMetaV1) MarshalBinary() ([]byte, error) { - b := bytes.Buffer{} - e := xdr.NewEncoder(&b) - err := s.EncodeTo(e) - return b.Bytes(), err -} - -// UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *LedgerCloseMetaV1) UnmarshalBinary(inp []byte) error { - r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) - return err -} - -var ( - _ encoding.BinaryMarshaler = (*LedgerCloseMetaV1)(nil) - _ encoding.BinaryUnmarshaler = (*LedgerCloseMetaV1)(nil) -) - -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. -func (s LedgerCloseMetaV1) xdrType() {} - -var _ xdrType = (*LedgerCloseMetaV1)(nil) - -// LedgerCloseMetaV2 is an XDR Struct defines as: -// -// struct LedgerCloseMetaV2 -// { -// // We forgot to add an ExtensionPoint in v1 but at least -// // we can add one now in v2. +// // We forgot to add an ExtensionPoint in v0 but at least +// // we can add one now in v1. // ExtensionPoint ext; // // LedgerHeaderHistoryEntry ledgerHeader; @@ -17318,14 +17156,14 @@ var _ xdrType = (*LedgerCloseMetaV1)(nil) // // systems calculating storage fees correctly. // uint64 totalByteSizeOfBucketList; // -// // Expired temp keys that are being evicted at this ledger. +// // Temp keys that are being evicted at this ledger. // LedgerKey evictedTemporaryLedgerKeys<>; // -// // Expired restorable ledger entries that are being +// // Archived restorable ledger entries that are being // // evicted at this ledger. // LedgerEntry evictedPersistentLedgerEntries<>; // }; -type LedgerCloseMetaV2 struct { +type LedgerCloseMetaV1 struct { Ext ExtensionPoint LedgerHeader LedgerHeaderHistoryEntry TxSet GeneralizedTransactionSet @@ -17338,7 +17176,7 @@ type LedgerCloseMetaV2 struct { } // EncodeTo encodes this value using the Encoder. -func (s *LedgerCloseMetaV2) EncodeTo(e *xdr.Encoder) error { +func (s *LedgerCloseMetaV1) EncodeTo(e *xdr.Encoder) error { var err error if err = s.Ext.EncodeTo(e); err != nil { return err @@ -17395,12 +17233,12 @@ func (s *LedgerCloseMetaV2) EncodeTo(e *xdr.Encoder) error { return nil } -var _ decoderFrom = (*LedgerCloseMetaV2)(nil) +var _ decoderFrom = (*LedgerCloseMetaV1)(nil) // DecodeFrom decodes this value using the Decoder. -func (s *LedgerCloseMetaV2) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { +func (s *LedgerCloseMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { if maxDepth == 0 { - return 0, fmt.Errorf("decoding LedgerCloseMetaV2: %w", ErrMaxDecodingDepthReached) + return 0, fmt.Errorf("decoding LedgerCloseMetaV1: %w", ErrMaxDecodingDepthReached) } maxDepth -= 1 var err error @@ -17510,7 +17348,7 @@ func (s *LedgerCloseMetaV2) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } // MarshalBinary implements encoding.BinaryMarshaler. -func (s LedgerCloseMetaV2) MarshalBinary() ([]byte, error) { +func (s LedgerCloseMetaV1) MarshalBinary() ([]byte, error) { b := bytes.Buffer{} e := xdr.NewEncoder(&b) err := s.EncodeTo(e) @@ -17518,7 +17356,7 @@ func (s LedgerCloseMetaV2) MarshalBinary() ([]byte, error) { } // UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *LedgerCloseMetaV2) UnmarshalBinary(inp []byte) error { +func (s *LedgerCloseMetaV1) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) d := xdr.NewDecoder(r) _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) @@ -17526,15 +17364,15 @@ func (s *LedgerCloseMetaV2) UnmarshalBinary(inp []byte) error { } var ( - _ encoding.BinaryMarshaler = (*LedgerCloseMetaV2)(nil) - _ encoding.BinaryUnmarshaler = (*LedgerCloseMetaV2)(nil) + _ encoding.BinaryMarshaler = (*LedgerCloseMetaV1)(nil) + _ encoding.BinaryUnmarshaler = (*LedgerCloseMetaV1)(nil) ) // xdrType signals that this type is an type representing // representing XDR values defined by this package. -func (s LedgerCloseMetaV2) xdrType() {} +func (s LedgerCloseMetaV1) xdrType() {} -var _ xdrType = (*LedgerCloseMetaV2)(nil) +var _ xdrType = (*LedgerCloseMetaV1)(nil) // LedgerCloseMeta is an XDR Union defines as: // @@ -17544,14 +17382,11 @@ var _ xdrType = (*LedgerCloseMetaV2)(nil) // LedgerCloseMetaV0 v0; // case 1: // LedgerCloseMetaV1 v1; -// case 2: -// LedgerCloseMetaV2 v2; // }; type LedgerCloseMeta struct { V int32 V0 *LedgerCloseMetaV0 V1 *LedgerCloseMetaV1 - V2 *LedgerCloseMetaV2 } // SwitchFieldName returns the field name in which this union's @@ -17568,8 +17403,6 @@ func (u LedgerCloseMeta) ArmForSwitch(sw int32) (string, bool) { return "V0", true case 1: return "V1", true - case 2: - return "V2", true } return "-", false } @@ -17592,13 +17425,6 @@ func NewLedgerCloseMeta(v int32, value interface{}) (result LedgerCloseMeta, err return } result.V1 = &tv - case 2: - tv, ok := value.(LedgerCloseMetaV2) - if !ok { - err = errors.New("invalid value, must be LedgerCloseMetaV2") - return - } - result.V2 = &tv } return } @@ -17653,31 +17479,6 @@ func (u LedgerCloseMeta) GetV1() (result LedgerCloseMetaV1, ok bool) { return } -// MustV2 retrieves the V2 value from the union, -// panicing if the value is not set. -func (u LedgerCloseMeta) MustV2() LedgerCloseMetaV2 { - val, ok := u.GetV2() - - if !ok { - panic("arm V2 is not set") - } - - return val -} - -// GetV2 retrieves the V2 value from the union, -// returning ok if the union's switch indicated the value is valid. -func (u LedgerCloseMeta) GetV2() (result LedgerCloseMetaV2, ok bool) { - armName, _ := u.ArmForSwitch(int32(u.V)) - - if armName == "V2" { - result = *u.V2 - ok = true - } - - return -} - // EncodeTo encodes this value using the Encoder. func (u LedgerCloseMeta) EncodeTo(e *xdr.Encoder) error { var err error @@ -17695,11 +17496,6 @@ func (u LedgerCloseMeta) EncodeTo(e *xdr.Encoder) error { return err } return nil - case 2: - if err = (*u.V2).EncodeTo(e); err != nil { - return err - } - return nil } return fmt.Errorf("V (int32) switch value '%d' is not valid for union LedgerCloseMeta", u.V) } @@ -17736,14 +17532,6 @@ func (u *LedgerCloseMeta) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding LedgerCloseMetaV1: %w", err) } return n, nil - case 2: - u.V2 = new(LedgerCloseMetaV2) - nTmp, err = (*u.V2).DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding LedgerCloseMetaV2: %w", err) - } - return n, nil } return n, fmt.Errorf("union LedgerCloseMeta has invalid V (int32) switch value '%d'", u.V) } @@ -22379,7 +22167,7 @@ var _ xdrType = (*DecoratedSignature)(nil) // LIQUIDITY_POOL_DEPOSIT = 22, // LIQUIDITY_POOL_WITHDRAW = 23, // INVOKE_HOST_FUNCTION = 24, -// BUMP_FOOTPRINT_EXPIRATION = 25, +// EXTEND_FOOTPRINT_TTL = 25, // RESTORE_FOOTPRINT = 26 // }; type OperationType int32 @@ -22410,7 +22198,7 @@ const ( OperationTypeLiquidityPoolDeposit OperationType = 22 OperationTypeLiquidityPoolWithdraw OperationType = 23 OperationTypeInvokeHostFunction OperationType = 24 - OperationTypeBumpFootprintExpiration OperationType = 25 + OperationTypeExtendFootprintTtl OperationType = 25 OperationTypeRestoreFootprint OperationType = 26 ) @@ -22440,7 +22228,7 @@ var operationTypeMap = map[int32]string{ 22: "OperationTypeLiquidityPoolDeposit", 23: "OperationTypeLiquidityPoolWithdraw", 24: "OperationTypeInvokeHostFunction", - 25: "OperationTypeBumpFootprintExpiration", + 25: "OperationTypeExtendFootprintTtl", 26: "OperationTypeRestoreFootprint", } @@ -26857,36 +26645,36 @@ func (s InvokeHostFunctionOp) xdrType() {} var _ xdrType = (*InvokeHostFunctionOp)(nil) -// BumpFootprintExpirationOp is an XDR Struct defines as: +// ExtendFootprintTtlOp is an XDR Struct defines as: // -// struct BumpFootprintExpirationOp +// struct ExtendFootprintTTLOp // { // ExtensionPoint ext; -// uint32 ledgersToExpire; +// uint32 extendTo; // }; -type BumpFootprintExpirationOp struct { - Ext ExtensionPoint - LedgersToExpire Uint32 +type ExtendFootprintTtlOp struct { + Ext ExtensionPoint + ExtendTo Uint32 } // EncodeTo encodes this value using the Encoder. -func (s *BumpFootprintExpirationOp) EncodeTo(e *xdr.Encoder) error { +func (s *ExtendFootprintTtlOp) EncodeTo(e *xdr.Encoder) error { var err error if err = s.Ext.EncodeTo(e); err != nil { return err } - if err = s.LedgersToExpire.EncodeTo(e); err != nil { + if err = s.ExtendTo.EncodeTo(e); err != nil { return err } return nil } -var _ decoderFrom = (*BumpFootprintExpirationOp)(nil) +var _ decoderFrom = (*ExtendFootprintTtlOp)(nil) // DecodeFrom decodes this value using the Decoder. -func (s *BumpFootprintExpirationOp) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { +func (s *ExtendFootprintTtlOp) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { if maxDepth == 0 { - return 0, fmt.Errorf("decoding BumpFootprintExpirationOp: %w", ErrMaxDecodingDepthReached) + return 0, fmt.Errorf("decoding ExtendFootprintTtlOp: %w", ErrMaxDecodingDepthReached) } maxDepth -= 1 var err error @@ -26896,7 +26684,7 @@ func (s *BumpFootprintExpirationOp) DecodeFrom(d *xdr.Decoder, maxDepth uint) (i if err != nil { return n, fmt.Errorf("decoding ExtensionPoint: %w", err) } - nTmp, err = s.LedgersToExpire.DecodeFrom(d, maxDepth) + nTmp, err = s.ExtendTo.DecodeFrom(d, maxDepth) n += nTmp if err != nil { return n, fmt.Errorf("decoding Uint32: %w", err) @@ -26905,7 +26693,7 @@ func (s *BumpFootprintExpirationOp) DecodeFrom(d *xdr.Decoder, maxDepth uint) (i } // MarshalBinary implements encoding.BinaryMarshaler. -func (s BumpFootprintExpirationOp) MarshalBinary() ([]byte, error) { +func (s ExtendFootprintTtlOp) MarshalBinary() ([]byte, error) { b := bytes.Buffer{} e := xdr.NewEncoder(&b) err := s.EncodeTo(e) @@ -26913,7 +26701,7 @@ func (s BumpFootprintExpirationOp) MarshalBinary() ([]byte, error) { } // UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *BumpFootprintExpirationOp) UnmarshalBinary(inp []byte) error { +func (s *ExtendFootprintTtlOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) d := xdr.NewDecoder(r) _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) @@ -26921,15 +26709,15 @@ func (s *BumpFootprintExpirationOp) UnmarshalBinary(inp []byte) error { } var ( - _ encoding.BinaryMarshaler = (*BumpFootprintExpirationOp)(nil) - _ encoding.BinaryUnmarshaler = (*BumpFootprintExpirationOp)(nil) + _ encoding.BinaryMarshaler = (*ExtendFootprintTtlOp)(nil) + _ encoding.BinaryUnmarshaler = (*ExtendFootprintTtlOp)(nil) ) // xdrType signals that this type is an type representing // representing XDR values defined by this package. -func (s BumpFootprintExpirationOp) xdrType() {} +func (s ExtendFootprintTtlOp) xdrType() {} -var _ xdrType = (*BumpFootprintExpirationOp)(nil) +var _ xdrType = (*ExtendFootprintTtlOp)(nil) // RestoreFootprintOp is an XDR Struct defines as: // @@ -27049,8 +26837,8 @@ var _ xdrType = (*RestoreFootprintOp)(nil) // LiquidityPoolWithdrawOp liquidityPoolWithdrawOp; // case INVOKE_HOST_FUNCTION: // InvokeHostFunctionOp invokeHostFunctionOp; -// case BUMP_FOOTPRINT_EXPIRATION: -// BumpFootprintExpirationOp bumpFootprintExpirationOp; +// case EXTEND_FOOTPRINT_TTL: +// ExtendFootprintTTLOp extendFootprintTTLOp; // case RESTORE_FOOTPRINT: // RestoreFootprintOp restoreFootprintOp; // } @@ -27079,7 +26867,7 @@ type OperationBody struct { LiquidityPoolDepositOp *LiquidityPoolDepositOp LiquidityPoolWithdrawOp *LiquidityPoolWithdrawOp InvokeHostFunctionOp *InvokeHostFunctionOp - BumpFootprintExpirationOp *BumpFootprintExpirationOp + ExtendFootprintTtlOp *ExtendFootprintTtlOp RestoreFootprintOp *RestoreFootprintOp } @@ -27143,8 +26931,8 @@ func (u OperationBody) ArmForSwitch(sw int32) (string, bool) { return "LiquidityPoolWithdrawOp", true case OperationTypeInvokeHostFunction: return "InvokeHostFunctionOp", true - case OperationTypeBumpFootprintExpiration: - return "BumpFootprintExpirationOp", true + case OperationTypeExtendFootprintTtl: + return "ExtendFootprintTtlOp", true case OperationTypeRestoreFootprint: return "RestoreFootprintOp", true } @@ -27320,13 +27108,13 @@ func NewOperationBody(aType OperationType, value interface{}) (result OperationB return } result.InvokeHostFunctionOp = &tv - case OperationTypeBumpFootprintExpiration: - tv, ok := value.(BumpFootprintExpirationOp) + case OperationTypeExtendFootprintTtl: + tv, ok := value.(ExtendFootprintTtlOp) if !ok { - err = errors.New("invalid value, must be BumpFootprintExpirationOp") + err = errors.New("invalid value, must be ExtendFootprintTtlOp") return } - result.BumpFootprintExpirationOp = &tv + result.ExtendFootprintTtlOp = &tv case OperationTypeRestoreFootprint: tv, ok := value.(RestoreFootprintOp) if !ok { @@ -27913,25 +27701,25 @@ func (u OperationBody) GetInvokeHostFunctionOp() (result InvokeHostFunctionOp, o return } -// MustBumpFootprintExpirationOp retrieves the BumpFootprintExpirationOp value from the union, +// MustExtendFootprintTtlOp retrieves the ExtendFootprintTtlOp value from the union, // panicing if the value is not set. -func (u OperationBody) MustBumpFootprintExpirationOp() BumpFootprintExpirationOp { - val, ok := u.GetBumpFootprintExpirationOp() +func (u OperationBody) MustExtendFootprintTtlOp() ExtendFootprintTtlOp { + val, ok := u.GetExtendFootprintTtlOp() if !ok { - panic("arm BumpFootprintExpirationOp is not set") + panic("arm ExtendFootprintTtlOp is not set") } return val } -// GetBumpFootprintExpirationOp retrieves the BumpFootprintExpirationOp value from the union, +// GetExtendFootprintTtlOp retrieves the ExtendFootprintTtlOp value from the union, // returning ok if the union's switch indicated the value is valid. -func (u OperationBody) GetBumpFootprintExpirationOp() (result BumpFootprintExpirationOp, ok bool) { +func (u OperationBody) GetExtendFootprintTtlOp() (result ExtendFootprintTtlOp, ok bool) { armName, _ := u.ArmForSwitch(int32(u.Type)) - if armName == "BumpFootprintExpirationOp" { - result = *u.BumpFootprintExpirationOp + if armName == "ExtendFootprintTtlOp" { + result = *u.ExtendFootprintTtlOp ok = true } @@ -28091,8 +27879,8 @@ func (u OperationBody) EncodeTo(e *xdr.Encoder) error { return err } return nil - case OperationTypeBumpFootprintExpiration: - if err = (*u.BumpFootprintExpirationOp).EncodeTo(e); err != nil { + case OperationTypeExtendFootprintTtl: + if err = (*u.ExtendFootprintTtlOp).EncodeTo(e); err != nil { return err } return nil @@ -28311,12 +28099,12 @@ func (u *OperationBody) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding InvokeHostFunctionOp: %w", err) } return n, nil - case OperationTypeBumpFootprintExpiration: - u.BumpFootprintExpirationOp = new(BumpFootprintExpirationOp) - nTmp, err = (*u.BumpFootprintExpirationOp).DecodeFrom(d, maxDepth) + case OperationTypeExtendFootprintTtl: + u.ExtendFootprintTtlOp = new(ExtendFootprintTtlOp) + nTmp, err = (*u.ExtendFootprintTtlOp).DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding BumpFootprintExpirationOp: %w", err) + return n, fmt.Errorf("decoding ExtendFootprintTtlOp: %w", err) } return n, nil case OperationTypeRestoreFootprint: @@ -28419,8 +28207,8 @@ var _ xdrType = (*OperationBody)(nil) // LiquidityPoolWithdrawOp liquidityPoolWithdrawOp; // case INVOKE_HOST_FUNCTION: // InvokeHostFunctionOp invokeHostFunctionOp; -// case BUMP_FOOTPRINT_EXPIRATION: -// BumpFootprintExpirationOp bumpFootprintExpirationOp; +// case EXTEND_FOOTPRINT_TTL: +// ExtendFootprintTTLOp extendFootprintTTLOp; // case RESTORE_FOOTPRINT: // RestoreFootprintOp restoreFootprintOp; // } @@ -30423,13 +30211,21 @@ var _ xdrType = (*SorobanResources)(nil) // { // ExtensionPoint ext; // SorobanResources resources; -// // Portion of transaction `fee` allocated to refundable fees. -// int64 refundableFee; +// // Amount of the transaction `fee` allocated to the Soroban resource fees. +// // The fraction of `resourceFee` corresponding to `resources` specified +// // above is *not* refundable (i.e. fees for instructions, ledger I/O), as +// // well as fees for the transaction size. +// // The remaining part of the fee is refundable and the charged value is +// // based on the actual consumption of refundable resources (events, ledger +// // rent bumps). +// // The `inclusionFee` used for prioritization of the transaction is defined +// // as `tx.fee - resourceFee`. +// int64 resourceFee; // }; type SorobanTransactionData struct { - Ext ExtensionPoint - Resources SorobanResources - RefundableFee Int64 + Ext ExtensionPoint + Resources SorobanResources + ResourceFee Int64 } // EncodeTo encodes this value using the Encoder. @@ -30441,7 +30237,7 @@ func (s *SorobanTransactionData) EncodeTo(e *xdr.Encoder) error { if err = s.Resources.EncodeTo(e); err != nil { return err } - if err = s.RefundableFee.EncodeTo(e); err != nil { + if err = s.ResourceFee.EncodeTo(e); err != nil { return err } return nil @@ -30467,7 +30263,7 @@ func (s *SorobanTransactionData) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, if err != nil { return n, fmt.Errorf("decoding SorobanResources: %w", err) } - nTmp, err = s.RefundableFee.DecodeFrom(d, maxDepth) + nTmp, err = s.ResourceFee.DecodeFrom(d, maxDepth) n += nTmp if err != nil { return n, fmt.Errorf("decoding Int64: %w", err) @@ -40345,7 +40141,7 @@ var _ xdrType = (*LiquidityPoolWithdrawResult)(nil) // INVOKE_HOST_FUNCTION_MALFORMED = -1, // INVOKE_HOST_FUNCTION_TRAPPED = -2, // INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED = -3, -// INVOKE_HOST_FUNCTION_ENTRY_EXPIRED = -4, +// INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED = -4, // INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE = -5 // }; type InvokeHostFunctionResultCode int32 @@ -40355,7 +40151,7 @@ const ( InvokeHostFunctionResultCodeInvokeHostFunctionMalformed InvokeHostFunctionResultCode = -1 InvokeHostFunctionResultCodeInvokeHostFunctionTrapped InvokeHostFunctionResultCode = -2 InvokeHostFunctionResultCodeInvokeHostFunctionResourceLimitExceeded InvokeHostFunctionResultCode = -3 - InvokeHostFunctionResultCodeInvokeHostFunctionEntryExpired InvokeHostFunctionResultCode = -4 + InvokeHostFunctionResultCodeInvokeHostFunctionEntryArchived InvokeHostFunctionResultCode = -4 InvokeHostFunctionResultCodeInvokeHostFunctionInsufficientRefundableFee InvokeHostFunctionResultCode = -5 ) @@ -40364,7 +40160,7 @@ var invokeHostFunctionResultCodeMap = map[int32]string{ -1: "InvokeHostFunctionResultCodeInvokeHostFunctionMalformed", -2: "InvokeHostFunctionResultCodeInvokeHostFunctionTrapped", -3: "InvokeHostFunctionResultCodeInvokeHostFunctionResourceLimitExceeded", - -4: "InvokeHostFunctionResultCodeInvokeHostFunctionEntryExpired", + -4: "InvokeHostFunctionResultCodeInvokeHostFunctionEntryArchived", -5: "InvokeHostFunctionResultCodeInvokeHostFunctionInsufficientRefundableFee", } @@ -40445,7 +40241,7 @@ var _ xdrType = (*InvokeHostFunctionResultCode)(nil) // case INVOKE_HOST_FUNCTION_MALFORMED: // case INVOKE_HOST_FUNCTION_TRAPPED: // case INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED: -// case INVOKE_HOST_FUNCTION_ENTRY_EXPIRED: +// case INVOKE_HOST_FUNCTION_ENTRY_ARCHIVED: // case INVOKE_HOST_FUNCTION_INSUFFICIENT_REFUNDABLE_FEE: // void; // }; @@ -40472,7 +40268,7 @@ func (u InvokeHostFunctionResult) ArmForSwitch(sw int32) (string, bool) { return "", true case InvokeHostFunctionResultCodeInvokeHostFunctionResourceLimitExceeded: return "", true - case InvokeHostFunctionResultCodeInvokeHostFunctionEntryExpired: + case InvokeHostFunctionResultCodeInvokeHostFunctionEntryArchived: return "", true case InvokeHostFunctionResultCodeInvokeHostFunctionInsufficientRefundableFee: return "", true @@ -40497,7 +40293,7 @@ func NewInvokeHostFunctionResult(code InvokeHostFunctionResultCode, value interf // void case InvokeHostFunctionResultCodeInvokeHostFunctionResourceLimitExceeded: // void - case InvokeHostFunctionResultCodeInvokeHostFunctionEntryExpired: + case InvokeHostFunctionResultCodeInvokeHostFunctionEntryArchived: // void case InvokeHostFunctionResultCodeInvokeHostFunctionInsufficientRefundableFee: // void @@ -40551,7 +40347,7 @@ func (u InvokeHostFunctionResult) EncodeTo(e *xdr.Encoder) error { case InvokeHostFunctionResultCodeInvokeHostFunctionResourceLimitExceeded: // Void return nil - case InvokeHostFunctionResultCodeInvokeHostFunctionEntryExpired: + case InvokeHostFunctionResultCodeInvokeHostFunctionEntryArchived: // Void return nil case InvokeHostFunctionResultCodeInvokeHostFunctionInsufficientRefundableFee: @@ -40594,7 +40390,7 @@ func (u *InvokeHostFunctionResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) (in case InvokeHostFunctionResultCodeInvokeHostFunctionResourceLimitExceeded: // Void return n, nil - case InvokeHostFunctionResultCodeInvokeHostFunctionEntryExpired: + case InvokeHostFunctionResultCodeInvokeHostFunctionEntryArchived: // Void return n, nil case InvokeHostFunctionResultCodeInvokeHostFunctionInsufficientRefundableFee: @@ -40631,77 +40427,77 @@ func (s InvokeHostFunctionResult) xdrType() {} var _ xdrType = (*InvokeHostFunctionResult)(nil) -// BumpFootprintExpirationResultCode is an XDR Enum defines as: +// ExtendFootprintTtlResultCode is an XDR Enum defines as: // -// enum BumpFootprintExpirationResultCode +// enum ExtendFootprintTTLResultCode // { // // codes considered as "success" for the operation -// BUMP_FOOTPRINT_EXPIRATION_SUCCESS = 0, +// EXTEND_FOOTPRINT_TTL_SUCCESS = 0, // // // codes considered as "failure" for the operation -// BUMP_FOOTPRINT_EXPIRATION_MALFORMED = -1, -// BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED = -2, -// BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE = -3 +// EXTEND_FOOTPRINT_TTL_MALFORMED = -1, +// EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED = -2, +// EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE = -3 // }; -type BumpFootprintExpirationResultCode int32 +type ExtendFootprintTtlResultCode int32 const ( - BumpFootprintExpirationResultCodeBumpFootprintExpirationSuccess BumpFootprintExpirationResultCode = 0 - BumpFootprintExpirationResultCodeBumpFootprintExpirationMalformed BumpFootprintExpirationResultCode = -1 - BumpFootprintExpirationResultCodeBumpFootprintExpirationResourceLimitExceeded BumpFootprintExpirationResultCode = -2 - BumpFootprintExpirationResultCodeBumpFootprintExpirationInsufficientRefundableFee BumpFootprintExpirationResultCode = -3 + ExtendFootprintTtlResultCodeExtendFootprintTtlSuccess ExtendFootprintTtlResultCode = 0 + ExtendFootprintTtlResultCodeExtendFootprintTtlMalformed ExtendFootprintTtlResultCode = -1 + ExtendFootprintTtlResultCodeExtendFootprintTtlResourceLimitExceeded ExtendFootprintTtlResultCode = -2 + ExtendFootprintTtlResultCodeExtendFootprintTtlInsufficientRefundableFee ExtendFootprintTtlResultCode = -3 ) -var bumpFootprintExpirationResultCodeMap = map[int32]string{ - 0: "BumpFootprintExpirationResultCodeBumpFootprintExpirationSuccess", - -1: "BumpFootprintExpirationResultCodeBumpFootprintExpirationMalformed", - -2: "BumpFootprintExpirationResultCodeBumpFootprintExpirationResourceLimitExceeded", - -3: "BumpFootprintExpirationResultCodeBumpFootprintExpirationInsufficientRefundableFee", +var extendFootprintTtlResultCodeMap = map[int32]string{ + 0: "ExtendFootprintTtlResultCodeExtendFootprintTtlSuccess", + -1: "ExtendFootprintTtlResultCodeExtendFootprintTtlMalformed", + -2: "ExtendFootprintTtlResultCodeExtendFootprintTtlResourceLimitExceeded", + -3: "ExtendFootprintTtlResultCodeExtendFootprintTtlInsufficientRefundableFee", } // ValidEnum validates a proposed value for this enum. Implements -// the Enum interface for BumpFootprintExpirationResultCode -func (e BumpFootprintExpirationResultCode) ValidEnum(v int32) bool { - _, ok := bumpFootprintExpirationResultCodeMap[v] +// the Enum interface for ExtendFootprintTtlResultCode +func (e ExtendFootprintTtlResultCode) ValidEnum(v int32) bool { + _, ok := extendFootprintTtlResultCodeMap[v] return ok } // String returns the name of `e` -func (e BumpFootprintExpirationResultCode) String() string { - name, _ := bumpFootprintExpirationResultCodeMap[int32(e)] +func (e ExtendFootprintTtlResultCode) String() string { + name, _ := extendFootprintTtlResultCodeMap[int32(e)] return name } // EncodeTo encodes this value using the Encoder. -func (e BumpFootprintExpirationResultCode) EncodeTo(enc *xdr.Encoder) error { - if _, ok := bumpFootprintExpirationResultCodeMap[int32(e)]; !ok { - return fmt.Errorf("'%d' is not a valid BumpFootprintExpirationResultCode enum value", e) +func (e ExtendFootprintTtlResultCode) EncodeTo(enc *xdr.Encoder) error { + if _, ok := extendFootprintTtlResultCodeMap[int32(e)]; !ok { + return fmt.Errorf("'%d' is not a valid ExtendFootprintTtlResultCode enum value", e) } _, err := enc.EncodeInt(int32(e)) return err } -var _ decoderFrom = (*BumpFootprintExpirationResultCode)(nil) +var _ decoderFrom = (*ExtendFootprintTtlResultCode)(nil) // DecodeFrom decodes this value using the Decoder. -func (e *BumpFootprintExpirationResultCode) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { +func (e *ExtendFootprintTtlResultCode) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { if maxDepth == 0 { - return 0, fmt.Errorf("decoding BumpFootprintExpirationResultCode: %w", ErrMaxDecodingDepthReached) + return 0, fmt.Errorf("decoding ExtendFootprintTtlResultCode: %w", ErrMaxDecodingDepthReached) } maxDepth -= 1 v, n, err := d.DecodeInt() if err != nil { - return n, fmt.Errorf("decoding BumpFootprintExpirationResultCode: %w", err) + return n, fmt.Errorf("decoding ExtendFootprintTtlResultCode: %w", err) } - if _, ok := bumpFootprintExpirationResultCodeMap[v]; !ok { - return n, fmt.Errorf("'%d' is not a valid BumpFootprintExpirationResultCode enum value", v) + if _, ok := extendFootprintTtlResultCodeMap[v]; !ok { + return n, fmt.Errorf("'%d' is not a valid ExtendFootprintTtlResultCode enum value", v) } - *e = BumpFootprintExpirationResultCode(v) + *e = ExtendFootprintTtlResultCode(v) return n, nil } // MarshalBinary implements encoding.BinaryMarshaler. -func (s BumpFootprintExpirationResultCode) MarshalBinary() ([]byte, error) { +func (s ExtendFootprintTtlResultCode) MarshalBinary() ([]byte, error) { b := bytes.Buffer{} e := xdr.NewEncoder(&b) err := s.EncodeTo(e) @@ -40709,7 +40505,7 @@ func (s BumpFootprintExpirationResultCode) MarshalBinary() ([]byte, error) { } // UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *BumpFootprintExpirationResultCode) UnmarshalBinary(inp []byte) error { +func (s *ExtendFootprintTtlResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) d := xdr.NewDecoder(r) _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) @@ -40717,98 +40513,98 @@ func (s *BumpFootprintExpirationResultCode) UnmarshalBinary(inp []byte) error { } var ( - _ encoding.BinaryMarshaler = (*BumpFootprintExpirationResultCode)(nil) - _ encoding.BinaryUnmarshaler = (*BumpFootprintExpirationResultCode)(nil) + _ encoding.BinaryMarshaler = (*ExtendFootprintTtlResultCode)(nil) + _ encoding.BinaryUnmarshaler = (*ExtendFootprintTtlResultCode)(nil) ) // xdrType signals that this type is an type representing // representing XDR values defined by this package. -func (s BumpFootprintExpirationResultCode) xdrType() {} +func (s ExtendFootprintTtlResultCode) xdrType() {} -var _ xdrType = (*BumpFootprintExpirationResultCode)(nil) +var _ xdrType = (*ExtendFootprintTtlResultCode)(nil) -// BumpFootprintExpirationResult is an XDR Union defines as: +// ExtendFootprintTtlResult is an XDR Union defines as: // -// union BumpFootprintExpirationResult switch (BumpFootprintExpirationResultCode code) +// union ExtendFootprintTTLResult switch (ExtendFootprintTTLResultCode code) // { -// case BUMP_FOOTPRINT_EXPIRATION_SUCCESS: +// case EXTEND_FOOTPRINT_TTL_SUCCESS: // void; -// case BUMP_FOOTPRINT_EXPIRATION_MALFORMED: -// case BUMP_FOOTPRINT_EXPIRATION_RESOURCE_LIMIT_EXCEEDED: -// case BUMP_FOOTPRINT_EXPIRATION_INSUFFICIENT_REFUNDABLE_FEE: +// case EXTEND_FOOTPRINT_TTL_MALFORMED: +// case EXTEND_FOOTPRINT_TTL_RESOURCE_LIMIT_EXCEEDED: +// case EXTEND_FOOTPRINT_TTL_INSUFFICIENT_REFUNDABLE_FEE: // void; // }; -type BumpFootprintExpirationResult struct { - Code BumpFootprintExpirationResultCode +type ExtendFootprintTtlResult struct { + Code ExtendFootprintTtlResultCode } // SwitchFieldName returns the field name in which this union's // discriminant is stored -func (u BumpFootprintExpirationResult) SwitchFieldName() string { +func (u ExtendFootprintTtlResult) SwitchFieldName() string { return "Code" } // ArmForSwitch returns which field name should be used for storing -// the value for an instance of BumpFootprintExpirationResult -func (u BumpFootprintExpirationResult) ArmForSwitch(sw int32) (string, bool) { - switch BumpFootprintExpirationResultCode(sw) { - case BumpFootprintExpirationResultCodeBumpFootprintExpirationSuccess: +// the value for an instance of ExtendFootprintTtlResult +func (u ExtendFootprintTtlResult) ArmForSwitch(sw int32) (string, bool) { + switch ExtendFootprintTtlResultCode(sw) { + case ExtendFootprintTtlResultCodeExtendFootprintTtlSuccess: return "", true - case BumpFootprintExpirationResultCodeBumpFootprintExpirationMalformed: + case ExtendFootprintTtlResultCodeExtendFootprintTtlMalformed: return "", true - case BumpFootprintExpirationResultCodeBumpFootprintExpirationResourceLimitExceeded: + case ExtendFootprintTtlResultCodeExtendFootprintTtlResourceLimitExceeded: return "", true - case BumpFootprintExpirationResultCodeBumpFootprintExpirationInsufficientRefundableFee: + case ExtendFootprintTtlResultCodeExtendFootprintTtlInsufficientRefundableFee: return "", true } return "-", false } -// NewBumpFootprintExpirationResult creates a new BumpFootprintExpirationResult. -func NewBumpFootprintExpirationResult(code BumpFootprintExpirationResultCode, value interface{}) (result BumpFootprintExpirationResult, err error) { +// NewExtendFootprintTtlResult creates a new ExtendFootprintTtlResult. +func NewExtendFootprintTtlResult(code ExtendFootprintTtlResultCode, value interface{}) (result ExtendFootprintTtlResult, err error) { result.Code = code - switch BumpFootprintExpirationResultCode(code) { - case BumpFootprintExpirationResultCodeBumpFootprintExpirationSuccess: + switch ExtendFootprintTtlResultCode(code) { + case ExtendFootprintTtlResultCodeExtendFootprintTtlSuccess: // void - case BumpFootprintExpirationResultCodeBumpFootprintExpirationMalformed: + case ExtendFootprintTtlResultCodeExtendFootprintTtlMalformed: // void - case BumpFootprintExpirationResultCodeBumpFootprintExpirationResourceLimitExceeded: + case ExtendFootprintTtlResultCodeExtendFootprintTtlResourceLimitExceeded: // void - case BumpFootprintExpirationResultCodeBumpFootprintExpirationInsufficientRefundableFee: + case ExtendFootprintTtlResultCodeExtendFootprintTtlInsufficientRefundableFee: // void } return } // EncodeTo encodes this value using the Encoder. -func (u BumpFootprintExpirationResult) EncodeTo(e *xdr.Encoder) error { +func (u ExtendFootprintTtlResult) EncodeTo(e *xdr.Encoder) error { var err error if err = u.Code.EncodeTo(e); err != nil { return err } - switch BumpFootprintExpirationResultCode(u.Code) { - case BumpFootprintExpirationResultCodeBumpFootprintExpirationSuccess: + switch ExtendFootprintTtlResultCode(u.Code) { + case ExtendFootprintTtlResultCodeExtendFootprintTtlSuccess: // Void return nil - case BumpFootprintExpirationResultCodeBumpFootprintExpirationMalformed: + case ExtendFootprintTtlResultCodeExtendFootprintTtlMalformed: // Void return nil - case BumpFootprintExpirationResultCodeBumpFootprintExpirationResourceLimitExceeded: + case ExtendFootprintTtlResultCodeExtendFootprintTtlResourceLimitExceeded: // Void return nil - case BumpFootprintExpirationResultCodeBumpFootprintExpirationInsufficientRefundableFee: + case ExtendFootprintTtlResultCodeExtendFootprintTtlInsufficientRefundableFee: // Void return nil } - return fmt.Errorf("Code (BumpFootprintExpirationResultCode) switch value '%d' is not valid for union BumpFootprintExpirationResult", u.Code) + return fmt.Errorf("Code (ExtendFootprintTtlResultCode) switch value '%d' is not valid for union ExtendFootprintTtlResult", u.Code) } -var _ decoderFrom = (*BumpFootprintExpirationResult)(nil) +var _ decoderFrom = (*ExtendFootprintTtlResult)(nil) // DecodeFrom decodes this value using the Decoder. -func (u *BumpFootprintExpirationResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { +func (u *ExtendFootprintTtlResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { if maxDepth == 0 { - return 0, fmt.Errorf("decoding BumpFootprintExpirationResult: %w", ErrMaxDecodingDepthReached) + return 0, fmt.Errorf("decoding ExtendFootprintTtlResult: %w", ErrMaxDecodingDepthReached) } maxDepth -= 1 var err error @@ -40816,27 +40612,27 @@ func (u *BumpFootprintExpirationResult) DecodeFrom(d *xdr.Decoder, maxDepth uint nTmp, err = u.Code.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding BumpFootprintExpirationResultCode: %w", err) + return n, fmt.Errorf("decoding ExtendFootprintTtlResultCode: %w", err) } - switch BumpFootprintExpirationResultCode(u.Code) { - case BumpFootprintExpirationResultCodeBumpFootprintExpirationSuccess: + switch ExtendFootprintTtlResultCode(u.Code) { + case ExtendFootprintTtlResultCodeExtendFootprintTtlSuccess: // Void return n, nil - case BumpFootprintExpirationResultCodeBumpFootprintExpirationMalformed: + case ExtendFootprintTtlResultCodeExtendFootprintTtlMalformed: // Void return n, nil - case BumpFootprintExpirationResultCodeBumpFootprintExpirationResourceLimitExceeded: + case ExtendFootprintTtlResultCodeExtendFootprintTtlResourceLimitExceeded: // Void return n, nil - case BumpFootprintExpirationResultCodeBumpFootprintExpirationInsufficientRefundableFee: + case ExtendFootprintTtlResultCodeExtendFootprintTtlInsufficientRefundableFee: // Void return n, nil } - return n, fmt.Errorf("union BumpFootprintExpirationResult has invalid Code (BumpFootprintExpirationResultCode) switch value '%d'", u.Code) + return n, fmt.Errorf("union ExtendFootprintTtlResult has invalid Code (ExtendFootprintTtlResultCode) switch value '%d'", u.Code) } // MarshalBinary implements encoding.BinaryMarshaler. -func (s BumpFootprintExpirationResult) MarshalBinary() ([]byte, error) { +func (s ExtendFootprintTtlResult) MarshalBinary() ([]byte, error) { b := bytes.Buffer{} e := xdr.NewEncoder(&b) err := s.EncodeTo(e) @@ -40844,7 +40640,7 @@ func (s BumpFootprintExpirationResult) MarshalBinary() ([]byte, error) { } // UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *BumpFootprintExpirationResult) UnmarshalBinary(inp []byte) error { +func (s *ExtendFootprintTtlResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) d := xdr.NewDecoder(r) _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) @@ -40852,15 +40648,15 @@ func (s *BumpFootprintExpirationResult) UnmarshalBinary(inp []byte) error { } var ( - _ encoding.BinaryMarshaler = (*BumpFootprintExpirationResult)(nil) - _ encoding.BinaryUnmarshaler = (*BumpFootprintExpirationResult)(nil) + _ encoding.BinaryMarshaler = (*ExtendFootprintTtlResult)(nil) + _ encoding.BinaryUnmarshaler = (*ExtendFootprintTtlResult)(nil) ) // xdrType signals that this type is an type representing // representing XDR values defined by this package. -func (s BumpFootprintExpirationResult) xdrType() {} +func (s ExtendFootprintTtlResult) xdrType() {} -var _ xdrType = (*BumpFootprintExpirationResult)(nil) +var _ xdrType = (*ExtendFootprintTtlResult)(nil) // RestoreFootprintResultCode is an XDR Enum defines as: // @@ -41250,8 +41046,8 @@ var _ xdrType = (*OperationResultCode)(nil) // LiquidityPoolWithdrawResult liquidityPoolWithdrawResult; // case INVOKE_HOST_FUNCTION: // InvokeHostFunctionResult invokeHostFunctionResult; -// case BUMP_FOOTPRINT_EXPIRATION: -// BumpFootprintExpirationResult bumpFootprintExpirationResult; +// case EXTEND_FOOTPRINT_TTL: +// ExtendFootprintTTLResult extendFootprintTTLResult; // case RESTORE_FOOTPRINT: // RestoreFootprintResult restoreFootprintResult; // } @@ -41282,7 +41078,7 @@ type OperationResultTr struct { LiquidityPoolDepositResult *LiquidityPoolDepositResult LiquidityPoolWithdrawResult *LiquidityPoolWithdrawResult InvokeHostFunctionResult *InvokeHostFunctionResult - BumpFootprintExpirationResult *BumpFootprintExpirationResult + ExtendFootprintTtlResult *ExtendFootprintTtlResult RestoreFootprintResult *RestoreFootprintResult } @@ -41346,8 +41142,8 @@ func (u OperationResultTr) ArmForSwitch(sw int32) (string, bool) { return "LiquidityPoolWithdrawResult", true case OperationTypeInvokeHostFunction: return "InvokeHostFunctionResult", true - case OperationTypeBumpFootprintExpiration: - return "BumpFootprintExpirationResult", true + case OperationTypeExtendFootprintTtl: + return "ExtendFootprintTtlResult", true case OperationTypeRestoreFootprint: return "RestoreFootprintResult", true } @@ -41533,13 +41329,13 @@ func NewOperationResultTr(aType OperationType, value interface{}) (result Operat return } result.InvokeHostFunctionResult = &tv - case OperationTypeBumpFootprintExpiration: - tv, ok := value.(BumpFootprintExpirationResult) + case OperationTypeExtendFootprintTtl: + tv, ok := value.(ExtendFootprintTtlResult) if !ok { - err = errors.New("invalid value, must be BumpFootprintExpirationResult") + err = errors.New("invalid value, must be ExtendFootprintTtlResult") return } - result.BumpFootprintExpirationResult = &tv + result.ExtendFootprintTtlResult = &tv case OperationTypeRestoreFootprint: tv, ok := value.(RestoreFootprintResult) if !ok { @@ -42176,25 +41972,25 @@ func (u OperationResultTr) GetInvokeHostFunctionResult() (result InvokeHostFunct return } -// MustBumpFootprintExpirationResult retrieves the BumpFootprintExpirationResult value from the union, +// MustExtendFootprintTtlResult retrieves the ExtendFootprintTtlResult value from the union, // panicing if the value is not set. -func (u OperationResultTr) MustBumpFootprintExpirationResult() BumpFootprintExpirationResult { - val, ok := u.GetBumpFootprintExpirationResult() +func (u OperationResultTr) MustExtendFootprintTtlResult() ExtendFootprintTtlResult { + val, ok := u.GetExtendFootprintTtlResult() if !ok { - panic("arm BumpFootprintExpirationResult is not set") + panic("arm ExtendFootprintTtlResult is not set") } return val } -// GetBumpFootprintExpirationResult retrieves the BumpFootprintExpirationResult value from the union, +// GetExtendFootprintTtlResult retrieves the ExtendFootprintTtlResult value from the union, // returning ok if the union's switch indicated the value is valid. -func (u OperationResultTr) GetBumpFootprintExpirationResult() (result BumpFootprintExpirationResult, ok bool) { +func (u OperationResultTr) GetExtendFootprintTtlResult() (result ExtendFootprintTtlResult, ok bool) { armName, _ := u.ArmForSwitch(int32(u.Type)) - if armName == "BumpFootprintExpirationResult" { - result = *u.BumpFootprintExpirationResult + if armName == "ExtendFootprintTtlResult" { + result = *u.ExtendFootprintTtlResult ok = true } @@ -42358,8 +42154,8 @@ func (u OperationResultTr) EncodeTo(e *xdr.Encoder) error { return err } return nil - case OperationTypeBumpFootprintExpiration: - if err = (*u.BumpFootprintExpirationResult).EncodeTo(e); err != nil { + case OperationTypeExtendFootprintTtl: + if err = (*u.ExtendFootprintTtlResult).EncodeTo(e); err != nil { return err } return nil @@ -42588,12 +42384,12 @@ func (u *OperationResultTr) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding InvokeHostFunctionResult: %w", err) } return n, nil - case OperationTypeBumpFootprintExpiration: - u.BumpFootprintExpirationResult = new(BumpFootprintExpirationResult) - nTmp, err = (*u.BumpFootprintExpirationResult).DecodeFrom(d, maxDepth) + case OperationTypeExtendFootprintTtl: + u.ExtendFootprintTtlResult = new(ExtendFootprintTtlResult) + nTmp, err = (*u.ExtendFootprintTtlResult).DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding BumpFootprintExpirationResult: %w", err) + return n, fmt.Errorf("decoding ExtendFootprintTtlResult: %w", err) } return n, nil case OperationTypeRestoreFootprint: @@ -42692,8 +42488,8 @@ var _ xdrType = (*OperationResultTr)(nil) // LiquidityPoolWithdrawResult liquidityPoolWithdrawResult; // case INVOKE_HOST_FUNCTION: // InvokeHostFunctionResult invokeHostFunctionResult; -// case BUMP_FOOTPRINT_EXPIRATION: -// BumpFootprintExpirationResult bumpFootprintExpirationResult; +// case EXTEND_FOOTPRINT_TTL: +// ExtendFootprintTTLResult extendFootprintTTLResult; // case RESTORE_FOOTPRINT: // RestoreFootprintResult restoreFootprintResult; // } @@ -51028,18 +50824,18 @@ var _ xdrType = (*Int256Parts)(nil) // enum ContractExecutableType // { // CONTRACT_EXECUTABLE_WASM = 0, -// CONTRACT_EXECUTABLE_TOKEN = 1 +// CONTRACT_EXECUTABLE_STELLAR_ASSET = 1 // }; type ContractExecutableType int32 const ( - ContractExecutableTypeContractExecutableWasm ContractExecutableType = 0 - ContractExecutableTypeContractExecutableToken ContractExecutableType = 1 + ContractExecutableTypeContractExecutableWasm ContractExecutableType = 0 + ContractExecutableTypeContractExecutableStellarAsset ContractExecutableType = 1 ) var contractExecutableTypeMap = map[int32]string{ 0: "ContractExecutableTypeContractExecutableWasm", - 1: "ContractExecutableTypeContractExecutableToken", + 1: "ContractExecutableTypeContractExecutableStellarAsset", } // ValidEnum validates a proposed value for this enum. Implements @@ -51116,7 +50912,7 @@ var _ xdrType = (*ContractExecutableType)(nil) // { // case CONTRACT_EXECUTABLE_WASM: // Hash wasm_hash; -// case CONTRACT_EXECUTABLE_TOKEN: +// case CONTRACT_EXECUTABLE_STELLAR_ASSET: // void; // }; type ContractExecutable struct { @@ -51136,7 +50932,7 @@ func (u ContractExecutable) ArmForSwitch(sw int32) (string, bool) { switch ContractExecutableType(sw) { case ContractExecutableTypeContractExecutableWasm: return "WasmHash", true - case ContractExecutableTypeContractExecutableToken: + case ContractExecutableTypeContractExecutableStellarAsset: return "", true } return "-", false @@ -51153,7 +50949,7 @@ func NewContractExecutable(aType ContractExecutableType, value interface{}) (res return } result.WasmHash = &tv - case ContractExecutableTypeContractExecutableToken: + case ContractExecutableTypeContractExecutableStellarAsset: // void } return @@ -51196,7 +50992,7 @@ func (u ContractExecutable) EncodeTo(e *xdr.Encoder) error { return err } return nil - case ContractExecutableTypeContractExecutableToken: + case ContractExecutableTypeContractExecutableStellarAsset: // Void return nil } @@ -51227,7 +51023,7 @@ func (u *ContractExecutable) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, err return n, fmt.Errorf("decoding Hash: %w", err) } return n, nil - case ContractExecutableTypeContractExecutableToken: + case ContractExecutableTypeContractExecutableStellarAsset: // Void return n, nil } @@ -53451,6 +53247,90 @@ func (s StoredTransactionSet) xdrType() {} var _ xdrType = (*StoredTransactionSet)(nil) +// StoredDebugTransactionSet is an XDR Struct defines as: +// +// struct StoredDebugTransactionSet +// { +// StoredTransactionSet txSet; +// uint32 ledgerSeq; +// StellarValue scpValue; +// }; +type StoredDebugTransactionSet struct { + TxSet StoredTransactionSet + LedgerSeq Uint32 + ScpValue StellarValue +} + +// EncodeTo encodes this value using the Encoder. +func (s *StoredDebugTransactionSet) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.TxSet.EncodeTo(e); err != nil { + return err + } + if err = s.LedgerSeq.EncodeTo(e); err != nil { + return err + } + if err = s.ScpValue.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*StoredDebugTransactionSet)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *StoredDebugTransactionSet) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding StoredDebugTransactionSet: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.TxSet.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding StoredTransactionSet: %w", err) + } + nTmp, err = s.LedgerSeq.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.ScpValue.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding StellarValue: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s StoredDebugTransactionSet) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *StoredDebugTransactionSet) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + d := xdr.NewDecoder(r) + _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*StoredDebugTransactionSet)(nil) + _ encoding.BinaryUnmarshaler = (*StoredDebugTransactionSet)(nil) +) + +// xdrType signals that this type is an type representing +// representing XDR values defined by this package. +func (s StoredDebugTransactionSet) xdrType() {} + +var _ xdrType = (*StoredDebugTransactionSet)(nil) + // PersistedScpStateV0 is an XDR Struct defines as: // // struct PersistedSCPStateV0 @@ -54500,127 +54380,107 @@ var _ xdrType = (*ConfigSettingContractBandwidthV0)(nil) // enum ContractCostType { // // Cost of running 1 wasm instruction // WasmInsnExec = 0, -// // Cost of growing wasm linear memory by 1 page -// WasmMemAlloc = 1, -// // Cost of allocating a chuck of host memory (in bytes) -// HostMemAlloc = 2, -// // Cost of copying a chuck of bytes into a pre-allocated host memory -// HostMemCpy = 3, -// // Cost of comparing two slices of host memory -// HostMemCmp = 4, +// // Cost of allocating a slice of memory (in bytes) +// MemAlloc = 1, +// // Cost of copying a slice of bytes into a pre-allocated memory +// MemCpy = 2, +// // Cost of comparing two slices of memory +// MemCmp = 3, // // Cost of a host function dispatch, not including the actual work done by // // the function nor the cost of VM invocation machinary -// DispatchHostFunction = 5, +// DispatchHostFunction = 4, // // Cost of visiting a host object from the host object storage. Exists to // // make sure some baseline cost coverage, i.e. repeatly visiting objects // // by the guest will always incur some charges. -// VisitObject = 6, +// VisitObject = 5, // // Cost of serializing an xdr object to bytes -// ValSer = 7, +// ValSer = 6, // // Cost of deserializing an xdr object from bytes -// ValDeser = 8, +// ValDeser = 7, // // Cost of computing the sha256 hash from bytes -// ComputeSha256Hash = 9, +// ComputeSha256Hash = 8, // // Cost of computing the ed25519 pubkey from bytes -// ComputeEd25519PubKey = 10, -// // Cost of accessing an entry in a Map. -// MapEntry = 11, -// // Cost of accessing an entry in a Vec -// VecEntry = 12, +// ComputeEd25519PubKey = 9, // // Cost of verifying ed25519 signature of a payload. -// VerifyEd25519Sig = 13, -// // Cost of reading a slice of vm linear memory -// VmMemRead = 14, -// // Cost of writing to a slice of vm linear memory -// VmMemWrite = 15, +// VerifyEd25519Sig = 10, // // Cost of instantiation a VM from wasm bytes code. -// VmInstantiation = 16, +// VmInstantiation = 11, // // Cost of instantiation a VM from a cached state. -// VmCachedInstantiation = 17, +// VmCachedInstantiation = 12, // // Cost of invoking a function on the VM. If the function is a host function, // // additional cost will be covered by `DispatchHostFunction`. -// InvokeVmFunction = 18, +// InvokeVmFunction = 13, // // Cost of computing a keccak256 hash from bytes. -// ComputeKeccak256Hash = 19, -// // Cost of computing an ECDSA secp256k1 pubkey from bytes. -// ComputeEcdsaSecp256k1Key = 20, +// ComputeKeccak256Hash = 14, // // Cost of computing an ECDSA secp256k1 signature from bytes. -// ComputeEcdsaSecp256k1Sig = 21, +// ComputeEcdsaSecp256k1Sig = 15, // // Cost of recovering an ECDSA secp256k1 key from a signature. -// RecoverEcdsaSecp256k1Key = 22, +// RecoverEcdsaSecp256k1Key = 16, // // Cost of int256 addition (`+`) and subtraction (`-`) operations -// Int256AddSub = 23, +// Int256AddSub = 17, // // Cost of int256 multiplication (`*`) operation -// Int256Mul = 24, +// Int256Mul = 18, // // Cost of int256 division (`/`) operation -// Int256Div = 25, +// Int256Div = 19, // // Cost of int256 power (`exp`) operation -// Int256Pow = 26, +// Int256Pow = 20, // // Cost of int256 shift (`shl`, `shr`) operation -// Int256Shift = 27 +// Int256Shift = 21, +// // Cost of drawing random bytes using a ChaCha20 PRNG +// ChaCha20DrawBytes = 22 // }; type ContractCostType int32 const ( ContractCostTypeWasmInsnExec ContractCostType = 0 - ContractCostTypeWasmMemAlloc ContractCostType = 1 - ContractCostTypeHostMemAlloc ContractCostType = 2 - ContractCostTypeHostMemCpy ContractCostType = 3 - ContractCostTypeHostMemCmp ContractCostType = 4 - ContractCostTypeDispatchHostFunction ContractCostType = 5 - ContractCostTypeVisitObject ContractCostType = 6 - ContractCostTypeValSer ContractCostType = 7 - ContractCostTypeValDeser ContractCostType = 8 - ContractCostTypeComputeSha256Hash ContractCostType = 9 - ContractCostTypeComputeEd25519PubKey ContractCostType = 10 - ContractCostTypeMapEntry ContractCostType = 11 - ContractCostTypeVecEntry ContractCostType = 12 - ContractCostTypeVerifyEd25519Sig ContractCostType = 13 - ContractCostTypeVmMemRead ContractCostType = 14 - ContractCostTypeVmMemWrite ContractCostType = 15 - ContractCostTypeVmInstantiation ContractCostType = 16 - ContractCostTypeVmCachedInstantiation ContractCostType = 17 - ContractCostTypeInvokeVmFunction ContractCostType = 18 - ContractCostTypeComputeKeccak256Hash ContractCostType = 19 - ContractCostTypeComputeEcdsaSecp256k1Key ContractCostType = 20 - ContractCostTypeComputeEcdsaSecp256k1Sig ContractCostType = 21 - ContractCostTypeRecoverEcdsaSecp256k1Key ContractCostType = 22 - ContractCostTypeInt256AddSub ContractCostType = 23 - ContractCostTypeInt256Mul ContractCostType = 24 - ContractCostTypeInt256Div ContractCostType = 25 - ContractCostTypeInt256Pow ContractCostType = 26 - ContractCostTypeInt256Shift ContractCostType = 27 + ContractCostTypeMemAlloc ContractCostType = 1 + ContractCostTypeMemCpy ContractCostType = 2 + ContractCostTypeMemCmp ContractCostType = 3 + ContractCostTypeDispatchHostFunction ContractCostType = 4 + ContractCostTypeVisitObject ContractCostType = 5 + ContractCostTypeValSer ContractCostType = 6 + ContractCostTypeValDeser ContractCostType = 7 + ContractCostTypeComputeSha256Hash ContractCostType = 8 + ContractCostTypeComputeEd25519PubKey ContractCostType = 9 + ContractCostTypeVerifyEd25519Sig ContractCostType = 10 + ContractCostTypeVmInstantiation ContractCostType = 11 + ContractCostTypeVmCachedInstantiation ContractCostType = 12 + ContractCostTypeInvokeVmFunction ContractCostType = 13 + ContractCostTypeComputeKeccak256Hash ContractCostType = 14 + ContractCostTypeComputeEcdsaSecp256k1Sig ContractCostType = 15 + ContractCostTypeRecoverEcdsaSecp256k1Key ContractCostType = 16 + ContractCostTypeInt256AddSub ContractCostType = 17 + ContractCostTypeInt256Mul ContractCostType = 18 + ContractCostTypeInt256Div ContractCostType = 19 + ContractCostTypeInt256Pow ContractCostType = 20 + ContractCostTypeInt256Shift ContractCostType = 21 + ContractCostTypeChaCha20DrawBytes ContractCostType = 22 ) var contractCostTypeMap = map[int32]string{ 0: "ContractCostTypeWasmInsnExec", - 1: "ContractCostTypeWasmMemAlloc", - 2: "ContractCostTypeHostMemAlloc", - 3: "ContractCostTypeHostMemCpy", - 4: "ContractCostTypeHostMemCmp", - 5: "ContractCostTypeDispatchHostFunction", - 6: "ContractCostTypeVisitObject", - 7: "ContractCostTypeValSer", - 8: "ContractCostTypeValDeser", - 9: "ContractCostTypeComputeSha256Hash", - 10: "ContractCostTypeComputeEd25519PubKey", - 11: "ContractCostTypeMapEntry", - 12: "ContractCostTypeVecEntry", - 13: "ContractCostTypeVerifyEd25519Sig", - 14: "ContractCostTypeVmMemRead", - 15: "ContractCostTypeVmMemWrite", - 16: "ContractCostTypeVmInstantiation", - 17: "ContractCostTypeVmCachedInstantiation", - 18: "ContractCostTypeInvokeVmFunction", - 19: "ContractCostTypeComputeKeccak256Hash", - 20: "ContractCostTypeComputeEcdsaSecp256k1Key", - 21: "ContractCostTypeComputeEcdsaSecp256k1Sig", - 22: "ContractCostTypeRecoverEcdsaSecp256k1Key", - 23: "ContractCostTypeInt256AddSub", - 24: "ContractCostTypeInt256Mul", - 25: "ContractCostTypeInt256Div", - 26: "ContractCostTypeInt256Pow", - 27: "ContractCostTypeInt256Shift", + 1: "ContractCostTypeMemAlloc", + 2: "ContractCostTypeMemCpy", + 3: "ContractCostTypeMemCmp", + 4: "ContractCostTypeDispatchHostFunction", + 5: "ContractCostTypeVisitObject", + 6: "ContractCostTypeValSer", + 7: "ContractCostTypeValDeser", + 8: "ContractCostTypeComputeSha256Hash", + 9: "ContractCostTypeComputeEd25519PubKey", + 10: "ContractCostTypeVerifyEd25519Sig", + 11: "ContractCostTypeVmInstantiation", + 12: "ContractCostTypeVmCachedInstantiation", + 13: "ContractCostTypeInvokeVmFunction", + 14: "ContractCostTypeComputeKeccak256Hash", + 15: "ContractCostTypeComputeEcdsaSecp256k1Sig", + 16: "ContractCostTypeRecoverEcdsaSecp256k1Key", + 17: "ContractCostTypeInt256AddSub", + 18: "ContractCostTypeInt256Mul", + 19: "ContractCostTypeInt256Div", + 20: "ContractCostTypeInt256Pow", + 21: "ContractCostTypeInt256Shift", + 22: "ContractCostTypeChaCha20DrawBytes", } // ValidEnum validates a proposed value for this enum. Implements @@ -54776,19 +54636,19 @@ func (s ContractCostParamEntry) xdrType() {} var _ xdrType = (*ContractCostParamEntry)(nil) -// StateExpirationSettings is an XDR Struct defines as: +// StateArchivalSettings is an XDR Struct defines as: // -// struct StateExpirationSettings { -// uint32 maxEntryExpiration; -// uint32 minTempEntryExpiration; -// uint32 minPersistentEntryExpiration; +// struct StateArchivalSettings { +// uint32 maxEntryTTL; +// uint32 minTemporaryTTL; +// uint32 minPersistentTTL; // // // rent_fee = wfee_rate_average / rent_rate_denominator_for_type // int64 persistentRentRateDenominator; // int64 tempRentRateDenominator; // -// // max number of entries that emit expiration meta in a single ledger -// uint32 maxEntriesToExpire; +// // max number of entries that emit archival meta in a single ledger +// uint32 maxEntriesToArchive; // // // Number of snapshots to use when calculating average BucketList size // uint32 bucketListSizeWindowSampleSize; @@ -54799,28 +54659,28 @@ var _ xdrType = (*ContractCostParamEntry)(nil) // // Lowest BucketList level to be scanned to evict entries // uint32 startingEvictionScanLevel; // }; -type StateExpirationSettings struct { - MaxEntryExpiration Uint32 - MinTempEntryExpiration Uint32 - MinPersistentEntryExpiration Uint32 +type StateArchivalSettings struct { + MaxEntryTtl Uint32 + MinTemporaryTtl Uint32 + MinPersistentTtl Uint32 PersistentRentRateDenominator Int64 TempRentRateDenominator Int64 - MaxEntriesToExpire Uint32 + MaxEntriesToArchive Uint32 BucketListSizeWindowSampleSize Uint32 EvictionScanSize Uint64 StartingEvictionScanLevel Uint32 } // EncodeTo encodes this value using the Encoder. -func (s *StateExpirationSettings) EncodeTo(e *xdr.Encoder) error { +func (s *StateArchivalSettings) EncodeTo(e *xdr.Encoder) error { var err error - if err = s.MaxEntryExpiration.EncodeTo(e); err != nil { + if err = s.MaxEntryTtl.EncodeTo(e); err != nil { return err } - if err = s.MinTempEntryExpiration.EncodeTo(e); err != nil { + if err = s.MinTemporaryTtl.EncodeTo(e); err != nil { return err } - if err = s.MinPersistentEntryExpiration.EncodeTo(e); err != nil { + if err = s.MinPersistentTtl.EncodeTo(e); err != nil { return err } if err = s.PersistentRentRateDenominator.EncodeTo(e); err != nil { @@ -54829,7 +54689,7 @@ func (s *StateExpirationSettings) EncodeTo(e *xdr.Encoder) error { if err = s.TempRentRateDenominator.EncodeTo(e); err != nil { return err } - if err = s.MaxEntriesToExpire.EncodeTo(e); err != nil { + if err = s.MaxEntriesToArchive.EncodeTo(e); err != nil { return err } if err = s.BucketListSizeWindowSampleSize.EncodeTo(e); err != nil { @@ -54844,27 +54704,27 @@ func (s *StateExpirationSettings) EncodeTo(e *xdr.Encoder) error { return nil } -var _ decoderFrom = (*StateExpirationSettings)(nil) +var _ decoderFrom = (*StateArchivalSettings)(nil) // DecodeFrom decodes this value using the Decoder. -func (s *StateExpirationSettings) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { +func (s *StateArchivalSettings) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { if maxDepth == 0 { - return 0, fmt.Errorf("decoding StateExpirationSettings: %w", ErrMaxDecodingDepthReached) + return 0, fmt.Errorf("decoding StateArchivalSettings: %w", ErrMaxDecodingDepthReached) } maxDepth -= 1 var err error var n, nTmp int - nTmp, err = s.MaxEntryExpiration.DecodeFrom(d, maxDepth) + nTmp, err = s.MaxEntryTtl.DecodeFrom(d, maxDepth) n += nTmp if err != nil { return n, fmt.Errorf("decoding Uint32: %w", err) } - nTmp, err = s.MinTempEntryExpiration.DecodeFrom(d, maxDepth) + nTmp, err = s.MinTemporaryTtl.DecodeFrom(d, maxDepth) n += nTmp if err != nil { return n, fmt.Errorf("decoding Uint32: %w", err) } - nTmp, err = s.MinPersistentEntryExpiration.DecodeFrom(d, maxDepth) + nTmp, err = s.MinPersistentTtl.DecodeFrom(d, maxDepth) n += nTmp if err != nil { return n, fmt.Errorf("decoding Uint32: %w", err) @@ -54879,7 +54739,7 @@ func (s *StateExpirationSettings) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int if err != nil { return n, fmt.Errorf("decoding Int64: %w", err) } - nTmp, err = s.MaxEntriesToExpire.DecodeFrom(d, maxDepth) + nTmp, err = s.MaxEntriesToArchive.DecodeFrom(d, maxDepth) n += nTmp if err != nil { return n, fmt.Errorf("decoding Uint32: %w", err) @@ -54903,7 +54763,7 @@ func (s *StateExpirationSettings) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int } // MarshalBinary implements encoding.BinaryMarshaler. -func (s StateExpirationSettings) MarshalBinary() ([]byte, error) { +func (s StateArchivalSettings) MarshalBinary() ([]byte, error) { b := bytes.Buffer{} e := xdr.NewEncoder(&b) err := s.EncodeTo(e) @@ -54911,7 +54771,7 @@ func (s StateExpirationSettings) MarshalBinary() ([]byte, error) { } // UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *StateExpirationSettings) UnmarshalBinary(inp []byte) error { +func (s *StateArchivalSettings) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) d := xdr.NewDecoder(r) _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) @@ -54919,15 +54779,15 @@ func (s *StateExpirationSettings) UnmarshalBinary(inp []byte) error { } var ( - _ encoding.BinaryMarshaler = (*StateExpirationSettings)(nil) - _ encoding.BinaryUnmarshaler = (*StateExpirationSettings)(nil) + _ encoding.BinaryMarshaler = (*StateArchivalSettings)(nil) + _ encoding.BinaryUnmarshaler = (*StateArchivalSettings)(nil) ) // xdrType signals that this type is an type representing // representing XDR values defined by this package. -func (s StateExpirationSettings) xdrType() {} +func (s StateArchivalSettings) xdrType() {} -var _ xdrType = (*StateExpirationSettings)(nil) +var _ xdrType = (*StateArchivalSettings)(nil) // EvictionIterator is an XDR Struct defines as: // @@ -55115,7 +54975,7 @@ var _ xdrType = (*ContractCostParams)(nil) // CONFIG_SETTING_CONTRACT_COST_PARAMS_MEMORY_BYTES = 7, // CONFIG_SETTING_CONTRACT_DATA_KEY_SIZE_BYTES = 8, // CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES = 9, -// CONFIG_SETTING_STATE_EXPIRATION = 10, +// CONFIG_SETTING_STATE_ARCHIVAL = 10, // CONFIG_SETTING_CONTRACT_EXECUTION_LANES = 11, // CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW = 12, // CONFIG_SETTING_EVICTION_ITERATOR = 13 @@ -55133,7 +54993,7 @@ const ( ConfigSettingIdConfigSettingContractCostParamsMemoryBytes ConfigSettingId = 7 ConfigSettingIdConfigSettingContractDataKeySizeBytes ConfigSettingId = 8 ConfigSettingIdConfigSettingContractDataEntrySizeBytes ConfigSettingId = 9 - ConfigSettingIdConfigSettingStateExpiration ConfigSettingId = 10 + ConfigSettingIdConfigSettingStateArchival ConfigSettingId = 10 ConfigSettingIdConfigSettingContractExecutionLanes ConfigSettingId = 11 ConfigSettingIdConfigSettingBucketlistSizeWindow ConfigSettingId = 12 ConfigSettingIdConfigSettingEvictionIterator ConfigSettingId = 13 @@ -55150,7 +55010,7 @@ var configSettingIdMap = map[int32]string{ 7: "ConfigSettingIdConfigSettingContractCostParamsMemoryBytes", 8: "ConfigSettingIdConfigSettingContractDataKeySizeBytes", 9: "ConfigSettingIdConfigSettingContractDataEntrySizeBytes", - 10: "ConfigSettingIdConfigSettingStateExpiration", + 10: "ConfigSettingIdConfigSettingStateArchival", 11: "ConfigSettingIdConfigSettingContractExecutionLanes", 12: "ConfigSettingIdConfigSettingBucketlistSizeWindow", 13: "ConfigSettingIdConfigSettingEvictionIterator", @@ -55248,8 +55108,8 @@ var _ xdrType = (*ConfigSettingId)(nil) // uint32 contractDataKeySizeBytes; // case CONFIG_SETTING_CONTRACT_DATA_ENTRY_SIZE_BYTES: // uint32 contractDataEntrySizeBytes; -// case CONFIG_SETTING_STATE_EXPIRATION: -// StateExpirationSettings stateExpirationSettings; +// case CONFIG_SETTING_STATE_ARCHIVAL: +// StateArchivalSettings stateArchivalSettings; // case CONFIG_SETTING_CONTRACT_EXECUTION_LANES: // ConfigSettingContractExecutionLanesV0 contractExecutionLanes; // case CONFIG_SETTING_BUCKETLIST_SIZE_WINDOW: @@ -55269,7 +55129,7 @@ type ConfigSettingEntry struct { ContractCostParamsMemBytes *ContractCostParams ContractDataKeySizeBytes *Uint32 ContractDataEntrySizeBytes *Uint32 - StateExpirationSettings *StateExpirationSettings + StateArchivalSettings *StateArchivalSettings ContractExecutionLanes *ConfigSettingContractExecutionLanesV0 BucketListSizeWindow *[]Uint64 EvictionIterator *EvictionIterator @@ -55305,8 +55165,8 @@ func (u ConfigSettingEntry) ArmForSwitch(sw int32) (string, bool) { return "ContractDataKeySizeBytes", true case ConfigSettingIdConfigSettingContractDataEntrySizeBytes: return "ContractDataEntrySizeBytes", true - case ConfigSettingIdConfigSettingStateExpiration: - return "StateExpirationSettings", true + case ConfigSettingIdConfigSettingStateArchival: + return "StateArchivalSettings", true case ConfigSettingIdConfigSettingContractExecutionLanes: return "ContractExecutionLanes", true case ConfigSettingIdConfigSettingBucketlistSizeWindow: @@ -55391,13 +55251,13 @@ func NewConfigSettingEntry(configSettingId ConfigSettingId, value interface{}) ( return } result.ContractDataEntrySizeBytes = &tv - case ConfigSettingIdConfigSettingStateExpiration: - tv, ok := value.(StateExpirationSettings) + case ConfigSettingIdConfigSettingStateArchival: + tv, ok := value.(StateArchivalSettings) if !ok { - err = errors.New("invalid value, must be StateExpirationSettings") + err = errors.New("invalid value, must be StateArchivalSettings") return } - result.StateExpirationSettings = &tv + result.StateArchivalSettings = &tv case ConfigSettingIdConfigSettingContractExecutionLanes: tv, ok := value.(ConfigSettingContractExecutionLanesV0) if !ok { @@ -55673,25 +55533,25 @@ func (u ConfigSettingEntry) GetContractDataEntrySizeBytes() (result Uint32, ok b return } -// MustStateExpirationSettings retrieves the StateExpirationSettings value from the union, +// MustStateArchivalSettings retrieves the StateArchivalSettings value from the union, // panicing if the value is not set. -func (u ConfigSettingEntry) MustStateExpirationSettings() StateExpirationSettings { - val, ok := u.GetStateExpirationSettings() +func (u ConfigSettingEntry) MustStateArchivalSettings() StateArchivalSettings { + val, ok := u.GetStateArchivalSettings() if !ok { - panic("arm StateExpirationSettings is not set") + panic("arm StateArchivalSettings is not set") } return val } -// GetStateExpirationSettings retrieves the StateExpirationSettings value from the union, +// GetStateArchivalSettings retrieves the StateArchivalSettings value from the union, // returning ok if the union's switch indicated the value is valid. -func (u ConfigSettingEntry) GetStateExpirationSettings() (result StateExpirationSettings, ok bool) { +func (u ConfigSettingEntry) GetStateArchivalSettings() (result StateArchivalSettings, ok bool) { armName, _ := u.ArmForSwitch(int32(u.ConfigSettingId)) - if armName == "StateExpirationSettings" { - result = *u.StateExpirationSettings + if armName == "StateArchivalSettings" { + result = *u.StateArchivalSettings ok = true } @@ -55830,8 +55690,8 @@ func (u ConfigSettingEntry) EncodeTo(e *xdr.Encoder) error { return err } return nil - case ConfigSettingIdConfigSettingStateExpiration: - if err = (*u.StateExpirationSettings).EncodeTo(e); err != nil { + case ConfigSettingIdConfigSettingStateArchival: + if err = (*u.StateArchivalSettings).EncodeTo(e); err != nil { return err } return nil @@ -55955,12 +55815,12 @@ func (u *ConfigSettingEntry) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, err return n, fmt.Errorf("decoding Uint32: %w", err) } return n, nil - case ConfigSettingIdConfigSettingStateExpiration: - u.StateExpirationSettings = new(StateExpirationSettings) - nTmp, err = (*u.StateExpirationSettings).DecodeFrom(d, maxDepth) + case ConfigSettingIdConfigSettingStateArchival: + u.StateArchivalSettings = new(StateArchivalSettings) + nTmp, err = (*u.StateArchivalSettings).DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding StateExpirationSettings: %w", err) + return n, fmt.Errorf("decoding StateArchivalSettings: %w", err) } return n, nil case ConfigSettingIdConfigSettingContractExecutionLanes: From a4a7a6102310812581e4cb7646e0bbbcb4f506fb Mon Sep 17 00:00:00 2001 From: Tsachi Herman <24438559+tsachiherman@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:08:51 -0400 Subject: [PATCH 008/234] fix comment (#5090) --- services/horizon/internal/ingest/processors/contract_data.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/horizon/internal/ingest/processors/contract_data.go b/services/horizon/internal/ingest/processors/contract_data.go index dec9199747..bce4ac6b25 100644 --- a/services/horizon/internal/ingest/processors/contract_data.go +++ b/services/horizon/internal/ingest/processors/contract_data.go @@ -413,7 +413,7 @@ func metadataObjFromAsset(isNative bool, code, issuer string) (*xdr.ScMap, error // ledger entry containing the asset info entry written to contract storage by the // Stellar Asset Contract. // -// Warning: Only for use in tests. This does not set a realistic expirationLedgerSeq +// Warning: Only for use in tests. This does not create the accompanied TTLEntry which would typically be created by core. func AssetToContractData(isNative bool, code, issuer string, contractID [32]byte) (xdr.LedgerEntryData, error) { storageMap, err := metadataObjFromAsset(isNative, code, issuer) if err != nil { @@ -450,7 +450,7 @@ func AssetToContractData(isNative bool, code, issuer string, contractID [32]byte // creates a ledger entry containing the asset balance of a contract holder // written to contract storage by the Stellar Asset Contract. // -// Warning: Only for use in tests. This does not set a realistic expirationLedgerSeq +// Warning: Only for use in tests. This does not create the accompanied TTLEntry which would typically be created by core. func BalanceToContractData(assetContractId, holderID [32]byte, amt uint64) xdr.LedgerEntryData { return balanceToContractData(assetContractId, holderID, xdr.Int128Parts{ Lo: xdr.Uint64(amt), From 558003ca0b06b12b2a57e8370709f82c23130dff Mon Sep 17 00:00:00 2001 From: Tsachi Herman <24438559+tsachiherman@users.noreply.github.com> Date: Tue, 31 Oct 2023 15:54:16 -0400 Subject: [PATCH 009/234] update (#5097) --- ingest/checkpoint_change_reader.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ingest/checkpoint_change_reader.go b/ingest/checkpoint_change_reader.go index 7fbc74e294..37e5e994e1 100644 --- a/ingest/checkpoint_change_reader.go +++ b/ingest/checkpoint_change_reader.go @@ -325,6 +325,8 @@ func (r *CheckpointChangeReader) streamBucketContents(hash historyarchive.Hash, var batch []xdr.BucketEntry lastBatch := false + preloadKeys := make([]string, 0, preloadedEntries) + LoopBucketEntry: for { // Preload entries for faster retrieve from temp store. @@ -332,8 +334,10 @@ LoopBucketEntry: if lastBatch { return true } + batch = make([]xdr.BucketEntry, 0, preloadedEntries) - preloadKeys := []string{} + // reset the content of the preloadKeys + preloadKeys = preloadKeys[:0] for i := 0; i < preloadedEntries; i++ { var entry xdr.BucketEntry From 15700ce393e3eefa01a751e0dcdade16814d64f6 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Wed, 15 Nov 2023 18:34:01 +0100 Subject: [PATCH 010/234] xdr: Add String() method to ScVal (#5112) * xdr: Add String() method to ScVal * Also add String() method for contract events --- xdr/event.go | 23 +++++++++++ xdr/scval.go | 100 ++++++++++++++++++++++++++++++++++++++++++++++ xdr/scval_test.go | 28 +++++++++++-- 3 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 xdr/event.go diff --git a/xdr/event.go b/xdr/event.go new file mode 100644 index 0000000000..2ae97a0d2c --- /dev/null +++ b/xdr/event.go @@ -0,0 +1,23 @@ +package xdr + +import ( + "encoding/hex" + "fmt" +) + +func (ce ContractEvent) String() string { + result := ce.Type.String() + "(" + if ce.ContractId != nil { + result += hex.EncodeToString(ce.ContractId[:]) + "," + } + result += ce.Body.String() + ")" + return result +} + +func (eb ContractEventBody) String() string { + return fmt.Sprintf("%+v", *eb.V0) +} + +func (de DiagnosticEvent) String() string { + return fmt.Sprintf("%s, successful call: %t", de.Event, de.InSuccessfulContractCall) +} diff --git a/xdr/scval.go b/xdr/scval.go index 4648a6ab82..003efe885f 100644 --- a/xdr/scval.go +++ b/xdr/scval.go @@ -2,7 +2,10 @@ package xdr import ( "bytes" + "encoding/hex" "fmt" + "math/big" + "time" "github.com/stellar/go/strkey" ) @@ -182,3 +185,100 @@ func (s ScMapEntry) Equals(o ScMapEntry) bool { func (s ScNonceKey) Equals(o ScNonceKey) bool { return s.Nonce == o.Nonce } + +func bigIntFromParts(hi Int64, lowerParts ...Uint64) *big.Int { + result := new(big.Int).SetInt64(int64(hi)) + secondary := new(big.Int) + for _, part := range lowerParts { + result.Lsh(result, 64) + result.Or(result, secondary.SetUint64(uint64(part))) + } + return result +} + +func bigUIntFromParts(hi Uint64, lowerParts ...Uint64) *big.Int { + result := new(big.Int).SetUint64(uint64(hi)) + secondary := new(big.Int) + for _, part := range lowerParts { + result.Lsh(result, 64) + result.Or(result, secondary.SetUint64(uint64(part))) + } + return result +} + +func (s ScVal) String() string { + switch s.Type { + case ScValTypeScvBool: + return fmt.Sprintf("%t", *s.B) + case ScValTypeScvVoid: + return "(void)" + case ScValTypeScvError: + switch s.Error.Type { + case ScErrorTypeSceContract: + return fmt.Sprintf("%s(%d)", s.Error.Type, *s.Error.ContractCode) + case ScErrorTypeSceWasmVm, ScErrorTypeSceContext, ScErrorTypeSceStorage, ScErrorTypeSceObject, + ScErrorTypeSceCrypto, ScErrorTypeSceEvents, ScErrorTypeSceBudget, ScErrorTypeSceValue, ScErrorTypeSceAuth: + return fmt.Sprintf("%s(%s)", s.Error.Type, *s.Error.Code) + } + case ScValTypeScvU32: + return fmt.Sprintf("%d", *s.U32) + case ScValTypeScvI32: + return fmt.Sprintf("%d", *s.I32) + case ScValTypeScvU64: + return fmt.Sprintf("%d", *s.U64) + case ScValTypeScvI64: + return fmt.Sprintf("%d", *s.I64) + case ScValTypeScvTimepoint: + return time.Unix(int64(*s.Timepoint), 0).String() + case ScValTypeScvDuration: + return fmt.Sprintf("%d", *s.Duration) + case ScValTypeScvU128: + return bigUIntFromParts(s.U128.Hi, s.U128.Lo).String() + case ScValTypeScvI128: + return bigIntFromParts(s.I128.Hi, s.I128.Lo).String() + case ScValTypeScvU256: + return bigUIntFromParts(s.U256.HiHi, s.U256.HiLo, s.U256.LoHi, s.U256.LoLo).String() + case ScValTypeScvI256: + return bigIntFromParts(s.I256.HiHi, s.I256.HiLo, s.I256.LoHi, s.I256.LoLo).String() + case ScValTypeScvBytes: + return hex.EncodeToString(*s.Bytes) + case ScValTypeScvString: + return string(*s.Str) + case ScValTypeScvSymbol: + return string(*s.Sym) + case ScValTypeScvVec: + if *s.Vec == nil { + return "nil" + } + return fmt.Sprintf("%s", **s.Vec) + case ScValTypeScvMap: + if *s.Map == nil { + return "nil" + } + return fmt.Sprintf("%v", **s.Map) + case ScValTypeScvAddress: + str, err := s.Address.String() + if err != nil { + return err.Error() + } + return str + case ScValTypeScvContractInstance: + result := "" + switch s.Instance.Executable.Type { + case ContractExecutableTypeContractExecutableStellarAsset: + result = "(StellarAssetContract)" + case ContractExecutableTypeContractExecutableWasm: + result = hex.EncodeToString(s.Instance.Executable.WasmHash[:]) + } + if s.Instance.Storage != nil && len(*s.Instance.Storage) > 0 { + result += fmt.Sprintf(": %v", *s.Instance.Storage) + } + return result + case ScValTypeScvLedgerKeyContractInstance: + return "(LedgerKeyContractInstance)" + case ScValTypeScvLedgerKeyNonce: + return fmt.Sprintf("%X", *s.NonceKey) + } + + return "unknown" +} diff --git a/xdr/scval_test.go b/xdr/scval_test.go index 812a12942b..8bfa97deb3 100644 --- a/xdr/scval_test.go +++ b/xdr/scval_test.go @@ -3,7 +3,7 @@ package xdr import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stellar/go/gxdr" "github.com/stellar/go/randxdr" @@ -19,10 +19,30 @@ func TestScValEqualsCoverage(t *testing.T) { shape, []randxdr.Preset{}, ) - assert.NoError(t, gxdr.Convert(shape, &scVal)) + require.NoError(t, gxdr.Convert(shape, &scVal)) clonedScVal := ScVal{} - assert.NoError(t, gxdr.Convert(shape, &clonedScVal)) - assert.True(t, scVal.Equals(clonedScVal), "scVal: %#v, clonedScVal: %#v", scVal, clonedScVal) + require.NoError(t, gxdr.Convert(shape, &clonedScVal)) + require.True(t, scVal.Equals(clonedScVal), "scVal: %#v, clonedScVal: %#v", scVal, clonedScVal) + } +} + +func TestScValStringCoverage(t *testing.T) { + gen := randxdr.NewGenerator() + for i := 0; i < 30000; i++ { + scVal := ScVal{} + + shape := &gxdr.SCVal{} + gen.Next( + shape, + []randxdr.Preset{}, + ) + require.NoError(t, gxdr.Convert(shape, &scVal)) + + var str string + require.NotPanics(t, func() { + str = scVal.String() + }) + require.NotEqual(t, str, "unknown") } } From 0bf3b30d88b33cc06e00cb69528d8ca8d06b095e Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 17 Nov 2023 14:38:34 +0000 Subject: [PATCH 011/234] services/horizon/internal/ingest/processors: Accommodate eviction of soroban ledger entries in asset stats endpoint (#5033) --- protocols/horizon/main.go | 2 + .../docker/captive-core-integration-tests.cfg | 5 +- .../docker/stellar-core-integration-tests.cfg | 5 +- services/horizon/internal/actions/asset.go | 2 +- .../horizon/internal/actions/asset_test.go | 5 + .../horizon/internal/db2/assets/asset_stat.go | 60 -- .../internal/db2/assets/asset_stat_test.go | 107 --- .../internal/db2/history/asset_stats.go | 259 ++++++- .../internal/db2/history/asset_stats_test.go | 567 +++++++++++----- services/horizon/internal/db2/history/main.go | 69 +- .../db2/history/mock_q_asset_stats.go | 67 +- .../horizon/internal/db2/schema/bindata.go | 173 +++-- .../migrations/66_contract_asset_stats.sql | 18 + services/horizon/internal/ingest/main.go | 4 +- .../internal/ingest/processor_runner.go | 2 +- .../internal/ingest/processor_runner_test.go | 32 +- .../processors/asset_stats_processor.go | 453 +++++++++---- .../processors/asset_stats_processor_test.go | 637 +++++++++++++++--- .../ingest/processors/asset_stats_set.go | 251 +------ .../ingest/processors/asset_stats_set_test.go | 436 +----------- .../ingest/processors/contract_asset_stats.go | 412 +++++++++++ .../processors/contract_asset_stats_test.go | 636 +++++++++++++++++ .../ingest/processors/contract_data.go | 38 +- services/horizon/internal/ingest/verify.go | 214 +++++- .../ingest/verify_range_state_test.go | 85 ++- .../horizon/internal/ingest/verify_test.go | 27 +- .../integration/extend_footprint_ttl_test.go | 4 +- .../horizon/internal/integration/sac_test.go | 297 ++++---- .../internal/resourceadapter/asset_stat.go | 40 +- .../resourceadapter/asset_stat_test.go | 49 +- 30 files changed, 3372 insertions(+), 1584 deletions(-) delete mode 100644 services/horizon/internal/db2/assets/asset_stat.go delete mode 100644 services/horizon/internal/db2/assets/asset_stat_test.go create mode 100644 services/horizon/internal/db2/schema/migrations/66_contract_asset_stats.sql create mode 100644 services/horizon/internal/ingest/processors/contract_asset_stats.go create mode 100644 services/horizon/internal/ingest/processors/contract_asset_stats_test.go diff --git a/protocols/horizon/main.go b/protocols/horizon/main.go index 1cfc10bf9b..08da47b7b4 100644 --- a/protocols/horizon/main.go +++ b/protocols/horizon/main.go @@ -173,12 +173,14 @@ type AssetStat struct { NumClaimableBalances int32 `json:"num_claimable_balances"` NumLiquidityPools int32 `json:"num_liquidity_pools"` NumContracts int32 `json:"num_contracts"` + NumArchivedContracts int32 `json:"num_archived_contracts"` // Action needed in release: horizon-v3.0.0: deprecated field Amount string `json:"amount"` Accounts AssetStatAccounts `json:"accounts"` ClaimableBalancesAmount string `json:"claimable_balances_amount"` LiquidityPoolsAmount string `json:"liquidity_pools_amount"` ContractsAmount string `json:"contracts_amount"` + ArchivedContractsAmount string `json:"archived_contracts_amount"` Balances AssetStatBalances `json:"balances"` Flags AccountFlags `json:"flags"` } diff --git a/services/horizon/docker/captive-core-integration-tests.cfg b/services/horizon/docker/captive-core-integration-tests.cfg index 2215cb9fd0..abea8c8ede 100644 --- a/services/horizon/docker/captive-core-integration-tests.cfg +++ b/services/horizon/docker/captive-core-integration-tests.cfg @@ -1,11 +1,12 @@ PEER_PORT=11725 ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true -ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true -TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE=true UNSAFE_QUORUM=true FAILURE_SAFETY=0 +ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true +TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE=true + [[VALIDATORS]] NAME="local_core" HOME_DOMAIN="core.local" diff --git a/services/horizon/docker/stellar-core-integration-tests.cfg b/services/horizon/docker/stellar-core-integration-tests.cfg index 53d5a9816b..27adf63f4b 100644 --- a/services/horizon/docker/stellar-core-integration-tests.cfg +++ b/services/horizon/docker/stellar-core-integration-tests.cfg @@ -1,5 +1,4 @@ ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true -TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE=true NETWORK_PASSPHRASE="Standalone Network ; February 2017" @@ -15,6 +14,8 @@ FAILURE_SAFETY=0 DATABASE="postgresql://user=postgres password=mysecretpassword host=core-postgres port=5641 dbname=stellar" +TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE=true + [QUORUM_SET] THRESHOLD_PERCENT=100 VALIDATORS=["GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS"] @@ -22,4 +23,4 @@ VALIDATORS=["GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS"] [HISTORY.vs] get="cp history/vs/{0} {1}" put="cp {0} history/vs/{1}" -mkdir="mkdir -p history/vs/{0}" \ No newline at end of file +mkdir="mkdir -p history/vs/{0}" diff --git a/services/horizon/internal/actions/asset.go b/services/horizon/internal/actions/asset.go index 27235fb5eb..7236b482ff 100644 --- a/services/horizon/internal/actions/asset.go +++ b/services/horizon/internal/actions/asset.go @@ -81,7 +81,7 @@ func (handler AssetStatsHandler) validateAssetParams(code, issuer string, pq db2 func (handler AssetStatsHandler) findIssuersForAssets( ctx context.Context, historyQ *history.Q, - assetStats []history.ExpAssetStat, + assetStats []history.AssetAndContractStat, ) (map[string]history.AccountEntry, error) { issuerSet := map[string]bool{} issuers := []string{} diff --git a/services/horizon/internal/actions/asset_test.go b/services/horizon/internal/actions/asset_test.go index eb1a1df07d..0e4995d93c 100644 --- a/services/horizon/internal/actions/asset_test.go +++ b/services/horizon/internal/actions/asset_test.go @@ -160,6 +160,7 @@ func TestAssetStats(t *testing.T) { Amount: "0.0000001", NumAccounts: usdAssetStat.NumAccounts, ContractsAmount: "0.0000000", + ArchivedContractsAmount: "0.0000000", Asset: base.Asset{ Type: "credit_alphanum4", Code: usdAssetStat.AssetCode, @@ -204,6 +205,7 @@ func TestAssetStats(t *testing.T) { ClaimableBalancesAmount: "0.0000000", LiquidityPoolsAmount: "0.0000000", ContractsAmount: "0.0000000", + ArchivedContractsAmount: "0.0000000", Amount: "0.0000023", NumAccounts: etherAssetStat.NumAccounts, Asset: base.Asset{ @@ -251,6 +253,7 @@ func TestAssetStats(t *testing.T) { LiquidityPoolsAmount: "0.0000000", Amount: "0.0000001", ContractsAmount: "0.0000000", + ArchivedContractsAmount: "0.0000000", NumAccounts: otherUSDAssetStat.NumAccounts, Asset: base.Asset{ Type: "credit_alphanum4", @@ -299,6 +302,7 @@ func TestAssetStats(t *testing.T) { LiquidityPoolsAmount: "0.0000000", Amount: "0.0000111", ContractsAmount: "0.0000000", + ArchivedContractsAmount: "0.0000000", NumAccounts: eurAssetStat.NumAccounts, Asset: base.Asset{ Type: "credit_alphanum4", @@ -476,6 +480,7 @@ func TestAssetStatsIssuerDoesNotExist(t *testing.T) { LiquidityPoolsAmount: "0.0000000", Amount: "0.0000001", ContractsAmount: "0.0000000", + ArchivedContractsAmount: "0.0000000", NumAccounts: usdAssetStat.NumAccounts, Asset: base.Asset{ Type: "credit_alphanum4", diff --git a/services/horizon/internal/db2/assets/asset_stat.go b/services/horizon/internal/db2/assets/asset_stat.go deleted file mode 100644 index c7a5e69eec..0000000000 --- a/services/horizon/internal/db2/assets/asset_stat.go +++ /dev/null @@ -1,60 +0,0 @@ -package assets - -import ( - sq "github.com/Masterminds/squirrel" - "github.com/stellar/go/services/horizon/internal/db2" -) - -// PagingToken implementation for hal.Pageable -//func (res AssetStat) PagingToken() string { -// return res.PT -//} - -// AssetStatsQ is the query to fetch all assets in the system -type AssetStatsQ struct { - AssetCode *string - AssetIssuer *string - PageQuery *db2.PageQuery -} - -// GetSQL allows this query to be executed by the caller -func (q AssetStatsQ) GetSQL() (sq.SelectBuilder, error) { - sql := selectQuery - if q.AssetCode != nil && *q.AssetCode != "" { - sql = sql.Where("hist.asset_code = ?", *q.AssetCode) - } - if q.AssetIssuer != nil && *q.AssetIssuer != "" { - sql = sql.Where("hist.asset_issuer = ?", *q.AssetIssuer) - } - - var err error - if q.PageQuery != nil { - // cursor needs to work for descending case as well - cursor := q.PageQuery.Cursor - if q.PageQuery.Order == "desc" && cursor == "" { - cursor = "zzzzzzzzzzzzz" // 12 + 1 "z"s so it will always be greater than the _ delimiter since code is max 12 chars - } - - sql, err = q.PageQuery.ApplyToUsingCursor(sql, "concat(hist.asset_code, '_', hist.asset_issuer, '_', hist.asset_type)", cursor) - if err != nil { - return sql, err - } - } else { - sql = sql.OrderBy("sort_key ASC") - } - return sql, nil -} - -var selectQuery = sq. - Select( - "concat(hist.asset_code, '_', hist.asset_issuer, '_', hist.asset_type) as sort_key", - "hist.asset_type", - "hist.asset_code", - "hist.asset_issuer", - "stats.amount", - "stats.num_accounts", - "stats.flags", - "stats.toml", - ). - From("history_assets hist"). - Join("asset_stats stats ON hist.id = stats.id") diff --git a/services/horizon/internal/db2/assets/asset_stat_test.go b/services/horizon/internal/db2/assets/asset_stat_test.go deleted file mode 100644 index c87edc19cb..0000000000 --- a/services/horizon/internal/db2/assets/asset_stat_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package assets - -import ( - "context" - "strconv" - "testing" - - "github.com/stellar/go/services/horizon/internal/db2" - "github.com/stellar/go/services/horizon/internal/db2/history" - "github.com/stellar/go/services/horizon/internal/test" -) - -// AssetStatsR is the result from the AssetStatsQ query -type AssetStatsR struct { - SortKey string `db:"sort_key"` - Type string `db:"asset_type"` - Code string `db:"asset_code"` - Issuer string `db:"asset_issuer"` - Amount string `db:"amount"` - NumAccounts int32 `db:"num_accounts"` - Flags int8 `db:"flags"` - Toml string `db:"toml"` -} - -func TestAssetsStatsQExec(t *testing.T) { - item0 := AssetStatsR{ - SortKey: "BTC_GC23QF2HUE52AMXUFUH3AYJAXXGXXV2VHXYYR6EYXETPKDXZSAW67XO4_credit_alphanum4", - Type: "credit_alphanum4", - Code: "BTC", - Issuer: "GC23QF2HUE52AMXUFUH3AYJAXXGXXV2VHXYYR6EYXETPKDXZSAW67XO4", - Amount: "1009876000", - NumAccounts: 1, - Flags: 1, - Toml: "https://test.com/.well-known/stellar.toml", - } - - item1 := AssetStatsR{ - SortKey: "SCOT_GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU_credit_alphanum4", - Type: "credit_alphanum4", - Code: "SCOT", - Issuer: "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", - Amount: "10000000000", - NumAccounts: 1, - Flags: 2, - Toml: "", - } - - item2 := AssetStatsR{ - SortKey: "USD_GC23QF2HUE52AMXUFUH3AYJAXXGXXV2VHXYYR6EYXETPKDXZSAW67XO4_credit_alphanum4", - Type: "credit_alphanum4", - Code: "USD", - Issuer: "GC23QF2HUE52AMXUFUH3AYJAXXGXXV2VHXYYR6EYXETPKDXZSAW67XO4", - Amount: "3000010434000", - NumAccounts: 2, - Flags: 1, - Toml: "https://test.com/.well-known/stellar.toml", - } - - testCases := []struct { - query AssetStatsQ - want []AssetStatsR - }{ - { - AssetStatsQ{}, - []AssetStatsR{item0, item1, item2}, - }, { - AssetStatsQ{ - PageQuery: &db2.PageQuery{ - Order: "asc", - Limit: 10, - }, - }, - []AssetStatsR{item0, item1, item2}, - }, { - AssetStatsQ{ - PageQuery: &db2.PageQuery{ - Order: "desc", - Limit: 10, - }, - }, - []AssetStatsR{item2, item1, item0}, - }, - } - - for i, kase := range testCases { - t.Run(strconv.Itoa(i), func(t *testing.T) { - tt := test.Start(t) - tt.Scenario("ingest_asset_stats") - defer tt.Finish() - - sql, err := kase.query.GetSQL() - tt.Require.NoError(err) - - var results []AssetStatsR - err = history.Q{SessionInterface: tt.HorizonSession()}.Select(context.Background(), &results, sql) - tt.Require.NoError(err) - if !tt.Assert.Equal(3, len(results)) { - return - } - - tt.Assert.Equal(len(kase.want), len(results)) - for i := range kase.want { - tt.Assert.Equal(kase.want[i], results[i]) - } - }) - } -} diff --git a/services/horizon/internal/db2/history/asset_stats.go b/services/horizon/internal/db2/history/asset_stats.go index b13d913141..94f0d61e00 100644 --- a/services/horizon/internal/db2/history/asset_stats.go +++ b/services/horizon/internal/db2/history/asset_stats.go @@ -6,6 +6,7 @@ import ( "strings" sq "github.com/Masterminds/squirrel" + "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" @@ -33,11 +34,23 @@ func assetStatToPrimaryKeyMap(assetStat ExpAssetStat) map[string]interface{} { } } +// ContractAssetStatRow represents a row in the contract_asset_stats table +type ContractAssetStatRow struct { + // ContractID is the contract id of the stellar asset contract + ContractID []byte `db:"contract_id"` + // Stat is a json blob containing statistics on the contract holders + // this asset + Stat ContractStat `db:"stat"` +} + // InsertAssetStats a set of asset stats into the exp_asset_stats -func (q *Q) InsertAssetStats(ctx context.Context, assetStats []ExpAssetStat, batchSize int) error { +func (q *Q) InsertAssetStats(ctx context.Context, assetStats []ExpAssetStat) error { + if len(assetStats) == 0 { + return nil + } + builder := &db.BatchInsertBuilder{ - Table: q.GetTable("exp_asset_stats"), - MaxBatchSize: batchSize, + Table: q.GetTable("exp_asset_stats"), } for _, assetStat := range assetStats { @@ -53,6 +66,175 @@ func (q *Q) InsertAssetStats(ctx context.Context, assetStats []ExpAssetStat, bat return nil } +// InsertContractAssetStats inserts the given list of rows into the contract_asset_stats table +func (q *Q) InsertContractAssetStats(ctx context.Context, rows []ContractAssetStatRow) error { + if len(rows) == 0 { + return nil + } + builder := &db.BatchInsertBuilder{ + Table: q.GetTable("contract_asset_stats"), + } + + for _, row := range rows { + if err := builder.RowStruct(ctx, row); err != nil { + return errors.Wrap(err, "could not insert asset assetStat row") + } + } + + if err := builder.Exec(ctx); err != nil { + return errors.Wrap(err, "could not exec asset assetStats insert builder") + } + + return nil +} + +// ContractAssetBalance represents a row in the contract_asset_balances table +type ContractAssetBalance struct { + // KeyHash is a hash of the contract balance's ledger entry key + KeyHash []byte `db:"key_hash"` + // ContractID is the contract id of the stellar asset contract + ContractID []byte `db:"asset_contract_id"` + // Amount is the amount held by the contract + Amount string `db:"amount"` + // ExpirationLedger is the latest ledger for which this contract balance + // ledger entry is active + ExpirationLedger uint32 `db:"expiration_ledger"` +} + +// InsertContractAssetBalances will insert the given list of rows into the contract_asset_balances table +func (q *Q) InsertContractAssetBalances(ctx context.Context, rows []ContractAssetBalance) error { + if len(rows) == 0 { + return nil + } + builder := &db.BatchInsertBuilder{ + Table: q.GetTable("contract_asset_balances"), + } + + for _, row := range rows { + if err := builder.RowStruct(ctx, row); err != nil { + return errors.Wrap(err, "could not insert asset assetStat row") + } + } + + if err := builder.Exec(ctx); err != nil { + return errors.Wrap(err, "could not exec asset assetStats insert builder") + } + + return nil +} + +const maxUpdateBatchSize = 30000 + +// UpdateContractAssetBalanceAmounts will update the expiration ledgers for the given list of keys +// (if they exist in the db). +func (q *Q) UpdateContractAssetBalanceAmounts(ctx context.Context, keys []xdr.Hash, amounts []string) error { + for len(keys) > 0 { + var args []interface{} + var values []string + + for i := 0; len(keys) > 0 && i < maxUpdateBatchSize; i++ { + args = append(args, keys[0][:], amounts[0]) + values = append(values, "(cast(? as bytea), cast(? as numeric))") + keys = keys[1:] + amounts = amounts[1:] + } + + sql := fmt.Sprintf(` + UPDATE contract_asset_balances + SET + amount = myvalues.amount + FROM ( + VALUES + %s + ) AS myvalues (key_hash, amount) + WHERE contract_asset_balances.key_hash = myvalues.key_hash`, + strings.Join(values, ","), + ) + + _, err := q.ExecRaw(ctx, sql, args...) + if err != nil { + return err + } + } + return nil +} + +// UpdateContractAssetBalanceExpirations will update the expiration ledgers for the given list of keys +// (if they exist in the db). +func (q *Q) UpdateContractAssetBalanceExpirations(ctx context.Context, keys []xdr.Hash, expirationLedgers []uint32) error { + for len(keys) > 0 { + var args []interface{} + var values []string + + for i := 0; len(keys) > 0 && i < maxUpdateBatchSize; i++ { + args = append(args, keys[0][:], expirationLedgers[0]) + values = append(values, "(cast(? as bytea), cast(? as integer))") + keys = keys[1:] + expirationLedgers = expirationLedgers[1:] + } + + sql := fmt.Sprintf(` + UPDATE contract_asset_balances + SET + expiration_ledger = myvalues.expiration + FROM ( + VALUES + %s + ) AS myvalues (key_hash, expiration) + WHERE contract_asset_balances.key_hash = myvalues.key_hash`, + strings.Join(values, ","), + ) + + _, err := q.ExecRaw(ctx, sql, args...) + if err != nil { + return err + } + } + + return nil +} + +// GetContractAssetBalancesExpiringAt returns all contract asset balances which are active +// at `ledger` and expired at `ledger+1` +func (q *Q) GetContractAssetBalancesExpiringAt(ctx context.Context, ledger uint32) ([]ContractAssetBalance, error) { + sql := sq.Select("contract_asset_balances.*").From("contract_asset_balances"). + Where(map[string]interface{}{"expiration_ledger": ledger}) + var balances []ContractAssetBalance + err := q.Select(ctx, &balances, sql) + return balances, err +} + +// GetContractAssetBalances fetches all contract_asset_balances rows for the +// given list of key hashes. +func (q *Q) GetContractAssetBalances(ctx context.Context, keys []xdr.Hash) ([]ContractAssetBalance, error) { + keyBytes := make([][]byte, len(keys)) + for i := range keys { + keyBytes[i] = keys[i][:] + } + sql := sq.Select("contract_asset_balances.*").From("contract_asset_balances"). + Where(map[string]interface{}{"key_hash": keyBytes}) + var balances []ContractAssetBalance + err := q.Select(ctx, &balances, sql) + return balances, err +} + +// RemoveContractAssetBalances removes rows from the contract_asset_balances table +func (q *Q) RemoveContractAssetBalances(ctx context.Context, keys []xdr.Hash) error { + if len(keys) == 0 { + return nil + } + keyBytes := make([][]byte, len(keys)) + for i := range keys { + keyBytes[i] = keys[i][:] + } + + _, err := q.Exec(ctx, sq.Delete("contract_asset_balances"). + Where(map[string]interface{}{ + "key_hash": keyBytes, + })) + return err +} + // InsertAssetStat a single asset assetStat row into the exp_asset_stats // Returns number of rows affected and error. func (q *Q) InsertAssetStat(ctx context.Context, assetStat ExpAssetStat) (int64, error) { @@ -65,6 +247,20 @@ func (q *Q) InsertAssetStat(ctx context.Context, assetStat ExpAssetStat) (int64, return result.RowsAffected() } +// InsertContractAssetStat inserts a row into the contract_asset_stats table +func (q *Q) InsertContractAssetStat(ctx context.Context, row ContractAssetStatRow) (int64, error) { + sql := sq.Insert("contract_asset_stats").SetMap(map[string]interface{}{ + "contract_id": row.ContractID, + "stat": row.Stat, + }) + result, err := q.Exec(ctx, sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + // UpdateAssetStat updates a row in the exp_asset_stats table. // Returns number of rows affected and error. func (q *Q) UpdateAssetStat(ctx context.Context, assetStat ExpAssetStat) (int64, error) { @@ -79,6 +275,19 @@ func (q *Q) UpdateAssetStat(ctx context.Context, assetStat ExpAssetStat) (int64, return result.RowsAffected() } +// UpdateContractAssetStat updates a row in the contract_asset_stats table. +// Returns number of rows afected and error. +func (q *Q) UpdateContractAssetStat(ctx context.Context, row ContractAssetStatRow) (int64, error) { + sql := sq.Update("contract_asset_stats").Set("stat", row.Stat). + Where("contract_id = ?", row.ContractID) + result, err := q.Exec(ctx, sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + // RemoveAssetStat removes a row in the exp_asset_stats table. func (q *Q) RemoveAssetStat(ctx context.Context, assetType xdr.AssetType, assetCode, assetIssuer string) (int64, error) { sql := sq.Delete("exp_asset_stats"). @@ -95,6 +304,18 @@ func (q *Q) RemoveAssetStat(ctx context.Context, assetType xdr.AssetType, assetC return result.RowsAffected() } +// RemoveAssetContractStat removes a row in the contract_asset_stats table. +func (q *Q) RemoveAssetContractStat(ctx context.Context, contractID []byte) (int64, error) { + sql := sq.Delete("contract_asset_stats"). + Where("contract_id = ?", contractID) + result, err := q.Exec(ctx, sql) + if err != nil { + return 0, err + } + + return result.RowsAffected() +} + // GetAssetStat returns a row in the exp_asset_stats table. func (q *Q) GetAssetStat(ctx context.Context, assetType xdr.AssetType, assetCode, assetIssuer string) (ExpAssetStat, error) { sql := selectAssetStats.Where(map[string]interface{}{ @@ -107,23 +328,21 @@ func (q *Q) GetAssetStat(ctx context.Context, assetType xdr.AssetType, assetCode return assetStat, err } -func (q *Q) GetAssetStatByContract(ctx context.Context, contractID [32]byte) (ExpAssetStat, error) { - sql := selectAssetStats.Where("contract_id = ?", contractID[:]) - var assetStat ExpAssetStat +// GetContractAssetStat returns a row in the contract_asset_stats table. +func (q *Q) GetContractAssetStat(ctx context.Context, contractID []byte) (ContractAssetStatRow, error) { + sql := sq.Select("*").From("contract_asset_stats").Where("contract_id = ?", contractID) + var assetStat ContractAssetStatRow err := q.Get(ctx, &assetStat, sql) return assetStat, err } -func (q *Q) GetAssetStatByContracts(ctx context.Context, contractIDs [][32]byte) ([]ExpAssetStat, error) { - contractIDBytes := make([][]byte, len(contractIDs)) - for i := range contractIDs { - contractIDBytes[i] = contractIDs[i][:] - } - sql := selectAssetStats.Where(map[string]interface{}{"contract_id": contractIDBytes}) - - var assetStats []ExpAssetStat - err := q.Select(ctx, &assetStats, sql) - return assetStats, err +// GetAssetStatByContract returns the row in the exp_asset_stats table corresponding +// to the given contract id +func (q *Q) GetAssetStatByContract(ctx context.Context, contractID xdr.Hash) (ExpAssetStat, error) { + sql := selectAssetStats.Where("contract_id = ?", contractID[:]) + var assetStat ExpAssetStat + err := q.Get(ctx, &assetStat, sql) + return assetStat, err } func parseAssetStatsCursor(cursor string) (string, string, error) { @@ -158,8 +377,10 @@ func parseAssetStatsCursor(cursor string) (string, string, error) { } // GetAssetStats returns a page of exp_asset_stats rows. -func (q *Q) GetAssetStats(ctx context.Context, assetCode, assetIssuer string, page db2.PageQuery) ([]ExpAssetStat, error) { - sql := selectAssetStats +func (q *Q) GetAssetStats(ctx context.Context, assetCode, assetIssuer string, page db2.PageQuery) ([]AssetAndContractStat, error) { + sql := sq.Select("exp_asset_stats.*, contract_asset_stats.stat as contracts"). + From("exp_asset_stats"). + LeftJoin("contract_asset_stats ON exp_asset_stats.contract_id = contract_asset_stats.contract_id") filters := map[string]interface{}{} if assetCode != "" { filters["asset_code"] = assetCode @@ -193,7 +414,7 @@ func (q *Q) GetAssetStats(ctx context.Context, assetCode, assetIssuer string, pa sql = sql.OrderBy("(asset_code, asset_issuer) " + orderBy).Limit(page.Limit) - var results []ExpAssetStat + var results []AssetAndContractStat if err := q.Select(ctx, &results, sql); err != nil { return nil, errors.Wrap(err, "could not run select query") } diff --git a/services/horizon/internal/db2/history/asset_stats_test.go b/services/horizon/internal/db2/history/asset_stats_test.go index ceeb962a05..6485aec02b 100644 --- a/services/horizon/internal/db2/history/asset_stats_test.go +++ b/services/horizon/internal/db2/history/asset_stats_test.go @@ -1,10 +1,15 @@ package history import ( + "bytes" + "context" "database/sql" "sort" "testing" + "github.com/stretchr/testify/assert" + "golang.org/x/exp/slices" + "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/services/horizon/internal/test" "github.com/stellar/go/xdr" @@ -25,7 +30,6 @@ func TestAssetStatContracts(t *testing.T) { ClaimableBalances: 0, LiquidityPools: 0, Unauthorized: 0, - Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "0", @@ -33,7 +37,6 @@ func TestAssetStatContracts(t *testing.T) { ClaimableBalances: "0", LiquidityPools: "0", Unauthorized: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -46,7 +49,6 @@ func TestAssetStatContracts(t *testing.T) { Authorized: 1, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, - Contracts: 7, }, Balances: ExpAssetStatBalances{ Authorized: "23", @@ -54,7 +56,6 @@ func TestAssetStatContracts(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", - Contracts: "60", }, Amount: "23", NumAccounts: 1, @@ -67,7 +68,6 @@ func TestAssetStatContracts(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, - Contracts: 8, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -75,7 +75,6 @@ func TestAssetStatContracts(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", - Contracts: "90", }, Amount: "1", NumAccounts: 2, @@ -86,7 +85,7 @@ func TestAssetStatContracts(t *testing.T) { assetStats[i].SetContractID(contractID) contractID[0]++ } - tt.Assert.NoError(q.InsertAssetStats(tt.Ctx, assetStats, 1)) + tt.Assert.NoError(q.InsertAssetStats(tt.Ctx, assetStats)) contractID[0] = 0 for i := 0; i < 2; i++ { @@ -97,22 +96,9 @@ func TestAssetStatContracts(t *testing.T) { contractID[0]++ } - contractIDs := make([][32]byte, 2) - contractIDs[1][0]++ - rows, err := q.GetAssetStatByContracts(tt.Ctx, contractIDs) - tt.Assert.NoError(err) - tt.Assert.Len(rows, 2) - sort.Slice(rows, func(i, j int) bool { - return rows[i].AssetCode < rows[j].AssetCode - }) - - for i, row := range rows { - tt.Assert.True(row.Equals(assetStats[i])) - } - usd := assetStats[2] usd.SetContractID([32]byte{}) - _, err = q.UpdateAssetStat(tt.Ctx, usd) + _, err := q.UpdateAssetStat(tt.Ctx, usd) tt.Assert.EqualError(err, "exec failed: pq: duplicate key value violates unique constraint \"exp_asset_stats_contract_id_key\"") usd.SetContractID([32]byte{2}) @@ -129,17 +115,73 @@ func TestAssetStatContracts(t *testing.T) { tt.Assert.True(assetStat.Equals(assetStats[i])) contractID[0]++ } +} - contractIDs = [][32]byte{{}, {1}, {2}} - rows, err = q.GetAssetStatByContracts(tt.Ctx, contractIDs) +func TestAssetContractStats(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + c1 := ContractAssetStatRow{ + ContractID: []byte{1}, + Stat: ContractStat{ + ActiveBalance: "100", + ActiveHolders: 2, + ArchivedBalance: "0", + ArchivedHolders: 0, + }, + } + c2 := ContractAssetStatRow{ + ContractID: []byte{2}, + Stat: ContractStat{ + ActiveBalance: "40", + ActiveHolders: 1, + ArchivedBalance: "0", + ArchivedHolders: 0, + }, + } + c3 := ContractAssetStatRow{ + ContractID: []byte{3}, + Stat: ContractStat{ + ActiveBalance: "900", + ActiveHolders: 12, + ArchivedBalance: "23", + ArchivedHolders: 3, + }, + } + + rows := []ContractAssetStatRow{c1, c2, c3} + tt.Assert.NoError(q.InsertContractAssetStats(tt.Ctx, rows)) + + for _, row := range rows { + result, err := q.GetContractAssetStat(tt.Ctx, row.ContractID) + tt.Assert.NoError(err) + tt.Assert.Equal(result, row) + } + + c2.Stat.ActiveHolders = 3 + c2.Stat.ActiveBalance = "20" + c3.Stat.ArchivedBalance = "900" + c2.Stat.ActiveHolders = 5 + numRows, err := q.UpdateContractAssetStat(tt.Ctx, c2) tt.Assert.NoError(err) - tt.Assert.Len(rows, 3) - sort.Slice(rows, func(i, j int) bool { - return rows[i].AssetCode < rows[j].AssetCode - }) + tt.Assert.Equal(int64(1), numRows) + row, err := q.GetContractAssetStat(tt.Ctx, c2.ContractID) + tt.Assert.NoError(err) + tt.Assert.Equal(c2, row) - for i, row := range rows { - tt.Assert.True(row.Equals(assetStats[i])) + numRows, err = q.RemoveAssetContractStat(tt.Ctx, c3.ContractID) + tt.Assert.NoError(err) + tt.Assert.Equal(int64(1), numRows) + + _, err = q.GetContractAssetStat(tt.Ctx, c3.ContractID) + tt.Assert.Equal(sql.ErrNoRows, err) + + for _, row := range []ContractAssetStatRow{c1, c2} { + result, err := q.GetContractAssetStat(tt.Ctx, row.ContractID) + tt.Assert.NoError(err) + tt.Assert.Equal(result, row) } } @@ -148,7 +190,7 @@ func TestInsertAssetStats(t *testing.T) { defer tt.Finish() test.ResetHorizonDB(t, tt.HorizonDB) q := &Q{tt.HorizonSession()} - tt.Assert.NoError(q.InsertAssetStats(tt.Ctx, []ExpAssetStat{}, 1)) + tt.Assert.NoError(q.InsertAssetStats(tt.Ctx, []ExpAssetStat{})) assetStats := []ExpAssetStat{ { @@ -159,7 +201,6 @@ func TestInsertAssetStats(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, - Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -167,7 +208,6 @@ func TestInsertAssetStats(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", - Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -180,7 +220,6 @@ func TestInsertAssetStats(t *testing.T) { Authorized: 1, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, - Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "23", @@ -188,13 +227,12 @@ func TestInsertAssetStats(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", - Contracts: "0", }, Amount: "23", NumAccounts: 1, }, } - tt.Assert.NoError(q.InsertAssetStats(tt.Ctx, assetStats, 1)) + tt.Assert.NoError(q.InsertAssetStats(tt.Ctx, assetStats)) for _, assetStat := range assetStats { got, err := q.GetAssetStat(tt.Ctx, assetStat.AssetType, assetStat.AssetCode, assetStat.AssetIssuer) @@ -218,7 +256,6 @@ func TestInsertAssetStat(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, - Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -226,7 +263,6 @@ func TestInsertAssetStat(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", - Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -239,7 +275,6 @@ func TestInsertAssetStat(t *testing.T) { Authorized: 1, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, - Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "23", @@ -247,7 +282,6 @@ func TestInsertAssetStat(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", - Contracts: "0", }, Amount: "23", NumAccounts: 1, @@ -279,7 +313,6 @@ func TestInsertAssetStatAlreadyExistsError(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, - Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -287,7 +320,6 @@ func TestInsertAssetStatAlreadyExistsError(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", - Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -328,7 +360,6 @@ func TestUpdateAssetStatDoesNotExistsError(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, - Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -336,7 +367,6 @@ func TestUpdateAssetStatDoesNotExistsError(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", - Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -365,7 +395,6 @@ func TestUpdateStat(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, - Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -373,7 +402,6 @@ func TestUpdateStat(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", - Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -388,9 +416,7 @@ func TestUpdateStat(t *testing.T) { tt.Assert.Equal(got, assetStat) assetStat.NumAccounts = 50 - assetStat.Accounts.Contracts = 4 assetStat.Amount = "23" - assetStat.Balances.Contracts = "56" assetStat.SetContractID([32]byte{23}) numChanged, err = q.UpdateAssetStat(tt.Ctx, assetStat) @@ -416,7 +442,6 @@ func TestGetAssetStatDoesNotExist(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, - Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -424,7 +449,6 @@ func TestGetAssetStatDoesNotExist(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", - Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -449,7 +473,6 @@ func TestRemoveAssetStat(t *testing.T) { Authorized: 2, AuthorizedToMaintainLiabilities: 3, Unauthorized: 4, - Contracts: 0, }, Balances: ExpAssetStatBalances{ Authorized: "1", @@ -457,7 +480,6 @@ func TestRemoveAssetStat(t *testing.T) { Unauthorized: "3", ClaimableBalances: "4", LiquidityPools: "5", - Contracts: "0", }, Amount: "1", NumAccounts: 2, @@ -565,123 +587,153 @@ func TestGetAssetStatsOrderValidation(t *testing.T) { tt.Assert.Contains(err.Error(), "invalid page order") } -func reverseAssetStats(a []ExpAssetStat) { - for i := len(a)/2 - 1; i >= 0; i-- { - opp := len(a) - 1 - i - a[i], a[opp] = a[opp], a[i] - } -} - func TestGetAssetStatsFiltersAndCursor(t *testing.T) { tt := test.Start(t) defer tt.Finish() test.ResetHorizonDB(t, tt.HorizonDB) q := &Q{tt.HorizonSession()} - - usdAssetStat := ExpAssetStat{ - AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, - AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", - AssetCode: "USD", - Accounts: ExpAssetStatAccounts{ - Authorized: 2, - AuthorizedToMaintainLiabilities: 3, - Unauthorized: 4, - Contracts: 0, - }, - Balances: ExpAssetStatBalances{ - Authorized: "1", - AuthorizedToMaintainLiabilities: "2", - Unauthorized: "3", - ClaimableBalances: "0", - LiquidityPools: "0", - Contracts: "0", - }, - Amount: "1", - NumAccounts: 2, + zero := ContractStat{ + ActiveBalance: "0", + ActiveHolders: 0, + ArchivedBalance: "0", + ArchivedHolders: 0, } - etherAssetStat := ExpAssetStat{ - AssetType: xdr.AssetTypeAssetTypeCreditAlphanum12, - AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", - AssetCode: "ETHER", - Accounts: ExpAssetStatAccounts{ - Authorized: 1, - AuthorizedToMaintainLiabilities: 3, - Unauthorized: 4, - Contracts: 0, - }, - Balances: ExpAssetStatBalances{ - Authorized: "23", - AuthorizedToMaintainLiabilities: "2", - Unauthorized: "3", - ClaimableBalances: "0", - LiquidityPools: "0", - Contracts: "0", + usdAssetStat := AssetAndContractStat{ + ExpAssetStat: ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + AssetCode: "USD", + Accounts: ExpAssetStatAccounts{ + Authorized: 2, + AuthorizedToMaintainLiabilities: 3, + Unauthorized: 4, + }, + Balances: ExpAssetStatBalances{ + Authorized: "1", + AuthorizedToMaintainLiabilities: "2", + Unauthorized: "3", + ClaimableBalances: "0", + LiquidityPools: "0", + }, + Amount: "1", + NumAccounts: 2, }, - Amount: "23", - NumAccounts: 1, + Contracts: zero, } - otherUSDAssetStat := ExpAssetStat{ - AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, - AssetIssuer: "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", - AssetCode: "USD", - Accounts: ExpAssetStatAccounts{ - Authorized: 2, - AuthorizedToMaintainLiabilities: 3, - Unauthorized: 4, - Contracts: 0, + etherAssetStat := AssetAndContractStat{ + ExpAssetStat: ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum12, + AssetIssuer: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + AssetCode: "ETHER", + Accounts: ExpAssetStatAccounts{ + Authorized: 1, + AuthorizedToMaintainLiabilities: 3, + Unauthorized: 4, + }, + Balances: ExpAssetStatBalances{ + Authorized: "23", + AuthorizedToMaintainLiabilities: "2", + Unauthorized: "3", + ClaimableBalances: "0", + LiquidityPools: "0", + }, + Amount: "23", + NumAccounts: 1, }, - Balances: ExpAssetStatBalances{ - Authorized: "1", - AuthorizedToMaintainLiabilities: "2", - Unauthorized: "3", - ClaimableBalances: "0", - LiquidityPools: "0", - Contracts: "0", + Contracts: zero, + } + otherUSDAssetStat := AssetAndContractStat{ + ExpAssetStat: ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + AssetCode: "USD", + Accounts: ExpAssetStatAccounts{ + Authorized: 2, + AuthorizedToMaintainLiabilities: 3, + Unauthorized: 4, + }, + Balances: ExpAssetStatBalances{ + Authorized: "1", + AuthorizedToMaintainLiabilities: "2", + Unauthorized: "3", + ClaimableBalances: "0", + LiquidityPools: "0", + }, + Amount: "1", + NumAccounts: 2, }, - Amount: "1", - NumAccounts: 2, + Contracts: zero, } - eurAssetStat := ExpAssetStat{ - AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, - AssetIssuer: "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", - AssetCode: "EUR", - Accounts: ExpAssetStatAccounts{ - Authorized: 3, - AuthorizedToMaintainLiabilities: 2, - Unauthorized: 4, - Contracts: 0, + eurAssetStat := AssetAndContractStat{ + ExpAssetStat: ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetIssuer: "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", + AssetCode: "EUR", + Accounts: ExpAssetStatAccounts{ + Authorized: 3, + AuthorizedToMaintainLiabilities: 2, + Unauthorized: 4, + }, + Balances: ExpAssetStatBalances{ + Authorized: "111", + AuthorizedToMaintainLiabilities: "2", + Unauthorized: "3", + ClaimableBalances: "1", + LiquidityPools: "2", + }, + Amount: "111", + NumAccounts: 3, }, - Balances: ExpAssetStatBalances{ - Authorized: "111", - AuthorizedToMaintainLiabilities: "2", - Unauthorized: "3", - ClaimableBalances: "1", - LiquidityPools: "2", - Contracts: "0", + Contracts: ContractStat{ + ActiveBalance: "120", + ActiveHolders: 3, + ArchivedBalance: "90", + ArchivedHolders: 1, }, - Amount: "111", - NumAccounts: 3, } - assetStats := []ExpAssetStat{ + eurAssetStat.SetContractID([32]byte{}) + assetStats := []AssetAndContractStat{ etherAssetStat, eurAssetStat, otherUSDAssetStat, usdAssetStat, } for _, assetStat := range assetStats { - numChanged, err := q.InsertAssetStat(tt.Ctx, assetStat) + numChanged, err := q.InsertAssetStat(tt.Ctx, assetStat.ExpAssetStat) tt.Assert.NoError(err) tt.Assert.Equal(numChanged, int64(1)) + if assetStat.Contracts != zero { + numChanged, err = q.InsertContractAssetStat(tt.Ctx, ContractAssetStatRow{ + ContractID: *assetStat.ContractID, + Stat: assetStat.Contracts, + }) + tt.Assert.NoError(err) + tt.Assert.Equal(numChanged, int64(1)) + } } + // insert contract stat which has no corresponding asset stat row + // to test that it isn't included in the results + numChanged, err := q.InsertContractAssetStat(tt.Ctx, ContractAssetStatRow{ + ContractID: []byte{1}, + Stat: ContractStat{ + ActiveBalance: "400", + ActiveHolders: 30, + ArchivedBalance: "0", + ArchivedHolders: 0, + }, + }) + tt.Assert.NoError(err) + tt.Assert.Equal(numChanged, int64(1)) + for _, testCase := range []struct { name string assetCode string assetIssuer string cursor string order string - expected []ExpAssetStat + expected []AssetAndContractStat }{ { "no filter without cursor", @@ -689,7 +741,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "", "", "asc", - []ExpAssetStat{ + []AssetAndContractStat{ etherAssetStat, eurAssetStat, otherUSDAssetStat, @@ -702,7 +754,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "", "ABC_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "asc", - []ExpAssetStat{ + []AssetAndContractStat{ etherAssetStat, eurAssetStat, otherUSDAssetStat, @@ -715,7 +767,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "", "ZZZ_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum4", "desc", - []ExpAssetStat{ + []AssetAndContractStat{ usdAssetStat, otherUSDAssetStat, eurAssetStat, @@ -728,7 +780,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "", "ETHER_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum12", "asc", - []ExpAssetStat{ + []AssetAndContractStat{ eurAssetStat, otherUSDAssetStat, usdAssetStat, @@ -740,7 +792,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "", "EUR_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "desc", - []ExpAssetStat{ + []AssetAndContractStat{ etherAssetStat, }, }, @@ -750,7 +802,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "", "EUR_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum4", "desc", - []ExpAssetStat{ + []AssetAndContractStat{ eurAssetStat, etherAssetStat, }, @@ -761,7 +813,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "", "", "asc", - []ExpAssetStat{ + []AssetAndContractStat{ otherUSDAssetStat, usdAssetStat, }, @@ -772,7 +824,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "", "USD_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "asc", - []ExpAssetStat{ + []AssetAndContractStat{ usdAssetStat, }, }, @@ -782,7 +834,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "", "USD_GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H_credit_alphanum4", "desc", - []ExpAssetStat{ + []AssetAndContractStat{ otherUSDAssetStat, }, }, @@ -792,7 +844,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", "", "asc", - []ExpAssetStat{ + []AssetAndContractStat{ eurAssetStat, otherUSDAssetStat, }, @@ -803,7 +855,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", "EUR_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "asc", - []ExpAssetStat{ + []AssetAndContractStat{ otherUSDAssetStat, }, }, @@ -813,7 +865,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", "USD_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "desc", - []ExpAssetStat{ + []AssetAndContractStat{ eurAssetStat, }, }, @@ -871,7 +923,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", "", "asc", - []ExpAssetStat{ + []AssetAndContractStat{ otherUSDAssetStat, }, }, @@ -881,7 +933,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", "USC_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "asc", - []ExpAssetStat{ + []AssetAndContractStat{ otherUSDAssetStat, }, }, @@ -891,7 +943,7 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", "USE_GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2_credit_alphanum4", "desc", - []ExpAssetStat{ + []AssetAndContractStat{ otherUSDAssetStat, }, }, @@ -929,9 +981,218 @@ func TestGetAssetStatsFiltersAndCursor(t *testing.T) { results, err = q.GetAssetStats(tt.Ctx, testCase.assetCode, testCase.assetIssuer, page) tt.Assert.NoError(err) - reverseAssetStats(results) + slices.Reverse(results) tt.Assert.Equal(testCase.expected, results) } }) } } + +func assertContractAssetBalancesEqual(t *testing.T, balances, otherBalances []ContractAssetBalance) { + assert.Equal(t, len(balances), len(otherBalances)) + + sort.Slice(balances, func(i, j int) bool { + return bytes.Compare(balances[i].KeyHash, balances[j].KeyHash) < 0 + }) + sort.Slice(otherBalances, func(i, j int) bool { + return bytes.Compare(otherBalances[i].KeyHash, otherBalances[j].KeyHash) < 0 + }) + + for i, balance := range balances { + other := otherBalances[i] + assert.Equal(t, balance, other) + } +} + +func TestInsertContractAssetBalances(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + + q := &Q{tt.HorizonSession()} + + keyHash := [32]byte{} + contractID := [32]byte{1} + balance := ContractAssetBalance{ + KeyHash: keyHash[:], + ContractID: contractID[:], + Amount: "100", + ExpirationLedger: 10, + } + + otherKeyHash := [32]byte{2} + otherContractID := [32]byte{3} + otherBalance := ContractAssetBalance{ + KeyHash: otherKeyHash[:], + ContractID: otherContractID[:], + Amount: "101", + ExpirationLedger: 11, + } + + tt.Assert.NoError( + q.InsertContractAssetBalances(context.Background(), []ContractAssetBalance{balance, otherBalance}), + ) + + nonExistantKeyHash := [32]byte{4} + balances, err := q.GetContractAssetBalances(context.Background(), []xdr.Hash{keyHash, otherKeyHash, nonExistantKeyHash}) + tt.Assert.NoError(err) + + assertContractAssetBalancesEqual(t, balances, []ContractAssetBalance{balance, otherBalance}) +} + +func TestRemoveContractAssetBalances(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + + q := &Q{tt.HorizonSession()} + + keyHash := [32]byte{} + contractID := [32]byte{1} + balance := ContractAssetBalance{ + KeyHash: keyHash[:], + ContractID: contractID[:], + Amount: "100", + ExpirationLedger: 10, + } + + otherKeyHash := [32]byte{2} + otherContractID := [32]byte{3} + otherBalance := ContractAssetBalance{ + KeyHash: otherKeyHash[:], + ContractID: otherContractID[:], + Amount: "101", + ExpirationLedger: 11, + } + + tt.Assert.NoError( + q.InsertContractAssetBalances(context.Background(), []ContractAssetBalance{balance, otherBalance}), + ) + nonExistantKeyHash := xdr.Hash{4} + + tt.Assert.NoError( + q.RemoveContractAssetBalances(context.Background(), []xdr.Hash{nonExistantKeyHash}), + ) + balances, err := q.GetContractAssetBalances(context.Background(), []xdr.Hash{keyHash, otherKeyHash, nonExistantKeyHash}) + tt.Assert.NoError(err) + + assertContractAssetBalancesEqual(t, balances, []ContractAssetBalance{balance, otherBalance}) + + tt.Assert.NoError( + q.RemoveContractAssetBalances(context.Background(), []xdr.Hash{nonExistantKeyHash, otherKeyHash}), + ) + + balances, err = q.GetContractAssetBalances(context.Background(), []xdr.Hash{keyHash, otherKeyHash, nonExistantKeyHash}) + tt.Assert.NoError(err) + + assertContractAssetBalancesEqual(t, balances, []ContractAssetBalance{balance}) +} + +func TestUpdateContractAssetBalanceAmounts(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + + q := &Q{tt.HorizonSession()} + + keyHash := [32]byte{} + contractID := [32]byte{1} + balance := ContractAssetBalance{ + KeyHash: keyHash[:], + ContractID: contractID[:], + Amount: "100", + ExpirationLedger: 10, + } + + otherKeyHash := [32]byte{2} + otherContractID := [32]byte{3} + otherBalance := ContractAssetBalance{ + KeyHash: otherKeyHash[:], + ContractID: otherContractID[:], + Amount: "101", + ExpirationLedger: 11, + } + + tt.Assert.NoError( + q.InsertContractAssetBalances(context.Background(), []ContractAssetBalance{balance, otherBalance}), + ) + + nonExistantKeyHash := xdr.Hash{4} + + tt.Assert.NoError( + q.UpdateContractAssetBalanceAmounts( + context.Background(), + []xdr.Hash{otherKeyHash, keyHash, nonExistantKeyHash}, + []string{"1", "2", "3"}, + ), + ) + + balances, err := q.GetContractAssetBalances(context.Background(), []xdr.Hash{keyHash, otherKeyHash}) + tt.Assert.NoError(err) + + balance.Amount = "2" + otherBalance.Amount = "1" + assertContractAssetBalancesEqual(t, balances, []ContractAssetBalance{balance, otherBalance}) +} + +func TestUpdateContractAssetBalanceExpirations(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + + q := &Q{tt.HorizonSession()} + + keyHash := [32]byte{} + contractID := [32]byte{1} + balance := ContractAssetBalance{ + KeyHash: keyHash[:], + ContractID: contractID[:], + Amount: "100", + ExpirationLedger: 10, + } + + otherKeyHash := [32]byte{2} + otherContractID := [32]byte{3} + otherBalance := ContractAssetBalance{ + KeyHash: otherKeyHash[:], + ContractID: otherContractID[:], + Amount: "101", + ExpirationLedger: 11, + } + + tt.Assert.NoError( + q.InsertContractAssetBalances(context.Background(), []ContractAssetBalance{balance, otherBalance}), + ) + + balances, err := q.GetContractAssetBalancesExpiringAt(context.Background(), 10) + tt.Assert.NoError(err) + assertContractAssetBalancesEqual(t, balances, []ContractAssetBalance{balance}) + + balances, err = q.GetContractAssetBalancesExpiringAt(context.Background(), 11) + tt.Assert.NoError(err) + assertContractAssetBalancesEqual(t, balances, []ContractAssetBalance{otherBalance}) + + nonExistantKeyHash := xdr.Hash{4} + + tt.Assert.NoError( + q.UpdateContractAssetBalanceExpirations( + context.Background(), + []xdr.Hash{otherKeyHash, keyHash, nonExistantKeyHash}, + []uint32{200, 200, 500}, + ), + ) + + balances, err = q.GetContractAssetBalances(context.Background(), []xdr.Hash{keyHash, otherKeyHash}) + tt.Assert.NoError(err) + balance.ExpirationLedger = 200 + otherBalance.ExpirationLedger = 200 + assertContractAssetBalancesEqual(t, balances, []ContractAssetBalance{balance, otherBalance}) + + balances, err = q.GetContractAssetBalancesExpiringAt(context.Background(), 10) + tt.Assert.NoError(err) + assert.Empty(t, balances) + + balances, err = q.GetContractAssetBalancesExpiringAt(context.Background(), 200) + tt.Assert.NoError(err) + assertContractAssetBalancesEqual(t, balances, []ContractAssetBalance{balance, otherBalance}) +} diff --git a/services/horizon/internal/db2/history/main.go b/services/horizon/internal/db2/history/main.go index 0acae7cb18..5bb5cff13a 100644 --- a/services/horizon/internal/db2/history/main.go +++ b/services/horizon/internal/db2/history/main.go @@ -368,6 +368,50 @@ type Asset struct { Issuer string `db:"asset_issuer"` } +type ContractStat struct { + ActiveBalance string `json:"balance"` + ActiveHolders int32 `json:"holders"` + ArchivedBalance string `json:"archived_balance"` + ArchivedHolders int32 `json:"archived_holders"` +} + +func (c ContractStat) Value() (driver.Value, error) { + return json.Marshal(c) +} + +func (c *ContractStat) Scan(src interface{}) error { + if src == nil { + c.ActiveBalance = "0" + c.ArchivedBalance = "0" + return nil + } + + source, ok := src.([]byte) + if !ok { + return errors.New("Type assertion .([]byte) failed.") + } + + err := json.Unmarshal(source, &c) + if err != nil { + return err + } + + // Sets zero values for empty balances + if c.ActiveBalance == "" { + c.ActiveBalance = "0" + } + if c.ArchivedBalance == "" { + c.ArchivedBalance = "0" + } + + return nil +} + +type AssetAndContractStat struct { + ExpAssetStat + Contracts ContractStat `db:"contracts"` +} + // ExpAssetStat is a row in the exp_asset_stats table representing the stats per Asset type ExpAssetStat struct { AssetType xdr.AssetType `db:"asset_type"` @@ -397,7 +441,6 @@ type ExpAssetStatAccounts struct { AuthorizedToMaintainLiabilities int32 `json:"authorized_to_maintain_liabilities"` ClaimableBalances int32 `json:"claimable_balances"` LiquidityPools int32 `json:"liquidity_pools"` - Contracts int32 `json:"contracts"` Unauthorized int32 `json:"unauthorized"` } @@ -454,7 +497,6 @@ func (a ExpAssetStatAccounts) Add(b ExpAssetStatAccounts) ExpAssetStatAccounts { ClaimableBalances: a.ClaimableBalances + b.ClaimableBalances, LiquidityPools: a.LiquidityPools + b.LiquidityPools, Unauthorized: a.Unauthorized + b.Unauthorized, - Contracts: a.Contracts + b.Contracts, } } @@ -469,7 +511,6 @@ type ExpAssetStatBalances struct { AuthorizedToMaintainLiabilities string `json:"authorized_to_maintain_liabilities"` ClaimableBalances string `json:"claimable_balances"` LiquidityPools string `json:"liquidity_pools"` - Contracts string `json:"contracts"` Unauthorized string `json:"unauthorized"` } @@ -479,7 +520,6 @@ func (e ExpAssetStatBalances) IsZero() bool { AuthorizedToMaintainLiabilities: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", Unauthorized: "0", } } @@ -515,23 +555,30 @@ func (e *ExpAssetStatBalances) Scan(src interface{}) error { if e.Unauthorized == "" { e.Unauthorized = "0" } - if e.Contracts == "" { - e.Contracts = "0" - } return nil } // QAssetStats defines exp_asset_stats related queries. type QAssetStats interface { - InsertAssetStats(ctx context.Context, stats []ExpAssetStat, batchSize int) error + InsertContractAssetBalances(ctx context.Context, rows []ContractAssetBalance) error + RemoveContractAssetBalances(ctx context.Context, keys []xdr.Hash) error + UpdateContractAssetBalanceAmounts(ctx context.Context, keys []xdr.Hash, amounts []string) error + UpdateContractAssetBalanceExpirations(ctx context.Context, keys []xdr.Hash, expirationLedgers []uint32) error + GetContractAssetBalances(ctx context.Context, keys []xdr.Hash) ([]ContractAssetBalance, error) + GetContractAssetBalancesExpiringAt(ctx context.Context, ledger uint32) ([]ContractAssetBalance, error) + InsertAssetStats(ctx context.Context, stats []ExpAssetStat) error + InsertContractAssetStats(ctx context.Context, rows []ContractAssetStatRow) error InsertAssetStat(ctx context.Context, stat ExpAssetStat) (int64, error) + InsertContractAssetStat(ctx context.Context, row ContractAssetStatRow) (int64, error) UpdateAssetStat(ctx context.Context, stat ExpAssetStat) (int64, error) + UpdateContractAssetStat(ctx context.Context, row ContractAssetStatRow) (int64, error) GetAssetStat(ctx context.Context, assetType xdr.AssetType, assetCode, assetIssuer string) (ExpAssetStat, error) - GetAssetStatByContract(ctx context.Context, contractID [32]byte) (ExpAssetStat, error) - GetAssetStatByContracts(ctx context.Context, contractIDs [][32]byte) ([]ExpAssetStat, error) + GetAssetStatByContract(ctx context.Context, contractID xdr.Hash) (ExpAssetStat, error) + GetContractAssetStat(ctx context.Context, contractID []byte) (ContractAssetStatRow, error) RemoveAssetStat(ctx context.Context, assetType xdr.AssetType, assetCode, assetIssuer string) (int64, error) - GetAssetStats(ctx context.Context, assetCode, assetIssuer string, page db2.PageQuery) ([]ExpAssetStat, error) + RemoveAssetContractStat(ctx context.Context, contractID []byte) (int64, error) + GetAssetStats(ctx context.Context, assetCode, assetIssuer string, page db2.PageQuery) ([]AssetAndContractStat, error) CountTrustLines(ctx context.Context) (int, error) } diff --git a/services/horizon/internal/db2/history/mock_q_asset_stats.go b/services/horizon/internal/db2/history/mock_q_asset_stats.go index 6b4563181c..84927b53ee 100644 --- a/services/horizon/internal/db2/history/mock_q_asset_stats.go +++ b/services/horizon/internal/db2/history/mock_q_asset_stats.go @@ -14,8 +14,63 @@ type MockQAssetStats struct { mock.Mock } -func (m *MockQAssetStats) InsertAssetStats(ctx context.Context, assetStats []ExpAssetStat, batchSize int) error { - a := m.Called(ctx, assetStats, batchSize) +func (m *MockQAssetStats) InsertContractAssetBalances(ctx context.Context, rows []ContractAssetBalance) error { + a := m.Called(ctx, rows) + return a.Error(0) +} + +func (m *MockQAssetStats) RemoveContractAssetBalances(ctx context.Context, keys []xdr.Hash) error { + a := m.Called(ctx, keys) + return a.Error(0) +} + +func (m *MockQAssetStats) UpdateContractAssetBalanceAmounts(ctx context.Context, keys []xdr.Hash, amounts []string) error { + a := m.Called(ctx, keys, amounts) + return a.Error(0) +} + +func (m *MockQAssetStats) UpdateContractAssetBalanceExpirations(ctx context.Context, keys []xdr.Hash, expirationLedgers []uint32) error { + a := m.Called(ctx, keys, expirationLedgers) + return a.Error(0) +} + +func (m *MockQAssetStats) GetContractAssetBalances(ctx context.Context, keys []xdr.Hash) ([]ContractAssetBalance, error) { + a := m.Called(ctx, keys) + return a.Get(0).([]ContractAssetBalance), a.Error(1) +} + +func (m *MockQAssetStats) GetContractAssetBalancesExpiringAt(ctx context.Context, ledger uint32) ([]ContractAssetBalance, error) { + a := m.Called(ctx, ledger) + return a.Get(0).([]ContractAssetBalance), a.Error(1) +} + +func (m *MockQAssetStats) InsertContractAssetStats(ctx context.Context, rows []ContractAssetStatRow) error { + a := m.Called(ctx, rows) + return a.Error(0) +} + +func (m *MockQAssetStats) InsertContractAssetStat(ctx context.Context, row ContractAssetStatRow) (int64, error) { + a := m.Called(ctx, row) + return a.Get(0).(int64), a.Error(1) +} + +func (m *MockQAssetStats) UpdateContractAssetStat(ctx context.Context, row ContractAssetStatRow) (int64, error) { + a := m.Called(ctx, row) + return a.Get(0).(int64), a.Error(1) +} + +func (m *MockQAssetStats) GetContractAssetStat(ctx context.Context, contractID []byte) (ContractAssetStatRow, error) { + a := m.Called(ctx, contractID) + return a.Get(0).(ContractAssetStatRow), a.Error(1) +} + +func (m *MockQAssetStats) RemoveAssetContractStat(ctx context.Context, contractID []byte) (int64, error) { + a := m.Called(ctx, contractID) + return a.Get(0).(int64), a.Error(1) +} + +func (m *MockQAssetStats) InsertAssetStats(ctx context.Context, assetStats []ExpAssetStat) error { + a := m.Called(ctx, assetStats) return a.Error(0) } @@ -34,7 +89,7 @@ func (m *MockQAssetStats) GetAssetStat(ctx context.Context, assetType xdr.AssetT return a.Get(0).(ExpAssetStat), a.Error(1) } -func (m *MockQAssetStats) GetAssetStatByContract(ctx context.Context, contractID [32]byte) (ExpAssetStat, error) { +func (m *MockQAssetStats) GetAssetStatByContract(ctx context.Context, contractID xdr.Hash) (ExpAssetStat, error) { a := m.Called(ctx, contractID) return a.Get(0).(ExpAssetStat), a.Error(1) } @@ -44,12 +99,12 @@ func (m *MockQAssetStats) RemoveAssetStat(ctx context.Context, assetType xdr.Ass return a.Get(0).(int64), a.Error(1) } -func (m *MockQAssetStats) GetAssetStats(ctx context.Context, assetCode, assetIssuer string, page db2.PageQuery) ([]ExpAssetStat, error) { +func (m *MockQAssetStats) GetAssetStats(ctx context.Context, assetCode, assetIssuer string, page db2.PageQuery) ([]AssetAndContractStat, error) { a := m.Called(ctx, assetCode, assetIssuer, page) - return a.Get(0).([]ExpAssetStat), a.Error(1) + return a.Get(0).([]AssetAndContractStat), a.Error(1) } -func (m *MockQAssetStats) GetAssetStatByContracts(ctx context.Context, contractIDs [][32]byte) ([]ExpAssetStat, error) { +func (m *MockQAssetStats) GetAssetStatByContracts(ctx context.Context, contractIDs []xdr.Hash) ([]ExpAssetStat, error) { a := m.Called(ctx, contractIDs) return a.Get(0).([]ExpAssetStat), a.Error(1) } diff --git a/services/horizon/internal/db2/schema/bindata.go b/services/horizon/internal/db2/schema/bindata.go index 3f1e470840..c22c742949 100644 --- a/services/horizon/internal/db2/schema/bindata.go +++ b/services/horizon/internal/db2/schema/bindata.go @@ -63,6 +63,7 @@ // migrations/64_add_payment_flag_history_ops.sql (145B) // migrations/65_drop_payment_index.sql (260B) // migrations/65_remove_unused_indexes.sql (2.897kB) +// migrations/66_contract_asset_stats.sql (583B) // migrations/6_create_assets_table.sql (366B) // migrations/7_modify_trades_table.sql (2.303kB) // migrations/8_add_aggregators.sql (907B) @@ -1396,6 +1397,26 @@ func migrations65_remove_unused_indexesSql() (*asset, error) { return a, nil } +var _migrations66_contract_asset_statsSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\x91\x41\x6b\xf2\x40\x10\x86\xef\xf9\x15\x2f\x9e\x94\xcf\xc0\xd7\x7a\xa9\x78\x8a\x35\x14\x5b\x9b\x48\x8c\x50\x4f\xcb\x66\x33\xc6\xa1\xba\x91\xdd\x91\xd6\x7f\x5f\x24\x44\x0b\x56\xf7\x3a\xcf\xcc\xf3\xce\x4e\x18\xe2\xdf\x8e\x2b\xa7\x85\xb0\xdc\x07\xcf\x59\x1c\xe5\x31\xf2\x68\x3c\x8b\x61\x6a\x2b\x4e\x1b\x51\xda\x7b\x12\xe5\x45\x8b\x47\x37\xc0\xe9\x9d\x6b\x5c\x62\xbc\xca\xe3\x08\xf3\x6c\xfa\x1e\x65\x2b\xbc\xc5\xab\x7e\xc3\x9c\x1a\xd0\xbc\xd7\x45\x9a\x8c\x91\xa4\x39\x92\xe5\x6c\x16\xf4\x46\xc1\x5d\x55\xa1\xb7\xda\x1a\x3a\xdb\x3e\xe9\xa8\x36\xda\x6f\x6e\xaa\x9a\xb6\xeb\x50\xad\xb1\xc5\x76\xf5\xc1\xb6\x99\xec\x61\x47\x8e\x4d\x77\x30\xec\xff\xef\x5d\x48\x84\x21\x06\x43\x94\x5c\xb1\x78\xb0\x87\x3f\xac\xd7\x6c\x98\xac\x60\x5d\x3b\x68\x3c\x3c\x3e\xa1\x60\x01\x5b\xa1\x8a\x5c\x33\x9a\xbe\xf7\xec\xb4\x70\x6d\xd5\x96\xca\x8a\x5c\x5b\xfe\x73\xeb\x69\x32\x89\x3f\xd0\xb9\xb1\xb6\x2a\x8e\xea\x32\xaf\x83\x34\xb9\xf9\x41\xcb\xc5\x34\x79\x41\x21\x8e\x08\xdd\xab\x0c\x27\xe3\xef\x0b\x4f\xea\x2f\x1b\x4c\xb2\x74\x7e\xef\xc2\x46\x7b\xa3\x4b\x1a\xdd\x01\xcf\xfa\x96\xfd\x09\x00\x00\xff\xff\x78\xde\xe2\x11\x47\x02\x00\x00") + +func migrations66_contract_asset_statsSqlBytes() ([]byte, error) { + return bindataRead( + _migrations66_contract_asset_statsSql, + "migrations/66_contract_asset_stats.sql", + ) +} + +func migrations66_contract_asset_statsSql() (*asset, error) { + bytes, err := migrations66_contract_asset_statsSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "migrations/66_contract_asset_stats.sql", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x2a, 0x9a, 0xab, 0xd3, 0x1d, 0x20, 0x82, 0x82, 0x64, 0xb1, 0x7e, 0x9f, 0xcb, 0xb5, 0xe6, 0x2b, 0xc2, 0x72, 0x8e, 0x1e, 0x58, 0x23, 0xc, 0xfd, 0x25, 0xb3, 0xe7, 0x9, 0x89, 0x43, 0x3e, 0x15}} + return a, nil +} + var _migrations6_create_assets_tableSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x6c\x90\x3d\x4f\xc3\x30\x18\x84\x77\xff\x8a\x1b\x1d\x91\x0e\x20\xe8\x92\xc9\x34\x16\x58\x18\xa7\xb8\x31\xa2\x53\xe5\x26\x16\x78\x80\x54\xb6\x11\xca\xbf\x47\xaa\x28\xf9\x50\xe6\x7b\xf4\xbc\xef\xdd\x6a\x85\xab\x4f\xff\x1e\x6c\x72\x30\x27\xb2\xd1\x9c\xd5\x1c\x35\xbb\x97\x1c\x1f\x3e\xa6\x2e\xf4\x07\x1b\xa3\x4b\x11\x94\x00\x80\x6f\xb1\xe3\x5a\x30\x89\xad\x16\xcf\x4c\xef\xf1\xc4\xf7\xc8\xcf\xd9\x19\x3c\xa4\xfe\xe4\xf0\xca\xf4\xe6\x91\x69\xba\xbe\xcd\xa0\xaa\x1a\xca\x48\x39\x86\x9a\xae\x1d\xa0\xeb\x9b\x65\xc8\xc7\xf8\xed\xc2\x3f\x76\xb7\x9e\x63\x46\x89\x17\xc3\xe9\xa0\xcc\x47\x3f\xe4\x13\x4b\x46\xb2\x82\x5c\xfa\x09\x55\xf2\xb7\xbf\xf8\xd8\x5f\xee\x54\x6a\x5e\xd9\xec\x84\x7a\xc0\x31\x05\xe7\x40\x27\xb6\x82\x90\xf1\x74\x65\xf7\xf3\x45\x4a\x5d\x6d\x97\xa7\x6b\x6c\x6c\x6c\xeb\x8a\xdf\x00\x00\x00\xff\xff\xfb\x53\x3e\x81\x6e\x01\x00\x00") func migrations6_create_assets_tableSqlBytes() ([]byte, error) { @@ -1650,6 +1671,7 @@ var _bindata = map[string]func() (*asset, error){ "migrations/64_add_payment_flag_history_ops.sql": migrations64_add_payment_flag_history_opsSql, "migrations/65_drop_payment_index.sql": migrations65_drop_payment_indexSql, "migrations/65_remove_unused_indexes.sql": migrations65_remove_unused_indexesSql, + "migrations/66_contract_asset_stats.sql": migrations66_contract_asset_statsSql, "migrations/6_create_assets_table.sql": migrations6_create_assets_tableSql, "migrations/7_modify_trades_table.sql": migrations7_modify_trades_tableSql, "migrations/8_add_aggregators.sql": migrations8_add_aggregatorsSql, @@ -1661,11 +1683,13 @@ var _bindata = map[string]func() (*asset, error){ // directory embedded in the file by go-bindata. // For example if you run go-bindata on data/... and data contains the // following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png +// +// data/ +// foo.txt +// img/ +// a.png +// b.png +// // then AssetDir("data") would return []string{"foo.txt", "img"}, // AssetDir("data/img") would return []string{"a.png", "b.png"}, // AssetDir("foo.txt") and AssetDir("notexist") would return an error, and @@ -1698,75 +1722,76 @@ type bintree struct { } var _bintree = &bintree{nil, map[string]*bintree{ - "migrations": &bintree{nil, map[string]*bintree{ - "10_add_trades_price.sql": &bintree{migrations10_add_trades_priceSql, map[string]*bintree{}}, - "11_add_trades_account_index.sql": &bintree{migrations11_add_trades_account_indexSql, map[string]*bintree{}}, - "12_asset_stats_amount_string.sql": &bintree{migrations12_asset_stats_amount_stringSql, map[string]*bintree{}}, - "13_trade_offer_ids.sql": &bintree{migrations13_trade_offer_idsSql, map[string]*bintree{}}, - "14_fix_asset_toml_field.sql": &bintree{migrations14_fix_asset_toml_fieldSql, map[string]*bintree{}}, - "15_ledger_failed_txs.sql": &bintree{migrations15_ledger_failed_txsSql, map[string]*bintree{}}, - "16_ingest_failed_transactions.sql": &bintree{migrations16_ingest_failed_transactionsSql, map[string]*bintree{}}, - "17_transaction_fee_paid.sql": &bintree{migrations17_transaction_fee_paidSql, map[string]*bintree{}}, - "18_account_for_signers.sql": &bintree{migrations18_account_for_signersSql, map[string]*bintree{}}, - "19_offers.sql": &bintree{migrations19_offersSql, map[string]*bintree{}}, - "1_initial_schema.sql": &bintree{migrations1_initial_schemaSql, map[string]*bintree{}}, - "20_account_for_signer_index.sql": &bintree{migrations20_account_for_signer_indexSql, map[string]*bintree{}}, - "21_trades_remove_zero_amount_constraints.sql": &bintree{migrations21_trades_remove_zero_amount_constraintsSql, map[string]*bintree{}}, - "22_trust_lines.sql": &bintree{migrations22_trust_linesSql, map[string]*bintree{}}, - "23_exp_asset_stats.sql": &bintree{migrations23_exp_asset_statsSql, map[string]*bintree{}}, - "24_accounts.sql": &bintree{migrations24_accountsSql, map[string]*bintree{}}, - "25_expingest_rename_columns.sql": &bintree{migrations25_expingest_rename_columnsSql, map[string]*bintree{}}, - "26_exp_history_ledgers.sql": &bintree{migrations26_exp_history_ledgersSql, map[string]*bintree{}}, - "27_exp_history_transactions.sql": &bintree{migrations27_exp_history_transactionsSql, map[string]*bintree{}}, - "28_exp_history_operations.sql": &bintree{migrations28_exp_history_operationsSql, map[string]*bintree{}}, - "29_exp_history_assets.sql": &bintree{migrations29_exp_history_assetsSql, map[string]*bintree{}}, - "2_index_participants_by_toid.sql": &bintree{migrations2_index_participants_by_toidSql, map[string]*bintree{}}, - "30_exp_history_trades.sql": &bintree{migrations30_exp_history_tradesSql, map[string]*bintree{}}, - "31_exp_history_effects.sql": &bintree{migrations31_exp_history_effectsSql, map[string]*bintree{}}, - "32_drop_exp_history_tables.sql": &bintree{migrations32_drop_exp_history_tablesSql, map[string]*bintree{}}, - "33_remove_unused.sql": &bintree{migrations33_remove_unusedSql, map[string]*bintree{}}, - "34_fee_bump_transactions.sql": &bintree{migrations34_fee_bump_transactionsSql, map[string]*bintree{}}, - "35_drop_participant_id.sql": &bintree{migrations35_drop_participant_idSql, map[string]*bintree{}}, - "36_deleted_offers.sql": &bintree{migrations36_deleted_offersSql, map[string]*bintree{}}, - "37_add_tx_set_operation_count_to_ledgers.sql": &bintree{migrations37_add_tx_set_operation_count_to_ledgersSql, map[string]*bintree{}}, - "38_add_constraints.sql": &bintree{migrations38_add_constraintsSql, map[string]*bintree{}}, - "39_claimable_balances.sql": &bintree{migrations39_claimable_balancesSql, map[string]*bintree{}}, - "39_history_trades_indices.sql": &bintree{migrations39_history_trades_indicesSql, map[string]*bintree{}}, - "3_use_sequence_in_history_accounts.sql": &bintree{migrations3_use_sequence_in_history_accountsSql, map[string]*bintree{}}, - "40_fix_inner_tx_max_fee_constraint.sql": &bintree{migrations40_fix_inner_tx_max_fee_constraintSql, map[string]*bintree{}}, - "41_add_sponsor_to_state_tables.sql": &bintree{migrations41_add_sponsor_to_state_tablesSql, map[string]*bintree{}}, - "42_add_num_sponsored_and_num_sponsoring_to_accounts.sql": &bintree{migrations42_add_num_sponsored_and_num_sponsoring_to_accountsSql, map[string]*bintree{}}, - "43_add_claimable_balances_flags.sql": &bintree{migrations43_add_claimable_balances_flagsSql, map[string]*bintree{}}, - "44_asset_stat_accounts_and_balances.sql": &bintree{migrations44_asset_stat_accounts_and_balancesSql, map[string]*bintree{}}, - "45_add_claimable_balances_history.sql": &bintree{migrations45_add_claimable_balances_historySql, map[string]*bintree{}}, - "46_add_muxed_accounts.sql": &bintree{migrations46_add_muxed_accountsSql, map[string]*bintree{}}, - "47_precompute_trade_aggregations.sql": &bintree{migrations47_precompute_trade_aggregationsSql, map[string]*bintree{}}, - "48_rebuild_trade_aggregations.sql": &bintree{migrations48_rebuild_trade_aggregationsSql, map[string]*bintree{}}, - "49_add_brin_index_trade_aggregations.sql": &bintree{migrations49_add_brin_index_trade_aggregationsSql, map[string]*bintree{}}, - "4_add_protocol_version.sql": &bintree{migrations4_add_protocol_versionSql, map[string]*bintree{}}, - "50_liquidity_pools.sql": &bintree{migrations50_liquidity_poolsSql, map[string]*bintree{}}, - "51_remove_ht_unused_indexes.sql": &bintree{migrations51_remove_ht_unused_indexesSql, map[string]*bintree{}}, - "52_add_trade_type_index.sql": &bintree{migrations52_add_trade_type_indexSql, map[string]*bintree{}}, - "53_add_trades_rounding_slippage.sql": &bintree{migrations53_add_trades_rounding_slippageSql, map[string]*bintree{}}, - "54_tx_preconditions_and_account_fields.sql": &bintree{migrations54_tx_preconditions_and_account_fieldsSql, map[string]*bintree{}}, - "55_filter_rules.sql": &bintree{migrations55_filter_rulesSql, map[string]*bintree{}}, - "56_txsub_read_only.sql": &bintree{migrations56_txsub_read_onlySql, map[string]*bintree{}}, - "57_trade_aggregation_autovac.sql": &bintree{migrations57_trade_aggregation_autovacSql, map[string]*bintree{}}, - "58_add_index_by_id_optimization.sql": &bintree{migrations58_add_index_by_id_optimizationSql, map[string]*bintree{}}, - "59_remove_foreign_key_constraints.sql": &bintree{migrations59_remove_foreign_key_constraintsSql, map[string]*bintree{}}, - "5_create_trades_table.sql": &bintree{migrations5_create_trades_tableSql, map[string]*bintree{}}, - "60_add_asset_id_indexes.sql": &bintree{migrations60_add_asset_id_indexesSql, map[string]*bintree{}}, - "61_trust_lines_by_account_type_code_issuer.sql": &bintree{migrations61_trust_lines_by_account_type_code_issuerSql, map[string]*bintree{}}, - "62_claimable_balance_claimants.sql": &bintree{migrations62_claimable_balance_claimantsSql, map[string]*bintree{}}, - "63_add_contract_id_to_asset_stats.sql": &bintree{migrations63_add_contract_id_to_asset_statsSql, map[string]*bintree{}}, - "64_add_payment_flag_history_ops.sql": &bintree{migrations64_add_payment_flag_history_opsSql, map[string]*bintree{}}, - "65_drop_payment_index.sql": &bintree{migrations65_drop_payment_indexSql, map[string]*bintree{}}, - "65_remove_unused_indexes.sql": &bintree{migrations65_remove_unused_indexesSql, map[string]*bintree{}}, - "6_create_assets_table.sql": &bintree{migrations6_create_assets_tableSql, map[string]*bintree{}}, - "7_modify_trades_table.sql": &bintree{migrations7_modify_trades_tableSql, map[string]*bintree{}}, - "8_add_aggregators.sql": &bintree{migrations8_add_aggregatorsSql, map[string]*bintree{}}, - "8_create_asset_stats_table.sql": &bintree{migrations8_create_asset_stats_tableSql, map[string]*bintree{}}, - "9_add_header_xdr.sql": &bintree{migrations9_add_header_xdrSql, map[string]*bintree{}}, + "migrations": {nil, map[string]*bintree{ + "10_add_trades_price.sql": {migrations10_add_trades_priceSql, map[string]*bintree{}}, + "11_add_trades_account_index.sql": {migrations11_add_trades_account_indexSql, map[string]*bintree{}}, + "12_asset_stats_amount_string.sql": {migrations12_asset_stats_amount_stringSql, map[string]*bintree{}}, + "13_trade_offer_ids.sql": {migrations13_trade_offer_idsSql, map[string]*bintree{}}, + "14_fix_asset_toml_field.sql": {migrations14_fix_asset_toml_fieldSql, map[string]*bintree{}}, + "15_ledger_failed_txs.sql": {migrations15_ledger_failed_txsSql, map[string]*bintree{}}, + "16_ingest_failed_transactions.sql": {migrations16_ingest_failed_transactionsSql, map[string]*bintree{}}, + "17_transaction_fee_paid.sql": {migrations17_transaction_fee_paidSql, map[string]*bintree{}}, + "18_account_for_signers.sql": {migrations18_account_for_signersSql, map[string]*bintree{}}, + "19_offers.sql": {migrations19_offersSql, map[string]*bintree{}}, + "1_initial_schema.sql": {migrations1_initial_schemaSql, map[string]*bintree{}}, + "20_account_for_signer_index.sql": {migrations20_account_for_signer_indexSql, map[string]*bintree{}}, + "21_trades_remove_zero_amount_constraints.sql": {migrations21_trades_remove_zero_amount_constraintsSql, map[string]*bintree{}}, + "22_trust_lines.sql": {migrations22_trust_linesSql, map[string]*bintree{}}, + "23_exp_asset_stats.sql": {migrations23_exp_asset_statsSql, map[string]*bintree{}}, + "24_accounts.sql": {migrations24_accountsSql, map[string]*bintree{}}, + "25_expingest_rename_columns.sql": {migrations25_expingest_rename_columnsSql, map[string]*bintree{}}, + "26_exp_history_ledgers.sql": {migrations26_exp_history_ledgersSql, map[string]*bintree{}}, + "27_exp_history_transactions.sql": {migrations27_exp_history_transactionsSql, map[string]*bintree{}}, + "28_exp_history_operations.sql": {migrations28_exp_history_operationsSql, map[string]*bintree{}}, + "29_exp_history_assets.sql": {migrations29_exp_history_assetsSql, map[string]*bintree{}}, + "2_index_participants_by_toid.sql": {migrations2_index_participants_by_toidSql, map[string]*bintree{}}, + "30_exp_history_trades.sql": {migrations30_exp_history_tradesSql, map[string]*bintree{}}, + "31_exp_history_effects.sql": {migrations31_exp_history_effectsSql, map[string]*bintree{}}, + "32_drop_exp_history_tables.sql": {migrations32_drop_exp_history_tablesSql, map[string]*bintree{}}, + "33_remove_unused.sql": {migrations33_remove_unusedSql, map[string]*bintree{}}, + "34_fee_bump_transactions.sql": {migrations34_fee_bump_transactionsSql, map[string]*bintree{}}, + "35_drop_participant_id.sql": {migrations35_drop_participant_idSql, map[string]*bintree{}}, + "36_deleted_offers.sql": {migrations36_deleted_offersSql, map[string]*bintree{}}, + "37_add_tx_set_operation_count_to_ledgers.sql": {migrations37_add_tx_set_operation_count_to_ledgersSql, map[string]*bintree{}}, + "38_add_constraints.sql": {migrations38_add_constraintsSql, map[string]*bintree{}}, + "39_claimable_balances.sql": {migrations39_claimable_balancesSql, map[string]*bintree{}}, + "39_history_trades_indices.sql": {migrations39_history_trades_indicesSql, map[string]*bintree{}}, + "3_use_sequence_in_history_accounts.sql": {migrations3_use_sequence_in_history_accountsSql, map[string]*bintree{}}, + "40_fix_inner_tx_max_fee_constraint.sql": {migrations40_fix_inner_tx_max_fee_constraintSql, map[string]*bintree{}}, + "41_add_sponsor_to_state_tables.sql": {migrations41_add_sponsor_to_state_tablesSql, map[string]*bintree{}}, + "42_add_num_sponsored_and_num_sponsoring_to_accounts.sql": {migrations42_add_num_sponsored_and_num_sponsoring_to_accountsSql, map[string]*bintree{}}, + "43_add_claimable_balances_flags.sql": {migrations43_add_claimable_balances_flagsSql, map[string]*bintree{}}, + "44_asset_stat_accounts_and_balances.sql": {migrations44_asset_stat_accounts_and_balancesSql, map[string]*bintree{}}, + "45_add_claimable_balances_history.sql": {migrations45_add_claimable_balances_historySql, map[string]*bintree{}}, + "46_add_muxed_accounts.sql": {migrations46_add_muxed_accountsSql, map[string]*bintree{}}, + "47_precompute_trade_aggregations.sql": {migrations47_precompute_trade_aggregationsSql, map[string]*bintree{}}, + "48_rebuild_trade_aggregations.sql": {migrations48_rebuild_trade_aggregationsSql, map[string]*bintree{}}, + "49_add_brin_index_trade_aggregations.sql": {migrations49_add_brin_index_trade_aggregationsSql, map[string]*bintree{}}, + "4_add_protocol_version.sql": {migrations4_add_protocol_versionSql, map[string]*bintree{}}, + "50_liquidity_pools.sql": {migrations50_liquidity_poolsSql, map[string]*bintree{}}, + "51_remove_ht_unused_indexes.sql": {migrations51_remove_ht_unused_indexesSql, map[string]*bintree{}}, + "52_add_trade_type_index.sql": {migrations52_add_trade_type_indexSql, map[string]*bintree{}}, + "53_add_trades_rounding_slippage.sql": {migrations53_add_trades_rounding_slippageSql, map[string]*bintree{}}, + "54_tx_preconditions_and_account_fields.sql": {migrations54_tx_preconditions_and_account_fieldsSql, map[string]*bintree{}}, + "55_filter_rules.sql": {migrations55_filter_rulesSql, map[string]*bintree{}}, + "56_txsub_read_only.sql": {migrations56_txsub_read_onlySql, map[string]*bintree{}}, + "57_trade_aggregation_autovac.sql": {migrations57_trade_aggregation_autovacSql, map[string]*bintree{}}, + "58_add_index_by_id_optimization.sql": {migrations58_add_index_by_id_optimizationSql, map[string]*bintree{}}, + "59_remove_foreign_key_constraints.sql": {migrations59_remove_foreign_key_constraintsSql, map[string]*bintree{}}, + "5_create_trades_table.sql": {migrations5_create_trades_tableSql, map[string]*bintree{}}, + "60_add_asset_id_indexes.sql": {migrations60_add_asset_id_indexesSql, map[string]*bintree{}}, + "61_trust_lines_by_account_type_code_issuer.sql": {migrations61_trust_lines_by_account_type_code_issuerSql, map[string]*bintree{}}, + "62_claimable_balance_claimants.sql": {migrations62_claimable_balance_claimantsSql, map[string]*bintree{}}, + "63_add_contract_id_to_asset_stats.sql": {migrations63_add_contract_id_to_asset_statsSql, map[string]*bintree{}}, + "64_add_payment_flag_history_ops.sql": {migrations64_add_payment_flag_history_opsSql, map[string]*bintree{}}, + "65_drop_payment_index.sql": {migrations65_drop_payment_indexSql, map[string]*bintree{}}, + "65_remove_unused_indexes.sql": {migrations65_remove_unused_indexesSql, map[string]*bintree{}}, + "66_contract_asset_stats.sql": {migrations66_contract_asset_statsSql, map[string]*bintree{}}, + "6_create_assets_table.sql": {migrations6_create_assets_tableSql, map[string]*bintree{}}, + "7_modify_trades_table.sql": {migrations7_modify_trades_tableSql, map[string]*bintree{}}, + "8_add_aggregators.sql": {migrations8_add_aggregatorsSql, map[string]*bintree{}}, + "8_create_asset_stats_table.sql": {migrations8_create_asset_stats_tableSql, map[string]*bintree{}}, + "9_add_header_xdr.sql": {migrations9_add_header_xdrSql, map[string]*bintree{}}, }}, }} diff --git a/services/horizon/internal/db2/schema/migrations/66_contract_asset_stats.sql b/services/horizon/internal/db2/schema/migrations/66_contract_asset_stats.sql new file mode 100644 index 0000000000..c36b88dc71 --- /dev/null +++ b/services/horizon/internal/db2/schema/migrations/66_contract_asset_stats.sql @@ -0,0 +1,18 @@ +-- +migrate Up +CREATE TABLE contract_asset_stats ( + contract_id BYTEA PRIMARY KEY, + stat JSONB NOT NULL +); + +CREATE TABLE contract_asset_balances ( + key_hash BYTEA PRIMARY KEY, + asset_contract_id BYTEA NOT NULL, + amount numeric(39,0) NOT NULL, -- 39 digits is sufficient for a 128 bit integer + expiration_ledger integer NOT NULL +); + +CREATE INDEX "contract_asset_balances_by_expiration" ON contract_asset_balances USING btree (expiration_ledger); + +-- +migrate Down +DROP TABLE contract_asset_stats cascade; +DROP TABLE contract_asset_balances cascade; \ No newline at end of file diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 0369f3a69d..2cf067441e 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -58,7 +58,9 @@ const ( // claimable balances for claimant queries. // - 17: Add contract_id column to exp_asset_stats table which is derived by ingesting // contract data ledger entries. - CurrentVersion = 17 + // - 18: Ingest contract asset balances so we can keep track of expired / restore asset + // balances for asset stats. + CurrentVersion = 18 // MaxDBConnections is the size of the postgres connection pool dedicated to Horizon ingestion: // * Ledger ingestion, diff --git a/services/horizon/internal/ingest/processor_runner.go b/services/horizon/internal/ingest/processor_runner.go index fe7894d737..ed066a20d2 100644 --- a/services/horizon/internal/ingest/processor_runner.go +++ b/services/horizon/internal/ingest/processor_runner.go @@ -122,7 +122,7 @@ func buildChangeProcessor( processors.NewAccountDataProcessor(historyQ), processors.NewAccountsProcessor(historyQ), processors.NewOffersProcessor(historyQ, ledgerSequence), - processors.NewAssetStatsProcessor(historyQ, networkPassphrase, useLedgerCache), + processors.NewAssetStatsProcessor(historyQ, networkPassphrase, source == historyArchiveSource, ledgerSequence), processors.NewSignersProcessor(historyQ, useLedgerCache), processors.NewTrustLinesProcessor(historyQ), processors.NewClaimableBalancesChangeProcessor(historyQ), diff --git a/services/horizon/internal/ingest/processor_runner_test.go b/services/horizon/internal/ingest/processor_runner_test.go index aa250d29ba..13036bce1f 100644 --- a/services/horizon/internal/ingest/processor_runner_test.go +++ b/services/horizon/internal/ingest/processor_runner_test.go @@ -188,8 +188,8 @@ func TestProcessorRunnerBuildChangeProcessor(t *testing.T) { assert.IsType(t, &processors.AccountsProcessor{}, processor.processors[2]) assert.IsType(t, &processors.OffersProcessor{}, processor.processors[3]) assert.IsType(t, &processors.AssetStatsProcessor{}, processor.processors[4]) - assert.True(t, reflect.ValueOf(processor.processors[4]). - Elem().FieldByName("useLedgerEntryCache").Bool()) + assert.False(t, reflect.ValueOf(processor.processors[4]). + Elem().FieldByName("ingestFromHistoryArchive").Bool()) assert.IsType(t, &processors.SignersProcessor{}, processor.processors[5]) assert.True(t, reflect.ValueOf(processor.processors[5]). Elem().FieldByName("useLedgerEntryCache").Bool()) @@ -209,8 +209,8 @@ func TestProcessorRunnerBuildChangeProcessor(t *testing.T) { assert.IsType(t, &processors.AccountsProcessor{}, processor.processors[2]) assert.IsType(t, &processors.OffersProcessor{}, processor.processors[3]) assert.IsType(t, &processors.AssetStatsProcessor{}, processor.processors[4]) - assert.False(t, reflect.ValueOf(processor.processors[4]). - Elem().FieldByName("useLedgerEntryCache").Bool()) + assert.True(t, reflect.ValueOf(processor.processors[4]). + Elem().FieldByName("ingestFromHistoryArchive").Bool()) assert.IsType(t, &processors.SignersProcessor{}, processor.processors[5]) assert.False(t, reflect.ValueOf(processor.processors[5]). Elem().FieldByName("useLedgerEntryCache").Bool()) @@ -282,6 +282,7 @@ func TestProcessorRunnerWithFilterEnabled(t *testing.T) { LedgerHeader: xdr.LedgerHeaderHistoryEntry{ Header: xdr.LedgerHeader{ BucketListHash: xdr.Hash([32]byte{0, 1, 2}), + LedgerSeq: 23, }, }, }, @@ -310,6 +311,17 @@ func TestProcessorRunnerWithFilterEnabled(t *testing.T) { ).Return(nil) defer mock.AssertExpectationsForObjects(t, mockBatchInsertBuilder) + q.MockQAssetStats.On("RemoveContractAssetBalances", ctx, []xdr.Hash(nil)). + Return(nil).Once() + q.MockQAssetStats.On("UpdateContractAssetBalanceAmounts", ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + q.MockQAssetStats.On("InsertContractAssetBalances", ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + q.MockQAssetStats.On("UpdateContractAssetBalanceExpirations", ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + q.MockQAssetStats.On("GetContractAssetBalancesExpiringAt", ctx, uint32(22)). + Return([]history.ContractAssetBalance{}, nil).Once() + runner := ProcessorRunner{ ctx: ctx, config: config, @@ -338,6 +350,7 @@ func TestProcessorRunnerRunAllProcessorsOnLedger(t *testing.T) { LedgerHeader: xdr.LedgerHeaderHistoryEntry{ Header: xdr.LedgerHeader{ BucketListHash: xdr.Hash([32]byte{0, 1, 2}), + LedgerSeq: 23, }, }, }, @@ -438,6 +451,17 @@ func TestProcessorRunnerRunTransactionsProcessorsOnLedgers(t *testing.T) { defer mock.AssertExpectationsForObjects(t, mockBatchInsertBuilder) + q.MockQAssetStats.On("RemoveContractAssetBalances", ctx, []xdr.Hash(nil)). + Return(nil).Once() + q.MockQAssetStats.On("UpdateContractAssetBalanceAmounts", ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + q.MockQAssetStats.On("InsertContractAssetBalances", ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + q.MockQAssetStats.On("UpdateContractAssetBalanceExpirations", ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + q.MockQAssetStats.On("GetContractAssetBalancesExpiringAt", ctx, uint32(22)). + Return([]history.ContractAssetBalance{}, nil).Once() + runner := ProcessorRunner{ ctx: ctx, config: config, diff --git a/services/horizon/internal/ingest/processors/asset_stats_processor.go b/services/horizon/internal/ingest/processors/asset_stats_processor.go index 32c9733b1c..c3da42b730 100644 --- a/services/horizon/internal/ingest/processors/asset_stats_processor.go +++ b/services/horizon/internal/ingest/processors/asset_stats_processor.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/hex" + "fmt" "math/big" "github.com/stellar/go/ingest" @@ -13,116 +14,319 @@ import ( ) type AssetStatsProcessor struct { - assetStatsQ history.QAssetStats - cache *ingest.ChangeCompactor - assetStatSet AssetStatSet - useLedgerEntryCache bool - networkPassphrase string + assetStatsQ history.QAssetStats + cache *ingest.ChangeCompactor + currentLedger uint32 + removedExpirationEntries map[xdr.Hash]uint32 + createdExpirationEntries map[xdr.Hash]uint32 + updatedExpirationEntries map[xdr.Hash][2]uint32 + ingestFromHistoryArchive bool + networkPassphrase string } // NewAssetStatsProcessor constructs a new AssetStatsProcessor instance. -// If useLedgerEntryCache is false we don't use ledger cache and we just -// add trust lines to assetStatSet, then we insert all the stats in one -// insert query. This is done to make history buckets processing faster -// (batch inserting). func NewAssetStatsProcessor( assetStatsQ history.QAssetStats, networkPassphrase string, - useLedgerEntryCache bool, + ingestFromHistoryArchive bool, + currentLedger uint32, ) *AssetStatsProcessor { p := &AssetStatsProcessor{ - assetStatsQ: assetStatsQ, - useLedgerEntryCache: useLedgerEntryCache, - networkPassphrase: networkPassphrase, + currentLedger: currentLedger, + assetStatsQ: assetStatsQ, + ingestFromHistoryArchive: ingestFromHistoryArchive, + networkPassphrase: networkPassphrase, + cache: ingest.NewChangeCompactor(), + removedExpirationEntries: map[xdr.Hash]uint32{}, + createdExpirationEntries: map[xdr.Hash]uint32{}, + updatedExpirationEntries: map[xdr.Hash][2]uint32{}, } - p.reset() return p } -func (p *AssetStatsProcessor) reset() { - p.cache = ingest.NewChangeCompactor() - p.assetStatSet = NewAssetStatSet(p.networkPassphrase) -} - func (p *AssetStatsProcessor) ProcessChange(ctx context.Context, change ingest.Change) error { if change.Type != xdr.LedgerEntryTypeLiquidityPool && change.Type != xdr.LedgerEntryTypeClaimableBalance && change.Type != xdr.LedgerEntryTypeTrustline && - change.Type != xdr.LedgerEntryTypeContractData { + change.Type != xdr.LedgerEntryTypeContractData && + change.Type != xdr.LedgerEntryTypeTtl { return nil } - if p.useLedgerEntryCache { - return p.addToCache(ctx, change) - } - if change.Pre != nil || change.Post == nil { - return errors.New("AssetStatsProcessor is in insert only mode") - } - - switch change.Type { - case xdr.LedgerEntryTypeLiquidityPool: - return p.assetStatSet.AddLiquidityPool(change) - case xdr.LedgerEntryTypeClaimableBalance: - return p.assetStatSet.AddClaimableBalance(change) - case xdr.LedgerEntryTypeTrustline: - return p.assetStatSet.AddTrustline(change) - case xdr.LedgerEntryTypeContractData: - return p.assetStatSet.AddContractData(change) - default: - return nil + // only ingest contract data entries which could be relevant to + // asset stats + if change.Type == xdr.LedgerEntryTypeContractData { + ledgerEntry := change.Post + if ledgerEntry == nil { + ledgerEntry = change.Pre + } + asset := AssetFromContractData(*ledgerEntry, p.networkPassphrase) + _, _, balanceFound := ContractBalanceFromContractData(*ledgerEntry, p.networkPassphrase) + if asset == nil && !balanceFound { + return nil + } } -} - -func (p *AssetStatsProcessor) addToCache(ctx context.Context, change ingest.Change) error { - err := p.cache.AddChange(change) - if err != nil { + if err := p.cache.AddChange(change); err != nil { return errors.Wrap(err, "error adding to ledgerCache") } + return nil +} - if p.cache.Size() > maxBatchSize { - err = p.Commit(ctx) - if err != nil { - return errors.Wrap(err, "error in Commit") +func (p *AssetStatsProcessor) addExpirationChange(change ingest.Change) error { + switch { + case change.Pre == nil && change.Post != nil: // created + post := change.Post.Data.MustTtl() + p.createdExpirationEntries[post.KeyHash] = uint32(post.LiveUntilLedgerSeq) + case change.Pre != nil && change.Post == nil: // removed + pre := change.Pre.Data.MustTtl() + p.removedExpirationEntries[pre.KeyHash] = uint32(pre.LiveUntilLedgerSeq) + case change.Pre != nil && change.Post != nil: // updated + pre := change.Pre.Data.MustTtl() + post := change.Post.Data.MustTtl() + // it's unclear if core could emit a ledger entry change where the + // expiration ledger remains the same + if pre.LiveUntilLedgerSeq == post.LiveUntilLedgerSeq { + return nil + } + // but we expect that the expiration ledger will never decrease + if pre.LiveUntilLedgerSeq > post.LiveUntilLedgerSeq { + return errors.Errorf( + "unexpected change in expiration ledger Pre: %v Post: %v", + pre.LiveUntilLedgerSeq, + post.LiveUntilLedgerSeq, + ) } - p.reset() + // also the new expiration ledger must always be greater than or equal + // to the current ledger + if uint32(post.LiveUntilLedgerSeq) < p.currentLedger { + return errors.Errorf( + "post expiration ledger is less than current ledger."+ + " Pre: %v Post: %v current ledger: %v", + pre.LiveUntilLedgerSeq, + post.LiveUntilLedgerSeq, + p.currentLedger, + ) + } + p.updatedExpirationEntries[pre.KeyHash] = [2]uint32{ + uint32(pre.LiveUntilLedgerSeq), + uint32(post.LiveUntilLedgerSeq), + } + default: + return errors.Errorf("unexpected change Pre: %v Post: %v", change.Pre, change.Post) } return nil } func (p *AssetStatsProcessor) Commit(ctx context.Context) error { - if !p.useLedgerEntryCache { - assetStatsDeltas, err := p.assetStatSet.AllFromSnapshot() - if err != nil { - return err - } - if len(assetStatsDeltas) == 0 { - return nil - } - - return p.assetStatsQ.InsertAssetStats(ctx, assetStatsDeltas, maxBatchSize) - } + assetStatSet := NewAssetStatSet() changes := p.cache.GetChanges() + var contractDataChanges []ingest.Change for _, change := range changes { var err error switch change.Type { case xdr.LedgerEntryTypeLiquidityPool: - err = p.assetStatSet.AddLiquidityPool(change) + err = assetStatSet.AddLiquidityPool(change) case xdr.LedgerEntryTypeClaimableBalance: - err = p.assetStatSet.AddClaimableBalance(change) + err = assetStatSet.AddClaimableBalance(change) case xdr.LedgerEntryTypeTrustline: - err = p.assetStatSet.AddTrustline(change) + err = assetStatSet.AddTrustline(change) case xdr.LedgerEntryTypeContractData: - err = p.assetStatSet.AddContractData(change) + contractDataChanges = append(contractDataChanges, change) + case xdr.LedgerEntryTypeTtl: + err = p.addExpirationChange(change) default: return errors.Errorf("Change type %v is unexpected", change.Type) } - if err != nil { return errors.Wrap(err, "Error adjusting asset stat") } } - assetStatsDeltas, contractToAsset, contractAssetStats := p.assetStatSet.All() + contractAssetStatSet := NewContractAssetStatSet( + p.assetStatsQ, + p.networkPassphrase, + p.removedExpirationEntries, + p.createdExpirationEntries, + p.updatedExpirationEntries, + p.currentLedger, + ) + for _, change := range contractDataChanges { + if err := contractAssetStatSet.AddContractData(ctx, change); err != nil { + return errors.Wrap(err, "Error ingesting contract data") + } + } + + return p.updateDB(ctx, assetStatSet, contractAssetStatSet) +} + +func (p *AssetStatsProcessor) updateDB( + ctx context.Context, + assetStatSet AssetStatSet, + contractAssetStatSet *ContractAssetStatSet, +) error { + if p.ingestFromHistoryArchive { + // When ingesting from the history archives we can take advantage of the fact + // that there are only created ledger entries. We don't need to execute any + // updates or removals on the asset stats tables. And we can also skip + // ingesting restored contract balances and expired contract balances. + assetStatsDeltas := assetStatSet.All() + if len(assetStatsDeltas) > 0 { + var err error + assetStatsDeltas, err = IncludeContractIDsInAssetStats( + p.networkPassphrase, + assetStatsDeltas, + contractAssetStatSet.contractToAsset, + ) + if err != nil { + return errors.Wrap(err, "Error extracting asset stat rows") + } + if err := p.assetStatsQ.InsertAssetStats(ctx, assetStatsDeltas); err != nil { + return errors.Wrap(err, "Error inserting asset stats") + } + } + + if rows := contractAssetStatSet.GetContractStats(); len(rows) > 0 { + if err := p.assetStatsQ.InsertContractAssetStats(ctx, rows); err != nil { + return errors.Wrap(err, "Error inserting asset contract stats") + } + } + + if len(contractAssetStatSet.createdBalances) > 0 { + if err := p.assetStatsQ.InsertContractAssetBalances(ctx, contractAssetStatSet.createdBalances); err != nil { + return errors.Wrap(err, "Error inserting asset contract stats") + } + } + return nil + } + + assetStatsDeltas := assetStatSet.All() + + if err := p.updateAssetStats(ctx, assetStatsDeltas, contractAssetStatSet.contractToAsset); err != nil { + return err + } + if err := p.updateContractIDs(ctx, contractAssetStatSet.contractToAsset); err != nil { + return err + } + + if err := p.assetStatsQ.RemoveContractAssetBalances(ctx, contractAssetStatSet.removedBalances); err != nil { + return errors.Wrap(err, "Error removing contract asset balances") + } + + if err := p.updateContractAssetBalanceAmounts(ctx, contractAssetStatSet.updatedBalances); err != nil { + return err + } + + if err := p.assetStatsQ.InsertContractAssetBalances(ctx, contractAssetStatSet.createdBalances); err != nil { + return errors.Wrap(err, "Error inserting contract asset balances") + } + + if err := contractAssetStatSet.ingestRestoredBalances(ctx); err != nil { + return err + } + + if err := p.updateContractAssetBalanceExpirations(ctx); err != nil { + return err + } + + if err := contractAssetStatSet.ingestExpiredBalances(ctx); err != nil { + return err + } + + return p.updateContractAssetStats(ctx, contractAssetStatSet.contractAssetStats) +} + +func (p *AssetStatsProcessor) updateContractAssetBalanceAmounts(ctx context.Context, updatedBalances map[xdr.Hash]*big.Int) error { + keys := make([]xdr.Hash, 0, len(updatedBalances)) + amounts := make([]string, 0, len(updatedBalances)) + for key, amount := range updatedBalances { + keys = append(keys, key) + amounts = append(amounts, amount.String()) + } + if err := p.assetStatsQ.UpdateContractAssetBalanceAmounts(ctx, keys, amounts); err != nil { + return errors.Wrap(err, "Error updating contract asset balance amounts") + } + return nil +} + +func (p *AssetStatsProcessor) updateContractAssetBalanceExpirations(ctx context.Context) error { + keys := make([]xdr.Hash, 0, len(p.updatedExpirationEntries)) + expirationLedgers := make([]uint32, 0, len(p.updatedExpirationEntries)) + for key, update := range p.updatedExpirationEntries { + keys = append(keys, key) + expirationLedgers = append(expirationLedgers, update[1]) + } + if err := p.assetStatsQ.UpdateContractAssetBalanceExpirations(ctx, keys, expirationLedgers); err != nil { + return errors.Wrap(err, "Error updating contract asset balance expirations") + } + return nil +} + +func IncludeContractIDsInAssetStats( + networkPassphrase string, + assetStatsDeltas []history.ExpAssetStat, + contractToAsset map[xdr.Hash]*xdr.Asset, +) ([]history.ExpAssetStat, error) { + included := map[xdr.Hash]bool{} + // modify the asset stat row to update the contract_id column whenever we encounter a + // contract data ledger entry with the Stellar asset metadata. + for i, assetStatDelta := range assetStatsDeltas { + // asset stats only supports non-native assets + asset := xdr.MustNewCreditAsset(assetStatDelta.AssetCode, assetStatDelta.AssetIssuer) + contractID, err := asset.ContractID(networkPassphrase) + if err != nil { + return nil, errors.Wrap(err, "cannot compute contract id for asset") + } + if asset, ok := contractToAsset[contractID]; ok && asset == nil { + return nil, ingest.NewStateError(fmt.Errorf( + "unexpected contract data removal in history archives: %s", + hex.EncodeToString(contractID[:]), + )) + } else if ok { + assetStatDelta.SetContractID(contractID) + included[contractID] = true + } + + assetStatsDeltas[i] = assetStatDelta + } + + // There is also a corner case where a Stellar Asset contract is initialized before there exists any + // trustlines / claimable balances for the Stellar Asset. In this case, when ingesting contract data + // ledger entries, there will be no existing asset stat row. We handle this case by inserting a row + // with zero stats just so we can populate the contract id. + for contractID, asset := range contractToAsset { + if included[contractID] { + continue + } + if asset == nil { + return nil, ingest.NewStateError(fmt.Errorf( + "unexpected contract data removal in history archives: %s", + hex.EncodeToString(contractID[:]), + )) + } + var assetType xdr.AssetType + var assetCode, assetIssuer string + asset.MustExtract(&assetType, &assetCode, &assetIssuer) + row := history.ExpAssetStat{ + AssetType: assetType, + AssetCode: assetCode, + AssetIssuer: assetIssuer, + Accounts: history.ExpAssetStatAccounts{}, + Balances: newAssetStatBalance().ConvertToHistoryObject(), + Amount: "0", + NumAccounts: 0, + } + row.SetContractID(contractID) + assetStatsDeltas = append(assetStatsDeltas, row) + } + + return assetStatsDeltas, nil +} + +func (p *AssetStatsProcessor) updateAssetStats( + ctx context.Context, + assetStatsDeltas []history.ExpAssetStat, + contractToAsset map[xdr.Hash]*xdr.Asset, +) error { for _, delta := range assetStatsDeltas { var rowsAffected int64 var stat history.ExpAssetStat @@ -134,12 +338,6 @@ func (p *AssetStatsProcessor) Commit(ctx context.Context) error { return errors.Wrap(err, "cannot compute contract id for asset") } - if contractAssetStat, ok := contractAssetStats[contractID]; ok { - delta.Balances.Contracts = contractAssetStat.balance.String() - delta.Accounts.Contracts = contractAssetStat.numHolders - delete(contractAssetStats, contractID) - } - stat, err = p.assetStatsQ.GetAssetStat(ctx, delta.AssetType, delta.AssetCode, @@ -225,12 +423,12 @@ func (p *AssetStatsProcessor) Commit(ctx context.Context) error { } } else { var statBalances assetStatBalances - if err = statBalances.Parse(&stat.Balances); err != nil { + if err = statBalances.Parse(stat.Balances); err != nil { return errors.Wrap(err, "Error parsing balances") } var deltaBalances assetStatBalances - if err = deltaBalances.Parse(&delta.Balances); err != nil { + if err = deltaBalances.Parse(delta.Balances); err != nil { return errors.Wrap(err, "Error parsing balances") } @@ -285,31 +483,26 @@ func (p *AssetStatsProcessor) Commit(ctx context.Context) error { )) } } - - if err := p.updateContractIDs(ctx, contractToAsset, contractAssetStats); err != nil { - return err - } - return p.updateContractAssetStats(ctx, contractAssetStats) + return nil } func (p *AssetStatsProcessor) updateContractIDs( ctx context.Context, - contractToAsset map[[32]byte]*xdr.Asset, - contractAssetStats map[[32]byte]contractAssetStatValue, + contractToAsset map[xdr.Hash]*xdr.Asset, ) error { for contractID, asset := range contractToAsset { - if err := p.updateContractID(ctx, contractAssetStats, contractID, asset); err != nil { + if err := p.updateContractID(ctx, contractID, asset); err != nil { return err } } return nil } -// updateContractID will update the asset stat row for the corresponding asset to either add or remove the given contract id +// updateContractID will update the asset stat row for the corresponding asset to either +// add or remove the given contract id func (p *AssetStatsProcessor) updateContractID( ctx context.Context, - contractAssetStats map[[32]byte]contractAssetStatValue, - contractID [32]byte, + contractID xdr.Hash, asset *xdr.Asset, ) error { var rowsAffected int64 @@ -326,10 +519,6 @@ func (p *AssetStatsProcessor) updateContractID( return errors.Wrap(err, "error querying asset by contract id") } - if err = p.maybeAddContractAssetStat(contractAssetStats, contractID, &stat); err != nil { - return errors.Wrapf(err, "could not update asset stat with contract id %v with contract delta", contractID) - } - if stat.Accounts.IsZero() { if !stat.Balances.IsZero() { return ingest.NewStateError(errors.Errorf( @@ -346,11 +535,6 @@ func (p *AssetStatsProcessor) updateContractID( if err != nil { return errors.Wrap(err, "could not remove asset stat") } - } else if stat.Accounts.Contracts != 0 || stat.Balances.Contracts != "0" { - return ingest.NewStateError(errors.Errorf( - "asset stat has contract holders but is attempting to remove contract id: %s", - hex.EncodeToString(contractID[:]), - )) } else { // update the row to set the contract_id column to NULL stat.ContractID = nil @@ -376,9 +560,6 @@ func (p *AssetStatsProcessor) updateContractID( NumAccounts: 0, } row.SetContractID(contractID) - if err = p.maybeAddContractAssetStat(contractAssetStats, contractID, &row); err != nil { - return errors.Wrapf(err, "could not update asset stat with contract id %v with contract delta", contractID) - } rowsAffected, err = p.assetStatsQ.InsertAssetStat(ctx, row) if err != nil { @@ -397,10 +578,6 @@ func (p *AssetStatsProcessor) updateContractID( } else { // update the column_id column stat.SetContractID(contractID) - if err = p.maybeAddContractAssetStat(contractAssetStats, contractID, &stat); err != nil { - return errors.Wrapf(err, "could not update asset stat with contract id %v with contract delta", contractID) - } - rowsAffected, err = p.assetStatsQ.UpdateAssetStat(ctx, stat) if err != nil { return errors.Wrap(err, "could not update asset stat") @@ -419,67 +596,77 @@ func (p *AssetStatsProcessor) updateContractID( return nil } -func (p *AssetStatsProcessor) addContractAssetStat(contractAssetStat contractAssetStatValue, stat *history.ExpAssetStat) error { - stat.Accounts.Contracts += contractAssetStat.numHolders - contracts, ok := new(big.Int).SetString(stat.Balances.Contracts, 10) +func (p *AssetStatsProcessor) addContractAssetStat(contractAssetStat assetContractStatValue, row *history.ContractAssetStatRow) error { + row.Stat.ActiveHolders += contractAssetStat.activeHolders + row.Stat.ArchivedHolders += contractAssetStat.archivedHolders + activeBalance, ok := new(big.Int).SetString(row.Stat.ActiveBalance, 10) if !ok { - return errors.New("Error parsing: " + stat.Balances.Contracts) + return errors.New("Error parsing: " + row.Stat.ActiveBalance) } - stat.Balances.Contracts = (new(big.Int).Add(contracts, contractAssetStat.balance)).String() - return nil -} - -func (p *AssetStatsProcessor) maybeAddContractAssetStat(contractAssetStats map[[32]byte]contractAssetStatValue, contractID [32]byte, stat *history.ExpAssetStat) error { - if contractAssetStat, ok := contractAssetStats[contractID]; ok { - if err := p.addContractAssetStat(contractAssetStat, stat); err != nil { - return err - } - delete(contractAssetStats, contractID) + row.Stat.ActiveBalance = activeBalance.Add(activeBalance, contractAssetStat.activeBalance).String() + archivedBalance, ok := new(big.Int).SetString(row.Stat.ArchivedBalance, 10) + if !ok { + return errors.New("Error parsing: " + row.Stat.ArchivedBalance) } + row.Stat.ArchivedBalance = archivedBalance.Add(archivedBalance, contractAssetStat.archivedBalance).String() return nil } func (p *AssetStatsProcessor) updateContractAssetStats( ctx context.Context, - contractAssetStats map[[32]byte]contractAssetStatValue, + contractAssetStats map[xdr.Hash]assetContractStatValue, ) error { for contractID, contractAssetStat := range contractAssetStats { - if err := p.updateContractAssetStat(ctx, contractID, contractAssetStat); err != nil { + if err := p.updateAssetContractStats(ctx, contractID, contractAssetStat); err != nil { return err } } return nil } -// updateContractAssetStat will look up an asset stat by contract id and, if it exists, -// it will adjust the contract balance and holders based on contractAssetStatValue -func (p *AssetStatsProcessor) updateContractAssetStat( +// updateAssetContractStats will look up an asset contract stat by contract id and +// it will adjust the contract balance and holders based on assetContractStat +func (p *AssetStatsProcessor) updateAssetContractStats( ctx context.Context, - contractID [32]byte, - contractAssetStat contractAssetStatValue, + contractID xdr.Hash, + assetContractStat assetContractStatValue, ) error { - stat, err := p.assetStatsQ.GetAssetStatByContract(ctx, contractID) + var rowsAffected int64 + row, err := p.assetStatsQ.GetContractAssetStat(ctx, contractID[:]) if err == sql.ErrNoRows { - return nil + rowsAffected, err = p.assetStatsQ.InsertContractAssetStat(ctx, assetContractStat.ConvertToHistoryObject()) + if err != nil { + return errors.Wrap(err, "error inserting asset contract stat") + } } else if err != nil { return errors.Wrap(err, "error querying asset by contract id") - } - if err = p.addContractAssetStat(contractAssetStat, &stat); err != nil { - return errors.Wrapf(err, "could not update asset stat with contract id %v with contract delta", contractID) - } + } else { + if err = p.addContractAssetStat(assetContractStat, &row); err != nil { + return errors.Wrapf(err, "could not update asset stat with contract id %v with contract delta", contractID) + } - var rowsAffected int64 - rowsAffected, err = p.assetStatsQ.UpdateAssetStat(ctx, stat) - if err != nil { - return errors.Wrap(err, "could not update asset stat") + if row.Stat == (history.ContractStat{ + ActiveBalance: "0", + ActiveHolders: 0, + ArchivedBalance: "0", + ArchivedHolders: 0, + }) { + rowsAffected, err = p.assetStatsQ.RemoveAssetContractStat(ctx, contractID[:]) + } else { + rowsAffected, err = p.assetStatsQ.UpdateContractAssetStat(ctx, row) + } + + if err != nil { + return errors.Wrap(err, "could not update asset stat") + } } if rowsAffected != 1 { // assert that we have updated exactly one row return ingest.NewStateError(errors.Errorf( - "%d rows affected (expected exactly 1) when adjusting asset stat for asset: %s", + "%d rows affected (expected exactly 1) when adjusting asset contract stat for contract: %s", rowsAffected, - stat.AssetCode+":"+stat.AssetIssuer, + contractID, )) } return nil diff --git a/services/horizon/internal/ingest/processors/asset_stats_processor_test.go b/services/horizon/internal/ingest/processors/asset_stats_processor_test.go index e95b9b3c44..a1fb68237c 100644 --- a/services/horizon/internal/ingest/processors/asset_stats_processor_test.go +++ b/services/horizon/internal/ingest/processors/asset_stats_processor_test.go @@ -3,10 +3,13 @@ package processors import ( + "bytes" "context" "database/sql" "testing" + "github.com/stretchr/testify/assert" + "github.com/stellar/go/ingest" "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/xdr" @@ -29,7 +32,7 @@ type AssetStatsProcessorTestSuiteState struct { func (s *AssetStatsProcessorTestSuiteState) SetupTest() { s.ctx = context.Background() s.mockQ = &history.MockQAssetStats{} - s.processor = NewAssetStatsProcessor(s.mockQ, "", false) + s.processor = NewAssetStatsProcessor(s.mockQ, "", true, 123) } func (s *AssetStatsProcessorTestSuiteState) TearDownTest() { @@ -70,12 +73,11 @@ func (s *AssetStatsProcessorTestSuiteState) TestCreateTrustLine() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 1, }, - }, maxBatchSize).Return(nil).Once() + }).Return(nil).Once() } func (s *AssetStatsProcessorTestSuiteState) TestCreatePoolShareTrustLine() { @@ -138,12 +140,11 @@ func (s *AssetStatsProcessorTestSuiteState) TestCreateTrustLineWithClawback() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 1, }, - }, maxBatchSize).Return(nil).Once() + }).Return(nil).Once() } func (s *AssetStatsProcessorTestSuiteState) TestCreateTrustLineUnauthorized() { @@ -177,12 +178,11 @@ func (s *AssetStatsProcessorTestSuiteState) TestCreateTrustLineUnauthorized() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 0, }, - }, maxBatchSize).Return(nil).Once() + }).Return(nil).Once() } func TestAssetStatsProcessorTestSuiteLedger(t *testing.T) { @@ -200,7 +200,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) SetupTest() { s.ctx = context.Background() s.mockQ = &history.MockQAssetStats{} - s.processor = NewAssetStatsProcessor(s.mockQ, "", true) + s.processor = NewAssetStatsProcessor(s.mockQ, "", false, 1235) } func (s *AssetStatsProcessorTestSuiteLedger) TearDownTest() { @@ -319,7 +319,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertClaimableBalance() { Unauthorized: "0", ClaimableBalances: "24", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -343,12 +342,22 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertClaimableBalance() { Unauthorized: "0", ClaimableBalances: "46", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 0, }).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -477,7 +486,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertTrustLine() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "10", NumAccounts: 1, @@ -501,12 +509,22 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertTrustLine() { Unauthorized: "10", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 0, }).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -580,7 +598,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertContractID() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 1, @@ -607,7 +624,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertContractID() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -617,6 +633,17 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertContractID() { return usdAssetStat.Equals(assetStat) })).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -633,34 +660,115 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertContractBalance() { }, })) - usdAssetStat := history.ExpAssetStat{ - AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, - AssetIssuer: trustLineIssuer.Address(), - AssetCode: "USD", - Accounts: history.ExpAssetStatAccounts{ - Contracts: 1, + keyHash := getKeyHashForBalance(s.T(), usdID, [32]byte{1}) + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeTtl, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: keyHash, + LiveUntilLedgerSeq: 2234, + }, + }, }, - Balances: history.ExpAssetStatBalances{ - Authorized: "0", - AuthorizedToMaintainLiabilities: "0", - Unauthorized: "0", - ClaimableBalances: "0", - LiquidityPools: "0", - Contracts: "150", + })) + + s.mockQ.On("GetContractAssetStat", s.ctx, usdID[:]). + Return(history.ContractAssetStatRow{}, sql.ErrNoRows).Once() + + usdAssetContractStat := history.ContractAssetStatRow{ + ContractID: usdID[:], + Stat: history.ContractStat{ + ActiveBalance: "200", + ActiveHolders: 1, + ArchivedBalance: "0", + ArchivedHolders: 0, }, - Amount: "0", - NumAccounts: 0, } - usdAssetStat.SetContractID(usdID) - s.mockQ.On("GetAssetStatByContract", s.ctx, usdID). - Return(usdAssetStat, nil).Once() + s.mockQ.On("InsertContractAssetStat", s.ctx, mock.MatchedBy(func(row history.ContractAssetStatRow) bool { + return bytes.Equal(usdID[:], row.ContractID) && + usdAssetContractStat.Stat == row.Stat + })).Return(int64(1), nil).Once() - usdAssetStat.Accounts.Contracts++ - usdAssetStat.Balances.Contracts = "350" - s.mockQ.On("UpdateAssetStat", s.ctx, mock.MatchedBy(func(assetStat history.ExpAssetStat) bool { - return usdAssetStat.Equals(assetStat) + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance{ + { + KeyHash: keyHash[:], + ContractID: usdID[:], + Amount: "200", + ExpirationLedger: 2234, + }, + }).Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + + s.Assert().NoError(s.processor.Commit(s.ctx)) +} + +func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractBalance() { + lastModifiedLedgerSeq := xdr.Uint32(1234) + usdID, err := xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()).ContractID("") + s.Assert().NoError(err) + + keyHash := getKeyHashForBalance(s.T(), usdID, [32]byte{1}) + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: BalanceToContractData(usdID, [32]byte{1}, 100), + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: BalanceToContractData(usdID, [32]byte{1}, 300), + }, + })) + + usdAssetContractStat := history.ContractAssetStatRow{ + ContractID: usdID[:], + Stat: history.ContractStat{ + ActiveBalance: "150", + ActiveHolders: 1, + ArchivedBalance: "20", + ArchivedHolders: 2, + }, + } + s.mockQ.On("GetContractAssetStat", s.ctx, usdID[:]). + Return(usdAssetContractStat, nil).Once() + + s.mockQ.On("GetContractAssetBalances", s.ctx, []xdr.Hash{keyHash}). + Return([]history.ContractAssetBalance{ + { + KeyHash: keyHash[:], + ContractID: usdID[:], + Amount: "100", + ExpirationLedger: 2234, + }, + }, nil).Once() + + usdAssetContractStat.Stat.ActiveBalance = "350" + s.mockQ.On("UpdateContractAssetStat", s.ctx, mock.MatchedBy(func(row history.ContractAssetStatRow) bool { + return bytes.Equal(usdID[:], row.ContractID) && + usdAssetContractStat.Stat == row.Stat })).Return(int64(1), nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{keyHash}, []string{"300"}). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -677,33 +785,46 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractBalance() { }, })) - usdAssetStat := history.ExpAssetStat{ - AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, - AssetIssuer: trustLineIssuer.Address(), - AssetCode: "USD", - Accounts: history.ExpAssetStatAccounts{ - Contracts: 1, + keyHash := getKeyHashForBalance(s.T(), usdID, [32]byte{1}) + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeTtl, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: keyHash, + LiveUntilLedgerSeq: 2234, + }, + }, }, - Balances: history.ExpAssetStatBalances{ - Authorized: "0", - AuthorizedToMaintainLiabilities: "0", - Unauthorized: "0", - ClaimableBalances: "0", - LiquidityPools: "0", - Contracts: "200", + })) + + usdAssetContractStat := history.ContractAssetStatRow{ + ContractID: usdID[:], + Stat: history.ContractStat{ + ActiveBalance: "200", + ActiveHolders: 1, + ArchivedHolders: 0, + ArchivedBalance: "0", }, - Amount: "0", - NumAccounts: 0, } - usdAssetStat.SetContractID(usdID) - s.mockQ.On("GetAssetStatByContract", s.ctx, usdID). - Return(usdAssetStat, nil).Once() - - usdAssetStat.Accounts.Contracts = 0 - usdAssetStat.Balances.Contracts = "0" - s.mockQ.On("UpdateAssetStat", s.ctx, mock.MatchedBy(func(assetStat history.ExpAssetStat) bool { - return usdAssetStat.Equals(assetStat) - })).Return(int64(1), nil).Once() + s.mockQ.On("GetContractAssetStat", s.ctx, usdID[:]). + Return(usdAssetContractStat, nil).Once() + + usdAssetContractStat.Stat.ActiveHolders = 0 + usdAssetContractStat.Stat.ActiveBalance = "0" + s.mockQ.On("RemoveAssetContractStat", s.ctx, usdID[:]).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash{keyHash}). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -777,6 +898,21 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertContractIDWithBalance() { }, })) + keyHash := getKeyHashForBalance(s.T(), btcID, [32]byte{1}) + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeTtl, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: keyHash, + LiveUntilLedgerSeq: 2234, + }, + }, + }, + })) + s.mockQ.On("GetAssetStat", s.ctx, xdr.AssetTypeAssetTypeCreditAlphanum4, "EUR", @@ -795,7 +931,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertContractIDWithBalance() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 1, @@ -811,23 +946,33 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertContractIDWithBalance() { trustLineIssuer.Address(), ).Return(history.ExpAssetStat{}, sql.ErrNoRows).Once() - s.mockQ.On("GetAssetStatByContract", s.ctx, btcID). - Return(history.ExpAssetStat{}, sql.ErrNoRows).Once() + s.mockQ.On("GetContractAssetStat", s.ctx, btcID[:]). + Return(history.ContractAssetStatRow{}, sql.ErrNoRows).Once() + btcAssetContractStat := history.ContractAssetStatRow{ + ContractID: btcID[:], + Stat: history.ContractStat{ + ActiveBalance: "20", + ActiveHolders: 1, + ArchivedBalance: "0", + ArchivedHolders: 0, + }, + } + s.mockQ.On("InsertContractAssetStat", s.ctx, mock.MatchedBy(func(row history.ContractAssetStatRow) bool { + return bytes.Equal(btcID[:], row.ContractID) && + btcAssetContractStat.Stat == row.Stat + })).Return(int64(1), nil).Once() usdAssetStat := history.ExpAssetStat{ AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, AssetIssuer: trustLineIssuer.Address(), AssetCode: "USD", - Accounts: history.ExpAssetStatAccounts{ - Contracts: 1, - }, + Accounts: history.ExpAssetStatAccounts{}, Balances: history.ExpAssetStatBalances{ Authorized: "0", AuthorizedToMaintainLiabilities: "0", Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "150", }, Amount: "0", NumAccounts: 0, @@ -837,6 +982,23 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertContractIDWithBalance() { return usdAssetStat.Equals(assetStat) })).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance{ + { + KeyHash: keyHash[:], + ContractID: btcID[:], + Amount: "20", + ExpirationLedger: 2234, + }, + }).Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -934,12 +1096,22 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestInsertClaimableBalanceAndTrustl Unauthorized: "0", ClaimableBalances: "12", LiquidityPools: "100", - Contracts: "0", }, Amount: "9", NumAccounts: 1, }).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -975,7 +1147,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractID() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -992,7 +1163,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractID() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1002,9 +1172,94 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractID() { return eurAssetStat.Equals(assetStat) })).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } +func (s *AssetStatsProcessorTestSuiteLedger) TestExpirationLedgerCannotDecrease() { + lastModifiedLedgerSeq := xdr.Uint32(1234) + + eurID, err := xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()).ContractID("") + s.Assert().NoError(err) + + keyHash := getKeyHashForBalance(s.T(), eurID, [32]byte{1}) + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeTtl, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: keyHash, + LiveUntilLedgerSeq: 2235, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: keyHash, + LiveUntilLedgerSeq: 2234, + }, + }, + }, + })) + + s.Assert().EqualError( + s.processor.Commit(s.ctx), + "Error adjusting asset stat: unexpected change in expiration ledger Pre: 2235 Post: 2234", + ) +} + +func (s *AssetStatsProcessorTestSuiteLedger) TestExpirationLedgerCannotBeLessThanCurrentLedger() { + lastModifiedLedgerSeq := xdr.Uint32(1234) + + eurID, err := xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()).ContractID("") + s.Assert().NoError(err) + + keyHash := getKeyHashForBalance(s.T(), eurID, [32]byte{1}) + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeTtl, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: keyHash, + LiveUntilLedgerSeq: 1230, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: keyHash, + LiveUntilLedgerSeq: 1234, + }, + }, + }, + })) + + s.Assert().EqualError( + s.processor.Commit(s.ctx), + "Error adjusting asset stat: post expiration ledger is less than current ledger. Pre: 1230 Post: 1234 current ledger: 1235", + ) +} + func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractIDWithBalance() { lastModifiedLedgerSeq := xdr.Uint32(1234) @@ -1029,6 +1284,20 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractIDWithBalance() { Data: BalanceToContractData(eurID, [32]byte{1}, 150), }, })) + keyHash := getKeyHashForBalance(s.T(), eurID, [32]byte{1}) + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeTtl, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: keyHash, + LiveUntilLedgerSeq: 2234, + }, + }, + }, + })) s.mockQ.On("GetAssetStat", s.ctx, xdr.AssetTypeAssetTypeCreditAlphanum4, @@ -1045,7 +1314,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractIDWithBalance() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1057,7 +1325,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractIDWithBalance() { AssetCode: "EUR", Accounts: history.ExpAssetStatAccounts{ Authorized: 1, - Contracts: 1, }, Balances: history.ExpAssetStatBalances{ Authorized: "100", @@ -1065,7 +1332,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractIDWithBalance() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "150", }, Amount: "100", NumAccounts: 1, @@ -1075,6 +1341,43 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractIDWithBalance() { return eurAssetStat.Equals(assetStat) })).Return(int64(1), nil).Once() + eurAssetContractStat := history.ContractAssetStatRow{ + ContractID: eurID[:], + Stat: history.ContractStat{ + ActiveBalance: "10", + ActiveHolders: 2, + ArchivedBalance: "0", + ArchivedHolders: 0, + }, + } + s.mockQ.On("GetContractAssetStat", s.ctx, eurID[:]). + Return(eurAssetContractStat, nil).Once() + + eurAssetContractStat.Stat.ActiveHolders++ + eurAssetContractStat.Stat.ActiveBalance = "160" + s.mockQ.On("UpdateContractAssetStat", s.ctx, mock.MatchedBy(func(row history.ContractAssetStatRow) bool { + return bytes.Equal(eurID[:], row.ContractID) && + eurAssetContractStat.Stat == row.Stat + })).Return(int64(1), nil).Once() + + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance{ + { + KeyHash: keyHash[:], + ContractID: eurID[:], + Amount: "150", + ExpirationLedger: 2234, + }, + }). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -1108,7 +1411,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateContractIDError() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1188,7 +1490,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustlineAndContractIDErr Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1223,7 +1524,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractIDError() { }) s.Assert().NoError(err) - s.mockQ.On("GetAssetStatByContract", s.ctx, eurID). + s.mockQ.On("GetAssetStatByContract", s.ctx, xdr.Hash(eurID)). Return(history.ExpAssetStat{}, sql.ErrNoRows).Once() s.Assert().EqualError( @@ -1292,7 +1593,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustlineAndRemoveContrac Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1359,7 +1659,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLine() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1375,12 +1674,22 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLine() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "110", NumAccounts: 1, }).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -1501,7 +1810,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLineAuthorization() Unauthorized: "100", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -1519,7 +1827,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLineAuthorization() Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "10", NumAccounts: 1, @@ -1542,7 +1849,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLineAuthorization() Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1560,7 +1866,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLineAuthorization() Unauthorized: "10", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -1583,7 +1888,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLineAuthorization() Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1601,12 +1905,22 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustLineAuthorization() Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 0, }).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -1669,7 +1983,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveClaimableBalance() { Unauthorized: "0", ClaimableBalances: "12", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -1698,7 +2011,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveClaimableBalance() { Unauthorized: "0", ClaimableBalances: "21", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -1714,12 +2026,22 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveClaimableBalance() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 0, }).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -1777,7 +2099,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveTrustLine() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 1, @@ -1805,7 +2126,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveTrustLine() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 0, @@ -1816,6 +2136,17 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveTrustLine() { trustLineIssuer.Address(), ).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -1847,13 +2178,12 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractID() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "100", NumAccounts: 1, } eurAssetStat.SetContractID(eurID) - s.mockQ.On("GetAssetStatByContract", s.ctx, eurID). + s.mockQ.On("GetAssetStatByContract", s.ctx, xdr.Hash(eurID)). Return(eurAssetStat, nil).Once() eurAssetStat.ContractID = nil @@ -1861,6 +2191,17 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractID() { return eurAssetStat.Equals(assetStat) })).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -1924,7 +2265,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustlineAndRemoveContrac Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "100", NumAccounts: 1, @@ -1947,7 +2287,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustlineAndRemoveContrac Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "110", NumAccounts: 1, @@ -1956,6 +2295,17 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestUpdateTrustlineAndRemoveContrac return eurAssetStat.Equals(assetStat) })).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -1987,13 +2337,12 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractIDFromZeroRow() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 0, } eurAssetStat.SetContractID(eurID) - s.mockQ.On("GetAssetStatByContract", s.ctx, eurID). + s.mockQ.On("GetAssetStatByContract", s.ctx, xdr.Hash(eurID)). Return(eurAssetStat, nil).Once() s.mockQ.On("RemoveAssetStat", s.ctx, @@ -2002,6 +2351,17 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractIDFromZeroRow() { trustLineIssuer.Address(), ).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -2029,6 +2389,20 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractIDAndBalanceZeroR Data: BalanceToContractData(eurID, [32]byte{1}, 9), }, })) + keyHash := getKeyHashForBalance(s.T(), eurID, [32]byte{1}) + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeTtl, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: keyHash, + LiveUntilLedgerSeq: 2234, + }, + }, + }, + })) s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeContractData, @@ -2037,25 +2411,38 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractIDAndBalanceZeroR Data: BalanceToContractData(eurID, [32]byte{2}, 1), }, })) + otherKeyHash := getKeyHashForBalance(s.T(), eurID, [32]byte{2}) + s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeTtl, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTtl, + Ttl: &xdr.TtlEntry{ + KeyHash: otherKeyHash, + LiveUntilLedgerSeq: 2234, + }, + }, + }, + })) eurAssetStat := history.ExpAssetStat{ AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, AssetIssuer: trustLineIssuer.Address(), AssetCode: "EUR", - Accounts: history.ExpAssetStatAccounts{Contracts: 2}, + Accounts: history.ExpAssetStatAccounts{}, Balances: history.ExpAssetStatBalances{ Authorized: "0", AuthorizedToMaintainLiabilities: "0", Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "10", }, Amount: "0", NumAccounts: 0, } eurAssetStat.SetContractID(eurID) - s.mockQ.On("GetAssetStatByContract", s.ctx, eurID). + s.mockQ.On("GetAssetStatByContract", s.ctx, xdr.Hash(eurID)). Return(eurAssetStat, nil).Once() s.mockQ.On("RemoveAssetStat", s.ctx, @@ -2064,6 +2451,33 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractIDAndBalanceZeroR trustLineIssuer.Address(), ).Return(int64(1), nil).Once() + eurAssetContractStat := history.ContractAssetStatRow{ + ContractID: eurID[:], + Stat: history.ContractStat{ + ActiveBalance: "10", + ActiveHolders: 2, + ArchivedBalance: "0", + ArchivedHolders: 0, + }, + } + s.mockQ.On("GetContractAssetStat", s.ctx, eurID[:]). + Return(eurAssetContractStat, nil).Once() + s.mockQ.On("RemoveAssetContractStat", s.ctx, eurID[:]). + Return(int64(1), nil).Once() + + s.mockQ.On("RemoveContractAssetBalances", s.ctx, mock.MatchedBy(func(keys []xdr.Hash) bool { + return assert.ElementsMatch(s.T(), []xdr.Hash{keyHash, otherKeyHash}, keys) + })). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -2115,7 +2529,6 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractIDAndRow() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "0", NumAccounts: 1, @@ -2133,6 +2546,17 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestRemoveContractIDAndRow() { trustLineIssuer.Address(), ).Return(int64(1), nil).Once() + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -2202,10 +2626,21 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestProcessUpgradeChange() { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "10", NumAccounts: 1, }).Return(int64(1), nil).Once() + + s.mockQ.On("RemoveContractAssetBalances", s.ctx, []xdr.Hash(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceAmounts", s.ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + s.mockQ.On("InsertContractAssetBalances", s.ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + s.mockQ.On("UpdateContractAssetBalanceExpirations", s.ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + s.mockQ.On("GetContractAssetBalancesExpiringAt", s.ctx, uint32(1234)). + Return([]history.ContractAssetBalance{}, nil).Once() + s.Assert().NoError(s.processor.Commit(s.ctx)) } diff --git a/services/horizon/internal/ingest/processors/asset_stats_set.go b/services/horizon/internal/ingest/processors/asset_stats_set.go index 5beffd1af7..bc27c2a4ef 100644 --- a/services/horizon/internal/ingest/processors/asset_stats_set.go +++ b/services/horizon/internal/ingest/processors/asset_stats_set.go @@ -1,8 +1,6 @@ package processors import ( - "encoding/hex" - "fmt" "math/big" "github.com/stellar/go/ingest" @@ -29,7 +27,6 @@ type assetStatBalances struct { ClaimableBalances *big.Int LiquidityPools *big.Int Unauthorized *big.Int - Contracts *big.Int } func newAssetStatBalance() assetStatBalances { @@ -39,11 +36,10 @@ func newAssetStatBalance() assetStatBalances { ClaimableBalances: big.NewInt(0), LiquidityPools: big.NewInt(0), Unauthorized: big.NewInt(0), - Contracts: big.NewInt(0), } } -func (a *assetStatBalances) Parse(b *history.ExpAssetStatBalances) error { +func (a *assetStatBalances) Parse(b history.ExpAssetStatBalances) error { authorized, ok := new(big.Int).SetString(b.Authorized, 10) if !ok { return errors.New("Error parsing: " + b.Authorized) @@ -74,12 +70,6 @@ func (a *assetStatBalances) Parse(b *history.ExpAssetStatBalances) error { } a.Unauthorized = unauthorized - contracts, ok := new(big.Int).SetString(b.Contracts, 10) - if !ok { - return errors.New("Error parsing: " + b.Contracts) - } - a.Contracts = contracts - return nil } @@ -90,7 +80,6 @@ func (a assetStatBalances) Add(b assetStatBalances) assetStatBalances { ClaimableBalances: big.NewInt(0).Add(a.ClaimableBalances, b.ClaimableBalances), LiquidityPools: big.NewInt(0).Add(a.LiquidityPools, b.LiquidityPools), Unauthorized: big.NewInt(0).Add(a.Unauthorized, b.Unauthorized), - Contracts: big.NewInt(0).Add(a.Contracts, b.Contracts), } } @@ -99,8 +88,7 @@ func (a assetStatBalances) IsZero() bool { a.AuthorizedToMaintainLiabilities.Cmp(big.NewInt(0)) == 0 && a.ClaimableBalances.Cmp(big.NewInt(0)) == 0 && a.LiquidityPools.Cmp(big.NewInt(0)) == 0 && - a.Unauthorized.Cmp(big.NewInt(0)) == 0 && - a.Contracts.Cmp(big.NewInt(0)) == 0 + a.Unauthorized.Cmp(big.NewInt(0)) == 0 } func (a assetStatBalances) ConvertToHistoryObject() history.ExpAssetStatBalances { @@ -110,7 +98,6 @@ func (a assetStatBalances) ConvertToHistoryObject() history.ExpAssetStatBalances ClaimableBalances: a.ClaimableBalances.String(), LiquidityPools: a.LiquidityPools.String(), Unauthorized: a.Unauthorized.String(), - Contracts: a.Contracts.String(), } } @@ -127,28 +114,17 @@ func (value assetStatValue) ConvertToHistoryObject() history.ExpAssetStat { } } -type contractAssetStatValue struct { - balance *big.Int - numHolders int32 -} - // AssetStatSet represents a collection of asset stats and a mapping // of Soroban contract IDs to classic assets (which is unique to each // network). type AssetStatSet struct { - classicAssetStats map[assetStatKey]*assetStatValue - contractToAsset map[[32]byte]*xdr.Asset - contractAssetStats map[[32]byte]contractAssetStatValue - networkPassphrase string + classicAssetStats map[assetStatKey]*assetStatValue } // NewAssetStatSet constructs a new AssetStatSet instance -func NewAssetStatSet(networkPassphrase string) AssetStatSet { +func NewAssetStatSet() AssetStatSet { return AssetStatSet{ - classicAssetStats: map[assetStatKey]*assetStatValue{}, - contractToAsset: map[[32]byte]*xdr.Asset{}, - contractAssetStats: map[[32]byte]contractAssetStatValue{}, - networkPassphrase: networkPassphrase, + classicAssetStats: map[assetStatKey]*assetStatValue{}, } } @@ -363,225 +339,12 @@ func (s AssetStatSet) AddClaimableBalance(change ingest.Change) error { return nil } -// AddContractData updates the set to account for how a given contract data entry has changed. -// change must be a xdr.LedgerEntryTypeContractData type. -func (s AssetStatSet) AddContractData(change ingest.Change) error { - if err := s.ingestAssetContractMetadata(change); err != nil { - return err - } - s.ingestAssetContractBalance(change) - return nil -} - -func (s AssetStatSet) ingestAssetContractMetadata(change ingest.Change) error { - if change.Pre != nil { - asset := AssetFromContractData(*change.Pre, s.networkPassphrase) - if asset == nil { - return nil - } - pContractID := change.Pre.Data.MustContractData().Contract.ContractId - if pContractID == nil { - return nil - } - contractID := *pContractID - if change.Post == nil { - s.contractToAsset[contractID] = nil - return nil - } - // The contract id for a stellar asset should never change and - // therefore we return a fatal ingestion error if we encounter - // a stellar asset changing contract ids. - postAsset := AssetFromContractData(*change.Post, s.networkPassphrase) - if postAsset == nil || !(*postAsset).Equals(*asset) { - return ingest.NewStateError(fmt.Errorf("asset contract changed asset")) - } - } else if change.Post != nil { - asset := AssetFromContractData(*change.Post, s.networkPassphrase) - if asset == nil { - return nil - } - if pContactID := change.Post.Data.MustContractData().Contract.ContractId; pContactID != nil { - s.contractToAsset[*pContactID] = asset - } - } - return nil -} - -func (s AssetStatSet) ingestAssetContractBalance(change ingest.Change) { - if change.Pre != nil { - pContractID := change.Pre.Data.MustContractData().Contract.ContractId - if pContractID == nil { - return - } - holder, amt, ok := ContractBalanceFromContractData(*change.Pre, s.networkPassphrase) - if !ok { - return - } - stats, ok := s.contractAssetStats[*pContractID] - if !ok { - stats = contractAssetStatValue{ - balance: big.NewInt(0), - numHolders: 0, - } - } - - if change.Post == nil { - // the balance was removed so we need to deduct from - // contract holders and contract balance amount - stats.balance = new(big.Int).Sub(stats.balance, amt) - // only decrement holders if the removed balance - // contained a positive amount of the asset. - if amt.Cmp(big.NewInt(0)) > 0 { - stats.numHolders-- - } - s.maybeAddContractAssetStat(*pContractID, stats) - return - } - // if the updated ledger entry is not in the expected format then this - // cannot be emitted by the stellar asset contract, so ignore it - postHolder, postAmt, postOk := ContractBalanceFromContractData(*change.Post, s.networkPassphrase) - if !postOk || postHolder != holder { - return - } - - delta := new(big.Int).Sub(postAmt, amt) - stats.balance.Add(stats.balance, delta) - if postAmt.Cmp(big.NewInt(0)) == 0 && amt.Cmp(big.NewInt(0)) > 0 { - // if the pre amount is equal to the post amount it means the balance was wiped out so - // we can decrement the number of contract holders - stats.numHolders-- - } else if amt.Cmp(big.NewInt(0)) == 0 && postAmt.Cmp(big.NewInt(0)) > 0 { - // if the pre amount was zero and the post amount is positive the number of - // contract holders increased - stats.numHolders++ - } - s.maybeAddContractAssetStat(*pContractID, stats) - return - } - // in this case there was no balance before the change - pContractID := change.Post.Data.MustContractData().Contract.ContractId - if pContractID == nil { - return - } - - _, amt, ok := ContractBalanceFromContractData(*change.Post, s.networkPassphrase) - if !ok { - return - } - - // ignore zero balance amounts - if amt.Cmp(big.NewInt(0)) == 0 { - return - } - - // increase the number of contract holders because previously - // there was no balance - stats, ok := s.contractAssetStats[*pContractID] - if !ok { - stats = contractAssetStatValue{ - balance: amt, - numHolders: 1, - } - } else { - stats.balance = new(big.Int).Add(stats.balance, amt) - stats.numHolders++ - } - - s.maybeAddContractAssetStat(*pContractID, stats) -} - -func (s AssetStatSet) maybeAddContractAssetStat(contractID [32]byte, stat contractAssetStatValue) { - if stat.numHolders == 0 && stat.balance.Cmp(big.NewInt(0)) == 0 { - delete(s.contractAssetStats, contractID) - } else { - s.contractAssetStats[contractID] = stat - } -} - // All returns a list of all `history.ExpAssetStat` contained within the set // along with all contract id attribution changes in the set. -func (s AssetStatSet) All() ([]history.ExpAssetStat, map[[32]byte]*xdr.Asset, map[[32]byte]contractAssetStatValue) { +func (s AssetStatSet) All() []history.ExpAssetStat { assetStats := make([]history.ExpAssetStat, 0, len(s.classicAssetStats)) for _, value := range s.classicAssetStats { assetStats = append(assetStats, value.ConvertToHistoryObject()) } - contractToAsset := make(map[[32]byte]*xdr.Asset, len(s.contractToAsset)) - for key, val := range s.contractToAsset { - contractToAsset[key] = val - } - contractAssetStats := make(map[[32]byte]contractAssetStatValue, len(s.contractAssetStats)) - for key, val := range s.contractAssetStats { - contractAssetStats[key] = val - } - return assetStats, contractToAsset, contractAssetStats -} - -// AllFromSnapshot returns a list of all `history.ExpAssetStat` contained within the set. -// AllFromSnapshot should only be invoked when the AssetStatSet has been derived from ledger -// entry changes consisting of only inserts (no updates) reflecting the current state of -// the ledger without any missing entries (e.g. history archives). -func (s AssetStatSet) AllFromSnapshot() ([]history.ExpAssetStat, error) { - // merge assetStatsDeltas and contractToAsset into one list of history.ExpAssetStat. - assetStatsDeltas, contractToAsset, contractAssetStats := s.All() - - // modify the asset stat row to update the contract_id column whenever we encounter a - // contract data ledger entry with the Stellar asset metadata. - for i, assetStatDelta := range assetStatsDeltas { - // asset stats only supports non-native assets - asset := xdr.MustNewCreditAsset(assetStatDelta.AssetCode, assetStatDelta.AssetIssuer) - contractID, err := asset.ContractID(s.networkPassphrase) - if err != nil { - return nil, errors.Wrap(err, "cannot compute contract id for asset") - } - if asset, ok := contractToAsset[contractID]; ok && asset == nil { - return nil, ingest.NewStateError(fmt.Errorf( - "unexpected contract data removal in history archives: %s", - hex.EncodeToString(contractID[:]), - )) - } else if ok { - assetStatDelta.SetContractID(contractID) - delete(contractToAsset, contractID) - } - - if stats, ok := contractAssetStats[contractID]; ok { - assetStatDelta.Accounts.Contracts = stats.numHolders - assetStatDelta.Balances.Contracts = stats.balance.String() - } - assetStatsDeltas[i] = assetStatDelta - } - - // There is also a corner case where a Stellar Asset contract is initialized before there exists any - // trustlines / claimable balances for the Stellar Asset. In this case, when ingesting contract data - // ledger entries, there will be no existing asset stat row. We handle this case by inserting a row - // with zero stats just so we can populate the contract id. - for contractID, asset := range contractToAsset { - if asset == nil { - return nil, ingest.NewStateError(fmt.Errorf( - "unexpected contract data removal in history archives: %s", - hex.EncodeToString(contractID[:]), - )) - } - var assetType xdr.AssetType - var assetCode, assetIssuer string - asset.MustExtract(&assetType, &assetCode, &assetIssuer) - row := history.ExpAssetStat{ - AssetType: assetType, - AssetCode: assetCode, - AssetIssuer: assetIssuer, - Accounts: history.ExpAssetStatAccounts{}, - Balances: newAssetStatBalance().ConvertToHistoryObject(), - Amount: "0", - NumAccounts: 0, - } - if stats, ok := contractAssetStats[contractID]; ok { - row.Accounts.Contracts = stats.numHolders - row.Balances.Contracts = stats.balance.String() - } - row.SetContractID(contractID) - assetStatsDeltas = append(assetStatsDeltas, row) - } - // all balances remaining in contractAssetStats do not belong to - // stellar asset contracts (because all stellar asset contracts must - // be in contractToAsset) so we can ignore them - return assetStatsDeltas, nil + return assetStats } diff --git a/services/horizon/internal/ingest/processors/asset_stats_set_test.go b/services/horizon/internal/ingest/processors/asset_stats_set_test.go index 016eda2bbb..f9989fdc9e 100644 --- a/services/horizon/internal/ingest/processors/asset_stats_set_test.go +++ b/services/horizon/internal/ingest/processors/asset_stats_set_test.go @@ -2,12 +2,9 @@ package processors import ( "math" - "math/big" "sort" "testing" - "github.com/stellar/go/keypair" - "github.com/stretchr/testify/assert" "github.com/stellar/go/ingest" @@ -16,21 +13,13 @@ import ( ) func TestEmptyAssetStatSet(t *testing.T) { - set := NewAssetStatSet("") - all, m, cs := set.All() - assert.Empty(t, all) - assert.Empty(t, cs) - assert.Empty(t, m) - - all, err := set.AllFromSnapshot() + set := NewAssetStatSet() + all := set.All() assert.Empty(t, all) - assert.NoError(t, err) } func assertAllEquals(t *testing.T, set AssetStatSet, expected []history.ExpAssetStat) { - all, m, cs := set.All() - assert.Empty(t, m) - assert.Empty(t, cs) + all := set.All() assertAssetStatsAreEqual(t, all, expected) } @@ -44,398 +33,8 @@ func assertAssetStatsAreEqual(t *testing.T, all []history.ExpAssetStat, expected } } -func assertAllFromSnapshotEquals(t *testing.T, set AssetStatSet, expected []history.ExpAssetStat) { - all, err := set.AllFromSnapshot() - assert.NoError(t, err) - assertAssetStatsAreEqual(t, all, expected) -} - -func TestAddContractData(t *testing.T) { - xlmID, err := xdr.MustNewNativeAsset().ContractID("passphrase") - assert.NoError(t, err) - usdcIssuer := keypair.MustRandom().Address() - usdcAsset := xdr.MustNewCreditAsset("USDC", usdcIssuer) - usdcID, err := usdcAsset.ContractID("passphrase") - assert.NoError(t, err) - etherIssuer := keypair.MustRandom().Address() - etherAsset := xdr.MustNewCreditAsset("ETHER", etherIssuer) - etherID, err := etherAsset.ContractID("passphrase") - assert.NoError(t, err) - uniAsset := xdr.MustNewCreditAsset("UNI", etherIssuer) - uniID, err := uniAsset.ContractID("passphrase") - assert.NoError(t, err) - - set := NewAssetStatSet("passphrase") - - xlmContractData, err := AssetToContractData(true, "", "", xlmID) - assert.NoError(t, err) - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Post: &xdr.LedgerEntry{ - Data: xlmContractData, - }, - }) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(xlmID, [32]byte{}, 100), - }, - }) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(uniID, [32]byte{}, 0), - }, - }) - assert.NoError(t, err) - - usdcContractData, err := AssetToContractData(false, "USDC", usdcIssuer, usdcID) - assert.NoError(t, err) - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Post: &xdr.LedgerEntry{ - Data: usdcContractData, - }, - }) - assert.NoError(t, err) - - etherContractData, err := AssetToContractData(false, "ETHER", etherIssuer, etherID) - assert.NoError(t, err) - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Post: &xdr.LedgerEntry{ - Data: etherContractData, - }, - }) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(etherID, [32]byte{}, 50), - }, - }) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(etherID, [32]byte{1}, 150), - }, - }) - assert.NoError(t, err) - - // negative balances will be ignored - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Post: &xdr.LedgerEntry{ - Data: balanceToContractData(etherID, [32]byte{1}, xdr.Int128Parts{Hi: -1, Lo: 0}), - }, - }) - assert.NoError(t, err) - - btcAsset := xdr.MustNewCreditAsset("BTC", etherIssuer) - btcID, err := btcAsset.ContractID("passphrase") - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(btcID, [32]byte{2}, 300), - }, - }) - assert.NoError(t, err) - - assert.NoError( - t, - set.AddTrustline(trustlineChange(nil, &xdr.TrustLineEntry{ - AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), - Asset: xdr.MustNewCreditAsset("ETHER", etherIssuer).ToTrustLineAsset(), - Balance: 1, - Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), - })), - ) - - all, m, cs := set.All() - assert.Len(t, all, 1) - etherAssetStat := history.ExpAssetStat{ - AssetType: xdr.AssetTypeAssetTypeCreditAlphanum12, - AssetCode: "ETHER", - AssetIssuer: etherIssuer, - Accounts: history.ExpAssetStatAccounts{ - Authorized: 1, - }, - Balances: history.ExpAssetStatBalances{ - Authorized: "1", - AuthorizedToMaintainLiabilities: "0", - Unauthorized: "0", - ClaimableBalances: "0", - LiquidityPools: "0", - Contracts: "0", - }, - Amount: "1", - NumAccounts: 1, - } - assert.True(t, all[0].Equals(etherAssetStat)) - assert.Len(t, m, 2) - assert.True(t, m[usdcID].Equals(usdcAsset)) - assert.True(t, m[etherID].Equals(etherAsset)) - assert.Len(t, cs, 2) - assert.Equal(t, cs[etherID].numHolders, int32(2)) - assert.Zero(t, cs[etherID].balance.Cmp(big.NewInt(200))) - assert.Equal(t, cs[btcID].numHolders, int32(1)) - assert.Zero(t, cs[btcID].balance.Cmp(big.NewInt(300))) - - usdcAssetStat := history.ExpAssetStat{ - AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, - AssetCode: "USDC", - AssetIssuer: usdcIssuer, - Accounts: history.ExpAssetStatAccounts{}, - Balances: newAssetStatBalance().ConvertToHistoryObject(), - Amount: "0", - NumAccounts: 0, - ContractID: nil, - } - - etherAssetStat.SetContractID(etherID) - etherAssetStat.Balances.Contracts = "200" - etherAssetStat.Accounts.Contracts = 2 - usdcAssetStat.SetContractID(usdcID) - - assertAllFromSnapshotEquals(t, set, []history.ExpAssetStat{ - etherAssetStat, - usdcAssetStat, - }) -} - -func TestUpdateContractBalance(t *testing.T) { - usdcIssuer := keypair.MustRandom().Address() - usdcAsset := xdr.MustNewCreditAsset("USDC", usdcIssuer) - usdcID, err := usdcAsset.ContractID("passphrase") - assert.NoError(t, err) - etherIssuer := keypair.MustRandom().Address() - etherAsset := xdr.MustNewCreditAsset("ETHER", etherIssuer) - etherID, err := etherAsset.ContractID("passphrase") - assert.NoError(t, err) - btcAsset := xdr.MustNewCreditAsset("BTC", etherIssuer) - btcID, err := btcAsset.ContractID("passphrase") - assert.NoError(t, err) - uniAsset := xdr.MustNewCreditAsset("UNI", etherIssuer) - uniID, err := uniAsset.ContractID("passphrase") - assert.NoError(t, err) - - set := NewAssetStatSet("passphrase") - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Pre: &xdr.LedgerEntry{ - Data: BalanceToContractData(usdcID, [32]byte{}, 50), - }, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(usdcID, [32]byte{}, 100), - }, - }) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Pre: &xdr.LedgerEntry{ - Data: BalanceToContractData(usdcID, [32]byte{2}, 30), - }, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(usdcID, [32]byte{2}, 100), - }, - }) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Pre: &xdr.LedgerEntry{ - Data: BalanceToContractData(usdcID, [32]byte{4}, 0), - }, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(usdcID, [32]byte{4}, 100), - }, - }) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Pre: &xdr.LedgerEntry{ - Data: BalanceToContractData(etherID, [32]byte{}, 200), - }, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(etherID, [32]byte{}, 50), - }, - }) - assert.NoError(t, err) - - // negative balances will be ignored - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Pre: &xdr.LedgerEntry{ - Data: BalanceToContractData(etherID, [32]byte{}, 200), - }, - Post: &xdr.LedgerEntry{ - Data: balanceToContractData(etherID, [32]byte{1}, xdr.Int128Parts{Hi: -1, Lo: 0}), - }, - }) - assert.NoError(t, err) - - // negative balances will be ignored - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Pre: &xdr.LedgerEntry{ - Data: balanceToContractData(etherID, [32]byte{1}, xdr.Int128Parts{Hi: -1, Lo: 0}), - }, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(etherID, [32]byte{}, 200), - }, - }) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Pre: &xdr.LedgerEntry{ - Data: BalanceToContractData(btcID, [32]byte{2}, 300), - }, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(btcID, [32]byte{2}, 300), - }, - }) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Pre: &xdr.LedgerEntry{ - Data: BalanceToContractData(btcID, [32]byte{2}, 0), - }, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(btcID, [32]byte{2}, 0), - }, - }) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Pre: &xdr.LedgerEntry{ - Data: BalanceToContractData(btcID, [32]byte{2}, 0), - }, - }) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(btcID, [32]byte{2}, 0), - }, - }) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Pre: &xdr.LedgerEntry{ - Data: BalanceToContractData(uniID, [32]byte{2}, 300), - }, - }) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Pre: &xdr.LedgerEntry{ - Data: BalanceToContractData(uniID, [32]byte{3}, 100), - }, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(uniID, [32]byte{3}, 0), - }, - }) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Pre: &xdr.LedgerEntry{ - Data: BalanceToContractData(uniID, [32]byte{4}, 100), - }, - Post: &xdr.LedgerEntry{ - Data: BalanceToContractData(uniID, [32]byte{4}, 50), - }, - }) - assert.NoError(t, err) - - all, m, cs := set.All() - assert.Empty(t, all) - assert.Empty(t, m) - - assert.Len(t, cs, 3) - assert.Equal(t, cs[usdcID].numHolders, int32(1)) - assert.Zero(t, cs[usdcID].balance.Cmp(big.NewInt(220))) - assert.Equal(t, cs[etherID].numHolders, int32(0)) - assert.Zero(t, cs[etherID].balance.Cmp(big.NewInt(-150))) - assert.Equal(t, cs[uniID].numHolders, int32(-2)) - assert.Zero(t, cs[uniID].balance.Cmp(big.NewInt(-450))) - - all, err = set.AllFromSnapshot() - assert.NoError(t, err) - assert.Empty(t, all) -} - -func TestRemoveContractData(t *testing.T) { - eurID, err := xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()).ContractID("passphrase") - assert.NoError(t, err) - set := NewAssetStatSet("passphrase") - - eurContractData, err := AssetToContractData(false, "EUR", trustLineIssuer.Address(), eurID) - assert.NoError(t, err) - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Pre: &xdr.LedgerEntry{ - Data: eurContractData, - }, - }) - assert.NoError(t, err) - - all, m, cs := set.All() - assert.Empty(t, all) - assert.Empty(t, cs) - assert.Len(t, m, 1) - asset, ok := m[eurID] - assert.True(t, ok) - assert.Nil(t, asset) -} - -func TestChangeContractData(t *testing.T) { - eurID, err := xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()).ContractID("passphrase") - assert.NoError(t, err) - - usdcIssuer := keypair.MustRandom().Address() - usdcID, err := xdr.MustNewCreditAsset("USDC", usdcIssuer).ContractID("passphrase") - assert.NoError(t, err) - - set := NewAssetStatSet("passphrase") - - eurContractData, err := AssetToContractData(false, "EUR", trustLineIssuer.Address(), eurID) - assert.NoError(t, err) - usdcContractData, err := AssetToContractData(false, "USDC", usdcIssuer, usdcID) - assert.NoError(t, err) - - err = set.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Pre: &xdr.LedgerEntry{ - Data: eurContractData, - }, - Post: &xdr.LedgerEntry{ - Data: usdcContractData, - }, - }) - assert.EqualError(t, err, "asset contract changed asset") -} - func TestAddNativeClaimableBalance(t *testing.T) { - set := NewAssetStatSet("") + set := NewAssetStatSet() claimableBalance := xdr.ClaimableBalanceEntry{ BalanceId: xdr.ClaimableBalanceId{}, Claimants: nil, @@ -451,10 +50,8 @@ func TestAddNativeClaimableBalance(t *testing.T) { }, }, )) - all, m, cs := set.All() + all := set.All() assert.Empty(t, all) - assert.Empty(t, m) - assert.Empty(t, cs) } func trustlineChange(pre, post *xdr.TrustLineEntry) ingest.Change { @@ -477,7 +74,7 @@ func trustlineChange(pre, post *xdr.TrustLineEntry) ingest.Change { } func TestAddPoolShareTrustline(t *testing.T) { - set := NewAssetStatSet("") + set := NewAssetStatSet() assert.NoError( t, set.AddTrustline(trustlineChange(nil, &xdr.TrustLineEntry{ @@ -491,14 +88,12 @@ func TestAddPoolShareTrustline(t *testing.T) { }, )), ) - all, m, cs := set.All() + all := set.All() assert.Empty(t, all) - assert.Empty(t, m) - assert.Empty(t, cs) } func TestAddAssetStats(t *testing.T) { - set := NewAssetStatSet("") + set := NewAssetStatSet() eur := "EUR" eurAssetStat := history.ExpAssetStat{ AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, @@ -513,7 +108,6 @@ func TestAddAssetStats(t *testing.T) { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "1", NumAccounts: 1, @@ -619,7 +213,6 @@ func TestAddAssetStats(t *testing.T) { Unauthorized: "5", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "3", NumAccounts: 1, @@ -638,7 +231,6 @@ func TestAddAssetStats(t *testing.T) { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "10", NumAccounts: 1, @@ -648,7 +240,7 @@ func TestAddAssetStats(t *testing.T) { } func TestOverflowAssetStatSet(t *testing.T) { - set := NewAssetStatSet("") + set := NewAssetStatSet() eur := "EUR" err := set.AddTrustline(trustlineChange(nil, &xdr.TrustLineEntry{ AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), @@ -659,12 +251,10 @@ func TestOverflowAssetStatSet(t *testing.T) { if err != nil { t.Fatalf("unexpected error %v", err) } - all, m, cs := set.All() + all := set.All() if len(all) != 1 { t.Fatalf("expected list of 1 asset stat but got %v", all) } - assert.Empty(t, m) - assert.Empty(t, cs) eurAssetStat := history.ExpAssetStat{ AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, @@ -679,7 +269,6 @@ func TestOverflowAssetStatSet(t *testing.T) { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "9223372036854775807", NumAccounts: 1, @@ -697,12 +286,10 @@ func TestOverflowAssetStatSet(t *testing.T) { if err != nil { t.Fatalf("unexpected error %v", err) } - all, m, cs = set.All() + all = set.All() if len(all) != 1 { t.Fatalf("expected list of 1 asset stat but got %v", all) } - assert.Empty(t, m) - assert.Empty(t, cs) eurAssetStat = history.ExpAssetStat{ AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, @@ -717,7 +304,6 @@ func TestOverflowAssetStatSet(t *testing.T) { Unauthorized: "0", ClaimableBalances: "0", LiquidityPools: "0", - Contracts: "0", }, Amount: "18446744073709551614", NumAccounts: 2, diff --git a/services/horizon/internal/ingest/processors/contract_asset_stats.go b/services/horizon/internal/ingest/processors/contract_asset_stats.go new file mode 100644 index 0000000000..d39ceb6b6d --- /dev/null +++ b/services/horizon/internal/ingest/processors/contract_asset_stats.go @@ -0,0 +1,412 @@ +package processors + +import ( + "context" + "crypto/sha256" + "fmt" + "math/big" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +type assetContractStatValue struct { + contractID xdr.Hash + activeBalance *big.Int + activeHolders int32 + archivedBalance *big.Int + archivedHolders int32 +} + +func (v assetContractStatValue) ConvertToHistoryObject() history.ContractAssetStatRow { + return history.ContractAssetStatRow{ + ContractID: v.contractID[:], + Stat: history.ContractStat{ + ActiveBalance: v.activeBalance.String(), + ActiveHolders: v.activeHolders, + ArchivedBalance: v.archivedBalance.String(), + ArchivedHolders: v.archivedHolders, + }, + } +} + +type contractAssetBalancesQ interface { + GetContractAssetBalances(ctx context.Context, keys []xdr.Hash) ([]history.ContractAssetBalance, error) + GetContractAssetBalancesExpiringAt(ctx context.Context, ledger uint32) ([]history.ContractAssetBalance, error) +} + +// ContractAssetStatSet represents a collection of asset stats for +// contract asset holders +type ContractAssetStatSet struct { + contractToAsset map[xdr.Hash]*xdr.Asset + contractAssetStats map[xdr.Hash]assetContractStatValue + createdBalances []history.ContractAssetBalance + removedBalances []xdr.Hash + updatedBalances map[xdr.Hash]*big.Int + removedExpirationEntries map[xdr.Hash]uint32 + createdExpirationEntries map[xdr.Hash]uint32 + updatedExpirationEntries map[xdr.Hash][2]uint32 + networkPassphrase string + assetStatsQ contractAssetBalancesQ + currentLedger uint32 +} + +// NewContractAssetStatSet constructs a new ContractAssetStatSet instance +func NewContractAssetStatSet( + assetStatsQ contractAssetBalancesQ, + networkPassphrase string, + removedExpirationEntries map[xdr.Hash]uint32, + createdExpirationEntries map[xdr.Hash]uint32, + updatedExpirationEntries map[xdr.Hash][2]uint32, + currentLedger uint32, +) *ContractAssetStatSet { + return &ContractAssetStatSet{ + contractToAsset: map[xdr.Hash]*xdr.Asset{}, + contractAssetStats: map[xdr.Hash]assetContractStatValue{}, + networkPassphrase: networkPassphrase, + assetStatsQ: assetStatsQ, + removedExpirationEntries: removedExpirationEntries, + createdExpirationEntries: createdExpirationEntries, + updatedExpirationEntries: updatedExpirationEntries, + currentLedger: currentLedger, + updatedBalances: map[xdr.Hash]*big.Int{}, + } +} + +// AddContractData updates the set to account for how a given contract data entry has changed. +// change must be a xdr.LedgerEntryTypeContractData type. +func (s *ContractAssetStatSet) AddContractData(ctx context.Context, change ingest.Change) error { + // skip ingestion of contract asset balances if we find an asset contract metadata entry + // because a ledger entry cannot be both an asset contract metadata entry and a + // contract asset balance. + if found, err := s.ingestAssetContractMetadata(change); err != nil { + return err + } else if found { + return nil + } + return s.ingestContractAssetBalance(ctx, change) +} + +func (s *ContractAssetStatSet) GetContractStats() []history.ContractAssetStatRow { + var contractStats []history.ContractAssetStatRow + for _, contractStat := range s.contractAssetStats { + contractStats = append(contractStats, contractStat.ConvertToHistoryObject()) + } + return contractStats +} + +func (s *ContractAssetStatSet) GetCreatedBalances() []history.ContractAssetBalance { + return s.createdBalances +} + +func (s *ContractAssetStatSet) GetAssetToContractMap() map[xdr.Hash]*xdr.Asset { + return s.contractToAsset +} + +func (s *ContractAssetStatSet) ingestAssetContractMetadata(change ingest.Change) (bool, error) { + if change.Pre != nil { + asset := AssetFromContractData(*change.Pre, s.networkPassphrase) + if asset == nil { + return false, nil + } + pContractID := change.Pre.Data.MustContractData().Contract.ContractId + if pContractID == nil { + return false, nil + } + contractID := *pContractID + if change.Post == nil { + s.contractToAsset[contractID] = nil + return true, nil + } + // The contract id for any soroban contract should never change and + // therefore we return a fatal ingestion error if we encounter + // a stellar asset changing contract ids. + postAsset := AssetFromContractData(*change.Post, s.networkPassphrase) + if postAsset == nil || !(*postAsset).Equals(*asset) { + return false, ingest.NewStateError(fmt.Errorf("asset contract changed asset")) + } + return true, nil + } else if change.Post != nil { + asset := AssetFromContractData(*change.Post, s.networkPassphrase) + if asset == nil { + return false, nil + } + if pContactID := change.Post.Data.MustContractData().Contract.ContractId; pContactID != nil { + s.contractToAsset[*pContactID] = asset + return true, nil + } + } + return false, nil +} + +func getKeyHash(ledgerEntry xdr.LedgerEntry) (xdr.Hash, error) { + lk, err := ledgerEntry.LedgerKey() + if err != nil { + return xdr.Hash{}, errors.Wrap(err, "could not extract ledger key") + } + bin, err := lk.MarshalBinary() + if err != nil { + return xdr.Hash{}, errors.Wrap(err, "could not marshal key") + } + return sha256.Sum256(bin), nil +} + +func (s *ContractAssetStatSet) ingestContractAssetBalance(ctx context.Context, change ingest.Change) error { + switch { + case change.Pre == nil && change.Post != nil: // created + pContractID := change.Post.Data.MustContractData().Contract.ContractId + if pContractID == nil { + return nil + } + + _, postAmt, postOk := ContractBalanceFromContractData(*change.Post, s.networkPassphrase) + // we only ingest created ledger entries if we determine that they resemble the shape of + // a Stellar Asset Contract balance ledger entry + if !postOk { + return nil + } + + keyHash, err := getKeyHash(*change.Post) + if err != nil { + return err + } + expirationLedger, ok := s.createdExpirationEntries[keyHash] + if !ok { + return nil + } + s.createdBalances = append(s.createdBalances, history.ContractAssetBalance{ + KeyHash: keyHash[:], + ContractID: (*pContractID)[:], + Amount: postAmt.String(), + ExpirationLedger: expirationLedger, + }) + + stat := s.getContractAssetStat(*pContractID) + if expirationLedger >= s.currentLedger { + stat.activeHolders++ + stat.activeBalance.Add(stat.activeBalance, postAmt) + } else { + stat.archivedHolders++ + stat.archivedBalance.Add(stat.archivedBalance, postAmt) + } + s.maybeAddContractAssetStat(*pContractID, stat) + case change.Pre != nil && change.Post == nil: // removed + pContractID := change.Pre.Data.MustContractData().Contract.ContractId + if pContractID == nil { + return nil + } + + keyHash, err := getKeyHash(*change.Pre) + if err != nil { + return err + } + // We always include the key hash in s.removedBalances even + // if the ledger entry is not a valid balance ledger entry. + // It's possible that a contract is able to forge a created + // balance ledger entry which matches the Stellar Asset Contract + // and later on the ledger entry is updated to an invalid state. + // In such a scenario we still want to remove the balance ledger + // entry from our db when the entry is removed from the ledger. + s.removedBalances = append(s.removedBalances, keyHash) + + _, preAmt, ok := ContractBalanceFromContractData(*change.Pre, s.networkPassphrase) + if !ok { + return nil + } + + expirationLedger, ok := s.removedExpirationEntries[keyHash] + if !ok { + return nil + } + + stat := s.getContractAssetStat(*pContractID) + if expirationLedger >= s.currentLedger { + stat.activeHolders-- + stat.activeBalance = new(big.Int).Sub(stat.activeBalance, preAmt) + } else { + stat.archivedHolders-- + stat.archivedBalance = new(big.Int).Sub(stat.archivedBalance, preAmt) + } + s.maybeAddContractAssetStat(*pContractID, stat) + case change.Pre != nil && change.Post != nil: // updated + pContractID := change.Pre.Data.MustContractData().Contract.ContractId + if pContractID == nil { + return nil + } + + holder, amt, ok := ContractBalanceFromContractData(*change.Pre, s.networkPassphrase) + if !ok { + return nil + } + + // if the updated ledger entry is not in the expected format then this + // cannot be emitted by the stellar asset contract, so ignore it + postHolder, postAmt, postOk := ContractBalanceFromContractData(*change.Post, s.networkPassphrase) + if !postOk || postHolder != holder { + return nil + } + + amtDelta := new(big.Int).Sub(postAmt, amt) + if amtDelta.Cmp(big.NewInt(0)) == 0 { + return nil + } + + keyHash, err := getKeyHash(*change.Post) + if err != nil { + return err + } + + var preExpiration, postExpiration uint32 + if expirationUpdate, ok := s.updatedExpirationEntries[keyHash]; ok { + preExpiration, postExpiration = expirationUpdate[0], expirationUpdate[1] + } else { + rows, err := s.assetStatsQ.GetContractAssetBalances(ctx, []xdr.Hash{keyHash}) + if err != nil { + return errors.Wrapf(err, "could not query contract asset balance for %v", keyHash) + } + if len(rows) == 0 { + return nil + } + if len(rows) != 1 { + return errors.Wrapf( + err, + "expected 1 contract asset balance for %v but got %v", + keyHash, + len(rows), + ) + } + preExpiration = rows[0].ExpirationLedger + postExpiration = preExpiration + } + if postExpiration < s.currentLedger { + return errors.Errorf( + "contract balance has invalid expiration ledger keyhash %v expiration ledger %v", + keyHash, + postExpiration, + ) + } + + s.updatedBalances[keyHash] = postAmt + stat := s.getContractAssetStat(*pContractID) + if preExpiration+1 >= s.currentLedger { // active balance was updated + stat.activeBalance.Add(stat.activeBalance, amtDelta) + } else { // balance was restored + stat.activeHolders++ + stat.archivedHolders-- + stat.activeBalance.Add(stat.activeBalance, postAmt) + stat.archivedBalance.Sub(stat.archivedBalance, amt) + } + s.maybeAddContractAssetStat(*pContractID, stat) + default: + return errors.Errorf("unexpected change Pre: %v Post: %v", change.Pre, change.Post) + } + return nil +} + +func (s *ContractAssetStatSet) ingestRestoredBalances(ctx context.Context) error { + var keyHashes []xdr.Hash + for keyHash, expirationUpdate := range s.updatedExpirationEntries { + prevExpirationLedger := expirationUpdate[0] + // prevExpirationLedger+1 >= s.currentLedger indicates that this contract balance is still + // active in our DB and therefore don't need to restore it. + // s.updatedBalances[keyHash] != nil indicates that this contract balance was already ingested + // in ingestContractAssetBalance() so we don't need to ingest it again here. + if prevExpirationLedger+1 >= s.currentLedger || s.updatedBalances[keyHash] != nil { + continue + } + keyHashes = append(keyHashes, keyHash) + } + if len(keyHashes) == 0 { + return nil + } + + rows, err := s.assetStatsQ.GetContractAssetBalances(ctx, keyHashes) + if err != nil { + return errors.Wrap(err, "Error fetching contract asset balances") + } + + for _, row := range rows { + var contractID xdr.Hash + copy(contractID[:], row.ContractID) + stat := s.getContractAssetStat(contractID) + amt, ok := new(big.Int).SetString(row.Amount, 10) + if !ok { + return errors.Errorf( + "contract balance %v has invalid amount: %v", + row.KeyHash, + row.Amount, + ) + } + + stat.activeHolders++ + stat.activeBalance.Add(stat.activeBalance, amt) + stat.archivedHolders-- + stat.archivedBalance.Sub(stat.archivedBalance, amt) + s.maybeAddContractAssetStat(contractID, stat) + } + + return nil +} + +func (s *ContractAssetStatSet) ingestExpiredBalances(ctx context.Context) error { + rows, err := s.assetStatsQ.GetContractAssetBalancesExpiringAt(ctx, s.currentLedger-1) + if err != nil { + return errors.Wrap(err, "Error fetching contract asset balances") + } + + for _, row := range rows { + var keyHash, contractID xdr.Hash + copy(keyHash[:], row.KeyHash) + + if _, ok := s.updatedExpirationEntries[keyHash]; ok { + // the expiration of this contract balance was bumped, so we can + // skip this contract balance since it is still active + continue + } + + copy(contractID[:], row.ContractID) + stat := s.getContractAssetStat(contractID) + amt, ok := new(big.Int).SetString(row.Amount, 10) + if !ok { + return errors.Errorf( + "contract balance %v has invalid amount: %v", + row.KeyHash, + row.Amount, + ) + } + + stat.activeHolders-- + stat.activeBalance.Sub(stat.activeBalance, amt) + stat.archivedHolders++ + stat.archivedBalance.Add(stat.archivedBalance, amt) + s.maybeAddContractAssetStat(contractID, stat) + } + + return nil +} + +func (s *ContractAssetStatSet) maybeAddContractAssetStat(contractID xdr.Hash, stat assetContractStatValue) { + if stat.archivedHolders == 0 && stat.activeHolders == 0 && + stat.activeBalance.Cmp(big.NewInt(0)) == 0 && + stat.archivedBalance.Cmp(big.NewInt(0)) == 0 { + delete(s.contractAssetStats, contractID) + } else { + s.contractAssetStats[contractID] = stat + } +} + +func (s *ContractAssetStatSet) getContractAssetStat(contractID xdr.Hash) assetContractStatValue { + stat, ok := s.contractAssetStats[contractID] + if !ok { + stat = assetContractStatValue{ + contractID: contractID, + activeBalance: big.NewInt(0), + activeHolders: 0, + archivedBalance: big.NewInt(0), + archivedHolders: 0, + } + } + return stat +} diff --git a/services/horizon/internal/ingest/processors/contract_asset_stats_test.go b/services/horizon/internal/ingest/processors/contract_asset_stats_test.go new file mode 100644 index 0000000000..ce89920ecd --- /dev/null +++ b/services/horizon/internal/ingest/processors/contract_asset_stats_test.go @@ -0,0 +1,636 @@ +package processors + +import ( + "context" + "crypto/sha256" + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/keypair" + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/xdr" +) + +func getKeyHashForBalance(t *testing.T, assetContractId, holderID [32]byte) xdr.Hash { + ledgerKey := ContractBalanceLedgerKey(assetContractId, holderID) + bin, err := ledgerKey.MarshalBinary() + assert.NoError(t, err) + return sha256.Sum256(bin) +} + +func TestAddContractData(t *testing.T) { + xlmID, err := xdr.MustNewNativeAsset().ContractID("passphrase") + assert.NoError(t, err) + usdcIssuer := keypair.MustRandom().Address() + usdcAsset := xdr.MustNewCreditAsset("USDC", usdcIssuer) + usdcID, err := usdcAsset.ContractID("passphrase") + assert.NoError(t, err) + etherIssuer := keypair.MustRandom().Address() + etherAsset := xdr.MustNewCreditAsset("ETHER", etherIssuer) + etherID, err := etherAsset.ContractID("passphrase") + assert.NoError(t, err) + uniAsset := xdr.MustNewCreditAsset("UNI", etherIssuer) + uniID, err := uniAsset.ContractID("passphrase") + assert.NoError(t, err) + + set := NewContractAssetStatSet( + &history.MockQAssetStats{}, + "passphrase", + map[xdr.Hash]uint32{}, + map[xdr.Hash]uint32{}, + map[xdr.Hash][2]uint32{}, + 150, + ) + + xlmContractData, err := AssetToContractData(true, "", "", xlmID) + assert.NoError(t, err) + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: xlmContractData, + }, + }) + assert.NoError(t, err) + + xlmBalanceKeyHash := getKeyHashForBalance(t, xlmID, [32]byte{}) + assert.NoError(t, err) + set.createdExpirationEntries[xlmBalanceKeyHash] = 150 + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(xlmID, [32]byte{}, 100), + }, + }) + assert.NoError(t, err) + + uniBalanceKeyHash := getKeyHashForBalance(t, uniID, [32]byte{}) + set.createdExpirationEntries[uniBalanceKeyHash] = 150 + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(uniID, [32]byte{}, 0), + }, + }) + assert.NoError(t, err) + + usdcContractData, err := AssetToContractData(false, "USDC", usdcIssuer, usdcID) + assert.NoError(t, err) + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: usdcContractData, + }, + }) + assert.NoError(t, err) + + etherContractData, err := AssetToContractData(false, "ETHER", etherIssuer, etherID) + assert.NoError(t, err) + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: etherContractData, + }, + }) + assert.NoError(t, err) + + etherBalanceKeyHash := getKeyHashForBalance(t, etherID, [32]byte{}) + set.createdExpirationEntries[etherBalanceKeyHash] = 100 + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(etherID, [32]byte{}, 50), + }, + }) + assert.NoError(t, err) + + otherEtherBalanceKeyHash := getKeyHashForBalance(t, etherID, [32]byte{1}) + set.createdExpirationEntries[otherEtherBalanceKeyHash] = 150 + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(etherID, [32]byte{1}, 150), + }, + }) + assert.NoError(t, err) + + // negative balances will be ignored + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: balanceToContractData(etherID, [32]byte{1}, xdr.Int128Parts{Hi: -1, Lo: 0}), + }, + }) + assert.NoError(t, err) + + btcAsset := xdr.MustNewCreditAsset("BTC", etherIssuer) + btcID, err := btcAsset.ContractID("passphrase") + assert.NoError(t, err) + + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(btcID, [32]byte{2}, 300), + }, + }) + assert.NoError(t, err) + + assert.Empty(t, set.updatedBalances) + assert.Empty(t, set.removedBalances) + assert.Len(t, set.contractToAsset, 2) + assert.True(t, set.contractToAsset[usdcID].Equals(usdcAsset)) + assert.True(t, set.contractToAsset[etherID].Equals(etherAsset)) + assert.Equal(t, []history.ContractAssetBalance{ + { + KeyHash: uniBalanceKeyHash[:], + ContractID: uniID[:], + Amount: "0", + ExpirationLedger: 150, + }, + { + KeyHash: etherBalanceKeyHash[:], + ContractID: etherID[:], + Amount: "50", + ExpirationLedger: 100, + }, + { + KeyHash: otherEtherBalanceKeyHash[:], + ContractID: etherID[:], + Amount: "150", + ExpirationLedger: 150, + }, + }, set.createdBalances) + assert.ElementsMatch(t, set.GetContractStats(), []history.ContractAssetStatRow{ + { + ContractID: uniID[:], + Stat: history.ContractStat{ + ActiveBalance: "0", + ArchivedBalance: "0", + ActiveHolders: 1, + ArchivedHolders: 0, + }, + }, + { + ContractID: etherID[:], + Stat: history.ContractStat{ + ActiveBalance: "150", + ArchivedBalance: "50", + ActiveHolders: 1, + ArchivedHolders: 1, + }, + }, + }) +} + +func TestUpdateContractBalance(t *testing.T) { + usdcIssuer := keypair.MustRandom().Address() + usdcAsset := xdr.MustNewCreditAsset("USDC", usdcIssuer) + usdcID, err := usdcAsset.ContractID("passphrase") + assert.NoError(t, err) + etherIssuer := keypair.MustRandom().Address() + etherAsset := xdr.MustNewCreditAsset("ETHER", etherIssuer) + etherID, err := etherAsset.ContractID("passphrase") + assert.NoError(t, err) + btcAsset := xdr.MustNewCreditAsset("BTC", etherIssuer) + btcID, err := btcAsset.ContractID("passphrase") + assert.NoError(t, err) + uniAsset := xdr.MustNewCreditAsset("UNI", etherIssuer) + uniID, err := uniAsset.ContractID("passphrase") + assert.NoError(t, err) + + mockQ := &history.MockQAssetStats{} + set := NewContractAssetStatSet( + mockQ, + "passphrase", + map[xdr.Hash]uint32{}, + map[xdr.Hash]uint32{}, + map[xdr.Hash][2]uint32{}, + 150, + ) + expectedBalances := map[xdr.Hash]string{} + + keyHash := getKeyHashForBalance(t, usdcID, [32]byte{}) + set.updatedExpirationEntries[keyHash] = [2]uint32{160, 170} + expectedBalances[keyHash] = "100" + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{}, 50), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{}, 100), + }, + }) + assert.NoError(t, err) + + keyHash = getKeyHashForBalance(t, usdcID, [32]byte{2}) + ctx := context.Background() + mockQ.On("GetContractAssetBalances", ctx, []xdr.Hash{keyHash}). + Return([]history.ContractAssetBalance{ + { + KeyHash: keyHash[:], + ContractID: usdcID[:], + Amount: "30", + ExpirationLedger: 180, + }, + }, nil).Once() + expectedBalances[keyHash] = "100" + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{2}, 30), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{2}, 100), + }, + }) + assert.NoError(t, err) + + keyHash = getKeyHashForBalance(t, usdcID, [32]byte{4}) + // balances which don't exist in the db will be ignored + mockQ.On("GetContractAssetBalances", ctx, []xdr.Hash{keyHash}). + Return([]history.ContractAssetBalance{}, nil).Once() + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{4}, 0), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{4}, 100), + }, + }) + assert.NoError(t, err) + + keyHash = getKeyHashForBalance(t, etherID, [32]byte{}) + mockQ.On("GetContractAssetBalances", ctx, []xdr.Hash{keyHash}). + Return([]history.ContractAssetBalance{ + { + KeyHash: keyHash[:], + ContractID: etherID[:], + Amount: "200", + ExpirationLedger: 200, + }, + }, nil).Once() + expectedBalances[keyHash] = "50" + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(etherID, [32]byte{}, 200), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(etherID, [32]byte{}, 50), + }, + }) + assert.NoError(t, err) + + // negative balances will be ignored + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(etherID, [32]byte{}, 200), + }, + Post: &xdr.LedgerEntry{ + Data: balanceToContractData(etherID, [32]byte{1}, xdr.Int128Parts{Hi: -1, Lo: 0}), + }, + }) + assert.NoError(t, err) + + // negative balances will be ignored + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: balanceToContractData(etherID, [32]byte{1}, xdr.Int128Parts{Hi: -1, Lo: 0}), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(etherID, [32]byte{}, 200), + }, + }) + assert.NoError(t, err) + + // balances where the amount doesn't change will be ignored + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(btcID, [32]byte{2}, 300), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(btcID, [32]byte{2}, 300), + }, + }) + assert.NoError(t, err) + + keyHash = getKeyHashForBalance(t, btcID, [32]byte{5}) + mockQ.On("GetContractAssetBalances", ctx, []xdr.Hash{keyHash}). + Return([]history.ContractAssetBalance{ + { + KeyHash: keyHash[:], + ContractID: btcID[:], + Amount: "10", + ExpirationLedger: 20, + }, + }, nil).Once() + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(btcID, [32]byte{5}, 10), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(btcID, [32]byte{5}, 15), + }, + }) + assert.ErrorContains(t, err, "contract balance has invalid expiration ledger keyhash") + + keyHash = getKeyHashForBalance(t, btcID, [32]byte{6}) + set.updatedExpirationEntries[keyHash] = [2]uint32{100, 110} + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(btcID, [32]byte{6}, 120), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(btcID, [32]byte{6}, 135), + }, + }) + assert.ErrorContains(t, err, "contract balance has invalid expiration ledger keyhash") + + keyHash = getKeyHashForBalance(t, uniID, [32]byte{4}) + set.updatedExpirationEntries[keyHash] = [2]uint32{100, 170} + expectedBalances[keyHash] = "75" + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(uniID, [32]byte{4}, 50), + }, + Post: &xdr.LedgerEntry{ + Data: BalanceToContractData(uniID, [32]byte{4}, 75), + }, + }) + assert.NoError(t, err) + + assert.Empty(t, set.contractToAsset) + assert.Empty(t, set.removedBalances) + assert.Empty(t, set.createdExpirationEntries) + for key, amt := range set.updatedBalances { + assert.Equal(t, expectedBalances[key], amt.String()) + delete(expectedBalances, key) + } + assert.Empty(t, expectedBalances) + + assert.ElementsMatch(t, set.GetContractStats(), []history.ContractAssetStatRow{ + { + ContractID: usdcID[:], + Stat: history.ContractStat{ + ActiveBalance: "120", + ActiveHolders: 0, + ArchivedBalance: "0", + ArchivedHolders: 0, + }, + }, + { + ContractID: etherID[:], + Stat: history.ContractStat{ + ActiveBalance: "-150", + ActiveHolders: 0, + ArchivedBalance: "0", + ArchivedHolders: 0, + }, + }, + { + ContractID: uniID[:], + Stat: history.ContractStat{ + ActiveBalance: "75", + ActiveHolders: 1, + ArchivedBalance: "-50", + ArchivedHolders: -1, + }, + }, + }) + + mockQ.AssertExpectations(t) +} + +func TestRemoveContractData(t *testing.T) { + usdcIssuer := keypair.MustRandom().Address() + usdcAsset := xdr.MustNewCreditAsset("USDC", usdcIssuer) + usdcID, err := usdcAsset.ContractID("passphrase") + assert.NoError(t, err) + + set := NewContractAssetStatSet( + &history.MockQAssetStats{}, + "passphrase", + map[xdr.Hash]uint32{}, + map[xdr.Hash]uint32{}, + map[xdr.Hash][2]uint32{}, + 150, + ) + + usdcContractData, err := AssetToContractData(false, "USDC", usdcIssuer, usdcID) + assert.NoError(t, err) + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: usdcContractData, + }, + }) + assert.NoError(t, err) + + keyHash := getKeyHashForBalance(t, usdcID, [32]byte{}) + set.removedExpirationEntries[keyHash] = 170 + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{}, 50), + }, + }) + assert.NoError(t, err) + + keyHash1 := getKeyHashForBalance(t, usdcID, [32]byte{1}) + set.removedExpirationEntries[keyHash1] = 100 + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{1}, 20), + }, + }) + assert.NoError(t, err) + + keyHash2 := getKeyHashForBalance(t, usdcID, [32]byte{2}) + err = set.AddContractData(context.Background(), ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Pre: &xdr.LedgerEntry{ + Data: BalanceToContractData(usdcID, [32]byte{2}, 34), + }, + }) + assert.NoError(t, err) + + assert.Equal(t, []xdr.Hash{keyHash, keyHash1, keyHash2}, set.removedBalances) + assert.Len(t, set.contractToAsset, 1) + asset, ok := set.contractToAsset[usdcID] + assert.Nil(t, asset) + assert.True(t, ok) + + assert.ElementsMatch(t, set.GetContractStats(), []history.ContractAssetStatRow{ + { + ContractID: usdcID[:], + Stat: history.ContractStat{ + ActiveBalance: "-50", + ActiveHolders: -1, + ArchivedBalance: "-20", + ArchivedHolders: -1, + }, + }, + }) +} + +func TestIngestRestoredBalances(t *testing.T) { + usdcIssuer := keypair.MustRandom().Address() + usdcAsset := xdr.MustNewCreditAsset("USDC", usdcIssuer) + usdcID, err := usdcAsset.ContractID("passphrase") + assert.NoError(t, err) + + mockQ := &history.MockQAssetStats{} + set := NewContractAssetStatSet( + mockQ, + "passphrase", + map[xdr.Hash]uint32{}, + map[xdr.Hash]uint32{}, + map[xdr.Hash][2]uint32{}, + 150, + ) + + usdcKeyHash := getKeyHashForBalance(t, usdcID, [32]byte{}) + set.updatedBalances[usdcKeyHash] = big.NewInt(190) + set.updatedExpirationEntries[usdcKeyHash] = [2]uint32{120, 170} + + usdcKeyHash1 := getKeyHashForBalance(t, usdcID, [32]byte{1}) + set.updatedExpirationEntries[usdcKeyHash1] = [2]uint32{149, 190} + + usdcKeyHash2 := getKeyHashForBalance(t, usdcID, [32]byte{2}) + set.updatedExpirationEntries[usdcKeyHash2] = [2]uint32{100, 200} + + usdcKeyHash3 := getKeyHashForBalance(t, usdcID, [32]byte{3}) + set.updatedExpirationEntries[usdcKeyHash3] = [2]uint32{150, 210} + + usdcKeyHash4 := getKeyHashForBalance(t, usdcID, [32]byte{4}) + set.updatedExpirationEntries[usdcKeyHash4] = [2]uint32{170, 900} + + usdcKeyHash5 := getKeyHashForBalance(t, usdcID, [32]byte{5}) + set.updatedExpirationEntries[usdcKeyHash5] = [2]uint32{120, 600} + + ctx := context.Background() + + mockQ.On("GetContractAssetBalances", ctx, mock.MatchedBy(func(keys []xdr.Hash) bool { + return assert.ElementsMatch(t, []xdr.Hash{usdcKeyHash2, usdcKeyHash5}, keys) + })). + Return([]history.ContractAssetBalance{ + { + KeyHash: usdcKeyHash2[:], + ContractID: usdcID[:], + Amount: "67", + ExpirationLedger: 100, + }, + { + KeyHash: usdcKeyHash5[:], + ContractID: usdcID[:], + Amount: "200", + ExpirationLedger: 120, + }, + }, nil).Once() + + assert.NoError(t, set.ingestRestoredBalances(ctx)) + assert.ElementsMatch(t, set.GetContractStats(), []history.ContractAssetStatRow{ + { + ContractID: usdcID[:], + Stat: history.ContractStat{ + ActiveBalance: "267", + ActiveHolders: 2, + ArchivedBalance: "-267", + ArchivedHolders: -2, + }, + }, + }) + + mockQ.AssertExpectations(t) +} + +func TestIngestExpiredBalances(t *testing.T) { + usdcIssuer := keypair.MustRandom().Address() + usdcAsset := xdr.MustNewCreditAsset("USDC", usdcIssuer) + usdcID, err := usdcAsset.ContractID("passphrase") + assert.NoError(t, err) + + etherIssuer := keypair.MustRandom().Address() + etherAsset := xdr.MustNewCreditAsset("ETHER", etherIssuer) + etherID, err := etherAsset.ContractID("passphrase") + assert.NoError(t, err) + + mockQ := &history.MockQAssetStats{} + set := NewContractAssetStatSet( + mockQ, + "passphrase", + map[xdr.Hash]uint32{}, + map[xdr.Hash]uint32{}, + map[xdr.Hash][2]uint32{}, + 150, + ) + + usdcKeyHash := getKeyHashForBalance(t, usdcID, [32]byte{}) + usdcKeyHash1 := getKeyHashForBalance(t, usdcID, [32]byte{1}) + ethKeyHash := getKeyHashForBalance(t, etherID, [32]byte{}) + ethKeyHash1 := getKeyHashForBalance(t, etherID, [32]byte{1}) + set.updatedExpirationEntries[ethKeyHash1] = [2]uint32{149, 180} + ctx := context.Background() + mockQ.On("GetContractAssetBalancesExpiringAt", ctx, set.currentLedger-1). + Return([]history.ContractAssetBalance{ + { + KeyHash: usdcKeyHash[:], + ContractID: usdcID[:], + Amount: "67", + ExpirationLedger: 149, + }, + { + KeyHash: usdcKeyHash1[:], + ContractID: usdcID[:], + Amount: "200", + ExpirationLedger: 149, + }, + { + KeyHash: ethKeyHash[:], + ContractID: etherID[:], + Amount: "8", + ExpirationLedger: 149, + }, + { + KeyHash: ethKeyHash1[:], + ContractID: etherID[:], + Amount: "67", + ExpirationLedger: 149, + }, + }, nil).Once() + + assert.NoError(t, set.ingestExpiredBalances(ctx)) + assert.ElementsMatch(t, set.GetContractStats(), []history.ContractAssetStatRow{ + { + ContractID: usdcID[:], + Stat: history.ContractStat{ + ActiveBalance: "-267", + ActiveHolders: -2, + ArchivedBalance: "267", + ArchivedHolders: 2, + }, + }, + { + ContractID: etherID[:], + Stat: history.ContractStat{ + ActiveBalance: "-8", + ActiveHolders: -1, + ArchivedBalance: "8", + ArchivedHolders: 1, + }, + }, + }) + mockQ.AssertExpectations(t) +} diff --git a/services/horizon/internal/ingest/processors/contract_data.go b/services/horizon/internal/ingest/processors/contract_data.go index bce4ac6b25..7b15bd9c5b 100644 --- a/services/horizon/internal/ingest/processors/contract_data.go +++ b/services/horizon/internal/ingest/processors/contract_data.go @@ -458,7 +458,10 @@ func BalanceToContractData(assetContractId, holderID [32]byte, amt uint64) xdr.L }) } -func balanceToContractData(assetContractId, holderID [32]byte, amt xdr.Int128Parts) xdr.LedgerEntryData { +// ContractBalanceLedgerKey constructs the ledger key corresponding to the +// asset balance of a contract holder written to contract storage by the +// Stellar Asset Contract. +func ContractBalanceLedgerKey(assetContractId, holderID [32]byte) xdr.LedgerKey { holder := xdr.Hash(holderID) scAddress := &xdr.ScAddress{ Type: xdr.ScAddressTypeScAddressTypeContract, @@ -468,6 +471,25 @@ func balanceToContractData(assetContractId, holderID [32]byte, amt xdr.Int128Par xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &balanceMetadataSym}, xdr.ScVal{Type: xdr.ScValTypeScvAddress, Address: scAddress}, } + var contractIDHash xdr.Hash = assetContractId + return xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.LedgerKeyContractData{ + Contract: xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + ContractId: &contractIDHash, + }, + Key: xdr.ScVal{ + Type: xdr.ScValTypeScvVec, + Vec: &keyVec, + }, + Durability: xdr.ContractDataDurabilityPersistent, + }, + } +} + +func balanceToContractData(assetContractId, holderID [32]byte, amt xdr.Int128Parts) xdr.LedgerEntryData { + ledgerKey := ContractBalanceLedgerKey(assetContractId, holderID) amountSym := xdr.ScSymbol("amount") authorizedSym := xdr.ScSymbol("authorized") @@ -506,19 +528,13 @@ func balanceToContractData(assetContractId, holderID [32]byte, amt xdr.Int128Par }, } - var contractIDHash xdr.Hash = assetContractId + contractData := ledgerKey.MustContractData() return xdr.LedgerEntryData{ Type: xdr.LedgerEntryTypeContractData, ContractData: &xdr.ContractDataEntry{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractIDHash, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvVec, - Vec: &keyVec, - }, - Durability: xdr.ContractDataDurabilityPersistent, + Contract: contractData.Contract, + Key: contractData.Key, + Durability: contractData.Durability, Val: xdr.ScVal{ Type: xdr.ScValTypeScvMap, Map: &dataMap, diff --git a/services/horizon/internal/ingest/verify.go b/services/horizon/internal/ingest/verify.go index c7d6c8f0d6..bf1ddbe5b5 100644 --- a/services/horizon/internal/ingest/verify.go +++ b/services/horizon/internal/ingest/verify.go @@ -1,6 +1,7 @@ package ingest import ( + "bytes" "context" "database/sql" "encoding/hex" @@ -29,7 +30,7 @@ const assetStatsBatchSize = 500 // check them. // There is a test that checks it, to fix it: update the actual `verifyState` // method instead of just updating this value! -const stateVerifierExpectedIngestionVersion = 17 +const stateVerifierExpectedIngestionVersion = 18 // verifyState is called as a go routine from pipeline post hook every 64 // ledgers. It checks if the state is correct. If another go routine is already @@ -180,7 +181,9 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { return false, entry }) - assetStats := processors.NewAssetStatSet(s.config.NetworkPassphrase) + assetStats := processors.NewAssetStatSet() + createdExpirationEntries := map[xdr.Hash]uint32{} + var contractDataEntries []xdr.LedgerEntry total := int64(0) for { var entries []xdr.LedgerEntry @@ -234,21 +237,18 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { if err = verifier.Write(entry); err != nil { return err } - err = assetStats.AddContractData(ingest.Change{ - Type: xdr.LedgerEntryTypeContractData, - Post: &entry, - }) - if err != nil { - return errors.Wrap(err, "Error running assetStats.AddContractData") - } + contractDataEntries = append(contractDataEntries, entry) totalByType["contract_data"]++ case xdr.LedgerEntryTypeTtl: - // we don't store ttl entries in the db, - // so there is nothing to verify in that case. + // we don't store all expiration entries in the db, + // we will only verify expiration of contract balances in the horizon db. if err = verifier.Write(entry); err != nil { return err } totalByType["ttl"]++ + ttl := entry.Data.MustTtl() + createdExpirationEntries[ttl.KeyHash] = uint32(ttl.LiveUntilLedgerSeq) + totalByType["expiration"]++ default: return errors.New("GetLedgerEntries return unexpected type") } @@ -288,6 +288,24 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { localLog.WithField("total", total).Info("Batch added to StateVerifier") } + contractAssetStatSet := processors.NewContractAssetStatSet( + historyQ, + s.config.NetworkPassphrase, + map[xdr.Hash]uint32{}, + createdExpirationEntries, + map[xdr.Hash][2]uint32{}, + ledgerSequence, + ) + for i := range contractDataEntries { + entry := contractDataEntries[i] + if err = contractAssetStatSet.AddContractData(ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeContractData, + Post: &entry, + }); err != nil { + return errors.Wrap(err, "Error ingesting contract data") + } + } + localLog.WithField("total", total).Info("Finished writing to StateVerifier") countAccounts, err := historyQ.CountAccounts(ctx) @@ -328,7 +346,7 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { return errors.Wrap(err, "verifier.Verify failed") } - err = checkAssetStats(ctx, assetStats, historyQ) + err = checkAssetStats(ctx, assetStats, contractAssetStatSet, historyQ, s.config.NetworkPassphrase) if err != nil { return errors.Wrap(err, "checkAssetStats failed") } @@ -338,16 +356,26 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { return nil } -func checkAssetStats(ctx context.Context, set processors.AssetStatSet, q history.IngestionQ) error { +func checkAssetStats( + ctx context.Context, + set processors.AssetStatSet, + contractAssetStatSet *processors.ContractAssetStatSet, + q history.IngestionQ, + networkPassphrase string, +) error { page := db2.PageQuery{ Order: "asc", Limit: assetStatsBatchSize, } - assetStats, err := set.AllFromSnapshot() + contractToAsset := contractAssetStatSet.GetAssetToContractMap() + assetStats := set.All() + var err error + assetStats, err = processors.IncludeContractIDsInAssetStats(networkPassphrase, assetStats, contractToAsset) if err != nil { - return errors.Wrap(err, "could not fetch asset stats from asset stat set") + return err } + all := map[string]history.ExpAssetStat{} for _, assetStat := range assetStats { // no need to handle the native asset because asset stats only @@ -355,6 +383,24 @@ func checkAssetStats(ctx context.Context, set processors.AssetStatSet, q history all[assetStat.AssetCode+":"+assetStat.AssetIssuer] = assetStat } + contractToStats := map[xdr.Hash]history.ContractAssetStatRow{} + for _, row := range contractAssetStatSet.GetContractStats() { + var contractID xdr.Hash + copy(contractID[:], row.ContractID) + contractToStats[contractID] = row + } + + // only check contract asset balances which belong to stellar asset contracts + // because other balances may be forged. + var filteredBalances []history.ContractAssetBalance + for _, balance := range contractAssetStatSet.GetCreatedBalances() { + var contractID xdr.Hash + copy(contractID[:], balance.ContractID) + if _, ok := contractToAsset[contractID]; ok { + filteredBalances = append(filteredBalances, balance) + } + } + for { assetStats, err := q.GetAssetStats(ctx, "", "", page) if err != nil { @@ -377,7 +423,7 @@ func checkAssetStats(ctx context.Context, set processors.AssetStatSet, q history } delete(all, key) - if !fromSet.Equals(assetStat) { + if !fromSet.Equals(assetStat.ExpAssetStat) { return ingest.NewStateError( fmt.Errorf( "db asset stat with code %s issuer %s does not match asset stat from HAS: expected=%v actual=%v", @@ -385,6 +431,68 @@ func checkAssetStats(ctx context.Context, set processors.AssetStatSet, q history ), ) } + + if contractID, ok := assetStat.GetContractID(); ok { + asset := contractToAsset[contractID] + if asset == nil { + return ingest.NewStateError( + fmt.Errorf( + "asset %v has contract id %v in db but contract id is not in HAS", + key, + contractID, + ), + ) + } + + var assetType xdr.AssetType + var code, issuer string + if err := asset.Extract(&assetType, &code, &issuer); err != nil { + return ingest.NewStateError( + fmt.Errorf( + "could not parse asset %v", + asset, + ), + ) + } + + if assetType != assetStat.AssetType || + assetStat.AssetCode != code || + assetStat.AssetIssuer != issuer { + return ingest.NewStateError( + fmt.Errorf( + "contract id %v mapped to asset %v in db does not match HAS %v", + contractID, + key, + code+":"+issuer, + ), + ) + } + + entry, ok := contractToStats[contractID] + if !ok { + entry = history.ContractAssetStatRow{ + ContractID: contractID[:], + Stat: history.ContractStat{ + ActiveBalance: "0", + ActiveHolders: 0, + ArchivedBalance: "0", + ArchivedHolders: 0, + }, + } + } + if assetStat.Contracts != entry.Stat { + return ingest.NewStateError( + fmt.Errorf( + "contract stats for contract id %v in db %v does not match HAS %v", + contractID, + assetStat.Contracts, + entry.Stat, + ), + ) + } + + delete(contractToAsset, contractID) + } } page.Cursor = assetStats[len(assetStats)-1].PagingToken() @@ -398,6 +506,80 @@ func checkAssetStats(ctx context.Context, set processors.AssetStatSet, q history ), ) } + + if err := checkContractBalances(ctx, filteredBalances, q); err != nil { + return err + } + return nil +} + +func checkContractBalances( + ctx context.Context, + balances []history.ContractAssetBalance, + q history.IngestionQ, +) error { + for i := 0; i < len(balances); { + end := i + assetStatsBatchSize + if end > len(balances) { + end = len(balances) + } + + subset := balances[i:end] + var keys []xdr.Hash + byKey := map[xdr.Hash]history.ContractAssetBalance{} + for _, balance := range subset { + var key xdr.Hash + copy(key[:], balance.KeyHash) + keys = append(keys, key) + byKey[key] = balance + } + + rows, err := q.GetContractAssetBalances(ctx, keys) + if err != nil { + return err + } + + for _, row := range rows { + var key xdr.Hash + copy(key[:], row.KeyHash) + expected := byKey[key] + + if !bytes.Equal(row.ContractID, expected.ContractID) { + return ingest.NewStateError( + fmt.Errorf( + "contract balance %v has contract %v in HAS but is %v in db", + key, + expected.ContractID, + row.ContractID, + ), + ) + } + + if row.ExpirationLedger != expected.ExpirationLedger { + return ingest.NewStateError( + fmt.Errorf( + "contract balance %v has expiration %v in HAS but is %v in db", + key, + expected.ExpirationLedger, + row.ExpirationLedger, + ), + ) + } + + if row.Amount != expected.Amount { + return ingest.NewStateError( + fmt.Errorf( + "contract balance %v has amount %v in HAS but is %v in db", + key, + expected.Amount, + row.Amount, + ), + ) + } + } + + i = end + } return nil } diff --git a/services/horizon/internal/ingest/verify_range_state_test.go b/services/horizon/internal/ingest/verify_range_state_test.go index f71aeb11e7..7440f7dce0 100644 --- a/services/horizon/internal/ingest/verify_range_state_test.go +++ b/services/horizon/internal/ingest/verify_range_state_test.go @@ -12,6 +12,9 @@ import ( "github.com/guregu/null" "github.com/guregu/null/zero" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "github.com/stellar/go/ingest" "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/keypair" @@ -20,8 +23,6 @@ import ( "github.com/stellar/go/services/horizon/internal/ingest/processors" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" ) func TestVerifyRangeStateTestSuite(t *testing.T) { @@ -544,30 +545,32 @@ func (s *VerifyRangeStateTestSuite) TestSuccessWithVerify() { clonedQ.MockQAssetStats.On("GetAssetStats", s.ctx, "", "", db2.PageQuery{ Order: "asc", Limit: assetStatsBatchSize, - }).Return([]history.ExpAssetStat{ + }).Return([]history.AssetAndContractStat{ // Created by liquidity pool: { - AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, - AssetCode: "USD", - AssetIssuer: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - Accounts: history.ExpAssetStatAccounts{ - LiquidityPools: 1, - }, - Balances: history.ExpAssetStatBalances{ - Authorized: "0", - AuthorizedToMaintainLiabilities: "0", - ClaimableBalances: "0", - LiquidityPools: "450", - Unauthorized: "0", - Contracts: "0", + ExpAssetStat: history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetCode: "USD", + AssetIssuer: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + Accounts: history.ExpAssetStatAccounts{ + LiquidityPools: 1, + }, + Balances: history.ExpAssetStatBalances{ + Authorized: "0", + AuthorizedToMaintainLiabilities: "0", + ClaimableBalances: "0", + LiquidityPools: "450", + Unauthorized: "0", + }, + Amount: "0", }, - Amount: "0", - }}, nil).Once() + }, + }, nil).Once() clonedQ.MockQAssetStats.On("GetAssetStats", s.ctx, "", "", db2.PageQuery{ Cursor: "USD_GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML_credit_alphanum4", Order: "asc", Limit: assetStatsBatchSize, - }).Return([]history.ExpAssetStat{}, nil).Once() + }).Return([]history.AssetAndContractStat{}, nil).Once() clonedQ.MockQClaimableBalances.On("CountClaimableBalances", s.ctx).Return(1, nil).Once() clonedQ.MockQClaimableBalances. @@ -603,7 +606,15 @@ func (s *VerifyRangeStateTestSuite) TestSuccessWithVerify() { } func (s *VerifyRangeStateTestSuite) TestVerifyFailsWhenAssetStatsMismatch() { - set := processors.NewAssetStatSet(s.system.config.NetworkPassphrase) + set := processors.NewAssetStatSet() + contractAssetStatsSet := processors.NewContractAssetStatSet( + s.historyQ, + s.system.config.NetworkPassphrase, + map[xdr.Hash]uint32{}, + map[xdr.Hash]uint32{}, + map[xdr.Hash][2]uint32{}, + 100, + ) trustLineIssuer := xdr.MustAddress("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") set.AddTrustline( @@ -621,33 +632,35 @@ func (s *VerifyRangeStateTestSuite) TestVerifyFailsWhenAssetStatsMismatch() { }, ) - stat := history.ExpAssetStat{ - AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, - AssetCode: "EUR", - AssetIssuer: trustLineIssuer.Address(), - Accounts: history.ExpAssetStatAccounts{ - Unauthorized: 1, - }, - Balances: history.ExpAssetStatBalances{ - Authorized: "0", - AuthorizedToMaintainLiabilities: "0", - Unauthorized: "123", + stat := history.AssetAndContractStat{ + ExpAssetStat: history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetCode: "EUR", + AssetIssuer: trustLineIssuer.Address(), + Accounts: history.ExpAssetStatAccounts{ + Unauthorized: 1, + }, + Balances: history.ExpAssetStatBalances{ + Authorized: "0", + AuthorizedToMaintainLiabilities: "0", + Unauthorized: "123", + }, + Amount: "0", + NumAccounts: 0, }, - Amount: "0", - NumAccounts: 0, } s.historyQ.MockQAssetStats.On("GetAssetStats", s.ctx, "", "", db2.PageQuery{ Order: "asc", Limit: assetStatsBatchSize, - }).Return([]history.ExpAssetStat{stat}, nil).Once() + }).Return([]history.AssetAndContractStat{stat}, nil).Once() s.historyQ.MockQAssetStats.On("GetAssetStats", s.ctx, "", "", db2.PageQuery{ Cursor: stat.PagingToken(), Order: "asc", Limit: assetStatsBatchSize, - }).Return([]history.ExpAssetStat{}, nil).Once() + }).Return([]history.AssetAndContractStat{}, nil).Once() - err := checkAssetStats(s.ctx, set, s.historyQ) + err := checkAssetStats(s.ctx, set, contractAssetStatsSet, s.historyQ, s.system.config.NetworkPassphrase) s.Assert().Contains(err.Error(), fmt.Sprintf("db asset stat with code EUR issuer %s does not match asset stat from HAS", trustLineIssuer.Address())) // Satisfy the mock diff --git a/services/horizon/internal/ingest/verify_test.go b/services/horizon/internal/ingest/verify_test.go index dbff198de4..901f21a0ca 100644 --- a/services/horizon/internal/ingest/verify_test.go +++ b/services/horizon/internal/ingest/verify_test.go @@ -1,6 +1,7 @@ package ingest import ( + "crypto/sha256" "database/sql" "io" "math/rand" @@ -167,7 +168,6 @@ func genContractCode(tt *test.T, gen randxdr.Generator) xdr.LedgerEntryChange { []randxdr.Preset{ {randxdr.FieldEquals("type"), randxdr.SetU32(gxdr.LEDGER_ENTRY_CREATED.GetU32())}, {randxdr.FieldEquals("created.data.type"), randxdr.SetU32(gxdr.CONTRACT_CODE.GetU32())}, - //{randxdr.FieldEquals("created.data.contractcode.body.bodytype"), randxdr.SetU32(xdr.Body)}, }, ) tt.Assert.NoError(gxdr.Convert(shape, &change)) @@ -182,6 +182,8 @@ func genTTL(tt *test.T, gen randxdr.Generator) xdr.LedgerEntryChange { []randxdr.Preset{ {randxdr.FieldEquals("type"), randxdr.SetU32(gxdr.LEDGER_ENTRY_CREATED.GetU32())}, {randxdr.FieldEquals("created.data.type"), randxdr.SetU32(gxdr.TTL.GetU32())}, + {randxdr.FieldEquals("created.lastModifiedLedgerSeq"), randxdr.SetPositiveNum32}, + {randxdr.FieldEquals("created.data.ttl.liveUntilLedgerSeq"), randxdr.SetPositiveNum32}, }, ) tt.Assert.NoError(gxdr.Convert(shape, &change)) @@ -216,12 +218,16 @@ func genAssetContractMetadata(tt *test.T, gen randxdr.Generator) []xdr.LedgerEnt otherTrustline := genTrustLine(tt, gen, assetPreset) otherAssetContractMetadata := assetContractMetadataFromTrustline(tt, otherTrustline) + balance := balanceContractDataFromTrustline(tt, trustline) + otherBalance := balanceContractDataFromTrustline(tt, otherTrustline) return []xdr.LedgerEntryChange{ assetContractMetadata, trustline, - balanceContractDataFromTrustline(tt, trustline), + balance, + ttlForContractData(tt, gen, balance), otherAssetContractMetadata, - balanceContractDataFromTrustline(tt, otherTrustline), + otherBalance, + ttlForContractData(tt, gen, otherBalance), balanceContractDataFromTrustline(tt, genTrustLine(tt, gen, assetPreset)), } } @@ -265,6 +271,18 @@ func balanceContractDataFromTrustline(tt *test.T, trustline xdr.LedgerEntryChang return assetContractMetadata } +func ttlForContractData(tt *test.T, gen randxdr.Generator, contractData xdr.LedgerEntryChange) xdr.LedgerEntryChange { + ledgerEntry := contractData.MustCreated() + lk, err := ledgerEntry.LedgerKey() + tt.Assert.NoError(err) + bin, err := lk.MarshalBinary() + tt.Assert.NoError(err) + keyHash := sha256.Sum256(bin) + ttl := genTTL(tt, gen) + ttl.Created.Data.Ttl.KeyHash = keyHash + return ttl +} + func TestStateVerifierLockBusy(t *testing.T) { tt := test.Start(t) defer tt.Finish() @@ -330,7 +348,8 @@ func TestStateVerifier(t *testing.T) { tt.Assert.NoError(q.BeginTx(tt.Ctx, &sql.TxOptions{})) - checkpointLedger := uint32(63) + ledger := rand.Int31() + checkpointLedger := uint32(ledger - (ledger % 64) - 1) changeProcessor := buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, ledgerSource, checkpointLedger, "") mockChangeReader := &ingest.MockChangeReader{} diff --git a/services/horizon/internal/integration/extend_footprint_ttl_test.go b/services/horizon/internal/integration/extend_footprint_ttl_test.go index a9de3b5f69..b0c9258827 100644 --- a/services/horizon/internal/integration/extend_footprint_ttl_test.go +++ b/services/horizon/internal/integration/extend_footprint_ttl_test.go @@ -3,13 +3,13 @@ package integration import ( "testing" + "github.com/stretchr/testify/require" + "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/protocols/horizon/operations" "github.com/stellar/go/services/horizon/internal/test/integration" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" - - "github.com/stretchr/testify/require" ) func TestExtendFootprintTtl(t *testing.T) { diff --git a/services/horizon/internal/integration/sac_test.go b/services/horizon/internal/integration/sac_test.go index 06bca5a86d..68236ca3fe 100644 --- a/services/horizon/internal/integration/sac_test.go +++ b/services/horizon/internal/integration/sac_test.go @@ -41,8 +41,7 @@ func TestContractMintToAccount(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - // Create the contract - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(itest, issuer, asset)) + assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) recipientKp, recipient := itest.CreateAccount("100") itest.MustEstablishTrustline(recipientKp, recipient, txnbuild.MustAssetFromXDR(asset)) @@ -55,13 +54,15 @@ func TestContractMintToAccount(t *testing.T) { assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("20")) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 1, - balanceAccounts: amount.MustParse("20"), - numContracts: 0, - balanceContracts: big.NewInt(0), - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("20"), + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), }) fx := getTxEffects(itest, mintTx, asset) @@ -92,13 +93,15 @@ func TestContractMintToAccount(t *testing.T) { effects.EffectAccountCredited, effects.EffectAccountDebited) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 2, - balanceAccounts: amount.MustParse("50"), - numContracts: 0, - balanceContracts: big.NewInt(0), - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 2, + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + balanceAccounts: amount.MustParse("50"), + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), }) } @@ -116,8 +119,7 @@ func TestContractMintToContract(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - // Create the contract - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(itest, issuer, asset)) + assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) // Create recipient contract recipientContractID, _ := mustCreateAndInstallContract(itest, itest.Master(), "a1", add_u64_contract) @@ -171,13 +173,15 @@ func TestContractMintToContract(t *testing.T) { balanceContracts := new(big.Int).Lsh(big.NewInt(1), 127) balanceContracts.Sub(balanceContracts, big.NewInt(1)) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 0, - balanceAccounts: 0, - numContracts: 1, - balanceContracts: balanceContracts, - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 1, + balanceContracts: balanceContracts, + contractID: stellarAssetContractID(itest, asset), }) } @@ -195,8 +199,7 @@ func TestContractTransferBetweenAccounts(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - // Create the contract - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(itest, issuer, asset)) + assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) recipientKp, recipient := itest.CreateAccount("100") itest.MustEstablishTrustline(recipientKp, recipient, txnbuild.MustAssetFromXDR(asset)) @@ -217,13 +220,15 @@ func TestContractTransferBetweenAccounts(t *testing.T) { assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1000")) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 1, - balanceAccounts: amount.MustParse("1000"), - numContracts: 0, - balanceContracts: big.NewInt(0), - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("1000"), + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), }) otherRecipientKp, otherRecipient := itest.CreateAccount("100") @@ -242,13 +247,15 @@ func TestContractTransferBetweenAccounts(t *testing.T) { assert.NotEmpty(t, fx) assertContainsEffect(t, fx, effects.EffectAccountCredited, effects.EffectAccountDebited) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 2, - balanceAccounts: amount.MustParse("1000"), - numContracts: 0, - balanceContracts: big.NewInt(0), - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 2, + balanceAccounts: amount.MustParse("1000"), + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, transferTx, asset, recipientKp.Address(), otherRecipient.GetAccountID(), "transfer", "30.0000000") } @@ -267,8 +274,7 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { code := "USDLONG" asset := xdr.MustNewCreditAsset(code, issuer) - // Create the contract - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(itest, issuer, asset)) + assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) recipientKp, recipient := itest.CreateAccount("100") itest.MustEstablishTrustline(recipientKp, recipient, txnbuild.MustAssetFromXDR(asset)) @@ -310,13 +316,15 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { effects.EffectContractCredited) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 1, - balanceAccounts: amount.MustParse("1000"), - numContracts: 1, - balanceContracts: big.NewInt(int64(amount.MustParse("1000"))), - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("1000"), + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 1, + balanceContracts: big.NewInt(int64(amount.MustParse("1000"))), + contractID: stellarAssetContractID(itest, asset), }) // transfer from account to contract @@ -329,13 +337,15 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { assertContainsEffect(t, getTxEffects(itest, transferTx, asset), effects.EffectAccountDebited, effects.EffectContractCredited) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 1, - balanceAccounts: amount.MustParse("970"), - numContracts: 1, - balanceContracts: big.NewInt(int64(amount.MustParse("1030"))), - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("970"), + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 1, + balanceContracts: big.NewInt(int64(amount.MustParse("1030"))), + contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, transferTx, asset, recipientKp.Address(), strkeyRecipientContractID, "transfer", "30.0000000") @@ -349,13 +359,15 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { effects.EffectContractDebited, effects.EffectAccountCredited) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1470")) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 1, - balanceAccounts: amount.MustParse("1470"), - numContracts: 1, - balanceContracts: big.NewInt(int64(amount.MustParse("530"))), - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("1470"), + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 1, + balanceContracts: big.NewInt(int64(amount.MustParse("530"))), + contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, transferTx, asset, strkeyRecipientContractID, recipientKp.Address(), "transfer", "500.0000000") @@ -383,8 +395,7 @@ func TestContractTransferBetweenContracts(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - // Create the token contract - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(itest, issuer, asset)) + assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) // Create recipient contract recipientContractID, _ := mustCreateAndInstallContract(itest, itest.Master(), "a1", sac_contract) @@ -439,13 +450,15 @@ func TestContractTransferBetweenContracts(t *testing.T) { assert.Equal(itest.CurrentTest(), xdr.Int64(0), (*recipientBalanceAmount.I128).Hi) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 0, - balanceAccounts: 0, - numContracts: 2, - balanceContracts: big.NewInt(int64(amount.MustParse("1000"))), - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 2, + balanceContracts: big.NewInt(int64(amount.MustParse("1000"))), + contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, transferTx, asset, strkeyEmitterContractID, strkeyRecipientContractID, "transfer", "10.0000000") } @@ -464,8 +477,7 @@ func TestContractBurnFromAccount(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - // Create the contract - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(itest, issuer, asset)) + assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) recipientKp, recipient := itest.CreateAccount("100") itest.MustEstablishTrustline(recipientKp, recipient, txnbuild.MustAssetFromXDR(asset)) @@ -486,13 +498,15 @@ func TestContractBurnFromAccount(t *testing.T) { assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1000")) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 1, - balanceAccounts: amount.MustParse("1000"), - numContracts: 0, - balanceContracts: big.NewInt(0), - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("1000"), + numContracts: 0, + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), }) _, burnTx, _ := assertInvokeHostFnSucceeds( @@ -512,13 +526,15 @@ func TestContractBurnFromAccount(t *testing.T) { assert.Equal(t, "500.0000000", burnEffect.Amount) assert.Equal(t, recipientKp.Address(), burnEffect.Account) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 1, - balanceAccounts: amount.MustParse("500"), - numContracts: 0, - balanceContracts: big.NewInt(0), - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("500"), + numContracts: 0, + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, burnTx, asset, recipientKp.Address(), "", "burn", "500.0000000") } @@ -537,8 +553,7 @@ func TestContractBurnFromContract(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - // Create the contract - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(itest, issuer, asset)) + assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) // Create recipient contract recipientContractID, recipientContractHash := mustCreateAndInstallContract(itest, itest.Master(), "a1", sac_contract) @@ -579,13 +594,15 @@ func TestContractBurnFromContract(t *testing.T) { effects.EffectContractDebited) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 0, - balanceAccounts: 0, - numContracts: 1, - balanceContracts: big.NewInt(int64(amount.MustParse("990"))), - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 1, + balanceContracts: big.NewInt(int64(amount.MustParse("990"))), + contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, burnTx, asset, strkeyRecipientContractID, "", "burn", "10.0000000") } @@ -614,8 +631,7 @@ func TestContractClawbackFromAccount(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - // Create the contract - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(itest, issuer, asset)) + assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) recipientKp, recipient := itest.CreateAccount("100") itest.MustEstablishTrustline(recipientKp, recipient, txnbuild.MustAssetFromXDR(asset)) @@ -636,13 +652,15 @@ func TestContractClawbackFromAccount(t *testing.T) { assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1000")) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 1, - balanceAccounts: amount.MustParse("1000"), - numContracts: 0, - balanceContracts: big.NewInt(0), - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("1000"), + numContracts: 0, + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), }) _, clawTx, _ := assertInvokeHostFnSucceeds( @@ -654,13 +672,15 @@ func TestContractClawbackFromAccount(t *testing.T) { assertContainsEffect(t, getTxEffects(itest, clawTx, asset), effects.EffectAccountDebited) assertContainsBalance(itest, recipientKp, issuer, code, 0) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 1, - balanceAccounts: 0, - numContracts: 0, - balanceContracts: big.NewInt(0), - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: 0, + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, clawTx, asset, recipientKp.Address(), "", "clawback", "1000.0000000") } @@ -689,8 +709,7 @@ func TestContractClawbackFromContract(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - // Create the contract - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(itest, issuer, asset)) + assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) // Create recipient contract recipientContractID, _ := mustCreateAndInstallContract(itest, itest.Master(), "a2", sac_contract) @@ -724,13 +743,15 @@ func TestContractClawbackFromContract(t *testing.T) { effects.EffectContractDebited) assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 0, - balanceAccounts: 0, - numContracts: 1, - balanceContracts: big.NewInt(int64(amount.MustParse("990"))), - contractID: stellarAssetContractID(itest, asset), + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 1, + balanceContracts: big.NewInt(int64(amount.MustParse("990"))), + contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, clawTx, asset, strkeyRecipientContractID, "", "clawback", "10.0000000") } @@ -748,13 +769,15 @@ func assertContainsBalance(itest *integration.Test, acct *keypair.Full, issuer, } type assetStats struct { - code string - issuer string - numAccounts int32 - balanceAccounts xdr.Int64 - numContracts int32 - balanceContracts *big.Int - contractID [32]byte + code string + issuer string + numAccounts int32 + balanceAccounts xdr.Int64 + numContracts int32 + numArchivedContracts int32 + balanceContracts *big.Int + balanceArchivedContracts *big.Int + contractID [32]byte } func assertAssetStats(itest *integration.Test, expected assetStats) { @@ -779,12 +802,18 @@ func assertAssetStats(itest *integration.Test, expected assetStats) { assert.Equal(itest.CurrentTest(), expected.numAccounts, asset.Accounts.Authorized) assert.Equal(itest.CurrentTest(), expected.balanceAccounts, amount.MustParse(asset.Amount)) assert.Equal(itest.CurrentTest(), expected.numContracts, asset.NumContracts) - parts := strings.Split(asset.ContractsAmount, ".") + assert.Equal(itest.CurrentTest(), expected.numArchivedContracts, asset.NumArchivedContracts) + assert.Equal(itest.CurrentTest(), expected.balanceContracts.String(), parseBalance(itest, asset.ContractsAmount).String()) + assert.Equal(itest.CurrentTest(), expected.balanceArchivedContracts.String(), parseBalance(itest, asset.ArchivedContractsAmount).String()) + assert.Equal(itest.CurrentTest(), strkey.MustEncode(strkey.VersionByteContract, expected.contractID[:]), asset.ContractID) +} + +func parseBalance(itest *integration.Test, balance string) *big.Int { + parts := strings.Split(balance, ".") assert.Len(itest.CurrentTest(), parts, 2) contractsAmount, ok := new(big.Int).SetString(parts[0]+parts[1], 10) assert.True(itest.CurrentTest(), ok) - assert.Equal(itest.CurrentTest(), expected.balanceContracts.String(), contractsAmount.String()) - assert.Equal(itest.CurrentTest(), strkey.MustEncode(strkey.VersionByteContract, expected.contractID[:]), asset.ContractID) + return contractsAmount } // assertContainsEffect checks that the list of json effects contains the given @@ -884,7 +913,7 @@ func i128Param(hi int64, lo uint64) xdr.ScVal { } } -func createSAC(itest *integration.Test, sourceAccount string, asset xdr.Asset) *txnbuild.InvokeHostFunction { +func createSAC(sourceAccount string, asset xdr.Asset) *txnbuild.InvokeHostFunction { invokeHostFunction := &txnbuild.InvokeHostFunction{ HostFunction: xdr.HostFunction{ Type: xdr.HostFunctionTypeHostFunctionTypeCreateContract, diff --git a/services/horizon/internal/resourceadapter/asset_stat.go b/services/horizon/internal/resourceadapter/asset_stat.go index 89196e9c89..994cce99a0 100644 --- a/services/horizon/internal/resourceadapter/asset_stat.go +++ b/services/horizon/internal/resourceadapter/asset_stat.go @@ -18,7 +18,7 @@ import ( func PopulateAssetStat( ctx context.Context, res *protocol.AssetStat, - row history.ExpAssetStat, + row history.AssetAndContractStat, issuer history.AccountEntry, ) (err error) { if row.ContractID != nil { @@ -37,9 +37,10 @@ func PopulateAssetStat( } res.NumClaimableBalances = row.Accounts.ClaimableBalances res.NumLiquidityPools = row.Accounts.LiquidityPools - res.NumContracts = row.Accounts.Contracts + res.NumContracts = row.Contracts.ActiveHolders + res.NumArchivedContracts = row.Contracts.ArchivedHolders res.NumAccounts = row.NumAccounts - err = populateAssetStatBalances(res, row.Balances) + err = populateAssetStatBalances(res, row) if err != nil { return err } @@ -61,40 +62,45 @@ func PopulateAssetStat( return } -func populateAssetStatBalances(res *protocol.AssetStat, row history.ExpAssetStatBalances) (err error) { - res.Amount, err = amount.IntStringToAmount(row.Authorized) +func populateAssetStatBalances(res *protocol.AssetStat, row history.AssetAndContractStat) (err error) { + res.Amount, err = amount.IntStringToAmount(row.Balances.Authorized) if err != nil { return errors.Wrap(err, "Invalid amount in PopulateAssetStat") } - res.Balances.Authorized, err = amount.IntStringToAmount(row.Authorized) + res.Balances.Authorized, err = amount.IntStringToAmount(row.Balances.Authorized) if err != nil { - return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.Authorized) + return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.Balances.Authorized) } - res.Balances.AuthorizedToMaintainLiabilities, err = amount.IntStringToAmount(row.AuthorizedToMaintainLiabilities) + res.Balances.AuthorizedToMaintainLiabilities, err = amount.IntStringToAmount(row.Balances.AuthorizedToMaintainLiabilities) if err != nil { - return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.AuthorizedToMaintainLiabilities) + return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.Balances.AuthorizedToMaintainLiabilities) } - res.Balances.Unauthorized, err = amount.IntStringToAmount(row.Unauthorized) + res.Balances.Unauthorized, err = amount.IntStringToAmount(row.Balances.Unauthorized) if err != nil { - return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.Unauthorized) + return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.Balances.Unauthorized) } - res.ClaimableBalancesAmount, err = amount.IntStringToAmount(row.ClaimableBalances) + res.ClaimableBalancesAmount, err = amount.IntStringToAmount(row.Balances.ClaimableBalances) if err != nil { - return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.ClaimableBalances) + return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.Balances.ClaimableBalances) } - res.LiquidityPoolsAmount, err = amount.IntStringToAmount(row.LiquidityPools) + res.LiquidityPoolsAmount, err = amount.IntStringToAmount(row.Balances.LiquidityPools) if err != nil { - return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.LiquidityPools) + return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.Balances.LiquidityPools) } - res.ContractsAmount, err = amount.IntStringToAmount(row.Contracts) + res.ContractsAmount, err = amount.IntStringToAmount(row.Contracts.ActiveBalance) if err != nil { - return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.Contracts) + return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.Contracts.ActiveBalance) + } + + res.ArchivedContractsAmount, err = amount.IntStringToAmount(row.Contracts.ArchivedBalance) + if err != nil { + return errors.Wrapf(err, "Invalid amount in PopulateAssetStatBalances: %q", row.Contracts.ArchivedBalance) } return nil diff --git a/services/horizon/internal/resourceadapter/asset_stat_test.go b/services/horizon/internal/resourceadapter/asset_stat_test.go index b1530f88b6..d3b14d62e6 100644 --- a/services/horizon/internal/resourceadapter/asset_stat_test.go +++ b/services/horizon/internal/resourceadapter/asset_stat_test.go @@ -4,35 +4,42 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/stellar/go/protocols/horizon" protocol "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/xdr" - "github.com/stretchr/testify/assert" ) func TestPopulateExpAssetStat(t *testing.T) { - row := history.ExpAssetStat{ - AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, - AssetCode: "XIM", - AssetIssuer: "GBZ35ZJRIKJGYH5PBKLKOZ5L6EXCNTO7BKIL7DAVVDFQ2ODJEEHHJXIM", - Accounts: history.ExpAssetStatAccounts{ - Authorized: 429, - AuthorizedToMaintainLiabilities: 214, - Unauthorized: 107, - ClaimableBalances: 12, - Contracts: 6, + row := history.AssetAndContractStat{ + ExpAssetStat: history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetCode: "XIM", + AssetIssuer: "GBZ35ZJRIKJGYH5PBKLKOZ5L6EXCNTO7BKIL7DAVVDFQ2ODJEEHHJXIM", + Accounts: history.ExpAssetStatAccounts{ + Authorized: 429, + AuthorizedToMaintainLiabilities: 214, + Unauthorized: 107, + ClaimableBalances: 12, + }, + Balances: history.ExpAssetStatBalances{ + Authorized: "100000000000000000000", + AuthorizedToMaintainLiabilities: "50000000000000000000", + Unauthorized: "2500000000000000000", + ClaimableBalances: "1200000000000000000", + LiquidityPools: "7700000000000000000", + }, + Amount: "100000000000000000000", // 10T + NumAccounts: 429, }, - Balances: history.ExpAssetStatBalances{ - Authorized: "100000000000000000000", - AuthorizedToMaintainLiabilities: "50000000000000000000", - Unauthorized: "2500000000000000000", - ClaimableBalances: "1200000000000000000", - LiquidityPools: "7700000000000000000", - Contracts: "900000000000000000", + Contracts: history.ContractStat{ + ActiveBalance: "900000000000000000", + ActiveHolders: 6, + ArchivedBalance: "700000000000000000", + ArchivedHolders: 3, }, - Amount: "100000000000000000000", // 10T - NumAccounts: 429, } issuer := history.AccountEntry{ AccountID: "GBZ35ZJRIKJGYH5PBKLKOZ5L6EXCNTO7BKIL7DAVVDFQ2ODJEEHHJXIM", @@ -52,12 +59,14 @@ func TestPopulateExpAssetStat(t *testing.T) { assert.Equal(t, int32(107), res.Accounts.Unauthorized) assert.Equal(t, int32(12), res.NumClaimableBalances) assert.Equal(t, int32(6), res.NumContracts) + assert.Equal(t, int32(3), res.NumArchivedContracts) assert.Equal(t, "10000000000000.0000000", res.Balances.Authorized) assert.Equal(t, "5000000000000.0000000", res.Balances.AuthorizedToMaintainLiabilities) assert.Equal(t, "250000000000.0000000", res.Balances.Unauthorized) assert.Equal(t, "120000000000.0000000", res.ClaimableBalancesAmount) assert.Equal(t, "770000000000.0000000", res.LiquidityPoolsAmount) assert.Equal(t, "90000000000.0000000", res.ContractsAmount) + assert.Equal(t, "70000000000.0000000", res.ArchivedContractsAmount) assert.Equal(t, "10000000000000.0000000", res.Amount) assert.Equal(t, int32(429), res.NumAccounts) assert.Equal(t, horizon.AccountFlags{}, res.Flags) From 8580dca5fb2e3bd2b0b434ffe907c9295f24ab44 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Thu, 23 Nov 2023 17:32:57 +0100 Subject: [PATCH 012/234] xdr: Sanity-check allocations when decoding (#5116) --- Makefile | 2 +- go.mod | 2 +- go.sum | 4 +- gxdr/validator.go | 70 - gxdr/validator_test.go | 99 - .../internal/actions/submit_transaction.go | 8 +- xdr/main.go | 17 +- xdr/xdr_generated.go | 4158 ++++++++++------- 8 files changed, 2429 insertions(+), 1931 deletions(-) delete mode 100644 gxdr/validator.go delete mode 100644 gxdr/validator_test.go diff --git a/Makefile b/Makefile index 4266730900..7b0cda6df6 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ xdr/Stellar-contract.x \ xdr/Stellar-internal.x \ xdr/Stellar-contract-config-setting.x -XDRGEN_COMMIT=a231a92475ac6154c0c2f46dc503809823985060 +XDRGEN_COMMIT=f9995ef529eb83db6b70206a0a857dc88e33c750 XDR_COMMIT=6a620d160aab22609c982d54578ff6a63bfcdc01 .PHONY: xdr xdr-clean xdr-update diff --git a/go.mod b/go.mod index 8eefca4746..952e274f91 100644 --- a/go.mod +++ b/go.mod @@ -42,7 +42,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.17.0 - github.com/stellar/go-xdr v0.0.0-20230919160922-6c7b68458206 + github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 github.com/stellar/throttled v2.2.3-0.20190823235211-89d75816f59d+incompatible github.com/stretchr/testify v1.8.4 github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8 diff --git a/go.sum b/go.sum index e2ed069aa0..bb1175e120 100644 --- a/go.sum +++ b/go.sum @@ -386,8 +386,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= -github.com/stellar/go-xdr v0.0.0-20230919160922-6c7b68458206 h1:UFuvvpbWL8+jqO1QmKYWSVhiMp4MRiIFd8/zQlUINH0= -github.com/stellar/go-xdr v0.0.0-20230919160922-6c7b68458206/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps= +github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 h1:OzCVd0SV5qE3ZcDeSFCmOWLZfEWZ3Oe8KtmSOYKEVWE= +github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps= github.com/stellar/throttled v2.2.3-0.20190823235211-89d75816f59d+incompatible h1:jMXXAcz6xTarGDQ4VtVbtERogcmDQw4RaE85Cr9CgoQ= github.com/stellar/throttled v2.2.3-0.20190823235211-89d75816f59d+incompatible/go.mod h1:7CJ23pXirXBJq45DqvO6clzTEGM/l1SfKrgrzLry8b4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/gxdr/validator.go b/gxdr/validator.go deleted file mode 100644 index 9362ce3fdd..0000000000 --- a/gxdr/validator.go +++ /dev/null @@ -1,70 +0,0 @@ -package gxdr - -import ( - "encoding/base64" - "strings" - - goxdr "github.com/xdrpp/goxdr/xdr" -) - -const DefaultMaxDepth = 500 - -type depthLimiter struct { - depth int - maxDepth int - decoder *goxdr.XdrIn -} - -func (*depthLimiter) Sprintf(f string, args ...interface{}) string { - return "" -} - -func (d *depthLimiter) Marshal(field string, i goxdr.XdrType) { - switch t := goxdr.XdrBaseType(i).(type) { - case goxdr.XdrAggregate: - if d.depth > d.maxDepth { - goxdr.XdrPanic("max depth of %d exceeded", d.maxDepth) - } - d.depth++ - t.XdrRecurse(d, field) - d.depth-- - default: - d.decoder.Marshal(field, t) - } -} - -// ValidateTransactionEnvelope validates the given transaction envelope -// to make sure that it does not contain malicious arrays or nested -// structures which are too deep -func ValidateTransactionEnvelope(b64Envelope string, maxDepth int) error { - return validate(b64Envelope, &TransactionEnvelope{}, maxDepth) -} - -// ValidateLedgerKey validates the given ledger key -// to make sure that it does not contain malicious arrays or nested -// structures which are too deep -func ValidateLedgerKey(b64Key string, maxDepth int) error { - return validate(b64Key, &LedgerKey{}, maxDepth) -} - -func validate(b64 string, val goxdr.XdrType, maxDepth int) (err error) { - d := &depthLimiter{ - depth: 0, - maxDepth: maxDepth, - decoder: &goxdr.XdrIn{ - In: base64.NewDecoder(base64.StdEncoding, strings.NewReader(b64)), - }, - } - - defer func() { - switch i := recover().(type) { - case nil: - case goxdr.XdrError: - err = i - default: - panic(i) - } - }() - val.XdrMarshal(d, "") - return nil -} diff --git a/gxdr/validator_test.go b/gxdr/validator_test.go deleted file mode 100644 index 7e42e9467d..0000000000 --- a/gxdr/validator_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package gxdr - -import ( - "encoding/base64" - "testing" - - "github.com/stretchr/testify/assert" - goxdr "github.com/xdrpp/goxdr/xdr" -) - -func buildVec(depth int) SCVal { - if depth <= 0 { - symbol := SCSymbol("s") - return SCVal{ - Type: SCV_SYMBOL, - _u: &symbol, - } - } - vec := &SCVec{ - buildVec(depth - 1), - } - return SCVal{Type: SCV_VEC, _u: &vec} -} - -func buildMaliciousVec(t *testing.T) string { - vals := &SCVec{} - for i := 0; i < 0x0D; i++ { - symbol := SCSymbol("s") - *vals = append(*vals, SCVal{ - Type: SCV_SYMBOL, - _u: &symbol, - }) - } - vec := SCVal{Type: SCV_VEC, _u: &vals} - raw := Dump(&vec) - // raw[8-11] represents the part of the xdr that holds the - // length of the vector - for i, b := range raw { - if b == 0x0D { - assert.Equal(t, 11, i) - } - } - // here we override the most significant byte in the vector length - // so that the vector length in the xdr is 0xFA00000D which - // is equal to 4194304013 - raw[8] = 0xFA - return base64.StdEncoding.EncodeToString(raw) -} - -func TestValidator(t *testing.T) { - shallowVec := buildVec(2) - deepVec := buildVec(100) - for _, testCase := range []struct { - name string - input string - maxDepth int - val goxdr.XdrType - expectedError string - }{ - { - "invalid base 64 input", - "{}<>~!@$#", - 500, - &LedgerEntry{}, - "illegal base64 data at input byte 0", - }, - { - "valid depth", - base64.StdEncoding.EncodeToString(Dump(&shallowVec)), - 500, - &SCVal{}, - "", - }, - { - "invalid depth", - base64.StdEncoding.EncodeToString(Dump(&deepVec)), - 50, - &SCVal{}, - "max depth of 50 exceeded", - }, - { - "malicious length", - buildMaliciousVec(t), - 500, - &SCVal{}, - "EOF", - }, - } { - t.Run(testCase.name, func(t *testing.T) { - err := validate(testCase.input, testCase.val, testCase.maxDepth) - if testCase.expectedError == "" { - assert.NoError(t, err) - assert.Equal(t, testCase.input, base64.StdEncoding.EncodeToString(Dump(testCase.val))) - } else { - assert.EqualError(t, err, testCase.expectedError) - } - }) - } -} diff --git a/services/horizon/internal/actions/submit_transaction.go b/services/horizon/internal/actions/submit_transaction.go index e6a1f02b7f..4c8f2fd691 100644 --- a/services/horizon/internal/actions/submit_transaction.go +++ b/services/horizon/internal/actions/submit_transaction.go @@ -6,7 +6,6 @@ import ( "mime" "net/http" - "github.com/stellar/go/gxdr" "github.com/stellar/go/network" "github.com/stellar/go/protocols/horizon" hProblem "github.com/stellar/go/services/horizon/internal/render/problem" @@ -36,11 +35,8 @@ type envelopeInfo struct { parsed xdr.TransactionEnvelope } -func extractEnvelopeInfo(raw string, passphrase string) (envelopeInfo, error) { +func (handler SubmitTransactionHandler) extractEnvelopeInfo(raw string, passphrase string) (envelopeInfo, error) { result := envelopeInfo{raw: raw} - if err := gxdr.ValidateTransactionEnvelope(raw, gxdr.DefaultMaxDepth); err != nil { - return result, err - } err := xdr.SafeUnmarshalBase64(raw, &result.parsed) if err != nil { return result, err @@ -149,7 +145,7 @@ func (handler SubmitTransactionHandler) GetResource(w HeaderWriter, r *http.Requ return nil, err } - info, err := extractEnvelopeInfo(raw, handler.NetworkPassphrase) + info, err := handler.extractEnvelopeInfo(raw, handler.NetworkPassphrase) if err != nil { return nil, &problem.P{ Type: "transaction_malformed", diff --git a/xdr/main.go b/xdr/main.go index b0c31ad5d8..44e8ced3ea 100644 --- a/xdr/main.go +++ b/xdr/main.go @@ -36,11 +36,11 @@ var OperationTypeToStringMap = operationTypeMap var LedgerEntryTypeMap = ledgerEntryTypeMap -func safeUnmarshalString(decoder func(reader io.Reader) io.Reader, data string, dest interface{}) error { +func safeUnmarshalString(decoder func(reader io.Reader) io.Reader, options xdr.DecodeOptions, data string, dest interface{}) error { count := &countWriter{} l := len(data) - _, err := Unmarshal(decoder(io.TeeReader(strings.NewReader(data), count)), dest) + _, err := UnmarshalWithOptions(decoder(io.TeeReader(strings.NewReader(data), count)), dest, options) if err != nil { return err } @@ -52,14 +52,23 @@ func safeUnmarshalString(decoder func(reader io.Reader) io.Reader, data string, return nil } +func decodeOptionsWithMaxInputLen(maxInputLen int) xdr.DecodeOptions { + options := xdr.DefaultDecodeOptions + options.MaxInputLen = maxInputLen + return options +} + // SafeUnmarshalBase64 first decodes the provided reader from base64 before // decoding the xdr into the provided destination. Also ensures that the reader // is fully consumed. func SafeUnmarshalBase64(data string, dest interface{}) error { + decodedLen := base64.StdEncoding.DecodedLen(len(data)) + options := decodeOptionsWithMaxInputLen(decodedLen) return safeUnmarshalString( func(r io.Reader) io.Reader { return base64.NewDecoder(base64.StdEncoding, r) }, + options, data, dest, ) @@ -69,7 +78,9 @@ func SafeUnmarshalBase64(data string, dest interface{}) error { // decoding the xdr into the provided destination. Also ensures that the reader // is fully consumed. func SafeUnmarshalHex(data string, dest interface{}) error { - return safeUnmarshalString(hex.NewDecoder, data, dest) + decodedLen := hex.DecodedLen(len(data)) + options := decodeOptionsWithMaxInputLen(decodedLen) + return safeUnmarshalString(hex.NewDecoder, options, data, dest) } // SafeUnmarshal decodes the provided reader into the destination and verifies diff --git a/xdr/xdr_generated.go b/xdr/xdr_generated.go index f4f59553cc..d4d6b12107 100644 --- a/xdr/xdr_generated.go +++ b/xdr/xdr_generated.go @@ -25,10 +25,14 @@ import ( "errors" "fmt" "io" + "unsafe" "github.com/stellar/go-xdr/xdr3" ) +// Needed since unsafe is not used in all cases +var _ = unsafe.Sizeof(0) + // XdrFilesSHA256 is the SHA256 hashes of source files. var XdrFilesSHA256 = map[string]string{ "xdr/Stellar-SCP.x": "8f32b04d008f8bc33b8843d075e69837231a673691ee41d8b821ca229a6e802a", @@ -57,12 +61,17 @@ type decoderFrom interface { // Unmarshal reads an xdr element from `r` into `v`. func Unmarshal(r io.Reader, v interface{}) (int, error) { + return UnmarshalWithOptions(r, v, xdr.DefaultDecodeOptions) +} + +// UnmarshalWithOptions works like Unmarshal but uses decoding options. +func UnmarshalWithOptions(r io.Reader, v interface{}, options xdr.DecodeOptions) (int, error) { if decodable, ok := v.(decoderFrom); ok { - d := xdr.NewDecoder(r) - return decodable.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + d := xdr.NewDecoderWithOptions(r, options) + return decodable.DecodeFrom(d, options.MaxDepth) } // delegate to xdr package's Unmarshal - return xdr.Unmarshal(r, v) + return xdr.UnmarshalWithOptions(r, v, options) } // Marshal writes an xdr element `v` into `w`. @@ -123,8 +132,10 @@ func (s Value) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Value) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -133,8 +144,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Value)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Value) xdrType() {} var _ xdrType = (*Value)(nil) @@ -197,8 +207,10 @@ func (s ScpBallot) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScpBallot) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -207,8 +219,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScpBallot)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScpBallot) xdrType() {} var _ xdrType = (*ScpBallot)(nil) @@ -290,8 +301,10 @@ func (s ScpStatementType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScpStatementType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -300,8 +313,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScpStatementType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScpStatementType) xdrType() {} var _ xdrType = (*ScpStatementType)(nil) @@ -368,8 +380,11 @@ func (s *ScpNomination) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { } s.Votes = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding Value: length (%d) exceeds remaining input length (%d)", l, il) + } s.Votes = make([]Value, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Votes[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -384,8 +399,11 @@ func (s *ScpNomination) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { } s.Accepted = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding Value: length (%d) exceeds remaining input length (%d)", l, il) + } s.Accepted = make([]Value, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Accepted[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -407,8 +425,10 @@ func (s ScpNomination) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScpNomination) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -417,8 +437,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScpNomination)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScpNomination) xdrType() {} var _ xdrType = (*ScpNomination)(nil) @@ -550,8 +569,10 @@ func (s ScpStatementPrepare) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScpStatementPrepare) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -560,8 +581,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScpStatementPrepare)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScpStatementPrepare) xdrType() {} var _ xdrType = (*ScpStatementPrepare)(nil) @@ -654,8 +674,10 @@ func (s ScpStatementConfirm) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScpStatementConfirm) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -664,8 +686,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScpStatementConfirm)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScpStatementConfirm) xdrType() {} var _ xdrType = (*ScpStatementConfirm)(nil) @@ -738,8 +759,10 @@ func (s ScpStatementExternalize) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScpStatementExternalize) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -748,8 +771,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScpStatementExternalize)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScpStatementExternalize) xdrType() {} var _ xdrType = (*ScpStatementExternalize)(nil) @@ -1047,8 +1069,10 @@ func (s ScpStatementPledges) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScpStatementPledges) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -1057,8 +1081,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScpStatementPledges)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScpStatementPledges) xdrType() {} var _ xdrType = (*ScpStatementPledges)(nil) @@ -1163,8 +1186,10 @@ func (s ScpStatement) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScpStatement) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -1173,8 +1198,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScpStatement)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScpStatement) xdrType() {} var _ xdrType = (*ScpStatement)(nil) @@ -1237,8 +1261,10 @@ func (s ScpEnvelope) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScpEnvelope) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -1247,8 +1273,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScpEnvelope)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScpEnvelope) xdrType() {} var _ xdrType = (*ScpEnvelope)(nil) @@ -1315,8 +1340,11 @@ func (s *ScpQuorumSet) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { } s.Validators = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding NodeId: length (%d) exceeds remaining input length (%d)", l, il) + } s.Validators = make([]NodeId, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Validators[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -1331,8 +1359,11 @@ func (s *ScpQuorumSet) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { } s.InnerSets = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScpQuorumSet: length (%d) exceeds remaining input length (%d)", l, il) + } s.InnerSets = make([]ScpQuorumSet, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.InnerSets[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -1354,8 +1385,10 @@ func (s ScpQuorumSet) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScpQuorumSet) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -1364,8 +1397,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScpQuorumSet)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScpQuorumSet) xdrType() {} var _ xdrType = (*ScpQuorumSet)(nil) @@ -1418,8 +1450,10 @@ func (s Thresholds) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Thresholds) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -1428,8 +1462,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Thresholds)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Thresholds) xdrType() {} var _ xdrType = (*Thresholds)(nil) @@ -1484,8 +1517,10 @@ func (s String32) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *String32) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -1494,8 +1529,7 @@ var ( _ encoding.BinaryUnmarshaler = (*String32)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s String32) xdrType() {} var _ xdrType = (*String32)(nil) @@ -1550,8 +1584,10 @@ func (s String64) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *String64) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -1560,8 +1596,7 @@ var ( _ encoding.BinaryUnmarshaler = (*String64)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s String64) xdrType() {} var _ xdrType = (*String64)(nil) @@ -1609,8 +1644,10 @@ func (s SequenceNumber) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SequenceNumber) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -1619,8 +1656,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SequenceNumber)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SequenceNumber) xdrType() {} var _ xdrType = (*SequenceNumber)(nil) @@ -1673,8 +1709,10 @@ func (s DataValue) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *DataValue) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -1683,8 +1721,7 @@ var ( _ encoding.BinaryUnmarshaler = (*DataValue)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s DataValue) xdrType() {} var _ xdrType = (*DataValue)(nil) @@ -1732,8 +1769,10 @@ func (s PoolId) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PoolId) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -1742,8 +1781,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PoolId)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PoolId) xdrType() {} var _ xdrType = (*PoolId)(nil) @@ -1796,8 +1834,10 @@ func (s AssetCode4) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AssetCode4) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -1806,8 +1846,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AssetCode4)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AssetCode4) xdrType() {} var _ xdrType = (*AssetCode4)(nil) @@ -1860,8 +1899,10 @@ func (s AssetCode12) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AssetCode12) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -1870,8 +1911,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AssetCode12)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AssetCode12) xdrType() {} var _ xdrType = (*AssetCode12)(nil) @@ -1953,8 +1993,10 @@ func (s AssetType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AssetType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -1963,8 +2005,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AssetType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AssetType) xdrType() {} var _ xdrType = (*AssetType)(nil) @@ -2145,8 +2186,10 @@ func (s AssetCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AssetCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -2155,8 +2198,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AssetCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AssetCode) xdrType() {} var _ xdrType = (*AssetCode)(nil) @@ -2219,8 +2261,10 @@ func (s AlphaNum4) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AlphaNum4) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -2229,8 +2273,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AlphaNum4)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AlphaNum4) xdrType() {} var _ xdrType = (*AlphaNum4)(nil) @@ -2293,8 +2336,10 @@ func (s AlphaNum12) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AlphaNum12) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -2303,8 +2348,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AlphaNum12)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AlphaNum12) xdrType() {} var _ xdrType = (*AlphaNum12)(nil) @@ -2498,8 +2542,10 @@ func (s Asset) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Asset) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -2508,8 +2554,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Asset)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Asset) xdrType() {} var _ xdrType = (*Asset)(nil) @@ -2572,8 +2617,10 @@ func (s Price) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Price) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -2582,8 +2629,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Price)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Price) xdrType() {} var _ xdrType = (*Price)(nil) @@ -2646,8 +2692,10 @@ func (s Liabilities) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Liabilities) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -2656,8 +2704,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Liabilities)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Liabilities) xdrType() {} var _ xdrType = (*Liabilities)(nil) @@ -2739,8 +2786,10 @@ func (s ThresholdIndexes) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ThresholdIndexes) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -2749,8 +2798,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ThresholdIndexes)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ThresholdIndexes) xdrType() {} var _ xdrType = (*ThresholdIndexes)(nil) @@ -2850,8 +2898,10 @@ func (s LedgerEntryType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerEntryType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -2860,8 +2910,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerEntryType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerEntryType) xdrType() {} var _ xdrType = (*LedgerEntryType)(nil) @@ -2924,8 +2973,10 @@ func (s Signer) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Signer) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -2934,8 +2985,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Signer)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Signer) xdrType() {} var _ xdrType = (*Signer)(nil) @@ -3027,8 +3077,10 @@ func (s AccountFlags) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AccountFlags) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -3037,8 +3089,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AccountFlags)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AccountFlags) xdrType() {} var _ xdrType = (*AccountFlags)(nil) @@ -3137,8 +3188,10 @@ func (s AccountEntryExtensionV3) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AccountEntryExtensionV3) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -3147,8 +3200,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AccountEntryExtensionV3)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AccountEntryExtensionV3) xdrType() {} var _ xdrType = (*AccountEntryExtensionV3)(nil) @@ -3288,8 +3340,10 @@ func (s AccountEntryExtensionV2Ext) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AccountEntryExtensionV2Ext) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -3298,8 +3352,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AccountEntryExtensionV2Ext)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AccountEntryExtensionV2Ext) xdrType() {} var _ xdrType = (*AccountEntryExtensionV2Ext)(nil) @@ -3387,8 +3440,11 @@ func (s *AccountEntryExtensionV2) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int } s.SignerSponsoringIDs = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding SponsorshipDescriptor: length (%d) exceeds remaining input length (%d)", l, il) + } s.SignerSponsoringIDs = make([]SponsorshipDescriptor, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { var eb bool eb, nTmp, err = d.DecodeBool() n += nTmp @@ -3425,8 +3481,10 @@ func (s AccountEntryExtensionV2) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AccountEntryExtensionV2) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -3435,8 +3493,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AccountEntryExtensionV2)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AccountEntryExtensionV2) xdrType() {} var _ xdrType = (*AccountEntryExtensionV2)(nil) @@ -3576,8 +3633,10 @@ func (s AccountEntryExtensionV1Ext) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AccountEntryExtensionV1Ext) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -3586,8 +3645,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AccountEntryExtensionV1Ext)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AccountEntryExtensionV1Ext) xdrType() {} var _ xdrType = (*AccountEntryExtensionV1Ext)(nil) @@ -3658,8 +3716,10 @@ func (s AccountEntryExtensionV1) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AccountEntryExtensionV1) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -3668,8 +3728,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AccountEntryExtensionV1)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AccountEntryExtensionV1) xdrType() {} var _ xdrType = (*AccountEntryExtensionV1)(nil) @@ -3809,8 +3868,10 @@ func (s AccountEntryExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AccountEntryExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -3819,8 +3880,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AccountEntryExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AccountEntryExt) xdrType() {} var _ xdrType = (*AccountEntryExt)(nil) @@ -3985,8 +4045,11 @@ func (s *AccountEntry) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { } s.Signers = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding Signer: length (%d) exceeds remaining input length (%d)", l, il) + } s.Signers = make([]Signer, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Signers[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -4013,8 +4076,10 @@ func (s AccountEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AccountEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -4023,8 +4088,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AccountEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AccountEntry) xdrType() {} var _ xdrType = (*AccountEntry)(nil) @@ -4108,8 +4172,10 @@ func (s TrustLineFlags) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TrustLineFlags) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -4118,8 +4184,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TrustLineFlags)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TrustLineFlags) xdrType() {} var _ xdrType = (*TrustLineFlags)(nil) @@ -4207,8 +4272,10 @@ func (s LiquidityPoolType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LiquidityPoolType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -4217,8 +4284,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LiquidityPoolType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LiquidityPoolType) xdrType() {} var _ xdrType = (*LiquidityPoolType)(nil) @@ -4463,8 +4529,10 @@ func (s TrustLineAsset) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TrustLineAsset) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -4473,8 +4541,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TrustLineAsset)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TrustLineAsset) xdrType() {} var _ xdrType = (*TrustLineAsset)(nil) @@ -4564,8 +4631,10 @@ func (s TrustLineEntryExtensionV2Ext) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TrustLineEntryExtensionV2Ext) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -4574,8 +4643,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TrustLineEntryExtensionV2Ext)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TrustLineEntryExtensionV2Ext) xdrType() {} var _ xdrType = (*TrustLineEntryExtensionV2Ext)(nil) @@ -4644,8 +4712,10 @@ func (s TrustLineEntryExtensionV2) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TrustLineEntryExtensionV2) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -4654,8 +4724,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TrustLineEntryExtensionV2)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TrustLineEntryExtensionV2) xdrType() {} var _ xdrType = (*TrustLineEntryExtensionV2)(nil) @@ -4795,8 +4864,10 @@ func (s TrustLineEntryV1Ext) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TrustLineEntryV1Ext) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -4805,8 +4876,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TrustLineEntryV1Ext)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TrustLineEntryV1Ext) xdrType() {} var _ xdrType = (*TrustLineEntryV1Ext)(nil) @@ -4877,8 +4947,10 @@ func (s TrustLineEntryV1) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TrustLineEntryV1) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -4887,8 +4959,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TrustLineEntryV1)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TrustLineEntryV1) xdrType() {} var _ xdrType = (*TrustLineEntryV1)(nil) @@ -5040,8 +5111,10 @@ func (s TrustLineEntryExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TrustLineEntryExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -5050,8 +5123,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TrustLineEntryExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TrustLineEntryExt) xdrType() {} var _ xdrType = (*TrustLineEntryExt)(nil) @@ -5177,8 +5249,10 @@ func (s TrustLineEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TrustLineEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -5187,8 +5261,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TrustLineEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TrustLineEntry) xdrType() {} var _ xdrType = (*TrustLineEntry)(nil) @@ -5263,8 +5336,10 @@ func (s OfferEntryFlags) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *OfferEntryFlags) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -5273,8 +5348,7 @@ var ( _ encoding.BinaryUnmarshaler = (*OfferEntryFlags)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s OfferEntryFlags) xdrType() {} var _ xdrType = (*OfferEntryFlags)(nil) @@ -5369,8 +5443,10 @@ func (s OfferEntryExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *OfferEntryExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -5379,8 +5455,7 @@ var ( _ encoding.BinaryUnmarshaler = (*OfferEntryExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s OfferEntryExt) xdrType() {} var _ xdrType = (*OfferEntryExt)(nil) @@ -5516,8 +5591,10 @@ func (s OfferEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *OfferEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -5526,8 +5603,7 @@ var ( _ encoding.BinaryUnmarshaler = (*OfferEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s OfferEntry) xdrType() {} var _ xdrType = (*OfferEntry)(nil) @@ -5617,8 +5693,10 @@ func (s DataEntryExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *DataEntryExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -5627,8 +5705,7 @@ var ( _ encoding.BinaryUnmarshaler = (*DataEntryExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s DataEntryExt) xdrType() {} var _ xdrType = (*DataEntryExt)(nil) @@ -5718,8 +5795,10 @@ func (s DataEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *DataEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -5728,8 +5807,7 @@ var ( _ encoding.BinaryUnmarshaler = (*DataEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s DataEntry) xdrType() {} var _ xdrType = (*DataEntry)(nil) @@ -5817,8 +5895,10 @@ func (s ClaimPredicateType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimPredicateType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -5827,8 +5907,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimPredicateType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimPredicateType) xdrType() {} var _ xdrType = (*ClaimPredicateType)(nil) @@ -6142,8 +6221,11 @@ func (u *ClaimPredicate) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } (*u.AndPredicates) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ClaimPredicate: length (%d) exceeds remaining input length (%d)", l, il) + } (*u.AndPredicates) = make([]ClaimPredicate, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*u.AndPredicates)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -6165,8 +6247,11 @@ func (u *ClaimPredicate) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } (*u.OrPredicates) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ClaimPredicate: length (%d) exceeds remaining input length (%d)", l, il) + } (*u.OrPredicates) = make([]ClaimPredicate, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*u.OrPredicates)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -6224,8 +6309,10 @@ func (s ClaimPredicate) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimPredicate) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -6234,8 +6321,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimPredicate)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimPredicate) xdrType() {} var _ xdrType = (*ClaimPredicate)(nil) @@ -6308,8 +6394,10 @@ func (s ClaimantType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimantType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -6318,8 +6406,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimantType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimantType) xdrType() {} var _ xdrType = (*ClaimantType)(nil) @@ -6382,8 +6469,10 @@ func (s ClaimantV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimantV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -6392,8 +6481,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimantV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimantV0) xdrType() {} var _ xdrType = (*ClaimantV0)(nil) @@ -6525,8 +6613,10 @@ func (s Claimant) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Claimant) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -6535,8 +6625,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Claimant)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Claimant) xdrType() {} var _ xdrType = (*Claimant)(nil) @@ -6609,8 +6698,10 @@ func (s ClaimableBalanceIdType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimableBalanceIdType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -6619,8 +6710,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimableBalanceIdType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimableBalanceIdType) xdrType() {} var _ xdrType = (*ClaimableBalanceIdType)(nil) @@ -6748,8 +6838,10 @@ func (s ClaimableBalanceId) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimableBalanceId) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -6758,8 +6850,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimableBalanceId)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimableBalanceId) xdrType() {} var _ xdrType = (*ClaimableBalanceId)(nil) @@ -6834,8 +6925,10 @@ func (s ClaimableBalanceFlags) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimableBalanceFlags) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -6844,8 +6937,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimableBalanceFlags)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimableBalanceFlags) xdrType() {} var _ xdrType = (*ClaimableBalanceFlags)(nil) @@ -6940,8 +7032,10 @@ func (s ClaimableBalanceEntryExtensionV1Ext) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimableBalanceEntryExtensionV1Ext) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -6950,8 +7044,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimableBalanceEntryExtensionV1Ext)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimableBalanceEntryExtensionV1Ext) xdrType() {} var _ xdrType = (*ClaimableBalanceEntryExtensionV1Ext)(nil) @@ -7020,8 +7113,10 @@ func (s ClaimableBalanceEntryExtensionV1) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimableBalanceEntryExtensionV1) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -7030,8 +7125,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimableBalanceEntryExtensionV1)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimableBalanceEntryExtensionV1) xdrType() {} var _ xdrType = (*ClaimableBalanceEntryExtensionV1)(nil) @@ -7171,8 +7265,10 @@ func (s ClaimableBalanceEntryExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimableBalanceEntryExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -7181,8 +7277,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimableBalanceEntryExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimableBalanceEntryExt) xdrType() {} var _ xdrType = (*ClaimableBalanceEntryExt)(nil) @@ -7273,8 +7368,11 @@ func (s *ClaimableBalanceEntry) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, } s.Claimants = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding Claimant: length (%d) exceeds remaining input length (%d)", l, il) + } s.Claimants = make([]Claimant, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Claimants[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -7311,8 +7409,10 @@ func (s ClaimableBalanceEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimableBalanceEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -7321,8 +7421,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimableBalanceEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimableBalanceEntry) xdrType() {} var _ xdrType = (*ClaimableBalanceEntry)(nil) @@ -7395,8 +7494,10 @@ func (s LiquidityPoolConstantProductParameters) MarshalBinary() ([]byte, error) // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LiquidityPoolConstantProductParameters) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -7405,8 +7506,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LiquidityPoolConstantProductParameters)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LiquidityPoolConstantProductParameters) xdrType() {} var _ xdrType = (*LiquidityPoolConstantProductParameters)(nil) @@ -7501,8 +7601,10 @@ func (s LiquidityPoolEntryConstantProduct) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LiquidityPoolEntryConstantProduct) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -7511,8 +7613,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LiquidityPoolEntryConstantProduct)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LiquidityPoolEntryConstantProduct) xdrType() {} var _ xdrType = (*LiquidityPoolEntryConstantProduct)(nil) @@ -7649,8 +7750,10 @@ func (s LiquidityPoolEntryBody) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LiquidityPoolEntryBody) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -7659,8 +7762,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LiquidityPoolEntryBody)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LiquidityPoolEntryBody) xdrType() {} var _ xdrType = (*LiquidityPoolEntryBody)(nil) @@ -7738,8 +7840,10 @@ func (s LiquidityPoolEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LiquidityPoolEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -7748,8 +7852,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LiquidityPoolEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LiquidityPoolEntry) xdrType() {} var _ xdrType = (*LiquidityPoolEntry)(nil) @@ -7824,8 +7927,10 @@ func (s ContractDataDurability) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractDataDurability) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -7834,8 +7939,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractDataDurability)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractDataDurability) xdrType() {} var _ xdrType = (*ContractDataDurability)(nil) @@ -7928,8 +8032,10 @@ func (s ContractDataEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractDataEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -7938,8 +8044,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractDataEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractDataEntry) xdrType() {} var _ xdrType = (*ContractDataEntry)(nil) @@ -8012,8 +8117,10 @@ func (s ContractCodeEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractCodeEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -8022,8 +8129,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractCodeEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractCodeEntry) xdrType() {} var _ xdrType = (*ContractCodeEntry)(nil) @@ -8086,8 +8192,10 @@ func (s TtlEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TtlEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -8096,8 +8204,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TtlEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TtlEntry) xdrType() {} var _ xdrType = (*TtlEntry)(nil) @@ -8187,8 +8294,10 @@ func (s LedgerEntryExtensionV1Ext) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerEntryExtensionV1Ext) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -8197,8 +8306,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerEntryExtensionV1Ext)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerEntryExtensionV1Ext) xdrType() {} var _ xdrType = (*LedgerEntryExtensionV1Ext)(nil) @@ -8282,8 +8390,10 @@ func (s LedgerEntryExtensionV1) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerEntryExtensionV1) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -8292,8 +8402,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerEntryExtensionV1)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerEntryExtensionV1) xdrType() {} var _ xdrType = (*LedgerEntryExtensionV1)(nil) @@ -8871,8 +8980,10 @@ func (s LedgerEntryData) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerEntryData) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -8881,8 +8992,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerEntryData)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerEntryData) xdrType() {} var _ xdrType = (*LedgerEntryData)(nil) @@ -9022,8 +9132,10 @@ func (s LedgerEntryExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerEntryExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -9032,8 +9144,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerEntryExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerEntryExt) xdrType() {} var _ xdrType = (*LedgerEntryExt)(nil) @@ -9139,8 +9250,10 @@ func (s LedgerEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -9149,8 +9262,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerEntry) xdrType() {} var _ xdrType = (*LedgerEntry)(nil) @@ -9203,8 +9315,10 @@ func (s LedgerKeyAccount) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerKeyAccount) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -9213,8 +9327,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerKeyAccount)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerKeyAccount) xdrType() {} var _ xdrType = (*LedgerKeyAccount)(nil) @@ -9277,8 +9390,10 @@ func (s LedgerKeyTrustLine) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerKeyTrustLine) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -9287,8 +9402,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerKeyTrustLine)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerKeyTrustLine) xdrType() {} var _ xdrType = (*LedgerKeyTrustLine)(nil) @@ -9351,8 +9465,10 @@ func (s LedgerKeyOffer) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerKeyOffer) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -9361,8 +9477,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerKeyOffer)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerKeyOffer) xdrType() {} var _ xdrType = (*LedgerKeyOffer)(nil) @@ -9425,8 +9540,10 @@ func (s LedgerKeyData) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerKeyData) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -9435,8 +9552,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerKeyData)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerKeyData) xdrType() {} var _ xdrType = (*LedgerKeyData)(nil) @@ -9489,8 +9605,10 @@ func (s LedgerKeyClaimableBalance) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerKeyClaimableBalance) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -9499,8 +9617,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerKeyClaimableBalance)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerKeyClaimableBalance) xdrType() {} var _ xdrType = (*LedgerKeyClaimableBalance)(nil) @@ -9553,8 +9670,10 @@ func (s LedgerKeyLiquidityPool) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerKeyLiquidityPool) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -9563,8 +9682,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerKeyLiquidityPool)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerKeyLiquidityPool) xdrType() {} var _ xdrType = (*LedgerKeyLiquidityPool)(nil) @@ -9637,8 +9755,10 @@ func (s LedgerKeyContractData) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerKeyContractData) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -9647,8 +9767,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerKeyContractData)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerKeyContractData) xdrType() {} var _ xdrType = (*LedgerKeyContractData)(nil) @@ -9701,8 +9820,10 @@ func (s LedgerKeyContractCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerKeyContractCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -9711,8 +9832,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerKeyContractCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerKeyContractCode) xdrType() {} var _ xdrType = (*LedgerKeyContractCode)(nil) @@ -9765,8 +9885,10 @@ func (s LedgerKeyConfigSetting) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerKeyConfigSetting) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -9775,8 +9897,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerKeyConfigSetting)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerKeyConfigSetting) xdrType() {} var _ xdrType = (*LedgerKeyConfigSetting)(nil) @@ -9830,8 +9951,10 @@ func (s LedgerKeyTtl) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerKeyTtl) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -9840,8 +9963,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerKeyTtl)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerKeyTtl) xdrType() {} var _ xdrType = (*LedgerKeyTtl)(nil) @@ -10460,8 +10582,10 @@ func (s LedgerKey) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerKey) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -10470,8 +10594,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerKey)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerKey) xdrType() {} var _ xdrType = (*LedgerKey)(nil) @@ -10571,8 +10694,10 @@ func (s EnvelopeType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *EnvelopeType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -10581,8 +10706,7 @@ var ( _ encoding.BinaryUnmarshaler = (*EnvelopeType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s EnvelopeType) xdrType() {} var _ xdrType = (*EnvelopeType)(nil) @@ -10635,8 +10759,10 @@ func (s UpgradeType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *UpgradeType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -10645,8 +10771,7 @@ var ( _ encoding.BinaryUnmarshaler = (*UpgradeType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s UpgradeType) xdrType() {} var _ xdrType = (*UpgradeType)(nil) @@ -10722,8 +10847,10 @@ func (s StellarValueType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *StellarValueType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -10732,8 +10859,7 @@ var ( _ encoding.BinaryUnmarshaler = (*StellarValueType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s StellarValueType) xdrType() {} var _ xdrType = (*StellarValueType)(nil) @@ -10796,8 +10922,10 @@ func (s LedgerCloseValueSignature) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerCloseValueSignature) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -10806,8 +10934,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerCloseValueSignature)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerCloseValueSignature) xdrType() {} var _ xdrType = (*LedgerCloseValueSignature)(nil) @@ -10947,8 +11074,10 @@ func (s StellarValueExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *StellarValueExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -10957,8 +11086,7 @@ var ( _ encoding.BinaryUnmarshaler = (*StellarValueExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s StellarValueExt) xdrType() {} var _ xdrType = (*StellarValueExt)(nil) @@ -11048,8 +11176,11 @@ func (s *StellarValue) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { } s.Upgrades = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding UpgradeType: length (%d) exceeds remaining input length (%d)", l, il) + } s.Upgrades = make([]UpgradeType, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Upgrades[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -11076,8 +11207,10 @@ func (s StellarValue) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *StellarValue) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -11086,8 +11219,7 @@ var ( _ encoding.BinaryUnmarshaler = (*StellarValue)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s StellarValue) xdrType() {} var _ xdrType = (*StellarValue)(nil) @@ -11171,8 +11303,10 @@ func (s LedgerHeaderFlags) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerHeaderFlags) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -11181,8 +11315,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerHeaderFlags)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerHeaderFlags) xdrType() {} var _ xdrType = (*LedgerHeaderFlags)(nil) @@ -11272,8 +11405,10 @@ func (s LedgerHeaderExtensionV1Ext) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerHeaderExtensionV1Ext) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -11282,8 +11417,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerHeaderExtensionV1Ext)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerHeaderExtensionV1Ext) xdrType() {} var _ xdrType = (*LedgerHeaderExtensionV1Ext)(nil) @@ -11352,8 +11486,10 @@ func (s LedgerHeaderExtensionV1) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerHeaderExtensionV1) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -11362,8 +11498,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerHeaderExtensionV1)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerHeaderExtensionV1) xdrType() {} var _ xdrType = (*LedgerHeaderExtensionV1)(nil) @@ -11503,8 +11638,10 @@ func (s LedgerHeaderExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerHeaderExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -11513,8 +11650,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerHeaderExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerHeaderExt) xdrType() {} var _ xdrType = (*LedgerHeaderExt)(nil) @@ -11732,8 +11868,10 @@ func (s LedgerHeader) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerHeader) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -11742,8 +11880,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerHeader)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerHeader) xdrType() {} var _ xdrType = (*LedgerHeader)(nil) @@ -11834,8 +11971,10 @@ func (s LedgerUpgradeType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerUpgradeType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -11844,8 +11983,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerUpgradeType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerUpgradeType) xdrType() {} var _ xdrType = (*LedgerUpgradeType)(nil) @@ -11907,8 +12045,10 @@ func (s ConfigUpgradeSetKey) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ConfigUpgradeSetKey) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -11917,8 +12057,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ConfigUpgradeSetKey)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ConfigUpgradeSetKey) xdrType() {} var _ xdrType = (*ConfigUpgradeSetKey)(nil) @@ -12349,8 +12488,10 @@ func (s LedgerUpgrade) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerUpgrade) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -12359,8 +12500,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerUpgrade)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerUpgrade) xdrType() {} var _ xdrType = (*LedgerUpgrade)(nil) @@ -12406,8 +12546,11 @@ func (s *ConfigUpgradeSet) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error } s.UpdatedEntry = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ConfigSettingEntry: length (%d) exceeds remaining input length (%d)", l, il) + } s.UpdatedEntry = make([]ConfigSettingEntry, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.UpdatedEntry[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -12429,8 +12572,10 @@ func (s ConfigUpgradeSet) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ConfigUpgradeSet) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -12439,8 +12584,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ConfigUpgradeSet)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ConfigUpgradeSet) xdrType() {} var _ xdrType = (*ConfigUpgradeSet)(nil) @@ -12524,8 +12668,10 @@ func (s BucketEntryType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *BucketEntryType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -12534,8 +12680,7 @@ var ( _ encoding.BinaryUnmarshaler = (*BucketEntryType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s BucketEntryType) xdrType() {} var _ xdrType = (*BucketEntryType)(nil) @@ -12625,8 +12770,10 @@ func (s BucketMetadataExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *BucketMetadataExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -12635,8 +12782,7 @@ var ( _ encoding.BinaryUnmarshaler = (*BucketMetadataExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s BucketMetadataExt) xdrType() {} var _ xdrType = (*BucketMetadataExt)(nil) @@ -12707,8 +12853,10 @@ func (s BucketMetadata) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *BucketMetadata) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -12717,8 +12865,7 @@ var ( _ encoding.BinaryUnmarshaler = (*BucketMetadata)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s BucketMetadata) xdrType() {} var _ xdrType = (*BucketMetadata)(nil) @@ -12970,8 +13117,10 @@ func (s BucketEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *BucketEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -12980,8 +13129,7 @@ var ( _ encoding.BinaryUnmarshaler = (*BucketEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s BucketEntry) xdrType() {} var _ xdrType = (*BucketEntry)(nil) @@ -13056,8 +13204,10 @@ func (s TxSetComponentType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TxSetComponentType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -13066,8 +13216,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TxSetComponentType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TxSetComponentType) xdrType() {} var _ xdrType = (*TxSetComponentType)(nil) @@ -13139,8 +13288,11 @@ func (s *TxSetComponentTxsMaybeDiscountedFee) DecodeFrom(d *xdr.Decoder, maxDept } s.Txs = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding TransactionEnvelope: length (%d) exceeds remaining input length (%d)", l, il) + } s.Txs = make([]TransactionEnvelope, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Txs[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -13162,8 +13314,10 @@ func (s TxSetComponentTxsMaybeDiscountedFee) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TxSetComponentTxsMaybeDiscountedFee) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -13172,8 +13326,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TxSetComponentTxsMaybeDiscountedFee)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TxSetComponentTxsMaybeDiscountedFee) xdrType() {} var _ xdrType = (*TxSetComponentTxsMaybeDiscountedFee)(nil) @@ -13305,8 +13458,10 @@ func (s TxSetComponent) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TxSetComponent) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -13315,8 +13470,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TxSetComponent)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TxSetComponent) xdrType() {} var _ xdrType = (*TxSetComponent)(nil) @@ -13436,8 +13590,11 @@ func (u *TransactionPhase) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error } (*u.V0Components) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding TxSetComponent: length (%d) exceeds remaining input length (%d)", l, il) + } (*u.V0Components) = make([]TxSetComponent, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*u.V0Components)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -13461,8 +13618,10 @@ func (s TransactionPhase) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionPhase) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -13471,8 +13630,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionPhase)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionPhase) xdrType() {} var _ xdrType = (*TransactionPhase)(nil) @@ -13529,8 +13687,11 @@ func (s *TransactionSet) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } s.Txs = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding TransactionEnvelope: length (%d) exceeds remaining input length (%d)", l, il) + } s.Txs = make([]TransactionEnvelope, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Txs[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -13552,8 +13713,10 @@ func (s TransactionSet) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionSet) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -13562,8 +13725,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionSet)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionSet) xdrType() {} var _ xdrType = (*TransactionSet)(nil) @@ -13620,8 +13782,11 @@ func (s *TransactionSetV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error } s.Phases = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding TransactionPhase: length (%d) exceeds remaining input length (%d)", l, il) + } s.Phases = make([]TransactionPhase, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Phases[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -13643,8 +13808,10 @@ func (s TransactionSetV1) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionSetV1) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -13653,8 +13820,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionSetV1)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionSetV1) xdrType() {} var _ xdrType = (*TransactionSetV1)(nil) @@ -13783,8 +13949,10 @@ func (s GeneralizedTransactionSet) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *GeneralizedTransactionSet) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -13793,8 +13961,7 @@ var ( _ encoding.BinaryUnmarshaler = (*GeneralizedTransactionSet)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s GeneralizedTransactionSet) xdrType() {} var _ xdrType = (*GeneralizedTransactionSet)(nil) @@ -13857,8 +14024,10 @@ func (s TransactionResultPair) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionResultPair) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -13867,8 +14036,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionResultPair)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionResultPair) xdrType() {} var _ xdrType = (*TransactionResultPair)(nil) @@ -13915,8 +14083,11 @@ func (s *TransactionResultSet) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, e } s.Results = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding TransactionResultPair: length (%d) exceeds remaining input length (%d)", l, il) + } s.Results = make([]TransactionResultPair, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Results[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -13938,8 +14109,10 @@ func (s TransactionResultSet) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionResultSet) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -13948,8 +14121,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionResultSet)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionResultSet) xdrType() {} var _ xdrType = (*TransactionResultSet)(nil) @@ -14089,8 +14261,10 @@ func (s TransactionHistoryEntryExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionHistoryEntryExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -14099,8 +14273,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionHistoryEntryExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionHistoryEntryExt) xdrType() {} var _ xdrType = (*TransactionHistoryEntryExt)(nil) @@ -14182,8 +14355,10 @@ func (s TransactionHistoryEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionHistoryEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -14192,8 +14367,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionHistoryEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionHistoryEntry) xdrType() {} var _ xdrType = (*TransactionHistoryEntry)(nil) @@ -14283,8 +14457,10 @@ func (s TransactionHistoryResultEntryExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionHistoryResultEntryExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -14293,8 +14469,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionHistoryResultEntryExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionHistoryResultEntryExt) xdrType() {} var _ xdrType = (*TransactionHistoryResultEntryExt)(nil) @@ -14374,8 +14549,10 @@ func (s TransactionHistoryResultEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionHistoryResultEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -14384,8 +14561,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionHistoryResultEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionHistoryResultEntry) xdrType() {} var _ xdrType = (*TransactionHistoryResultEntry)(nil) @@ -14475,8 +14651,10 @@ func (s LedgerHeaderHistoryEntryExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerHeaderHistoryEntryExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -14485,8 +14663,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerHeaderHistoryEntryExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerHeaderHistoryEntryExt) xdrType() {} var _ xdrType = (*LedgerHeaderHistoryEntryExt)(nil) @@ -14566,8 +14743,10 @@ func (s LedgerHeaderHistoryEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerHeaderHistoryEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -14576,8 +14755,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerHeaderHistoryEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerHeaderHistoryEntry) xdrType() {} var _ xdrType = (*LedgerHeaderHistoryEntry)(nil) @@ -14634,8 +14812,11 @@ func (s *LedgerScpMessages) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } s.Messages = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScpEnvelope: length (%d) exceeds remaining input length (%d)", l, il) + } s.Messages = make([]ScpEnvelope, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Messages[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -14657,8 +14838,10 @@ func (s LedgerScpMessages) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerScpMessages) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -14667,8 +14850,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerScpMessages)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerScpMessages) xdrType() {} var _ xdrType = (*LedgerScpMessages)(nil) @@ -14720,8 +14902,11 @@ func (s *ScpHistoryEntryV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } s.QuorumSets = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScpQuorumSet: length (%d) exceeds remaining input length (%d)", l, il) + } s.QuorumSets = make([]ScpQuorumSet, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.QuorumSets[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -14748,8 +14933,10 @@ func (s ScpHistoryEntryV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScpHistoryEntryV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -14758,8 +14945,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScpHistoryEntryV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScpHistoryEntryV0) xdrType() {} var _ xdrType = (*ScpHistoryEntryV0)(nil) @@ -14887,8 +15073,10 @@ func (s ScpHistoryEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScpHistoryEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -14897,8 +15085,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScpHistoryEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScpHistoryEntry) xdrType() {} var _ xdrType = (*ScpHistoryEntry)(nil) @@ -14980,8 +15167,10 @@ func (s LedgerEntryChangeType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerEntryChangeType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -14990,8 +15179,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerEntryChangeType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerEntryChangeType) xdrType() {} var _ xdrType = (*LedgerEntryChangeType)(nil) @@ -15269,8 +15457,10 @@ func (s LedgerEntryChange) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerEntryChange) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -15279,8 +15469,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerEntryChange)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerEntryChange) xdrType() {} var _ xdrType = (*LedgerEntryChange)(nil) @@ -15322,8 +15511,11 @@ func (s *LedgerEntryChanges) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, err } (*s) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding LedgerEntryChange: length (%d) exceeds remaining input length (%d)", l, il) + } (*s) = make([]LedgerEntryChange, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -15345,8 +15537,10 @@ func (s LedgerEntryChanges) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerEntryChanges) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -15355,8 +15549,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerEntryChanges)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerEntryChanges) xdrType() {} var _ xdrType = (*LedgerEntryChanges)(nil) @@ -15409,8 +15602,10 @@ func (s OperationMeta) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *OperationMeta) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -15419,8 +15614,7 @@ var ( _ encoding.BinaryUnmarshaler = (*OperationMeta)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s OperationMeta) xdrType() {} var _ xdrType = (*OperationMeta)(nil) @@ -15477,8 +15671,11 @@ func (s *TransactionMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } s.Operations = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding OperationMeta: length (%d) exceeds remaining input length (%d)", l, il) + } s.Operations = make([]OperationMeta, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Operations[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -15500,8 +15697,10 @@ func (s TransactionMetaV1) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionMetaV1) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -15510,8 +15709,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionMetaV1)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionMetaV1) xdrType() {} var _ xdrType = (*TransactionMetaV1)(nil) @@ -15575,8 +15773,11 @@ func (s *TransactionMetaV2) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } s.Operations = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding OperationMeta: length (%d) exceeds remaining input length (%d)", l, il) + } s.Operations = make([]OperationMeta, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Operations[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -15603,8 +15804,10 @@ func (s TransactionMetaV2) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionMetaV2) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -15613,8 +15816,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionMetaV2)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionMetaV2) xdrType() {} var _ xdrType = (*TransactionMetaV2)(nil) @@ -15693,8 +15895,10 @@ func (s ContractEventType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractEventType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -15703,8 +15907,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractEventType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractEventType) xdrType() {} var _ xdrType = (*ContractEventType)(nil) @@ -15756,8 +15959,11 @@ func (s *ContractEventV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } s.Topics = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScVal: length (%d) exceeds remaining input length (%d)", l, il) + } s.Topics = make([]ScVal, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Topics[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -15784,8 +15990,10 @@ func (s ContractEventV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractEventV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -15794,8 +16002,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractEventV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractEventV0) xdrType() {} var _ xdrType = (*ContractEventV0)(nil) @@ -15927,8 +16134,10 @@ func (s ContractEventBody) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractEventBody) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -15937,8 +16146,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractEventBody)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractEventBody) xdrType() {} var _ xdrType = (*ContractEventBody)(nil) @@ -16049,8 +16257,10 @@ func (s ContractEvent) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractEvent) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -16059,8 +16269,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractEvent)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractEvent) xdrType() {} var _ xdrType = (*ContractEvent)(nil) @@ -16123,8 +16332,10 @@ func (s DiagnosticEvent) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *DiagnosticEvent) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -16133,8 +16344,7 @@ var ( _ encoding.BinaryUnmarshaler = (*DiagnosticEvent)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s DiagnosticEvent) xdrType() {} var _ xdrType = (*DiagnosticEvent)(nil) @@ -16212,8 +16422,11 @@ func (s *SorobanTransactionMeta) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, } s.Events = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ContractEvent: length (%d) exceeds remaining input length (%d)", l, il) + } s.Events = make([]ContractEvent, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Events[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -16233,8 +16446,11 @@ func (s *SorobanTransactionMeta) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, } s.DiagnosticEvents = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding DiagnosticEvent: length (%d) exceeds remaining input length (%d)", l, il) + } s.DiagnosticEvents = make([]DiagnosticEvent, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.DiagnosticEvents[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -16256,8 +16472,10 @@ func (s SorobanTransactionMeta) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SorobanTransactionMeta) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -16266,8 +16484,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SorobanTransactionMeta)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SorobanTransactionMeta) xdrType() {} var _ xdrType = (*SorobanTransactionMeta)(nil) @@ -16353,8 +16570,11 @@ func (s *TransactionMetaV3) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } s.Operations = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding OperationMeta: length (%d) exceeds remaining input length (%d)", l, il) + } s.Operations = make([]OperationMeta, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Operations[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -16396,8 +16616,10 @@ func (s TransactionMetaV3) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionMetaV3) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -16406,8 +16628,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionMetaV3)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionMetaV3) xdrType() {} var _ xdrType = (*TransactionMetaV3)(nil) @@ -16464,8 +16685,11 @@ func (s *InvokeHostFunctionSuccessPreImage) DecodeFrom(d *xdr.Decoder, maxDepth } s.Events = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ContractEvent: length (%d) exceeds remaining input length (%d)", l, il) + } s.Events = make([]ContractEvent, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Events[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -16487,8 +16711,10 @@ func (s InvokeHostFunctionSuccessPreImage) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *InvokeHostFunctionSuccessPreImage) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -16497,8 +16723,7 @@ var ( _ encoding.BinaryUnmarshaler = (*InvokeHostFunctionSuccessPreImage)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s InvokeHostFunctionSuccessPreImage) xdrType() {} var _ xdrType = (*InvokeHostFunctionSuccessPreImage)(nil) @@ -16744,8 +16969,11 @@ func (u *TransactionMeta) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } (*u.Operations) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding OperationMeta: length (%d) exceeds remaining input length (%d)", l, il) + } (*u.Operations) = make([]OperationMeta, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*u.Operations)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -16793,8 +17021,10 @@ func (s TransactionMeta) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionMeta) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -16803,8 +17033,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionMeta)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionMeta) xdrType() {} var _ xdrType = (*TransactionMeta)(nil) @@ -16877,8 +17106,10 @@ func (s TransactionResultMeta) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionResultMeta) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -16887,8 +17118,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionResultMeta)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionResultMeta) xdrType() {} var _ xdrType = (*TransactionResultMeta)(nil) @@ -16951,8 +17181,10 @@ func (s UpgradeEntryMeta) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *UpgradeEntryMeta) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -16961,8 +17193,7 @@ var ( _ encoding.BinaryUnmarshaler = (*UpgradeEntryMeta)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s UpgradeEntryMeta) xdrType() {} var _ xdrType = (*UpgradeEntryMeta)(nil) @@ -17058,8 +17289,11 @@ func (s *LedgerCloseMetaV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } s.TxProcessing = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding TransactionResultMeta: length (%d) exceeds remaining input length (%d)", l, il) + } s.TxProcessing = make([]TransactionResultMeta, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.TxProcessing[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17074,8 +17308,11 @@ func (s *LedgerCloseMetaV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } s.UpgradesProcessing = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding UpgradeEntryMeta: length (%d) exceeds remaining input length (%d)", l, il) + } s.UpgradesProcessing = make([]UpgradeEntryMeta, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.UpgradesProcessing[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17090,8 +17327,11 @@ func (s *LedgerCloseMetaV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } s.ScpInfo = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScpHistoryEntry: length (%d) exceeds remaining input length (%d)", l, il) + } s.ScpInfo = make([]ScpHistoryEntry, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.ScpInfo[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17113,8 +17353,10 @@ func (s LedgerCloseMetaV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerCloseMetaV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -17123,8 +17365,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerCloseMetaV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerCloseMetaV0) xdrType() {} var _ xdrType = (*LedgerCloseMetaV0)(nil) @@ -17266,8 +17507,11 @@ func (s *LedgerCloseMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } s.TxProcessing = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding TransactionResultMeta: length (%d) exceeds remaining input length (%d)", l, il) + } s.TxProcessing = make([]TransactionResultMeta, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.TxProcessing[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17282,8 +17526,11 @@ func (s *LedgerCloseMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } s.UpgradesProcessing = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding UpgradeEntryMeta: length (%d) exceeds remaining input length (%d)", l, il) + } s.UpgradesProcessing = make([]UpgradeEntryMeta, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.UpgradesProcessing[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17298,8 +17545,11 @@ func (s *LedgerCloseMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } s.ScpInfo = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScpHistoryEntry: length (%d) exceeds remaining input length (%d)", l, il) + } s.ScpInfo = make([]ScpHistoryEntry, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.ScpInfo[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17319,8 +17569,11 @@ func (s *LedgerCloseMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } s.EvictedTemporaryLedgerKeys = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding LedgerKey: length (%d) exceeds remaining input length (%d)", l, il) + } s.EvictedTemporaryLedgerKeys = make([]LedgerKey, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.EvictedTemporaryLedgerKeys[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17335,8 +17588,11 @@ func (s *LedgerCloseMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } s.EvictedPersistentLedgerEntries = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding LedgerEntry: length (%d) exceeds remaining input length (%d)", l, il) + } s.EvictedPersistentLedgerEntries = make([]LedgerEntry, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.EvictedPersistentLedgerEntries[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17358,8 +17614,10 @@ func (s LedgerCloseMetaV1) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerCloseMetaV1) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -17368,8 +17626,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerCloseMetaV1)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerCloseMetaV1) xdrType() {} var _ xdrType = (*LedgerCloseMetaV1)(nil) @@ -17547,8 +17804,10 @@ func (s LedgerCloseMeta) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerCloseMeta) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -17557,8 +17816,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerCloseMeta)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerCloseMeta) xdrType() {} var _ xdrType = (*LedgerCloseMeta)(nil) @@ -17643,8 +17901,10 @@ func (s ErrorCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ErrorCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -17653,8 +17913,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ErrorCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ErrorCode) xdrType() {} var _ xdrType = (*ErrorCode)(nil) @@ -17717,8 +17976,10 @@ func (s Error) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Error) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -17727,8 +17988,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Error)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Error) xdrType() {} var _ xdrType = (*Error)(nil) @@ -17781,8 +18041,10 @@ func (s SendMore) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SendMore) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -17791,8 +18053,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SendMore)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SendMore) xdrType() {} var _ xdrType = (*SendMore)(nil) @@ -17855,8 +18116,10 @@ func (s SendMoreExtended) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SendMoreExtended) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -17865,8 +18128,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SendMoreExtended)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SendMoreExtended) xdrType() {} var _ xdrType = (*SendMoreExtended)(nil) @@ -17939,8 +18201,10 @@ func (s AuthCert) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AuthCert) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -17949,8 +18213,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AuthCert)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AuthCert) xdrType() {} var _ xdrType = (*AuthCert)(nil) @@ -18083,8 +18346,10 @@ func (s Hello) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Hello) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -18093,8 +18358,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Hello)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Hello) xdrType() {} var _ xdrType = (*Hello)(nil) @@ -18152,8 +18416,10 @@ func (s Auth) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Auth) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -18162,8 +18428,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Auth)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Auth) xdrType() {} var _ xdrType = (*Auth)(nil) @@ -18239,8 +18504,10 @@ func (s IpAddrType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *IpAddrType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -18249,8 +18516,7 @@ var ( _ encoding.BinaryUnmarshaler = (*IpAddrType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s IpAddrType) xdrType() {} var _ xdrType = (*IpAddrType)(nil) @@ -18428,8 +18694,10 @@ func (s PeerAddressIp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PeerAddressIp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -18438,8 +18706,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PeerAddressIp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PeerAddressIp) xdrType() {} var _ xdrType = (*PeerAddressIp)(nil) @@ -18519,8 +18786,10 @@ func (s PeerAddress) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PeerAddress) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -18529,8 +18798,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PeerAddress)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PeerAddress) xdrType() {} var _ xdrType = (*PeerAddress)(nil) @@ -18670,8 +18938,10 @@ func (s MessageType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *MessageType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -18680,8 +18950,7 @@ var ( _ encoding.BinaryUnmarshaler = (*MessageType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s MessageType) xdrType() {} var _ xdrType = (*MessageType)(nil) @@ -18744,8 +19013,10 @@ func (s DontHave) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *DontHave) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -18754,8 +19025,7 @@ var ( _ encoding.BinaryUnmarshaler = (*DontHave)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s DontHave) xdrType() {} var _ xdrType = (*DontHave)(nil) @@ -18828,8 +19098,10 @@ func (s SurveyMessageCommandType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SurveyMessageCommandType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -18838,8 +19110,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SurveyMessageCommandType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SurveyMessageCommandType) xdrType() {} var _ xdrType = (*SurveyMessageCommandType)(nil) @@ -18915,8 +19186,10 @@ func (s SurveyMessageResponseType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SurveyMessageResponseType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -18925,8 +19198,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SurveyMessageResponseType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SurveyMessageResponseType) xdrType() {} var _ xdrType = (*SurveyMessageResponseType)(nil) @@ -19019,8 +19291,10 @@ func (s SurveyRequestMessage) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SurveyRequestMessage) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -19029,8 +19303,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SurveyRequestMessage)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SurveyRequestMessage) xdrType() {} var _ xdrType = (*SurveyRequestMessage)(nil) @@ -19093,8 +19366,10 @@ func (s SignedSurveyRequestMessage) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SignedSurveyRequestMessage) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -19103,8 +19378,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SignedSurveyRequestMessage)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SignedSurveyRequestMessage) xdrType() {} var _ xdrType = (*SignedSurveyRequestMessage)(nil) @@ -19157,8 +19431,10 @@ func (s EncryptedBody) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *EncryptedBody) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -19167,8 +19443,7 @@ var ( _ encoding.BinaryUnmarshaler = (*EncryptedBody)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s EncryptedBody) xdrType() {} var _ xdrType = (*EncryptedBody)(nil) @@ -19261,8 +19536,10 @@ func (s SurveyResponseMessage) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SurveyResponseMessage) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -19271,8 +19548,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SurveyResponseMessage)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SurveyResponseMessage) xdrType() {} var _ xdrType = (*SurveyResponseMessage)(nil) @@ -19335,8 +19611,10 @@ func (s SignedSurveyResponseMessage) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SignedSurveyResponseMessage) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -19345,8 +19623,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SignedSurveyResponseMessage)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SignedSurveyResponseMessage) xdrType() {} var _ xdrType = (*SignedSurveyResponseMessage)(nil) @@ -19541,8 +19818,10 @@ func (s PeerStats) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PeerStats) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -19551,8 +19830,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PeerStats)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PeerStats) xdrType() {} var _ xdrType = (*PeerStats)(nil) @@ -19602,8 +19880,11 @@ func (s *PeerStatList) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { } (*s) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding PeerStats: length (%d) exceeds remaining input length (%d)", l, il) + } (*s) = make([]PeerStats, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -19625,8 +19906,10 @@ func (s PeerStatList) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PeerStatList) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -19635,8 +19918,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PeerStatList)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PeerStatList) xdrType() {} var _ xdrType = (*PeerStatList)(nil) @@ -19720,8 +20002,10 @@ func (s TopologyResponseBodyV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TopologyResponseBodyV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -19730,8 +20014,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TopologyResponseBodyV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TopologyResponseBodyV0) xdrType() {} var _ xdrType = (*TopologyResponseBodyV0)(nil) @@ -19836,8 +20119,10 @@ func (s TopologyResponseBodyV1) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TopologyResponseBodyV1) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -19846,8 +20131,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TopologyResponseBodyV1)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TopologyResponseBodyV1) xdrType() {} var _ xdrType = (*TopologyResponseBodyV1)(nil) @@ -20025,8 +20309,10 @@ func (s SurveyResponseBody) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SurveyResponseBody) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -20035,8 +20321,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SurveyResponseBody)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SurveyResponseBody) xdrType() {} var _ xdrType = (*SurveyResponseBody)(nil) @@ -20091,8 +20376,11 @@ func (s *TxAdvertVector) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } (*s) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding Hash: length (%d) exceeds remaining input length (%d)", l, il) + } (*s) = make([]Hash, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -20114,8 +20402,10 @@ func (s TxAdvertVector) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TxAdvertVector) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -20124,8 +20414,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TxAdvertVector)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TxAdvertVector) xdrType() {} var _ xdrType = (*TxAdvertVector)(nil) @@ -20178,8 +20467,10 @@ func (s FloodAdvert) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *FloodAdvert) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -20188,8 +20479,7 @@ var ( _ encoding.BinaryUnmarshaler = (*FloodAdvert)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s FloodAdvert) xdrType() {} var _ xdrType = (*FloodAdvert)(nil) @@ -20244,8 +20534,11 @@ func (s *TxDemandVector) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } (*s) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding Hash: length (%d) exceeds remaining input length (%d)", l, il) + } (*s) = make([]Hash, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -20267,8 +20560,10 @@ func (s TxDemandVector) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TxDemandVector) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -20277,8 +20572,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TxDemandVector)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TxDemandVector) xdrType() {} var _ xdrType = (*TxDemandVector)(nil) @@ -20331,8 +20625,10 @@ func (s FloodDemand) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *FloodDemand) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -20341,8 +20637,7 @@ var ( _ encoding.BinaryUnmarshaler = (*FloodDemand)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s FloodDemand) xdrType() {} var _ xdrType = (*FloodDemand)(nil) @@ -21272,8 +21567,11 @@ func (u *StellarMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } (*u.Peers) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding PeerAddress: length (%d) exceeds remaining input length (%d)", l, il) + } (*u.Peers) = make([]PeerAddress, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*u.Peers)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -21409,8 +21707,10 @@ func (s StellarMessage) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *StellarMessage) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -21419,8 +21719,7 @@ var ( _ encoding.BinaryUnmarshaler = (*StellarMessage)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s StellarMessage) xdrType() {} var _ xdrType = (*StellarMessage)(nil) @@ -21493,8 +21792,10 @@ func (s AuthenticatedMessageV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AuthenticatedMessageV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -21503,8 +21804,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AuthenticatedMessageV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AuthenticatedMessageV0) xdrType() {} var _ xdrType = (*AuthenticatedMessageV0)(nil) @@ -21637,8 +21937,10 @@ func (s AuthenticatedMessage) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AuthenticatedMessage) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -21647,8 +21949,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AuthenticatedMessage)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AuthenticatedMessage) xdrType() {} var _ xdrType = (*AuthenticatedMessage)(nil) @@ -21781,8 +22082,10 @@ func (s LiquidityPoolParameters) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LiquidityPoolParameters) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -21791,8 +22094,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LiquidityPoolParameters)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LiquidityPoolParameters) xdrType() {} var _ xdrType = (*LiquidityPoolParameters)(nil) @@ -21855,8 +22157,10 @@ func (s MuxedAccountMed25519) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *MuxedAccountMed25519) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -21865,8 +22169,7 @@ var ( _ encoding.BinaryUnmarshaler = (*MuxedAccountMed25519)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s MuxedAccountMed25519) xdrType() {} var _ xdrType = (*MuxedAccountMed25519)(nil) @@ -22048,8 +22351,10 @@ func (s MuxedAccount) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *MuxedAccount) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -22058,8 +22363,7 @@ var ( _ encoding.BinaryUnmarshaler = (*MuxedAccount)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s MuxedAccount) xdrType() {} var _ xdrType = (*MuxedAccount)(nil) @@ -22122,8 +22426,10 @@ func (s DecoratedSignature) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *DecoratedSignature) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -22132,8 +22438,7 @@ var ( _ encoding.BinaryUnmarshaler = (*DecoratedSignature)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s DecoratedSignature) xdrType() {} var _ xdrType = (*DecoratedSignature)(nil) @@ -22284,8 +22589,10 @@ func (s OperationType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *OperationType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -22294,8 +22601,7 @@ var ( _ encoding.BinaryUnmarshaler = (*OperationType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s OperationType) xdrType() {} var _ xdrType = (*OperationType)(nil) @@ -22358,8 +22664,10 @@ func (s CreateAccountOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *CreateAccountOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -22368,8 +22676,7 @@ var ( _ encoding.BinaryUnmarshaler = (*CreateAccountOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s CreateAccountOp) xdrType() {} var _ xdrType = (*CreateAccountOp)(nil) @@ -22442,8 +22749,10 @@ func (s PaymentOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PaymentOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -22452,8 +22761,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PaymentOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PaymentOp) xdrType() {} var _ xdrType = (*PaymentOp)(nil) @@ -22557,8 +22865,11 @@ func (s *PathPaymentStrictReceiveOp) DecodeFrom(d *xdr.Decoder, maxDepth uint) ( } s.Path = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding Asset: length (%d) exceeds remaining input length (%d)", l, il) + } s.Path = make([]Asset, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Path[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -22580,8 +22891,10 @@ func (s PathPaymentStrictReceiveOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PathPaymentStrictReceiveOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -22590,8 +22903,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PathPaymentStrictReceiveOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PathPaymentStrictReceiveOp) xdrType() {} var _ xdrType = (*PathPaymentStrictReceiveOp)(nil) @@ -22695,8 +23007,11 @@ func (s *PathPaymentStrictSendOp) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int } s.Path = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding Asset: length (%d) exceeds remaining input length (%d)", l, il) + } s.Path = make([]Asset, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Path[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -22718,8 +23033,10 @@ func (s PathPaymentStrictSendOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PathPaymentStrictSendOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -22728,8 +23045,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PathPaymentStrictSendOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PathPaymentStrictSendOp) xdrType() {} var _ xdrType = (*PathPaymentStrictSendOp)(nil) @@ -22824,8 +23140,10 @@ func (s ManageSellOfferOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ManageSellOfferOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -22834,8 +23152,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ManageSellOfferOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ManageSellOfferOp) xdrType() {} var _ xdrType = (*ManageSellOfferOp)(nil) @@ -22931,8 +23248,10 @@ func (s ManageBuyOfferOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ManageBuyOfferOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -22941,8 +23260,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ManageBuyOfferOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ManageBuyOfferOp) xdrType() {} var _ xdrType = (*ManageBuyOfferOp)(nil) @@ -23025,8 +23343,10 @@ func (s CreatePassiveSellOfferOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *CreatePassiveSellOfferOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -23035,8 +23355,7 @@ var ( _ encoding.BinaryUnmarshaler = (*CreatePassiveSellOfferOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s CreatePassiveSellOfferOp) xdrType() {} var _ xdrType = (*CreatePassiveSellOfferOp)(nil) @@ -23303,8 +23622,10 @@ func (s SetOptionsOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SetOptionsOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -23313,8 +23634,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SetOptionsOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SetOptionsOp) xdrType() {} var _ xdrType = (*SetOptionsOp)(nil) @@ -23559,8 +23879,10 @@ func (s ChangeTrustAsset) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ChangeTrustAsset) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -23569,8 +23891,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ChangeTrustAsset)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ChangeTrustAsset) xdrType() {} var _ xdrType = (*ChangeTrustAsset)(nil) @@ -23635,8 +23956,10 @@ func (s ChangeTrustOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ChangeTrustOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -23645,8 +23968,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ChangeTrustOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ChangeTrustOp) xdrType() {} var _ xdrType = (*ChangeTrustOp)(nil) @@ -23721,8 +24043,10 @@ func (s AllowTrustOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AllowTrustOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -23731,8 +24055,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AllowTrustOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AllowTrustOp) xdrType() {} var _ xdrType = (*AllowTrustOp)(nil) @@ -23810,8 +24133,10 @@ func (s ManageDataOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ManageDataOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -23820,8 +24145,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ManageDataOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ManageDataOp) xdrType() {} var _ xdrType = (*ManageDataOp)(nil) @@ -23874,8 +24198,10 @@ func (s BumpSequenceOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *BumpSequenceOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -23884,8 +24210,7 @@ var ( _ encoding.BinaryUnmarshaler = (*BumpSequenceOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s BumpSequenceOp) xdrType() {} var _ xdrType = (*BumpSequenceOp)(nil) @@ -23955,8 +24280,11 @@ func (s *CreateClaimableBalanceOp) DecodeFrom(d *xdr.Decoder, maxDepth uint) (in } s.Claimants = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding Claimant: length (%d) exceeds remaining input length (%d)", l, il) + } s.Claimants = make([]Claimant, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Claimants[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -23978,8 +24306,10 @@ func (s CreateClaimableBalanceOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *CreateClaimableBalanceOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -23988,8 +24318,7 @@ var ( _ encoding.BinaryUnmarshaler = (*CreateClaimableBalanceOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s CreateClaimableBalanceOp) xdrType() {} var _ xdrType = (*CreateClaimableBalanceOp)(nil) @@ -24042,8 +24371,10 @@ func (s ClaimClaimableBalanceOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimClaimableBalanceOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -24052,8 +24383,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimClaimableBalanceOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimClaimableBalanceOp) xdrType() {} var _ xdrType = (*ClaimClaimableBalanceOp)(nil) @@ -24106,8 +24436,10 @@ func (s BeginSponsoringFutureReservesOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *BeginSponsoringFutureReservesOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -24116,8 +24448,7 @@ var ( _ encoding.BinaryUnmarshaler = (*BeginSponsoringFutureReservesOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s BeginSponsoringFutureReservesOp) xdrType() {} var _ xdrType = (*BeginSponsoringFutureReservesOp)(nil) @@ -24193,8 +24524,10 @@ func (s RevokeSponsorshipType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *RevokeSponsorshipType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -24203,8 +24536,7 @@ var ( _ encoding.BinaryUnmarshaler = (*RevokeSponsorshipType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s RevokeSponsorshipType) xdrType() {} var _ xdrType = (*RevokeSponsorshipType)(nil) @@ -24267,8 +24599,10 @@ func (s RevokeSponsorshipOpSigner) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *RevokeSponsorshipOpSigner) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -24277,8 +24611,7 @@ var ( _ encoding.BinaryUnmarshaler = (*RevokeSponsorshipOpSigner)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s RevokeSponsorshipOpSigner) xdrType() {} var _ xdrType = (*RevokeSponsorshipOpSigner)(nil) @@ -24460,8 +24793,10 @@ func (s RevokeSponsorshipOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *RevokeSponsorshipOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -24470,8 +24805,7 @@ var ( _ encoding.BinaryUnmarshaler = (*RevokeSponsorshipOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s RevokeSponsorshipOp) xdrType() {} var _ xdrType = (*RevokeSponsorshipOp)(nil) @@ -24544,8 +24878,10 @@ func (s ClawbackOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClawbackOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -24554,8 +24890,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClawbackOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClawbackOp) xdrType() {} var _ xdrType = (*ClawbackOp)(nil) @@ -24608,8 +24943,10 @@ func (s ClawbackClaimableBalanceOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClawbackClaimableBalanceOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -24618,8 +24955,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClawbackClaimableBalanceOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClawbackClaimableBalanceOp) xdrType() {} var _ xdrType = (*ClawbackClaimableBalanceOp)(nil) @@ -24703,8 +25039,10 @@ func (s SetTrustLineFlagsOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SetTrustLineFlagsOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -24713,8 +25051,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SetTrustLineFlagsOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SetTrustLineFlagsOp) xdrType() {} var _ xdrType = (*SetTrustLineFlagsOp)(nil) @@ -24812,8 +25149,10 @@ func (s LiquidityPoolDepositOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LiquidityPoolDepositOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -24822,8 +25161,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LiquidityPoolDepositOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LiquidityPoolDepositOp) xdrType() {} var _ xdrType = (*LiquidityPoolDepositOp)(nil) @@ -24906,8 +25244,10 @@ func (s LiquidityPoolWithdrawOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LiquidityPoolWithdrawOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -24916,8 +25256,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LiquidityPoolWithdrawOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LiquidityPoolWithdrawOp) xdrType() {} var _ xdrType = (*LiquidityPoolWithdrawOp)(nil) @@ -24996,8 +25335,10 @@ func (s HostFunctionType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *HostFunctionType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -25006,8 +25347,7 @@ var ( _ encoding.BinaryUnmarshaler = (*HostFunctionType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s HostFunctionType) xdrType() {} var _ xdrType = (*HostFunctionType)(nil) @@ -25083,8 +25423,10 @@ func (s ContractIdPreimageType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractIdPreimageType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -25093,8 +25435,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractIdPreimageType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractIdPreimageType) xdrType() {} var _ xdrType = (*ContractIdPreimageType)(nil) @@ -25157,8 +25498,10 @@ func (s ContractIdPreimageFromAddress) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractIdPreimageFromAddress) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -25167,8 +25510,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractIdPreimageFromAddress)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractIdPreimageFromAddress) xdrType() {} var _ xdrType = (*ContractIdPreimageFromAddress)(nil) @@ -25350,8 +25692,10 @@ func (s ContractIdPreimage) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractIdPreimage) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -25360,8 +25704,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractIdPreimage)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractIdPreimage) xdrType() {} var _ xdrType = (*ContractIdPreimage)(nil) @@ -25424,8 +25767,10 @@ func (s CreateContractArgs) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *CreateContractArgs) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -25434,8 +25779,7 @@ var ( _ encoding.BinaryUnmarshaler = (*CreateContractArgs)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s CreateContractArgs) xdrType() {} var _ xdrType = (*CreateContractArgs)(nil) @@ -25501,8 +25845,11 @@ func (s *InvokeContractArgs) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, err } s.Args = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScVal: length (%d) exceeds remaining input length (%d)", l, il) + } s.Args = make([]ScVal, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Args[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -25524,8 +25871,10 @@ func (s InvokeContractArgs) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *InvokeContractArgs) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -25534,8 +25883,7 @@ var ( _ encoding.BinaryUnmarshaler = (*InvokeContractArgs)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s InvokeContractArgs) xdrType() {} var _ xdrType = (*InvokeContractArgs)(nil) @@ -25763,8 +26111,10 @@ func (s HostFunction) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *HostFunction) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -25773,8 +26123,7 @@ var ( _ encoding.BinaryUnmarshaler = (*HostFunction)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s HostFunction) xdrType() {} var _ xdrType = (*HostFunction)(nil) @@ -25850,8 +26199,10 @@ func (s SorobanAuthorizedFunctionType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SorobanAuthorizedFunctionType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -25860,8 +26211,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SorobanAuthorizedFunctionType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SorobanAuthorizedFunctionType) xdrType() {} var _ xdrType = (*SorobanAuthorizedFunctionType)(nil) @@ -26039,8 +26389,10 @@ func (s SorobanAuthorizedFunction) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SorobanAuthorizedFunction) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -26049,8 +26401,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SorobanAuthorizedFunction)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SorobanAuthorizedFunction) xdrType() {} var _ xdrType = (*SorobanAuthorizedFunction)(nil) @@ -26107,8 +26458,11 @@ func (s *SorobanAuthorizedInvocation) DecodeFrom(d *xdr.Decoder, maxDepth uint) } s.SubInvocations = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding SorobanAuthorizedInvocation: length (%d) exceeds remaining input length (%d)", l, il) + } s.SubInvocations = make([]SorobanAuthorizedInvocation, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.SubInvocations[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -26130,8 +26484,10 @@ func (s SorobanAuthorizedInvocation) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SorobanAuthorizedInvocation) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -26140,8 +26496,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SorobanAuthorizedInvocation)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SorobanAuthorizedInvocation) xdrType() {} var _ xdrType = (*SorobanAuthorizedInvocation)(nil) @@ -26224,8 +26579,10 @@ func (s SorobanAddressCredentials) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SorobanAddressCredentials) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -26234,8 +26591,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SorobanAddressCredentials)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SorobanAddressCredentials) xdrType() {} var _ xdrType = (*SorobanAddressCredentials)(nil) @@ -26311,8 +26667,10 @@ func (s SorobanCredentialsType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SorobanCredentialsType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -26321,8 +26679,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SorobanCredentialsType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SorobanCredentialsType) xdrType() {} var _ xdrType = (*SorobanCredentialsType)(nil) @@ -26462,8 +26819,10 @@ func (s SorobanCredentials) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SorobanCredentials) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -26472,8 +26831,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SorobanCredentials)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SorobanCredentials) xdrType() {} var _ xdrType = (*SorobanCredentials)(nil) @@ -26536,8 +26894,10 @@ func (s SorobanAuthorizationEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SorobanAuthorizationEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -26546,8 +26906,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SorobanAuthorizationEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SorobanAuthorizationEntry) xdrType() {} var _ xdrType = (*SorobanAuthorizationEntry)(nil) @@ -26606,8 +26965,11 @@ func (s *InvokeHostFunctionOp) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, e } s.Auth = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding SorobanAuthorizationEntry: length (%d) exceeds remaining input length (%d)", l, il) + } s.Auth = make([]SorobanAuthorizationEntry, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Auth[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -26629,8 +26991,10 @@ func (s InvokeHostFunctionOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *InvokeHostFunctionOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -26639,8 +27003,7 @@ var ( _ encoding.BinaryUnmarshaler = (*InvokeHostFunctionOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s InvokeHostFunctionOp) xdrType() {} var _ xdrType = (*InvokeHostFunctionOp)(nil) @@ -26703,8 +27066,10 @@ func (s ExtendFootprintTtlOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ExtendFootprintTtlOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -26713,8 +27078,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ExtendFootprintTtlOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ExtendFootprintTtlOp) xdrType() {} var _ xdrType = (*ExtendFootprintTtlOp)(nil) @@ -26767,8 +27131,10 @@ func (s RestoreFootprintOp) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *RestoreFootprintOp) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -26777,8 +27143,7 @@ var ( _ encoding.BinaryUnmarshaler = (*RestoreFootprintOp)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s RestoreFootprintOp) xdrType() {} var _ xdrType = (*RestoreFootprintOp)(nil) @@ -28130,8 +28495,10 @@ func (s OperationBody) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *OperationBody) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -28140,8 +28507,7 @@ var ( _ encoding.BinaryUnmarshaler = (*OperationBody)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s OperationBody) xdrType() {} var _ xdrType = (*OperationBody)(nil) @@ -28280,8 +28646,10 @@ func (s Operation) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Operation) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -28290,8 +28658,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Operation)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Operation) xdrType() {} var _ xdrType = (*Operation)(nil) @@ -28364,8 +28731,10 @@ func (s HashIdPreimageOperationId) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *HashIdPreimageOperationId) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -28374,8 +28743,7 @@ var ( _ encoding.BinaryUnmarshaler = (*HashIdPreimageOperationId)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s HashIdPreimageOperationId) xdrType() {} var _ xdrType = (*HashIdPreimageOperationId)(nil) @@ -28468,8 +28836,10 @@ func (s HashIdPreimageRevokeId) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *HashIdPreimageRevokeId) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -28478,8 +28848,7 @@ var ( _ encoding.BinaryUnmarshaler = (*HashIdPreimageRevokeId)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s HashIdPreimageRevokeId) xdrType() {} var _ xdrType = (*HashIdPreimageRevokeId)(nil) @@ -28542,8 +28911,10 @@ func (s HashIdPreimageContractId) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *HashIdPreimageContractId) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -28552,8 +28923,7 @@ var ( _ encoding.BinaryUnmarshaler = (*HashIdPreimageContractId)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s HashIdPreimageContractId) xdrType() {} var _ xdrType = (*HashIdPreimageContractId)(nil) @@ -28636,8 +29006,10 @@ func (s HashIdPreimageSorobanAuthorization) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *HashIdPreimageSorobanAuthorization) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -28646,8 +29018,7 @@ var ( _ encoding.BinaryUnmarshaler = (*HashIdPreimageSorobanAuthorization)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s HashIdPreimageSorobanAuthorization) xdrType() {} var _ xdrType = (*HashIdPreimageSorobanAuthorization)(nil) @@ -28947,8 +29318,10 @@ func (s HashIdPreimage) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *HashIdPreimage) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -28957,8 +29330,7 @@ var ( _ encoding.BinaryUnmarshaler = (*HashIdPreimage)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s HashIdPreimage) xdrType() {} var _ xdrType = (*HashIdPreimage)(nil) @@ -29043,8 +29415,10 @@ func (s MemoType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *MemoType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -29053,8 +29427,7 @@ var ( _ encoding.BinaryUnmarshaler = (*MemoType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s MemoType) xdrType() {} var _ xdrType = (*MemoType)(nil) @@ -29344,8 +29717,10 @@ func (s Memo) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Memo) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -29354,8 +29729,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Memo)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Memo) xdrType() {} var _ xdrType = (*Memo)(nil) @@ -29418,8 +29792,10 @@ func (s TimeBounds) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TimeBounds) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -29428,8 +29804,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TimeBounds)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TimeBounds) xdrType() {} var _ xdrType = (*TimeBounds)(nil) @@ -29492,8 +29867,10 @@ func (s LedgerBounds) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerBounds) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -29502,8 +29879,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerBounds)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerBounds) xdrType() {} var _ xdrType = (*LedgerBounds)(nil) @@ -29669,8 +30045,11 @@ func (s *PreconditionsV2) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } s.ExtraSigners = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding SignerKey: length (%d) exceeds remaining input length (%d)", l, il) + } s.ExtraSigners = make([]SignerKey, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.ExtraSigners[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -29692,8 +30071,10 @@ func (s PreconditionsV2) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PreconditionsV2) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -29702,8 +30083,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PreconditionsV2)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PreconditionsV2) xdrType() {} var _ xdrType = (*PreconditionsV2)(nil) @@ -29782,8 +30162,10 @@ func (s PreconditionType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PreconditionType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -29792,8 +30174,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PreconditionType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PreconditionType) xdrType() {} var _ xdrType = (*PreconditionType)(nil) @@ -29983,8 +30364,10 @@ func (s Preconditions) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Preconditions) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -29993,8 +30376,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Preconditions)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Preconditions) xdrType() {} var _ xdrType = (*Preconditions)(nil) @@ -30051,8 +30433,11 @@ func (s *LedgerFootprint) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } s.ReadOnly = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding LedgerKey: length (%d) exceeds remaining input length (%d)", l, il) + } s.ReadOnly = make([]LedgerKey, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.ReadOnly[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -30067,8 +30452,11 @@ func (s *LedgerFootprint) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } s.ReadWrite = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding LedgerKey: length (%d) exceeds remaining input length (%d)", l, il) + } s.ReadWrite = make([]LedgerKey, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.ReadWrite[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -30090,8 +30478,10 @@ func (s LedgerFootprint) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LedgerFootprint) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -30100,8 +30490,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LedgerFootprint)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LedgerFootprint) xdrType() {} var _ xdrType = (*LedgerFootprint)(nil) @@ -30189,8 +30578,10 @@ func (s SorobanResources) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SorobanResources) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -30199,8 +30590,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SorobanResources)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SorobanResources) xdrType() {} var _ xdrType = (*SorobanResources)(nil) @@ -30282,8 +30672,10 @@ func (s SorobanTransactionData) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SorobanTransactionData) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -30292,8 +30684,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SorobanTransactionData)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SorobanTransactionData) xdrType() {} var _ xdrType = (*SorobanTransactionData)(nil) @@ -30383,8 +30774,10 @@ func (s TransactionV0Ext) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionV0Ext) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -30393,8 +30786,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionV0Ext)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionV0Ext) xdrType() {} var _ xdrType = (*TransactionV0Ext)(nil) @@ -30519,8 +30911,11 @@ func (s *TransactionV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { } s.Operations = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding Operation: length (%d) exceeds remaining input length (%d)", l, il) + } s.Operations = make([]Operation, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Operations[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -30547,8 +30942,10 @@ func (s TransactionV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -30557,8 +30954,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionV0) xdrType() {} var _ xdrType = (*TransactionV0)(nil) @@ -30620,8 +31016,11 @@ func (s *TransactionV0Envelope) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, } s.Signatures = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding DecoratedSignature: length (%d) exceeds remaining input length (%d)", l, il) + } s.Signatures = make([]DecoratedSignature, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Signatures[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -30643,8 +31042,10 @@ func (s TransactionV0Envelope) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionV0Envelope) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -30653,8 +31054,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionV0Envelope)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionV0Envelope) xdrType() {} var _ xdrType = (*TransactionV0Envelope)(nil) @@ -30794,8 +31194,10 @@ func (s TransactionExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -30804,8 +31206,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionExt) xdrType() {} var _ xdrType = (*TransactionExt)(nil) @@ -30928,8 +31329,11 @@ func (s *Transaction) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { } s.Operations = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding Operation: length (%d) exceeds remaining input length (%d)", l, il) + } s.Operations = make([]Operation, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Operations[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -30956,8 +31360,10 @@ func (s Transaction) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Transaction) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -30966,8 +31372,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Transaction)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Transaction) xdrType() {} var _ xdrType = (*Transaction)(nil) @@ -31029,8 +31434,11 @@ func (s *TransactionV1Envelope) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, } s.Signatures = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding DecoratedSignature: length (%d) exceeds remaining input length (%d)", l, il) + } s.Signatures = make([]DecoratedSignature, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Signatures[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -31052,8 +31460,10 @@ func (s TransactionV1Envelope) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionV1Envelope) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -31062,8 +31472,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionV1Envelope)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionV1Envelope) xdrType() {} var _ xdrType = (*TransactionV1Envelope)(nil) @@ -31191,8 +31600,10 @@ func (s FeeBumpTransactionInnerTx) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *FeeBumpTransactionInnerTx) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -31201,8 +31612,7 @@ var ( _ encoding.BinaryUnmarshaler = (*FeeBumpTransactionInnerTx)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s FeeBumpTransactionInnerTx) xdrType() {} var _ xdrType = (*FeeBumpTransactionInnerTx)(nil) @@ -31292,8 +31702,10 @@ func (s FeeBumpTransactionExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *FeeBumpTransactionExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -31302,8 +31714,7 @@ var ( _ encoding.BinaryUnmarshaler = (*FeeBumpTransactionExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s FeeBumpTransactionExt) xdrType() {} var _ xdrType = (*FeeBumpTransactionExt)(nil) @@ -31396,8 +31807,10 @@ func (s FeeBumpTransaction) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *FeeBumpTransaction) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -31406,8 +31819,7 @@ var ( _ encoding.BinaryUnmarshaler = (*FeeBumpTransaction)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s FeeBumpTransaction) xdrType() {} var _ xdrType = (*FeeBumpTransaction)(nil) @@ -31469,8 +31881,11 @@ func (s *FeeBumpTransactionEnvelope) DecodeFrom(d *xdr.Decoder, maxDepth uint) ( } s.Signatures = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding DecoratedSignature: length (%d) exceeds remaining input length (%d)", l, il) + } s.Signatures = make([]DecoratedSignature, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Signatures[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -31492,8 +31907,10 @@ func (s FeeBumpTransactionEnvelope) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *FeeBumpTransactionEnvelope) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -31502,8 +31919,7 @@ var ( _ encoding.BinaryUnmarshaler = (*FeeBumpTransactionEnvelope)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s FeeBumpTransactionEnvelope) xdrType() {} var _ xdrType = (*FeeBumpTransactionEnvelope)(nil) @@ -31731,8 +32147,10 @@ func (s TransactionEnvelope) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionEnvelope) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -31741,8 +32159,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionEnvelope)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionEnvelope) xdrType() {} var _ xdrType = (*TransactionEnvelope)(nil) @@ -31921,8 +32338,10 @@ func (s TransactionSignaturePayloadTaggedTransaction) MarshalBinary() ([]byte, e // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionSignaturePayloadTaggedTransaction) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -31931,8 +32350,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionSignaturePayloadTaggedTransaction)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionSignaturePayloadTaggedTransaction) xdrType() {} var _ xdrType = (*TransactionSignaturePayloadTaggedTransaction)(nil) @@ -32003,8 +32421,10 @@ func (s TransactionSignaturePayload) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionSignaturePayload) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -32013,8 +32433,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionSignaturePayload)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionSignaturePayload) xdrType() {} var _ xdrType = (*TransactionSignaturePayload)(nil) @@ -32093,8 +32512,10 @@ func (s ClaimAtomType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimAtomType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -32103,8 +32524,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimAtomType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimAtomType) xdrType() {} var _ xdrType = (*ClaimAtomType)(nil) @@ -32212,8 +32632,10 @@ func (s ClaimOfferAtomV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimOfferAtomV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -32222,8 +32644,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimOfferAtomV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimOfferAtomV0) xdrType() {} var _ xdrType = (*ClaimOfferAtomV0)(nil) @@ -32331,8 +32752,10 @@ func (s ClaimOfferAtom) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimOfferAtom) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -32341,8 +32764,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimOfferAtom)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimOfferAtom) xdrType() {} var _ xdrType = (*ClaimOfferAtom)(nil) @@ -32439,8 +32861,10 @@ func (s ClaimLiquidityAtom) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimLiquidityAtom) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -32449,8 +32873,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimLiquidityAtom)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimLiquidityAtom) xdrType() {} var _ xdrType = (*ClaimLiquidityAtom)(nil) @@ -32678,8 +33101,10 @@ func (s ClaimAtom) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimAtom) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -32688,8 +33113,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimAtom)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimAtom) xdrType() {} var _ xdrType = (*ClaimAtom)(nil) @@ -32778,8 +33202,10 @@ func (s CreateAccountResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *CreateAccountResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -32788,8 +33214,7 @@ var ( _ encoding.BinaryUnmarshaler = (*CreateAccountResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s CreateAccountResultCode) xdrType() {} var _ xdrType = (*CreateAccountResultCode)(nil) @@ -32924,8 +33349,10 @@ func (s CreateAccountResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *CreateAccountResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -32934,8 +33361,7 @@ var ( _ encoding.BinaryUnmarshaler = (*CreateAccountResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s CreateAccountResult) xdrType() {} var _ xdrType = (*CreateAccountResult)(nil) @@ -33038,8 +33464,10 @@ func (s PaymentResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PaymentResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -33048,8 +33476,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PaymentResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PaymentResultCode) xdrType() {} var _ xdrType = (*PaymentResultCode)(nil) @@ -33239,8 +33666,10 @@ func (s PaymentResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PaymentResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -33249,8 +33678,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PaymentResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PaymentResult) xdrType() {} var _ xdrType = (*PaymentResult)(nil) @@ -33371,8 +33799,10 @@ func (s PathPaymentStrictReceiveResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PathPaymentStrictReceiveResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -33381,8 +33811,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PathPaymentStrictReceiveResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PathPaymentStrictReceiveResultCode) xdrType() {} var _ xdrType = (*PathPaymentStrictReceiveResultCode)(nil) @@ -33455,8 +33884,10 @@ func (s SimplePaymentResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SimplePaymentResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -33465,8 +33896,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SimplePaymentResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SimplePaymentResult) xdrType() {} var _ xdrType = (*SimplePaymentResult)(nil) @@ -33518,8 +33948,11 @@ func (s *PathPaymentStrictReceiveResultSuccess) DecodeFrom(d *xdr.Decoder, maxDe } s.Offers = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ClaimAtom: length (%d) exceeds remaining input length (%d)", l, il) + } s.Offers = make([]ClaimAtom, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Offers[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -33546,8 +33979,10 @@ func (s PathPaymentStrictReceiveResultSuccess) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PathPaymentStrictReceiveResultSuccess) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -33556,8 +33991,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PathPaymentStrictReceiveResultSuccess)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PathPaymentStrictReceiveResultSuccess) xdrType() {} var _ xdrType = (*PathPaymentStrictReceiveResultSuccess)(nil) @@ -33863,8 +34297,10 @@ func (s PathPaymentStrictReceiveResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PathPaymentStrictReceiveResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -33873,8 +34309,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PathPaymentStrictReceiveResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PathPaymentStrictReceiveResult) xdrType() {} var _ xdrType = (*PathPaymentStrictReceiveResult)(nil) @@ -33994,8 +34429,10 @@ func (s PathPaymentStrictSendResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PathPaymentStrictSendResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -34004,8 +34441,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PathPaymentStrictSendResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PathPaymentStrictSendResultCode) xdrType() {} var _ xdrType = (*PathPaymentStrictSendResultCode)(nil) @@ -34057,8 +34493,11 @@ func (s *PathPaymentStrictSendResultSuccess) DecodeFrom(d *xdr.Decoder, maxDepth } s.Offers = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ClaimAtom: length (%d) exceeds remaining input length (%d)", l, il) + } s.Offers = make([]ClaimAtom, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Offers[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -34085,8 +34524,10 @@ func (s PathPaymentStrictSendResultSuccess) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PathPaymentStrictSendResultSuccess) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -34095,8 +34536,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PathPaymentStrictSendResultSuccess)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PathPaymentStrictSendResultSuccess) xdrType() {} var _ xdrType = (*PathPaymentStrictSendResultSuccess)(nil) @@ -34401,8 +34841,10 @@ func (s PathPaymentStrictSendResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PathPaymentStrictSendResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -34411,8 +34853,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PathPaymentStrictSendResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PathPaymentStrictSendResult) xdrType() {} var _ xdrType = (*PathPaymentStrictSendResult)(nil) @@ -34531,8 +34972,10 @@ func (s ManageSellOfferResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ManageSellOfferResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -34541,8 +34984,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ManageSellOfferResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ManageSellOfferResultCode) xdrType() {} var _ xdrType = (*ManageSellOfferResultCode)(nil) @@ -34621,8 +35063,10 @@ func (s ManageOfferEffect) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ManageOfferEffect) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -34631,8 +35075,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ManageOfferEffect)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ManageOfferEffect) xdrType() {} var _ xdrType = (*ManageOfferEffect)(nil) @@ -34795,8 +35238,10 @@ func (s ManageOfferSuccessResultOffer) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ManageOfferSuccessResultOffer) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -34805,8 +35250,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ManageOfferSuccessResultOffer)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ManageOfferSuccessResultOffer) xdrType() {} var _ xdrType = (*ManageOfferSuccessResultOffer)(nil) @@ -34868,8 +35312,11 @@ func (s *ManageOfferSuccessResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) (in } s.OffersClaimed = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ClaimAtom: length (%d) exceeds remaining input length (%d)", l, il) + } s.OffersClaimed = make([]ClaimAtom, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.OffersClaimed[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -34896,8 +35343,10 @@ func (s ManageOfferSuccessResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ManageOfferSuccessResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -34906,8 +35355,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ManageOfferSuccessResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ManageOfferSuccessResult) xdrType() {} var _ xdrType = (*ManageOfferSuccessResult)(nil) @@ -35168,8 +35616,10 @@ func (s ManageSellOfferResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ManageSellOfferResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -35178,8 +35628,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ManageSellOfferResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ManageSellOfferResult) xdrType() {} var _ xdrType = (*ManageSellOfferResult)(nil) @@ -35295,8 +35744,10 @@ func (s ManageBuyOfferResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ManageBuyOfferResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -35305,8 +35756,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ManageBuyOfferResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ManageBuyOfferResultCode) xdrType() {} var _ xdrType = (*ManageBuyOfferResultCode)(nil) @@ -35567,8 +36017,10 @@ func (s ManageBuyOfferResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ManageBuyOfferResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -35577,8 +36029,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ManageBuyOfferResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ManageBuyOfferResult) xdrType() {} var _ xdrType = (*ManageBuyOfferResult)(nil) @@ -35684,8 +36135,10 @@ func (s SetOptionsResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SetOptionsResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -35694,8 +36147,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SetOptionsResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SetOptionsResultCode) xdrType() {} var _ xdrType = (*SetOptionsResultCode)(nil) @@ -35896,8 +36348,10 @@ func (s SetOptionsResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SetOptionsResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -35906,8 +36360,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SetOptionsResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SetOptionsResult) xdrType() {} var _ xdrType = (*SetOptionsResult)(nil) @@ -36010,8 +36463,10 @@ func (s ChangeTrustResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ChangeTrustResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -36020,8 +36475,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ChangeTrustResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ChangeTrustResultCode) xdrType() {} var _ xdrType = (*ChangeTrustResultCode)(nil) @@ -36200,8 +36654,10 @@ func (s ChangeTrustResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ChangeTrustResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -36210,8 +36666,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ChangeTrustResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ChangeTrustResult) xdrType() {} var _ xdrType = (*ChangeTrustResult)(nil) @@ -36306,8 +36761,10 @@ func (s AllowTrustResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AllowTrustResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -36316,8 +36773,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AllowTrustResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AllowTrustResultCode) xdrType() {} var _ xdrType = (*AllowTrustResultCode)(nil) @@ -36474,8 +36930,10 @@ func (s AllowTrustResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AllowTrustResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -36484,8 +36942,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AllowTrustResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AllowTrustResult) xdrType() {} var _ xdrType = (*AllowTrustResult)(nil) @@ -36582,8 +37039,10 @@ func (s AccountMergeResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AccountMergeResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -36592,8 +37051,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AccountMergeResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AccountMergeResultCode) xdrType() {} var _ xdrType = (*AccountMergeResultCode)(nil) @@ -36799,8 +37257,10 @@ func (s AccountMergeResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AccountMergeResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -36809,8 +37269,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AccountMergeResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AccountMergeResult) xdrType() {} var _ xdrType = (*AccountMergeResult)(nil) @@ -36888,8 +37347,10 @@ func (s InflationResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *InflationResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -36898,8 +37359,7 @@ var ( _ encoding.BinaryUnmarshaler = (*InflationResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s InflationResultCode) xdrType() {} var _ xdrType = (*InflationResultCode)(nil) @@ -36962,8 +37422,10 @@ func (s InflationPayout) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *InflationPayout) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -36972,8 +37434,7 @@ var ( _ encoding.BinaryUnmarshaler = (*InflationPayout)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s InflationPayout) xdrType() {} var _ xdrType = (*InflationPayout)(nil) @@ -37102,8 +37563,11 @@ func (u *InflationResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } (*u.Payouts) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding InflationPayout: length (%d) exceeds remaining input length (%d)", l, il) + } (*u.Payouts) = make([]InflationPayout, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*u.Payouts)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -37130,8 +37594,10 @@ func (s InflationResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *InflationResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -37140,8 +37606,7 @@ var ( _ encoding.BinaryUnmarshaler = (*InflationResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s InflationResult) xdrType() {} var _ xdrType = (*InflationResult)(nil) @@ -37230,8 +37695,10 @@ func (s ManageDataResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ManageDataResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -37240,8 +37707,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ManageDataResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ManageDataResultCode) xdrType() {} var _ xdrType = (*ManageDataResultCode)(nil) @@ -37376,8 +37842,10 @@ func (s ManageDataResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ManageDataResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -37386,8 +37854,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ManageDataResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ManageDataResult) xdrType() {} var _ xdrType = (*ManageDataResult)(nil) @@ -37465,8 +37932,10 @@ func (s BumpSequenceResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *BumpSequenceResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -37475,8 +37944,7 @@ var ( _ encoding.BinaryUnmarshaler = (*BumpSequenceResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s BumpSequenceResultCode) xdrType() {} var _ xdrType = (*BumpSequenceResultCode)(nil) @@ -37578,8 +38046,10 @@ func (s BumpSequenceResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *BumpSequenceResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -37588,8 +38058,7 @@ var ( _ encoding.BinaryUnmarshaler = (*BumpSequenceResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s BumpSequenceResult) xdrType() {} var _ xdrType = (*BumpSequenceResult)(nil) @@ -37677,8 +38146,10 @@ func (s CreateClaimableBalanceResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *CreateClaimableBalanceResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -37687,8 +38158,7 @@ var ( _ encoding.BinaryUnmarshaler = (*CreateClaimableBalanceResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s CreateClaimableBalanceResultCode) xdrType() {} var _ xdrType = (*CreateClaimableBalanceResultCode)(nil) @@ -37873,8 +38343,10 @@ func (s CreateClaimableBalanceResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *CreateClaimableBalanceResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -37883,8 +38355,7 @@ var ( _ encoding.BinaryUnmarshaler = (*CreateClaimableBalanceResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s CreateClaimableBalanceResult) xdrType() {} var _ xdrType = (*CreateClaimableBalanceResult)(nil) @@ -37972,8 +38443,10 @@ func (s ClaimClaimableBalanceResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimClaimableBalanceResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -37982,8 +38455,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimClaimableBalanceResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimClaimableBalanceResultCode) xdrType() {} var _ xdrType = (*ClaimClaimableBalanceResultCode)(nil) @@ -38129,8 +38601,10 @@ func (s ClaimClaimableBalanceResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClaimClaimableBalanceResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -38139,8 +38613,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClaimClaimableBalanceResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClaimClaimableBalanceResult) xdrType() {} var _ xdrType = (*ClaimClaimableBalanceResult)(nil) @@ -38225,8 +38698,10 @@ func (s BeginSponsoringFutureReservesResultCode) MarshalBinary() ([]byte, error) // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *BeginSponsoringFutureReservesResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -38235,8 +38710,7 @@ var ( _ encoding.BinaryUnmarshaler = (*BeginSponsoringFutureReservesResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s BeginSponsoringFutureReservesResultCode) xdrType() {} var _ xdrType = (*BeginSponsoringFutureReservesResultCode)(nil) @@ -38361,8 +38835,10 @@ func (s BeginSponsoringFutureReservesResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *BeginSponsoringFutureReservesResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -38371,8 +38847,7 @@ var ( _ encoding.BinaryUnmarshaler = (*BeginSponsoringFutureReservesResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s BeginSponsoringFutureReservesResult) xdrType() {} var _ xdrType = (*BeginSponsoringFutureReservesResult)(nil) @@ -38451,8 +38926,10 @@ func (s EndSponsoringFutureReservesResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *EndSponsoringFutureReservesResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -38461,8 +38938,7 @@ var ( _ encoding.BinaryUnmarshaler = (*EndSponsoringFutureReservesResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s EndSponsoringFutureReservesResultCode) xdrType() {} var _ xdrType = (*EndSponsoringFutureReservesResultCode)(nil) @@ -38565,8 +39041,10 @@ func (s EndSponsoringFutureReservesResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *EndSponsoringFutureReservesResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -38575,8 +39053,7 @@ var ( _ encoding.BinaryUnmarshaler = (*EndSponsoringFutureReservesResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s EndSponsoringFutureReservesResult) xdrType() {} var _ xdrType = (*EndSponsoringFutureReservesResult)(nil) @@ -38667,8 +39144,10 @@ func (s RevokeSponsorshipResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *RevokeSponsorshipResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -38677,8 +39156,7 @@ var ( _ encoding.BinaryUnmarshaler = (*RevokeSponsorshipResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s RevokeSponsorshipResultCode) xdrType() {} var _ xdrType = (*RevokeSponsorshipResultCode)(nil) @@ -38824,8 +39302,10 @@ func (s RevokeSponsorshipResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *RevokeSponsorshipResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -38834,8 +39314,7 @@ var ( _ encoding.BinaryUnmarshaler = (*RevokeSponsorshipResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s RevokeSponsorshipResult) xdrType() {} var _ xdrType = (*RevokeSponsorshipResult)(nil) @@ -38923,8 +39402,10 @@ func (s ClawbackResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClawbackResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -38933,8 +39414,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClawbackResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClawbackResultCode) xdrType() {} var _ xdrType = (*ClawbackResultCode)(nil) @@ -39069,8 +39549,10 @@ func (s ClawbackResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClawbackResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -39079,8 +39561,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClawbackResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClawbackResult) xdrType() {} var _ xdrType = (*ClawbackResult)(nil) @@ -39165,8 +39646,10 @@ func (s ClawbackClaimableBalanceResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClawbackClaimableBalanceResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -39175,8 +39658,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClawbackClaimableBalanceResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClawbackClaimableBalanceResultCode) xdrType() {} var _ xdrType = (*ClawbackClaimableBalanceResultCode)(nil) @@ -39301,8 +39783,10 @@ func (s ClawbackClaimableBalanceResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ClawbackClaimableBalanceResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -39311,8 +39795,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ClawbackClaimableBalanceResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ClawbackClaimableBalanceResult) xdrType() {} var _ xdrType = (*ClawbackClaimableBalanceResult)(nil) @@ -39404,8 +39887,10 @@ func (s SetTrustLineFlagsResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SetTrustLineFlagsResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -39414,8 +39899,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SetTrustLineFlagsResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SetTrustLineFlagsResultCode) xdrType() {} var _ xdrType = (*SetTrustLineFlagsResultCode)(nil) @@ -39561,8 +40045,10 @@ func (s SetTrustLineFlagsResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SetTrustLineFlagsResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -39571,8 +40057,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SetTrustLineFlagsResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SetTrustLineFlagsResult) xdrType() {} var _ xdrType = (*SetTrustLineFlagsResult)(nil) @@ -39673,8 +40158,10 @@ func (s LiquidityPoolDepositResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LiquidityPoolDepositResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -39683,8 +40170,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LiquidityPoolDepositResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LiquidityPoolDepositResultCode) xdrType() {} var _ xdrType = (*LiquidityPoolDepositResultCode)(nil) @@ -39852,8 +40338,10 @@ func (s LiquidityPoolDepositResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LiquidityPoolDepositResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -39862,8 +40350,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LiquidityPoolDepositResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LiquidityPoolDepositResult) xdrType() {} var _ xdrType = (*LiquidityPoolDepositResult)(nil) @@ -39957,8 +40444,10 @@ func (s LiquidityPoolWithdrawResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LiquidityPoolWithdrawResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -39967,8 +40456,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LiquidityPoolWithdrawResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LiquidityPoolWithdrawResultCode) xdrType() {} var _ xdrType = (*LiquidityPoolWithdrawResultCode)(nil) @@ -40114,8 +40602,10 @@ func (s LiquidityPoolWithdrawResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *LiquidityPoolWithdrawResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -40124,8 +40614,7 @@ var ( _ encoding.BinaryUnmarshaler = (*LiquidityPoolWithdrawResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s LiquidityPoolWithdrawResult) xdrType() {} var _ xdrType = (*LiquidityPoolWithdrawResult)(nil) @@ -40216,8 +40705,10 @@ func (s InvokeHostFunctionResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *InvokeHostFunctionResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -40226,8 +40717,7 @@ var ( _ encoding.BinaryUnmarshaler = (*InvokeHostFunctionResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s InvokeHostFunctionResultCode) xdrType() {} var _ xdrType = (*InvokeHostFunctionResultCode)(nil) @@ -40411,8 +40901,10 @@ func (s InvokeHostFunctionResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *InvokeHostFunctionResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -40421,8 +40913,7 @@ var ( _ encoding.BinaryUnmarshaler = (*InvokeHostFunctionResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s InvokeHostFunctionResult) xdrType() {} var _ xdrType = (*InvokeHostFunctionResult)(nil) @@ -40507,8 +40998,10 @@ func (s ExtendFootprintTtlResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ExtendFootprintTtlResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -40517,8 +41010,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ExtendFootprintTtlResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ExtendFootprintTtlResultCode) xdrType() {} var _ xdrType = (*ExtendFootprintTtlResultCode)(nil) @@ -40642,8 +41134,10 @@ func (s ExtendFootprintTtlResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ExtendFootprintTtlResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -40652,8 +41146,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ExtendFootprintTtlResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ExtendFootprintTtlResult) xdrType() {} var _ xdrType = (*ExtendFootprintTtlResult)(nil) @@ -40738,8 +41231,10 @@ func (s RestoreFootprintResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *RestoreFootprintResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -40748,8 +41243,7 @@ var ( _ encoding.BinaryUnmarshaler = (*RestoreFootprintResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s RestoreFootprintResultCode) xdrType() {} var _ xdrType = (*RestoreFootprintResultCode)(nil) @@ -40873,8 +41367,10 @@ func (s RestoreFootprintResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *RestoreFootprintResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -40883,8 +41379,7 @@ var ( _ encoding.BinaryUnmarshaler = (*RestoreFootprintResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s RestoreFootprintResult) xdrType() {} var _ xdrType = (*RestoreFootprintResult)(nil) @@ -40976,8 +41471,10 @@ func (s OperationResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *OperationResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -40986,8 +41483,7 @@ var ( _ encoding.BinaryUnmarshaler = (*OperationResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s OperationResultCode) xdrType() {} var _ xdrType = (*OperationResultCode)(nil) @@ -42415,8 +42911,10 @@ func (s OperationResultTr) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *OperationResultTr) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -42425,8 +42923,7 @@ var ( _ encoding.BinaryUnmarshaler = (*OperationResultTr)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s OperationResultTr) xdrType() {} var _ xdrType = (*OperationResultTr)(nil) @@ -42678,8 +43175,10 @@ func (s OperationResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *OperationResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -42688,8 +43187,7 @@ var ( _ encoding.BinaryUnmarshaler = (*OperationResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s OperationResult) xdrType() {} var _ xdrType = (*OperationResult)(nil) @@ -42820,8 +43318,10 @@ func (s TransactionResultCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionResultCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -42830,8 +43330,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionResultCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionResultCode) xdrType() {} var _ xdrType = (*TransactionResultCode)(nil) @@ -43094,8 +43593,11 @@ func (u *InnerTransactionResultResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) } (*u.Results) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding OperationResult: length (%d) exceeds remaining input length (%d)", l, il) + } (*u.Results) = make([]OperationResult, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*u.Results)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -43114,8 +43616,11 @@ func (u *InnerTransactionResultResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) } (*u.Results) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding OperationResult: length (%d) exceeds remaining input length (%d)", l, il) + } (*u.Results) = make([]OperationResult, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*u.Results)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -43184,8 +43689,10 @@ func (s InnerTransactionResultResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *InnerTransactionResultResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -43194,8 +43701,7 @@ var ( _ encoding.BinaryUnmarshaler = (*InnerTransactionResultResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s InnerTransactionResultResult) xdrType() {} var _ xdrType = (*InnerTransactionResultResult)(nil) @@ -43285,8 +43791,10 @@ func (s InnerTransactionResultExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *InnerTransactionResultExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -43295,8 +43803,7 @@ var ( _ encoding.BinaryUnmarshaler = (*InnerTransactionResultExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s InnerTransactionResultExt) xdrType() {} var _ xdrType = (*InnerTransactionResultExt)(nil) @@ -43402,8 +43909,10 @@ func (s InnerTransactionResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *InnerTransactionResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -43412,8 +43921,7 @@ var ( _ encoding.BinaryUnmarshaler = (*InnerTransactionResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s InnerTransactionResult) xdrType() {} var _ xdrType = (*InnerTransactionResult)(nil) @@ -43476,8 +43984,10 @@ func (s InnerTransactionResultPair) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *InnerTransactionResultPair) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -43486,8 +43996,7 @@ var ( _ encoding.BinaryUnmarshaler = (*InnerTransactionResultPair)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s InnerTransactionResultPair) xdrType() {} var _ xdrType = (*InnerTransactionResultPair)(nil) @@ -43822,8 +44331,11 @@ func (u *TransactionResultResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int } (*u.Results) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding OperationResult: length (%d) exceeds remaining input length (%d)", l, il) + } (*u.Results) = make([]OperationResult, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*u.Results)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -43842,8 +44354,11 @@ func (u *TransactionResultResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int } (*u.Results) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding OperationResult: length (%d) exceeds remaining input length (%d)", l, il) + } (*u.Results) = make([]OperationResult, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*u.Results)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -43912,8 +44427,10 @@ func (s TransactionResultResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionResultResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -43922,8 +44439,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionResultResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionResultResult) xdrType() {} var _ xdrType = (*TransactionResultResult)(nil) @@ -44013,8 +44529,10 @@ func (s TransactionResultExt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionResultExt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -44023,8 +44541,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionResultExt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionResultExt) xdrType() {} var _ xdrType = (*TransactionResultExt)(nil) @@ -44131,8 +44648,10 @@ func (s TransactionResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TransactionResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -44141,8 +44660,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TransactionResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TransactionResult) xdrType() {} var _ xdrType = (*TransactionResult)(nil) @@ -44195,8 +44713,10 @@ func (s Hash) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Hash) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -44205,8 +44725,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Hash)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Hash) xdrType() {} var _ xdrType = (*Hash)(nil) @@ -44259,8 +44778,10 @@ func (s Uint256) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Uint256) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -44269,8 +44790,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Uint256)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Uint256) xdrType() {} var _ xdrType = (*Uint256)(nil) @@ -44320,8 +44840,10 @@ func (s Uint32) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Uint32) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -44330,8 +44852,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Uint32)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Uint32) xdrType() {} var _ xdrType = (*Uint32)(nil) @@ -44381,8 +44902,10 @@ func (s Int32) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Int32) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -44391,8 +44914,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Int32)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Int32) xdrType() {} var _ xdrType = (*Int32)(nil) @@ -44442,8 +44964,10 @@ func (s Uint64) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Uint64) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -44452,8 +44976,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Uint64)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Uint64) xdrType() {} var _ xdrType = (*Uint64)(nil) @@ -44503,8 +45026,10 @@ func (s Int64) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Int64) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -44513,8 +45038,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Int64)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Int64) xdrType() {} var _ xdrType = (*Int64)(nil) @@ -44562,8 +45086,10 @@ func (s TimePoint) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *TimePoint) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -44572,8 +45098,7 @@ var ( _ encoding.BinaryUnmarshaler = (*TimePoint)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s TimePoint) xdrType() {} var _ xdrType = (*TimePoint)(nil) @@ -44621,8 +45146,10 @@ func (s Duration) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Duration) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -44631,8 +45158,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Duration)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Duration) xdrType() {} var _ xdrType = (*Duration)(nil) @@ -44722,8 +45248,10 @@ func (s ExtensionPoint) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ExtensionPoint) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -44732,8 +45260,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ExtensionPoint)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ExtensionPoint) xdrType() {} var _ xdrType = (*ExtensionPoint)(nil) @@ -44820,8 +45347,10 @@ func (s CryptoKeyType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *CryptoKeyType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -44830,8 +45359,7 @@ var ( _ encoding.BinaryUnmarshaler = (*CryptoKeyType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s CryptoKeyType) xdrType() {} var _ xdrType = (*CryptoKeyType)(nil) @@ -44904,8 +45432,10 @@ func (s PublicKeyType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PublicKeyType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -44914,8 +45444,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PublicKeyType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PublicKeyType) xdrType() {} var _ xdrType = (*PublicKeyType)(nil) @@ -44997,8 +45526,10 @@ func (s SignerKeyType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SignerKeyType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -45007,8 +45538,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SignerKeyType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SignerKeyType) xdrType() {} var _ xdrType = (*SignerKeyType)(nil) @@ -45136,8 +45666,10 @@ func (s PublicKey) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PublicKey) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -45146,8 +45678,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PublicKey)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PublicKey) xdrType() {} var _ xdrType = (*PublicKey)(nil) @@ -45212,8 +45743,10 @@ func (s SignerKeyEd25519SignedPayload) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SignerKeyEd25519SignedPayload) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -45222,8 +45755,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SignerKeyEd25519SignedPayload)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SignerKeyEd25519SignedPayload) xdrType() {} var _ xdrType = (*SignerKeyEd25519SignedPayload)(nil) @@ -45509,8 +46041,10 @@ func (s SignerKey) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SignerKey) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -45519,8 +46053,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SignerKey)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SignerKey) xdrType() {} var _ xdrType = (*SignerKey)(nil) @@ -45573,8 +46106,10 @@ func (s Signature) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Signature) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -45583,8 +46118,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Signature)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Signature) xdrType() {} var _ xdrType = (*Signature)(nil) @@ -45637,8 +46171,10 @@ func (s SignatureHint) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *SignatureHint) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -45647,8 +46183,7 @@ var ( _ encoding.BinaryUnmarshaler = (*SignatureHint)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s SignatureHint) xdrType() {} var _ xdrType = (*SignatureHint)(nil) @@ -45727,8 +46262,10 @@ func (s NodeId) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *NodeId) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -45737,8 +46274,7 @@ var ( _ encoding.BinaryUnmarshaler = (*NodeId)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s NodeId) xdrType() {} var _ xdrType = (*NodeId)(nil) @@ -45817,8 +46353,10 @@ func (s AccountId) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *AccountId) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -45827,8 +46365,7 @@ var ( _ encoding.BinaryUnmarshaler = (*AccountId)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s AccountId) xdrType() {} var _ xdrType = (*AccountId)(nil) @@ -45881,8 +46418,10 @@ func (s Curve25519Secret) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Curve25519Secret) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -45891,8 +46430,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Curve25519Secret)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Curve25519Secret) xdrType() {} var _ xdrType = (*Curve25519Secret)(nil) @@ -45945,8 +46483,10 @@ func (s Curve25519Public) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Curve25519Public) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -45955,8 +46495,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Curve25519Public)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Curve25519Public) xdrType() {} var _ xdrType = (*Curve25519Public)(nil) @@ -46009,8 +46548,10 @@ func (s HmacSha256Key) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *HmacSha256Key) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -46019,8 +46560,7 @@ var ( _ encoding.BinaryUnmarshaler = (*HmacSha256Key)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s HmacSha256Key) xdrType() {} var _ xdrType = (*HmacSha256Key)(nil) @@ -46073,8 +46613,10 @@ func (s HmacSha256Mac) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *HmacSha256Mac) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -46083,8 +46625,7 @@ var ( _ encoding.BinaryUnmarshaler = (*HmacSha256Mac)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s HmacSha256Mac) xdrType() {} var _ xdrType = (*HmacSha256Mac)(nil) @@ -46157,8 +46698,10 @@ func (s ScEnvMetaKind) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScEnvMetaKind) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -46167,8 +46710,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScEnvMetaKind)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScEnvMetaKind) xdrType() {} var _ xdrType = (*ScEnvMetaKind)(nil) @@ -46296,8 +46838,10 @@ func (s ScEnvMetaEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScEnvMetaEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -46306,8 +46850,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScEnvMetaEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScEnvMetaEntry) xdrType() {} var _ xdrType = (*ScEnvMetaEntry)(nil) @@ -46370,8 +46913,10 @@ func (s ScMetaV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScMetaV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -46380,8 +46925,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScMetaV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScMetaV0) xdrType() {} var _ xdrType = (*ScMetaV0)(nil) @@ -46454,8 +46998,10 @@ func (s ScMetaKind) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScMetaKind) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -46464,8 +47010,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScMetaKind)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScMetaKind) xdrType() {} var _ xdrType = (*ScMetaKind)(nil) @@ -46593,8 +47138,10 @@ func (s ScMetaEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScMetaEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -46603,8 +47150,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScMetaEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScMetaEntry) xdrType() {} var _ xdrType = (*ScMetaEntry)(nil) @@ -46760,8 +47306,10 @@ func (s ScSpecType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -46770,8 +47318,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecType) xdrType() {} var _ xdrType = (*ScSpecType)(nil) @@ -46824,8 +47371,10 @@ func (s ScSpecTypeOption) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecTypeOption) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -46834,8 +47383,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecTypeOption)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecTypeOption) xdrType() {} var _ xdrType = (*ScSpecTypeOption)(nil) @@ -46898,8 +47446,10 @@ func (s ScSpecTypeResult) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecTypeResult) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -46908,8 +47458,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecTypeResult)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecTypeResult) xdrType() {} var _ xdrType = (*ScSpecTypeResult)(nil) @@ -46962,8 +47511,10 @@ func (s ScSpecTypeVec) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecTypeVec) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -46972,8 +47523,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecTypeVec)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecTypeVec) xdrType() {} var _ xdrType = (*ScSpecTypeVec)(nil) @@ -47036,8 +47586,10 @@ func (s ScSpecTypeMap) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecTypeMap) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -47046,8 +47598,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecTypeMap)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecTypeMap) xdrType() {} var _ xdrType = (*ScSpecTypeMap)(nil) @@ -47097,8 +47648,11 @@ func (s *ScSpecTypeTuple) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } s.ValueTypes = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScSpecTypeDef: length (%d) exceeds remaining input length (%d)", l, il) + } s.ValueTypes = make([]ScSpecTypeDef, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.ValueTypes[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -47120,8 +47674,10 @@ func (s ScSpecTypeTuple) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecTypeTuple) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -47130,8 +47686,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecTypeTuple)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecTypeTuple) xdrType() {} var _ xdrType = (*ScSpecTypeTuple)(nil) @@ -47184,8 +47739,10 @@ func (s ScSpecTypeBytesN) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecTypeBytesN) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -47194,8 +47751,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecTypeBytesN)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecTypeBytesN) xdrType() {} var _ xdrType = (*ScSpecTypeBytesN)(nil) @@ -47248,8 +47804,10 @@ func (s ScSpecTypeUdt) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecTypeUdt) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -47258,8 +47816,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecTypeUdt)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecTypeUdt) xdrType() {} var _ xdrType = (*ScSpecTypeUdt)(nil) @@ -47886,8 +48443,10 @@ func (s ScSpecTypeDef) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecTypeDef) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -47896,8 +48455,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecTypeDef)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecTypeDef) xdrType() {} var _ xdrType = (*ScSpecTypeDef)(nil) @@ -47970,8 +48528,10 @@ func (s ScSpecUdtStructFieldV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecUdtStructFieldV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -47980,8 +48540,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecUdtStructFieldV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecUdtStructFieldV0) xdrType() {} var _ xdrType = (*ScSpecUdtStructFieldV0)(nil) @@ -48061,8 +48620,11 @@ func (s *ScSpecUdtStructV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro } s.Fields = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScSpecUdtStructFieldV0: length (%d) exceeds remaining input length (%d)", l, il) + } s.Fields = make([]ScSpecUdtStructFieldV0, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Fields[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -48084,8 +48646,10 @@ func (s ScSpecUdtStructV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecUdtStructV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -48094,8 +48658,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecUdtStructV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecUdtStructV0) xdrType() {} var _ xdrType = (*ScSpecUdtStructV0)(nil) @@ -48158,8 +48721,10 @@ func (s ScSpecUdtUnionCaseVoidV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecUdtUnionCaseVoidV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -48168,8 +48733,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecUdtUnionCaseVoidV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecUdtUnionCaseVoidV0) xdrType() {} var _ xdrType = (*ScSpecUdtUnionCaseVoidV0)(nil) @@ -48239,8 +48803,11 @@ func (s *ScSpecUdtUnionCaseTupleV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (i } s.Type = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScSpecTypeDef: length (%d) exceeds remaining input length (%d)", l, il) + } s.Type = make([]ScSpecTypeDef, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Type[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -48262,8 +48829,10 @@ func (s ScSpecUdtUnionCaseTupleV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecUdtUnionCaseTupleV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -48272,8 +48841,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecUdtUnionCaseTupleV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecUdtUnionCaseTupleV0) xdrType() {} var _ xdrType = (*ScSpecUdtUnionCaseTupleV0)(nil) @@ -48349,8 +48917,10 @@ func (s ScSpecUdtUnionCaseV0Kind) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecUdtUnionCaseV0Kind) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -48359,8 +48929,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecUdtUnionCaseV0Kind)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecUdtUnionCaseV0Kind) xdrType() {} var _ xdrType = (*ScSpecUdtUnionCaseV0Kind)(nil) @@ -48538,8 +49107,10 @@ func (s ScSpecUdtUnionCaseV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecUdtUnionCaseV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -48548,8 +49119,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecUdtUnionCaseV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecUdtUnionCaseV0) xdrType() {} var _ xdrType = (*ScSpecUdtUnionCaseV0)(nil) @@ -48629,8 +49199,11 @@ func (s *ScSpecUdtUnionV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error } s.Cases = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScSpecUdtUnionCaseV0: length (%d) exceeds remaining input length (%d)", l, il) + } s.Cases = make([]ScSpecUdtUnionCaseV0, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Cases[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -48652,8 +49225,10 @@ func (s ScSpecUdtUnionV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecUdtUnionV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -48662,8 +49237,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecUdtUnionV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecUdtUnionV0) xdrType() {} var _ xdrType = (*ScSpecUdtUnionV0)(nil) @@ -48736,8 +49310,10 @@ func (s ScSpecUdtEnumCaseV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecUdtEnumCaseV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -48746,8 +49322,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecUdtEnumCaseV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecUdtEnumCaseV0) xdrType() {} var _ xdrType = (*ScSpecUdtEnumCaseV0)(nil) @@ -48827,8 +49402,11 @@ func (s *ScSpecUdtEnumV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) } s.Cases = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScSpecUdtEnumCaseV0: length (%d) exceeds remaining input length (%d)", l, il) + } s.Cases = make([]ScSpecUdtEnumCaseV0, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Cases[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -48850,8 +49428,10 @@ func (s ScSpecUdtEnumV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecUdtEnumV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -48860,8 +49440,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecUdtEnumV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecUdtEnumV0) xdrType() {} var _ xdrType = (*ScSpecUdtEnumV0)(nil) @@ -48934,8 +49513,10 @@ func (s ScSpecUdtErrorEnumCaseV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecUdtErrorEnumCaseV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -48944,8 +49525,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecUdtErrorEnumCaseV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecUdtErrorEnumCaseV0) xdrType() {} var _ xdrType = (*ScSpecUdtErrorEnumCaseV0)(nil) @@ -49025,8 +49605,11 @@ func (s *ScSpecUdtErrorEnumV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, e } s.Cases = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScSpecUdtErrorEnumCaseV0: length (%d) exceeds remaining input length (%d)", l, il) + } s.Cases = make([]ScSpecUdtErrorEnumCaseV0, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Cases[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -49048,8 +49631,10 @@ func (s ScSpecUdtErrorEnumV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecUdtErrorEnumV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -49058,8 +49643,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecUdtErrorEnumV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecUdtErrorEnumV0) xdrType() {} var _ xdrType = (*ScSpecUdtErrorEnumV0)(nil) @@ -49132,8 +49716,10 @@ func (s ScSpecFunctionInputV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecFunctionInputV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -49142,8 +49728,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecFunctionInputV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecFunctionInputV0) xdrType() {} var _ xdrType = (*ScSpecFunctionInputV0)(nil) @@ -49223,8 +49808,11 @@ func (s *ScSpecFunctionV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error } s.Inputs = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScSpecFunctionInputV0: length (%d) exceeds remaining input length (%d)", l, il) + } s.Inputs = make([]ScSpecFunctionInputV0, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Inputs[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -49242,8 +49830,11 @@ func (s *ScSpecFunctionV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error } s.Outputs = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScSpecTypeDef: length (%d) exceeds remaining input length (%d)", l, il) + } s.Outputs = make([]ScSpecTypeDef, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.Outputs[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -49265,8 +49856,10 @@ func (s ScSpecFunctionV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecFunctionV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -49275,8 +49868,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecFunctionV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecFunctionV0) xdrType() {} var _ xdrType = (*ScSpecFunctionV0)(nil) @@ -49361,8 +49953,10 @@ func (s ScSpecEntryKind) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecEntryKind) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -49371,8 +49965,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecEntryKind)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecEntryKind) xdrType() {} var _ xdrType = (*ScSpecEntryKind)(nil) @@ -49700,8 +50293,10 @@ func (s ScSpecEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSpecEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -49710,8 +50305,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSpecEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSpecEntry) xdrType() {} var _ xdrType = (*ScSpecEntry)(nil) @@ -49876,8 +50470,10 @@ func (s ScValType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScValType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -49886,8 +50482,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScValType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScValType) xdrType() {} var _ xdrType = (*ScValType)(nil) @@ -49987,8 +50582,10 @@ func (s ScErrorType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScErrorType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -49997,8 +50594,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScErrorType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScErrorType) xdrType() {} var _ xdrType = (*ScErrorType)(nil) @@ -50098,8 +50694,10 @@ func (s ScErrorCode) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScErrorCode) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -50108,8 +50706,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScErrorCode)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScErrorCode) xdrType() {} var _ xdrType = (*ScErrorCode)(nil) @@ -50471,8 +51068,10 @@ func (s ScError) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScError) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -50481,8 +51080,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScError)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScError) xdrType() {} var _ xdrType = (*ScError)(nil) @@ -50544,8 +51142,10 @@ func (s UInt128Parts) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *UInt128Parts) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -50554,8 +51154,7 @@ var ( _ encoding.BinaryUnmarshaler = (*UInt128Parts)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s UInt128Parts) xdrType() {} var _ xdrType = (*UInt128Parts)(nil) @@ -50617,8 +51216,10 @@ func (s Int128Parts) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Int128Parts) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -50627,8 +51228,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Int128Parts)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Int128Parts) xdrType() {} var _ xdrType = (*Int128Parts)(nil) @@ -50710,8 +51310,10 @@ func (s UInt256Parts) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *UInt256Parts) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -50720,8 +51322,7 @@ var ( _ encoding.BinaryUnmarshaler = (*UInt256Parts)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s UInt256Parts) xdrType() {} var _ xdrType = (*UInt256Parts)(nil) @@ -50803,8 +51404,10 @@ func (s Int256Parts) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *Int256Parts) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -50813,8 +51416,7 @@ var ( _ encoding.BinaryUnmarshaler = (*Int256Parts)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s Int256Parts) xdrType() {} var _ xdrType = (*Int256Parts)(nil) @@ -50890,8 +51492,10 @@ func (s ContractExecutableType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractExecutableType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -50900,8 +51504,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractExecutableType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractExecutableType) xdrType() {} var _ xdrType = (*ContractExecutableType)(nil) @@ -51041,8 +51644,10 @@ func (s ContractExecutable) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractExecutable) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -51051,8 +51656,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractExecutable)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractExecutable) xdrType() {} var _ xdrType = (*ContractExecutable)(nil) @@ -51128,8 +51732,10 @@ func (s ScAddressType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScAddressType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -51138,8 +51744,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScAddressType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScAddressType) xdrType() {} var _ xdrType = (*ScAddressType)(nil) @@ -51317,8 +51922,10 @@ func (s ScAddress) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScAddress) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -51327,8 +51934,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScAddress)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScAddress) xdrType() {} var _ xdrType = (*ScAddress)(nil) @@ -51375,8 +51981,11 @@ func (s *ScVec) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { } (*s) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScVal: length (%d) exceeds remaining input length (%d)", l, il) + } (*s) = make([]ScVal, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -51398,8 +52007,10 @@ func (s ScVec) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScVec) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -51408,8 +52019,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScVec)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScVec) xdrType() {} var _ xdrType = (*ScVec)(nil) @@ -51451,8 +52061,11 @@ func (s *ScMap) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { } (*s) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScMapEntry: length (%d) exceeds remaining input length (%d)", l, il) + } (*s) = make([]ScMapEntry, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -51474,8 +52087,10 @@ func (s ScMap) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScMap) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -51484,8 +52099,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScMap)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScMap) xdrType() {} var _ xdrType = (*ScMap)(nil) @@ -51533,8 +52147,10 @@ func (s ScBytes) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScBytes) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -51543,8 +52159,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScBytes)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScBytes) xdrType() {} var _ xdrType = (*ScBytes)(nil) @@ -51594,8 +52209,10 @@ func (s ScString) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScString) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -51604,8 +52221,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScString)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScString) xdrType() {} var _ xdrType = (*ScString)(nil) @@ -51660,8 +52276,10 @@ func (s ScSymbol) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScSymbol) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -51670,8 +52288,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScSymbol)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScSymbol) xdrType() {} var _ xdrType = (*ScSymbol)(nil) @@ -51723,8 +52340,10 @@ func (s ScNonceKey) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScNonceKey) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -51733,8 +52352,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScNonceKey)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScNonceKey) xdrType() {} var _ xdrType = (*ScNonceKey)(nil) @@ -51811,8 +52429,10 @@ func (s ScContractInstance) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScContractInstance) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -51821,8 +52441,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScContractInstance)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScContractInstance) xdrType() {} var _ xdrType = (*ScContractInstance)(nil) @@ -52968,8 +53587,10 @@ func (s ScVal) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScVal) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -52978,8 +53599,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScVal)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScVal) xdrType() {} var _ xdrType = (*ScVal)(nil) @@ -53042,8 +53662,10 @@ func (s ScMapEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ScMapEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -53052,8 +53674,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ScMapEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ScMapEntry) xdrType() {} var _ xdrType = (*ScMapEntry)(nil) @@ -53231,8 +53852,10 @@ func (s StoredTransactionSet) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *StoredTransactionSet) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -53241,8 +53864,7 @@ var ( _ encoding.BinaryUnmarshaler = (*StoredTransactionSet)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s StoredTransactionSet) xdrType() {} var _ xdrType = (*StoredTransactionSet)(nil) @@ -53315,8 +53937,10 @@ func (s StoredDebugTransactionSet) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *StoredDebugTransactionSet) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -53325,8 +53949,7 @@ var ( _ encoding.BinaryUnmarshaler = (*StoredDebugTransactionSet)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s StoredDebugTransactionSet) xdrType() {} var _ xdrType = (*StoredDebugTransactionSet)(nil) @@ -53393,8 +54016,11 @@ func (s *PersistedScpStateV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, er } s.ScpEnvelopes = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScpEnvelope: length (%d) exceeds remaining input length (%d)", l, il) + } s.ScpEnvelopes = make([]ScpEnvelope, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.ScpEnvelopes[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -53409,8 +54035,11 @@ func (s *PersistedScpStateV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, er } s.QuorumSets = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScpQuorumSet: length (%d) exceeds remaining input length (%d)", l, il) + } s.QuorumSets = make([]ScpQuorumSet, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.QuorumSets[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -53425,8 +54054,11 @@ func (s *PersistedScpStateV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, er } s.TxSets = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding StoredTransactionSet: length (%d) exceeds remaining input length (%d)", l, il) + } s.TxSets = make([]StoredTransactionSet, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.TxSets[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -53448,8 +54080,10 @@ func (s PersistedScpStateV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PersistedScpStateV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -53458,8 +54092,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PersistedScpStateV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PersistedScpStateV0) xdrType() {} var _ xdrType = (*PersistedScpStateV0)(nil) @@ -53517,8 +54150,11 @@ func (s *PersistedScpStateV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, er } s.ScpEnvelopes = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScpEnvelope: length (%d) exceeds remaining input length (%d)", l, il) + } s.ScpEnvelopes = make([]ScpEnvelope, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.ScpEnvelopes[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -53533,8 +54169,11 @@ func (s *PersistedScpStateV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, er } s.QuorumSets = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ScpQuorumSet: length (%d) exceeds remaining input length (%d)", l, il) + } s.QuorumSets = make([]ScpQuorumSet, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = s.QuorumSets[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -53556,8 +54195,10 @@ func (s PersistedScpStateV1) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PersistedScpStateV1) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -53566,8 +54207,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PersistedScpStateV1)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PersistedScpStateV1) xdrType() {} var _ xdrType = (*PersistedScpStateV1)(nil) @@ -53745,8 +54385,10 @@ func (s PersistedScpState) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *PersistedScpState) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -53755,8 +54397,7 @@ var ( _ encoding.BinaryUnmarshaler = (*PersistedScpState)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s PersistedScpState) xdrType() {} var _ xdrType = (*PersistedScpState)(nil) @@ -53810,8 +54451,10 @@ func (s ConfigSettingContractExecutionLanesV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ConfigSettingContractExecutionLanesV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -53820,8 +54463,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ConfigSettingContractExecutionLanesV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ConfigSettingContractExecutionLanesV0) xdrType() {} var _ xdrType = (*ConfigSettingContractExecutionLanesV0)(nil) @@ -53910,8 +54552,10 @@ func (s ConfigSettingContractComputeV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ConfigSettingContractComputeV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -53920,8 +54564,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ConfigSettingContractComputeV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ConfigSettingContractComputeV0) xdrType() {} var _ xdrType = (*ConfigSettingContractComputeV0)(nil) @@ -54131,8 +54774,10 @@ func (s ConfigSettingContractLedgerCostV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ConfigSettingContractLedgerCostV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -54141,8 +54786,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ConfigSettingContractLedgerCostV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ConfigSettingContractLedgerCostV0) xdrType() {} var _ xdrType = (*ConfigSettingContractLedgerCostV0)(nil) @@ -54195,8 +54839,10 @@ func (s ConfigSettingContractHistoricalDataV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ConfigSettingContractHistoricalDataV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -54205,8 +54851,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ConfigSettingContractHistoricalDataV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ConfigSettingContractHistoricalDataV0) xdrType() {} var _ xdrType = (*ConfigSettingContractHistoricalDataV0)(nil) @@ -54271,8 +54916,10 @@ func (s ConfigSettingContractEventsV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ConfigSettingContractEventsV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -54281,8 +54928,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ConfigSettingContractEventsV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ConfigSettingContractEventsV0) xdrType() {} var _ xdrType = (*ConfigSettingContractEventsV0)(nil) @@ -54359,8 +55005,10 @@ func (s ConfigSettingContractBandwidthV0) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ConfigSettingContractBandwidthV0) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -54369,8 +55017,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ConfigSettingContractBandwidthV0)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ConfigSettingContractBandwidthV0) xdrType() {} var _ xdrType = (*ConfigSettingContractBandwidthV0)(nil) @@ -54535,8 +55182,10 @@ func (s ContractCostType) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractCostType) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -54545,8 +55194,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractCostType)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractCostType) xdrType() {} var _ xdrType = (*ContractCostType)(nil) @@ -54620,8 +55268,10 @@ func (s ContractCostParamEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractCostParamEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -54630,8 +55280,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractCostParamEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractCostParamEntry) xdrType() {} var _ xdrType = (*ContractCostParamEntry)(nil) @@ -54773,8 +55422,10 @@ func (s StateArchivalSettings) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *StateArchivalSettings) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -54783,8 +55434,7 @@ var ( _ encoding.BinaryUnmarshaler = (*StateArchivalSettings)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s StateArchivalSettings) xdrType() {} var _ xdrType = (*StateArchivalSettings)(nil) @@ -54856,8 +55506,10 @@ func (s EvictionIterator) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *EvictionIterator) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -54866,8 +55518,7 @@ var ( _ encoding.BinaryUnmarshaler = (*EvictionIterator)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s EvictionIterator) xdrType() {} var _ xdrType = (*EvictionIterator)(nil) @@ -54922,8 +55573,11 @@ func (s *ContractCostParams) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, err } (*s) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding ContractCostParamEntry: length (%d) exceeds remaining input length (%d)", l, il) + } (*s) = make([]ContractCostParamEntry, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -54945,8 +55599,10 @@ func (s ContractCostParams) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ContractCostParams) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -54955,8 +55611,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ContractCostParams)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ContractCostParams) xdrType() {} var _ xdrType = (*ContractCostParams)(nil) @@ -55068,8 +55723,10 @@ func (s ConfigSettingId) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ConfigSettingId) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -55078,8 +55735,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ConfigSettingId)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ConfigSettingId) xdrType() {} var _ xdrType = (*ConfigSettingId)(nil) @@ -55841,8 +56497,11 @@ func (u *ConfigSettingEntry) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, err } (*u.BucketListSizeWindow) = nil if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding Uint64: length (%d) exceeds remaining input length (%d)", l, il) + } (*u.BucketListSizeWindow) = make([]Uint64, l) - for i := uint32(0); i < l; i++ { + for i := uint32(0); uint(i) < uint(l); i++ { nTmp, err = (*u.BucketListSizeWindow)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -55874,8 +56533,10 @@ func (s ConfigSettingEntry) MarshalBinary() ([]byte, error) { // UnmarshalBinary implements encoding.BinaryUnmarshaler. func (s *ConfigSettingEntry) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) - d := xdr.NewDecoder(r) - _, err := s.DecodeFrom(d, xdr.DecodeDefaultMaxDepth) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) return err } @@ -55884,8 +56545,7 @@ var ( _ encoding.BinaryUnmarshaler = (*ConfigSettingEntry)(nil) ) -// xdrType signals that this type is an type representing -// representing XDR values defined by this package. +// xdrType signals that this type represents XDR values defined by this package. func (s ConfigSettingEntry) xdrType() {} var _ xdrType = (*ConfigSettingEntry)(nil) From 31c48bc92326582c297dc7749803ca8bc8b8d2ff Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 27 Nov 2023 10:32:23 +0000 Subject: [PATCH 013/234] services/horizon: Add integration tests for asset balance expiration / restoration (#5120) --- .../docker/captive-core-integration-tests.cfg | 3 + ...-core-reingest-range-integration-tests.cfg | 2 + .../docker/stellar-core-integration-tests.cfg | 3 + services/horizon/internal/flags.go | 2 +- .../internal/integration/contracts/Cargo.lock | 162 +++---- .../internal/integration/contracts/Cargo.toml | 1 + .../integration/contracts/store/Cargo.toml | 17 + .../integration/contracts/store/src/lib.rs | 16 + .../integration/extend_footprint_ttl_test.go | 29 +- .../horizon/internal/integration/sac_test.go | 406 ++++++++++++++++-- .../integration/testdata/soroban_add_u64.wasm | Bin 631 -> 631 bytes .../testdata/soroban_increment_contract.wasm | Bin 701 -> 701 bytes .../testdata/soroban_sac_test.wasm | Bin 1924 -> 1924 bytes .../integration/testdata/soroban_store.wasm | Bin 0 -> 449 bytes .../internal/test/integration/integration.go | 110 ++++- 15 files changed, 597 insertions(+), 154 deletions(-) create mode 100644 services/horizon/internal/integration/contracts/store/Cargo.toml create mode 100644 services/horizon/internal/integration/contracts/store/src/lib.rs create mode 100755 services/horizon/internal/integration/testdata/soroban_store.wasm diff --git a/services/horizon/docker/captive-core-integration-tests.cfg b/services/horizon/docker/captive-core-integration-tests.cfg index abea8c8ede..275599bacd 100644 --- a/services/horizon/docker/captive-core-integration-tests.cfg +++ b/services/horizon/docker/captive-core-integration-tests.cfg @@ -5,6 +5,9 @@ UNSAFE_QUORUM=true FAILURE_SAFETY=0 ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true +# Lower the TTL of persistent ledger entries +# so that ledger entry extension/restoring becomes testeable +TESTING_MINIMUM_PERSISTENT_ENTRY_LIFETIME=10 TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE=true [[VALIDATORS]] diff --git a/services/horizon/docker/captive-core-reingest-range-integration-tests.cfg b/services/horizon/docker/captive-core-reingest-range-integration-tests.cfg index 26a4cd6fd2..44820f5933 100644 --- a/services/horizon/docker/captive-core-reingest-range-integration-tests.cfg +++ b/services/horizon/docker/captive-core-reingest-range-integration-tests.cfg @@ -1,5 +1,7 @@ ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE=true +TESTING_MINIMUM_PERSISTENT_ENTRY_LIFETIME=10 +ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true [[VALIDATORS]] NAME="local_core" diff --git a/services/horizon/docker/stellar-core-integration-tests.cfg b/services/horizon/docker/stellar-core-integration-tests.cfg index 27adf63f4b..594a35b244 100644 --- a/services/horizon/docker/stellar-core-integration-tests.cfg +++ b/services/horizon/docker/stellar-core-integration-tests.cfg @@ -14,6 +14,9 @@ FAILURE_SAFETY=0 DATABASE="postgresql://user=postgres password=mysecretpassword host=core-postgres port=5641 dbname=stellar" +# Lower the TTL of persistent ledger entries +# so that ledger entry extension/restoring becomes testeable +TESTING_MINIMUM_PERSISTENT_ENTRY_LIFETIME=10 TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE=true [QUORUM_SET] diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index 1b8cc65d71..e2783680fd 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -630,7 +630,7 @@ func Flags() (*Config, support.ConfigOptions) { ConfigKey: &config.IngestDisableStateVerification, OptType: types.Bool, FlagDefault: false, - Usage: "ingestion system runs a verification routing to compare state in local database with history buckets, this can be disabled however it's not recommended", + Usage: "disable periodic verification of ledger state in horizon db (not recommended)", UsedInCommands: IngestionCommands, }, &support.ConfigOption{ diff --git a/services/horizon/internal/integration/contracts/Cargo.lock b/services/horizon/internal/integration/contracts/Cargo.lock index 417d3ff74f..006b35e8e2 100644 --- a/services/horizon/internal/integration/contracts/Cargo.lock +++ b/services/horizon/internal/integration/contracts/Cargo.lock @@ -34,9 +34,9 @@ dependencies = [ [[package]] name = "arbitrary" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2e1373abdaa212b704512ec2bd8b26bd0b7d5c3f70117411a5d9a451383c859" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" dependencies = [ "derive_arbitrary", ] @@ -82,9 +82,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" @@ -161,9 +161,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -181,9 +181,9 @@ dependencies = [ [[package]] name = "crypto-bigint" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", "rand_core", @@ -230,9 +230,9 @@ dependencies = [ [[package]] name = "curve25519-dalek-derive" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", @@ -296,9 +296,9 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", @@ -325,9 +325,9 @@ checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" [[package]] name = "ecdsa" -version = "0.16.8" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", "digest", @@ -349,15 +349,16 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" +checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0" dependencies = [ "curve25519-dalek", "ed25519", "rand_core", "serde", "sha2", + "subtle", "zeroize", ] @@ -369,9 +370,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" -version = "0.13.6" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97ca172ae9dc9f9b779a6e3a65d308f2af74e5b8c921299075bdb4a0370e914" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", @@ -394,9 +395,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "ethnum" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8ff382b2fa527fb7fb06eeebfc5bbb3f17e3cc6b9d70b006c41daa8824adac" +checksum = "b90ca2580b73ab6a1f724b76ca11ab632df820fd6040c336200d2c1df7b3c82c" [[package]] name = "ff" @@ -410,9 +411,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.1" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" [[package]] name = "fnv" @@ -433,9 +434,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "js-sys", @@ -469,9 +470,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" [[package]] name = "hex" @@ -493,16 +494,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -533,12 +534,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.1", + "hashbrown 0.14.2", "serde", ] @@ -565,18 +566,18 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" dependencies = [ "wasm-bindgen", ] [[package]] name = "k256" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" dependencies = [ "cfg-if", "ecdsa", @@ -597,9 +598,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.149" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libm" @@ -702,9 +703,9 @@ dependencies = [ [[package]] name = "platforms" -version = "3.1.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" +checksum = "14e6ab3f592e6fb464fc9712d8d6e6912de6473954635fd76a589d832cffcbb0" [[package]] name = "powerfmt" @@ -829,18 +830,18 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.189" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" dependencies = [ "proc-macro2", "quote", @@ -849,9 +850,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -860,15 +861,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.0.2", + "indexmap 2.1.0", "serde", "serde_json", "serde_with_macros", @@ -877,9 +878,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" dependencies = [ "darling", "proc-macro2", @@ -910,9 +911,9 @@ dependencies = [ [[package]] name = "signature" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", "rand_core", @@ -920,9 +921,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "soroban-env-common" @@ -1086,6 +1087,13 @@ dependencies = [ "thiserror", ] +[[package]] +name = "soroban-store" +version = "0.0.0" +dependencies = [ + "soroban-sdk", +] + [[package]] name = "soroban-wasmi" version = "0.31.0-soroban1" @@ -1163,9 +1171,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" -version = "2.0.38" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -1174,18 +1182,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.49" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.49" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", @@ -1247,9 +1255,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1257,9 +1265,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" dependencies = [ "bumpalo", "log", @@ -1272,9 +1280,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1282,9 +1290,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", @@ -1295,9 +1303,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" [[package]] name = "wasmi_arena" @@ -1334,10 +1342,10 @@ dependencies = [ ] [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ "windows-targets", ] @@ -1401,6 +1409,6 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/services/horizon/internal/integration/contracts/Cargo.toml b/services/horizon/internal/integration/contracts/Cargo.toml index f7e3b81aed..d27b45dd4f 100644 --- a/services/horizon/internal/integration/contracts/Cargo.toml +++ b/services/horizon/internal/integration/contracts/Cargo.toml @@ -5,6 +5,7 @@ members = [ "sac_test", "increment", "add_u64", + "store", ] [profile.release-with-logs] diff --git a/services/horizon/internal/integration/contracts/store/Cargo.toml b/services/horizon/internal/integration/contracts/store/Cargo.toml new file mode 100644 index 0000000000..080ae44abd --- /dev/null +++ b/services/horizon/internal/integration/contracts/store/Cargo.toml @@ -0,0 +1,17 @@ +[package] +version = "0.0.0" +name = "soroban-store" +authors = ["Stellar Development Foundation "] +license = "Apache-2.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] +doctest = false + +[dependencies] +soroban-sdk = { workspace = true } + +[dev_dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } \ No newline at end of file diff --git a/services/horizon/internal/integration/contracts/store/src/lib.rs b/services/horizon/internal/integration/contracts/store/src/lib.rs new file mode 100644 index 0000000000..3a80d77ed7 --- /dev/null +++ b/services/horizon/internal/integration/contracts/store/src/lib.rs @@ -0,0 +1,16 @@ +#![no_std] +use soroban_sdk::{contract, contractimpl, Env, Val}; + +#[contract] +pub struct Contract; + +#[contractimpl] +impl Contract { + pub fn set(e: Env, key: Val, val: Val) { + e.storage().persistent().set(&key, &val) + } + + pub fn remove(e: Env, key: Val) { + e.storage().persistent().remove(&key) + } +} \ No newline at end of file diff --git a/services/horizon/internal/integration/extend_footprint_ttl_test.go b/services/horizon/internal/integration/extend_footprint_ttl_test.go index b0c9258827..e280184b65 100644 --- a/services/horizon/internal/integration/extend_footprint_ttl_test.go +++ b/services/horizon/internal/integration/extend_footprint_ttl_test.go @@ -9,7 +9,6 @@ import ( "github.com/stellar/go/protocols/horizon/operations" "github.com/stellar/go/services/horizon/internal/test/integration" "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" ) func TestExtendFootprintTtl(t *testing.T) { @@ -35,29 +34,11 @@ func TestExtendFootprintTtl(t *testing.T) { _, err = itest.Client().TransactionDetail(tx.Hash) require.NoError(t, err) - sourceAccount, err = itest.Client().AccountDetail(horizonclient.AccountRequest{ - AccountID: itest.Master().Address(), - }) - require.NoError(t, err) - - bumpFootPrint := txnbuild.ExtendFootprintTtl{ - ExtendTo: 10000, - SourceAccount: "", - Ext: xdr.TransactionExt{ - V: 1, - SorobanData: &xdr.SorobanTransactionData{ - Ext: xdr.ExtensionPoint{}, - Resources: xdr.SorobanResources{ - Footprint: xdr.LedgerFootprint{ - ReadOnly: preFlightOp.Ext.SorobanData.Resources.Footprint.ReadWrite, - ReadWrite: nil, - }, - }, - ResourceFee: 0, - }, - }, - } - bumpFootPrint, minFee = itest.PreflightBumpFootprintExpiration(&sourceAccount, bumpFootPrint) + sourceAccount, bumpFootPrint, minFee := itest.PreflightExtendExpiration( + itest.Master().Address(), + preFlightOp.Ext.SorobanData.Resources.Footprint.ReadWrite, + 10000, + ) tx = itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &bumpFootPrint) ops, err := itest.Client().Operations(horizonclient.OperationRequest{ForTransaction: tx.Hash}) diff --git a/services/horizon/internal/integration/sac_test.go b/services/horizon/internal/integration/sac_test.go index 68236ca3fe..64c772b44c 100644 --- a/services/horizon/internal/integration/sac_test.go +++ b/services/horizon/internal/integration/sac_test.go @@ -1,6 +1,7 @@ package integration import ( + "context" "math" "math/big" "strings" @@ -14,6 +15,8 @@ import ( "github.com/stellar/go/keypair" "github.com/stellar/go/protocols/horizon/effects" "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/services/horizon/internal/ingest/processors" "github.com/stellar/go/services/horizon/internal/test/integration" "github.com/stellar/go/strkey" "github.com/stellar/go/txnbuild" @@ -22,6 +25,11 @@ import ( const sac_contract = "soroban_sac_test.wasm" +// LongTermTTL is used to extend the lifetime of ledger entries by 10000 ledgers. +// This will ensure that the ledger entries never expire during the execution +// of the integration tests. +const LongTermTTL = 10000 + // Tests use precompiled wasm bin files that are added to the testdata directory. // Refer to ./services/horizon/internal/integration/contracts/README.md on how to recompile // contract code if needed to new wasm. @@ -41,7 +49,7 @@ func TestContractMintToAccount(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) + createSAC(itest, asset) recipientKp, recipient := itest.CreateAccount("100") itest.MustEstablishTrustline(recipientKp, recipient, txnbuild.MustAssetFromXDR(asset)) @@ -105,6 +113,32 @@ func TestContractMintToAccount(t *testing.T) { }) } +func createSAC(itest *integration.Test, asset xdr.Asset) { + invokeHostFunction := &txnbuild.InvokeHostFunction{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeCreateContract, + CreateContract: &xdr.CreateContractArgs{ + ContractIdPreimage: xdr.ContractIdPreimage{ + Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAsset, + FromAsset: &asset, + }, + Executable: xdr.ContractExecutable{ + Type: xdr.ContractExecutableTypeContractExecutableStellarAsset, + WasmHash: nil, + }, + }, + }, + SourceAccount: itest.Master().Address(), + } + _, _, preFlightOp := assertInvokeHostFnSucceeds(itest, itest.Master(), invokeHostFunction) + sourceAccount, extendTTLOp, minFee := itest.PreflightExtendExpiration( + itest.Master().Address(), + preFlightOp.Ext.SorobanData.Resources.Footprint.ReadWrite, + LongTermTTL, + ) + itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &extendTTLOp) +} + func TestContractMintToContract(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") @@ -119,7 +153,7 @@ func TestContractMintToContract(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) + createSAC(itest, asset) // Create recipient contract recipientContractID, _ := mustCreateAndInstallContract(itest, itest.Master(), "a1", add_u64_contract) @@ -185,6 +219,287 @@ func TestContractMintToContract(t *testing.T) { }) } +func TestExpirationAndRestoration(t *testing.T) { + if integration.GetCoreMaxSupportedProtocol() < 20 { + t.Skip("This test run does not support less than Protocol 20") + } + + itest := integration.NewTest(t, integration.Config{ + ProtocolVersion: 20, + EnableSorobanRPC: true, + HorizonIngestParameters: map[string]string{ + // disable state verification because we will insert + // a fake asset contract in the horizon db and we don't + // want state verification to detect this + "ingest-disable-state-verification": "true", + }, + }) + + issuer := itest.Master().Address() + code := "USD" + + // Create contract to store synthetic asset balances + storeContractID, _ := mustCreateAndInstallContract( + itest, + itest.Master(), + "a1", + "soroban_store.wasm", + ) + syntheticAssetStat := history.ExpAssetStat{ + AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, + AssetCode: code, + AssetIssuer: issuer, + Accounts: history.ExpAssetStatAccounts{ + Authorized: 0, + AuthorizedToMaintainLiabilities: 0, + ClaimableBalances: 0, + LiquidityPools: 0, + Unauthorized: 0, + }, + Balances: history.ExpAssetStatBalances{ + Authorized: "0", + AuthorizedToMaintainLiabilities: "0", + ClaimableBalances: "0", + LiquidityPools: "0", + Unauthorized: "0", + }, + Amount: "0", + NumAccounts: 0, + ContractID: nil, + } + syntheticAssetStat.SetContractID(storeContractID) + _, err := itest.HorizonIngest().HistoryQ().InsertAssetStat( + context.Background(), + syntheticAssetStat, + ) + assert.NoError(t, err) + + // create active balance + _, _, setOp := assertInvokeHostFnSucceeds( + itest, + itest.Master(), + invokeStoreSet( + itest, + storeContractID, + processors.BalanceToContractData( + storeContractID, + [32]byte{1}, + 23, + ), + ), + ) + sourceAccount, extendTTLOp, minFee := itest.PreflightExtendExpiration( + itest.Master().Address(), + setOp.Ext.SorobanData.Resources.Footprint.ReadWrite, + LongTermTTL, + ) + itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &extendTTLOp) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 1, + balanceContracts: big.NewInt(23), + contractID: storeContractID, + }) + + // create balance which we will expire + balanceToExpire := processors.BalanceToContractData( + storeContractID, + [32]byte{2}, + 37, + ) + assertInvokeHostFnSucceeds( + itest, + itest.Master(), + invokeStoreSet( + itest, + storeContractID, + balanceToExpire, + ), + ) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 2, + balanceContracts: big.NewInt(60), + contractID: storeContractID, + }) + + balanceToExpireLedgerKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.LedgerKeyContractData{ + Contract: balanceToExpire.ContractData.Contract, + Key: balanceToExpire.ContractData.Key, + Durability: balanceToExpire.ContractData.Durability, + }, + } + // The TESTING_MINIMUM_PERSISTENT_ENTRY_LIFETIME=10 configuration in stellar-core + // will ensure that the ledger entry expires after 10 ledgers. + // Because ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING is set to true, 10 ledgers + // should elapse in 10 seconds + itest.WaitUntilLedgerEntryTTL(balanceToExpireLedgerKey) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + balanceArchivedContracts: big.NewInt(37), + numArchivedContracts: 1, + numContracts: 1, + balanceContracts: big.NewInt(23), + contractID: storeContractID, + }) + + // increase active balance from 23 to 50 + assertInvokeHostFnSucceeds( + itest, + itest.Master(), + invokeStoreSet( + itest, + storeContractID, + processors.BalanceToContractData( + storeContractID, + [32]byte{1}, + 50, + ), + ), + ) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + balanceArchivedContracts: big.NewInt(37), + numArchivedContracts: 1, + numContracts: 1, + balanceContracts: big.NewInt(50), + contractID: storeContractID, + }) + + // restore expired balance + sourceAccount, restoreFootprint, minFee := itest.RestoreFootprint( + itest.Master().Address(), + balanceToExpireLedgerKey, + ) + itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &restoreFootprint) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 2, + balanceContracts: big.NewInt(87), + contractID: storeContractID, + }) + + // expire the balance again + itest.WaitUntilLedgerEntryTTL(balanceToExpireLedgerKey) + + // decrease active balance from 50 to 3 + assertInvokeHostFnSucceeds( + itest, + itest.Master(), + invokeStoreSet( + itest, + storeContractID, + processors.BalanceToContractData( + storeContractID, + [32]byte{1}, + 3, + ), + ), + ) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + balanceArchivedContracts: big.NewInt(37), + numArchivedContracts: 1, + numContracts: 1, + balanceContracts: big.NewInt(3), + contractID: storeContractID, + }) + + // remove active balance + assertInvokeHostFnSucceeds( + itest, + itest.Master(), + invokeStoreRemove( + itest, + storeContractID, + processors.ContractBalanceLedgerKey( + storeContractID, + [32]byte{1}, + ), + ), + ) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + balanceArchivedContracts: big.NewInt(37), + numArchivedContracts: 1, + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: storeContractID, + }) +} + +func invokeStoreSet( + itest *integration.Test, + storeContractID xdr.Hash, + ledgerEntryData xdr.LedgerEntryData, +) *txnbuild.InvokeHostFunction { + key := ledgerEntryData.MustContractData().Key + val := ledgerEntryData.MustContractData().Val + return &txnbuild.InvokeHostFunction{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, + InvokeContract: &xdr.InvokeContractArgs{ + ContractAddress: contractIDParam(storeContractID), + FunctionName: "set", + Args: xdr.ScVec{ + key, + val, + }, + }, + }, + SourceAccount: itest.Master().Address(), + } +} + +func invokeStoreRemove( + itest *integration.Test, + storeContractID xdr.Hash, + ledgerKey xdr.LedgerKey, +) *txnbuild.InvokeHostFunction { + return &txnbuild.InvokeHostFunction{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, + InvokeContract: &xdr.InvokeContractArgs{ + ContractAddress: contractIDParam(storeContractID), + FunctionName: "remove", + Args: xdr.ScVec{ + ledgerKey.MustContractData().Key, + }, + }, + }, + SourceAccount: itest.Master().Address(), + } +} + func TestContractTransferBetweenAccounts(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") @@ -199,7 +514,7 @@ func TestContractTransferBetweenAccounts(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) + createSAC(itest, asset) recipientKp, recipient := itest.CreateAccount("100") itest.MustEstablishTrustline(recipientKp, recipient, txnbuild.MustAssetFromXDR(asset)) @@ -274,7 +589,7 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { code := "USDLONG" asset := xdr.MustNewCreditAsset(code, issuer) - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) + createSAC(itest, asset) recipientKp, recipient := itest.CreateAccount("100") itest.MustEstablishTrustline(recipientKp, recipient, txnbuild.MustAssetFromXDR(asset)) @@ -395,7 +710,7 @@ func TestContractTransferBetweenContracts(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) + createSAC(itest, asset) // Create recipient contract recipientContractID, _ := mustCreateAndInstallContract(itest, itest.Master(), "a1", sac_contract) @@ -477,7 +792,7 @@ func TestContractBurnFromAccount(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) + createSAC(itest, asset) recipientKp, recipient := itest.CreateAccount("100") itest.MustEstablishTrustline(recipientKp, recipient, txnbuild.MustAssetFromXDR(asset)) @@ -553,7 +868,7 @@ func TestContractBurnFromContract(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) + createSAC(itest, asset) // Create recipient contract recipientContractID, recipientContractHash := mustCreateAndInstallContract(itest, itest.Master(), "a1", sac_contract) @@ -631,7 +946,7 @@ func TestContractClawbackFromAccount(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) + createSAC(itest, asset) recipientKp, recipient := itest.CreateAccount("100") itest.MustEstablishTrustline(recipientKp, recipient, txnbuild.MustAssetFromXDR(asset)) @@ -709,7 +1024,7 @@ func TestContractClawbackFromContract(t *testing.T) { code := "USD" asset := xdr.MustNewCreditAsset(code, issuer) - assertInvokeHostFnSucceeds(itest, itest.Master(), createSAC(issuer, asset)) + createSAC(itest, asset) // Create recipient contract recipientContractID, _ := mustCreateAndInstallContract(itest, itest.Master(), "a2", sac_contract) @@ -788,7 +1103,8 @@ func assertAssetStats(itest *integration.Test, expected assetStats) { }) assert.NoError(itest.CurrentTest(), err) - if expected.numContracts == 0 && expected.numAccounts == 0 && + if expected.numContracts == 0 && expected.numAccounts == 0 && expected.numArchivedContracts == 0 && + expected.balanceArchivedContracts.Cmp(big.NewInt(0)) == 0 && expected.balanceContracts.Cmp(big.NewInt(0)) == 0 && expected.balanceAccounts == 0 { assert.Empty(itest.CurrentTest(), assets) return @@ -913,27 +1229,6 @@ func i128Param(hi int64, lo uint64) xdr.ScVal { } } -func createSAC(sourceAccount string, asset xdr.Asset) *txnbuild.InvokeHostFunction { - invokeHostFunction := &txnbuild.InvokeHostFunction{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeCreateContract, - CreateContract: &xdr.CreateContractArgs{ - ContractIdPreimage: xdr.ContractIdPreimage{ - Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAsset, - FromAsset: &asset, - }, - Executable: xdr.ContractExecutable{ - Type: xdr.ContractExecutableTypeContractExecutableStellarAsset, - WasmHash: nil, - }, - }, - }, - SourceAccount: sourceAccount, - } - - return invokeHostFunction -} - func mint(itest *integration.Test, sourceAccount string, asset xdr.Asset, assetAmount string, recipient xdr.ScVal) *txnbuild.InvokeHostFunction { return mintWithAmt(itest, sourceAccount, asset, i128Param(0, uint64(amount.MustParse(assetAmount))), recipient) } @@ -1098,13 +1393,9 @@ func burn(itest *integration.Test, sourceAccount string, asset xdr.Asset, assetA func assertInvokeHostFnSucceeds(itest *integration.Test, signer *keypair.Full, op *txnbuild.InvokeHostFunction) (*xdr.ScVal, string, *txnbuild.InvokeHostFunction) { acc := itest.MustGetAccount(signer) preFlightOp, minFee := itest.PreflightHostFunctions(&acc, *op) - tx, err := itest.SubmitOperationsWithFee(&acc, signer, minFee+txnbuild.MinBaseFee, &preFlightOp) + clientTx, err := itest.SubmitOperationsWithFee(&acc, signer, minFee+txnbuild.MinBaseFee, &preFlightOp) require.NoError(itest.CurrentTest(), err) - clientTx, err := itest.Client().TransactionDetail(tx.Hash) - require.NoError(itest.CurrentTest(), err) - - assert.Equal(itest.CurrentTest(), tx.Hash, clientTx.Hash) var txResult xdr.TransactionResult err = xdr.SafeUnmarshalBase64(clientTx.ResultXdr, &txResult) require.NoError(itest.CurrentTest(), err) @@ -1122,7 +1413,7 @@ func assertInvokeHostFnSucceeds(itest *integration.Test, signer *keypair.Full, o returnValue := txMetaResult.MustV3().SorobanMeta.ReturnValue - return &returnValue, tx.Hash, &preFlightOp + return &returnValue, clientTx.Hash, &preFlightOp } func stellarAssetContractID(itest *integration.Test, asset xdr.Asset) xdr.Hash { @@ -1132,11 +1423,40 @@ func stellarAssetContractID(itest *integration.Test, asset xdr.Asset) xdr.Hash { } func mustCreateAndInstallContract(itest *integration.Test, signer *keypair.Full, contractSalt string, wasmFileName string) (xdr.Hash, xdr.Hash) { - installContractOp := assembleInstallContractCodeOp(itest.CurrentTest(), itest.Master().Address(), wasmFileName) - assertInvokeHostFnSucceeds(itest, signer, installContractOp) - createContractOp := assembleCreateContractOp(itest.CurrentTest(), itest.Master().Address(), wasmFileName, contractSalt, itest.GetPassPhrase()) - _, _, preflightOp := assertInvokeHostFnSucceeds(itest, signer, createContractOp) - contractHash := preflightOp.Ext.SorobanData.Resources.Footprint.ReadOnly[0].MustContractCode().Hash - contractID := preflightOp.Ext.SorobanData.Resources.Footprint.ReadWrite[0].MustContractData().Contract.ContractId + _, _, installContractOp := assertInvokeHostFnSucceeds( + itest, + signer, + assembleInstallContractCodeOp( + itest.CurrentTest(), + itest.Master().Address(), + wasmFileName, + ), + ) + _, _, createContractOp := assertInvokeHostFnSucceeds( + itest, + signer, + assembleCreateContractOp( + itest.CurrentTest(), + itest.Master().Address(), + wasmFileName, + contractSalt, + itest.GetPassPhrase(), + ), + ) + + keys := append( + installContractOp.Ext.SorobanData.Resources.Footprint.ReadWrite, + createContractOp.Ext.SorobanData.Resources.Footprint.ReadWrite..., + ) + + sourceAccount, extendTTLOp, minFee := itest.PreflightExtendExpiration( + itest.Master().Address(), + keys, + LongTermTTL, + ) + itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &extendTTLOp) + + contractHash := createContractOp.Ext.SorobanData.Resources.Footprint.ReadOnly[0].MustContractCode().Hash + contractID := createContractOp.Ext.SorobanData.Resources.Footprint.ReadWrite[0].MustContractData().Contract.ContractId return *contractID, contractHash } diff --git a/services/horizon/internal/integration/testdata/soroban_add_u64.wasm b/services/horizon/internal/integration/testdata/soroban_add_u64.wasm index 9aac34ea654e19ee731b56ced4e60fd08bd60c89..91ca4537f6e738227283b49ed13edb55bf6ddf2a 100755 GIT binary patch delta 13 Ucmey)@||Ua789e%WNjuN03$*K6aWAK delta 13 Ucmey)@||Ua789fKWNjuN03$sF6951J diff --git a/services/horizon/internal/integration/testdata/soroban_increment_contract.wasm b/services/horizon/internal/integration/testdata/soroban_increment_contract.wasm index 55308fd40a61dffbda9628f24a35cc266f5b5721..ddcbadea30ad4ca60ca158955ab4e0f5c9548019 100755 GIT binary patch delta 13 UcmdnXx|elB0TZLi 0 { + liveUntilLedgerSeq := *result.Entries[0].LiveUntilLedgerSeq + + root, err := i.horizonClient.Root() + assert.NoError(i.t, err) + if uint32(root.HorizonSequence) > liveUntilLedgerSeq { + ttled = true + i.t.Logf("ledger entry ttl'ed") + break + } + i.t.Log("waiting for ledger entry to ttl at ledger", liveUntilLedgerSeq) + } else { + i.t.Log("waiting for soroban-rpc to ingest the ledger entries") + } + time.Sleep(time.Second) + } + assert.True(i.t, ttled) +} + +func (i *Test) PreflightExtendExpiration( + account string, ledgerKeys []xdr.LedgerKey, extendAmt uint32, +) (proto.Account, txnbuild.ExtendFootprintTtl, int64) { + sourceAccount, err := i.Client().AccountDetail(sdk.AccountRequest{ + AccountID: account, + }) + assert.NoError(i.t, err) + + bumpFootprint := txnbuild.ExtendFootprintTtl{ + ExtendTo: extendAmt, + SourceAccount: "", + Ext: xdr.TransactionExt{ + V: 1, + SorobanData: &xdr.SorobanTransactionData{ + Ext: xdr.ExtensionPoint{}, + Resources: xdr.SorobanResources{ + Footprint: xdr.LedgerFootprint{ + ReadOnly: ledgerKeys, + ReadWrite: nil, + }, + }, + ResourceFee: 0, + }, + }, + } + result, transactionData := i.simulateTransaction(&sourceAccount, &bumpFootprint) bumpFootprint.Ext = xdr.TransactionExt{ V: 1, SorobanData: &transactionData, } - return bumpFootprint, result.MinResourceFee + return sourceAccount, bumpFootprint, result.MinResourceFee +} + +func (i *Test) RestoreFootprint( + account string, ledgerKey xdr.LedgerKey, +) (proto.Account, txnbuild.RestoreFootprint, int64) { + sourceAccount, err := i.Client().AccountDetail(sdk.AccountRequest{ + AccountID: account, + }) + assert.NoError(i.t, err) + + restoreFootprint := txnbuild.RestoreFootprint{ + SourceAccount: "", + Ext: xdr.TransactionExt{ + V: 1, + SorobanData: &xdr.SorobanTransactionData{ + Ext: xdr.ExtensionPoint{}, + Resources: xdr.SorobanResources{ + Footprint: xdr.LedgerFootprint{ + ReadWrite: []xdr.LedgerKey{ledgerKey}, + }, + }, + ResourceFee: 0, + }, + }, + } + result, transactionData := i.simulateTransaction(&sourceAccount, &restoreFootprint) + restoreFootprint.Ext = xdr.TransactionExt{ + V: 1, + SorobanData: &transactionData, + } + + return sourceAccount, restoreFootprint, result.MinResourceFee } // UpgradeProtocol arms Core with upgrade and blocks until protocol is upgraded. From acbe8a069894771a7df8b6cd25f6b905d646f368 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Thu, 30 Nov 2023 16:40:29 +0100 Subject: [PATCH 014/234] services/horizon: Bump Core and soroban-rpc versions (#5130) --- .github/workflows/horizon.yml | 6 +- ...ive-core-integration-tests.soroban-rpc.cfg | 7 +- .../internal/integration/contracts/Cargo.lock | 117 ++++++++++-------- .../internal/integration/contracts/Cargo.toml | 2 +- .../integration/testdata/soroban_add_u64.wasm | Bin 631 -> 631 bytes .../testdata/soroban_increment_contract.wasm | Bin 701 -> 701 bytes .../testdata/soroban_sac_test.wasm | Bin 1924 -> 1924 bytes .../integration/testdata/soroban_store.wasm | Bin 449 -> 449 bytes .../internal/test/integration/integration.go | 2 +- 9 files changed, 77 insertions(+), 57 deletions(-) diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index 6139ad6806..8cae10766d 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -33,9 +33,9 @@ jobs: env: HORIZON_INTEGRATION_TESTS_ENABLED: true HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL: ${{ matrix.protocol-version }} - PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 19.14.1-1529.fcbbad4ce.focal - PROTOCOL_20_CORE_DOCKER_IMG: 2opremio/stellar-core:19.14.1-1529.fcbbad4ce.focal - PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:20.0.0-rc4pubnet-42 + PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 19.14.1-1590.b6b730a0c.focal + PROTOCOL_20_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:19.14.1-1590.b6b730a0c.focal + PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:20.0.0-rc4231129-43 PROTOCOL_19_CORE_DEBIAN_PKG_VERSION: 19.12.0-1378.2109a168a.focal PROTOCOL_19_CORE_DOCKER_IMG: stellar/stellar-core:19.12.0-1378.2109a168a.focal PGHOST: localhost diff --git a/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg b/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg index 2e3dd346e5..9a7ad9d769 100644 --- a/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg +++ b/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg @@ -1,10 +1,15 @@ PEER_PORT=11725 ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true -ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true UNSAFE_QUORUM=true FAILURE_SAFETY=0 +ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true +# Lower the TTL of persistent ledger entries +# so that ledger entry extension/restoring becomes testeable +TESTING_MINIMUM_PERSISTENT_ENTRY_LIFETIME=10 +TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE=true + [[VALIDATORS]] NAME="local_core" HOME_DOMAIN="core.local" diff --git a/services/horizon/internal/integration/contracts/Cargo.lock b/services/horizon/internal/integration/contracts/Cargo.lock index 006b35e8e2..e80d7d5d28 100644 --- a/services/horizon/internal/integration/contracts/Cargo.lock +++ b/services/horizon/internal/integration/contracts/Cargo.lock @@ -447,9 +447,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "group" @@ -470,9 +470,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "hex" @@ -483,6 +483,12 @@ dependencies = [ "serde", ] +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + [[package]] name = "hmac" version = "0.12.1" @@ -539,7 +545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown 0.14.3", "serde", ] @@ -551,9 +557,9 @@ checksum = "8e04e2fd2b8188ea827b32ef11de88377086d690286ab35747ef7f9bf3ccb590" [[package]] name = "itertools" -version = "0.10.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] @@ -566,9 +572,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] @@ -731,9 +737,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] @@ -830,18 +836,18 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.192" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", @@ -925,10 +931,21 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +[[package]] +name = "soroban-builtin-sdk-macros" +version = "20.0.0-rc2" +source = "git+https://github.com/stellar/rs-soroban-env?rev=be04cf31e925ba5bacd9b22db7caf7b4f6dd8372#be04cf31e925ba5bacd9b22db7caf7b4f6dd8372" +dependencies = [ + "itertools", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "soroban-env-common" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-env?rev=91f44778389490ad863d61a8a90ac9875ba6d8fd#91f44778389490ad863d61a8a90ac9875ba6d8fd" +source = "git+https://github.com/stellar/rs-soroban-env?rev=be04cf31e925ba5bacd9b22db7caf7b4f6dd8372#be04cf31e925ba5bacd9b22db7caf7b4f6dd8372" dependencies = [ "arbitrary", "crate-git-revision", @@ -945,7 +962,7 @@ dependencies = [ [[package]] name = "soroban-env-guest" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-env?rev=91f44778389490ad863d61a8a90ac9875ba6d8fd#91f44778389490ad863d61a8a90ac9875ba6d8fd" +source = "git+https://github.com/stellar/rs-soroban-env?rev=be04cf31e925ba5bacd9b22db7caf7b4f6dd8372#be04cf31e925ba5bacd9b22db7caf7b4f6dd8372" dependencies = [ "soroban-env-common", "static_assertions", @@ -954,11 +971,14 @@ dependencies = [ [[package]] name = "soroban-env-host" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-env?rev=91f44778389490ad863d61a8a90ac9875ba6d8fd#91f44778389490ad863d61a8a90ac9875ba6d8fd" +source = "git+https://github.com/stellar/rs-soroban-env?rev=be04cf31e925ba5bacd9b22db7caf7b4f6dd8372#be04cf31e925ba5bacd9b22db7caf7b4f6dd8372" dependencies = [ "backtrace", + "curve25519-dalek", "ed25519-dalek", "getrandom", + "hex-literal", + "hmac", "k256", "num-derive", "num-integer", @@ -967,8 +987,8 @@ dependencies = [ "rand_chacha", "sha2", "sha3", + "soroban-builtin-sdk-macros", "soroban-env-common", - "soroban-native-sdk-macros", "soroban-wasmi", "static_assertions", "stellar-strkey", @@ -977,7 +997,7 @@ dependencies = [ [[package]] name = "soroban-env-macros" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-env?rev=91f44778389490ad863d61a8a90ac9875ba6d8fd#91f44778389490ad863d61a8a90ac9875ba6d8fd" +source = "git+https://github.com/stellar/rs-soroban-env?rev=be04cf31e925ba5bacd9b22db7caf7b4f6dd8372#be04cf31e925ba5bacd9b22db7caf7b4f6dd8372" dependencies = [ "itertools", "proc-macro2", @@ -998,26 +1018,16 @@ dependencies = [ [[package]] name = "soroban-ledger-snapshot" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-sdk?rev=729ed3ac5fe8600a3245d5816eadd3c95ab2eb54#729ed3ac5fe8600a3245d5816eadd3c95ab2eb54" +source = "git+https://github.com/stellar/rs-soroban-sdk?rev=e35bace9de5addae7c32f405cdc11bb459cb1d61#e35bace9de5addae7c32f405cdc11bb459cb1d61" dependencies = [ "serde", "serde_json", + "serde_with", "soroban-env-common", "soroban-env-host", "thiserror", ] -[[package]] -name = "soroban-native-sdk-macros" -version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-env?rev=91f44778389490ad863d61a8a90ac9875ba6d8fd#91f44778389490ad863d61a8a90ac9875ba6d8fd" -dependencies = [ - "itertools", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "soroban-sac-test" version = "0.0.0" @@ -1028,13 +1038,15 @@ dependencies = [ [[package]] name = "soroban-sdk" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-sdk?rev=729ed3ac5fe8600a3245d5816eadd3c95ab2eb54#729ed3ac5fe8600a3245d5816eadd3c95ab2eb54" +source = "git+https://github.com/stellar/rs-soroban-sdk?rev=e35bace9de5addae7c32f405cdc11bb459cb1d61#e35bace9de5addae7c32f405cdc11bb459cb1d61" dependencies = [ "arbitrary", "bytes-lit", "ctor", "ed25519-dalek", "rand", + "serde", + "serde_json", "soroban-env-guest", "soroban-env-host", "soroban-ledger-snapshot", @@ -1045,7 +1057,7 @@ dependencies = [ [[package]] name = "soroban-sdk-macros" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-sdk?rev=729ed3ac5fe8600a3245d5816eadd3c95ab2eb54#729ed3ac5fe8600a3245d5816eadd3c95ab2eb54" +source = "git+https://github.com/stellar/rs-soroban-sdk?rev=e35bace9de5addae7c32f405cdc11bb459cb1d61#e35bace9de5addae7c32f405cdc11bb459cb1d61" dependencies = [ "crate-git-revision", "darling", @@ -1064,7 +1076,7 @@ dependencies = [ [[package]] name = "soroban-spec" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-sdk?rev=729ed3ac5fe8600a3245d5816eadd3c95ab2eb54#729ed3ac5fe8600a3245d5816eadd3c95ab2eb54" +source = "git+https://github.com/stellar/rs-soroban-sdk?rev=e35bace9de5addae7c32f405cdc11bb459cb1d61#e35bace9de5addae7c32f405cdc11bb459cb1d61" dependencies = [ "base64 0.13.1", "stellar-xdr", @@ -1075,7 +1087,7 @@ dependencies = [ [[package]] name = "soroban-spec-rust" version = "20.0.0-rc2" -source = "git+https://github.com/stellar/rs-soroban-sdk?rev=729ed3ac5fe8600a3245d5816eadd3c95ab2eb54#729ed3ac5fe8600a3245d5816eadd3c95ab2eb54" +source = "git+https://github.com/stellar/rs-soroban-sdk?rev=e35bace9de5addae7c32f405cdc11bb459cb1d61#e35bace9de5addae7c32f405cdc11bb459cb1d61" dependencies = [ "prettyplease", "proc-macro2", @@ -1121,9 +1133,9 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "spki" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -1137,17 +1149,19 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "stellar-strkey" -version = "0.0.7" -source = "git+https://github.com/stellar/rs-stellar-strkey?rev=e6ba45c60c16de28c7522586b80ed0150157df73#e6ba45c60c16de28c7522586b80ed0150157df73" +version = "0.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12d2bf45e114117ea91d820a846fd1afbe3ba7d717988fee094ce8227a3bf8bd" dependencies = [ "base32", + "crate-git-revision", "thiserror", ] [[package]] name = "stellar-xdr" version = "20.0.0-rc1" -source = "git+https://github.com/stellar/rs-stellar-xdr?rev=9c97e4fa909a0b6455547a4f4a95800696b2a69a#9c97e4fa909a0b6455547a4f4a95800696b2a69a" +source = "git+https://github.com/stellar/rs-stellar-xdr?rev=d6f8ece2c89809d5e2800b9df64ae60787ee492b#d6f8ece2c89809d5e2800b9df64ae60787ee492b" dependencies = [ "arbitrary", "base64 0.13.1", @@ -1155,6 +1169,7 @@ dependencies = [ "hex", "serde", "serde_with", + "stellar-strkey", ] [[package]] @@ -1255,9 +1270,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1265,9 +1280,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", @@ -1280,9 +1295,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1290,9 +1305,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", @@ -1303,9 +1318,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "wasmi_arena" diff --git a/services/horizon/internal/integration/contracts/Cargo.toml b/services/horizon/internal/integration/contracts/Cargo.toml index d27b45dd4f..72ada87404 100644 --- a/services/horizon/internal/integration/contracts/Cargo.toml +++ b/services/horizon/internal/integration/contracts/Cargo.toml @@ -25,4 +25,4 @@ lto = true [workspace.dependencies.soroban-sdk] version = "20.0.0-rc2" git = "https://github.com/stellar/rs-soroban-sdk" -rev = "729ed3ac5fe8600a3245d5816eadd3c95ab2eb54" +rev = "e35bace9de5addae7c32f405cdc11bb459cb1d61" diff --git a/services/horizon/internal/integration/testdata/soroban_add_u64.wasm b/services/horizon/internal/integration/testdata/soroban_add_u64.wasm index 91ca4537f6e738227283b49ed13edb55bf6ddf2a..9ffd8622b744ae3355a7a3fbda92b20afdba0dc5 100755 GIT binary patch delta 55 zcmey)@||S^BNHRTWF{sb%~WI4q{QS@%al~p#FUi8RP$tGqcjr()8v$7L&Kyb6I09N KB*PRlLk0lM#1G~G delta 55 zcmey)@||S^BNL;`WF{sbO>-m5)D+{yWYe@%3o`?QL}Mcp(-cz+L$lPxloaD+OVh+8 Kqtql*69xdeCl5&g diff --git a/services/horizon/internal/integration/testdata/soroban_increment_contract.wasm b/services/horizon/internal/integration/testdata/soroban_increment_contract.wasm index ddcbadea30ad4ca60ca158955ab4e0f5c9548019..b975e5ccd5f8c20f1ce9364a928321773712ce85 100755 GIT binary patch delta 55 zcmdnXx|el>I};I}@YJWDh1EO>-m5)D+{yWYe@%3o`?QL}Mcp(-cz+L$lPxloaD+OVh+8 Kqtql*69xdg6Axnm diff --git a/services/horizon/internal/integration/testdata/soroban_sac_test.wasm b/services/horizon/internal/integration/testdata/soroban_sac_test.wasm index 984fe5911e881be7f79b1c24a196d285064fd2b3..9424da50fd6f580b243c78ea1a342b13d2432169 100755 GIT binary patch delta 55 zcmZqSZ{gp-$Ii$wnV;Q9Gu7BMDKRN&@d^<#MCl5 K$uPytkO2Uvat}HH delta 55 zcmZqSZ{gp-$Ij?7nV;Q9)7;21HN`kF*)%QH!py)R(b&kuG{w}y&@44ECB-<|(ljy2 KC^gB{gaH7P)efBi diff --git a/services/horizon/internal/integration/testdata/soroban_store.wasm b/services/horizon/internal/integration/testdata/soroban_store.wasm index 1082f8335ec9131f2ea414e66e67e4b374e03d9c..7f839bd20839c603505278596459bc09871e5760 100755 GIT binary patch delta 55 zcmX@ee2{sAHzOm%WFJN!%~WI4q{QS@%al~p#FUi8RP$tGqcjr()8v$7L&Kyb6I09N KB*PRlLk0lQs}KqR delta 55 zcmX@ee2{sAHzT9VWFJN!O>-m5)D+{yWYe@%3o`?QL}Mcp(-cz+L$lPxloaD+OVh+8 Kqtql*69xdi4i9Yr diff --git a/services/horizon/internal/test/integration/integration.go b/services/horizon/internal/test/integration/integration.go index 9bd12e28f5..87718d5a67 100644 --- a/services/horizon/internal/test/integration/integration.go +++ b/services/horizon/internal/test/integration/integration.go @@ -714,7 +714,7 @@ func (i *Test) WaitUntilLedgerEntryTTL(ledgerKey xdr.LedgerKey) { for attempt := 0; attempt < 50; attempt++ { var result struct { Entries []struct { - LiveUntilLedgerSeq *uint32 `json:"liveUntilLedgerSeqLedgerSeq,string,omitempty"` + LiveUntilLedgerSeq *uint32 `json:"liveUntilLedgerSeq,omitempty"` } `json:"entries"` } err := client.CallResult(context.Background(), "getLedgerEntries", request, &result) From 9ba150db67c1fe6dc66e2c7e77bdb9fe6e33774a Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 4 Dec 2023 18:36:05 +0000 Subject: [PATCH 015/234] Update xdrgen and xdr definitions (#5136) --- Makefile | 4 +- gxdr/xdr_generated.go | 6 +- xdr/Stellar-transaction.x | 6 +- xdr/xdr_commit_generated.txt | 2 +- xdr/xdr_generated.go | 162 +++++++++++++++++------------------ 5 files changed, 88 insertions(+), 92 deletions(-) diff --git a/Makefile b/Makefile index 7b0cda6df6..08b25c5665 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,8 @@ xdr/Stellar-contract.x \ xdr/Stellar-internal.x \ xdr/Stellar-contract-config-setting.x -XDRGEN_COMMIT=f9995ef529eb83db6b70206a0a857dc88e33c750 -XDR_COMMIT=6a620d160aab22609c982d54578ff6a63bfcdc01 +XDRGEN_COMMIT=e2cac557162d99b12ae73b846cf3d5bfe16636de +XDR_COMMIT=bb54e505f814386a3f45172e0b7e95b7badbe969 .PHONY: xdr xdr-clean xdr-update diff --git a/gxdr/xdr_generated.go b/gxdr/xdr_generated.go index 2c20497952..67270fcbbd 100644 --- a/gxdr/xdr_generated.go +++ b/gxdr/xdr_generated.go @@ -2159,7 +2159,7 @@ type SorobanAuthorizationEntry struct { } /* -Upload WASM, create, and invoke contracts in Soroban. +Upload Wasm, create, and invoke contracts in Soroban. Threshold: med Result: InvokeHostFunctionResult @@ -2176,7 +2176,7 @@ Extend the TTL of the entries specified in the readOnly footprint so they will live at least extendTo ledgers from lcl. - Threshold: med + Threshold: low Result: ExtendFootprintTTLResult */ type ExtendFootprintTTLOp struct { @@ -2187,7 +2187,7 @@ type ExtendFootprintTTLOp struct { /* Restore the archived entries specified in the readWrite footprint. - Threshold: med + Threshold: low Result: RestoreFootprintOp */ type RestoreFootprintOp struct { diff --git a/xdr/Stellar-transaction.x b/xdr/Stellar-transaction.x index c7f0f5e276..87dd32d385 100644 --- a/xdr/Stellar-transaction.x +++ b/xdr/Stellar-transaction.x @@ -572,7 +572,7 @@ struct SorobanAuthorizationEntry SorobanAuthorizedInvocation rootInvocation; }; -/* Upload WASM, create, and invoke contracts in Soroban. +/* Upload Wasm, create, and invoke contracts in Soroban. Threshold: med Result: InvokeHostFunctionResult @@ -588,7 +588,7 @@ struct InvokeHostFunctionOp /* Extend the TTL of the entries specified in the readOnly footprint so they will live at least extendTo ledgers from lcl. - Threshold: med + Threshold: low Result: ExtendFootprintTTLResult */ struct ExtendFootprintTTLOp @@ -599,7 +599,7 @@ struct ExtendFootprintTTLOp /* Restore the archived entries specified in the readWrite footprint. - Threshold: med + Threshold: low Result: RestoreFootprintOp */ struct RestoreFootprintOp diff --git a/xdr/xdr_commit_generated.txt b/xdr/xdr_commit_generated.txt index 3c6d18d90d..9746cc5569 100644 --- a/xdr/xdr_commit_generated.txt +++ b/xdr/xdr_commit_generated.txt @@ -1 +1 @@ -6a620d160aab22609c982d54578ff6a63bfcdc01 \ No newline at end of file +bb54e505f814386a3f45172e0b7e95b7badbe969 \ No newline at end of file diff --git a/xdr/xdr_generated.go b/xdr/xdr_generated.go index d4d6b12107..e8f49981ff 100644 --- a/xdr/xdr_generated.go +++ b/xdr/xdr_generated.go @@ -25,14 +25,10 @@ import ( "errors" "fmt" "io" - "unsafe" "github.com/stellar/go-xdr/xdr3" ) -// Needed since unsafe is not used in all cases -var _ = unsafe.Sizeof(0) - // XdrFilesSHA256 is the SHA256 hashes of source files. var XdrFilesSHA256 = map[string]string{ "xdr/Stellar-SCP.x": "8f32b04d008f8bc33b8843d075e69837231a673691ee41d8b821ca229a6e802a", @@ -45,7 +41,7 @@ var XdrFilesSHA256 = map[string]string{ "xdr/Stellar-ledger-entries.x": "4f8f2324f567a40065f54f696ea1428740f043ea4154f5986d9f499ad00ac333", "xdr/Stellar-ledger.x": "2c842f3fe6e269498af5467f849cf6818554e90babc845f34c87cda471298d0f", "xdr/Stellar-overlay.x": "de3957c58b96ae07968b3d3aebea84f83603e95322d1fa336360e13e3aba737a", - "xdr/Stellar-transaction.x": "c5dd8507bc84e10b67bf3bc74c8f716a660b425814b015025c6f6b6f20cb70e7", + "xdr/Stellar-transaction.x": "0d2b35a331a540b48643925d0869857236eb2487c02d340ea32e365e784ea2b8", "xdr/Stellar-types.x": "6e3b13f0d3e360b09fa5e2b0e55d43f4d974a769df66afb34e8aecbb329d3f15", } @@ -384,7 +380,7 @@ func (s *ScpNomination) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding Value: length (%d) exceeds remaining input length (%d)", l, il) } s.Votes = make([]Value, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Votes[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -403,7 +399,7 @@ func (s *ScpNomination) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding Value: length (%d) exceeds remaining input length (%d)", l, il) } s.Accepted = make([]Value, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Accepted[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -1344,7 +1340,7 @@ func (s *ScpQuorumSet) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding NodeId: length (%d) exceeds remaining input length (%d)", l, il) } s.Validators = make([]NodeId, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Validators[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -1363,7 +1359,7 @@ func (s *ScpQuorumSet) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding ScpQuorumSet: length (%d) exceeds remaining input length (%d)", l, il) } s.InnerSets = make([]ScpQuorumSet, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.InnerSets[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -3444,7 +3440,7 @@ func (s *AccountEntryExtensionV2) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int return n, fmt.Errorf("decoding SponsorshipDescriptor: length (%d) exceeds remaining input length (%d)", l, il) } s.SignerSponsoringIDs = make([]SponsorshipDescriptor, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { var eb bool eb, nTmp, err = d.DecodeBool() n += nTmp @@ -4049,7 +4045,7 @@ func (s *AccountEntry) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding Signer: length (%d) exceeds remaining input length (%d)", l, il) } s.Signers = make([]Signer, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Signers[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -6225,7 +6221,7 @@ func (u *ClaimPredicate) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding ClaimPredicate: length (%d) exceeds remaining input length (%d)", l, il) } (*u.AndPredicates) = make([]ClaimPredicate, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*u.AndPredicates)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -6251,7 +6247,7 @@ func (u *ClaimPredicate) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding ClaimPredicate: length (%d) exceeds remaining input length (%d)", l, il) } (*u.OrPredicates) = make([]ClaimPredicate, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*u.OrPredicates)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -7372,7 +7368,7 @@ func (s *ClaimableBalanceEntry) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, return n, fmt.Errorf("decoding Claimant: length (%d) exceeds remaining input length (%d)", l, il) } s.Claimants = make([]Claimant, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Claimants[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -11180,7 +11176,7 @@ func (s *StellarValue) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding UpgradeType: length (%d) exceeds remaining input length (%d)", l, il) } s.Upgrades = make([]UpgradeType, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Upgrades[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -12550,7 +12546,7 @@ func (s *ConfigUpgradeSet) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error return n, fmt.Errorf("decoding ConfigSettingEntry: length (%d) exceeds remaining input length (%d)", l, il) } s.UpdatedEntry = make([]ConfigSettingEntry, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.UpdatedEntry[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -13292,7 +13288,7 @@ func (s *TxSetComponentTxsMaybeDiscountedFee) DecodeFrom(d *xdr.Decoder, maxDept return n, fmt.Errorf("decoding TransactionEnvelope: length (%d) exceeds remaining input length (%d)", l, il) } s.Txs = make([]TransactionEnvelope, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Txs[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -13594,7 +13590,7 @@ func (u *TransactionPhase) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error return n, fmt.Errorf("decoding TxSetComponent: length (%d) exceeds remaining input length (%d)", l, il) } (*u.V0Components) = make([]TxSetComponent, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*u.V0Components)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -13691,7 +13687,7 @@ func (s *TransactionSet) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding TransactionEnvelope: length (%d) exceeds remaining input length (%d)", l, il) } s.Txs = make([]TransactionEnvelope, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Txs[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -13786,7 +13782,7 @@ func (s *TransactionSetV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error return n, fmt.Errorf("decoding TransactionPhase: length (%d) exceeds remaining input length (%d)", l, il) } s.Phases = make([]TransactionPhase, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Phases[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -14087,7 +14083,7 @@ func (s *TransactionResultSet) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, e return n, fmt.Errorf("decoding TransactionResultPair: length (%d) exceeds remaining input length (%d)", l, il) } s.Results = make([]TransactionResultPair, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Results[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -14816,7 +14812,7 @@ func (s *LedgerScpMessages) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding ScpEnvelope: length (%d) exceeds remaining input length (%d)", l, il) } s.Messages = make([]ScpEnvelope, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Messages[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -14906,7 +14902,7 @@ func (s *ScpHistoryEntryV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding ScpQuorumSet: length (%d) exceeds remaining input length (%d)", l, il) } s.QuorumSets = make([]ScpQuorumSet, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.QuorumSets[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -15515,7 +15511,7 @@ func (s *LedgerEntryChanges) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, err return n, fmt.Errorf("decoding LedgerEntryChange: length (%d) exceeds remaining input length (%d)", l, il) } (*s) = make([]LedgerEntryChange, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -15675,7 +15671,7 @@ func (s *TransactionMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding OperationMeta: length (%d) exceeds remaining input length (%d)", l, il) } s.Operations = make([]OperationMeta, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Operations[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -15777,7 +15773,7 @@ func (s *TransactionMetaV2) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding OperationMeta: length (%d) exceeds remaining input length (%d)", l, il) } s.Operations = make([]OperationMeta, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Operations[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -15963,7 +15959,7 @@ func (s *ContractEventV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding ScVal: length (%d) exceeds remaining input length (%d)", l, il) } s.Topics = make([]ScVal, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Topics[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -16426,7 +16422,7 @@ func (s *SorobanTransactionMeta) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, return n, fmt.Errorf("decoding ContractEvent: length (%d) exceeds remaining input length (%d)", l, il) } s.Events = make([]ContractEvent, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Events[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -16450,7 +16446,7 @@ func (s *SorobanTransactionMeta) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, return n, fmt.Errorf("decoding DiagnosticEvent: length (%d) exceeds remaining input length (%d)", l, il) } s.DiagnosticEvents = make([]DiagnosticEvent, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.DiagnosticEvents[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -16574,7 +16570,7 @@ func (s *TransactionMetaV3) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding OperationMeta: length (%d) exceeds remaining input length (%d)", l, il) } s.Operations = make([]OperationMeta, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Operations[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -16689,7 +16685,7 @@ func (s *InvokeHostFunctionSuccessPreImage) DecodeFrom(d *xdr.Decoder, maxDepth return n, fmt.Errorf("decoding ContractEvent: length (%d) exceeds remaining input length (%d)", l, il) } s.Events = make([]ContractEvent, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Events[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -16973,7 +16969,7 @@ func (u *TransactionMeta) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding OperationMeta: length (%d) exceeds remaining input length (%d)", l, il) } (*u.Operations) = make([]OperationMeta, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*u.Operations)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17293,7 +17289,7 @@ func (s *LedgerCloseMetaV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding TransactionResultMeta: length (%d) exceeds remaining input length (%d)", l, il) } s.TxProcessing = make([]TransactionResultMeta, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.TxProcessing[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17312,7 +17308,7 @@ func (s *LedgerCloseMetaV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding UpgradeEntryMeta: length (%d) exceeds remaining input length (%d)", l, il) } s.UpgradesProcessing = make([]UpgradeEntryMeta, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.UpgradesProcessing[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17331,7 +17327,7 @@ func (s *LedgerCloseMetaV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding ScpHistoryEntry: length (%d) exceeds remaining input length (%d)", l, il) } s.ScpInfo = make([]ScpHistoryEntry, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.ScpInfo[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17511,7 +17507,7 @@ func (s *LedgerCloseMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding TransactionResultMeta: length (%d) exceeds remaining input length (%d)", l, il) } s.TxProcessing = make([]TransactionResultMeta, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.TxProcessing[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17530,7 +17526,7 @@ func (s *LedgerCloseMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding UpgradeEntryMeta: length (%d) exceeds remaining input length (%d)", l, il) } s.UpgradesProcessing = make([]UpgradeEntryMeta, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.UpgradesProcessing[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17549,7 +17545,7 @@ func (s *LedgerCloseMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding ScpHistoryEntry: length (%d) exceeds remaining input length (%d)", l, il) } s.ScpInfo = make([]ScpHistoryEntry, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.ScpInfo[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17573,7 +17569,7 @@ func (s *LedgerCloseMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding LedgerKey: length (%d) exceeds remaining input length (%d)", l, il) } s.EvictedTemporaryLedgerKeys = make([]LedgerKey, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.EvictedTemporaryLedgerKeys[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -17592,7 +17588,7 @@ func (s *LedgerCloseMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding LedgerEntry: length (%d) exceeds remaining input length (%d)", l, il) } s.EvictedPersistentLedgerEntries = make([]LedgerEntry, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.EvictedPersistentLedgerEntries[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -19884,7 +19880,7 @@ func (s *PeerStatList) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding PeerStats: length (%d) exceeds remaining input length (%d)", l, il) } (*s) = make([]PeerStats, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -20380,7 +20376,7 @@ func (s *TxAdvertVector) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding Hash: length (%d) exceeds remaining input length (%d)", l, il) } (*s) = make([]Hash, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -20538,7 +20534,7 @@ func (s *TxDemandVector) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding Hash: length (%d) exceeds remaining input length (%d)", l, il) } (*s) = make([]Hash, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -21571,7 +21567,7 @@ func (u *StellarMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding PeerAddress: length (%d) exceeds remaining input length (%d)", l, il) } (*u.Peers) = make([]PeerAddress, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*u.Peers)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -22869,7 +22865,7 @@ func (s *PathPaymentStrictReceiveOp) DecodeFrom(d *xdr.Decoder, maxDepth uint) ( return n, fmt.Errorf("decoding Asset: length (%d) exceeds remaining input length (%d)", l, il) } s.Path = make([]Asset, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Path[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -23011,7 +23007,7 @@ func (s *PathPaymentStrictSendOp) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int return n, fmt.Errorf("decoding Asset: length (%d) exceeds remaining input length (%d)", l, il) } s.Path = make([]Asset, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Path[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -24284,7 +24280,7 @@ func (s *CreateClaimableBalanceOp) DecodeFrom(d *xdr.Decoder, maxDepth uint) (in return n, fmt.Errorf("decoding Claimant: length (%d) exceeds remaining input length (%d)", l, il) } s.Claimants = make([]Claimant, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Claimants[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -25849,7 +25845,7 @@ func (s *InvokeContractArgs) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, err return n, fmt.Errorf("decoding ScVal: length (%d) exceeds remaining input length (%d)", l, il) } s.Args = make([]ScVal, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Args[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -26462,7 +26458,7 @@ func (s *SorobanAuthorizedInvocation) DecodeFrom(d *xdr.Decoder, maxDepth uint) return n, fmt.Errorf("decoding SorobanAuthorizedInvocation: length (%d) exceeds remaining input length (%d)", l, il) } s.SubInvocations = make([]SorobanAuthorizedInvocation, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.SubInvocations[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -26969,7 +26965,7 @@ func (s *InvokeHostFunctionOp) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, e return n, fmt.Errorf("decoding SorobanAuthorizationEntry: length (%d) exceeds remaining input length (%d)", l, il) } s.Auth = make([]SorobanAuthorizationEntry, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Auth[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -30049,7 +30045,7 @@ func (s *PreconditionsV2) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding SignerKey: length (%d) exceeds remaining input length (%d)", l, il) } s.ExtraSigners = make([]SignerKey, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.ExtraSigners[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -30437,7 +30433,7 @@ func (s *LedgerFootprint) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding LedgerKey: length (%d) exceeds remaining input length (%d)", l, il) } s.ReadOnly = make([]LedgerKey, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.ReadOnly[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -30456,7 +30452,7 @@ func (s *LedgerFootprint) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding LedgerKey: length (%d) exceeds remaining input length (%d)", l, il) } s.ReadWrite = make([]LedgerKey, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.ReadWrite[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -30915,7 +30911,7 @@ func (s *TransactionV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding Operation: length (%d) exceeds remaining input length (%d)", l, il) } s.Operations = make([]Operation, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Operations[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -31020,7 +31016,7 @@ func (s *TransactionV0Envelope) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, return n, fmt.Errorf("decoding DecoratedSignature: length (%d) exceeds remaining input length (%d)", l, il) } s.Signatures = make([]DecoratedSignature, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Signatures[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -31333,7 +31329,7 @@ func (s *Transaction) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding Operation: length (%d) exceeds remaining input length (%d)", l, il) } s.Operations = make([]Operation, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Operations[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -31438,7 +31434,7 @@ func (s *TransactionV1Envelope) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, return n, fmt.Errorf("decoding DecoratedSignature: length (%d) exceeds remaining input length (%d)", l, il) } s.Signatures = make([]DecoratedSignature, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Signatures[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -31885,7 +31881,7 @@ func (s *FeeBumpTransactionEnvelope) DecodeFrom(d *xdr.Decoder, maxDepth uint) ( return n, fmt.Errorf("decoding DecoratedSignature: length (%d) exceeds remaining input length (%d)", l, il) } s.Signatures = make([]DecoratedSignature, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Signatures[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -33952,7 +33948,7 @@ func (s *PathPaymentStrictReceiveResultSuccess) DecodeFrom(d *xdr.Decoder, maxDe return n, fmt.Errorf("decoding ClaimAtom: length (%d) exceeds remaining input length (%d)", l, il) } s.Offers = make([]ClaimAtom, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Offers[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -34497,7 +34493,7 @@ func (s *PathPaymentStrictSendResultSuccess) DecodeFrom(d *xdr.Decoder, maxDepth return n, fmt.Errorf("decoding ClaimAtom: length (%d) exceeds remaining input length (%d)", l, il) } s.Offers = make([]ClaimAtom, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Offers[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -35316,7 +35312,7 @@ func (s *ManageOfferSuccessResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) (in return n, fmt.Errorf("decoding ClaimAtom: length (%d) exceeds remaining input length (%d)", l, il) } s.OffersClaimed = make([]ClaimAtom, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.OffersClaimed[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -37567,7 +37563,7 @@ func (u *InflationResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding InflationPayout: length (%d) exceeds remaining input length (%d)", l, il) } (*u.Payouts) = make([]InflationPayout, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*u.Payouts)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -43597,7 +43593,7 @@ func (u *InnerTransactionResultResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) return n, fmt.Errorf("decoding OperationResult: length (%d) exceeds remaining input length (%d)", l, il) } (*u.Results) = make([]OperationResult, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*u.Results)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -43620,7 +43616,7 @@ func (u *InnerTransactionResultResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) return n, fmt.Errorf("decoding OperationResult: length (%d) exceeds remaining input length (%d)", l, il) } (*u.Results) = make([]OperationResult, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*u.Results)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -44335,7 +44331,7 @@ func (u *TransactionResultResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int return n, fmt.Errorf("decoding OperationResult: length (%d) exceeds remaining input length (%d)", l, il) } (*u.Results) = make([]OperationResult, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*u.Results)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -44358,7 +44354,7 @@ func (u *TransactionResultResult) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int return n, fmt.Errorf("decoding OperationResult: length (%d) exceeds remaining input length (%d)", l, il) } (*u.Results) = make([]OperationResult, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*u.Results)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -47652,7 +47648,7 @@ func (s *ScSpecTypeTuple) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding ScSpecTypeDef: length (%d) exceeds remaining input length (%d)", l, il) } s.ValueTypes = make([]ScSpecTypeDef, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.ValueTypes[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -48624,7 +48620,7 @@ func (s *ScSpecUdtStructV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro return n, fmt.Errorf("decoding ScSpecUdtStructFieldV0: length (%d) exceeds remaining input length (%d)", l, il) } s.Fields = make([]ScSpecUdtStructFieldV0, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Fields[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -48807,7 +48803,7 @@ func (s *ScSpecUdtUnionCaseTupleV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (i return n, fmt.Errorf("decoding ScSpecTypeDef: length (%d) exceeds remaining input length (%d)", l, il) } s.Type = make([]ScSpecTypeDef, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Type[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -49203,7 +49199,7 @@ func (s *ScSpecUdtUnionV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error return n, fmt.Errorf("decoding ScSpecUdtUnionCaseV0: length (%d) exceeds remaining input length (%d)", l, il) } s.Cases = make([]ScSpecUdtUnionCaseV0, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Cases[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -49406,7 +49402,7 @@ func (s *ScSpecUdtEnumV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding ScSpecUdtEnumCaseV0: length (%d) exceeds remaining input length (%d)", l, il) } s.Cases = make([]ScSpecUdtEnumCaseV0, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Cases[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -49609,7 +49605,7 @@ func (s *ScSpecUdtErrorEnumV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, e return n, fmt.Errorf("decoding ScSpecUdtErrorEnumCaseV0: length (%d) exceeds remaining input length (%d)", l, il) } s.Cases = make([]ScSpecUdtErrorEnumCaseV0, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Cases[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -49812,7 +49808,7 @@ func (s *ScSpecFunctionV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error return n, fmt.Errorf("decoding ScSpecFunctionInputV0: length (%d) exceeds remaining input length (%d)", l, il) } s.Inputs = make([]ScSpecFunctionInputV0, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Inputs[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -49834,7 +49830,7 @@ func (s *ScSpecFunctionV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error return n, fmt.Errorf("decoding ScSpecTypeDef: length (%d) exceeds remaining input length (%d)", l, il) } s.Outputs = make([]ScSpecTypeDef, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.Outputs[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -51985,7 +51981,7 @@ func (s *ScVec) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding ScVal: length (%d) exceeds remaining input length (%d)", l, il) } (*s) = make([]ScVal, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -52065,7 +52061,7 @@ func (s *ScMap) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding ScMapEntry: length (%d) exceeds remaining input length (%d)", l, il) } (*s) = make([]ScMapEntry, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -54020,7 +54016,7 @@ func (s *PersistedScpStateV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, er return n, fmt.Errorf("decoding ScpEnvelope: length (%d) exceeds remaining input length (%d)", l, il) } s.ScpEnvelopes = make([]ScpEnvelope, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.ScpEnvelopes[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -54039,7 +54035,7 @@ func (s *PersistedScpStateV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, er return n, fmt.Errorf("decoding ScpQuorumSet: length (%d) exceeds remaining input length (%d)", l, il) } s.QuorumSets = make([]ScpQuorumSet, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.QuorumSets[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -54058,7 +54054,7 @@ func (s *PersistedScpStateV0) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, er return n, fmt.Errorf("decoding StoredTransactionSet: length (%d) exceeds remaining input length (%d)", l, il) } s.TxSets = make([]StoredTransactionSet, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.TxSets[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -54154,7 +54150,7 @@ func (s *PersistedScpStateV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, er return n, fmt.Errorf("decoding ScpEnvelope: length (%d) exceeds remaining input length (%d)", l, il) } s.ScpEnvelopes = make([]ScpEnvelope, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.ScpEnvelopes[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -54173,7 +54169,7 @@ func (s *PersistedScpStateV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, er return n, fmt.Errorf("decoding ScpQuorumSet: length (%d) exceeds remaining input length (%d)", l, il) } s.QuorumSets = make([]ScpQuorumSet, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = s.QuorumSets[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -55577,7 +55573,7 @@ func (s *ContractCostParams) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, err return n, fmt.Errorf("decoding ContractCostParamEntry: length (%d) exceeds remaining input length (%d)", l, il) } (*s) = make([]ContractCostParamEntry, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { @@ -56501,7 +56497,7 @@ func (u *ConfigSettingEntry) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, err return n, fmt.Errorf("decoding Uint64: length (%d) exceeds remaining input length (%d)", l, il) } (*u.BucketListSizeWindow) = make([]Uint64, l) - for i := uint32(0); uint(i) < uint(l); i++ { + for i := uint32(0); i < l; i++ { nTmp, err = (*u.BucketListSizeWindow)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { From c6f9070ee73d364016d59fa1c66d07bbb3e8a015 Mon Sep 17 00:00:00 2001 From: tamirms Date: Wed, 6 Dec 2023 19:07:10 +0000 Subject: [PATCH 016/234] Bump Core and soroban-rpc versions (#5137) --- .github/workflows/horizon.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index 8cae10766d..6b94521e0a 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -33,11 +33,11 @@ jobs: env: HORIZON_INTEGRATION_TESTS_ENABLED: true HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL: ${{ matrix.protocol-version }} - PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 19.14.1-1590.b6b730a0c.focal - PROTOCOL_20_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:19.14.1-1590.b6b730a0c.focal - PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:20.0.0-rc4231129-43 - PROTOCOL_19_CORE_DEBIAN_PKG_VERSION: 19.12.0-1378.2109a168a.focal - PROTOCOL_19_CORE_DOCKER_IMG: stellar/stellar-core:19.12.0-1378.2109a168a.focal + PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 20.0.0-1615.617729910.focal + PROTOCOL_20_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:20.0.0-1615.617729910.focal + PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:20.0.0-tests-45 + PROTOCOL_19_CORE_DEBIAN_PKG_VERSION: 19.14.0-1500.5664eff4e.focal + PROTOCOL_19_CORE_DOCKER_IMG: stellar/stellar-core:19.14.0-1500.5664eff4e.focal PGHOST: localhost PGPORT: 5432 PGUSER: postgres From 17d92caded4a700d5178a27710d7d2671028de91 Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 11 Dec 2023 18:27:52 +0000 Subject: [PATCH 017/234] Reorder remove unused indexes migration --- .../horizon/internal/db2/schema/bindata.go | 38 +++++++++---------- ...dexes.sql => 67_remove_unused_indexes.sql} | 0 2 files changed, 19 insertions(+), 19 deletions(-) rename services/horizon/internal/db2/schema/migrations/{65_remove_unused_indexes.sql => 67_remove_unused_indexes.sql} (100%) diff --git a/services/horizon/internal/db2/schema/bindata.go b/services/horizon/internal/db2/schema/bindata.go index c22c742949..3c68f6b2fd 100644 --- a/services/horizon/internal/db2/schema/bindata.go +++ b/services/horizon/internal/db2/schema/bindata.go @@ -62,8 +62,8 @@ // migrations/63_add_contract_id_to_asset_stats.sql (153B) // migrations/64_add_payment_flag_history_ops.sql (145B) // migrations/65_drop_payment_index.sql (260B) -// migrations/65_remove_unused_indexes.sql (2.897kB) // migrations/66_contract_asset_stats.sql (583B) +// migrations/67_remove_unused_indexes.sql (2.897kB) // migrations/6_create_assets_table.sql (366B) // migrations/7_modify_trades_table.sql (2.303kB) // migrations/8_add_aggregators.sql (907B) @@ -1377,43 +1377,43 @@ func migrations65_drop_payment_indexSql() (*asset, error) { return a, nil } -var _migrations65_remove_unused_indexesSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xa4\x55\xc9\x6e\xdb\x30\x10\xbd\xe7\x2b\x78\x8b\x8d\xc6\xa7\xdc\x12\x34\x40\x5b\xab\xad\x2f\x76\xeb\x05\xcd\x8d\xa0\xc4\xb1\xc5\x46\xe2\x08\xe4\x28\x8d\xff\xbe\x90\xe5\x85\x92\x45\x2d\xc9\x4d\x10\xdf\x9b\x37\x9c\xe5\x71\x32\x61\x9f\x52\xb5\x33\x82\x80\x6d\x32\xa6\x91\x8c\xd0\x56\x44\xa4\x50\xdf\xdc\x4c\x97\x8b\x5f\x6c\x36\x9f\x06\xcf\x6c\xf6\x9d\x05\xcf\xb3\xd5\x7a\xc5\x44\x14\x61\xae\xc9\xf2\x18\x53\xe0\x12\x53\xa1\xf4\x63\x07\x54\xe9\x6d\x22\x8a\x98\x5c\x82\x25\xa5\x0f\xdf\x3e\x92\xb5\x40\x3c\xdc\x73\x65\x6d\x0e\xc6\x83\x8a\x12\xa1\x52\x11\x8a\x30\x01\x1e\x8a\x44\xe8\x08\x6c\x41\x2a\xff\x6b\xb2\x1e\x5e\x4c\x46\x72\x52\x29\xf0\x04\xf1\x25\xcf\x3c\x30\xa5\x25\xbc\xf1\x58\x59\x42\xb3\xe7\xb0\xdd\x42\x44\x96\xa3\xe6\xb4\xcf\xa0\x17\x27\x01\xb9\x03\x73\xe0\x44\x09\x5a\x90\x5c\xd0\x50\xa2\x4a\x33\x34\x04\x86\xbf\x82\xb1\xfe\x8a\x79\xf9\xe5\x27\x8f\x85\x8d\x87\x52\x33\x03\xaf\x0a\x73\x3b\x38\x06\x66\x60\xca\x56\x1f\x5b\xe4\x36\xa8\xb8\x93\xf4\xb5\xc6\x13\x67\x58\xd9\x9d\xf9\x1d\x9c\x00\x19\x21\xe1\xdc\xec\x70\xcf\xd1\x48\x30\x3c\x44\x7c\xf1\x32\x72\x4b\x3c\x51\xba\x1c\xbe\xd3\xc4\xde\xb8\x6b\x35\xc5\x7f\xba\xb6\x58\xdf\x96\xc1\x97\x75\x70\x89\x36\x5f\xac\xdb\xb6\x8b\x2d\xe6\xe7\xff\x6c\xb3\x9a\xcd\x7f\xb0\xaf\xeb\x65\x10\x8c\x1c\xcc\xf8\xb1\x57\xd4\xc6\x45\xf4\xc6\x6f\x44\x77\x28\x55\xb7\xb7\x08\x7d\x6a\xcd\xe1\xe8\x24\x10\x92\x01\x60\xa3\x12\x5e\x62\xdb\x03\x77\x2e\x7c\x21\x75\xdd\xf1\xa3\xdc\x4e\xe9\xd1\x05\xf9\xd7\xa2\x0e\x79\x26\x28\xe6\x98\xd9\x76\xdd\xba\x61\xb8\x37\x3a\x0c\x4c\xb5\x64\xc7\x75\x39\xaf\x7c\x7b\xf0\x56\x9b\x71\x95\x8e\x47\xd5\xe2\x15\xa0\x21\xf1\x9b\x2c\xc9\x15\x39\x9e\x57\x45\xde\x75\x93\x16\x0f\xeb\x14\xac\x13\x2e\xba\x9b\xf9\xec\xf7\x66\xa0\xbc\x63\x5f\x9d\xca\x0e\xf6\x63\xa2\x4d\xe6\xd9\xa9\xde\x44\x7a\x47\x1a\x3d\xfc\xd7\x4d\xa5\x0d\x5e\xcd\xef\x9a\xa1\x24\xbb\x3b\x07\xba\xa2\x73\x25\x87\x8c\xcc\xb5\xdf\x37\xa6\xd9\xba\x02\xbd\x8b\xd4\xeb\x95\xa8\x6d\x7a\x0b\xa1\xb9\x50\x2e\xe7\x83\xa5\x6a\x79\x99\x3a\x5d\x62\x34\x92\x40\x42\x25\x96\x4d\x9e\x9e\xd8\xad\xc5\x44\x96\x56\x7c\x28\xf2\xed\xc3\x03\xc1\x1b\x8d\xc7\x77\xcc\x0f\x8c\x50\xf6\x03\x96\x46\xee\x87\x86\x98\xef\x62\xea\x25\x5f\x81\xb6\x27\x50\x81\xd6\x52\x18\xb3\x3f\x3f\x83\x65\x50\x4e\x0a\xfb\xcc\xee\xef\xbb\x2a\xdd\xf0\xa2\x17\x35\x76\x0e\x2a\x86\x5f\x7b\xc1\xfe\x07\x00\x00\xff\xff\x6e\x06\xca\xb9\x51\x0b\x00\x00") +var _migrations66_contract_asset_statsSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\x91\x41\x6b\xf2\x40\x10\x86\xef\xf9\x15\x2f\x9e\x94\xcf\xc0\xd7\x7a\xa9\x78\x8a\x35\x14\x5b\x9b\x48\x8c\x50\x4f\xcb\x66\x33\xc6\xa1\xba\x91\xdd\x91\xd6\x7f\x5f\x24\x44\x0b\x56\xf7\x3a\xcf\xcc\xf3\xce\x4e\x18\xe2\xdf\x8e\x2b\xa7\x85\xb0\xdc\x07\xcf\x59\x1c\xe5\x31\xf2\x68\x3c\x8b\x61\x6a\x2b\x4e\x1b\x51\xda\x7b\x12\xe5\x45\x8b\x47\x37\xc0\xe9\x9d\x6b\x5c\x62\xbc\xca\xe3\x08\xf3\x6c\xfa\x1e\x65\x2b\xbc\xc5\xab\x7e\xc3\x9c\x1a\xd0\xbc\xd7\x45\x9a\x8c\x91\xa4\x39\x92\xe5\x6c\x16\xf4\x46\xc1\x5d\x55\xa1\xb7\xda\x1a\x3a\xdb\x3e\xe9\xa8\x36\xda\x6f\x6e\xaa\x9a\xb6\xeb\x50\xad\xb1\xc5\x76\xf5\xc1\xb6\x99\xec\x61\x47\x8e\x4d\x77\x30\xec\xff\xef\x5d\x48\x84\x21\x06\x43\x94\x5c\xb1\x78\xb0\x87\x3f\xac\xd7\x6c\x98\xac\x60\x5d\x3b\x68\x3c\x3c\x3e\xa1\x60\x01\x5b\xa1\x8a\x5c\x33\x9a\xbe\xf7\xec\xb4\x70\x6d\xd5\x96\xca\x8a\x5c\x5b\xfe\x73\xeb\x69\x32\x89\x3f\xd0\xb9\xb1\xb6\x2a\x8e\xea\x32\xaf\x83\x34\xb9\xf9\x41\xcb\xc5\x34\x79\x41\x21\x8e\x08\xdd\xab\x0c\x27\xe3\xef\x0b\x4f\xea\x2f\x1b\x4c\xb2\x74\x7e\xef\xc2\x46\x7b\xa3\x4b\x1a\xdd\x01\xcf\xfa\x96\xfd\x09\x00\x00\xff\xff\x78\xde\xe2\x11\x47\x02\x00\x00") -func migrations65_remove_unused_indexesSqlBytes() ([]byte, error) { +func migrations66_contract_asset_statsSqlBytes() ([]byte, error) { return bindataRead( - _migrations65_remove_unused_indexesSql, - "migrations/65_remove_unused_indexes.sql", + _migrations66_contract_asset_statsSql, + "migrations/66_contract_asset_stats.sql", ) } -func migrations65_remove_unused_indexesSql() (*asset, error) { - bytes, err := migrations65_remove_unused_indexesSqlBytes() +func migrations66_contract_asset_statsSql() (*asset, error) { + bytes, err := migrations66_contract_asset_statsSqlBytes() if err != nil { return nil, err } - info := bindataFileInfo{name: "migrations/65_remove_unused_indexes.sql", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x99, 0x6, 0xc9, 0x3, 0x51, 0x52, 0x77, 0x87, 0x7f, 0x5, 0xe2, 0xdd, 0xd2, 0x49, 0xd0, 0xd1, 0x1b, 0x9a, 0x9, 0x2f, 0x10, 0xb7, 0x68, 0x44, 0xd1, 0x12, 0xce, 0x43, 0x50, 0x2e, 0xbd, 0xef}} + info := bindataFileInfo{name: "migrations/66_contract_asset_stats.sql", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x2a, 0x9a, 0xab, 0xd3, 0x1d, 0x20, 0x82, 0x82, 0x64, 0xb1, 0x7e, 0x9f, 0xcb, 0xb5, 0xe6, 0x2b, 0xc2, 0x72, 0x8e, 0x1e, 0x58, 0x23, 0xc, 0xfd, 0x25, 0xb3, 0xe7, 0x9, 0x89, 0x43, 0x3e, 0x15}} return a, nil } -var _migrations66_contract_asset_statsSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\x91\x41\x6b\xf2\x40\x10\x86\xef\xf9\x15\x2f\x9e\x94\xcf\xc0\xd7\x7a\xa9\x78\x8a\x35\x14\x5b\x9b\x48\x8c\x50\x4f\xcb\x66\x33\xc6\xa1\xba\x91\xdd\x91\xd6\x7f\x5f\x24\x44\x0b\x56\xf7\x3a\xcf\xcc\xf3\xce\x4e\x18\xe2\xdf\x8e\x2b\xa7\x85\xb0\xdc\x07\xcf\x59\x1c\xe5\x31\xf2\x68\x3c\x8b\x61\x6a\x2b\x4e\x1b\x51\xda\x7b\x12\xe5\x45\x8b\x47\x37\xc0\xe9\x9d\x6b\x5c\x62\xbc\xca\xe3\x08\xf3\x6c\xfa\x1e\x65\x2b\xbc\xc5\xab\x7e\xc3\x9c\x1a\xd0\xbc\xd7\x45\x9a\x8c\x91\xa4\x39\x92\xe5\x6c\x16\xf4\x46\xc1\x5d\x55\xa1\xb7\xda\x1a\x3a\xdb\x3e\xe9\xa8\x36\xda\x6f\x6e\xaa\x9a\xb6\xeb\x50\xad\xb1\xc5\x76\xf5\xc1\xb6\x99\xec\x61\x47\x8e\x4d\x77\x30\xec\xff\xef\x5d\x48\x84\x21\x06\x43\x94\x5c\xb1\x78\xb0\x87\x3f\xac\xd7\x6c\x98\xac\x60\x5d\x3b\x68\x3c\x3c\x3e\xa1\x60\x01\x5b\xa1\x8a\x5c\x33\x9a\xbe\xf7\xec\xb4\x70\x6d\xd5\x96\xca\x8a\x5c\x5b\xfe\x73\xeb\x69\x32\x89\x3f\xd0\xb9\xb1\xb6\x2a\x8e\xea\x32\xaf\x83\x34\xb9\xf9\x41\xcb\xc5\x34\x79\x41\x21\x8e\x08\xdd\xab\x0c\x27\xe3\xef\x0b\x4f\xea\x2f\x1b\x4c\xb2\x74\x7e\xef\xc2\x46\x7b\xa3\x4b\x1a\xdd\x01\xcf\xfa\x96\xfd\x09\x00\x00\xff\xff\x78\xde\xe2\x11\x47\x02\x00\x00") +var _migrations67_remove_unused_indexesSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xa4\x55\xc9\x6e\xdb\x30\x10\xbd\xe7\x2b\x78\x8b\x8d\xc6\xa7\xdc\x12\x34\x40\x5b\xab\xad\x2f\x76\xeb\x05\xcd\x8d\xa0\xc4\xb1\xc5\x46\xe2\x08\xe4\x28\x8d\xff\xbe\x90\xe5\x85\x92\x45\x2d\xc9\x4d\x10\xdf\x9b\x37\x9c\xe5\x71\x32\x61\x9f\x52\xb5\x33\x82\x80\x6d\x32\xa6\x91\x8c\xd0\x56\x44\xa4\x50\xdf\xdc\x4c\x97\x8b\x5f\x6c\x36\x9f\x06\xcf\x6c\xf6\x9d\x05\xcf\xb3\xd5\x7a\xc5\x44\x14\x61\xae\xc9\xf2\x18\x53\xe0\x12\x53\xa1\xf4\x63\x07\x54\xe9\x6d\x22\x8a\x98\x5c\x82\x25\xa5\x0f\xdf\x3e\x92\xb5\x40\x3c\xdc\x73\x65\x6d\x0e\xc6\x83\x8a\x12\xa1\x52\x11\x8a\x30\x01\x1e\x8a\x44\xe8\x08\x6c\x41\x2a\xff\x6b\xb2\x1e\x5e\x4c\x46\x72\x52\x29\xf0\x04\xf1\x25\xcf\x3c\x30\xa5\x25\xbc\xf1\x58\x59\x42\xb3\xe7\xb0\xdd\x42\x44\x96\xa3\xe6\xb4\xcf\xa0\x17\x27\x01\xb9\x03\x73\xe0\x44\x09\x5a\x90\x5c\xd0\x50\xa2\x4a\x33\x34\x04\x86\xbf\x82\xb1\xfe\x8a\x79\xf9\xe5\x27\x8f\x85\x8d\x87\x52\x33\x03\xaf\x0a\x73\x3b\x38\x06\x66\x60\xca\x56\x1f\x5b\xe4\x36\xa8\xb8\x93\xf4\xb5\xc6\x13\x67\x58\xd9\x9d\xf9\x1d\x9c\x00\x19\x21\xe1\xdc\xec\x70\xcf\xd1\x48\x30\x3c\x44\x7c\xf1\x32\x72\x4b\x3c\x51\xba\x1c\xbe\xd3\xc4\xde\xb8\x6b\x35\xc5\x7f\xba\xb6\x58\xdf\x96\xc1\x97\x75\x70\x89\x36\x5f\xac\xdb\xb6\x8b\x2d\xe6\xe7\xff\x6c\xb3\x9a\xcd\x7f\xb0\xaf\xeb\x65\x10\x8c\x1c\xcc\xf8\xb1\x57\xd4\xc6\x45\xf4\xc6\x6f\x44\x77\x28\x55\xb7\xb7\x08\x7d\x6a\xcd\xe1\xe8\x24\x10\x92\x01\x60\xa3\x12\x5e\x62\xdb\x03\x77\x2e\x7c\x21\x75\xdd\xf1\xa3\xdc\x4e\xe9\xd1\x05\xf9\xd7\xa2\x0e\x79\x26\x28\xe6\x98\xd9\x76\xdd\xba\x61\xb8\x37\x3a\x0c\x4c\xb5\x64\xc7\x75\x39\xaf\x7c\x7b\xf0\x56\x9b\x71\x95\x8e\x47\xd5\xe2\x15\xa0\x21\xf1\x9b\x2c\xc9\x15\x39\x9e\x57\x45\xde\x75\x93\x16\x0f\xeb\x14\xac\x13\x2e\xba\x9b\xf9\xec\xf7\x66\xa0\xbc\x63\x5f\x9d\xca\x0e\xf6\x63\xa2\x4d\xe6\xd9\xa9\xde\x44\x7a\x47\x1a\x3d\xfc\xd7\x4d\xa5\x0d\x5e\xcd\xef\x9a\xa1\x24\xbb\x3b\x07\xba\xa2\x73\x25\x87\x8c\xcc\xb5\xdf\x37\xa6\xd9\xba\x02\xbd\x8b\xd4\xeb\x95\xa8\x6d\x7a\x0b\xa1\xb9\x50\x2e\xe7\x83\xa5\x6a\x79\x99\x3a\x5d\x62\x34\x92\x40\x42\x25\x96\x4d\x9e\x9e\xd8\xad\xc5\x44\x96\x56\x7c\x28\xf2\xed\xc3\x03\xc1\x1b\x8d\xc7\x77\xcc\x0f\x8c\x50\xf6\x03\x96\x46\xee\x87\x86\x98\xef\x62\xea\x25\x5f\x81\xb6\x27\x50\x81\xd6\x52\x18\xb3\x3f\x3f\x83\x65\x50\x4e\x0a\xfb\xcc\xee\xef\xbb\x2a\xdd\xf0\xa2\x17\x35\x76\x0e\x2a\x86\x5f\x7b\xc1\xfe\x07\x00\x00\xff\xff\x6e\x06\xca\xb9\x51\x0b\x00\x00") -func migrations66_contract_asset_statsSqlBytes() ([]byte, error) { +func migrations67_remove_unused_indexesSqlBytes() ([]byte, error) { return bindataRead( - _migrations66_contract_asset_statsSql, - "migrations/66_contract_asset_stats.sql", + _migrations67_remove_unused_indexesSql, + "migrations/67_remove_unused_indexes.sql", ) } -func migrations66_contract_asset_statsSql() (*asset, error) { - bytes, err := migrations66_contract_asset_statsSqlBytes() +func migrations67_remove_unused_indexesSql() (*asset, error) { + bytes, err := migrations67_remove_unused_indexesSqlBytes() if err != nil { return nil, err } - info := bindataFileInfo{name: "migrations/66_contract_asset_stats.sql", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x2a, 0x9a, 0xab, 0xd3, 0x1d, 0x20, 0x82, 0x82, 0x64, 0xb1, 0x7e, 0x9f, 0xcb, 0xb5, 0xe6, 0x2b, 0xc2, 0x72, 0x8e, 0x1e, 0x58, 0x23, 0xc, 0xfd, 0x25, 0xb3, 0xe7, 0x9, 0x89, 0x43, 0x3e, 0x15}} + info := bindataFileInfo{name: "migrations/67_remove_unused_indexes.sql", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x99, 0x6, 0xc9, 0x3, 0x51, 0x52, 0x77, 0x87, 0x7f, 0x5, 0xe2, 0xdd, 0xd2, 0x49, 0xd0, 0xd1, 0x1b, 0x9a, 0x9, 0x2f, 0x10, 0xb7, 0x68, 0x44, 0xd1, 0x12, 0xce, 0x43, 0x50, 0x2e, 0xbd, 0xef}} return a, nil } @@ -1670,8 +1670,8 @@ var _bindata = map[string]func() (*asset, error){ "migrations/63_add_contract_id_to_asset_stats.sql": migrations63_add_contract_id_to_asset_statsSql, "migrations/64_add_payment_flag_history_ops.sql": migrations64_add_payment_flag_history_opsSql, "migrations/65_drop_payment_index.sql": migrations65_drop_payment_indexSql, - "migrations/65_remove_unused_indexes.sql": migrations65_remove_unused_indexesSql, "migrations/66_contract_asset_stats.sql": migrations66_contract_asset_statsSql, + "migrations/67_remove_unused_indexes.sql": migrations67_remove_unused_indexesSql, "migrations/6_create_assets_table.sql": migrations6_create_assets_tableSql, "migrations/7_modify_trades_table.sql": migrations7_modify_trades_tableSql, "migrations/8_add_aggregators.sql": migrations8_add_aggregatorsSql, @@ -1785,8 +1785,8 @@ var _bintree = &bintree{nil, map[string]*bintree{ "63_add_contract_id_to_asset_stats.sql": {migrations63_add_contract_id_to_asset_statsSql, map[string]*bintree{}}, "64_add_payment_flag_history_ops.sql": {migrations64_add_payment_flag_history_opsSql, map[string]*bintree{}}, "65_drop_payment_index.sql": {migrations65_drop_payment_indexSql, map[string]*bintree{}}, - "65_remove_unused_indexes.sql": {migrations65_remove_unused_indexesSql, map[string]*bintree{}}, "66_contract_asset_stats.sql": {migrations66_contract_asset_statsSql, map[string]*bintree{}}, + "67_remove_unused_indexes.sql": {migrations67_remove_unused_indexesSql, map[string]*bintree{}}, "6_create_assets_table.sql": {migrations6_create_assets_tableSql, map[string]*bintree{}}, "7_modify_trades_table.sql": {migrations7_modify_trades_tableSql, map[string]*bintree{}}, "8_add_aggregators.sql": {migrations8_add_aggregatorsSql, map[string]*bintree{}}, diff --git a/services/horizon/internal/db2/schema/migrations/65_remove_unused_indexes.sql b/services/horizon/internal/db2/schema/migrations/67_remove_unused_indexes.sql similarity index 100% rename from services/horizon/internal/db2/schema/migrations/65_remove_unused_indexes.sql rename to services/horizon/internal/db2/schema/migrations/67_remove_unused_indexes.sql From 645c32f49d5b8ecf59f6e6747072b455e8de7bae Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 11 Dec 2023 19:28:49 +0000 Subject: [PATCH 018/234] Fix TestProcessorRunnerRunAllProcessorsOnLedger --- .../horizon/internal/ingest/processor_runner_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/services/horizon/internal/ingest/processor_runner_test.go b/services/horizon/internal/ingest/processor_runner_test.go index 13036bce1f..eaeca95661 100644 --- a/services/horizon/internal/ingest/processor_runner_test.go +++ b/services/horizon/internal/ingest/processor_runner_test.go @@ -373,6 +373,17 @@ func TestProcessorRunnerRunAllProcessorsOnLedger(t *testing.T) { defer mock.AssertExpectationsForObjects(t, mockBatchInsertBuilder) + q.MockQAssetStats.On("RemoveContractAssetBalances", ctx, []xdr.Hash(nil)). + Return(nil).Once() + q.MockQAssetStats.On("UpdateContractAssetBalanceAmounts", ctx, []xdr.Hash{}, []string{}). + Return(nil).Once() + q.MockQAssetStats.On("InsertContractAssetBalances", ctx, []history.ContractAssetBalance(nil)). + Return(nil).Once() + q.MockQAssetStats.On("UpdateContractAssetBalanceExpirations", ctx, []xdr.Hash{}, []uint32{}). + Return(nil).Once() + q.MockQAssetStats.On("GetContractAssetBalancesExpiringAt", ctx, uint32(22)). + Return([]history.ContractAssetBalance{}, nil).Once() + runner := ProcessorRunner{ ctx: ctx, config: config, From bc7173e667a6830b2df0cd5deb2060f68b87ac02 Mon Sep 17 00:00:00 2001 From: tamirms Date: Tue, 12 Dec 2023 22:53:59 +0000 Subject: [PATCH 019/234] Use FastBatchInsertBuilder for asset stats (#5140) --- .../internal/db2/history/asset_stats.go | 24 +++++++----------- .../internal/db2/history/asset_stats_test.go | 25 +++++++++++++++++++ services/horizon/internal/db2/history/main.go | 18 ++++++++++--- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/services/horizon/internal/db2/history/asset_stats.go b/services/horizon/internal/db2/history/asset_stats.go index 94f0d61e00..622619680b 100644 --- a/services/horizon/internal/db2/history/asset_stats.go +++ b/services/horizon/internal/db2/history/asset_stats.go @@ -49,17 +49,15 @@ func (q *Q) InsertAssetStats(ctx context.Context, assetStats []ExpAssetStat) err return nil } - builder := &db.BatchInsertBuilder{ - Table: q.GetTable("exp_asset_stats"), - } + builder := &db.FastBatchInsertBuilder{} for _, assetStat := range assetStats { - if err := builder.Row(ctx, assetStatToMap(assetStat)); err != nil { + if err := builder.Row(assetStatToMap(assetStat)); err != nil { return errors.Wrap(err, "could not insert asset assetStat row") } } - if err := builder.Exec(ctx); err != nil { + if err := builder.Exec(ctx, q, "exp_asset_stats"); err != nil { return errors.Wrap(err, "could not exec asset assetStats insert builder") } @@ -71,17 +69,15 @@ func (q *Q) InsertContractAssetStats(ctx context.Context, rows []ContractAssetSt if len(rows) == 0 { return nil } - builder := &db.BatchInsertBuilder{ - Table: q.GetTable("contract_asset_stats"), - } + builder := &db.FastBatchInsertBuilder{} for _, row := range rows { - if err := builder.RowStruct(ctx, row); err != nil { + if err := builder.RowStruct(row); err != nil { return errors.Wrap(err, "could not insert asset assetStat row") } } - if err := builder.Exec(ctx); err != nil { + if err := builder.Exec(ctx, q, "contract_asset_stats"); err != nil { return errors.Wrap(err, "could not exec asset assetStats insert builder") } @@ -106,17 +102,15 @@ func (q *Q) InsertContractAssetBalances(ctx context.Context, rows []ContractAsse if len(rows) == 0 { return nil } - builder := &db.BatchInsertBuilder{ - Table: q.GetTable("contract_asset_balances"), - } + builder := &db.FastBatchInsertBuilder{} for _, row := range rows { - if err := builder.RowStruct(ctx, row); err != nil { + if err := builder.RowStruct(row); err != nil { return errors.Wrap(err, "could not insert asset assetStat row") } } - if err := builder.Exec(ctx); err != nil { + if err := builder.Exec(ctx, q, "contract_asset_balances"); err != nil { return errors.Wrap(err, "could not exec asset assetStats insert builder") } diff --git a/services/horizon/internal/db2/history/asset_stats_test.go b/services/horizon/internal/db2/history/asset_stats_test.go index 6485aec02b..a8352e4d97 100644 --- a/services/horizon/internal/db2/history/asset_stats_test.go +++ b/services/horizon/internal/db2/history/asset_stats_test.go @@ -21,6 +21,8 @@ func TestAssetStatContracts(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) q := &Q{tt.HorizonSession()} + tt.Assert.NoError(q.Begin(context.Background())) + assetStats := []ExpAssetStat{ { AssetType: xdr.AssetTypeAssetTypeNative, @@ -86,6 +88,7 @@ func TestAssetStatContracts(t *testing.T) { contractID[0]++ } tt.Assert.NoError(q.InsertAssetStats(tt.Ctx, assetStats)) + tt.Assert.NoError(q.Commit()) contractID[0] = 0 for i := 0; i < 2; i++ { @@ -123,6 +126,8 @@ func TestAssetContractStats(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) q := &Q{tt.HorizonSession()} + tt.Assert.NoError(q.Begin(context.Background())) + c1 := ContractAssetStatRow{ ContractID: []byte{1}, Stat: ContractStat{ @@ -183,6 +188,8 @@ func TestAssetContractStats(t *testing.T) { tt.Assert.NoError(err) tt.Assert.Equal(result, row) } + + tt.Assert.NoError(q.Rollback()) } func TestInsertAssetStats(t *testing.T) { @@ -190,6 +197,9 @@ func TestInsertAssetStats(t *testing.T) { defer tt.Finish() test.ResetHorizonDB(t, tt.HorizonDB) q := &Q{tt.HorizonSession()} + + tt.Assert.NoError(q.Begin(context.Background())) + tt.Assert.NoError(q.InsertAssetStats(tt.Ctx, []ExpAssetStat{})) assetStats := []ExpAssetStat{ @@ -239,6 +249,8 @@ func TestInsertAssetStats(t *testing.T) { tt.Assert.NoError(err) tt.Assert.Equal(got, assetStat) } + + tt.Assert.NoError(q.Rollback()) } func TestInsertAssetStat(t *testing.T) { @@ -1011,6 +1023,8 @@ func TestInsertContractAssetBalances(t *testing.T) { q := &Q{tt.HorizonSession()} + tt.Assert.NoError(q.Begin(context.Background())) + keyHash := [32]byte{} contractID := [32]byte{1} balance := ContractAssetBalance{ @@ -1038,6 +1052,8 @@ func TestInsertContractAssetBalances(t *testing.T) { tt.Assert.NoError(err) assertContractAssetBalancesEqual(t, balances, []ContractAssetBalance{balance, otherBalance}) + + tt.Assert.NoError(q.Rollback()) } func TestRemoveContractAssetBalances(t *testing.T) { @@ -1046,6 +1062,7 @@ func TestRemoveContractAssetBalances(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) q := &Q{tt.HorizonSession()} + tt.Assert.NoError(q.Begin(context.Background())) keyHash := [32]byte{} contractID := [32]byte{1} @@ -1086,6 +1103,8 @@ func TestRemoveContractAssetBalances(t *testing.T) { tt.Assert.NoError(err) assertContractAssetBalancesEqual(t, balances, []ContractAssetBalance{balance}) + + tt.Assert.NoError(q.Rollback()) } func TestUpdateContractAssetBalanceAmounts(t *testing.T) { @@ -1094,6 +1113,7 @@ func TestUpdateContractAssetBalanceAmounts(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) q := &Q{tt.HorizonSession()} + tt.Assert.NoError(q.Begin(context.Background())) keyHash := [32]byte{} contractID := [32]byte{1} @@ -1133,6 +1153,8 @@ func TestUpdateContractAssetBalanceAmounts(t *testing.T) { balance.Amount = "2" otherBalance.Amount = "1" assertContractAssetBalancesEqual(t, balances, []ContractAssetBalance{balance, otherBalance}) + + tt.Assert.NoError(q.Rollback()) } func TestUpdateContractAssetBalanceExpirations(t *testing.T) { @@ -1141,6 +1163,7 @@ func TestUpdateContractAssetBalanceExpirations(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) q := &Q{tt.HorizonSession()} + tt.Assert.NoError(q.Begin(context.Background())) keyHash := [32]byte{} contractID := [32]byte{1} @@ -1195,4 +1218,6 @@ func TestUpdateContractAssetBalanceExpirations(t *testing.T) { balances, err = q.GetContractAssetBalancesExpiringAt(context.Background(), 200) tt.Assert.NoError(err) assertContractAssetBalancesEqual(t, balances, []ContractAssetBalance{balance, otherBalance}) + + tt.Assert.NoError(q.Rollback()) } diff --git a/services/horizon/internal/db2/history/main.go b/services/horizon/internal/db2/history/main.go index 5bb5cff13a..e3a8212731 100644 --- a/services/horizon/internal/db2/history/main.go +++ b/services/horizon/internal/db2/history/main.go @@ -376,7 +376,11 @@ type ContractStat struct { } func (c ContractStat) Value() (driver.Value, error) { - return json.Marshal(c) + // Convert the byte array into a string as a workaround to bypass buggy encoding in the pq driver + // (More info about this bug here https://github.com/stellar/go/issues/5086#issuecomment-1773215436). + // By doing so, the data will be written as a string rather than hex encoded bytes. + val, err := json.Marshal(c) + return string(val), err } func (c *ContractStat) Scan(src interface{}) error { @@ -445,7 +449,11 @@ type ExpAssetStatAccounts struct { } func (e ExpAssetStatAccounts) Value() (driver.Value, error) { - return json.Marshal(e) + // Convert the byte array into a string as a workaround to bypass buggy encoding in the pq driver + // (More info about this bug here https://github.com/stellar/go/issues/5086#issuecomment-1773215436). + // By doing so, the data will be written as a string rather than hex encoded bytes. + val, err := json.Marshal(e) + return string(val), err } func (e *ExpAssetStatAccounts) Scan(src interface{}) error { @@ -525,7 +533,11 @@ func (e ExpAssetStatBalances) IsZero() bool { } func (e ExpAssetStatBalances) Value() (driver.Value, error) { - return json.Marshal(e) + // Convert the byte array into a string as a workaround to bypass buggy encoding in the pq driver + // (More info about this bug here https://github.com/stellar/go/issues/5086#issuecomment-1773215436). + // By doing so, the data will be written as a string rather than hex encoded bytes. + val, err := json.Marshal(e) + return string(val), err } func (e *ExpAssetStatBalances) Scan(src interface{}) error { From b0220a0d92ba925cfb22f976f7805f4c4a3b4992 Mon Sep 17 00:00:00 2001 From: tamirms Date: Wed, 20 Dec 2023 15:21:32 +0000 Subject: [PATCH 020/234] Update core and soroban-rpc builds in horizon integration tests (#5146) --- .github/workflows/horizon.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index 6b94521e0a..0ea0686904 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -33,9 +33,9 @@ jobs: env: HORIZON_INTEGRATION_TESTS_ENABLED: true HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL: ${{ matrix.protocol-version }} - PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 20.0.0-1615.617729910.focal - PROTOCOL_20_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:20.0.0-1615.617729910.focal - PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:20.0.0-tests-45 + PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 20.0.2-1633.669916b56.focal + PROTOCOL_20_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:20.0.2-1633.669916b56.focal + PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:20.0.2-47 PROTOCOL_19_CORE_DEBIAN_PKG_VERSION: 19.14.0-1500.5664eff4e.focal PROTOCOL_19_CORE_DOCKER_IMG: stellar/stellar-core:19.14.0-1500.5664eff4e.focal PGHOST: localhost From cf708f8b2f8cfa3ce06a2ecff5cd3a8ad12d66a2 Mon Sep 17 00:00:00 2001 From: tamirms Date: Thu, 21 Dec 2023 17:17:11 +0000 Subject: [PATCH 021/234] Fix comparison of claimable balance ids in verify-range script (#5147) --- services/horizon/docker/verify-range/start | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/services/horizon/docker/verify-range/start b/services/horizon/docker/verify-range/start index 8da48db6cc..0e7c69403d 100644 --- a/services/horizon/docker/verify-range/start +++ b/services/horizon/docker/verify-range/start @@ -53,7 +53,9 @@ dump_horizon_db() { # skip is_payment column which was only introduced in the most recent horizon v2.27.0 psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -t -A -F"," --variable="FETCH_COUNT=100" -c "select id, transaction_id, application_order, type, details, source_account, source_account_muxed from history_operations order by id asc" > "${1}_operations" echo "dumping history_operation_claimable_balances" - psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -t -A -F"," --variable="FETCH_COUNT=100" -c "select history_operation_id, history_claimable_balance_id from history_operation_claimable_balances left join history_claimable_balances on history_claimable_balances.id = history_operation_claimable_balances.history_claimable_balance_id order by history_operation_id asc, id asc" > "${1}_operation_claimable_balances" + psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -t -A -F"," --variable="FETCH_COUNT=100" -c "select history_operation_id, claimable_balance_id from history_operation_claimable_balances left join history_claimable_balances on history_claimable_balances.id = history_operation_claimable_balances.history_claimable_balance_id order by history_operation_id asc, claimable_balance_id asc" > "${1}_operation_claimable_balances" + echo "dumping history_operation_liquidity_pools" + psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -t -A -F"," --variable="FETCH_COUNT=100" -c "select history_operation_id, liquidity_pool_id from history_operation_liquidity_pools left join history_liquidity_pools on history_liquidity_pools.id = history_operation_liquidity_pools.history_liquidity_pool_id order by history_operation_id asc, liquidity_pool_id asc" > "${1}_operation_liquidity_pools" echo "dumping history_operation_participants" psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -t -A -F"," --variable="FETCH_COUNT=100" -c "select history_operation_id, address from history_operation_participants left join history_accounts on history_accounts.id = history_operation_participants.history_account_id order by history_operation_id asc, address asc" > "${1}_operation_participants" echo "dumping history_trades" @@ -63,7 +65,9 @@ dump_horizon_db() { # in different Stellar-Core instances. The final fix should probably: unmarshal `tx_meta`, sort it, marshal and compare. psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -t -A -F"," --variable="FETCH_COUNT=100" -c "select transaction_hash, ledger_sequence, application_order, account, account_sequence, max_fee, operation_count, id, tx_envelope, tx_result, tx_fee_meta, signatures, memo_type, memo, time_bounds, successful, fee_charged, inner_transaction_hash, fee_account, inner_signatures, new_max_fee, account_muxed, fee_account_muxed from history_transactions order by id asc" > "${1}_transactions" echo "dumping history_transaction_claimable_balances" - psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -t -A -F"," --variable="FETCH_COUNT=100" -c "select history_transaction_id, history_claimable_balance_id from history_transaction_claimable_balances left join history_claimable_balances on history_claimable_balances.id = history_transaction_claimable_balances.history_claimable_balance_id order by history_transaction_id, id" > "${1}_transaction_claimable_balances" + psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -t -A -F"," --variable="FETCH_COUNT=100" -c "select history_transaction_id, claimable_balance_id from history_transaction_claimable_balances left join history_claimable_balances on history_claimable_balances.id = history_transaction_claimable_balances.history_claimable_balance_id order by history_transaction_id, claimable_balance_id" > "${1}_transaction_claimable_balances" + echo "dumping history_transaction_liquidity_pools" + psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -t -A -F"," --variable="FETCH_COUNT=100" -c "select history_transaction_id, liquidity_pool_id from history_transaction_liquidity_pools left join history_liquidity_pools on history_liquidity_pools.id = history_transaction_liquidity_pools.history_liquidity_pool_id order by history_transaction_id, liquidity_pool_id" > "${1}_transaction_liquidity_pools" echo "dumping history_transaction_participants" psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -t -A -F"," --variable="FETCH_COUNT=100" -c "select history_transaction_id, address from history_transaction_participants left join history_accounts on history_accounts.id = history_transaction_participants.history_account_id order by history_transaction_id, address" > "${1}_transaction_participants" } @@ -93,12 +97,15 @@ function alter_tables_unlogged() { psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE history_accounts SET UNLOGGED;" psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE history_assets SET UNLOGGED;" psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE history_claimable_balances SET UNLOGGED;" + psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE history_liquidity_pools SET UNLOGGED;" psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE history_effects SET UNLOGGED;" psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE history_ledgers SET UNLOGGED;" psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE history_operation_claimable_balances SET UNLOGGED;" + psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE history_operation_liquidity_pools SET UNLOGGED;" psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE history_operation_participants SET UNLOGGED;" psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE history_operations SET UNLOGGED;" psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE history_transaction_claimable_balances SET UNLOGGED;" + psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE history_transaction_liquidity_pools SET UNLOGGED;" psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE history_transaction_participants SET UNLOGGED;" psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE history_transactions SET UNLOGGED;" psql "postgres://postgres:postgres@localhost:5432/horizon?sslmode=disable" -c "ALTER TABLE offers SET UNLOGGED;" From 51c1b15719447e7f609d1ac007b60f96e095be49 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Tue, 2 Jan 2024 17:12:34 +0100 Subject: [PATCH 022/234] clients/stellarcore horizon: Obtain and expose diagnostic events in transaction endpoint (#5148) --- clients/stellarcore/client_test.go | 23 ++++++++- protocols/stellarcore/tx_response.go | 37 ++++++++++++++ protocols/stellarcore/tx_response_test.go | 16 ++++++ .../internal/actions/submit_transaction.go | 30 +++++++---- .../actions/submit_transaction_test.go | 51 +++++++++++++++++-- services/horizon/internal/codes/main.go | 2 + services/horizon/internal/txsub/errors.go | 4 +- services/horizon/internal/txsub/submitter.go | 2 +- 8 files changed, 150 insertions(+), 15 deletions(-) create mode 100644 protocols/stellarcore/tx_response_test.go diff --git a/clients/stellarcore/client_test.go b/clients/stellarcore/client_test.go index 90f4c1cc55..6cfd01b210 100644 --- a/clients/stellarcore/client_test.go +++ b/clients/stellarcore/client_test.go @@ -5,9 +5,10 @@ import ( "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) { @@ -27,6 +28,26 @@ func TestSubmitTransaction(t *testing.T) { } } +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"}`, + ) + + 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) + } +} + func TestManualClose(t *testing.T) { hmock := httptest.NewClient() c := &Client{HTTP: hmock, URL: "http://localhost:11626"} diff --git a/protocols/stellarcore/tx_response.go b/protocols/stellarcore/tx_response.go index ee8556adc3..c4434ea280 100644 --- a/protocols/stellarcore/tx_response.go +++ b/protocols/stellarcore/tx_response.go @@ -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 @@ -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[i] = encoded + } + return result, nil +} diff --git a/protocols/stellarcore/tx_response_test.go b/protocols/stellarcore/tx_response_test.go new file mode 100644 index 0000000000..bf2baf90d0 --- /dev/null +++ b/protocols/stellarcore/tx_response_test.go @@ -0,0 +1,16 @@ +package stellarcore + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDiagnosticEventsToSlice(t *testing.T) { + events := "AAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAFZXJyb3IAAAAAAAACAAAAAwAAAAUAAAAQAAAAAQAAAAMAAAAOAAAAU3RyYW5zYWN0aW9uIGBzb3JvYmFuRGF0YS5yZXNvdXJjZUZlZWAgaXMgbG93ZXIgdGhhbiB0aGUgYWN0dWFsIFNvcm9iYW4gcmVzb3VyY2UgZmVlAAAAAAUAAAAAAAEJcwAAAAUAAAAAAAG6fAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAFZXJyb3IAAAAAAAACAAAAAwAAAAUAAAAQAAAAAQAAAAMAAAAOAAAAU3RyYW5zYWN0aW9uIGBzb3JvYmFuRGF0YS5yZXNvdXJjZUZlZWAgaXMgbG93ZXIgdGhhbiB0aGUgYWN0dWFsIFNvcm9iYW4gcmVzb3VyY2UgZmVlAAAAAAUAAAAAAAEJcwAAAAUAAAAAAAG6fA==" + slice, err := DiagnosticEventsToSlice(events) + require.NoError(t, err) + require.Len(t, slice, 2) + require.Equal(t, slice[0], "AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAVlcnJvcgAAAAAAAAIAAAADAAAABQAAABAAAAABAAAAAwAAAA4AAABTdHJhbnNhY3Rpb24gYHNvcm9iYW5EYXRhLnJlc291cmNlRmVlYCBpcyBsb3dlciB0aGFuIHRoZSBhY3R1YWwgU29yb2JhbiByZXNvdXJjZSBmZWUAAAAABQAAAAAAAQlzAAAABQAAAAAAAbp8") + require.Equal(t, slice[1], "AAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAVlcnJvcgAAAAAAAAIAAAADAAAABQAAABAAAAABAAAAAwAAAA4AAABTdHJhbnNhY3Rpb24gYHNvcm9iYW5EYXRhLnJlc291cmNlRmVlYCBpcyBsb3dlciB0aGFuIHRoZSBhY3R1YWwgU29yb2JhbiByZXNvdXJjZSBmZWUAAAAABQAAAAAAAQlzAAAABQAAAAAAAbp8") +} diff --git a/services/horizon/internal/actions/submit_transaction.go b/services/horizon/internal/actions/submit_transaction.go index 4c8f2fd691..b877f75a7b 100644 --- a/services/horizon/internal/actions/submit_transaction.go +++ b/services/horizon/internal/actions/submit_transaction.go @@ -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" @@ -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.DiagnosticEventsXDR != "" { + events, err := stellarcore.DiagnosticEventsToSlice(failedErr.DiagnosticEventsXDR) + if err != nil { + return nil, err + } + extras["diagnostic_events"] = events + } return nil, &problem.P{ Type: "transaction_failed", @@ -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, } } diff --git a/services/horizon/internal/actions/submit_transaction_test.go b/services/horizon/internal/actions/submit_transaction_test.go index 273099b528..a15ce3bd94 100644 --- a/services/horizon/internal/actions/submit_transaction_test.go +++ b/services/horizon/internal/actions/submit_transaction_test.go @@ -9,15 +9,16 @@ import ( "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) { @@ -199,3 +200,47 @@ func TestDisableTxSubFlagSubmission(t *testing.T) { _, 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==", + DiagnosticEventsXDR: "AAAAAQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAFZXJyb3IAAAAAAAACAAAAAwAAAAUAAAAQAAAAAQAAAAMAAAAOAAAAU3RyYW5zYWN0aW9uIGBzb3JvYmFuRGF0YS5yZXNvdXJjZUZlZWAgaXMgbG93ZXIgdGhhbiB0aGUgYWN0dWFsIFNvcm9iYW4gcmVzb3VyY2UgZmVlAAAAAAUAAAAAAAEJcwAAAAUAAAAAAAG6fA==", + }, + } + + handler := SubmitTransactionHandler{ + Submitter: mockSubmitter, + NetworkPassphrase: network.PublicNetworkPassphrase, + CoreStateGetter: mock, + } + + form := url.Values{} + form.Set("tx", "AAAAAAGUcmKO5465JxTSLQOQljwk2SfqAJmZSG6JH6wtqpwhAAABLAAAAAAAAAABAAAAAAAAAAEAAAALaGVsbG8gd29ybGQAAAAAAwAAAAAAAAAAAAAAABbxCy3mLg3hiTqX4VUEEp60pFOrJNxYM1JtxXTwXhY2AAAAAAvrwgAAAAAAAAAAAQAAAAAW8Qst5i4N4Yk6l+FVBBKetKRTqyTcWDNSbcV08F4WNgAAAAAN4Lazj4x61AAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABLaqcIQAAAEBKwqWy3TaOxoGnfm9eUjfTRBvPf34dvDA0Nf+B8z4zBob90UXtuCqmQqwMCyH+okOI3c05br3khkH0yP4kCwcE") + + request, err := http.NewRequest( + "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"}) +} diff --git a/services/horizon/internal/codes/main.go b/services/horizon/internal/codes/main.go index 5af63bed1a..ebf90a0233 100644 --- a/services/horizon/internal/codes/main.go +++ b/services/horizon/internal/codes/main.go @@ -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 { diff --git a/services/horizon/internal/txsub/errors.go b/services/horizon/internal/txsub/errors.go index 5652498327..160ea7a4a4 100644 --- a/services/horizon/internal/txsub/errors.go +++ b/services/horizon/internal/txsub/errors.go @@ -16,7 +16,7 @@ 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 @@ -24,6 +24,8 @@ var ( // encoded TransactionResult struct type FailedTransactionError struct { ResultXDR string + // DiagnosticEventsXDR is a base64-encoded []xdr.DiagnosticEvent + DiagnosticEventsXDR string } func (err *FailedTransactionError) Error() string { diff --git a/services/horizon/internal/txsub/submitter.go b/services/horizon/internal/txsub/submitter.go index 85fd858233..27ce85c87a 100644 --- a/services/horizon/internal/txsub/submitter.go +++ b/services/horizon/internal/txsub/submitter.go @@ -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: From 38f67b9ee0c9b9c57afe2c9d710e29b2be6d7c96 Mon Sep 17 00:00:00 2001 From: tamirms Date: Wed, 3 Jan 2024 18:46:27 +0100 Subject: [PATCH 023/234] services/horizon/internal/ingest/processors: Dedupe participants deterministically (#5149) --- .../liquidity_pools_transaction_processor.go | 23 +++++++++------- .../ingest/processors/operations_processor.go | 26 +++++++++++++------ 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor.go b/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor.go index 0a38215f08..c721f9e4ba 100644 --- a/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor.go +++ b/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor.go @@ -2,10 +2,10 @@ package processors import ( "context" + "sort" "github.com/stellar/go/ingest" "github.com/stellar/go/services/horizon/internal/db2/history" - "github.com/stellar/go/support/collections/set" "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" "github.com/stellar/go/toid" @@ -73,16 +73,21 @@ func liquidityPoolsForTransaction(transaction ingest.LedgerTransaction) ([]strin } func dedupeStrings(in []string) []string { - set := set.Set[string]{} - for _, id := range in { - set.Add(id) + if len(in) <= 1 { + return in } - - out := make([]string, 0, len(in)) - for id := range set { - out = append(out, id) + sort.Strings(in) + insert := 1 + for cur := 1; cur < len(in); cur++ { + if in[cur] == in[cur-1] { + continue + } + if insert != cur { + in[insert] = in[cur] + } + insert++ } - return out + return in[:insert] } func liquidityPoolsForChanges( diff --git a/services/horizon/internal/ingest/processors/operations_processor.go b/services/horizon/internal/ingest/processors/operations_processor.go index b9a23229d5..8ad023145c 100644 --- a/services/horizon/internal/ingest/processors/operations_processor.go +++ b/services/horizon/internal/ingest/processors/operations_processor.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "sort" "github.com/guregu/null" @@ -1034,16 +1035,25 @@ func (operation *transactionOperationWrapper) Participants() ([]xdr.AccountId, e } // dedupeParticipants remove any duplicate ids from `in` -func dedupeParticipants(in []xdr.AccountId) (out []xdr.AccountId) { - set := map[string]xdr.AccountId{} - for _, id := range in { - set[id.Address()] = id +func dedupeParticipants(in []xdr.AccountId) []xdr.AccountId { + if len(in) <= 1 { + return in } - - for _, id := range set { - out = append(out, id) + sort.Slice(in, func(i, j int) bool { + return in[i].Address() < in[j].Address() + }) + insert := 1 + for cur := 1; cur < len(in); cur++ { + if in[cur].Equals(in[cur-1]) { + continue + } + if insert != cur { + in[insert] = in[cur] + } + insert++ } - return + return in[:insert] + } // OperationsParticipants returns a map with all participants per operation From 3ca501f09055d64f6721cb2531df7673db09ffed Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Tue, 9 Jan 2024 18:51:36 +0100 Subject: [PATCH 024/234] ingest/ledgerbacked/toml: Add support for ENABLE_DIAGNOSTICS_FOR_TX_SUBMISSION in captive core (#5157) --- ingest/ledgerbackend/toml.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ingest/ledgerbackend/toml.go b/ingest/ledgerbackend/toml.go index 7c42bc11c8..55e36e9b9f 100644 --- a/ingest/ledgerbackend/toml.go +++ b/ingest/ledgerbackend/toml.go @@ -94,6 +94,7 @@ type captiveCoreTomlValues struct { EnableSorobanDiagnosticEvents *bool `toml:"ENABLE_SOROBAN_DIAGNOSTIC_EVENTS,omitempty"` TestingMinimumPersistentEntryLifetime *uint `toml:"TESTING_MINIMUM_PERSISTENT_ENTRY_LIFETIME,omitempty"` TestingSorobanHighLimitOverride *bool `toml:"TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE,omitempty"` + EnableDiagnosticsForTxSubmission *bool `toml:"ENABLE_DIAGNOSTICS_FOR_TX_SUBMISSION,omitempty"` } // QuorumSetIsConfigured returns true if there is a quorum set defined in the configuration. From 423fa1f4e145a7818628a178adb2d2752f28ad87 Mon Sep 17 00:00:00 2001 From: Paul Bellamy Date: Tue, 9 Jan 2024 19:30:43 +0000 Subject: [PATCH 025/234] exp: Add the Zenith (Light Horizon) prototype (#4352) * exp/lighthorizon: Add initial support for XDR serialization (#4369) * exp/lighthorizon: Improve trie tests to avoid raw comparisons/outputs. (#4373) * exp/lighthorizon: Add XDR marshalling for the `TrieNode` structure. (#4375) * Add encoding stdlib interfaces * lighthorizon: Sync with upstream master branch (#4404) * services/ticker: ingest assets optimizations (#4218) * Add CHANGELOG entry for Horizon 2.14.0 release (#4208) (#4220) * Make sure we test reingestion for all possible operations (#4231) * services/horizon: Allow captive core to run with sqlite database (#4092) * services/horizon: Release DB connection in /paths when no longer needed (#4228) * services/horizon: Exclude trades with >10% rounding slippage from trade aggregations (#4178) * all: staticcheck fixes (#4239) * Migrate Horizon integration tests to GitHub Actions (#4242) * Fix StreamAllLiquidityPools and StreamAllOffers (#4236) * all: run builds and tests with go1.18rc1 (#4143) * all: cache go module downloads and other build and test artifacts (#3727) * services/horizon: Add LedgerHashStore to Captive-Core config (#4251) * all: migrate the rest of the CircleCI jobs to GitHub Actions (#4250) * horizon: Fix GitHub action problem with verify-range push in master (#4253) * all: fix ci ref_protected check for caching (#4254) * Switch over from CircleCI to GitHub A tions (#4256) * all: [GitHub actions] Reset the module and build cache in master/protected (#4266) * Forgot to add sudo in #4266 (#4270) * all: More go-setup github action fixes (#4274) * xdr: add instructions for generating xdr (#4280) * services/ticker: cache tomls during scraping (#4286) * services/ticker: use log fields during asset ingestion (#4288) * services/ticker: reduce size of toml cache in memory (#4289) * historyarchive: add --skip-optional flag (#3906) * all: Add Protocol 19 XDR and update StrKey to support Signed Payloads (#4279) * Replace keybase with publicnode in the stellar core config (#4291) * Fix captive core tests to write to /tmp, instead of polluting the repo (#4296) * all: remove go1.16 add go1.18 (#4284) * Rename methods and functions in submission system (#4298) * PR feedback (#4300) * Support new account fields for protocol-19. (#4294) * xdr, keypair: Add helpers to create CAP-40 decorated signatures (#4302) * services/horizon: Update txsub queue to account for new CAP-21 preconditions (#4301) * Uncomment StateVerifier test that generates account v3 extensions now that they are implemented. (#4304) * txnbuild: Add support for new CAP-21 preconditions. (#4303) * services/horizon: Support new CAP-21 transaction conditions (#4297) * txnbuild: Complete rename, avoid using XDR types in `TransactionParams`. (#4307) * all: Update Protocol 19 XDR to the latest (#4308) * services/horizon: Add a rate limit for path finding requests. (#4310) * clients/horizonclient: fix multi-parameter url for claimable balance query (#4248) * all: Fix Horizon integration tests (#4292) * horizon: Fix integration tests (#4314) * horizon: Set up protocol 19 integration tests infrastructure (#4312) * all: Change outdated CircleCI build badge (#4324) * horizon: Test new protocol 19 account fields (#4322) * all: update staticcheck to 2022.1 (#4326) * all: remove go.list and related docs (#4328) * horizon: Add transaction submission test for Protocol 19 (#4327) * Horizon v2.16.1 CHANGELOG (#4333) * Revert "Pin go versions temporarily" (#4338) * services/horizon: Use `bigint` over `timestamp` to accommodate large years (#4337) * xdr: Update xdrgen (#4341) * services/horizon: Change `min_account_sequence_age` column from `bigint` to string (#4339) * services/horizon: Bump stellar-core to v19.0.0rc1 for Horizon tests (#4345) * services/horizon: expose supported protocol version on root endpoint (#4347) * horizon: Small transaction submission refactoring (#4344) * services/horizon: Pass through nil ExtraSigners to avoid nil pointer deref (#4349) * doc: rename license file (#4350) * all: upgrade dep github.com/valyala/fasthttp (#4351) * services/horizon: Promote Stellar Core to v19.0.0 stable. (#4353) * services/horizon/integration: Precondition edge cases and V18->19 upgrade boundary. (#4354) * xdr: Synchronizes monorepo XDR with Stellar Core (#4355) * services/horizon: Properly allow nullable Protocol 19 account fields (#4357) * services/friendbot: include txhash in logs (#4359) * services/horizon: Improve transaction precondition `omitempty` behavior (#4360) * tools/horizon-cmp: Improve panic error message (#4365) * services/horizon: Merge stable v2.17.0 back into master: (#4363) * Use UNIX timestamps instead of RFC3339 strings for timebounds. (#4361) * xdrgen: remove gemfile and rakefile to just use docker for the xdrgen (#4366) * Conservatively limit the number of DB connections of integration tests (#4368) * internal/integrations: db_test should drop test db instances when finished (#4185) * GHA: Bump Core version to v19.0.1 in Horizon workflows. (#4378) * services/horizon, clients/horizonclient: Allow filtering ingested transactions by account or asset. (#4277) * Push stellar/ledger-state-diff images from Github actions (#4380) * services/horizon: Fixes copy-paste typo in `--help` text (#4383) * tools/alb-replay: Add new features to alb-replay (#4384) * services/horizon: Optimize claimable balances query to limit records earlier (#4385) * support/db, services/horizon/internal: Configure postgres client connection timeouts for read only db (#4390) * Refactor trade aggregation query. (#4389) * services/horizon/internal/db2/history: Implement StreamAllOffers using batches (#4397) * Add flag to disable path finding endpoints (#4399) Co-authored-by: stfung77 Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Co-authored-by: Alfonso Acosta Co-authored-by: Paul Bellamy Co-authored-by: Bartek Nowotarski Co-authored-by: tamirms Co-authored-by: Alfonso Acosta Co-authored-by: Graydon Hoare Co-authored-by: Satyam Zode <5508956+satyamz@users.noreply.github.com> Co-authored-by: Satyam Zode Co-authored-by: erika-sdf <92893480+erika-sdf@users.noreply.github.com> Co-authored-by: iateadonut Co-authored-by: Shawn Reuland Co-authored-by: shawn Co-authored-by: Shivendra Mishra Co-authored-by: Jacek Nykis Co-authored-by: jacekn * Explain map and reduce commands * exp/lighthorizon: Refactor single-process index builder. (#4410) * Refactor index builder: - allow worker count to be a command line parameter - split work by checkpoints rather than ledgers - move actual index insertion work to helpers - move progress bar into helpers - simplify participants code, payments vs. all * Properly work on a checkpoint range at a time: - previously, it was just arbitrary 64-ledger chunks which is not as helpful * Define a generic module processing function * Move index building into a separate object * Fix off-by-one error in checkpoint index builder: - Keeping this as-is would mean that the first chunk of ledgers will be "Checkpoint 0" which doesn't make sense in the bitmap - Calling index.setActive(0) is essentially a no-op, because no bit will ever be set. - In the case of an empty index in which the only active account checkpoint is the first one, this is indistinguishable from an index with no activity. * exp/services/ledgerexporter: Extend tool to support lower ledger bound. (#4405) * exp/lighthorizon: Refactor and repair the reduce job (#4424) * Use envvars for every configurable thing, incl. index sources and final merged index target: This removes any hard dependency on S3 and lets you use any supported backend for the map-reduce operation. It was done specifically with local filesystem-based testing in mind, but naturally opens up other backends as well. * Add lots of helper functions: Specifically, helpers now exist for both merging two sets of named indices together and partitioning work based on the account/transaction hashes into separate jobs/routines. * Lots more logging! For progress tracking, debugging, etc. * Create a thread-safe string set abstraction for tracking completed work. * Better error handling: `os.IsNotExist(err)` is much more reliable over a direct equality check to `ErrNotExist`. This also ties in to backend-independence. We can also log and return an error rather than immediately panicking on its occurrence. * Transaction flushes need to be thread-safe if they're going to be done from different goroutines during reduction. Otherwise, you get panics from concurrent writes to its maps. * The "account list" (aka the file containing a list of all accounts in the partitioned index) needs to be flushed at the same time as the index itself: If this isn't done, then `FlushAccounts()` will do absolutely nothing after a `Flush()`, because the previous `Flush()` will clear the map of indices out of memory. Since the account list comes from memory, it becomes a no-op. * Split work across multiple channels rather than just one If the work comes from a single channel, accounts can get skipped overall because they aren't put back on the queue if they're skipped by a single worker. It makes more sense to make each worker have its own channel, partitioning the work *before* it gets to the worker rather than after. * exp/lighthorizon: Unify map-reduce and single-process index builders (#4423) * Main thing: `./index/cmd/single` and `./index/cmd/batch/map` now leverage the same index building code (i.e. `BuildIndices`) * This also extends the map-reduce builder to take the txmeta source / index destination URLs from envvars rather: This eliminates a hard dependency on S3, and it's done here because splitting that out from the giga-PR was difficult. * We can infer checkpoints from `ledger.LedgerSequence()` rather than passing them in as a parameter, which cleans up modules. * This finally adds a new `ProcessAccountsWithoutBackend` module for the Map job * exp/lighthorizon: Thread-safe support for reading account list via FileBackend (#4422) Three key changes: - actually read the account list when using a filesystem backend - using `O_APPEND` on the file to support concurrent writes - ensure that the read list is a unique set of accounts * exp/lighthorizon: Restructure index package into sensible sub-packages (#4427) * exp/lighthorizon: Merge on-disk index with in-memory one on load. (#4435) * Add test for single-process index builder * Merge in-memory index with on-disk one when loading * Add fixture of unpacked ledgers for fast local testing * Isolate the index we need to merge * Use a ByteReader so that multiple indices in one file work :facepalm: * Add to/from XDR support to bitmap index * Fix and extend gzip tests to handle the bytereader bug * Simplify participant processing code * exp/lighthorizon: Allow indexer to continually update as new txmeta appears (#4432) * exp/lighthorizon: enforce the limit from request on the response size (#4431) * Dockerize ledgerexport to run in AWS Batch This Change: 1. Creates docker image (stellar/horizon-ledgerexporter) which works in a similar fashion to stellar/horizon-verify-rage and is tested and pushed as part of the Horizon GitHub workflow. 2. Adds two more parameters to ledgerexporter * --end-ledger: which indicates at what ledger to stop the export * --write-latest-path: which indicates whether to udpate the /latest path of the target Latest path writing is disabled in the container by default in order to avoid race-conditions between parallel jobs * exp/lighthorizon: Add test for batch index building map job (#4440) * Modify single-process test to generalize to whatever fixture data exists This also adds a test to check that single-process works on a non-checkpoint starting point which is important. * Fix map program to properly build sub-paths depending on its job index Previously, this only happened for explicitly S3 backends. * Make map job default to using all CPUs * Stop clearing indices from memory if using unbacked module * Use historyarchive.CheckpointManager for all checkpoint math * Update lastBuiltLedger w/ safely concurrent writes * Refactor bound preparation and add --continue flag * Address review feedback and rework env variable names * Run gofmt -w (I don't know why those files were changed) * Add proper logging to indicate what range is being exported * Add clarification about end ledger * Fix boolean argument passing * Address review feedback * Address feedback * Use sqlite for captive core * exp/lighthorizon: Add basic scaffolding for metrics. (#4456) * Use correct network passphrase when populating transaction * Add scaffolding for Prom/log metrics and some example ones * Misc. clarifications and fixes to the index builder * lighthorizon: Prepend version to ledger files (#4450) * Prepend version to ledger files * Encode versioning in XDR * Regenerate fixtures * Fix ledger fixtures * Appease govet * Move all lighthorizon types to /xdr * exp/lighthorizon/index: More testing for batch indexing and off-by-one bugfix. (#4442) * Add reduce test to ensure combining map jobs works * Actually test that TOIDs are correct * Bugfix: Transaction prefix loop should be inclusive * Isolate loggers to individual processing "sections" * Minor ledgerexporter infrastructure improvements (#4461) * Push the stellar/horizon-ledgerexporter docker image when pushing to the lighthorizon branch * Fix the ledger exporter aws batch jobs when running on the first batch * Forgot to add login step to ledgerexporter workflow * exp/lighthorizon: Set a default number of workers. (#4465) * Default to the number of CPUs if worker count isn't specified * Set a timeout on the reduce job to avoid test suite hanging indefinitely * exp/lighthorizon: Fix the single-process index builder data race. (#4470) * Add synchronization for the work submission routine. Thank you @sreuland! Co-authored-by: shawn * /exp/lighthorizon: new endpoints for tx and ops paged listing by account id (#4453) * exp/lighthorizon: Add an on-disk cache for frequently accessed ledgers. (#4457) * Replace custom LRU solution with an off-the-shelf data structure. * Add a filesystem cache in front of the ledger backend to lower latency * Add cache size parameter; only setup cache if not file:// * Extract S3 region from the archive URL if it's applicable. * exp/lighthorizon/index: Drop building indices for successful transactions. (#4482) * Add metrics middleware to collect request duration metrics (#4486) * exp/lighthorizon: Isolate cursor advancement code to its own interface (#4484) * Move cursor manipulation code to a separate interface * Small test refactor to improve readability and long-running lines * Combine tx and op tests into subtests * Fix how IndexStore is mocked out * exp/lighthorizon/index: Parse network passphrase from the env. (#4491) * Refactor access to meta archive (#4488) Refactor `historyarchive` and `ledgerbackend` to allow better access to the new meta archives: * Created `metaarchive` package that connects to the new meta archives (and allows accessing `xdr.SerializedLedgerCloseMeta`). * Extracted `ArchiveBackend` to the new `support/storage` package as it contains only storage related methods. New package is used in both `historyarchive` and `metaarchive`. * exp/lighthorizon: Add response age prometheus metrics (#4492) * exp/lighthorizon/index: Allow accounts to be indexed by ledger. (#4495) * Add builders to make account indices by ledger * Add `MODULE` parameter to map job in batch builder * Don't build transaction indices by default * services/horizon/docker/ledgerexporter: deploy ledgerexporter image as service (#4490) * Make indexing s3 bucket configurable (#4507) * exp/lighthorizon: Add duration metrics for on-the-fly ingestion elements. (#4476) Add basic aggregate metrics for request fulfillment: - how long did ledger downloads take, on average? - how long did ledger processing take, on average? - how long did index lookups take, on average? - how many ledgers were needed? - how long did the entire request take, in total? * exp/lighthorizon: Add JSON content type to responses. (#4509) * exp/lighthorizon: *Correctly* set `Content-Type`, plus JSONify errors (#4513) * exp/lighthorizon/services: Move service-specific stuff to its own file. (#4502) * exp/lighthorizon, xdr: Rename `CheckpointIndex` to better reflect its capabilty. (#4510) * Rename NextActive -> NextActiveBit to be descriptive * exp/lighthorizon: Add a suite of tools to manage the on-disk ledger cache. (#4522) * Run 'go mod tidy' after merge * exp/lighthorizon: add horizon web docker/k8s deployment (#4519) * It seems like the merge caused some deleted files to stay in: The commit b3407fd51796213822fb7c60dd000e44a48c8e60 from PR #4418 deleted these files, so we just do the same. A quick manual inspection showed us that the deltas transferred over, just not the deletions, for some reason. Idk why these changes ended up in the code, kinda sus... More deleted files snuck in? * One more that didn't get removed :thinking: * all: Incorporate generics into Light Horizon code. (#4537) * bump go version to 18 on lighthorizon docker images, they need it now (#4541) * exp/lighthorizon/actions: use standard Problem model on API error responses (#4542) * exp/lighthorizon/build/index-batch: carry over map/reduce updates to latest docker layout on feature branch (#4543) * exp/lighthorizon: Properly transform transactions into JSON. (#4531) * exp/lighthorizon: Add a set of tools to aide in index inspection. (#4561) * exp/lighthorizon/cmd: index batch fix s3 sub paths in reduce (#4552) * exp/lighthorzon: Add a generic, thread-safe `SafeSet`. (#4572) * support/storage: Make the on-disk cache thread-safe. (#4575) * exp/lighthorizon: Incorporate tool subcommands into the webserver. (#4579) * exp/lighthorizon/index/cmd: Fix index single watch, slow down the retry on not-found ledgers (#4582) * exp/lighthorizon: Refactor archive interface and support parallel ledger downloads. (#4548) - Refactor and simplify Archive abstraction to incorporate MetaArchive - Actually add & use parallel downloads, preparing checkpoint chunks - Fix test structures and mocking - Fix cache to ignore on-disk if lockfile present * exp/lighthorizon: Minor error-handling and deployment improvements. (#4599) - actually set the PARALLEL_DOWNLOADS parameter to use #4468 - return a 404 rather than a 500 if a ledger is missing as its more descriptive - handle `count = 0` in average metric calculations * exp/lighthorizon/index: Add ability to disable bits in index. (#4601) * exp/lighthorizon: Add parameters to preload ledger cache. (#4615) * Add ability to preload cache in parallel after launching webserver * Default to 1 day of ledgers @ 6s each --------- Co-authored-by: Bartek Nowotarski Co-authored-by: Paul Bellamy Co-authored-by: Bartek Co-authored-by: Bartek Co-authored-by: tamirms Co-authored-by: George Co-authored-by: stfung77 Co-authored-by: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Co-authored-by: Alfonso Acosta Co-authored-by: Alfonso Acosta Co-authored-by: Graydon Hoare Co-authored-by: Satyam Zode <5508956+satyamz@users.noreply.github.com> Co-authored-by: Satyam Zode Co-authored-by: erika-sdf <92893480+erika-sdf@users.noreply.github.com> Co-authored-by: iateadonut Co-authored-by: Shawn Reuland Co-authored-by: shawn Co-authored-by: Shivendra Mishra Co-authored-by: Jacek Nykis Co-authored-by: jacekn Co-authored-by: George Kudrayvtsev --- .github/workflows/horizon.yml | 47 ++ Makefile | 3 +- clients/horizonclient/CHANGELOG.md | 7 + exp/lighthorizon/actions/accounts.go | 142 ++++++ exp/lighthorizon/actions/accounts_test.go | 191 +++++++ exp/lighthorizon/actions/apidocs.go | 26 + exp/lighthorizon/actions/main.go | 124 +++++ exp/lighthorizon/actions/problem.go | 94 ++++ exp/lighthorizon/actions/root.go | 29 ++ exp/lighthorizon/actions/static/api_docs.yml | 228 +++++++++ exp/lighthorizon/adapters/account_merge.go | 21 + exp/lighthorizon/adapters/allow_trust.go | 43 ++ .../begin_sponsoring_future_reserves.go | 15 + exp/lighthorizon/adapters/bump_sequence.go | 17 + exp/lighthorizon/adapters/change_trust.go | 63 +++ .../adapters/claim_claimable_balance.go | 26 + exp/lighthorizon/adapters/clawback.go | 37 ++ .../adapters/clawback_claimable_balance.go | 22 + exp/lighthorizon/adapters/create_account.go | 18 + .../adapters/create_claimable_balance.go | 27 + .../adapters/create_passive_sell_offer.go | 51 ++ .../end_sponsoring_future_reserves.go | 38 ++ exp/lighthorizon/adapters/inflation.go | 12 + .../adapters/liquidity_pool_deposit.go | 33 ++ .../adapters/liquidity_pool_withdraw.go | 24 + exp/lighthorizon/adapters/manage_buy_offer.go | 52 ++ exp/lighthorizon/adapters/manage_data.go | 23 + .../adapters/manage_sell_offer.go | 52 ++ exp/lighthorizon/adapters/operation.go | 93 ++++ .../adapters/path_payment_strict_receive.go | 78 +++ .../adapters/path_payment_strict_send.go | 78 +++ exp/lighthorizon/adapters/payment.go | 35 ++ .../adapters/revoke_sponsorship.go | 66 +++ exp/lighthorizon/adapters/set_options.go | 122 +++++ .../adapters/set_trust_line_flags.go | 83 +++ .../adapters/testdata/transactions.json | 67 +++ exp/lighthorizon/adapters/transaction.go | 295 +++++++++++ exp/lighthorizon/adapters/transaction_test.go | 81 +++ exp/lighthorizon/build/README.md | 24 + exp/lighthorizon/build/build.sh | 56 ++ exp/lighthorizon/build/index-batch/Dockerfile | 20 + exp/lighthorizon/build/index-batch/README.md | 7 + exp/lighthorizon/build/index-batch/start | 17 + .../build/index-single/Dockerfile | 25 + exp/lighthorizon/build/k8s/ledgerexporter.yml | 125 +++++ .../build/k8s/lighthorizon_batch_map_job.yml | 43 ++ .../k8s/lighthorizon_batch_reduce_job.yml | 42 ++ .../build/k8s/lighthorizon_index.yml | 74 +++ .../build/k8s/lighthorizon_web.yml | 133 +++++ .../build/ledgerexporter/Dockerfile | 33 ++ .../build/ledgerexporter/README.md | 42 ++ .../ledgerexporter/captive-core-pubnet.cfg | 206 ++++++++ .../ledgerexporter/captive-core-testnet.cfg | 30 ++ exp/lighthorizon/build/ledgerexporter/start | 55 ++ exp/lighthorizon/build/web/Dockerfile | 24 + exp/lighthorizon/common/operation.go | 52 ++ exp/lighthorizon/common/transaction.go | 70 +++ exp/lighthorizon/http.go | 78 +++ exp/lighthorizon/http_test.go | 64 +++ exp/lighthorizon/index/Makefile | 24 + exp/lighthorizon/index/backend/backend.go | 14 + exp/lighthorizon/index/backend/file.go | 214 ++++++++ exp/lighthorizon/index/backend/file_test.go | 43 ++ exp/lighthorizon/index/backend/gzip.go | 74 +++ exp/lighthorizon/index/backend/gzip_test.go | 61 +++ .../index/backend/parallel_flush.go | 73 +++ exp/lighthorizon/index/backend/s3.go | 220 ++++++++ exp/lighthorizon/index/builder.go | 366 ++++++++++++++ exp/lighthorizon/index/cmd/batch/doc.go | 52 ++ exp/lighthorizon/index/cmd/batch/map/main.go | 144 ++++++ .../index/cmd/batch/reduce/main.go | 389 ++++++++++++++ exp/lighthorizon/index/cmd/map.sh | 96 ++++ exp/lighthorizon/index/cmd/mapreduce_test.go | 232 +++++++++ exp/lighthorizon/index/cmd/reduce.sh | 75 +++ exp/lighthorizon/index/cmd/single/main.go | 59 +++ exp/lighthorizon/index/cmd/single_test.go | 279 ++++++++++ exp/lighthorizon/index/cmd/testdata/latest | 1 + .../index/cmd/testdata/ledgers/1410048 | Bin 0 -> 4160 bytes .../index/cmd/testdata/ledgers/1410049 | Bin 0 -> 5340 bytes .../index/cmd/testdata/ledgers/1410050 | Bin 0 -> 5136 bytes .../index/cmd/testdata/ledgers/1410051 | Bin 0 -> 4872 bytes .../index/cmd/testdata/ledgers/1410052 | Bin 0 -> 5052 bytes .../index/cmd/testdata/ledgers/1410053 | Bin 0 -> 3896 bytes .../index/cmd/testdata/ledgers/1410054 | Bin 0 -> 2820 bytes .../index/cmd/testdata/ledgers/1410055 | Bin 0 -> 2968 bytes .../index/cmd/testdata/ledgers/1410056 | Bin 0 -> 6084 bytes .../index/cmd/testdata/ledgers/1410057 | Bin 0 -> 4876 bytes .../index/cmd/testdata/ledgers/1410058 | Bin 0 -> 4876 bytes .../index/cmd/testdata/ledgers/1410059 | Bin 0 -> 4876 bytes .../index/cmd/testdata/ledgers/1410060 | Bin 0 -> 4084 bytes .../index/cmd/testdata/ledgers/1410061 | Bin 0 -> 5944 bytes .../index/cmd/testdata/ledgers/1410062 | Bin 0 -> 6732 bytes .../index/cmd/testdata/ledgers/1410063 | Bin 0 -> 6004 bytes .../index/cmd/testdata/ledgers/1410064 | Bin 0 -> 5940 bytes .../index/cmd/testdata/ledgers/1410065 | Bin 0 -> 4992 bytes .../index/cmd/testdata/ledgers/1410066 | Bin 0 -> 4004 bytes .../index/cmd/testdata/ledgers/1410067 | Bin 0 -> 6092 bytes .../index/cmd/testdata/ledgers/1410068 | Bin 0 -> 4864 bytes .../index/cmd/testdata/ledgers/1410069 | Bin 0 -> 4084 bytes .../index/cmd/testdata/ledgers/1410070 | Bin 0 -> 3928 bytes .../index/cmd/testdata/ledgers/1410071 | Bin 0 -> 2900 bytes .../index/cmd/testdata/ledgers/1410072 | Bin 0 -> 5316 bytes .../index/cmd/testdata/ledgers/1410073 | Bin 0 -> 5080 bytes .../index/cmd/testdata/ledgers/1410074 | Bin 0 -> 5928 bytes .../index/cmd/testdata/ledgers/1410075 | Bin 0 -> 6060 bytes .../index/cmd/testdata/ledgers/1410076 | Bin 0 -> 3876 bytes .../index/cmd/testdata/ledgers/1410077 | Bin 0 -> 3700 bytes .../index/cmd/testdata/ledgers/1410078 | Bin 0 -> 5132 bytes .../index/cmd/testdata/ledgers/1410079 | Bin 0 -> 4852 bytes .../index/cmd/testdata/ledgers/1410080 | Bin 0 -> 4704 bytes .../index/cmd/testdata/ledgers/1410081 | Bin 0 -> 6180 bytes .../index/cmd/testdata/ledgers/1410082 | Bin 0 -> 4884 bytes .../index/cmd/testdata/ledgers/1410083 | Bin 0 -> 5916 bytes .../index/cmd/testdata/ledgers/1410084 | Bin 0 -> 6220 bytes .../index/cmd/testdata/ledgers/1410085 | Bin 0 -> 5972 bytes .../index/cmd/testdata/ledgers/1410086 | Bin 0 -> 4528 bytes .../index/cmd/testdata/ledgers/1410087 | Bin 0 -> 3704 bytes .../index/cmd/testdata/ledgers/1410088 | Bin 0 -> 4048 bytes .../index/cmd/testdata/ledgers/1410089 | Bin 0 -> 5080 bytes .../index/cmd/testdata/ledgers/1410090 | Bin 0 -> 3696 bytes .../index/cmd/testdata/ledgers/1410091 | Bin 0 -> 2680 bytes .../index/cmd/testdata/ledgers/1410092 | Bin 0 -> 2904 bytes .../index/cmd/testdata/ledgers/1410093 | Bin 0 -> 4696 bytes .../index/cmd/testdata/ledgers/1410094 | Bin 0 -> 4628 bytes .../index/cmd/testdata/ledgers/1410095 | Bin 0 -> 5132 bytes .../index/cmd/testdata/ledgers/1410096 | Bin 0 -> 7196 bytes .../index/cmd/testdata/ledgers/1410097 | Bin 0 -> 6016 bytes .../index/cmd/testdata/ledgers/1410098 | Bin 0 -> 7080 bytes .../index/cmd/testdata/ledgers/1410099 | Bin 0 -> 4708 bytes .../index/cmd/testdata/ledgers/1410100 | Bin 0 -> 4844 bytes .../index/cmd/testdata/ledgers/1410101 | Bin 0 -> 3860 bytes .../index/cmd/testdata/ledgers/1410102 | Bin 0 -> 5768 bytes .../index/cmd/testdata/ledgers/1410103 | Bin 0 -> 5580 bytes .../index/cmd/testdata/ledgers/1410104 | Bin 0 -> 4964 bytes .../index/cmd/testdata/ledgers/1410105 | Bin 0 -> 4984 bytes .../index/cmd/testdata/ledgers/1410106 | Bin 0 -> 5080 bytes .../index/cmd/testdata/ledgers/1410107 | Bin 0 -> 4032 bytes .../index/cmd/testdata/ledgers/1410108 | Bin 0 -> 2968 bytes .../index/cmd/testdata/ledgers/1410109 | Bin 0 -> 5084 bytes .../index/cmd/testdata/ledgers/1410110 | Bin 0 -> 2740 bytes .../index/cmd/testdata/ledgers/1410111 | Bin 0 -> 5212 bytes .../index/cmd/testdata/ledgers/1410112 | Bin 0 -> 4884 bytes .../index/cmd/testdata/ledgers/1410113 | Bin 0 -> 4708 bytes .../index/cmd/testdata/ledgers/1410114 | Bin 0 -> 4644 bytes .../index/cmd/testdata/ledgers/1410115 | Bin 0 -> 4868 bytes .../index/cmd/testdata/ledgers/1410116 | Bin 0 -> 4696 bytes .../index/cmd/testdata/ledgers/1410117 | Bin 0 -> 5836 bytes .../index/cmd/testdata/ledgers/1410118 | Bin 0 -> 4876 bytes .../index/cmd/testdata/ledgers/1410119 | Bin 0 -> 7452 bytes .../index/cmd/testdata/ledgers/1410120 | Bin 0 -> 6060 bytes .../index/cmd/testdata/ledgers/1410121 | Bin 0 -> 5948 bytes .../index/cmd/testdata/ledgers/1410122 | Bin 0 -> 4908 bytes .../index/cmd/testdata/ledgers/1410123 | Bin 0 -> 3924 bytes .../index/cmd/testdata/ledgers/1410124 | Bin 0 -> 3844 bytes .../index/cmd/testdata/ledgers/1410125 | Bin 0 -> 2496 bytes .../index/cmd/testdata/ledgers/1410126 | Bin 0 -> 2752 bytes .../index/cmd/testdata/ledgers/1410127 | Bin 0 -> 3928 bytes .../index/cmd/testdata/ledgers/1410128 | Bin 0 -> 4960 bytes .../index/cmd/testdata/ledgers/1410129 | Bin 0 -> 3976 bytes .../index/cmd/testdata/ledgers/1410130 | Bin 0 -> 5184 bytes .../index/cmd/testdata/ledgers/1410131 | Bin 0 -> 5880 bytes .../index/cmd/testdata/ledgers/1410132 | Bin 0 -> 6120 bytes .../index/cmd/testdata/ledgers/1410133 | Bin 0 -> 5292 bytes .../index/cmd/testdata/ledgers/1410134 | Bin 0 -> 5124 bytes .../index/cmd/testdata/ledgers/1410135 | Bin 0 -> 4876 bytes .../index/cmd/testdata/ledgers/1410136 | Bin 0 -> 5036 bytes .../index/cmd/testdata/ledgers/1410137 | Bin 0 -> 5144 bytes .../index/cmd/testdata/ledgers/1410138 | Bin 0 -> 3876 bytes .../index/cmd/testdata/ledgers/1410139 | Bin 0 -> 4908 bytes .../index/cmd/testdata/ledgers/1410140 | Bin 0 -> 3924 bytes .../index/cmd/testdata/ledgers/1410141 | Bin 0 -> 3844 bytes .../index/cmd/testdata/ledgers/1410142 | Bin 0 -> 6092 bytes .../index/cmd/testdata/ledgers/1410143 | Bin 0 -> 5960 bytes .../index/cmd/testdata/ledgers/1410144 | Bin 0 -> 6080 bytes .../index/cmd/testdata/ledgers/1410145 | Bin 0 -> 3976 bytes .../index/cmd/testdata/ledgers/1410146 | Bin 0 -> 3896 bytes .../index/cmd/testdata/ledgers/1410147 | Bin 0 -> 2840 bytes .../index/cmd/testdata/ledgers/1410148 | Bin 0 -> 2920 bytes .../index/cmd/testdata/ledgers/1410149 | Bin 0 -> 2744 bytes .../index/cmd/testdata/ledgers/1410150 | Bin 0 -> 6084 bytes .../index/cmd/testdata/ledgers/1410151 | Bin 0 -> 4796 bytes .../index/cmd/testdata/ledgers/1410152 | Bin 0 -> 4748 bytes .../index/cmd/testdata/ledgers/1410153 | Bin 0 -> 6116 bytes .../index/cmd/testdata/ledgers/1410154 | Bin 0 -> 5896 bytes .../index/cmd/testdata/ledgers/1410155 | Bin 0 -> 5132 bytes .../index/cmd/testdata/ledgers/1410156 | Bin 0 -> 3844 bytes .../index/cmd/testdata/ledgers/1410157 | Bin 0 -> 3796 bytes .../index/cmd/testdata/ledgers/1410158 | Bin 0 -> 4884 bytes .../index/cmd/testdata/ledgers/1410159 | Bin 0 -> 4708 bytes .../index/cmd/testdata/ledgers/1410160 | Bin 0 -> 4644 bytes .../index/cmd/testdata/ledgers/1410161 | Bin 0 -> 7296 bytes .../index/cmd/testdata/ledgers/1410162 | Bin 0 -> 7176 bytes .../index/cmd/testdata/ledgers/1410163 | Bin 0 -> 4700 bytes .../index/cmd/testdata/ledgers/1410164 | Bin 0 -> 2920 bytes .../index/cmd/testdata/ledgers/1410165 | Bin 0 -> 4032 bytes .../index/cmd/testdata/ledgers/1410166 | Bin 0 -> 7036 bytes .../index/cmd/testdata/ledgers/1410167 | Bin 0 -> 3856 bytes .../index/cmd/testdata/ledgers/1410168 | Bin 0 -> 5648 bytes .../index/cmd/testdata/ledgers/1410169 | Bin 0 -> 5600 bytes .../index/cmd/testdata/ledgers/1410170 | Bin 0 -> 3876 bytes .../index/cmd/testdata/ledgers/1410171 | Bin 0 -> 4908 bytes .../index/cmd/testdata/ledgers/1410172 | Bin 0 -> 3924 bytes .../index/cmd/testdata/ledgers/1410173 | Bin 0 -> 3844 bytes .../index/cmd/testdata/ledgers/1410174 | Bin 0 -> 4704 bytes .../index/cmd/testdata/ledgers/1410175 | Bin 0 -> 4860 bytes .../index/cmd/testdata/ledgers/1410176 | Bin 0 -> 6248 bytes .../index/cmd/testdata/ledgers/1410177 | Bin 0 -> 7168 bytes .../index/cmd/testdata/ledgers/1410178 | Bin 0 -> 5828 bytes .../index/cmd/testdata/ledgers/1410179 | Bin 0 -> 4932 bytes .../index/cmd/testdata/ledgers/1410180 | Bin 0 -> 3844 bytes .../index/cmd/testdata/ledgers/1410181 | Bin 0 -> 2840 bytes .../index/cmd/testdata/ledgers/1410182 | Bin 0 -> 3872 bytes .../index/cmd/testdata/ledgers/1410183 | Bin 0 -> 4904 bytes .../index/cmd/testdata/ledgers/1410184 | Bin 0 -> 3920 bytes .../index/cmd/testdata/ledgers/1410185 | Bin 0 -> 3840 bytes .../index/cmd/testdata/ledgers/1410186 | Bin 0 -> 2840 bytes .../index/cmd/testdata/ledgers/1410187 | Bin 0 -> 5164 bytes .../index/cmd/testdata/ledgers/1410188 | Bin 0 -> 4908 bytes .../index/cmd/testdata/ledgers/1410189 | Bin 0 -> 7128 bytes .../index/cmd/testdata/ledgers/1410190 | Bin 0 -> 5108 bytes .../index/cmd/testdata/ledgers/1410191 | Bin 0 -> 4884 bytes .../index/cmd/testdata/ledgers/1410192 | Bin 0 -> 4708 bytes .../index/cmd/testdata/ledgers/1410193 | Bin 0 -> 4884 bytes .../index/cmd/testdata/ledgers/1410194 | Bin 0 -> 6092 bytes .../index/cmd/testdata/ledgers/1410195 | Bin 0 -> 4864 bytes .../index/cmd/testdata/ledgers/1410196 | Bin 0 -> 4992 bytes .../index/cmd/testdata/ledgers/1410197 | Bin 0 -> 4004 bytes .../index/cmd/testdata/ledgers/1410198 | Bin 0 -> 4828 bytes .../index/cmd/testdata/ledgers/1410199 | Bin 0 -> 4828 bytes .../index/cmd/testdata/ledgers/1410200 | Bin 0 -> 6368 bytes .../index/cmd/testdata/ledgers/1410201 | Bin 0 -> 4928 bytes .../index/cmd/testdata/ledgers/1410202 | Bin 0 -> 4612 bytes .../index/cmd/testdata/ledgers/1410203 | Bin 0 -> 4168 bytes .../index/cmd/testdata/ledgers/1410204 | Bin 0 -> 3992 bytes .../index/cmd/testdata/ledgers/1410205 | Bin 0 -> 6092 bytes .../index/cmd/testdata/ledgers/1410206 | Bin 0 -> 4884 bytes .../index/cmd/testdata/ledgers/1410207 | Bin 0 -> 4864 bytes .../index/cmd/testdata/ledgers/1410208 | Bin 0 -> 4992 bytes .../index/cmd/testdata/ledgers/1410209 | Bin 0 -> 5012 bytes .../index/cmd/testdata/ledgers/1410210 | Bin 0 -> 4884 bytes .../index/cmd/testdata/ledgers/1410211 | Bin 0 -> 11720 bytes .../index/cmd/testdata/ledgers/1410212 | Bin 0 -> 6068 bytes .../index/cmd/testdata/ledgers/1410213 | Bin 0 -> 6184 bytes .../index/cmd/testdata/ledgers/1410214 | Bin 0 -> 5112 bytes .../index/cmd/testdata/ledgers/1410215 | Bin 0 -> 6116 bytes .../index/cmd/testdata/ledgers/1410216 | Bin 0 -> 6016 bytes .../index/cmd/testdata/ledgers/1410217 | Bin 0 -> 3984 bytes .../index/cmd/testdata/ledgers/1410218 | Bin 0 -> 4336 bytes .../index/cmd/testdata/ledgers/1410219 | Bin 0 -> 2920 bytes .../index/cmd/testdata/ledgers/1410220 | Bin 0 -> 2900 bytes .../index/cmd/testdata/ledgers/1410221 | Bin 0 -> 3028 bytes .../index/cmd/testdata/ledgers/1410222 | Bin 0 -> 5372 bytes .../index/cmd/testdata/ledgers/1410223 | Bin 0 -> 5500 bytes .../index/cmd/testdata/ledgers/1410224 | Bin 0 -> 6136 bytes .../index/cmd/testdata/ledgers/1410225 | Bin 0 -> 6004 bytes .../index/cmd/testdata/ledgers/1410226 | Bin 0 -> 5920 bytes .../index/cmd/testdata/ledgers/1410227 | Bin 0 -> 6140 bytes .../index/cmd/testdata/ledgers/1410228 | Bin 0 -> 3844 bytes .../index/cmd/testdata/ledgers/1410229 | Bin 0 -> 4728 bytes .../index/cmd/testdata/ledgers/1410230 | Bin 0 -> 4808 bytes .../index/cmd/testdata/ledgers/1410231 | Bin 0 -> 4876 bytes .../index/cmd/testdata/ledgers/1410232 | Bin 0 -> 4796 bytes .../index/cmd/testdata/ledgers/1410233 | Bin 0 -> 5052 bytes .../index/cmd/testdata/ledgers/1410234 | Bin 0 -> 5436 bytes .../index/cmd/testdata/ledgers/1410235 | Bin 0 -> 5156 bytes .../index/cmd/testdata/ledgers/1410236 | Bin 0 -> 5044 bytes .../index/cmd/testdata/ledgers/1410237 | Bin 0 -> 3036 bytes .../index/cmd/testdata/ledgers/1410238 | Bin 0 -> 5196 bytes .../index/cmd/testdata/ledgers/1410239 | Bin 0 -> 5412 bytes .../index/cmd/testdata/ledgers/1410240 | Bin 0 -> 3280 bytes .../index/cmd/testdata/ledgers/1410241 | Bin 0 -> 5268 bytes .../index/cmd/testdata/ledgers/1410242 | Bin 0 -> 6556 bytes .../index/cmd/testdata/ledgers/1410243 | Bin 0 -> 4884 bytes .../index/cmd/testdata/ledgers/1410244 | Bin 0 -> 7236 bytes .../index/cmd/testdata/ledgers/1410245 | Bin 0 -> 7088 bytes .../index/cmd/testdata/ledgers/1410246 | Bin 0 -> 7160 bytes .../index/cmd/testdata/ledgers/1410247 | Bin 0 -> 4728 bytes .../index/cmd/testdata/ledgers/1410248 | Bin 0 -> 5928 bytes .../index/cmd/testdata/ledgers/1410249 | Bin 0 -> 5132 bytes .../index/cmd/testdata/ledgers/1410250 | Bin 0 -> 3844 bytes .../index/cmd/testdata/ledgers/1410251 | Bin 0 -> 3844 bytes .../index/cmd/testdata/ledgers/1410252 | Bin 0 -> 4148 bytes .../index/cmd/testdata/ledgers/1410253 | Bin 0 -> 5088 bytes .../index/cmd/testdata/ledgers/1410254 | Bin 0 -> 4932 bytes .../index/cmd/testdata/ledgers/1410255 | Bin 0 -> 6208 bytes .../index/cmd/testdata/ledgers/1410256 | Bin 0 -> 2864 bytes .../index/cmd/testdata/ledgers/1410257 | Bin 0 -> 3892 bytes .../index/cmd/testdata/ledgers/1410258 | Bin 0 -> 1528 bytes .../index/cmd/testdata/ledgers/1410259 | Bin 0 -> 2648 bytes .../index/cmd/testdata/ledgers/1410260 | Bin 0 -> 2736 bytes .../index/cmd/testdata/ledgers/1410261 | Bin 0 -> 2428 bytes .../index/cmd/testdata/ledgers/1410262 | Bin 0 -> 2428 bytes .../index/cmd/testdata/ledgers/1410263 | Bin 0 -> 2428 bytes .../index/cmd/testdata/ledgers/1410264 | Bin 0 -> 3588 bytes .../index/cmd/testdata/ledgers/1410265 | Bin 0 -> 1476 bytes .../index/cmd/testdata/ledgers/1410266 | Bin 0 -> 2684 bytes .../index/cmd/testdata/ledgers/1410267 | Bin 0 -> 2764 bytes .../index/cmd/testdata/ledgers/1410268 | Bin 0 -> 3876 bytes .../index/cmd/testdata/ledgers/1410269 | Bin 0 -> 7072 bytes .../index/cmd/testdata/ledgers/1410270 | Bin 0 -> 6052 bytes .../index/cmd/testdata/ledgers/1410271 | Bin 0 -> 6060 bytes .../index/cmd/testdata/ledgers/1410272 | Bin 0 -> 6276 bytes .../index/cmd/testdata/ledgers/1410273 | Bin 0 -> 4864 bytes .../index/cmd/testdata/ledgers/1410274 | Bin 0 -> 3976 bytes .../index/cmd/testdata/ledgers/1410275 | Bin 0 -> 3896 bytes .../index/cmd/testdata/ledgers/1410276 | Bin 0 -> 3896 bytes .../index/cmd/testdata/ledgers/1410277 | Bin 0 -> 5352 bytes .../index/cmd/testdata/ledgers/1410278 | Bin 0 -> 4076 bytes .../index/cmd/testdata/ledgers/1410279 | Bin 0 -> 4876 bytes .../index/cmd/testdata/ledgers/1410280 | Bin 0 -> 6020 bytes .../index/cmd/testdata/ledgers/1410281 | Bin 0 -> 4100 bytes .../index/cmd/testdata/ledgers/1410282 | Bin 0 -> 2684 bytes .../index/cmd/testdata/ledgers/1410283 | Bin 0 -> 2596 bytes .../index/cmd/testdata/ledgers/1410284 | Bin 0 -> 1476 bytes .../index/cmd/testdata/ledgers/1410285 | Bin 0 -> 2484 bytes .../index/cmd/testdata/ledgers/1410286 | Bin 0 -> 2484 bytes .../index/cmd/testdata/ledgers/1410287 | Bin 0 -> 2484 bytes .../index/cmd/testdata/ledgers/1410288 | Bin 0 -> 3692 bytes .../index/cmd/testdata/ledgers/1410289 | Bin 0 -> 2484 bytes .../index/cmd/testdata/ledgers/1410290 | Bin 0 -> 2484 bytes .../index/cmd/testdata/ledgers/1410291 | Bin 0 -> 3772 bytes .../index/cmd/testdata/ledgers/1410292 | Bin 0 -> 2708 bytes .../index/cmd/testdata/ledgers/1410293 | Bin 0 -> 6368 bytes .../index/cmd/testdata/ledgers/1410294 | Bin 0 -> 3920 bytes .../index/cmd/testdata/ledgers/1410295 | Bin 0 -> 4736 bytes .../index/cmd/testdata/ledgers/1410296 | Bin 0 -> 4076 bytes .../index/cmd/testdata/ledgers/1410297 | Bin 0 -> 2664 bytes .../index/cmd/testdata/ledgers/1410298 | Bin 0 -> 4080 bytes .../index/cmd/testdata/ledgers/1410299 | Bin 0 -> 4828 bytes .../index/cmd/testdata/ledgers/1410300 | Bin 0 -> 4148 bytes .../index/cmd/testdata/ledgers/1410301 | Bin 0 -> 3844 bytes .../index/cmd/testdata/ledgers/1410302 | Bin 0 -> 5088 bytes .../index/cmd/testdata/ledgers/1410303 | Bin 0 -> 6076 bytes .../index/cmd/testdata/ledgers/1410304 | Bin 0 -> 6376 bytes .../index/cmd/testdata/ledgers/1410305 | Bin 0 -> 8292 bytes .../index/cmd/testdata/ledgers/1410306 | Bin 0 -> 6692 bytes .../index/cmd/testdata/ledgers/1410307 | Bin 0 -> 5688 bytes .../index/cmd/testdata/ledgers/1410308 | Bin 0 -> 3228 bytes .../index/cmd/testdata/ledgers/1410309 | Bin 0 -> 2428 bytes .../index/cmd/testdata/ledgers/1410310 | Bin 0 -> 3636 bytes .../index/cmd/testdata/ledgers/1410311 | Bin 0 -> 1472 bytes .../index/cmd/testdata/ledgers/1410312 | Bin 0 -> 1472 bytes .../index/cmd/testdata/ledgers/1410313 | Bin 0 -> 520 bytes .../index/cmd/testdata/ledgers/1410314 | Bin 0 -> 1596 bytes .../index/cmd/testdata/ledgers/1410315 | Bin 0 -> 1728 bytes .../index/cmd/testdata/ledgers/1410316 | Bin 0 -> 2764 bytes .../index/cmd/testdata/ledgers/1410317 | Bin 0 -> 1476 bytes .../index/cmd/testdata/ledgers/1410318 | Bin 0 -> 4892 bytes .../index/cmd/testdata/ledgers/1410319 | Bin 0 -> 3596 bytes .../index/cmd/testdata/ledgers/1410320 | Bin 0 -> 4884 bytes .../index/cmd/testdata/ledgers/1410321 | Bin 0 -> 6140 bytes .../index/cmd/testdata/ledgers/1410322 | Bin 0 -> 4628 bytes .../index/cmd/testdata/ledgers/1410323 | Bin 0 -> 5088 bytes .../index/cmd/testdata/ledgers/1410324 | Bin 0 -> 3620 bytes .../index/cmd/testdata/ledgers/1410325 | Bin 0 -> 5032 bytes .../index/cmd/testdata/ledgers/1410326 | Bin 0 -> 5648 bytes .../index/cmd/testdata/ledgers/1410327 | Bin 0 -> 7596 bytes .../index/cmd/testdata/ledgers/1410328 | Bin 0 -> 4796 bytes .../index/cmd/testdata/ledgers/1410329 | Bin 0 -> 4244 bytes .../index/cmd/testdata/ledgers/1410330 | Bin 0 -> 3036 bytes .../index/cmd/testdata/ledgers/1410331 | Bin 0 -> 3124 bytes .../index/cmd/testdata/ledgers/1410332 | Bin 0 -> 5040 bytes .../index/cmd/testdata/ledgers/1410333 | Bin 0 -> 3608 bytes .../index/cmd/testdata/ledgers/1410334 | Bin 0 -> 3660 bytes .../index/cmd/testdata/ledgers/1410335 | Bin 0 -> 4236 bytes .../index/cmd/testdata/ledgers/1410336 | Bin 0 -> 2484 bytes .../index/cmd/testdata/ledgers/1410337 | Bin 0 -> 4768 bytes .../index/cmd/testdata/ledgers/1410338 | Bin 0 -> 2484 bytes .../index/cmd/testdata/ledgers/1410339 | Bin 0 -> 3772 bytes .../index/cmd/testdata/ledgers/1410340 | Bin 0 -> 1476 bytes .../index/cmd/testdata/ledgers/1410341 | Bin 0 -> 3548 bytes .../index/cmd/testdata/ledgers/1410342 | Bin 0 -> 2428 bytes .../index/cmd/testdata/ledgers/1410343 | Bin 0 -> 3636 bytes .../index/cmd/testdata/ledgers/1410344 | Bin 0 -> 2428 bytes .../index/cmd/testdata/ledgers/1410345 | Bin 0 -> 2764 bytes .../index/cmd/testdata/ledgers/1410346 | Bin 0 -> 3876 bytes .../index/cmd/testdata/ledgers/1410347 | Bin 0 -> 3700 bytes .../index/cmd/testdata/ledgers/1410348 | Bin 0 -> 5252 bytes .../index/cmd/testdata/ledgers/1410349 | Bin 0 -> 2888 bytes .../index/cmd/testdata/ledgers/1410350 | Bin 0 -> 5600 bytes .../index/cmd/testdata/ledgers/1410351 | Bin 0 -> 3280 bytes .../index/cmd/testdata/ledgers/1410352 | Bin 0 -> 3936 bytes .../index/cmd/testdata/ledgers/1410353 | Bin 0 -> 6092 bytes .../index/cmd/testdata/ledgers/1410354 | Bin 0 -> 4708 bytes .../index/cmd/testdata/ledgers/1410355 | Bin 0 -> 6060 bytes .../index/cmd/testdata/ledgers/1410356 | Bin 0 -> 5804 bytes .../index/cmd/testdata/ledgers/1410357 | Bin 0 -> 4728 bytes .../index/cmd/testdata/ledgers/1410358 | Bin 0 -> 4808 bytes .../index/cmd/testdata/ledgers/1410359 | Bin 0 -> 6084 bytes .../index/cmd/testdata/ledgers/1410360 | Bin 0 -> 4920 bytes .../index/cmd/testdata/ledgers/1410361 | Bin 0 -> 3844 bytes .../index/cmd/testdata/ledgers/1410362 | Bin 0 -> 5436 bytes .../index/cmd/testdata/ledgers/1410363 | Bin 0 -> 4080 bytes .../index/cmd/testdata/ledgers/1410364 | Bin 0 -> 6252 bytes .../index/cmd/testdata/ledgers/1410365 | Bin 0 -> 5000 bytes .../index/cmd/testdata/ledgers/1410366 | Bin 0 -> 4996 bytes .../index/cmd/testdata/ledgers/1410367 | Bin 0 -> 6884 bytes .../index/cmd/testdata/regenerate.sh | 3 + exp/lighthorizon/index/connect.go | 68 +++ exp/lighthorizon/index/mock_store.go | 78 +++ exp/lighthorizon/index/modules.go | 314 ++++++++++++ exp/lighthorizon/index/store.go | 377 ++++++++++++++ exp/lighthorizon/index/types/bitmap.go | 367 ++++++++++++++ exp/lighthorizon/index/types/bitmap_test.go | 382 ++++++++++++++ exp/lighthorizon/index/types/trie.go | 345 +++++++++++++ exp/lighthorizon/index/types/trie_test.go | 297 +++++++++++ exp/lighthorizon/ingester/ingester.go | 55 ++ exp/lighthorizon/ingester/main.go | 87 ++++ exp/lighthorizon/ingester/mock_ingester.go | 44 ++ .../ingester/parallel_ingester.go | 141 ++++++ exp/lighthorizon/ingester/participants.go | 35 ++ exp/lighthorizon/main.go | 183 +++++++ exp/lighthorizon/services/cursor.go | 102 ++++ exp/lighthorizon/services/cursor_test.go | 96 ++++ exp/lighthorizon/services/main.go | 216 ++++++++ exp/lighthorizon/services/main_test.go | 250 +++++++++ exp/lighthorizon/services/mock_services.go | 32 ++ exp/lighthorizon/services/operations.go | 90 ++++ exp/lighthorizon/services/transactions.go | 76 +++ exp/lighthorizon/tools/cache.go | 270 ++++++++++ exp/lighthorizon/tools/index.go | 356 +++++++++++++ exp/lighthorizon/tools/index_test.go | 58 +++ exp/services/ledgerexporter/main.go | 181 +++++++ exp/tools/dump-ledger-state/main.go | 16 +- exp/tools/dump-orderbook/main.go | 16 +- go.mod | 55 +- go.sum | 97 ++-- gxdr/xdr_generated.go | 238 ++++++++- historyarchive/archive.go | 66 +-- historyarchive/archive_pool.go | 8 +- historyarchive/archive_test.go | 29 +- historyarchive/mock_archive.go | 8 +- historyarchive/range.go | 6 + historyarchive/util.go | 24 +- ingest/doc_test.go | 7 +- ingest/ledgerbackend/captive_core_backend.go | 7 +- .../ledgerbackend/history_archive_backend.go | 51 ++ ingest/tutorial/example_claimables.go | 9 +- metaarchive/main.go | 62 +++ services/horizon/CHANGELOG.md | 128 +++++ .../horizon/docker/verify-range/README.md | 2 +- .../internal/configs/captive-core-pubnet.cfg | 4 +- services/horizon/internal/httpx/middleware.go | 58 +-- services/horizon/internal/ingest/main.go | 9 +- .../horizon/internal/integration/db_test.go | 25 +- support/collections/maps/map.go | 17 + support/collections/maps/map_test.go | 26 + support/collections/set/iset.go | 12 + support/collections/set/safeset.go | 51 ++ support/collections/set/set.go | 16 + support/collections/set/set_test.go | 13 +- support/http/logging_middleware.go | 45 ++ .../http/sanitize_route_test.go | 7 +- support/ordered/math.go | 26 + .../storage/filesystem.go | 32 +- support/storage/gcs.go | 137 +++++ .../storage/http.go | 78 +-- support/storage/main.go | 118 +++++ support/storage/ondisk_cache.go | 260 ++++++++++ .../s3_archive.go => support/storage/s3.go | 138 +++-- support/storage/s3_test.go | 114 +++++ toid/main.go | 3 + tools/archive-reader/archive_reader.go | 14 +- tools/stellar-archivist/main.go | 2 +- tools/stellar-archivist/main_test.go | 6 +- xdr/Stellar-lighthorizon.x | 39 ++ xdr/xdr_generated.go | 478 ++++++++++++++++++ 467 files changed, 12809 insertions(+), 350 deletions(-) create mode 100644 exp/lighthorizon/actions/accounts.go create mode 100644 exp/lighthorizon/actions/accounts_test.go create mode 100644 exp/lighthorizon/actions/apidocs.go create mode 100644 exp/lighthorizon/actions/main.go create mode 100644 exp/lighthorizon/actions/problem.go create mode 100644 exp/lighthorizon/actions/root.go create mode 100644 exp/lighthorizon/actions/static/api_docs.yml create mode 100644 exp/lighthorizon/adapters/account_merge.go create mode 100644 exp/lighthorizon/adapters/allow_trust.go create mode 100644 exp/lighthorizon/adapters/begin_sponsoring_future_reserves.go create mode 100644 exp/lighthorizon/adapters/bump_sequence.go create mode 100644 exp/lighthorizon/adapters/change_trust.go create mode 100644 exp/lighthorizon/adapters/claim_claimable_balance.go create mode 100644 exp/lighthorizon/adapters/clawback.go create mode 100644 exp/lighthorizon/adapters/clawback_claimable_balance.go create mode 100644 exp/lighthorizon/adapters/create_account.go create mode 100644 exp/lighthorizon/adapters/create_claimable_balance.go create mode 100644 exp/lighthorizon/adapters/create_passive_sell_offer.go create mode 100644 exp/lighthorizon/adapters/end_sponsoring_future_reserves.go create mode 100644 exp/lighthorizon/adapters/inflation.go create mode 100644 exp/lighthorizon/adapters/liquidity_pool_deposit.go create mode 100644 exp/lighthorizon/adapters/liquidity_pool_withdraw.go create mode 100644 exp/lighthorizon/adapters/manage_buy_offer.go create mode 100644 exp/lighthorizon/adapters/manage_data.go create mode 100644 exp/lighthorizon/adapters/manage_sell_offer.go create mode 100644 exp/lighthorizon/adapters/operation.go create mode 100644 exp/lighthorizon/adapters/path_payment_strict_receive.go create mode 100644 exp/lighthorizon/adapters/path_payment_strict_send.go create mode 100644 exp/lighthorizon/adapters/payment.go create mode 100644 exp/lighthorizon/adapters/revoke_sponsorship.go create mode 100644 exp/lighthorizon/adapters/set_options.go create mode 100644 exp/lighthorizon/adapters/set_trust_line_flags.go create mode 100644 exp/lighthorizon/adapters/testdata/transactions.json create mode 100644 exp/lighthorizon/adapters/transaction.go create mode 100644 exp/lighthorizon/adapters/transaction_test.go create mode 100644 exp/lighthorizon/build/README.md create mode 100755 exp/lighthorizon/build/build.sh create mode 100644 exp/lighthorizon/build/index-batch/Dockerfile create mode 100644 exp/lighthorizon/build/index-batch/README.md create mode 100644 exp/lighthorizon/build/index-batch/start create mode 100644 exp/lighthorizon/build/index-single/Dockerfile create mode 100644 exp/lighthorizon/build/k8s/ledgerexporter.yml create mode 100644 exp/lighthorizon/build/k8s/lighthorizon_batch_map_job.yml create mode 100644 exp/lighthorizon/build/k8s/lighthorizon_batch_reduce_job.yml create mode 100644 exp/lighthorizon/build/k8s/lighthorizon_index.yml create mode 100644 exp/lighthorizon/build/k8s/lighthorizon_web.yml create mode 100644 exp/lighthorizon/build/ledgerexporter/Dockerfile create mode 100644 exp/lighthorizon/build/ledgerexporter/README.md create mode 100644 exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg create mode 100644 exp/lighthorizon/build/ledgerexporter/captive-core-testnet.cfg create mode 100644 exp/lighthorizon/build/ledgerexporter/start create mode 100644 exp/lighthorizon/build/web/Dockerfile create mode 100644 exp/lighthorizon/common/operation.go create mode 100644 exp/lighthorizon/common/transaction.go create mode 100644 exp/lighthorizon/http.go create mode 100644 exp/lighthorizon/http_test.go create mode 100644 exp/lighthorizon/index/Makefile create mode 100644 exp/lighthorizon/index/backend/backend.go create mode 100644 exp/lighthorizon/index/backend/file.go create mode 100644 exp/lighthorizon/index/backend/file_test.go create mode 100644 exp/lighthorizon/index/backend/gzip.go create mode 100644 exp/lighthorizon/index/backend/gzip_test.go create mode 100644 exp/lighthorizon/index/backend/parallel_flush.go create mode 100644 exp/lighthorizon/index/backend/s3.go create mode 100644 exp/lighthorizon/index/builder.go create mode 100644 exp/lighthorizon/index/cmd/batch/doc.go create mode 100644 exp/lighthorizon/index/cmd/batch/map/main.go create mode 100644 exp/lighthorizon/index/cmd/batch/reduce/main.go create mode 100755 exp/lighthorizon/index/cmd/map.sh create mode 100644 exp/lighthorizon/index/cmd/mapreduce_test.go create mode 100755 exp/lighthorizon/index/cmd/reduce.sh create mode 100644 exp/lighthorizon/index/cmd/single/main.go create mode 100644 exp/lighthorizon/index/cmd/single_test.go create mode 100644 exp/lighthorizon/index/cmd/testdata/latest create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410048 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410049 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410050 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410051 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410052 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410053 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410054 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410055 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410056 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410057 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410058 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410059 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410060 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410061 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410062 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410063 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410064 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410065 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410066 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410067 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410068 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410069 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410070 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410071 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410072 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410073 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410074 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410075 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410076 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410077 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410078 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410079 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410080 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410081 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410082 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410083 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410084 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410085 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410086 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410087 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410088 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410089 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410090 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410091 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410092 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410093 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410094 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410095 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410096 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410097 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410098 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410099 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410100 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410101 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410102 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410103 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410104 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410105 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410106 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410107 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410108 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410109 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410110 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410111 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410112 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410113 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410114 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410115 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410116 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410117 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410118 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410119 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410120 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410121 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410122 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410123 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410124 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410125 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410126 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410127 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410128 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410129 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410130 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410131 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410132 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410133 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410134 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410135 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410136 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410137 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410138 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410139 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410140 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410141 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410142 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410143 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410144 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410145 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410146 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410147 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410148 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410149 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410150 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410151 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410152 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410153 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410154 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410155 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410156 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410157 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410158 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410159 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410160 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410161 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410162 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410163 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410164 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410165 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410166 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410167 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410168 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410169 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410170 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410171 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410172 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410173 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410174 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410175 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410176 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410177 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410178 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410179 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410180 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410181 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410182 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410183 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410184 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410185 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410186 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410187 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410188 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410189 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410190 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410191 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410192 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410193 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410194 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410195 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410196 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410197 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410198 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410199 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410200 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410201 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410202 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410203 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410204 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410205 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410206 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410207 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410208 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410209 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410210 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410211 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410212 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410213 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410214 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410215 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410216 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410217 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410218 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410219 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410220 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410221 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410222 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410223 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410224 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410225 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410226 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410227 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410228 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410229 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410230 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410231 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410232 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410233 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410234 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410235 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410236 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410237 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410238 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410239 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410240 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410241 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410242 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410243 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410244 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410245 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410246 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410247 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410248 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410249 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410250 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410251 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410252 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410253 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410254 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410255 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410256 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410257 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410258 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410259 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410260 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410261 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410262 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410263 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410264 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410265 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410266 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410267 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410268 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410269 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410270 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410271 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410272 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410273 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410274 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410275 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410276 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410277 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410278 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410279 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410280 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410281 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410282 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410283 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410284 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410285 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410286 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410287 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410288 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410289 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410290 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410291 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410292 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410293 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410294 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410295 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410296 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410297 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410298 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410299 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410300 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410301 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410302 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410303 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410304 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410305 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410306 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410307 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410308 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410309 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410310 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410311 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410312 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410313 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410314 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410315 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410316 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410317 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410318 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410319 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410320 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410321 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410322 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410323 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410324 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410325 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410326 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410327 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410328 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410329 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410330 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410331 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410332 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410333 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410334 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410335 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410336 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410337 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410338 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410339 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410340 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410341 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410342 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410343 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410344 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410345 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410346 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410347 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410348 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410349 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410350 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410351 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410352 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410353 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410354 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410355 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410356 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410357 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410358 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410359 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410360 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410361 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410362 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410363 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410364 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410365 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410366 create mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410367 create mode 100644 exp/lighthorizon/index/cmd/testdata/regenerate.sh create mode 100644 exp/lighthorizon/index/connect.go create mode 100644 exp/lighthorizon/index/mock_store.go create mode 100644 exp/lighthorizon/index/modules.go create mode 100644 exp/lighthorizon/index/store.go create mode 100644 exp/lighthorizon/index/types/bitmap.go create mode 100644 exp/lighthorizon/index/types/bitmap_test.go create mode 100644 exp/lighthorizon/index/types/trie.go create mode 100644 exp/lighthorizon/index/types/trie_test.go create mode 100644 exp/lighthorizon/ingester/ingester.go create mode 100644 exp/lighthorizon/ingester/main.go create mode 100644 exp/lighthorizon/ingester/mock_ingester.go create mode 100644 exp/lighthorizon/ingester/parallel_ingester.go create mode 100644 exp/lighthorizon/ingester/participants.go create mode 100644 exp/lighthorizon/main.go create mode 100644 exp/lighthorizon/services/cursor.go create mode 100644 exp/lighthorizon/services/cursor_test.go create mode 100644 exp/lighthorizon/services/main.go create mode 100644 exp/lighthorizon/services/main_test.go create mode 100644 exp/lighthorizon/services/mock_services.go create mode 100644 exp/lighthorizon/services/operations.go create mode 100644 exp/lighthorizon/services/transactions.go create mode 100644 exp/lighthorizon/tools/cache.go create mode 100644 exp/lighthorizon/tools/index.go create mode 100644 exp/lighthorizon/tools/index_test.go create mode 100644 exp/services/ledgerexporter/main.go create mode 100644 ingest/ledgerbackend/history_archive_backend.go create mode 100644 metaarchive/main.go create mode 100644 support/collections/maps/map.go create mode 100644 support/collections/maps/map_test.go create mode 100644 support/collections/set/iset.go create mode 100644 support/collections/set/safeset.go rename services/horizon/internal/httpx/middleware_test.go => support/http/sanitize_route_test.go (90%) rename historyarchive/fs_archive.go => support/storage/filesystem.go (76%) create mode 100644 support/storage/gcs.go rename historyarchive/http_archive.go => support/storage/http.go (66%) create mode 100644 support/storage/main.go create mode 100644 support/storage/ondisk_cache.go rename historyarchive/s3_archive.go => support/storage/s3.go (68%) create mode 100644 support/storage/s3_test.go create mode 100644 xdr/Stellar-lighthorizon.x diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index 0ea0686904..598da76bca 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -158,3 +158,50 @@ jobs: - if: github.ref == 'refs/heads/master' name: Push to DockerHub run: docker push stellar/horizon-verify-range:latest + + ledger-exporter: + name: Test and push the Ledger Exporter images + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + # For pull requests, build and test the PR head not a merge of the PR with the destination. + ref: ${{ github.event.pull_request.head.sha || github.ref }} + - name: Build and test Ledger Exporter images + # Any range should do for basic testing, this range was chosen pretty early in history so that it only takes a few mins to run + run: | + chmod 755 ./exp/lighthorizon/build/build.sh + mkdir $PWD/ledgerexport + # mkdir $PWD/index + + ./exp/lighthorizon/build/build.sh ledgerexporter stellar latest false + docker run -e ARCHIVE_TARGET=file:///ledgerexport\ + -e START=5\ + -e END=150\ + -e NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015"\ + -e CAPTIVE_CORE_CONFIG="/captive-core-pubnet.cfg"\ + -e HISTORY_ARCHIVE_URLS="https://history.stellar.org/prd/core-live/core_live_001"\ + -v $PWD/ledgerexport:/ledgerexport\ + stellar/lighthorizon-ledgerexporter + + # # run map job + # docker run -e NETWORK_PASSPHRASE='pubnet' -e JOB_INDEX_ENV=AWS_BATCH_JOB_ARRAY_INDEX -e AWS_BATCH_JOB_ARRAY_INDEX=0 -e BATCH_SIZE=64 -e FIRST_CHECKPOINT=64 \ + # -e WORKER_COUNT=1 -e RUN_MODE=map -v $PWD/ledgerexport:/ledgermeta -e TXMETA_SOURCE=file:///ledgermeta -v $PWD/index:/index -e INDEX_TARGET=file:///index stellar/lighthorizon-index-batch + + # # run reduce job + # docker run -e NETWORK_PASSPHRASE='pubnet' -e JOB_INDEX_ENV=AWS_BATCH_JOB_ARRAY_INDEX -e AWS_BATCH_JOB_ARRAY_INDEX=0 -e MAP_JOB_COUNT=1 -e REDUCE_JOB_COUNT=1 \ + # -e WORKER_COUNT=1 -e RUN_MODE=reduce -v $PWD/index:/index -e INDEX_SOURCE_ROOT=file:///index -e INDEX_TARGET=file:///index stellar/lighthorizon-index-batch + + # Push images + - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/lighthorizon' + name: Login to DockerHub + uses: docker/login-action@bb984efc561711aaa26e433c32c3521176eae55b + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/lighthorizon' + name: Push to DockerHub + run: | + chmod 755 ./exp/lighthorizon/build/build.sh + ./exp/lighthorizon/build/build.sh ledgerexporter stellar latest true diff --git a/Makefile b/Makefile index 08b25c5665..e07da4d9b8 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,8 @@ xdr/Stellar-contract-meta.x \ xdr/Stellar-contract-spec.x \ xdr/Stellar-contract.x \ xdr/Stellar-internal.x \ -xdr/Stellar-contract-config-setting.x +xdr/Stellar-contract-config-setting.x \ +xdr/Stellar-lighthorizon.x XDRGEN_COMMIT=e2cac557162d99b12ae73b846cf3d5bfe16636de XDR_COMMIT=bb54e505f814386a3f45172e0b7e95b7badbe969 diff --git a/clients/horizonclient/CHANGELOG.md b/clients/horizonclient/CHANGELOG.md index aecb94bb9e..bcd3ef331d 100644 --- a/clients/horizonclient/CHANGELOG.md +++ b/clients/horizonclient/CHANGELOG.md @@ -16,6 +16,13 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). * The library is updated to align with breaking changes to `txnbuild`. +## [v10.0.0](https://github.com/stellar/go/releases/tag/horizonclient-v10.0.0) - 2022-04-18 + +**This release adds support for Protocol 19:** + +* The library is updated to align with breaking changes to `txnbuild`. + + ## [v9.0.0](https://github.com/stellar/go/releases/tag/horizonclient-v9.0.0) - 2022-01-10 None diff --git a/exp/lighthorizon/actions/accounts.go b/exp/lighthorizon/actions/accounts.go new file mode 100644 index 0000000000..86673afa68 --- /dev/null +++ b/exp/lighthorizon/actions/accounts.go @@ -0,0 +1,142 @@ +package actions + +import ( + "errors" + "net/http" + "os" + "strconv" + + "github.com/stellar/go/support/log" + "github.com/stellar/go/xdr" + + "github.com/stellar/go/exp/lighthorizon/adapters" + "github.com/stellar/go/exp/lighthorizon/services" + hProtocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/render/hal" + supportProblem "github.com/stellar/go/support/render/problem" + "github.com/stellar/go/toid" +) + +const ( + urlAccountId = "account_id" +) + +func accountRequestParams(w http.ResponseWriter, r *http.Request) (string, pagination, error) { + var accountId string + var accountErr bool + + if accountId, accountErr = getURLParam(r, urlAccountId); !accountErr { + return "", pagination{}, errors.New("unable to find account_id in url path") + } + + paginate, err := paging(r) + if err != nil { + return "", pagination{}, err + } + + if paginate.Cursor < 1 { + paginate.Cursor = toid.New(1, 1, 1).ToInt64() + } + + if paginate.Limit == 0 { + paginate.Limit = 10 + } + + return accountId, paginate, nil +} + +func NewTXByAccountHandler(lightHorizon services.LightHorizon) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var accountId string + var paginate pagination + var err error + + if accountId, paginate, err = accountRequestParams(w, r); err != nil { + errorMsg := supportProblem.MakeInvalidFieldProblem("account_id", err) + sendErrorResponse(r.Context(), w, *errorMsg) + return + } + + page := hal.Page{ + Cursor: strconv.FormatInt(paginate.Cursor, 10), + Order: string(paginate.Order), + Limit: uint64(paginate.Limit), + } + page.Init() + page.FullURL = r.URL + + txns, err := lightHorizon.Transactions.GetTransactionsByAccount(ctx, paginate.Cursor, paginate.Limit, accountId) + if err != nil { + log.Error(err) + if os.IsNotExist(err) { + sendErrorResponse(r.Context(), w, supportProblem.NotFound) + } else if err != nil { + sendErrorResponse(r.Context(), w, supportProblem.ServerError) + } + return + } + + encoder := xdr.NewEncodingBuffer() + for _, txn := range txns { + var response hProtocol.Transaction + response, err = adapters.PopulateTransaction(r.URL, &txn, encoder) + if err != nil { + log.Error(err) + sendErrorResponse(r.Context(), w, supportProblem.ServerError) + return + } + + page.Add(response) + } + + page.PopulateLinks() + sendPageResponse(r.Context(), w, page) + } +} + +func NewOpsByAccountHandler(lightHorizon services.LightHorizon) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + var accountId string + var paginate pagination + var err error + + if accountId, paginate, err = accountRequestParams(w, r); err != nil { + errorMsg := supportProblem.MakeInvalidFieldProblem("account_id", err) + sendErrorResponse(r.Context(), w, *errorMsg) + return + } + + page := hal.Page{ + Cursor: strconv.FormatInt(paginate.Cursor, 10), + Order: string(paginate.Order), + Limit: uint64(paginate.Limit), + } + page.Init() + page.FullURL = r.URL + + ops, err := lightHorizon.Operations.GetOperationsByAccount(ctx, paginate.Cursor, paginate.Limit, accountId) + if err != nil { + log.Error(err) + sendErrorResponse(r.Context(), w, supportProblem.ServerError) + return + } + + for _, op := range ops { + var response operations.Operation + response, err = adapters.PopulateOperation(r, &op) + if err != nil { + log.Error(err) + sendErrorResponse(r.Context(), w, supportProblem.ServerError) + return + } + + page.Add(response) + } + + page.PopulateLinks() + sendPageResponse(r.Context(), w, page) + } +} diff --git a/exp/lighthorizon/actions/accounts_test.go b/exp/lighthorizon/actions/accounts_test.go new file mode 100644 index 0000000000..40576fb7e4 --- /dev/null +++ b/exp/lighthorizon/actions/accounts_test.go @@ -0,0 +1,191 @@ +package actions + +import ( + "context" + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/go-chi/chi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/exp/lighthorizon/services" + "github.com/stellar/go/support/render/problem" +) + +func setupTest() { + problem.RegisterHost("") +} + +func TestTxByAccountMissingParamError(t *testing.T) { + setupTest() + recorder := httptest.NewRecorder() + request := buildHttpRequest( + t, + map[string]string{}, + map[string]string{}, + ) + + mockOperationService := &services.MockOperationService{} + mockTransactionService := &services.MockTransactionService{} + + lh := services.LightHorizon{ + Operations: mockOperationService, + Transactions: mockTransactionService, + } + + handler := NewTXByAccountHandler(lh) + handler(recorder, request) + + resp := recorder.Result() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + + raw, err := ioutil.ReadAll(resp.Body) + assert.NoError(t, err) + + var problem problem.P + err = json.Unmarshal(raw, &problem) + assert.NoError(t, err) + assert.Equal(t, "Bad Request", problem.Title) + assert.Equal(t, "bad_request", problem.Type) + assert.Equal(t, "account_id", problem.Extras["invalid_field"]) + assert.Equal(t, "The request you sent was invalid in some way.", problem.Detail) + assert.Equal(t, "unable to find account_id in url path", problem.Extras["reason"]) +} + +func TestTxByAccountServerError(t *testing.T) { + setupTest() + recorder := httptest.NewRecorder() + pathParams := make(map[string]string) + pathParams["account_id"] = "G1234" + request := buildHttpRequest( + t, + map[string]string{}, + pathParams, + ) + + mockOperationService := &services.MockOperationService{} + mockTransactionService := &services.MockTransactionService{} + mockTransactionService.On("GetTransactionsByAccount", mock.Anything, mock.Anything, mock.Anything, "G1234").Return([]common.Transaction{}, errors.New("not good")) + + lh := services.LightHorizon{ + Operations: mockOperationService, + Transactions: mockTransactionService, + } + + handler := NewTXByAccountHandler(lh) + handler(recorder, request) + + resp := recorder.Result() + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + + raw, err := ioutil.ReadAll(resp.Body) + assert.NoError(t, err) + + var problem problem.P + err = json.Unmarshal(raw, &problem) + assert.NoError(t, err) + assert.Equal(t, "Internal Server Error", problem.Title) + assert.Equal(t, "server_error", problem.Type) +} + +func TestOpsByAccountMissingParamError(t *testing.T) { + setupTest() + recorder := httptest.NewRecorder() + request := buildHttpRequest( + t, + map[string]string{}, + map[string]string{}, + ) + + mockOperationService := &services.MockOperationService{} + mockTransactionService := &services.MockTransactionService{} + + lh := services.LightHorizon{ + Operations: mockOperationService, + Transactions: mockTransactionService, + } + + handler := NewOpsByAccountHandler(lh) + handler(recorder, request) + + resp := recorder.Result() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + + raw, err := ioutil.ReadAll(resp.Body) + assert.NoError(t, err) + + var problem problem.P + err = json.Unmarshal(raw, &problem) + assert.NoError(t, err) + assert.Equal(t, "Bad Request", problem.Title) + assert.Equal(t, "bad_request", problem.Type) + assert.Equal(t, "account_id", problem.Extras["invalid_field"]) + assert.Equal(t, "The request you sent was invalid in some way.", problem.Detail) + assert.Equal(t, "unable to find account_id in url path", problem.Extras["reason"]) +} + +func TestOpsByAccountServerError(t *testing.T) { + setupTest() + recorder := httptest.NewRecorder() + pathParams := make(map[string]string) + pathParams["account_id"] = "G1234" + request := buildHttpRequest( + t, + map[string]string{}, + pathParams, + ) + + mockOperationService := &services.MockOperationService{} + mockTransactionService := &services.MockTransactionService{} + mockOperationService.On("GetOperationsByAccount", mock.Anything, mock.Anything, mock.Anything, "G1234").Return([]common.Operation{}, errors.New("not good")) + + lh := services.LightHorizon{ + Operations: mockOperationService, + Transactions: mockTransactionService, + } + + handler := NewOpsByAccountHandler(lh) + handler(recorder, request) + + resp := recorder.Result() + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + + raw, err := ioutil.ReadAll(resp.Body) + assert.NoError(t, err) + + var problem problem.P + err = json.Unmarshal(raw, &problem) + assert.NoError(t, err) + assert.Equal(t, "Internal Server Error", problem.Title) + assert.Equal(t, "server_error", problem.Type) +} + +func buildHttpRequest( + t *testing.T, + queryParams map[string]string, + routeParams map[string]string, +) *http.Request { + request, err := http.NewRequest("GET", "/", nil) + require.NoError(t, err) + + query := url.Values{} + for key, value := range queryParams { + query.Set(key, value) + } + request.URL.RawQuery = query.Encode() + + chiRouteContext := chi.NewRouteContext() + for key, value := range routeParams { + chiRouteContext.URLParams.Add(key, value) + } + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiRouteContext) + return request.WithContext(ctx) +} diff --git a/exp/lighthorizon/actions/apidocs.go b/exp/lighthorizon/actions/apidocs.go new file mode 100644 index 0000000000..713c4054fa --- /dev/null +++ b/exp/lighthorizon/actions/apidocs.go @@ -0,0 +1,26 @@ +package actions + +import ( + supportProblem "github.com/stellar/go/support/render/problem" + "net/http" +) + +func ApiDocs() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + r.URL.Scheme = "http" + r.URL.Host = "localhost:8080" + + if r.Method != "GET" { + sendErrorResponse(r.Context(), w, supportProblem.BadRequest) + return + } + + p, err := staticFiles.ReadFile("static/api_docs.yml") + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "application/openapi+yaml") + w.Write(p) + } +} diff --git a/exp/lighthorizon/actions/main.go b/exp/lighthorizon/actions/main.go new file mode 100644 index 0000000000..01769682b5 --- /dev/null +++ b/exp/lighthorizon/actions/main.go @@ -0,0 +1,124 @@ +package actions + +import ( + "context" + "embed" + "encoding/json" + "net/http" + "net/url" + "strconv" + + "github.com/go-chi/chi" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/stellar/go/support/log" + "github.com/stellar/go/support/render/hal" + supportProblem "github.com/stellar/go/support/render/problem" +) + +var ( + //go:embed static + staticFiles embed.FS + //lint:ignore U1000 temporary + requestCount = promauto.NewCounter(prometheus.CounterOpts{ + Name: "horizon_lite_request_count", + Help: "How many requests have occurred?", + }) + //lint:ignore U1000 temporary + requestTime = promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "horizon_lite_request_duration", + Help: "How long do requests take?", + Buckets: append( + prometheus.LinearBuckets(0, 50, 20), + prometheus.LinearBuckets(1000, 1000, 8)..., + ), + }) +) + +type order string + +const ( + orderAsc order = "asc" + orderDesc order = "desc" +) + +type pagination struct { + Limit uint64 + Cursor int64 + Order order +} + +func sendPageResponse(ctx context.Context, w http.ResponseWriter, page hal.Page) { + w.Header().Set("Content-Type", "application/hal+json; charset=utf-8") + encoder := json.NewEncoder(w) + encoder.SetIndent("", " ") + err := encoder.Encode(page) + if err != nil { + log.Error(err) + sendErrorResponse(ctx, w, supportProblem.ServerError) + } +} + +func sendErrorResponse(ctx context.Context, w http.ResponseWriter, problem supportProblem.P) { + supportProblem.Render(ctx, w, problem) +} + +func requestUnaryParam(r *http.Request, paramName string) (string, error) { + query, err := url.ParseQuery(r.URL.RawQuery) + if err != nil { + return "", err + } + return query.Get(paramName), nil +} + +func paging(r *http.Request) (pagination, error) { + paginate := pagination{ + Order: orderAsc, + } + + if cursorRequested, err := requestUnaryParam(r, "cursor"); err != nil { + return pagination{}, err + } else if cursorRequested != "" { + paginate.Cursor, err = strconv.ParseInt(cursorRequested, 10, 64) + if err != nil { + return pagination{}, err + } + } + + if limitRequested, err := requestUnaryParam(r, "limit"); err != nil { + return pagination{}, err + } else if limitRequested != "" { + paginate.Limit, err = strconv.ParseUint(limitRequested, 10, 64) + if err != nil { + return pagination{}, err + } + } + + if orderRequested, err := requestUnaryParam(r, "order"); err != nil { + return pagination{}, err + } else if orderRequested != "" && orderRequested == string(orderDesc) { + paginate.Order = orderDesc + } + + return paginate, nil +} + +func getURLParam(r *http.Request, key string) (string, bool) { + rctx := chi.RouteContext(r.Context()) + + if rctx == nil { + return "", false + } + + if len(rctx.URLParams.Keys) != len(rctx.URLParams.Values) { + return "", false + } + + for k := len(rctx.URLParams.Keys) - 1; k >= 0; k-- { + if rctx.URLParams.Keys[k] == key { + return rctx.URLParams.Values[k], true + } + } + + return "", false +} diff --git a/exp/lighthorizon/actions/problem.go b/exp/lighthorizon/actions/problem.go new file mode 100644 index 0000000000..cd82cfb1e8 --- /dev/null +++ b/exp/lighthorizon/actions/problem.go @@ -0,0 +1,94 @@ +package actions + +import ( + "net/http" + + "github.com/stellar/go/support/render/problem" +) + +// Well-known and reused problems below: +// inspired by similar default established in horizon - services/horizon/internal/render/problem/problem.go +var ( + + // ClientDisconnected, represented by a non-standard HTTP status code of 499, which was introduced by + // nginix.org(https://www.nginx.com/resources/wiki/extending/api/http/) as a way to capture this state. Use it as a shortcut + // in your actions. + ClientDisconnected = problem.P{ + Type: "client_disconnected", + Title: "Client Disconnected", + Status: 499, + Detail: "The client has closed the connection.", + } + + // ServiceUnavailable is a well-known problem type. Use it as a shortcut + // in your actions. + ServiceUnavailable = problem.P{ + Type: "service_unavailable", + Title: "Service Unavailable", + Status: http.StatusServiceUnavailable, + Detail: "The request cannot be serviced at this time.", + } + + // RateLimitExceeded is a well-known problem type. Use it as a shortcut + // in your actions. + RateLimitExceeded = problem.P{ + Type: "rate_limit_exceeded", + Title: "Rate Limit Exceeded", + Status: 429, + Detail: "The rate limit for the requesting IP address is over its alloted " + + "limit. The allowed limit and requests left per time period are " + + "communicated to clients via the http response headers 'X-RateLimit-*' " + + "headers.", + } + + // NotImplemented is a well-known problem type. Use it as a shortcut + // in your actions. + NotImplemented = problem.P{ + Type: "not_implemented", + Title: "Resource Not Yet Implemented", + Status: http.StatusNotFound, + Detail: "While the requested URL is expected to eventually point to a " + + "valid resource, the work to implement the resource has not yet " + + "been completed.", + } + + // NotAcceptable is a well-known problem type. Use it as a shortcut + // in your actions. + NotAcceptable = problem.P{ + Type: "not_acceptable", + Title: "An acceptable response content-type could not be provided for " + + "this request", + Status: http.StatusNotAcceptable, + } + + // ServerOverCapacity is a well-known problem type. Use it as a shortcut + // in your actions. + ServerOverCapacity = problem.P{ + Type: "server_over_capacity", + Title: "Server Over Capacity", + Status: http.StatusServiceUnavailable, + Detail: "This horizon server is currently overloaded. Please wait for " + + "several minutes before trying your request again.", + } + + // Timeout is a well-known problem type. Use it as a shortcut + // in your actions. + Timeout = problem.P{ + Type: "timeout", + Title: "Timeout", + Status: http.StatusGatewayTimeout, + Detail: "Your request timed out before completing. Please try your " + + "request again. If you are submitting a transaction make sure you are " + + "sending exactly the same transaction (with the same sequence number).", + } + + // UnsupportedMediaType is a well-known problem type. Use it as a shortcut + // in your actions. + UnsupportedMediaType = problem.P{ + Type: "unsupported_media_type", + Title: "Unsupported Media Type", + Status: http.StatusUnsupportedMediaType, + Detail: "The request has an unsupported content type. Presently, the " + + "only supported content type is application/x-www-form-urlencoded.", + } +) diff --git a/exp/lighthorizon/actions/root.go b/exp/lighthorizon/actions/root.go new file mode 100644 index 0000000000..3dfa4341a0 --- /dev/null +++ b/exp/lighthorizon/actions/root.go @@ -0,0 +1,29 @@ +package actions + +import ( + "encoding/json" + "net/http" + + "github.com/stellar/go/support/log" + supportProblem "github.com/stellar/go/support/render/problem" +) + +type RootResponse struct { + Version string `json:"version"` + LedgerSource string `json:"ledger_source"` + IndexSource string `json:"index_source"` + LatestLedger uint32 `json:"latest_indexed_ledger"` +} + +func Root(config RootResponse) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/hal+json; charset=utf-8") + encoder := json.NewEncoder(w) + encoder.SetIndent("", " ") + err := encoder.Encode(config) + if err != nil { + log.Error(err) + sendErrorResponse(r.Context(), w, supportProblem.ServerError) + } + } +} diff --git a/exp/lighthorizon/actions/static/api_docs.yml b/exp/lighthorizon/actions/static/api_docs.yml new file mode 100644 index 0000000000..281cf2b605 --- /dev/null +++ b/exp/lighthorizon/actions/static/api_docs.yml @@ -0,0 +1,228 @@ +openapi: 3.1.0 +info: + title: Horizon Lite API + version: 0.0.1 + description: |- + The Horizon Lite API is a published web service on port 8080. It's considered + extremely experimental and only provides a minimal subset of endpoints. +servers: + - url: http://localhost:8080/ +paths: + /accounts/{account_id}/operations: + get: + operationId: GetOperationsByAccountId + parameters: + - $ref: '#/components/parameters/CursorParam' + - $ref: '#/components/parameters/LimitParam' + - $ref: '#/components/parameters/AccountIDParam' + responses: + '200': + description: OK + headers: {} + content: + application/json: + schema: + $ref: '#/components/schemas/CollectionModel_Operation' + example: + _links: + self: + href: http://localhost:8080/accounts/GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ/operations?cursor=6606617478959105&limit=1&order=asc + next: + href: http://localhost:8080/accounts/GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ/operations?cursor=6606621773926401&limit=1&order=asc + prev: + href: http://localhost:8080/accounts/GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ/operations?cursor=6606621773926401&limit=1&order=desc + _embedded: + records: + - _links: + self: + href: http://localhost:8080/operations/6606621773926401 + id: '6606621773926401' + paging_token: '6606621773926401' + transaction_successful: true + source_account: GBGTCH47BOEEKLPHHMR2GOK6KQFGL3O7Q53FIZTJ7S7YEDWYJ5IUDJDJ + type: manage_sell_offer + type_i: 3 + created_at: '2022-06-17T23:29:42Z' + transaction_hash: 544469b76cd90978345a4734a0ce69a9d0ddb4a6595a7afc503225a77826722a + amount: '0.0000000' + price: '0.0000001' + price_r: + n: 1 + d: 10000000 + buying_asset_type: credit_alphanum4 + buying_asset_code: USDV + buying_asset_issuer: GAXXMQMTDUQ4YEPXJMKFBGN3GETPJNEXEUHFCQJKGJDVI3XQCNBU3OZI + selling_asset_type: credit_alphanum4 + selling_asset_code: EURV + selling_asset_issuer: GAXXMQMTDUQ4YEPXJMKFBGN3GETPJNEXEUHFCQJKGJDVI3XQCNBU3OZI + offer_id: '425531' + summary: Get Operations by Account ID and Paged list + description: Get Operations by Account ID and Paged list + tags: [] + /accounts/{account_id}/transactions: + get: + operationId: GetTransactionsByAccountId + parameters: + - $ref: '#/components/parameters/CursorParam' + - $ref: '#/components/parameters/LimitParam' + - $ref: '#/components/parameters/AccountIDParam' + responses: + '200': + description: OK + headers: {} + content: + application/json: + schema: + $ref: '#/components/schemas/CollectionModel_Tx' + example: + _links: + self: + href: http://localhost:8080/accounts/GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ/transactions?cursor=&limit=0&order= + next: + href: http://localhost:8080/accounts/GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ/transactions?cursor=6606621773930497&limit=0&order= + prev: + href: http://localhost:8080/accounts/GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ/transactions?cursor=6606621773930497&limit=0&order=asc + _embedded: + records: + - memo: xdr.MemoText("psp:1405") + _links: + self: + href: http://localhost:8080/transactions/5fef21d5ef75ecf18d65a160cfab17dca8dbf6dbc4e2fd66a510719ad8dddb09 + id: 5fef21d5ef75ecf18d65a160cfab17dca8dbf6dbc4e2fd66a510719ad8dddb09 + paging_token: '6606621773930497' + successful: false + hash: 5fef21d5ef75ecf18d65a160cfab17dca8dbf6dbc4e2fd66a510719ad8dddb09 + ledger: 1538224 + created_at: '2022-06-17T23:29:42Z' + source_account: GCFJN22UG6IZHXKDVAJWAVEQ3NERGCRCURR2FHARNRBNLYFEQZGML4PW + source_account_sequence: '' + fee_account: '' + fee_charged: '3000' + max_fee: '0' + operation_count: 1 + envelope_xdr: AAAAAgAAAACKlutUN5GT3UOoE2BUkNtJEwoipGOinBFsQtXgpIZMxQAAJxAAE05oAAHUKAAAAAEAAAAAAAAAAAAAAABirQ6AAAAAAQAAAAhwc3A6MTQwNQAAAAEAAAAAAAAAAQAAAADpPdN37FA9KVcJfmMBuD8pPcaT5jqlrMeYEOTP36Zo2AAAAAJBVE1ZUgAAAAAAAAAAAAAAZ8rWY3iaDnWNtfpvLpNaCEbKdDjrd2gQODOuKpmj1vMAAAAAGHAagAAAAAAAAAABpIZMxQAAAEDNJwYToiBR6bzElRL4ORJdXXZYO9cE3-ishQLC_fWGrPGhWrW7_UkPJWvxWdQDJBjVOHuA1Jjc94NSe91hSwEL + result_xdr: AAAAAAAAC7j_____AAAAAQAAAAAAAAAB____-gAAAAA= + result_meta_xdr: '' + fee_meta_xdr: '' + memo_type: MemoTypeMemoText + signatures: + - pIZMxQAAAEDNJwYToiBR6bzElRL4ORJdXXZYO9cE3-ishQLC_fWGrPGhWrW7_UkPJWvxWdQDJBjVOHuA1Jjc94NSe91hSwEL + summary: Get Transactions by Account ID and Paged list + description: Get Transactions by Account ID and Paged list + tags: [] +components: + parameters: + CursorParam: + name: cursor + in: query + required: false + schema: + type: integer + example: 6606617478959105 + description: The packed order id consisting of Ledger Num, TX Order Num, Operation Order Num + LimitParam: + in: query + name: limit + required: false + schema: + type: integer + default: 10 + description: The numbers of items to return + AccountIDParam: + name: account_id + in: path + required: true + description: The strkey encoded Account ID + schema: + type: string + example: GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ + TransactionIDParam: + name: tx_id + in: path + required: true + description: The Transaction hash, it's id. + schema: + type: string + example: a221f4743450736cba4a78940f22b01e1f64568eec8cb04c2ae37874d86cee3d + schemas: + CollectionModelItem: + type: object + properties: + _embedded: + type: object + properties: + records: + type: array + items: + "$ref": "#/components/schemas/Item" + _links: + "$ref": "#/components/schemas/Links" + Item: + type: object + properties: + id: + type: string + _links: + "$ref": "#/components/schemas/Links" + CollectionModel_Tx: + type: object + allOf: + - $ref: "#/components/schemas/CollectionModelItem" + properties: + _embedded: + type: object + properties: + records: + type: array + items: + $ref: "#/components/schemas/EntityModel_Tx" + EntityModel_Tx: + type: object + allOf: + - $ref: "#/components/schemas/Tx" + - $ref: "#/components/schemas/Links" + Tx: + type: object + properties: + id: + type: string + hash: + type: string + ledger: + type: integer + CollectionModel_Operation: + type: object + allOf: + - $ref: "#/components/schemas/CollectionModelItem" + properties: + _embedded: + type: object + properties: + records: + type: array + items: + $ref: "#/components/schemas/EntityModel_Operation" + EntityModel_Operation: + type: object + allOf: + - $ref: "#/components/schemas/Operation" + - $ref: "#/components/schemas/Links" + Operation: + type: object + properties: + id: + type: string + type: + type: string + source_account: + type: string + Links: + type: object + additionalProperties: + "$ref": "#/components/schemas/Link" + Link: + type: object + properties: + href: + type: string +tags: [] diff --git a/exp/lighthorizon/adapters/account_merge.go b/exp/lighthorizon/adapters/account_merge.go new file mode 100644 index 0000000000..1fa6934638 --- /dev/null +++ b/exp/lighthorizon/adapters/account_merge.go @@ -0,0 +1,21 @@ +package adapters + +import ( + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/operations" +) + +func populateAccountMergeOperation(op *common.Operation, baseOp operations.Base) (operations.AccountMerge, error) { + destination := op.Get().Body.MustDestination() + + return operations.AccountMerge{ + Base: baseOp, + Account: op.SourceAccount().Address(), + Into: destination.Address(), + // TODO: + AccountMuxed: "", + AccountMuxedID: 0, + IntoMuxed: "", + IntoMuxedID: 0, + }, nil +} diff --git a/exp/lighthorizon/adapters/allow_trust.go b/exp/lighthorizon/adapters/allow_trust.go new file mode 100644 index 0000000000..2e3fea2188 --- /dev/null +++ b/exp/lighthorizon/adapters/allow_trust.go @@ -0,0 +1,43 @@ +package adapters + +import ( + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +func populateAllowTrustOperation(op *common.Operation, baseOp operations.Base) (operations.AllowTrust, error) { + allowTrust := op.Get().Body.MustAllowTrustOp() + + var ( + assetType string + code string + issuer string + ) + + err := allowTrust.Asset.ToAsset(op.SourceAccount()).Extract(&assetType, &code, &issuer) + if err != nil { + return operations.AllowTrust{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + flags := xdr.TrustLineFlags(allowTrust.Authorize) + + return operations.AllowTrust{ + Base: baseOp, + Asset: base.Asset{ + Type: assetType, + Code: code, + Issuer: issuer, + }, + + Trustee: op.SourceAccount().Address(), + Trustor: allowTrust.Trustor.Address(), + Authorize: flags.IsAuthorized(), + AuthorizeToMaintainLiabilities: flags.IsAuthorizedToMaintainLiabilitiesFlag(), + // TODO: + TrusteeMuxed: "", + TrusteeMuxedID: 0, + }, nil +} diff --git a/exp/lighthorizon/adapters/begin_sponsoring_future_reserves.go b/exp/lighthorizon/adapters/begin_sponsoring_future_reserves.go new file mode 100644 index 0000000000..a5fe86a3ce --- /dev/null +++ b/exp/lighthorizon/adapters/begin_sponsoring_future_reserves.go @@ -0,0 +1,15 @@ +package adapters + +import ( + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/operations" +) + +func populateBeginSponsoringFutureReservesOperation(op *common.Operation, baseOp operations.Base) (operations.BeginSponsoringFutureReserves, error) { + beginSponsoringFutureReserves := op.Get().Body.MustBeginSponsoringFutureReservesOp() + + return operations.BeginSponsoringFutureReserves{ + Base: baseOp, + SponsoredID: beginSponsoringFutureReserves.SponsoredId.Address(), + }, nil +} diff --git a/exp/lighthorizon/adapters/bump_sequence.go b/exp/lighthorizon/adapters/bump_sequence.go new file mode 100644 index 0000000000..53fe0125a2 --- /dev/null +++ b/exp/lighthorizon/adapters/bump_sequence.go @@ -0,0 +1,17 @@ +package adapters + +import ( + "strconv" + + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/operations" +) + +func populateBumpSequenceOperation(op *common.Operation, baseOp operations.Base) (operations.BumpSequence, error) { + bumpSequence := op.Get().Body.MustBumpSequenceOp() + + return operations.BumpSequence{ + Base: baseOp, + BumpTo: strconv.FormatInt(int64(bumpSequence.BumpTo), 10), + }, nil +} diff --git a/exp/lighthorizon/adapters/change_trust.go b/exp/lighthorizon/adapters/change_trust.go new file mode 100644 index 0000000000..e06dbcfb39 --- /dev/null +++ b/exp/lighthorizon/adapters/change_trust.go @@ -0,0 +1,63 @@ +package adapters + +import ( + "github.com/stellar/go/amount" + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +func populateChangeTrustOperation(op *common.Operation, baseOp operations.Base) (operations.ChangeTrust, error) { + changeTrust := op.Get().Body.MustChangeTrustOp() + + var ( + assetType string + code string + issuer string + + liquidityPoolID string + ) + + switch changeTrust.Line.Type { + case xdr.AssetTypeAssetTypeCreditAlphanum4, xdr.AssetTypeAssetTypeCreditAlphanum12: + err := changeTrust.Line.ToAsset().Extract(&assetType, &code, &issuer) + if err != nil { + return operations.ChangeTrust{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + case xdr.AssetTypeAssetTypePoolShare: + assetType = "liquidity_pool_shares" + + if changeTrust.Line.LiquidityPool.Type != xdr.LiquidityPoolTypeLiquidityPoolConstantProduct { + return operations.ChangeTrust{}, errors.Errorf("unkown liquidity pool type %d", changeTrust.Line.LiquidityPool.Type) + } + + cp := changeTrust.Line.LiquidityPool.ConstantProduct + poolID, err := xdr.NewPoolId(cp.AssetA, cp.AssetB, cp.Fee) + if err != nil { + return operations.ChangeTrust{}, errors.Wrap(err, "error generating pool id") + } + liquidityPoolID = xdr.Hash(poolID).HexString() + default: + return operations.ChangeTrust{}, errors.Errorf("unknown asset type %d", changeTrust.Line.Type) + } + + return operations.ChangeTrust{ + Base: baseOp, + LiquidityPoolOrAsset: base.LiquidityPoolOrAsset{ + Asset: base.Asset{ + Type: assetType, + Code: code, + Issuer: issuer, + }, + LiquidityPoolID: liquidityPoolID, + }, + Limit: amount.String(changeTrust.Limit), + Trustee: issuer, + Trustor: op.SourceAccount().Address(), + // TODO: + TrustorMuxed: "", + TrustorMuxedID: 0, + }, nil +} diff --git a/exp/lighthorizon/adapters/claim_claimable_balance.go b/exp/lighthorizon/adapters/claim_claimable_balance.go new file mode 100644 index 0000000000..7dffe49d13 --- /dev/null +++ b/exp/lighthorizon/adapters/claim_claimable_balance.go @@ -0,0 +1,26 @@ +package adapters + +import ( + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +func populateClaimClaimableBalanceOperation(op *common.Operation, baseOp operations.Base) (operations.ClaimClaimableBalance, error) { + claimClaimableBalance := op.Get().Body.MustClaimClaimableBalanceOp() + + balanceID, err := xdr.MarshalHex(claimClaimableBalance.BalanceId) + if err != nil { + return operations.ClaimClaimableBalance{}, errors.New("invalid balanceId") + } + + return operations.ClaimClaimableBalance{ + Base: baseOp, + BalanceID: balanceID, + Claimant: op.SourceAccount().Address(), + // TODO + ClaimantMuxed: "", + ClaimantMuxedID: 0, + }, nil +} diff --git a/exp/lighthorizon/adapters/clawback.go b/exp/lighthorizon/adapters/clawback.go new file mode 100644 index 0000000000..32f6ed7401 --- /dev/null +++ b/exp/lighthorizon/adapters/clawback.go @@ -0,0 +1,37 @@ +package adapters + +import ( + "github.com/stellar/go/amount" + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/errors" +) + +func populateClawbackOperation(op *common.Operation, baseOp operations.Base) (operations.Clawback, error) { + clawback := op.Get().Body.MustClawbackOp() + + var ( + assetType string + code string + issuer string + ) + err := clawback.Asset.Extract(&assetType, &code, &issuer) + if err != nil { + return operations.Clawback{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + return operations.Clawback{ + Base: baseOp, + Asset: base.Asset{ + Type: assetType, + Code: code, + Issuer: issuer, + }, + Amount: amount.String(clawback.Amount), + From: clawback.From.Address(), + // TODO: + FromMuxed: "", + FromMuxedID: 0, + }, nil +} diff --git a/exp/lighthorizon/adapters/clawback_claimable_balance.go b/exp/lighthorizon/adapters/clawback_claimable_balance.go new file mode 100644 index 0000000000..a24d4828b0 --- /dev/null +++ b/exp/lighthorizon/adapters/clawback_claimable_balance.go @@ -0,0 +1,22 @@ +package adapters + +import ( + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +func populateClawbackClaimableBalanceOperation(op *common.Operation, baseOp operations.Base) (operations.ClawbackClaimableBalance, error) { + clawbackClaimableBalance := op.Get().Body.MustClawbackClaimableBalanceOp() + + balanceID, err := xdr.MarshalHex(clawbackClaimableBalance.BalanceId) + if err != nil { + return operations.ClawbackClaimableBalance{}, errors.Wrap(err, "invalid balanceId") + } + + return operations.ClawbackClaimableBalance{ + Base: baseOp, + BalanceID: balanceID, + }, nil +} diff --git a/exp/lighthorizon/adapters/create_account.go b/exp/lighthorizon/adapters/create_account.go new file mode 100644 index 0000000000..d9a7c678a1 --- /dev/null +++ b/exp/lighthorizon/adapters/create_account.go @@ -0,0 +1,18 @@ +package adapters + +import ( + "github.com/stellar/go/amount" + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/operations" +) + +func populateCreateAccountOperation(op *common.Operation, baseOp operations.Base) (operations.CreateAccount, error) { + createAccount := op.Get().Body.MustCreateAccountOp() + + return operations.CreateAccount{ + Base: baseOp, + StartingBalance: amount.String(createAccount.StartingBalance), + Funder: op.SourceAccount().Address(), + Account: createAccount.Destination.Address(), + }, nil +} diff --git a/exp/lighthorizon/adapters/create_claimable_balance.go b/exp/lighthorizon/adapters/create_claimable_balance.go new file mode 100644 index 0000000000..472e43b30c --- /dev/null +++ b/exp/lighthorizon/adapters/create_claimable_balance.go @@ -0,0 +1,27 @@ +package adapters + +import ( + "github.com/stellar/go/amount" + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/protocols/horizon/operations" +) + +func populateCreateClaimableBalanceOperation(op *common.Operation, baseOp operations.Base) (operations.CreateClaimableBalance, error) { + createClaimableBalance := op.Get().Body.MustCreateClaimableBalanceOp() + + claimants := make([]horizon.Claimant, len(createClaimableBalance.Claimants)) + for i, claimant := range createClaimableBalance.Claimants { + claimants[i] = horizon.Claimant{ + Destination: claimant.MustV0().Destination.Address(), + Predicate: claimant.MustV0().Predicate, + } + } + + return operations.CreateClaimableBalance{ + Base: baseOp, + Asset: createClaimableBalance.Asset.StringCanonical(), + Amount: amount.String(createClaimableBalance.Amount), + Claimants: claimants, + }, nil +} diff --git a/exp/lighthorizon/adapters/create_passive_sell_offer.go b/exp/lighthorizon/adapters/create_passive_sell_offer.go new file mode 100644 index 0000000000..89b2b29e97 --- /dev/null +++ b/exp/lighthorizon/adapters/create_passive_sell_offer.go @@ -0,0 +1,51 @@ +package adapters + +import ( + "github.com/stellar/go/amount" + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/errors" +) + +func populateCreatePassiveSellOfferOperation(op *common.Operation, baseOp operations.Base) (operations.CreatePassiveSellOffer, error) { + createPassiveSellOffer := op.Get().Body.MustCreatePassiveSellOfferOp() + + var ( + buyingAssetType string + buyingCode string + buyingIssuer string + ) + err := createPassiveSellOffer.Buying.Extract(&buyingAssetType, &buyingCode, &buyingIssuer) + if err != nil { + return operations.CreatePassiveSellOffer{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + var ( + sellingAssetType string + sellingCode string + sellingIssuer string + ) + err = createPassiveSellOffer.Selling.Extract(&sellingAssetType, &sellingCode, &sellingIssuer) + if err != nil { + return operations.CreatePassiveSellOffer{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + return operations.CreatePassiveSellOffer{ + Offer: operations.Offer{ + Base: baseOp, + Amount: amount.String(createPassiveSellOffer.Amount), + Price: createPassiveSellOffer.Price.String(), + PriceR: base.Price{ + N: int32(createPassiveSellOffer.Price.N), + D: int32(createPassiveSellOffer.Price.D), + }, + BuyingAssetType: buyingAssetType, + BuyingAssetCode: buyingCode, + BuyingAssetIssuer: buyingIssuer, + SellingAssetType: sellingAssetType, + SellingAssetCode: sellingCode, + SellingAssetIssuer: sellingIssuer, + }, + }, nil +} diff --git a/exp/lighthorizon/adapters/end_sponsoring_future_reserves.go b/exp/lighthorizon/adapters/end_sponsoring_future_reserves.go new file mode 100644 index 0000000000..b6ca7a1742 --- /dev/null +++ b/exp/lighthorizon/adapters/end_sponsoring_future_reserves.go @@ -0,0 +1,38 @@ +package adapters + +import ( + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/operations" +) + +func populateEndSponsoringFutureReservesOperation(op *common.Operation, baseOp operations.Base) (operations.EndSponsoringFutureReserves, error) { + return operations.EndSponsoringFutureReserves{ + Base: baseOp, + BeginSponsor: findInitatingSandwichSponsor(op), + // TODO + BeginSponsorMuxed: "", + BeginSponsorMuxedID: 0, + }, nil +} + +func findInitatingSandwichSponsor(op *common.Operation) string { + if !op.TransactionResult.Successful() { + // Failed transactions may not have a compliant sandwich structure + // we can rely on (e.g. invalid nesting or a being operation with the wrong sponsoree ID) + // and thus we bail out since we could return incorrect information. + return "" + } + sponsoree := op.SourceAccount() + operations := op.TransactionEnvelope.Operations() + for i := int(op.OpIndex) - 1; i >= 0; i-- { + if beginOp, ok := operations[i].Body.GetBeginSponsoringFutureReservesOp(); ok && + beginOp.SponsoredId.Address() == sponsoree.Address() { + if operations[i].SourceAccount != nil { + return operations[i].SourceAccount.Address() + } else { + return op.TransactionEnvelope.SourceAccount().ToAccountId().Address() + } + } + } + return "" +} diff --git a/exp/lighthorizon/adapters/inflation.go b/exp/lighthorizon/adapters/inflation.go new file mode 100644 index 0000000000..57c927263d --- /dev/null +++ b/exp/lighthorizon/adapters/inflation.go @@ -0,0 +1,12 @@ +package adapters + +import ( + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/operations" +) + +func populateInflationOperation(op *common.Operation, baseOp operations.Base) (operations.Inflation, error) { + return operations.Inflation{ + Base: baseOp, + }, nil +} diff --git a/exp/lighthorizon/adapters/liquidity_pool_deposit.go b/exp/lighthorizon/adapters/liquidity_pool_deposit.go new file mode 100644 index 0000000000..f0b4384009 --- /dev/null +++ b/exp/lighthorizon/adapters/liquidity_pool_deposit.go @@ -0,0 +1,33 @@ +package adapters + +import ( + "github.com/stellar/go/amount" + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/xdr" +) + +func populateLiquidityPoolDepositOperation(op *common.Operation, baseOp operations.Base) (operations.LiquidityPoolDeposit, error) { + liquidityPoolDeposit := op.Get().Body.MustLiquidityPoolDepositOp() + + return operations.LiquidityPoolDeposit{ + Base: baseOp, + // TODO: some fields missing because derived from meta + LiquidityPoolID: xdr.Hash(liquidityPoolDeposit.LiquidityPoolId).HexString(), + ReservesMax: []base.AssetAmount{ + {Amount: amount.String(liquidityPoolDeposit.MaxAmountA)}, + {Amount: amount.String(liquidityPoolDeposit.MaxAmountB)}, + }, + MinPrice: liquidityPoolDeposit.MinPrice.String(), + MinPriceR: base.Price{ + N: int32(liquidityPoolDeposit.MinPrice.N), + D: int32(liquidityPoolDeposit.MinPrice.D), + }, + MaxPrice: liquidityPoolDeposit.MaxPrice.String(), + MaxPriceR: base.Price{ + N: int32(liquidityPoolDeposit.MaxPrice.N), + D: int32(liquidityPoolDeposit.MaxPrice.D), + }, + }, nil +} diff --git a/exp/lighthorizon/adapters/liquidity_pool_withdraw.go b/exp/lighthorizon/adapters/liquidity_pool_withdraw.go new file mode 100644 index 0000000000..c618baf2de --- /dev/null +++ b/exp/lighthorizon/adapters/liquidity_pool_withdraw.go @@ -0,0 +1,24 @@ +package adapters + +import ( + "github.com/stellar/go/amount" + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/xdr" +) + +func populateLiquidityPoolWithdrawOperation(op *common.Operation, baseOp operations.Base) (operations.LiquidityPoolWithdraw, error) { + liquidityPoolWithdraw := op.Get().Body.MustLiquidityPoolWithdrawOp() + + return operations.LiquidityPoolWithdraw{ + Base: baseOp, + // TODO: some fields missing because derived from meta + LiquidityPoolID: xdr.Hash(liquidityPoolWithdraw.LiquidityPoolId).HexString(), + ReservesMin: []base.AssetAmount{ + {Amount: amount.String(liquidityPoolWithdraw.MinAmountA)}, + {Amount: amount.String(liquidityPoolWithdraw.MinAmountB)}, + }, + Shares: amount.String(liquidityPoolWithdraw.Amount), + }, nil +} diff --git a/exp/lighthorizon/adapters/manage_buy_offer.go b/exp/lighthorizon/adapters/manage_buy_offer.go new file mode 100644 index 0000000000..ccdd66bc69 --- /dev/null +++ b/exp/lighthorizon/adapters/manage_buy_offer.go @@ -0,0 +1,52 @@ +package adapters + +import ( + "github.com/stellar/go/amount" + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/errors" +) + +func populateManageBuyOfferOperation(op *common.Operation, baseOp operations.Base) (operations.ManageBuyOffer, error) { + manageBuyOffer := op.Get().Body.MustManageBuyOfferOp() + + var ( + buyingAssetType string + buyingCode string + buyingIssuer string + ) + err := manageBuyOffer.Buying.Extract(&buyingAssetType, &buyingCode, &buyingIssuer) + if err != nil { + return operations.ManageBuyOffer{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + var ( + sellingAssetType string + sellingCode string + sellingIssuer string + ) + err = manageBuyOffer.Selling.Extract(&sellingAssetType, &sellingCode, &sellingIssuer) + if err != nil { + return operations.ManageBuyOffer{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + return operations.ManageBuyOffer{ + Offer: operations.Offer{ + Base: baseOp, + Amount: amount.String(manageBuyOffer.BuyAmount), + Price: manageBuyOffer.Price.String(), + PriceR: base.Price{ + N: int32(manageBuyOffer.Price.N), + D: int32(manageBuyOffer.Price.D), + }, + BuyingAssetType: buyingAssetType, + BuyingAssetCode: buyingCode, + BuyingAssetIssuer: buyingIssuer, + SellingAssetType: sellingAssetType, + SellingAssetCode: sellingCode, + SellingAssetIssuer: sellingIssuer, + }, + OfferID: int64(manageBuyOffer.OfferId), + }, nil +} diff --git a/exp/lighthorizon/adapters/manage_data.go b/exp/lighthorizon/adapters/manage_data.go new file mode 100644 index 0000000000..dd66ed2ae4 --- /dev/null +++ b/exp/lighthorizon/adapters/manage_data.go @@ -0,0 +1,23 @@ +package adapters + +import ( + "encoding/base64" + + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/operations" +) + +func populateManageDataOperation(op *common.Operation, baseOp operations.Base) (operations.ManageData, error) { + manageData := op.Get().Body.MustManageDataOp() + + dataValue := "" + if manageData.DataValue != nil { + dataValue = base64.StdEncoding.EncodeToString(*manageData.DataValue) + } + + return operations.ManageData{ + Base: baseOp, + Name: string(manageData.DataName), + Value: dataValue, + }, nil +} diff --git a/exp/lighthorizon/adapters/manage_sell_offer.go b/exp/lighthorizon/adapters/manage_sell_offer.go new file mode 100644 index 0000000000..56893cc1ab --- /dev/null +++ b/exp/lighthorizon/adapters/manage_sell_offer.go @@ -0,0 +1,52 @@ +package adapters + +import ( + "github.com/stellar/go/amount" + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/errors" +) + +func populateManageSellOfferOperation(op *common.Operation, baseOp operations.Base) (operations.ManageSellOffer, error) { + manageSellOffer := op.Get().Body.MustManageSellOfferOp() + + var ( + buyingAssetType string + buyingCode string + buyingIssuer string + ) + err := manageSellOffer.Buying.Extract(&buyingAssetType, &buyingCode, &buyingIssuer) + if err != nil { + return operations.ManageSellOffer{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + var ( + sellingAssetType string + sellingCode string + sellingIssuer string + ) + err = manageSellOffer.Selling.Extract(&sellingAssetType, &sellingCode, &sellingIssuer) + if err != nil { + return operations.ManageSellOffer{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + return operations.ManageSellOffer{ + Offer: operations.Offer{ + Base: baseOp, + Amount: amount.String(manageSellOffer.Amount), + Price: manageSellOffer.Price.String(), + PriceR: base.Price{ + N: int32(manageSellOffer.Price.N), + D: int32(manageSellOffer.Price.D), + }, + BuyingAssetType: buyingAssetType, + BuyingAssetCode: buyingCode, + BuyingAssetIssuer: buyingIssuer, + SellingAssetType: sellingAssetType, + SellingAssetCode: sellingCode, + SellingAssetIssuer: sellingIssuer, + }, + OfferID: int64(manageSellOffer.OfferId), + }, nil +} diff --git a/exp/lighthorizon/adapters/operation.go b/exp/lighthorizon/adapters/operation.go new file mode 100644 index 0000000000..a2448c8c58 --- /dev/null +++ b/exp/lighthorizon/adapters/operation.go @@ -0,0 +1,93 @@ +package adapters + +import ( + "fmt" + "net/http" + "strconv" + "time" + + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/render/hal" + "github.com/stellar/go/xdr" +) + +func PopulateOperation(r *http.Request, op *common.Operation) (operations.Operation, error) { + hash, err := op.TransactionHash() + if err != nil { + return nil, err + } + + toid := strconv.FormatInt(op.TOID(), 10) + baseOp := operations.Base{ + ID: toid, + PT: toid, + TransactionSuccessful: op.TransactionResult.Successful(), + SourceAccount: op.SourceAccount().Address(), + LedgerCloseTime: time.Unix(int64(op.LedgerHeader.ScpValue.CloseTime), 0).UTC(), + TransactionHash: hash, + Type: operations.TypeNames[op.Get().Body.Type], + TypeI: int32(op.Get().Body.Type), + } + + lb := hal.LinkBuilder{Base: r.URL} + self := fmt.Sprintf("/operations/%s", toid) + baseOp.Links.Self = lb.Link(self) + baseOp.Links.Succeeds = lb.Linkf("/effects?order=desc&cursor=%s", baseOp.PT) + baseOp.Links.Precedes = lb.Linkf("/effects?order=asc&cursor=%s", baseOp.PT) + baseOp.Links.Transaction = lb.Linkf("/transactions/%s", hash) + baseOp.Links.Effects = lb.Link(self, "effects") + + switch op.Get().Body.Type { + case xdr.OperationTypeCreateAccount: + return populateCreateAccountOperation(op, baseOp) + case xdr.OperationTypePayment: + return populatePaymentOperation(op, baseOp) + case xdr.OperationTypePathPaymentStrictReceive: + return populatePathPaymentStrictReceiveOperation(op, baseOp) + case xdr.OperationTypePathPaymentStrictSend: + return populatePathPaymentStrictSendOperation(op, baseOp) + case xdr.OperationTypeManageBuyOffer: + return populateManageBuyOfferOperation(op, baseOp) + case xdr.OperationTypeManageSellOffer: + return populateManageSellOfferOperation(op, baseOp) + case xdr.OperationTypeCreatePassiveSellOffer: + return populateCreatePassiveSellOfferOperation(op, baseOp) + case xdr.OperationTypeSetOptions: + return populateSetOptionsOperation(op, baseOp) + case xdr.OperationTypeChangeTrust: + return populateChangeTrustOperation(op, baseOp) + case xdr.OperationTypeAllowTrust: + return populateAllowTrustOperation(op, baseOp) + case xdr.OperationTypeAccountMerge: + return populateAccountMergeOperation(op, baseOp) + case xdr.OperationTypeInflation: + return populateInflationOperation(op, baseOp) + case xdr.OperationTypeManageData: + return populateManageDataOperation(op, baseOp) + case xdr.OperationTypeBumpSequence: + return populateBumpSequenceOperation(op, baseOp) + case xdr.OperationTypeCreateClaimableBalance: + return populateCreateClaimableBalanceOperation(op, baseOp) + case xdr.OperationTypeClaimClaimableBalance: + return populateClaimClaimableBalanceOperation(op, baseOp) + case xdr.OperationTypeBeginSponsoringFutureReserves: + return populateBeginSponsoringFutureReservesOperation(op, baseOp) + case xdr.OperationTypeEndSponsoringFutureReserves: + return populateEndSponsoringFutureReservesOperation(op, baseOp) + case xdr.OperationTypeRevokeSponsorship: + return populateRevokeSponsorshipOperation(op, baseOp) + case xdr.OperationTypeClawback: + return populateClawbackOperation(op, baseOp) + case xdr.OperationTypeClawbackClaimableBalance: + return populateClawbackClaimableBalanceOperation(op, baseOp) + case xdr.OperationTypeSetTrustLineFlags: + return populateSetTrustLineFlagsOperation(op, baseOp) + case xdr.OperationTypeLiquidityPoolDeposit: + return populateLiquidityPoolDepositOperation(op, baseOp) + case xdr.OperationTypeLiquidityPoolWithdraw: + return populateLiquidityPoolWithdrawOperation(op, baseOp) + default: + return nil, fmt.Errorf("unknown operation type: %s", op.Get().Body.Type) + } +} diff --git a/exp/lighthorizon/adapters/path_payment_strict_receive.go b/exp/lighthorizon/adapters/path_payment_strict_receive.go new file mode 100644 index 0000000000..eeaabad969 --- /dev/null +++ b/exp/lighthorizon/adapters/path_payment_strict_receive.go @@ -0,0 +1,78 @@ +package adapters + +import ( + "github.com/stellar/go/amount" + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/errors" +) + +func populatePathPaymentStrictReceiveOperation(op *common.Operation, baseOp operations.Base) (operations.PathPayment, error) { + payment := op.Get().Body.MustPathPaymentStrictReceiveOp() + + var ( + sendAssetType string + sendCode string + sendIssuer string + ) + err := payment.SendAsset.Extract(&sendAssetType, &sendCode, &sendIssuer) + if err != nil { + return operations.PathPayment{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + var ( + destAssetType string + destCode string + destIssuer string + ) + err = payment.DestAsset.Extract(&destAssetType, &destCode, &destIssuer) + if err != nil { + return operations.PathPayment{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + sourceAmount := amount.String(0) + if op.TransactionResult.Successful() { + result := op.OperationResult().MustPathPaymentStrictReceiveResult() + sourceAmount = amount.String(result.SendAmount()) + } + + var path = make([]base.Asset, len(payment.Path)) + for i := range payment.Path { + var ( + assetType string + code string + issuer string + ) + err = payment.Path[i].Extract(&assetType, &code, &issuer) + if err != nil { + return operations.PathPayment{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + path[i] = base.Asset{ + Type: assetType, + Code: code, + Issuer: issuer, + } + } + + return operations.PathPayment{ + Payment: operations.Payment{ + Base: baseOp, + From: op.SourceAccount().Address(), + To: payment.Destination.Address(), + Asset: base.Asset{ + Type: destAssetType, + Code: destCode, + Issuer: destIssuer, + }, + Amount: amount.String(payment.DestAmount), + }, + Path: path, + SourceAmount: sourceAmount, + SourceMax: amount.String(payment.SendMax), + SourceAssetType: sendAssetType, + SourceAssetCode: sendCode, + SourceAssetIssuer: sendIssuer, + }, nil +} diff --git a/exp/lighthorizon/adapters/path_payment_strict_send.go b/exp/lighthorizon/adapters/path_payment_strict_send.go new file mode 100644 index 0000000000..0068db30b5 --- /dev/null +++ b/exp/lighthorizon/adapters/path_payment_strict_send.go @@ -0,0 +1,78 @@ +package adapters + +import ( + "github.com/stellar/go/amount" + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/errors" +) + +func populatePathPaymentStrictSendOperation(op *common.Operation, baseOp operations.Base) (operations.PathPaymentStrictSend, error) { + payment := op.Get().Body.MustPathPaymentStrictSendOp() + + var ( + sendAssetType string + sendCode string + sendIssuer string + ) + err := payment.SendAsset.Extract(&sendAssetType, &sendCode, &sendIssuer) + if err != nil { + return operations.PathPaymentStrictSend{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + var ( + destAssetType string + destCode string + destIssuer string + ) + err = payment.DestAsset.Extract(&destAssetType, &destCode, &destIssuer) + if err != nil { + return operations.PathPaymentStrictSend{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + destAmount := amount.String(0) + if op.TransactionResult.Successful() { + result := op.OperationResult().MustPathPaymentStrictSendResult() + destAmount = amount.String(result.DestAmount()) + } + + var path = make([]base.Asset, len(payment.Path)) + for i := range payment.Path { + var ( + assetType string + code string + issuer string + ) + err = payment.Path[i].Extract(&assetType, &code, &issuer) + if err != nil { + return operations.PathPaymentStrictSend{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + path[i] = base.Asset{ + Type: assetType, + Code: code, + Issuer: issuer, + } + } + + return operations.PathPaymentStrictSend{ + Payment: operations.Payment{ + Base: baseOp, + From: op.SourceAccount().Address(), + To: payment.Destination.Address(), + Asset: base.Asset{ + Type: destAssetType, + Code: destCode, + Issuer: destIssuer, + }, + Amount: destAmount, + }, + Path: path, + SourceAmount: amount.String(payment.SendAmount), + DestinationMin: amount.String(payment.DestMin), + SourceAssetType: sendAssetType, + SourceAssetCode: sendCode, + SourceAssetIssuer: sendIssuer, + }, nil +} diff --git a/exp/lighthorizon/adapters/payment.go b/exp/lighthorizon/adapters/payment.go new file mode 100644 index 0000000000..97af5f6120 --- /dev/null +++ b/exp/lighthorizon/adapters/payment.go @@ -0,0 +1,35 @@ +package adapters + +import ( + "github.com/stellar/go/amount" + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/errors" +) + +func populatePaymentOperation(op *common.Operation, baseOp operations.Base) (operations.Payment, error) { + payment := op.Get().Body.MustPaymentOp() + + var ( + assetType string + code string + issuer string + ) + err := payment.Asset.Extract(&assetType, &code, &issuer) + if err != nil { + return operations.Payment{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + return operations.Payment{ + Base: baseOp, + To: payment.Destination.Address(), + From: op.SourceAccount().Address(), + Asset: base.Asset{ + Type: assetType, + Code: code, + Issuer: issuer, + }, + Amount: amount.StringFromInt64(int64(payment.Amount)), + }, nil +} diff --git a/exp/lighthorizon/adapters/revoke_sponsorship.go b/exp/lighthorizon/adapters/revoke_sponsorship.go new file mode 100644 index 0000000000..cb19decc5c --- /dev/null +++ b/exp/lighthorizon/adapters/revoke_sponsorship.go @@ -0,0 +1,66 @@ +package adapters + +import ( + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +func populateRevokeSponsorshipOperation(op *common.Operation, baseOp operations.Base) (operations.RevokeSponsorship, error) { + revokeSponsorship := op.Get().Body.MustRevokeSponsorshipOp() + + switch revokeSponsorship.Type { + case xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry: + ret := operations.RevokeSponsorship{ + Base: baseOp, + } + + ledgerKey := revokeSponsorship.LedgerKey + + switch ledgerKey.Type { + case xdr.LedgerEntryTypeAccount: + accountID := ledgerKey.Account.AccountId.Address() + ret.AccountID = &accountID + case xdr.LedgerEntryTypeClaimableBalance: + marshalHex, err := xdr.MarshalHex(ledgerKey.ClaimableBalance.BalanceId) + if err != nil { + return operations.RevokeSponsorship{}, err + } + ret.ClaimableBalanceID = &marshalHex + case xdr.LedgerEntryTypeData: + accountID := ledgerKey.Data.AccountId.Address() + dataName := string(ledgerKey.Data.DataName) + ret.DataAccountID = &accountID + ret.DataName = &dataName + case xdr.LedgerEntryTypeOffer: + offerID := int64(ledgerKey.Offer.OfferId) + ret.OfferID = &offerID + case xdr.LedgerEntryTypeTrustline: + trustlineAccountID := ledgerKey.TrustLine.AccountId.Address() + ret.TrustlineAccountID = &trustlineAccountID + if ledgerKey.TrustLine.Asset.Type == xdr.AssetTypeAssetTypePoolShare { + trustlineLiquidityPoolID := xdr.Hash(*ledgerKey.TrustLine.Asset.LiquidityPoolId).HexString() + ret.TrustlineLiquidityPoolID = &trustlineLiquidityPoolID + } else { + trustlineAsset := ledgerKey.TrustLine.Asset.ToAsset().StringCanonical() + ret.TrustlineAsset = &trustlineAsset + } + default: + return operations.RevokeSponsorship{}, errors.Errorf("invalid ledger key type: %d", ledgerKey.Type) + } + + return ret, nil + case xdr.RevokeSponsorshipTypeRevokeSponsorshipSigner: + signerAccountID := revokeSponsorship.Signer.AccountId.Address() + signerKey := revokeSponsorship.Signer.SignerKey.Address() + + return operations.RevokeSponsorship{ + Base: baseOp, + SignerAccountID: &signerAccountID, + SignerKey: &signerKey, + }, nil + } + + return operations.RevokeSponsorship{}, errors.Errorf("invalid revoke type: %d", revokeSponsorship.Type) +} diff --git a/exp/lighthorizon/adapters/set_options.go b/exp/lighthorizon/adapters/set_options.go new file mode 100644 index 0000000000..cf2cdeb20f --- /dev/null +++ b/exp/lighthorizon/adapters/set_options.go @@ -0,0 +1,122 @@ +package adapters + +import ( + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/xdr" +) + +func populateSetOptionsOperation(op *common.Operation, baseOp operations.Base) (operations.SetOptions, error) { + setOptions := op.Get().Body.MustSetOptionsOp() + + homeDomain := "" + if setOptions.HomeDomain != nil { + homeDomain = string(*setOptions.HomeDomain) + } + + inflationDest := "" + if setOptions.InflationDest != nil { + inflationDest = setOptions.InflationDest.Address() + } + + var signerKey string + var signerWeight *int + if setOptions.Signer != nil { + signerKey = setOptions.Signer.Key.Address() + signerWeightInt := int(setOptions.Signer.Weight) + signerWeight = &signerWeightInt + } + + var masterKeyWeight, lowThreshold, medThreshold, highThreshold *int + if setOptions.MasterWeight != nil { + masterKeyWeightInt := int(*setOptions.MasterWeight) + masterKeyWeight = &masterKeyWeightInt + } + if setOptions.LowThreshold != nil { + lowThresholdInt := int(*setOptions.LowThreshold) + lowThreshold = &lowThresholdInt + } + if setOptions.MedThreshold != nil { + medThresholdInt := int(*setOptions.MedThreshold) + medThreshold = &medThresholdInt + } + if setOptions.HighThreshold != nil { + highThresholdInt := int(*setOptions.HighThreshold) + highThreshold = &highThresholdInt + } + + var ( + setFlags []int + setFlagsS []string + + clearFlags []int + clearFlagsS []string + ) + + if setOptions.SetFlags != nil && *setOptions.SetFlags > 0 { + f := xdr.AccountFlags(*setOptions.SetFlags) + + if f.IsAuthRequired() { + setFlags = append(setFlags, int(xdr.AccountFlagsAuthRequiredFlag)) + setFlagsS = append(setFlagsS, "auth_required") + } + + if f.IsAuthRevocable() { + setFlags = append(setFlags, int(xdr.AccountFlagsAuthRevocableFlag)) + setFlagsS = append(setFlagsS, "auth_revocable") + } + + if f.IsAuthImmutable() { + setFlags = append(setFlags, int(xdr.AccountFlagsAuthImmutableFlag)) + setFlagsS = append(setFlagsS, "auth_immutable") + } + + if f.IsAuthClawbackEnabled() { + setFlags = append(setFlags, int(xdr.AccountFlagsAuthClawbackEnabledFlag)) + setFlagsS = append(setFlagsS, "auth_clawback_enabled") + } + } + + if setOptions.ClearFlags != nil && *setOptions.ClearFlags > 0 { + f := xdr.AccountFlags(*setOptions.ClearFlags) + + if f.IsAuthRequired() { + clearFlags = append(clearFlags, int(xdr.AccountFlagsAuthRequiredFlag)) + clearFlagsS = append(clearFlagsS, "auth_required") + } + + if f.IsAuthRevocable() { + clearFlags = append(clearFlags, int(xdr.AccountFlagsAuthRevocableFlag)) + clearFlagsS = append(clearFlagsS, "auth_revocable") + } + + if f.IsAuthImmutable() { + clearFlags = append(clearFlags, int(xdr.AccountFlagsAuthImmutableFlag)) + clearFlagsS = append(clearFlagsS, "auth_immutable") + } + + if f.IsAuthClawbackEnabled() { + clearFlags = append(clearFlags, int(xdr.AccountFlagsAuthClawbackEnabledFlag)) + clearFlagsS = append(clearFlagsS, "auth_clawback_enabled") + } + } + + return operations.SetOptions{ + Base: baseOp, + HomeDomain: homeDomain, + InflationDest: inflationDest, + + MasterKeyWeight: masterKeyWeight, + SignerKey: signerKey, + SignerWeight: signerWeight, + + SetFlags: setFlags, + SetFlagsS: setFlagsS, + ClearFlags: clearFlags, + ClearFlagsS: clearFlagsS, + + LowThreshold: lowThreshold, + MedThreshold: medThreshold, + HighThreshold: highThreshold, + }, nil +} diff --git a/exp/lighthorizon/adapters/set_trust_line_flags.go b/exp/lighthorizon/adapters/set_trust_line_flags.go new file mode 100644 index 0000000000..2969dcb2b5 --- /dev/null +++ b/exp/lighthorizon/adapters/set_trust_line_flags.go @@ -0,0 +1,83 @@ +package adapters + +import ( + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/protocols/horizon/base" + "github.com/stellar/go/protocols/horizon/operations" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +func populateSetTrustLineFlagsOperation(op *common.Operation, baseOp operations.Base) (operations.SetTrustLineFlags, error) { + setTrustLineFlags := op.Get().Body.MustSetTrustLineFlagsOp() + + var ( + assetType string + code string + issuer string + ) + err := setTrustLineFlags.Asset.Extract(&assetType, &code, &issuer) + if err != nil { + return operations.SetTrustLineFlags{}, errors.Wrap(err, "xdr.Asset.Extract error") + } + + var ( + setFlags []int + setFlagsS []string + + clearFlags []int + clearFlagsS []string + ) + + if setTrustLineFlags.SetFlags > 0 { + f := xdr.TrustLineFlags(setTrustLineFlags.SetFlags) + + if f.IsAuthorized() { + setFlags = append(setFlags, int(xdr.TrustLineFlagsAuthorizedFlag)) + setFlagsS = append(setFlagsS, "authorized") + } + + if f.IsAuthorizedToMaintainLiabilitiesFlag() { + setFlags = append(setFlags, int(xdr.TrustLineFlagsAuthorizedToMaintainLiabilitiesFlag)) + setFlagsS = append(setFlagsS, "authorized_to_maintain_liabilites") + } + + if f.IsClawbackEnabledFlag() { + setFlags = append(setFlags, int(xdr.TrustLineFlagsTrustlineClawbackEnabledFlag)) + setFlagsS = append(setFlagsS, "clawback_enabled") + } + } + + if setTrustLineFlags.ClearFlags > 0 { + f := xdr.TrustLineFlags(setTrustLineFlags.ClearFlags) + + if f.IsAuthorized() { + clearFlags = append(clearFlags, int(xdr.TrustLineFlagsAuthorizedFlag)) + clearFlagsS = append(clearFlagsS, "authorized") + } + + if f.IsAuthorizedToMaintainLiabilitiesFlag() { + clearFlags = append(clearFlags, int(xdr.TrustLineFlagsAuthorizedToMaintainLiabilitiesFlag)) + clearFlagsS = append(clearFlagsS, "authorized_to_maintain_liabilites") + } + + if f.IsClawbackEnabledFlag() { + clearFlags = append(clearFlags, int(xdr.TrustLineFlagsTrustlineClawbackEnabledFlag)) + clearFlagsS = append(clearFlagsS, "clawback_enabled") + } + } + + return operations.SetTrustLineFlags{ + Base: baseOp, + Asset: base.Asset{ + Type: assetType, + Code: code, + Issuer: issuer, + }, + Trustor: setTrustLineFlags.Trustor.Address(), + SetFlags: setFlags, + SetFlagsS: setFlagsS, + ClearFlags: clearFlags, + ClearFlagsS: clearFlagsS, + }, nil +} diff --git a/exp/lighthorizon/adapters/testdata/transactions.json b/exp/lighthorizon/adapters/testdata/transactions.json new file mode 100644 index 0000000000..6128801533 --- /dev/null +++ b/exp/lighthorizon/adapters/testdata/transactions.json @@ -0,0 +1,67 @@ +{ + "_links": { + "self": { + "href": "https://horizon.stellar.org/accounts/GBFHFINUD6NVGSX33PY25DDRCABN3H2JTDMLUEXAUEJVV22HTXVGLEZD/transactions?cursor=179530990183178241\u0026limit=1\u0026order=desc" + }, + "next": { + "href": "https://horizon.stellar.org/accounts/GBFHFINUD6NVGSX33PY25DDRCABN3H2JTDMLUEXAUEJVV22HTXVGLEZD/transactions?cursor=179530990183174144\u0026limit=1\u0026order=desc" + }, + "prev": { + "href": "https://horizon.stellar.org/accounts/GBFHFINUD6NVGSX33PY25DDRCABN3H2JTDMLUEXAUEJVV22HTXVGLEZD/transactions?cursor=179530990183174144\u0026limit=1\u0026order=asc" + } + }, + "_embedded": { + "records": [ + { + "_links": { + "self": { + "href": "https://horizon.stellar.org/transactions/55d8aa3693489ffc1d70b8ba33b8b5c012ec098f6f104383e3f090048488febd" + }, + "account": { + "href": "https://horizon.stellar.org/accounts/GBFHFINUD6NVGSX33PY25DDRCABN3H2JTDMLUEXAUEJVV22HTXVGLEZD" + }, + "ledger": { + "href": "https://horizon.stellar.org/ledgers/41800316" + }, + "operations": { + "href": "https://horizon.stellar.org/transactions/55d8aa3693489ffc1d70b8ba33b8b5c012ec098f6f104383e3f090048488febd/operations{?cursor,limit,order}", + "templated": true + }, + "effects": { + "href": "https://horizon.stellar.org/transactions/55d8aa3693489ffc1d70b8ba33b8b5c012ec098f6f104383e3f090048488febd/effects{?cursor,limit,order}", + "templated": true + }, + "precedes": { + "href": "https://horizon.stellar.org/transactions?order=asc\u0026cursor=179530990183174144" + }, + "succeeds": { + "href": "https://horizon.stellar.org/transactions?order=desc\u0026cursor=179530990183174144" + }, + "transaction": { + "href": "https://horizon.stellar.org/transactions/55d8aa3693489ffc1d70b8ba33b8b5c012ec098f6f104383e3f090048488febd" + } + }, + "id": "55d8aa3693489ffc1d70b8ba33b8b5c012ec098f6f104383e3f090048488febd", + "paging_token": "179530990183174144", + "successful": true, + "hash": "55d8aa3693489ffc1d70b8ba33b8b5c012ec098f6f104383e3f090048488febd", + "ledger": 41800316, + "created_at": "2022-07-17T13:08:41Z", + "source_account": "GBFHFINUD6NVGSX33PY25DDRCABN3H2JTDMLUEXAUEJVV22HTXVGLEZD", + "source_account_sequence": "172589382434294350", + "fee_account": "GBFHFINUD6NVGSX33PY25DDRCABN3H2JTDMLUEXAUEJVV22HTXVGLEZD", + "fee_charged": "100", + "max_fee": "100000", + "operation_count": 1, + "envelope_xdr": "AAAAAgAAAABKcqG0H5tTSvvb8a6McRAC3Z9JmNi6EuChE1rrR53qZQABhqACZSkhAAAKTgAAAAAAAAAAAAAAAQAAAAEAAAAASnKhtB+bU0r72/GujHEQAt2fSZjYuhLgoRNa60ed6mUAAAANAAAAAXlYTE0AAAAAIjbXcP4NPgFSGXXVz3rEhCtwldaxqddo0+mmMumZBr4AAAACVAvkAAAAAABKcqG0H5tTSvvb8a6McRAC3Z9JmNi6EuChE1rrR53qZQAAAAJEUklGVAAAAAAAAAAAAAAAvSOzPqUOGnDIcJOm7T85qDFRM0wfOVoubgkEPk95DZ0AAAEQvqAGdQAAAAEAAAAAAAAAAAAAAAFHneplAAAAQAVm9muIrK31Z+m2ZvhDYhtuoHcc/n+MO0DOaiQjfW+tsUNVCOw7foHiDRVLBdAHBZT+xxa3F+Ek9wQiKzxtQQM=", + "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAANAAAAAAAAAAIAAAABAAAAAPaTW9sBV2ja6yDUtPcpGpUrnVEHaTHC4I065TklIsguAAAAAD1H0goAAAAAAAAAAlQE8wsAAAABeVhMTQAAAAAiNtdw/g0+AVIZddXPesSEK3CV1rGp12jT6aYy6ZkGvgAAAAJUC+QAAAAAAgRnzllf8Sas0MUQlkxROsBgUzEoIN2XrYP9tlH5SINjAAAAAkRSSUZUAAAAAAAAAAAAAAC9I7M+pQ4acMhwk6btPzmoMVEzTB85Wi5uCQQ+T3kNnQAAARN/56NFAAAAAAAAAAJUBPMLAAAAAEpyobQfm1NK+9vxroxxEALdn0mY2LoS4KETWutHneplAAAAAkRSSUZUAAAAAAAAAAAAAAC9I7M+pQ4acMhwk6btPzmoMVEzTB85Wi5uCQQ+T3kNnQAAARN/56NFAAAAAA==", + "result_meta_xdr": "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", + "fee_meta_xdr": "AAAAAgAAAAMCfdJjAAAAAAAAAABKcqG0H5tTSvvb8a6McRAC3Z9JmNi6EuChE1rrR53qZQAAAAAQ2K4DAmUpIQAACk0AAAAKAAAAAQAAAADEccZDcGLJUGqJNC5TihraQE0vQc8dOiVfQyH3xuDhdQAAAAAAAAAJbG9ic3RyLmNvAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAJ90mMAAAAAYtQJSQAAAAAAAAABAn3SfAAAAAAAAAAASnKhtB+bU0r72/GujHEQAt2fSZjYuhLgoRNa60ed6mUAAAAAENitnwJlKSEAAApNAAAACgAAAAEAAAAAxHHGQ3BiyVBqiTQuU4oa2kBNL0HPHTolX0Mh98bg4XUAAAAAAAAACWxvYnN0ci5jbwAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAACfdJjAAAAAGLUCUkAAAAA", + "memo_type": "none", + "signatures": [ + "BWb2a4isrfVn6bZm+ENiG26gdxz+f4w7QM5qJCN9b62xQ1UI7Dt+geINFUsF0AcFlP7HFrcX4ST3BCIrPG1BAw==" + ] + } + ] + } +} \ No newline at end of file diff --git a/exp/lighthorizon/adapters/transaction.go b/exp/lighthorizon/adapters/transaction.go new file mode 100644 index 0000000000..6942668c8d --- /dev/null +++ b/exp/lighthorizon/adapters/transaction.go @@ -0,0 +1,295 @@ +package adapters + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + "fmt" + "net/url" + "strconv" + "strings" + "time" + "unicode/utf8" + + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/exp/lighthorizon/ingester" + "github.com/stellar/go/network" + protocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/support/render/hal" + "github.com/stellar/go/xdr" + "golang.org/x/exp/constraints" +) + +// PopulateTransaction converts between ingested XDR and RESTful JSON. In +// Horizon Classic, the data goes from Captive Core -> DB -> JSON. In our case, +// there's no DB intermediary, so we need to directly translate. +func PopulateTransaction( + baseUrl *url.URL, + tx *common.Transaction, + encoder *xdr.EncodingBuffer, +) (dest protocol.Transaction, err error) { + txHash, err := tx.TransactionHash() + if err != nil { + return + } + + dest.ID = txHash + dest.Successful = tx.Result.Successful() + dest.Hash = txHash + dest.Ledger = int32(tx.LedgerHeader.LedgerSeq) + dest.LedgerCloseTime = time.Unix(int64(tx.LedgerHeader.ScpValue.CloseTime), 0).UTC() + + source := tx.SourceAccount() + dest.Account = source.ToAccountId().Address() + if _, ok := source.GetMed25519(); ok { + dest.AccountMuxed, err = source.GetAddress() + if err != nil { + return + } + dest.AccountMuxedID, err = source.GetId() + if err != nil { + return + } + } + dest.AccountSequence = tx.Envelope.SeqNum() + + envelopeBase64, err := encoder.MarshalBase64(tx.Envelope) + if err != nil { + return + } + resultBase64, err := encoder.MarshalBase64(&tx.Result.Result) + if err != nil { + return + } + metaBase64, err := encoder.MarshalBase64(tx.UnsafeMeta) + if err != nil { + return + } + feeMetaBase64, err := encoder.MarshalBase64(tx.FeeChanges) + if err != nil { + return + } + + dest.OperationCount = int32(len(tx.Envelope.Operations())) + dest.EnvelopeXdr = envelopeBase64 + dest.ResultXdr = resultBase64 + dest.ResultMetaXdr = metaBase64 + dest.FeeMetaXdr = feeMetaBase64 + dest.MemoType = memoType(*tx.LedgerTransaction) + if m, ok := memo(*tx.LedgerTransaction); ok { + dest.Memo = m + if dest.MemoType == "text" { + var mb string + if mb, err = memoBytes(envelopeBase64); err != nil { + return + } else { + dest.MemoBytes = mb + } + } + } + + dest.Signatures = signatures(tx.Envelope.Signatures()) + + // If we never use this, we'll remove it later. This just defends us against + // nil dereferences. + dest.Preconditions = &protocol.TransactionPreconditions{} + + if tb := tx.Envelope.Preconditions().TimeBounds; tb != nil { + dest.Preconditions.TimeBounds = &protocol.TransactionPreconditionsTimebounds{ + MaxTime: formatTime(tb.MaxTime), + MinTime: formatTime(tb.MinTime), + } + } + + if lb := tx.Envelope.LedgerBounds(); lb != nil { + dest.Preconditions.LedgerBounds = &protocol.TransactionPreconditionsLedgerbounds{ + MinLedger: uint32(lb.MinLedger), + MaxLedger: uint32(lb.MaxLedger), + } + } + + if minSeq := tx.Envelope.MinSeqNum(); minSeq != nil { + dest.Preconditions.MinAccountSequence = fmt.Sprint(*minSeq) + } + + if minSeqAge := tx.Envelope.MinSeqAge(); minSeqAge != nil && *minSeqAge > 0 { + dest.Preconditions.MinAccountSequenceAge = formatTime(*minSeqAge) + } + + if minSeqGap := tx.Envelope.MinSeqLedgerGap(); minSeqGap != nil { + dest.Preconditions.MinAccountSequenceLedgerGap = uint32(*minSeqGap) + } + + if signers := tx.Envelope.ExtraSigners(); len(signers) > 0 { + dest.Preconditions.ExtraSigners = formatSigners(signers) + } + + if tx.Envelope.IsFeeBump() { + innerTx, ok := tx.Envelope.FeeBump.Tx.InnerTx.GetV1() + if !ok { + panic("Failed to parse inner transaction from fee-bump tx.") + } + + var rawInnerHash [32]byte + rawInnerHash, err = network.HashTransaction(innerTx.Tx, tx.NetworkPassphrase) + if err != nil { + return + } + innerHash := hex.EncodeToString(rawInnerHash[:]) + + feeAccountMuxed := tx.Envelope.FeeBumpAccount() + dest.FeeAccount = feeAccountMuxed.ToAccountId().Address() + if _, ok := feeAccountMuxed.GetMed25519(); ok { + dest.FeeAccountMuxed, err = feeAccountMuxed.GetAddress() + if err != nil { + return + } + dest.FeeAccountMuxedID, err = feeAccountMuxed.GetId() + if err != nil { + return + } + } + + dest.MaxFee = tx.Envelope.FeeBumpFee() + dest.FeeBumpTransaction = &protocol.FeeBumpTransaction{ + Hash: txHash, + Signatures: signatures(tx.Envelope.FeeBumpSignatures()), + } + dest.InnerTransaction = &protocol.InnerTransaction{ + Hash: innerHash, + MaxFee: int64(innerTx.Tx.Fee), + Signatures: signatures(tx.Envelope.Signatures()), + } + // TODO: Figure out what this means? Maybe @tamirms knows. + // if transactionHash != row.TransactionHash { + // dest.Signatures = dest.InnerTransaction.Signatures + // } + } else { + dest.FeeAccount = dest.Account + dest.FeeAccountMuxed = dest.AccountMuxed + dest.FeeAccountMuxedID = dest.AccountMuxedID + dest.MaxFee = int64(tx.Envelope.Fee()) + } + dest.FeeCharged = int64(tx.Result.Result.FeeCharged) + + lb := hal.LinkBuilder{Base: baseUrl} + dest.PT = strconv.FormatUint(uint64(tx.TOID()), 10) + dest.Links.Account = lb.Link("/accounts", dest.Account) + dest.Links.Ledger = lb.Link("/ledgers", fmt.Sprint(dest.Ledger)) + dest.Links.Operations = lb.PagedLink("/transactions", dest.ID, "operations") + dest.Links.Effects = lb.PagedLink("/transactions", dest.ID, "effects") + dest.Links.Self = lb.Link("/transactions", dest.ID) + dest.Links.Transaction = dest.Links.Self + dest.Links.Succeeds = lb.Linkf("/transactions?order=desc&cursor=%s", dest.PT) + dest.Links.Precedes = lb.Linkf("/transactions?order=asc&cursor=%s", dest.PT) + + // If we didn't need the structure, drop it. + if !tx.HasPreconditions() { + dest.Preconditions = nil + } + + return +} + +func formatSigners(s []xdr.SignerKey) []string { + if s == nil { + return nil + } + + signers := make([]string, len(s)) + for i, key := range s { + signers[i] = key.Address() + } + return signers +} + +func signatures(xdrSignatures []xdr.DecoratedSignature) []string { + signatures := make([]string, len(xdrSignatures)) + for i, sig := range xdrSignatures { + signatures[i] = base64.StdEncoding.EncodeToString(sig.Signature) + } + return signatures +} + +func memoType(tx ingester.LedgerTransaction) string { + switch tx.Envelope.Memo().Type { + case xdr.MemoTypeMemoNone: + return "none" + case xdr.MemoTypeMemoText: + return "text" + case xdr.MemoTypeMemoId: + return "id" + case xdr.MemoTypeMemoHash: + return "hash" + case xdr.MemoTypeMemoReturn: + return "return" + default: + panic(fmt.Errorf("invalid memo type: %v", tx.Envelope.Memo().Type)) + } +} + +func memo(tx ingester.LedgerTransaction) (value string, valid bool) { + valid = true + memo := tx.Envelope.Memo() + + switch memo.Type { + case xdr.MemoTypeMemoNone: + value, valid = "", false + + case xdr.MemoTypeMemoText: + scrubbed := scrub(memo.MustText()) + notnull := strings.Join(strings.Split(scrubbed, "\x00"), "") + value = notnull + + case xdr.MemoTypeMemoId: + value = fmt.Sprintf("%d", memo.MustId()) + + case xdr.MemoTypeMemoHash: + hash := memo.MustHash() + value = base64.StdEncoding.EncodeToString(hash[:]) + + case xdr.MemoTypeMemoReturn: + hash := memo.MustRetHash() + value = base64.StdEncoding.EncodeToString(hash[:]) + + default: + panic(fmt.Errorf("invalid memo type: %v", memo.Type)) + } + + return +} + +func memoBytes(envelopeXDR string) (string, error) { + var parsedEnvelope xdr.TransactionEnvelope + if err := xdr.SafeUnmarshalBase64(envelopeXDR, &parsedEnvelope); err != nil { + return "", err + } + + memo := *parsedEnvelope.Memo().Text + return base64.StdEncoding.EncodeToString([]byte(memo)), nil +} + +// scrub ensures that a given string is valid utf-8, replacing any invalid byte +// sequences with the utf-8 replacement character. +func scrub(in string) string { + // First check validity using the stdlib, returning if the string is already + // valid + if utf8.ValidString(in) { + return in + } + + left := []byte(in) + var result bytes.Buffer + + for len(left) > 0 { + r, n := utf8.DecodeRune(left) + result.WriteRune(r) // never errors, only panics + left = left[n:] + } + + return result.String() +} + +func formatTime[T constraints.Integer](t T) string { + return strconv.FormatUint(uint64(t), 10) +} diff --git a/exp/lighthorizon/adapters/transaction_test.go b/exp/lighthorizon/adapters/transaction_test.go new file mode 100644 index 0000000000..5a8ba4ab80 --- /dev/null +++ b/exp/lighthorizon/adapters/transaction_test.go @@ -0,0 +1,81 @@ +package adapters + +import ( + "encoding/json" + "net/url" + "os" + "path/filepath" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/exp/lighthorizon/ingester" + "github.com/stellar/go/ingest" + "github.com/stellar/go/network" + protocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/toid" + "github.com/stellar/go/xdr" +) + +// TestTransactionAdapter confirms that the adapter correctly serializes a +// transaction to JSON by actually pulling a transaction from the +// known-to-be-true horizon.stellar.org, turning it into an "ingested" +// transaction, and serializing it. +func TestTransactionAdapter(t *testing.T) { + f, err := os.Open(filepath.Join("./testdata", "transactions.json")) + require.NoErrorf(t, err, "are fixtures missing?") + + page := protocol.TransactionsPage{} + decoder := json.NewDecoder(f) + require.NoError(t, decoder.Decode(&page)) + require.Len(t, page.Embedded.Records, 1) + expectedTx := page.Embedded.Records[0] + + parsedUrl, err := url.Parse(page.Links.Self.Href) + require.NoError(t, err) + parsedToid, err := strconv.ParseInt(expectedTx.PagingToken(), 10, 64) + require.NoError(t, err) + expectedTxIndex := toid.Parse(parsedToid).TransactionOrder + + txEnv := xdr.TransactionEnvelope{} + txResult := xdr.TransactionResult{} + txMeta := xdr.TransactionMeta{} + txFeeMeta := xdr.LedgerEntryChanges{} + + require.NoError(t, xdr.SafeUnmarshalBase64(expectedTx.EnvelopeXdr, &txEnv)) + require.NoError(t, xdr.SafeUnmarshalBase64(expectedTx.ResultMetaXdr, &txMeta)) + require.NoError(t, xdr.SafeUnmarshalBase64(expectedTx.ResultXdr, &txResult)) + require.NoError(t, xdr.SafeUnmarshalBase64(expectedTx.FeeMetaXdr, &txFeeMeta)) + + closeTimestamp := expectedTx.LedgerCloseTime.UTC().Unix() + + tx := common.Transaction{ + LedgerTransaction: &ingester.LedgerTransaction{ + LedgerTransaction: &ingest.LedgerTransaction{ + Index: 0, + Envelope: txEnv, + Result: xdr.TransactionResultPair{ + TransactionHash: xdr.Hash{}, + Result: txResult, + }, + FeeChanges: txFeeMeta, + UnsafeMeta: txMeta, + }, + }, + LedgerHeader: &xdr.LedgerHeader{ + LedgerSeq: xdr.Uint32(expectedTx.Ledger), + ScpValue: xdr.StellarValue{ + CloseTime: xdr.TimePoint(closeTimestamp), + }, + }, + TxIndex: expectedTxIndex - 1, // TOIDs have a 1-based index + NetworkPassphrase: network.PublicNetworkPassphrase, + } + + result, err := PopulateTransaction(parsedUrl, &tx, xdr.NewEncodingBuffer()) + require.NoError(t, err) + assert.Equal(t, expectedTx, result) +} diff --git a/exp/lighthorizon/build/README.md b/exp/lighthorizon/build/README.md new file mode 100644 index 0000000000..d9dcf4556d --- /dev/null +++ b/exp/lighthorizon/build/README.md @@ -0,0 +1,24 @@ +# Light Horizon services deployment + +Light Horizon is composed of a few micro services: +* index-batch - contains map and reduce binaries to parallize tx-meta reads and index writes. +* index-single - contains single binary that reads tx-meta and writes indexes. +* ledgerexporter - contains single binary that reads from captive core and writes tx-meta +* web - contains single binary that runs web api which reads from tx-meta and index. + +See [godoc](https://godoc.org/github.com/stellar/go/exp/lighthorizon) for details on each service. + +## Buiding docker images of each service +Each service is packaged into a Docker image, use the helper script included here to build: +`./build.sh ` + +example to build just the mydockerhubname/lighthorizon-index-single:latest image to docker local images, no push to registry: +`./build.sh index-single mydockerhubname latest false` + +example to build images for all the services and push them to mydockerhubname/lighthorizon-:testversion: +`./build.sh all mydockerhubname testversion true` + +## Deploy service images on kubernetes(k8s) +* `k8s/ledgerexporter.yml` - creates a deployment with ledgerexporter image and supporting resources, such as configmap, secret, pvc for captive core on-disk storage. Review the settings to confirm they work in your environment before deployment. +* `k8s/lighthorizon_index.yml` - creates a deployment with index-single image and supporting resources, such as configmap, secret. Review the settings to confirm they work in your environment before deployment. +* `k8s/lighthorizon_web.yml` - creates a deployment with the web image and supporting resources, such as configmap, ingress rule. Review the settings to confirm they work in your environment before deployment. diff --git a/exp/lighthorizon/build/build.sh b/exp/lighthorizon/build/build.sh new file mode 100755 index 0000000000..e884fc4914 --- /dev/null +++ b/exp/lighthorizon/build/build.sh @@ -0,0 +1,56 @@ +#!/bin/bash -e + +# Move to repo root +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$DIR/../../.." +# module name is the sub-folder name under ./build +MODULE=$1 +DOCKER_REPO_PREFIX=$2 +DOCKER_TAG=$3 +DOCKER_PUSH=$4 + +if [ -z "$MODULE" ] ||\ + [ -z "$DOCKER_REPO_PREFIX" ] ||\ + [ -z "$DOCKER_TAG" ] ||\ + [ -z "$DOCKER_PUSH" ]; then + echo "invalid parameters, requires './build.sh '" + exit 1 +fi + +build_target () { + DOCKER_LABEL="$DOCKER_REPO_PREFIX"/lighthorizon-"$MODULE":"$DOCKER_TAG" + docker build --tag $DOCKER_LABEL --platform linux/amd64 -f "exp/lighthorizon/build/$MODULE/Dockerfile" . + if [ "$DOCKER_PUSH" == "true" ]; then + docker push $DOCKER_LABEL + fi +} + +case $MODULE in +index-batch) + build_target + ;; +ledgerexporter) + build_target + ;; +index-single) + build_target + ;; +web) + build_target + ;; +all) + MODULE=index-batch + build_target + MODULE=web + build_target + MODULE=index-single + build_target + MODULE=ledgerexporter + build_target + ;; +*) + echo "unknown MODULE build parameter ('$MODULE'), must be one of all|index-batch|web|index-single|ledgerexporter" + exit 1 + ;; +esac + diff --git a/exp/lighthorizon/build/index-batch/Dockerfile b/exp/lighthorizon/build/index-batch/Dockerfile new file mode 100644 index 0000000000..1780df682f --- /dev/null +++ b/exp/lighthorizon/build/index-batch/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:1.20 AS builder + +WORKDIR /go/src/github.com/stellar/go +COPY . ./ +RUN go mod download +RUN go install github.com/stellar/go/exp/lighthorizon/index/cmd/batch/map +RUN go install github.com/stellar/go/exp/lighthorizon/index/cmd/batch/reduce + +FROM ubuntu:22.04 +ENV DEBIAN_FRONTEND=noninteractive +# ca-certificates are required to make tls connections +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl wget gnupg apt-utils +RUN apt-get clean + +COPY --from=builder /go/src/github.com/stellar/go/exp/lighthorizon/build/index-batch/start ./ +COPY --from=builder /go/bin/map ./ +COPY --from=builder /go/bin/reduce ./ +RUN ["chmod", "+x", "/start"] + +ENTRYPOINT ["/start"] diff --git a/exp/lighthorizon/build/index-batch/README.md b/exp/lighthorizon/build/index-batch/README.md new file mode 100644 index 0000000000..c300066536 --- /dev/null +++ b/exp/lighthorizon/build/index-batch/README.md @@ -0,0 +1,7 @@ +# `stellar/lighthorizon-index-batch` + +This docker image contains the ledger/checkpoint indexing executables. It allows running multiple instances of `map`/`reduce` on a single machine or running it in [AWS Batch](https://aws.amazon.com/batch/). + +## Env variables + +See the [package documentation](../../index/cmd/batch/doc.go) for more details diff --git a/exp/lighthorizon/build/index-batch/start b/exp/lighthorizon/build/index-batch/start new file mode 100644 index 0000000000..88fb5335fb --- /dev/null +++ b/exp/lighthorizon/build/index-batch/start @@ -0,0 +1,17 @@ +#! /usr/bin/env bash +set -e + +# RUN_MODE must be set to 'map' or 'reduce' + +export TRACY_NO_INVARIANT_CHECK=1 +NETWORK_PASSPHRASE="${NETWORK_PASSPHRASE:=Public Global Stellar Network ; September 2015}" +if [ "$RUN_MODE" == "reduce" ]; then + echo "Running Reduce, REDUCE JOBS: $REDUCE_JOB_COUNT MAP JOBS: $MAP_JOB_COUNT TARGET INDEX: $INDEX_TARGET" + /reduce +elif [ "$RUN_MODE" == "map" ]; then + echo "Running Map, TARGET INDEX: $INDEX_TARGET FIRST CHECKPOINT: $FIRST_CHECKPOINT" + /map +else + echo "error: undefined RUN_MODE env variable ('$RUN_MODE'), must be 'map' or 'reduce'" + exit 1 +fi diff --git a/exp/lighthorizon/build/index-single/Dockerfile b/exp/lighthorizon/build/index-single/Dockerfile new file mode 100644 index 0000000000..1473f59f5c --- /dev/null +++ b/exp/lighthorizon/build/index-single/Dockerfile @@ -0,0 +1,25 @@ +FROM golang:1.20 AS builder + +WORKDIR /go/src/github.com/stellar/go +COPY . ./ +RUN go mod download +RUN go install github.com/stellar/go/exp/lighthorizon/index/cmd/single + +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive +# ca-certificates are required to make tls connections +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl wget gnupg apt-utils +RUN apt-get clean + +COPY --from=builder /go/bin/single ./ + +ENTRYPOINT ./single \ + -source "$TXMETA_SOURCE" \ + -target "$INDEXES_SOURCE" \ + -network-passphrase "$NETWORK_PASSPHRASE" \ + -start "$START" \ + -end "$END" \ + -modules "$MODULES" \ + -watch="$WATCH" \ + -workers "$WORKERS" diff --git a/exp/lighthorizon/build/k8s/ledgerexporter.yml b/exp/lighthorizon/build/k8s/ledgerexporter.yml new file mode 100644 index 0000000000..290dd85c63 --- /dev/null +++ b/exp/lighthorizon/build/k8s/ledgerexporter.yml @@ -0,0 +1,125 @@ +# this file contains the ledgerexporter deployment and it's config artifacts. +# +# when applying the manifest on a cluster, make sure to include namespace destination, +# as the manifest does not specify namespace, otherwise it'll go in your current kubectl context. +# +# make sure to set the secrets values, substitue placeholders. +# +# $ kubectl apply -f ledgerexporter.yml -n horizon-dev +apiVersion: v1 +kind: ConfigMap +metadata: + annotations: + fluxcd.io/ignore: "true" + labels: + app: ledgerexporter + name: ledgerexporter-pubnet-env +data: + # when using core 'on disk', the earliest ledger to get streamed out after catchup to 2, is 3 + # whereas on in-memory it streas out 2, adjusted here, otherwise horizon ingest will abort + # and stop process with error that ledger 3 is not <= expected ledger of 2. + START: "0" + END: "0" + + # can only have CONTINUE or START set, not both. + CONTINUE: "true" + WRITE_LATEST_PATH: "true" + CAPTIVE_CORE_USE_DB: "true" + + # configure the network to export + HISTORY_ARCHIVE_URLS: "https://history.stellar.org/prd/core-live/core_live_001,https://history.stellar.org/prd/core-live/core_live_002,https://history.stellar.org/prd/core-live/core_live_003" + NETWORK_PASSPHRASE: "Public Global Stellar Network ; September 2015" + # can refer to canned cfg's for pubnet and testnet which are included on the image + # `/captive-core-pubnet.cfg` or `/captive-core-testnet.cfg`. + # If exporting a standalone network, then mount a volume to the pod container with your standalone core's .cfg, + # and set full path to that volume here + CAPTIVE_CORE_CONFIG: "/captive-core-pubnet.cfg" + + # example of testnet network config. + # HISTORY_ARCHIVE_URLS: "https://history.stellar.org/prd/core-testnet/core_testnet_001,https://history.stellar.org/prd/core-testnet/core_testnet_002" + # NETWORK_PASSPHRASE: "Test SDF Network ; September 2015" + # CAPTIVE_CORE_CONFIG: "/captive-core-testnet.cfg" + + # provide the url for the external s3 bucket to be populated + # update the ledgerexporter-pubnet-secret to have correct aws key/secret for access to the bucket + ARCHIVE_TARGET: "s3://horizon-ledgermeta-prodnet-test" +--- +apiVersion: v1 +kind: Secret +metadata: + labels: + app: ledgerexporter + name: ledgerexporter-pubnet-secret +type: Opaque +data: + AWS_REGION: + AWS_ACCESS_KEY_ID: + AWS_SECRET_ACCESS_KEY: +--- +# running captive core with on-disk mode limits RAM to around 2G usage, but +# requires some dedicated disk storage space that has at least 3k IOPS for read/write. +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ledgerexporter-pubnet-core-storage +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 500Gi + storageClassName: default + volumeMode: Filesystem +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + fluxcd.io/ignore: "true" + deployment.kubernetes.io/revision: "3" + labels: + app: ledgerexporter-pubnet + name: ledgerexporter-pubnet-deployment +spec: + selector: + matchLabels: + app: ledgerexporter-pubnet + replicas: 1 + template: + metadata: + annotations: + fluxcd.io/ignore: "true" + # if we expect to add metrics at some point to ledgerexporter + # this just needs to be set to true + prometheus.io/port: "6060" + prometheus.io/scrape: "false" + labels: + app: ledgerexporter-pubnet + spec: + containers: + - envFrom: + - secretRef: + name: ledgerexporter-pubnet-secret + - configMapRef: + name: ledgerexporter-pubnet-env + image: stellar/lighthorizon-ledgerexporter:latest + imagePullPolicy: Always + name: ledgerexporter-pubnet + resources: + limits: + cpu: 3 + memory: 8Gi + requests: + cpu: 500m + memory: 2Gi + volumeMounts: + - mountPath: /cc + name: core-storage + dnsPolicy: ClusterFirst + volumes: + - name: core-storage + persistentVolumeClaim: + claimName: ledgerexporter-pubnet-core-storage + + + diff --git a/exp/lighthorizon/build/k8s/lighthorizon_batch_map_job.yml b/exp/lighthorizon/build/k8s/lighthorizon_batch_map_job.yml new file mode 100644 index 0000000000..a2671b66c1 --- /dev/null +++ b/exp/lighthorizon/build/k8s/lighthorizon_batch_map_job.yml @@ -0,0 +1,43 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: 'batch-map-job' +spec: + completions: 52 + parallelism: 10 + completionMode: Indexed + template: + spec: + restartPolicy: Never + containers: + - name: 'worker' + image: 'stellar/lighthorizon-index-batch' + imagePullPolicy: Always + envFrom: + - secretRef: + name: + env: + - name: RUN_MODE + value: "map" + - name: BATCH_SIZE + value: "10048" + - name: FIRST_CHECKPOINT + value: "41426080" + - name: WORKER_COUNT + value: "8" + - name: TXMETA_SOURCE + value: "" + - name: JOB_INDEX_ENV + value: "JOB_COMPLETION_INDEX" + - name: NETWORK_PASSPHRASE + value: "pubnet" + - name: INDEX_TARGET + value: "url of target index" + resources: + limits: + cpu: 4 + memory: 5Gi + requests: + cpu: 500m + memory: 500Mi + \ No newline at end of file diff --git a/exp/lighthorizon/build/k8s/lighthorizon_batch_reduce_job.yml b/exp/lighthorizon/build/k8s/lighthorizon_batch_reduce_job.yml new file mode 100644 index 0000000000..1bc9cb7f6c --- /dev/null +++ b/exp/lighthorizon/build/k8s/lighthorizon_batch_reduce_job.yml @@ -0,0 +1,42 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: 'batch-reduce-job' +spec: + completions: 52 + parallelism: 10 + completionMode: Indexed + template: + spec: + restartPolicy: Never + containers: + - name: 'worker' + image: 'stellar/lighthorizon-index-batch' + imagePullPolicy: Always + envFrom: + - secretRef: + name: + env: + - name: RUN_MODE + value: "reduce" + - name: MAP_JOB_COUNT + value: "52" + - name: REDUCE_JOB_COUNT + value: "52" + - name: WORKER_COUNT + value: "8" + - name: INDEX_SOURCE_ROOT + value: "" + - name: JOB_INDEX_ENV + value: JOB_COMPLETION_INDEX + - name: INDEX_TARGET + value: "" + resources: + limits: + cpu: 4 + memory: 5Gi + requests: + cpu: 500m + memory: 500Mi + + \ No newline at end of file diff --git a/exp/lighthorizon/build/k8s/lighthorizon_index.yml b/exp/lighthorizon/build/k8s/lighthorizon_index.yml new file mode 100644 index 0000000000..1e7931fb2a --- /dev/null +++ b/exp/lighthorizon/build/k8s/lighthorizon_index.yml @@ -0,0 +1,74 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + annotations: + fluxcd.io/ignore: "true" + labels: + app: lighthorizon-pubnet-index + name: lighthorizon-pubnet-index-env +data: + TXMETA_SOURCE: "s3://horizon-ledgermeta-prodnet-test" + INDEXES_SOURCE: "s3://horizon-index-prodnet-test" + NETWORK_PASSPHRASE: "Public Global Stellar Network ; September 2015" + START: "41809728" + END: "0" + WATCH: "true" + MODULES: "accounts" + WORKERS: "3" +--- +apiVersion: v1 +kind: Secret +metadata: + labels: + app: lighthorizon-pubnet-index + name: lighthorizon-pubnet-index-secret +type: Opaque +data: + AWS_REGION: + AWS_ACCESS_KEY_ID: + AWS_SECRET_ACCESS_KEY: +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + fluxcd.io/ignore: "true" + labels: + app: lighthorizon-pubnet-index + name: lighthorizon-pubnet-index +spec: + replicas: 1 + selector: + matchLabels: + app: lighthorizon-pubnet-index + template: + metadata: + annotations: + fluxcd.io/ignore: "true" + prometheus.io/port: "6060" + prometheus.io/scrape: "false" + labels: + app: lighthorizon-pubnet-index + spec: + containers: + - envFrom: + - secretRef: + name: lighthorizon-pubnet-index-secret + - configMapRef: + name: lighthorizon-pubnet-index-env + image: stellar/lighthorizon-index-single:latest + imagePullPolicy: Always + name: index + ports: + - containerPort: 6060 + name: metrics + protocol: TCP + resources: + limits: + cpu: 3 + memory: 6Gi + requests: + cpu: 500m + memory: 1Gi + + \ No newline at end of file diff --git a/exp/lighthorizon/build/k8s/lighthorizon_web.yml b/exp/lighthorizon/build/k8s/lighthorizon_web.yml new file mode 100644 index 0000000000..b680e7fb2c --- /dev/null +++ b/exp/lighthorizon/build/k8s/lighthorizon_web.yml @@ -0,0 +1,133 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + annotations: + fluxcd.io/ignore: "true" + labels: + app: lighthorizon-pubnet-web + name: lighthorizon-pubnet-web-env +data: + TXMETA_SOURCE: "s3://horizon-indices-pubnet" + INDEXES_SOURCE: "s3://horizon-ledgermeta-pubnet" + NETWORK_PASSPHRASE: "Public Global Stellar Network ; September 2015" + MAX_PARALLEL_DOWNLOADS: 16 + CACHE_PATH: "/ledgercache" + CACHE_PRELOAD_START_LEDGER: 0 + CACHE_PRELOAD_COUNT: 14400 +--- +apiVersion: v1 +kind: Secret +metadata: + labels: + app: lighthorizon-pubnet-web + name: lighthorizon-pubnet-web-secret +type: Opaque +data: + AWS_REGION: + AWS_ACCESS_KEY_ID: + AWS_SECRET_ACCESS_KEY: +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + fluxcd.io/ignore: "true" + labels: + app: lighthorizon-pubnet-web + name: lighthorizon-pubnet-web +spec: + replicas: 1 + selector: + matchLabels: + app: lighthorizon-pubnet-web + template: + metadata: + annotations: + fluxcd.io/ignore: "true" + prometheus.io/port: "6060" + prometheus.io/scrape: "false" + creationTimestamp: null + labels: + app: lighthorizon-pubnet-web + spec: + containers: + - envFrom: + - secretRef: + name: lighthorizon-pubnet-web-secret + - configMapRef: + name: lighthorizon-pubnet-web-env + image: stellar/lighthorizon-web:latest + imagePullPolicy: Always + name: web + ports: + - containerPort: 8080 + name: web + protocol: TCP + - containerPort: 6060 + name: metrics + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 8080 + scheme: HTTP + initialDelaySeconds: 30 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 5 + resources: + limits: + cpu: 2 + memory: 4Gi + requests: + cpu: 500m + memory: 1Gi + volumeMounts: + - mountPath: /ledgercache + name: cache-storage + volumes: + - name: cache-storage + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: lighthorizon-pubnet-web + name: lighthorizon-pubnet-web +spec: + ports: + - name: http + port: 8000 + protocol: TCP + targetPort: 8080 + selector: + app: lighthorizon-pubnet-web + sessionAffinity: None + type: ClusterIP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + cert-manager.io/cluster-issuer: default + ingress.kubernetes.io/ssl-redirect: "true" + kubernetes.io/ingress.class: public + name: lighthorizon-pubnet-web +spec: + rules: + - host: lighthorizon-pubnet.prototypes.kube001.services.stellar-ops.com + http: + paths: + - backend: + service: + name: lighthorizon-pubnet-web + port: + number: 8000 + path: / + pathType: ImplementationSpecific + tls: + - hosts: + - lighthorizon-pubnet.prototypes.kube001.services.stellar-ops.com + secretName: lighthorizon-pubnet-web-cert diff --git a/exp/lighthorizon/build/ledgerexporter/Dockerfile b/exp/lighthorizon/build/ledgerexporter/Dockerfile new file mode 100644 index 0000000000..f7129d7be2 --- /dev/null +++ b/exp/lighthorizon/build/ledgerexporter/Dockerfile @@ -0,0 +1,33 @@ +FROM golang:1.20 AS builder + +WORKDIR /go/src/github.com/stellar/go +COPY . ./ +RUN go mod download +RUN go install github.com/stellar/go/exp/services/ledgerexporter + +FROM ubuntu:22.04 +ARG STELLAR_CORE_VERSION +ENV STELLAR_CORE_VERSION=${STELLAR_CORE_VERSION:-*} +ENV STELLAR_CORE_BINARY_PATH /usr/bin/stellar-core + +ENV DEBIAN_FRONTEND=noninteractive +# ca-certificates are required to make tls connections +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl wget gnupg apt-utils +RUN wget -qO - https://apt.stellar.org/SDF.asc | APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=true apt-key add - +RUN echo "deb https://apt.stellar.org jammy stable" >/etc/apt/sources.list.d/SDF.list +RUN echo "deb https://apt.stellar.org jammy unstable" >/etc/apt/sources.list.d/SDF-unstable.list +RUN apt-get update && apt-get install -y stellar-core=${STELLAR_CORE_VERSION} +RUN apt-get clean + +COPY --from=builder /go/src/github.com/stellar/go/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg / +COPY --from=builder /go/src/github.com/stellar/go/exp/lighthorizon/build/ledgerexporter/captive-core-testnet.cfg / +COPY --from=builder /go/src/github.com/stellar/go/exp/lighthorizon/build/ledgerexporter/start / + +RUN ["chmod", "+x", "/start"] + +# for the captive core sqlite database +RUN mkdir -p /cc + +COPY --from=builder /go/bin/ledgerexporter ./ + +ENTRYPOINT ["/start"] diff --git a/exp/lighthorizon/build/ledgerexporter/README.md b/exp/lighthorizon/build/ledgerexporter/README.md new file mode 100644 index 0000000000..5534b2809a --- /dev/null +++ b/exp/lighthorizon/build/ledgerexporter/README.md @@ -0,0 +1,42 @@ +# `stellar/horizon-ledgerexporter` + +This docker image allows running multiple instances of `ledgerexporter` on a single machine or running it in [AWS Batch](https://aws.amazon.com/batch/). + +## Env variables + +### Running locally + +| Name | Description | +|---------|------------------------| +| `START` | First ledger to export | +| `END` | Last ledger to export | + +### Running in AWS Batch + +| Name | Description | +|----------------------|----------------------------------------------------------------------| +| `BATCH_START_LEDGER` | First ledger of the AWS Batch Job, must be a checkpoint ledger or 1. | +| `BATCH_SIZE` | Size of the batch, must be multiple of 64. | + +#### Example + +When you start 10 jobs with `BATCH_START_LEDGER=63` and `BATCH_SIZE=64` +it will run the following ranges: + +| `AWS_BATCH_JOB_ARRAY_INDEX` | `FROM` | `TO` | +|-----------------------------|--------|------| +| 0 | 63 | 127 | +| 1 | 127 | 191 | +| 2 | 191 | 255 | +| 3 | 255 | 319 | + +## Tips when using AWS Batch + +* In "Job definition" set vCPUs to 2 and Memory to 4096. This represents the `c5.large` instances Horizon should be using. +* In "Compute environments": + * Set instance type to "c5.large". + * Set "Maximum vCPUs" to 2x the number of instances you want to start (because "c5.large" has 2 vCPUs). Ex. 10 vCPUs = 5 x "c5.large" instances. +* Use spot instances! It's much cheaper and speed of testing will be the same in 99% of cases. +* You need to publish the image if there are any changes in `Dockerfile` or one of the scripts. +* When batch processing is over check if instances have been terminated. Sometimes AWS doesn't terminate them. +* Make sure the job timeout is set to a larger value if you export larger ranges. Default is just 100 seconds. diff --git a/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg b/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg new file mode 100644 index 0000000000..22b149e3f8 --- /dev/null +++ b/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg @@ -0,0 +1,206 @@ +PEER_PORT=11725 +DATABASE = "sqlite3:///cc/stellar.db" + +FAILURE_SAFETY=1 + +# WARNING! Do not use this config in production. Quorum sets should +# be carefully selected manually. +NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" +HTTP_PORT=11626 + +[[HOME_DOMAINS]] +HOME_DOMAIN="stellar.org" +QUALITY="HIGH" + +[[HOME_DOMAINS]] +HOME_DOMAIN="satoshipay.io" +QUALITY="HIGH" + +[[HOME_DOMAINS]] +HOME_DOMAIN="lobstr.co" +QUALITY="HIGH" + +[[HOME_DOMAINS]] +HOME_DOMAIN="www.coinqvest.com" +QUALITY="HIGH" + +[[HOME_DOMAINS]] +HOME_DOMAIN="publicnode.org" +QUALITY="HIGH" + +[[HOME_DOMAINS]] +HOME_DOMAIN="stellar.blockdaemon.com" +QUALITY="HIGH" + +[[HOME_DOMAINS]] +HOME_DOMAIN = "www.franklintempleton.com" +QUALITY = "HIGH" + +[[VALIDATORS]] +NAME="sdf_1" +HOME_DOMAIN="stellar.org" +PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" +ADDRESS="core-live-a.stellar.org:11625" +HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" + +[[VALIDATORS]] +NAME="sdf_2" +HOME_DOMAIN="stellar.org" +PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" +ADDRESS="core-live-b.stellar.org:11625" +HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" + +[[VALIDATORS]] +NAME="sdf_3" +HOME_DOMAIN="stellar.org" +PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" +ADDRESS="core-live-c.stellar.org:11625" +HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" + +[[VALIDATORS]] +NAME="satoshipay_singapore" +HOME_DOMAIN="satoshipay.io" +PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" +ADDRESS="stellar-sg-sin.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" + +[[VALIDATORS]] +NAME="satoshipay_iowa" +HOME_DOMAIN="satoshipay.io" +PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" +ADDRESS="stellar-us-iowa.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" + +[[VALIDATORS]] +NAME="satoshipay_frankfurt" +HOME_DOMAIN="satoshipay.io" +PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" +ADDRESS="stellar-de-fra.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" + +[[VALIDATORS]] +NAME="lobstr_1_europe" +HOME_DOMAIN="lobstr.co" +PUBLIC_KEY="GCFONE23AB7Y6C5YZOMKUKGETPIAJA4QOYLS5VNS4JHBGKRZCPYHDLW7" +ADDRESS="v1.stellar.lobstr.co:11625" +HISTORY="curl -sf https://stellar-archive-1-lobstr.s3.amazonaws.com/{0} -o {1}" + +[[VALIDATORS]] +NAME="lobstr_2_europe" +HOME_DOMAIN="lobstr.co" +PUBLIC_KEY="GDXQB3OMMQ6MGG43PWFBZWBFKBBDUZIVSUDAZZTRAWQZKES2CDSE5HKJ" +ADDRESS="v2.stellar.lobstr.co:11625" +HISTORY="curl -sf https://stellar-archive-2-lobstr.s3.amazonaws.com/{0} -o {1}" + +[[VALIDATORS]] +NAME="lobstr_3_north_america" +HOME_DOMAIN="lobstr.co" +PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" +ADDRESS="v3.stellar.lobstr.co:11625" +HISTORY="curl -sf https://stellar-archive-3-lobstr.s3.amazonaws.com/{0} -o {1}" + +[[VALIDATORS]] +NAME="lobstr_4_asia" +HOME_DOMAIN="lobstr.co" +PUBLIC_KEY="GA7TEPCBDQKI7JQLQ34ZURRMK44DVYCIGVXQQWNSWAEQR6KB4FMCBT7J" +ADDRESS="v4.stellar.lobstr.co:11625" +HISTORY="curl -sf https://stellar-archive-4-lobstr.s3.amazonaws.com/{0} -o {1}" + +[[VALIDATORS]] +NAME="lobstr_5_australia" +HOME_DOMAIN="lobstr.co" +PUBLIC_KEY="GA5STBMV6QDXFDGD62MEHLLHZTPDI77U3PFOD2SELU5RJDHQWBR5NNK7" +ADDRESS="v5.stellar.lobstr.co:11625" +HISTORY="curl -sf https://stellar-archive-5-lobstr.s3.amazonaws.com/{0} -o {1}" + +[[VALIDATORS]] +NAME="coinqvest_hong_kong" +HOME_DOMAIN="www.coinqvest.com" +PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" +ADDRESS="hongkong.stellar.coinqvest.com:11625" +HISTORY="curl -sf https://hongkong.stellar.coinqvest.com/history/{0} -o {1}" + +[[VALIDATORS]] +NAME="coinqvest_germany" +HOME_DOMAIN="www.coinqvest.com" +PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" +ADDRESS="germany.stellar.coinqvest.com:11625" +HISTORY="curl -sf https://germany.stellar.coinqvest.com/history/{0} -o {1}" + +[[VALIDATORS]] +NAME="coinqvest_finland" +HOME_DOMAIN="www.coinqvest.com" +PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" +ADDRESS="finland.stellar.coinqvest.com:11625" +HISTORY="curl -sf https://finland.stellar.coinqvest.com/history/{0} -o {1}" + +[[VALIDATORS]] +NAME="bootes" +HOME_DOMAIN="publicnode.org" +PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" +ADDRESS="bootes.publicnode.org" +HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" + +[[VALIDATORS]] +NAME="hercules" +HOME_DOMAIN="publicnode.org" +PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" +ADDRESS="hercules.publicnode.org" +HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" + +[[VALIDATORS]] +NAME="lyra" +HOME_DOMAIN="publicnode.org" +PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" +ADDRESS="lyra.publicnode.org" +HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" + +[[VALIDATORS]] +NAME="Blockdaemon_Validator_1" +HOME_DOMAIN="stellar.blockdaemon.com" +PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" +ADDRESS="stellar-full-validator1.bdnodes.net" +HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" + +[[VALIDATORS]] +NAME="Blockdaemon_Validator_2" +HOME_DOMAIN="stellar.blockdaemon.com" +PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" +ADDRESS="stellar-full-validator2.bdnodes.net" +HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" + +[[VALIDATORS]] +NAME="Blockdaemon_Validator_3" +HOME_DOMAIN="stellar.blockdaemon.com" +PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" +ADDRESS="stellar-full-validator3.bdnodes.net" +HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" + +[[VALIDATORS]] +NAME = "FT_SCV_1" +HOME_DOMAIN = "www.franklintempleton.com" +PUBLIC_KEY = "GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" +ADDRESS = "stellar1.franklintempleton.com:11625" +HISTORY = "curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" + +[[VALIDATORS]] +NAME = "FT_SCV_2" +HOME_DOMAIN = "www.franklintempleton.com" +PUBLIC_KEY = "GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" +ADDRESS = "stellar2.franklintempleton.com:11625" +HISTORY = "curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" + +[[VALIDATORS]] +<<<<<<<< HEAD:exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg +NAME="wirexSG" +ADDRESS="sg.stellar.wirexapp.com" +HOME_DOMAIN="wirexapp.com" +PUBLIC_KEY="GAB3GZIE6XAYWXGZUDM4GMFFLJBFMLE2JDPUCWUZXMOMT3NHXDHEWXAS" +HISTORY="curl -sf http://wxhorizonasiastga1.blob.core.windows.net/history/{0} -o {1}" +======== +NAME = "FT_SCV_3" +HOME_DOMAIN = "www.franklintempleton.com" +PUBLIC_KEY = "GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" +ADDRESS = "stellar3.franklintempleton.com:11625" +HISTORY = "curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" +>>>>>>>> master:services/horizon/internal/configs/captive-core-pubnet.cfg diff --git a/exp/lighthorizon/build/ledgerexporter/captive-core-testnet.cfg b/exp/lighthorizon/build/ledgerexporter/captive-core-testnet.cfg new file mode 100644 index 0000000000..0cd9b2f496 --- /dev/null +++ b/exp/lighthorizon/build/ledgerexporter/captive-core-testnet.cfg @@ -0,0 +1,30 @@ +PEER_PORT=11725 +DATABASE = "sqlite3:///cc/stellar.db" + +UNSAFE_QUORUM=true +FAILURE_SAFETY=1 + +[[HOME_DOMAINS]] +HOME_DOMAIN="testnet.stellar.org" +QUALITY="HIGH" + +[[VALIDATORS]] +NAME="sdf_testnet_1" +HOME_DOMAIN="testnet.stellar.org" +PUBLIC_KEY="GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" +ADDRESS="core-testnet1.stellar.org" +HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_001/{0} -o {1}" + +[[VALIDATORS]] +NAME="sdf_testnet_2" +HOME_DOMAIN="testnet.stellar.org" +PUBLIC_KEY="GCUCJTIYXSOXKBSNFGNFWW5MUQ54HKRPGJUTQFJ5RQXZXNOLNXYDHRAP" +ADDRESS="core-testnet2.stellar.org" +HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_002/{0} -o {1}" + +[[VALIDATORS]] +NAME="sdf_testnet_3" +HOME_DOMAIN="testnet.stellar.org" +PUBLIC_KEY="GC2V2EFSXN6SQTWVYA5EPJPBWWIMSD2XQNKUOHGEKB535AQE2I6IXV2Z" +ADDRESS="core-testnet3.stellar.org" +HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_003/{0} -o {1}" \ No newline at end of file diff --git a/exp/lighthorizon/build/ledgerexporter/start b/exp/lighthorizon/build/ledgerexporter/start new file mode 100644 index 0000000000..11d863effa --- /dev/null +++ b/exp/lighthorizon/build/ledgerexporter/start @@ -0,0 +1,55 @@ +#! /usr/bin/env bash +set -e + +START="${START:=2}" +END="${END:=0}" +CONTINUE="${CONTINUE:=false}" +# Writing to /latest is disabled by default to avoid race conditions between parallel container runs +WRITE_LATEST_PATH="${WRITE_LATEST_PATH:=false}" + +# config defaults to pubnet core, any other network requires setting all 3 of these in container env +NETWORK_PASSPHRASE="${NETWORK_PASSPHRASE:=Public Global Stellar Network ; September 2015}" +HISTORY_ARCHIVE_URLS="${HISTORY_ARCHIVE_URLS:=https://s3-eu-west-1.amazonaws.com/history.stellar.org/prd/core-live/core_live_001}" +CAPTIVE_CORE_CONFIG="${CAPTIVE_CORE_CONFIG:=/captive-core-pubnet.cfg}" + +CAPTIVE_CORE_USE_DB="${CAPTIVE_CORE_USE_DB:=true}" + +if [ -z "$ARCHIVE_TARGET" ]; then + echo "error: undefined ARCHIVE_TARGET env variable" + exit 1 +fi + +# Calculate params for AWS Batch +if [ ! -z "$AWS_BATCH_JOB_ARRAY_INDEX" ]; then + # The batch should have three env variables: + # * BATCH_START_LEDGER - start ledger of the job, must be equal 1 or a + # checkpoint ledger (i + 1) % 64 == 0. + # * BATCH_SIZE - size of the batch in ledgers, must be multiple of 64! + # * BRANCH - git branch to build + # + # Ex: BATCH_START_LEDGER=63, BATCH_SIZE=64 will create the following ranges: + # AWS_BATCH_JOB_ARRAY_INDEX=0: [63, 127] + # AWS_BATCH_JOB_ARRAY_INDEX=1: [127, 191] + # AWS_BATCH_JOB_ARRAY_INDEX=2: [191, 255] + # AWS_BATCH_JOB_ARRAY_INDEX=3: [255, 319] + # ... + START=`expr "$BATCH_SIZE" \* "$AWS_BATCH_JOB_ARRAY_INDEX" + "$BATCH_START_LEDGER"` + END=`expr "$BATCH_SIZE" \* "$AWS_BATCH_JOB_ARRAY_INDEX" + "$BATCH_START_LEDGER" + "$BATCH_SIZE"` + + if [ "$START" -lt 2 ]; then + # The minimum ledger expected by the ledger exporter is 2 + START=2 + fi + +fi + +echo "START: $START END: $END" + +export TRACY_NO_INVARIANT_CHECK=1 +/ledgerexporter --target "$ARCHIVE_TARGET" \ + --captive-core-toml-path "$CAPTIVE_CORE_CONFIG" \ + --history-archive-urls "$HISTORY_ARCHIVE_URLS" --network-passphrase "$NETWORK_PASSPHRASE" \ + --continue="$CONTINUE" --write-latest-path="$WRITE_LATEST_PATH" \ + --start-ledger "$START" --end-ledger "$END" --captive-core-use-db="$CAPTIVE_CORE_USE_DB" + +echo "OK" diff --git a/exp/lighthorizon/build/web/Dockerfile b/exp/lighthorizon/build/web/Dockerfile new file mode 100644 index 0000000000..83d0002ebc --- /dev/null +++ b/exp/lighthorizon/build/web/Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.20 AS builder + +WORKDIR /go/src/github.com/stellar/go +COPY . ./ +RUN go mod download +RUN go install github.com/stellar/go/exp/lighthorizon + +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive +# ca-certificates are required to make tls connections +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl wget gnupg apt-utils +RUN apt-get clean + +COPY --from=builder /go/bin/lighthorizon ./ + +ENTRYPOINT ./lighthorizon serve \ + --network-passphrase "$NETWORK_PASSPHRASE" \ + --parallel-downloads "$MAX_PARALLEL_DOWNLOADS" \ + --ledger-cache "$CACHE_PATH" \ + --ledger-cache-preload "$CACHE_PRELOAD_COUNT" \ + --ledger-cache-preload-start "$CACHE_PRELOAD_START_LEDGER" \ + --log-level debug \ + "$TXMETA_SOURCE" "$INDEXES_SOURCE" diff --git a/exp/lighthorizon/common/operation.go b/exp/lighthorizon/common/operation.go new file mode 100644 index 0000000000..ca5f7bfe61 --- /dev/null +++ b/exp/lighthorizon/common/operation.go @@ -0,0 +1,52 @@ +package common + +import ( + "encoding/hex" + + "github.com/stellar/go/network" + "github.com/stellar/go/toid" + "github.com/stellar/go/xdr" +) + +type Operation struct { + TransactionEnvelope *xdr.TransactionEnvelope + TransactionResult *xdr.TransactionResult + LedgerHeader *xdr.LedgerHeader + OpIndex int32 + TxIndex int32 +} + +func (o *Operation) Get() *xdr.Operation { + return &o.TransactionEnvelope.Operations()[o.OpIndex] +} + +func (o *Operation) OperationResult() *xdr.OperationResultTr { + results, _ := o.TransactionResult.OperationResults() + tr := results[o.OpIndex].MustTr() + return &tr +} + +func (o *Operation) TransactionHash() (string, error) { + hash, err := network.HashTransactionInEnvelope(*o.TransactionEnvelope, network.PublicNetworkPassphrase) + if err != nil { + return "", err + } + + return hex.EncodeToString(hash[:]), nil +} + +func (o *Operation) SourceAccount() xdr.AccountId { + sourceAccount := o.TransactionEnvelope.SourceAccount().ToAccountId() + if o.Get().SourceAccount != nil { + sourceAccount = o.Get().SourceAccount.ToAccountId() + } + return sourceAccount +} + +func (o *Operation) TOID() int64 { + return toid.New( + int32(o.LedgerHeader.LedgerSeq), + o.TxIndex+1, + o.OpIndex+1, + ).ToInt64() +} diff --git a/exp/lighthorizon/common/transaction.go b/exp/lighthorizon/common/transaction.go new file mode 100644 index 0000000000..104fd3bc6b --- /dev/null +++ b/exp/lighthorizon/common/transaction.go @@ -0,0 +1,70 @@ +package common + +import ( + "encoding/hex" + "errors" + + "github.com/stellar/go/exp/lighthorizon/ingester" + "github.com/stellar/go/network" + "github.com/stellar/go/toid" + "github.com/stellar/go/xdr" +) + +type Transaction struct { + *ingester.LedgerTransaction + LedgerHeader *xdr.LedgerHeader + TxIndex int32 + + NetworkPassphrase string +} + +// type Transaction struct { +// TransactionEnvelope *xdr.TransactionEnvelope +// TransactionResult *xdr.TransactionResult +// } + +func (tx *Transaction) TransactionHash() (string, error) { + if tx.NetworkPassphrase == "" { + return "", errors.New("network passphrase unspecified") + } + + hash, err := network.HashTransactionInEnvelope(tx.Envelope, tx.NetworkPassphrase) + if err != nil { + return "", err + } + + return hex.EncodeToString(hash[:]), nil +} + +func (o *Transaction) SourceAccount() xdr.MuxedAccount { + return o.Envelope.SourceAccount() +} + +func (tx *Transaction) TOID() int64 { + return toid.New( + int32(tx.LedgerHeader.LedgerSeq), + // TOID indexing is 1-based, so the 1st tx comes at position 1, + tx.TxIndex+1, + // but the TOID of a transaction comes BEFORE any operation + 0, + ).ToInt64() +} + +func (tx *Transaction) HasPreconditions() bool { + switch pc := tx.Envelope.Preconditions(); pc.Type { + case xdr.PreconditionTypePrecondNone: + return false + case xdr.PreconditionTypePrecondTime: + return pc.TimeBounds != nil + case xdr.PreconditionTypePrecondV2: + // TODO: 2x check these + return (pc.V2.TimeBounds != nil || + pc.V2.LedgerBounds != nil || + pc.V2.MinSeqNum != nil || + pc.V2.MinSeqAge > 0 || + pc.V2.MinSeqLedgerGap > 0 || + len(pc.V2.ExtraSigners) > 0) + } + + return false +} diff --git a/exp/lighthorizon/http.go b/exp/lighthorizon/http.go new file mode 100644 index 0000000000..e61ad4c716 --- /dev/null +++ b/exp/lighthorizon/http.go @@ -0,0 +1,78 @@ +package main + +import ( + "net/http" + "strconv" + "time" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + + "github.com/stellar/go/exp/lighthorizon/actions" + "github.com/stellar/go/exp/lighthorizon/services" + supportHttp "github.com/stellar/go/support/http" + "github.com/stellar/go/support/render/problem" +) + +func newWrapResponseWriter(w http.ResponseWriter, r *http.Request) middleware.WrapResponseWriter { + mw, ok := w.(middleware.WrapResponseWriter) + if !ok { + mw = middleware.NewWrapResponseWriter(w, r.ProtoMajor) + } + + return mw +} + +func prometheusMiddleware(requestDurationMetric *prometheus.SummaryVec) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + route := supportHttp.GetChiRoutePattern(r) + mw := newWrapResponseWriter(w, r) + + then := time.Now() + next.ServeHTTP(mw, r) + duration := time.Since(then) + + requestDurationMetric.With(prometheus.Labels{ + "status": strconv.FormatInt(int64(mw.Status()), 10), + "method": r.Method, + "route": route, + }).Observe(float64(duration.Seconds())) + }) + } +} + +func lightHorizonHTTPHandler(registry *prometheus.Registry, lightHorizon services.LightHorizon) http.Handler { + requestDurationMetric := prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Namespace: "horizon_lite", Subsystem: "http", Name: "requests_duration_seconds", + Help: "HTTP requests durations, sliding window = 10m", + }, + []string{"status", "method", "route"}, + ) + registry.MustRegister(requestDurationMetric) + + router := chi.NewMux() + router.Use(prometheusMiddleware(requestDurationMetric)) + + router.Route("/accounts/{account_id}", func(r chi.Router) { + r.MethodFunc(http.MethodGet, "/transactions", actions.NewTXByAccountHandler(lightHorizon)) + r.MethodFunc(http.MethodGet, "/operations", actions.NewOpsByAccountHandler(lightHorizon)) + }) + + router.MethodFunc(http.MethodGet, "/", actions.Root(actions.RootResponse{ + Version: HorizonLiteVersion, + // by default, no other fields are known yet + })) + router.MethodFunc(http.MethodGet, "/api", actions.ApiDocs()) + router.Method(http.MethodGet, "/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) + + problem.RegisterHost("") + router.NotFound(func(w http.ResponseWriter, request *http.Request) { + problem.Render(request.Context(), w, problem.NotFound) + }) + + return router +} diff --git a/exp/lighthorizon/http_test.go b/exp/lighthorizon/http_test.go new file mode 100644 index 0000000000..f59e2719d5 --- /dev/null +++ b/exp/lighthorizon/http_test.go @@ -0,0 +1,64 @@ +package main + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stellar/go/exp/lighthorizon/actions" + "github.com/stellar/go/exp/lighthorizon/services" + "github.com/stellar/go/support/render/problem" +) + +func TestUnknownUrl(t *testing.T) { + recorder := httptest.NewRecorder() + request, err := http.NewRequest("GET", "/unknown", nil) + require.NoError(t, err) + + prepareTestHttpHandler().ServeHTTP(recorder, request) + + resp := recorder.Result() + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + + raw, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + + var problem problem.P + err = json.Unmarshal(raw, &problem) + assert.NoError(t, err) + assert.Equal(t, "Resource Missing", problem.Title) + assert.Equal(t, "not_found", problem.Type) +} + +func TestRootResponse(t *testing.T) { + recorder := httptest.NewRecorder() + request, err := http.NewRequest("GET", "/", nil) + require.NoError(t, err) + + prepareTestHttpHandler().ServeHTTP(recorder, request) + + var root actions.RootResponse + raw, err := io.ReadAll(recorder.Result().Body) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(raw, &root)) + require.Equal(t, HorizonLiteVersion, root.Version) +} + +func prepareTestHttpHandler() http.Handler { + mockOperationService := &services.MockOperationService{} + mockTransactionService := &services.MockTransactionService{} + registry := prometheus.NewRegistry() + + lh := services.LightHorizon{ + Operations: mockOperationService, + Transactions: mockTransactionService, + } + + return lightHorizonHTTPHandler(registry, lh) +} diff --git a/exp/lighthorizon/index/Makefile b/exp/lighthorizon/index/Makefile new file mode 100644 index 0000000000..38361d7d37 --- /dev/null +++ b/exp/lighthorizon/index/Makefile @@ -0,0 +1,24 @@ +XDRS = xdr/LightHorizon-types.x + +XDRGEN_COMMIT=3f6808cd161d72474ffbe9eedbd7013de7f92748 + +.PHONY: xdr clean update + +xdr/xdr_generated.go: $(XDRS) + docker run -it --rm -v $$PWD:/wd -w /wd ruby /bin/bash -c '\ + gem install specific_install -v 0.3.7 && \ + gem specific_install https://github.com/stellar/xdrgen.git -b $(XDRGEN_COMMIT) && \ + xdrgen \ + --language go \ + --namespace xdr \ + --output xdr/ \ + $(XDRS)' + ls -lAh + go fmt $@ + +xdr: xdr/xdr_generated.go + +clean: + rm ./xdr/xdr_generated.go || true + +update: clean xdr diff --git a/exp/lighthorizon/index/backend/backend.go b/exp/lighthorizon/index/backend/backend.go new file mode 100644 index 0000000000..580e5f4d6e --- /dev/null +++ b/exp/lighthorizon/index/backend/backend.go @@ -0,0 +1,14 @@ +package index + +import types "github.com/stellar/go/exp/lighthorizon/index/types" + +// TODO: Use a more standardized filesystem-style backend, so we can re-use +// code +type Backend interface { + Flush(map[string]types.NamedIndices) error + FlushAccounts([]string) error + Read(account string) (types.NamedIndices, error) + ReadAccounts() ([]string, error) + FlushTransactions(map[string]*types.TrieIndex) error + ReadTransactions(prefix string) (*types.TrieIndex, error) +} diff --git a/exp/lighthorizon/index/backend/file.go b/exp/lighthorizon/index/backend/file.go new file mode 100644 index 0000000000..062b1efcdb --- /dev/null +++ b/exp/lighthorizon/index/backend/file.go @@ -0,0 +1,214 @@ +package index + +import ( + "bufio" + "compress/gzip" + "io" + "io/fs" + "os" + "path/filepath" + + types "github.com/stellar/go/exp/lighthorizon/index/types" + + "github.com/stellar/go/support/collections/set" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/log" +) + +type FileBackend struct { + dir string + parallel uint32 +} + +// NewFileBackend connects to indices stored at `dir`, creating the directory if one doesn't +// exist, and uses `parallel` to control how many workers to use when flushing to disk. +func NewFileBackend(dir string, parallel uint32) (*FileBackend, error) { + if parallel <= 0 { + parallel = 1 + } + + err := os.MkdirAll(dir, fs.ModeDir|0755) + if err != nil { + log.Errorf("Unable to mkdir %s, %v", dir, err) + return nil, err + } + + return &FileBackend{ + dir: dir, + parallel: parallel, + }, nil +} + +func (s *FileBackend) Flush(indexes map[string]types.NamedIndices) error { + return parallelFlush(s.parallel, indexes, s.writeBatch) +} + +func (s *FileBackend) FlushAccounts(accounts []string) error { + path := filepath.Join(s.dir, "accounts") + + f, err := os.OpenFile(path, os.O_CREATE| + os.O_APPEND| // crucial! since we might flush from various sources + os.O_WRONLY, + 0664) // rw-rw-r-- + + if err != nil { + return errors.Wrapf(err, "failed to open account file at %s", path) + } + + defer f.Close() + + // We write one account at a time because writes that occur within a single + // `write()` syscall are thread-safe. A larger write might be split into + // many calls and thus get interleaved, so we play it safe. + for _, account := range accounts { + f.Write([]byte(account + "\n")) + } + + return nil +} + +func (s *FileBackend) writeBatch(b *batch) error { + if len(b.indexes) == 0 { + return nil + } + + path := filepath.Join(s.dir, b.account[:3], b.account) + + err := os.MkdirAll(filepath.Dir(path), fs.ModeDir|0755) + if err != nil { + log.Errorf("Unable to mkdir %s, %v", filepath.Dir(path), err) + return nil + } + + f, err := os.Create(path) + if err != nil { + log.Errorf("Unable to create %s: %v", path, err) + return nil + } + defer f.Close() + + if _, err := writeGzippedTo(f, b.indexes); err != nil { + log.Errorf("Unable to serialize %s: %v", b.account, err) + return nil + } + + return nil +} + +func (s *FileBackend) FlushTransactions(indexes map[string]*types.TrieIndex) error { + // TODO: Parallelize this + for key, index := range indexes { + path := filepath.Join(s.dir, "tx", key) + + err := os.MkdirAll(filepath.Dir(path), fs.ModeDir|0755) + if err != nil { + log.Errorf("Unable to mkdir %s, %v", filepath.Dir(path), err) + continue + } + + f, err := os.Create(path) + if err != nil { + log.Errorf("Unable to create %s: %v", path, err) + continue + } + + zw := gzip.NewWriter(f) + if _, err := index.WriteTo(zw); err != nil { + log.Errorf("Unable to serialize %s: %v", path, err) + f.Close() + continue + } + + if err := zw.Close(); err != nil { + log.Errorf("Unable to serialize %s: %v", path, err) + f.Close() + continue + } + + if err := f.Close(); err != nil { + log.Errorf("Unable to save %s: %v", path, err) + } + } + return nil +} + +func (s *FileBackend) Read(account string) (types.NamedIndices, error) { + log.Debugf("Opening index: %s", account) + b, err := os.Open(filepath.Join(s.dir, account[:3], account)) + if err != nil { + return nil, err + } + defer b.Close() + + indexes, _, err := readGzippedFrom(bufio.NewReader(b)) + if err != nil { + log.Errorf("Unable to parse %s: %v", account, err) + return nil, os.ErrNotExist + } + return indexes, nil +} + +func (s *FileBackend) ReadAccounts() ([]string, error) { + path := filepath.Join(s.dir, "accounts") + log.Debugf("Opening accounts list at %s", path) + + f, err := os.Open(path) + if err != nil { + return nil, errors.Wrapf(err, "failed to open %s", path) + } + + const gAddressSize = 56 + + // We ballpark the capacity assuming all of the values being G-addresses. + preallocationSize := 100 * gAddressSize // default to 100 lines + info, err := os.Stat(path) + if err == nil { // we can still safely continue w/ errors + // Note that this will never be too large, but may be too small. + preallocationSize = int(info.Size()) / (gAddressSize + 1) // +1 for \n + } + accountMap := set.NewSet[string](preallocationSize) + accounts := make([]string, 0, preallocationSize) + + reader := bufio.NewReaderSize(f, 100*gAddressSize) // reasonable buffer size + for { + line, err := reader.ReadString(byte('\n')) + if err == io.EOF { + break + } else if err != nil { + return accounts, errors.Wrapf(err, "failed to read %s", path) + } + + account := line[:len(line)-1] // trim newline + + // The account list is very unlikely to be unique (especially if it was made + // w/ parallel flushes), so let's ensure that that's the case. + if !accountMap.Contains(account) { + accountMap.Add(account) + accounts = append(accounts, account) + } + } + + return accounts, nil +} + +func (s *FileBackend) ReadTransactions(prefix string) (*types.TrieIndex, error) { + log.Debugf("Opening index: %s", prefix) + b, err := os.Open(filepath.Join(s.dir, "tx", prefix)) + if err != nil { + return nil, err + } + defer b.Close() + zr, err := gzip.NewReader(b) + if err != nil { + log.Errorf("Unable to parse %s: %v", prefix, err) + return nil, os.ErrNotExist + } + defer zr.Close() + var index types.TrieIndex + _, err = index.ReadFrom(zr) + if err != nil { + log.Errorf("Unable to parse %s: %v", prefix, err) + return nil, os.ErrNotExist + } + return &index, nil +} diff --git a/exp/lighthorizon/index/backend/file_test.go b/exp/lighthorizon/index/backend/file_test.go new file mode 100644 index 0000000000..6197f7b5c3 --- /dev/null +++ b/exp/lighthorizon/index/backend/file_test.go @@ -0,0 +1,43 @@ +package index + +import ( + "math/rand" + "testing" + + "github.com/stellar/go/keypair" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/require" +) + +func TestSimpleFileStore(t *testing.T) { + tmpDir := t.TempDir() + + // Create a large (beyond a single chunk) list of arbitrary accounts, some + // regular and some muxed. + accountList := make([]string, 123) + for i := range accountList { + var err error + var muxed xdr.MuxedAccount + address := keypair.MustRandom().Address() + + if rand.Intn(2) == 1 { + muxed, err = xdr.MuxedAccountFromAccountId(address, 12345678) + require.NoErrorf(t, err, "shouldn't happen") + } else { + muxed = xdr.MustMuxedAddress(address) + } + + accountList[i] = muxed.Address() + } + + require.Len(t, accountList, 123) + + file, err := NewFileBackend(tmpDir, 1) + require.NoError(t, err) + + require.NoError(t, file.FlushAccounts(accountList)) + + accounts, err := file.ReadAccounts() + require.NoError(t, err) + require.Equal(t, accountList, accounts) +} diff --git a/exp/lighthorizon/index/backend/gzip.go b/exp/lighthorizon/index/backend/gzip.go new file mode 100644 index 0000000000..63c8e332c2 --- /dev/null +++ b/exp/lighthorizon/index/backend/gzip.go @@ -0,0 +1,74 @@ +package index + +import ( + "bytes" + "compress/gzip" + "errors" + "io" + + types "github.com/stellar/go/exp/lighthorizon/index/types" +) + +func writeGzippedTo(w io.Writer, indexes types.NamedIndices) (int64, error) { + zw := gzip.NewWriter(w) + + var n int64 + for id, index := range indexes { + zw.Name = id + nWrote, err := io.Copy(zw, index.Buffer()) + n += nWrote + if err != nil { + return n, err + } + + if err := zw.Close(); err != nil { + return n, err + } + + zw.Reset(w) + } + + return n, nil +} + +func readGzippedFrom(r io.Reader) (types.NamedIndices, int64, error) { + if _, ok := r.(io.ByteReader); !ok { + return nil, 0, errors.New("reader *must* implement ByteReader") + } + + zr, err := gzip.NewReader(r) + if err != nil { + return nil, 0, err + } + + indexes := types.NamedIndices{} + var buf bytes.Buffer + var n int64 + for { + zr.Multistream(false) + + nRead, err := io.Copy(&buf, zr) + n += nRead + if err != nil { + return nil, n, err + } + + ind, err := types.NewBitmapIndex(buf.Bytes()) + if err != nil { + return nil, n, err + } + + indexes[zr.Name] = ind + + buf.Reset() + + err = zr.Reset(r) + if err == io.EOF { + break + } else if err != nil { + return nil, n, err + } + } + + return indexes, n, zr.Close() +} diff --git a/exp/lighthorizon/index/backend/gzip_test.go b/exp/lighthorizon/index/backend/gzip_test.go new file mode 100644 index 0000000000..730e13185d --- /dev/null +++ b/exp/lighthorizon/index/backend/gzip_test.go @@ -0,0 +1,61 @@ +package index + +import ( + "bufio" + "bytes" + "math/rand" + "os" + "path/filepath" + "testing" + + types "github.com/stellar/go/exp/lighthorizon/index/types" + "github.com/stretchr/testify/require" +) + +func TestGzipRoundtrip(t *testing.T) { + index := &types.BitmapIndex{} + anotherIndex := &types.BitmapIndex{} + for i := 0; i < 100+rand.Intn(1000); i++ { + index.SetActive(uint32(rand.Intn(10_000))) + anotherIndex.SetActive(uint32(rand.Intn(10_000))) + } + + indices := types.NamedIndices{ + "a": index, + "short/name": anotherIndex, + "slightlyLonger/name": index, + } + + var buf bytes.Buffer + wroteBytes, err := writeGzippedTo(&buf, indices) + require.NoError(t, err) + require.Greater(t, wroteBytes, int64(0)) + + gz := filepath.Join(t.TempDir(), "test.gzip") + require.NoError(t, os.WriteFile(gz, buf.Bytes(), 0644)) + f, err := os.Open(gz) + require.NoError(t, err) + defer f.Close() + + // Ensure that reading directly from a file errors out. + _, _, err = readGzippedFrom(f) + require.Error(t, err) + + read, readBytes, err := readGzippedFrom(bufio.NewReader(f)) + require.NoError(t, err) + require.Greater(t, readBytes, int64(0)) + + require.Equal(t, indices, read) + require.Equal(t, wroteBytes, readBytes) + require.Len(t, read, len(indices)) + + for name, index := range indices { + raw1, err := index.ToXDR().MarshalBinary() + require.NoError(t, err) + + raw2, err := read[name].ToXDR().MarshalBinary() + require.NoError(t, err) + + require.Equal(t, raw1, raw2) + } +} diff --git a/exp/lighthorizon/index/backend/parallel_flush.go b/exp/lighthorizon/index/backend/parallel_flush.go new file mode 100644 index 0000000000..6f65bedc42 --- /dev/null +++ b/exp/lighthorizon/index/backend/parallel_flush.go @@ -0,0 +1,73 @@ +package index + +import ( + "sync" + "sync/atomic" + "time" + + types "github.com/stellar/go/exp/lighthorizon/index/types" + "github.com/stellar/go/support/log" +) + +type batch struct { + account string + indexes types.NamedIndices +} + +type flushBatch func(b *batch) error + +func parallelFlush(parallel uint32, allIndexes map[string]types.NamedIndices, f flushBatch) error { + var wg sync.WaitGroup + + batches := make(chan *batch, parallel) + + wg.Add(1) + go func() { + // forces this async func to be waited on also, otherwise the outer + // method returns before this finishes. + defer wg.Done() + + for account, indexes := range allIndexes { + batches <- &batch{ + account: account, + indexes: indexes, + } + } + + if len(allIndexes) == 0 { + close(batches) + } + }() + + written := uint64(0) + for i := uint32(0); i < parallel; i++ { + wg.Add(1) + go func(workerNum uint32) { + defer wg.Done() + for batch := range batches { + if err := f(batch); err != nil { + log.Errorf("Error occurred writing batch: %v, retrying...", err) + time.Sleep(50 * time.Millisecond) + batches <- batch + continue + } + + nwritten := atomic.AddUint64(&written, 1) + if nwritten%1234 == 0 { + log.WithField("worker", workerNum). + Infof("Writing indices... %d/%d (%.2f%%)", + nwritten, len(allIndexes), + (float64(nwritten)/float64(len(allIndexes)))*100) + } + + if nwritten == uint64(len(allIndexes)) { + close(batches) + } + } + }(i) + } + + wg.Wait() + + return nil +} diff --git a/exp/lighthorizon/index/backend/s3.go b/exp/lighthorizon/index/backend/s3.go new file mode 100644 index 0000000000..a4f5a7e751 --- /dev/null +++ b/exp/lighthorizon/index/backend/s3.go @@ -0,0 +1,220 @@ +package index + +import ( + "bytes" + "compress/gzip" + "os" + "path/filepath" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/log" + + types "github.com/stellar/go/exp/lighthorizon/index/types" +) + +type S3Backend struct { + s3Session *session.Session + downloader *s3manager.Downloader + uploader *s3manager.Uploader + parallel uint32 + pathPrefix string + bucket string +} + +func NewS3Backend(awsConfig *aws.Config, bucket string, pathPrefix string, parallel uint32) (*S3Backend, error) { + s3Session, err := session.NewSession(awsConfig) + if err != nil { + return nil, err + } + + return &S3Backend{ + s3Session: s3Session, + downloader: s3manager.NewDownloader(s3Session), + uploader: s3manager.NewUploader(s3Session), + parallel: parallel, + pathPrefix: pathPrefix, + bucket: bucket, + }, nil +} + +func (s *S3Backend) FlushAccounts(accounts []string) error { + var buf bytes.Buffer + accountsString := strings.Join(accounts, "\n") + _, err := buf.WriteString(accountsString) + if err != nil { + return err + } + + path := filepath.Join(s.pathPrefix, "accounts") + + _, err = s.uploader.Upload(&s3manager.UploadInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(path), + Body: &buf, + }) + if err != nil { + return err + } + + return nil +} + +func (s *S3Backend) Flush(indexes map[string]types.NamedIndices) error { + return parallelFlush(s.parallel, indexes, s.writeBatch) +} + +func (s *S3Backend) writeBatch(b *batch) error { + // TODO: re-use buffers in a pool + var buf bytes.Buffer + if _, err := writeGzippedTo(&buf, b.indexes); err != nil { + // TODO: Should we retry or what here?? + return errors.Wrapf(err, "unable to serialize %s", b.account) + } + + path := s.path(b.account) + + _, err := s.uploader.Upload(&s3manager.UploadInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(path), + Body: &buf, + }) + if err != nil { + return errors.Wrapf(err, "unable to upload %s", b.account) + } + + return nil +} + +func (s *S3Backend) FlushTransactions(indexes map[string]*types.TrieIndex) error { + // TODO: Parallelize this + var buf bytes.Buffer + for key, index := range indexes { + buf.Reset() + path := filepath.Join(s.pathPrefix, "tx", key) + + zw := gzip.NewWriter(&buf) + if _, err := index.WriteTo(zw); err != nil { + log.Errorf("Unable to serialize %s: %v", path, err) + continue + } + + if err := zw.Close(); err != nil { + log.Errorf("Unable to serialize %s: %v", path, err) + continue + } + + _, err := s.uploader.Upload(&s3manager.UploadInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(path), + Body: &buf, + }) + if err != nil { + log.Errorf("Unable to upload %s: %v", path, err) + // TODO: retries + continue + } + } + return nil +} + +func (s *S3Backend) ReadAccounts() ([]string, error) { + log.Debugf("Downloading accounts list") + b := &aws.WriteAtBuffer{} + path := filepath.Join(s.pathPrefix, "accounts") + n, err := s.downloader.Download(b, &s3.GetObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(path), + }) + if err != nil { + if aerr, ok := err.(awserr.Error); ok && aerr.Code() == s3.ErrCodeNoSuchKey { + return nil, os.ErrNotExist + } + return nil, errors.Wrapf(err, "Unable to download accounts list") + } + if n == 0 { + return nil, os.ErrNotExist + } + body := b.Bytes() + accounts := strings.Split(string(body), "\n") + return accounts, nil +} + +func (s *S3Backend) path(account string) string { + return filepath.Join(s.pathPrefix, account[:10], account) +} + +func (s *S3Backend) Read(account string) (types.NamedIndices, error) { + // Check if index exists in S3 + log.Debugf("Downloading index: %s", account) + var err error + for i := 0; i < 10; i++ { + b := &aws.WriteAtBuffer{} + path := s.path(account) + var n int64 + n, err = s.downloader.Download(b, &s3.GetObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(path), + }) + if err != nil { + if aerr, ok := err.(awserr.Error); ok && aerr.Code() == s3.ErrCodeNoSuchKey { + return nil, os.ErrNotExist + } + err = errors.Wrapf(err, "Unable to download %s", account) + time.Sleep(100 * time.Millisecond) + continue + } + if n == 0 { + return nil, os.ErrNotExist + } + var indexes map[string]*types.BitmapIndex + indexes, _, err = readGzippedFrom(bytes.NewReader(b.Bytes())) + if err != nil { + log.Errorf("Unable to parse %s: %v", account, err) + return nil, os.ErrNotExist + } + return indexes, nil + } + + return nil, err +} + +func (s *S3Backend) ReadTransactions(prefix string) (*types.TrieIndex, error) { + // Check if index exists in S3 + log.Debugf("Downloading index: %s", prefix) + b := &aws.WriteAtBuffer{} + path := filepath.Join(s.pathPrefix, "tx", prefix) + n, err := s.downloader.Download(b, &s3.GetObjectInput{ + Bucket: aws.String(s.bucket), + Key: aws.String(path), + }) + if err != nil { + if aerr, ok := err.(awserr.Error); ok && aerr.Code() == s3.ErrCodeNoSuchKey { + return nil, os.ErrNotExist + } + return nil, errors.Wrapf(err, "Unable to download %s", prefix) + } + if n == 0 { + return nil, os.ErrNotExist + } + zr, err := gzip.NewReader(bytes.NewReader(b.Bytes())) + if err != nil { + log.Errorf("Unable to parse %s: %v", prefix, err) + return nil, os.ErrNotExist + } + defer zr.Close() + + var index types.TrieIndex + _, err = index.ReadFrom(zr) + if err != nil { + log.Errorf("Unable to parse %s: %v", prefix, err) + return nil, os.ErrNotExist + } + return &index, nil +} diff --git a/exp/lighthorizon/index/builder.go b/exp/lighthorizon/index/builder.go new file mode 100644 index 0000000000..324783b4f0 --- /dev/null +++ b/exp/lighthorizon/index/builder.go @@ -0,0 +1,366 @@ +package index + +import ( + "context" + "fmt" + "io" + "math" + "os" + "sync" + "sync/atomic" + "time" + + "golang.org/x/sync/errgroup" + + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/ingest" + "github.com/stellar/go/ingest/ledgerbackend" + "github.com/stellar/go/metaarchive" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/log" + "github.com/stellar/go/support/storage" + "github.com/stellar/go/xdr" +) + +func BuildIndices( + ctx context.Context, + sourceUrl string, // where is raw txmeta coming from? + targetUrl string, // where should the resulting indices go? + networkPassphrase string, + ledgerRange historyarchive.Range, // inclusive + modules []string, + workerCount int, +) (*IndexBuilder, error) { + L := log.Ctx(ctx).WithField("service", "builder") + + indexStore, err := ConnectWithConfig(StoreConfig{ + URL: targetUrl, + Workers: uint32(workerCount), + Log: L.WithField("subservice", "index"), + }) + if err != nil { + return nil, err + } + + // We use historyarchive as a backend here just to abstract away dealing + // with the filesystem directly. + source, err := historyarchive.ConnectBackend( + sourceUrl, + storage.ConnectOptions{ + Context: ctx, + S3Region: "us-east-1", + }, + ) + if err != nil { + return nil, err + } + + metaArchive := metaarchive.NewMetaArchive(source) + + ledgerBackend := ledgerbackend.NewHistoryArchiveBackend(metaArchive) + + if ledgerRange.High == 0 { + var backendErr error + ledgerRange.High, backendErr = ledgerBackend.GetLatestLedgerSequence(ctx) + if backendErr != nil { + return nil, backendErr + } + } + + if ledgerRange.High < ledgerRange.Low { + return nil, fmt.Errorf("invalid ledger range: %s", ledgerRange.String()) + } + + ledgerCount := 1 + (ledgerRange.High - ledgerRange.Low) // +1 bc inclusive + parallel := int(max(1, uint32(workerCount))) + + startTime := time.Now() + L.Infof("Creating indices for ledger range: [%d, %d] (%d ledgers)", + ledgerRange.Low, ledgerRange.High, ledgerCount) + L.Infof("Using %d workers", parallel) + + // Create a bunch of workers that process ledgers a checkpoint range at a + // time (better than a ledger at a time to minimize flushes). + wg, ctx := errgroup.WithContext(ctx) + ch := make(chan historyarchive.Range, parallel) + + indexBuilder := NewIndexBuilder(indexStore, metaArchive, networkPassphrase) + for _, part := range modules { + switch part { + case "transactions": + indexBuilder.RegisterModule(ProcessTransaction) + case "accounts": + indexBuilder.RegisterModule(ProcessAccountsByCheckpoint) + case "accounts_by_ledger": + indexBuilder.RegisterModule(ProcessAccountsByLedger) + case "accounts_unbacked": + indexBuilder.RegisterModule(ProcessAccountsByCheckpointWithoutBackend) + indexStore.ClearMemory(false) + case "accounts_by_ledger_unbacked": + indexBuilder.RegisterModule(ProcessAccountsByLedgerWithoutBackend) + indexStore.ClearMemory(false) + default: + return indexBuilder, fmt.Errorf("unknown module '%s'", part) + } + } + + // Submit the work to the channels, breaking up the range into individual + // checkpoint ranges. + checkpoints := historyarchive.NewCheckpointManager(0) + go func() { + for ledger := range ledgerRange.GenerateCheckpoints(checkpoints) { + chunk := checkpoints.GetCheckpointRange(ledger) + chunk.High = min(chunk.High, ledgerRange.High) // don't exceed upper bound + chunk.Low = max(chunk.Low, ledgerRange.Low) // nor the lower bound + + ch <- chunk + } + + close(ch) + }() + + processed := uint64(0) + for i := 0; i < parallel; i++ { + wg.Go(func() error { + for ledgerRange := range ch { + count := (ledgerRange.High - ledgerRange.Low) + 1 + L.Debugf("Working on checkpoint range [%d, %d] (%d ledgers)", + ledgerRange.Low, ledgerRange.High, count) + + if err := indexBuilder.Build(ctx, ledgerRange); err != nil { + return errors.Wrapf(err, + "building indices for ledger range [%d, %d] failed", + ledgerRange.Low, ledgerRange.High) + } + + nprocessed := atomic.AddUint64(&processed, uint64(count)) + if nprocessed%1234 == 0 { + PrintProgress("Reading ledgers", nprocessed, uint64(ledgerCount), startTime) + } + + // Upload indices once every 10 checkpoints to save memory + if nprocessed%(10*uint64(checkpoints.GetCheckpointFrequency())) == 0 { + if err := indexStore.Flush(); err != nil { + return errors.Wrap(err, "flushing indices failed") + } + } + } + return nil + }) + } + + if err := wg.Wait(); err != nil { + return indexBuilder, errors.Wrap(err, "one or more workers failed") + } + + PrintProgress("Reading ledgers", processed, uint64(ledgerCount), startTime) + + L.Infof("Processed %d ledgers via %d workers", processed, parallel) + L.Infof("Uploading indices to %s", targetUrl) + if err := indexStore.Flush(); err != nil { + return indexBuilder, errors.Wrap(err, "flushing indices failed") + } + + // Assertion for testing + if processed != uint64(ledgerCount) { + L.Warnf("processed %d but expected %d", processed, ledgerCount) + } + + return indexBuilder, nil +} + +// Module is a way to process ingested data and shove it into an index store. +type Module func( + indexStore Store, + ledger xdr.LedgerCloseMeta, + transaction ingest.LedgerTransaction, +) error + +// IndexBuilder contains everything needed to build indices from ledger ranges. +type IndexBuilder struct { + store Store + metaArchive metaarchive.MetaArchive + networkPassphrase string + + lastBuiltLedgerWriteLock sync.Mutex + lastBuiltLedger uint32 + + modules []Module +} + +func NewIndexBuilder( + indexStore Store, + metaArchive metaarchive.MetaArchive, + networkPassphrase string, +) *IndexBuilder { + return &IndexBuilder{ + store: indexStore, + metaArchive: metaArchive, + networkPassphrase: networkPassphrase, + } +} + +// RegisterModule adds a module to process every given ledger. It is not +// threadsafe and all calls should be made *before* any calls to `Build`. +func (builder *IndexBuilder) RegisterModule(module Module) { + builder.modules = append(builder.modules, module) +} + +// RunModules executes all of the registered modules on the given ledger. +func (builder *IndexBuilder) RunModules( + ledger xdr.LedgerCloseMeta, + tx ingest.LedgerTransaction, +) error { + for _, module := range builder.modules { + if err := module(builder.store, ledger, tx); err != nil { + return err + } + } + + return nil +} + +// Build sequentially creates indices for each ledger in the given range based +// on the registered modules. +// +// TODO: We can probably optimize this by doing GetLedger in parallel with the +// ingestion & index building, since the network will be idle during the latter +// portion. +func (builder *IndexBuilder) Build(ctx context.Context, ledgerRange historyarchive.Range) error { + for ledgerSeq := ledgerRange.Low; ledgerSeq <= ledgerRange.High; ledgerSeq++ { + ledger, err := builder.metaArchive.GetLedger(ctx, ledgerSeq) + if err != nil { + if !os.IsNotExist(err) { + log.Errorf("error getting ledger %d: %v", ledgerSeq, err) + } + return err + } + + reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta( + builder.networkPassphrase, *ledger.V0) + if err != nil { + return err + } + + for { + tx, err := reader.Read() + if err == io.EOF { + break + } else if err != nil { + return err + } + + if err := builder.RunModules(*ledger.V0, tx); err != nil { + return err + } + } + } + + builder.lastBuiltLedgerWriteLock.Lock() + defer builder.lastBuiltLedgerWriteLock.Unlock() + builder.lastBuiltLedger = max(builder.lastBuiltLedger, ledgerRange.High) + + return nil +} + +func (builder *IndexBuilder) Watch(ctx context.Context) error { + latestLedger, err := builder.metaArchive.GetLatestLedgerSequence(ctx) + if err != nil { + log.Errorf("Failed to retrieve latest ledger: %v", err) + return err + } + nextLedger := builder.lastBuiltLedger + 1 + + log.Infof("Catching up to latest ledger: (%d, %d]", nextLedger, latestLedger) + if err = builder.Build(ctx, historyarchive.Range{ + Low: nextLedger, + High: latestLedger, + }); err != nil { + log.Errorf("Initial catchup failed: %v", err) + } + + for { + nextLedger = builder.lastBuiltLedger + 1 + log.Infof("Awaiting next ledger (%d)", nextLedger) + + // To keep the MVP simple, let's just naively poll the backend until the + // ledger we want becomes available. + // + // Refer to this thread [1] for a deeper brain dump on why we're + // preferring this over doing proper filesystem monitoring (e.g. + // fsnotify for on-disk). Essentially, supporting this for every + // possible index backend is a non-trivial amount of work with an + // uncertain payoff. + // + // [1]: https://stellarfoundation.slack.com/archives/C02B04RMK/p1654903342555669 + + // We sleep with linear backoff starting with 6s. Ledgers get posted + // every 5-7s on average, but to be extra careful, let's give it a full + // minute before we give up entirely. + timedCtx, cancel := context.WithTimeout(ctx, 60*time.Second) + defer cancel() + + sleepTime := (6 * time.Second) + outer: + for { + time.Sleep(sleepTime) + select { + case <-timedCtx.Done(): + return errors.Wrap(timedCtx.Err(), "awaiting next ledger failed") + + default: + buildErr := builder.Build(timedCtx, historyarchive.Range{ + Low: nextLedger, + High: nextLedger, + }) + if buildErr == nil { + break outer + } + + if os.IsNotExist(buildErr) { + sleepTime += (time.Second * 2) + continue + } + + return errors.Wrap(buildErr, "awaiting next ledger failed") + } + } + } +} + +func PrintProgress(prefix string, done, total uint64, startTime time.Time) { + progress := float64(done) / float64(total) + elapsed := time.Since(startTime) + + // Approximate based on how many stuff is left to do and how long this much + // progress took, e.g. if 4/10 took 2s then 6/10 will "take" 3s (though this + // assumes consistent load). + remaining := (float64(elapsed) / float64(done)) * float64(total-done) + + var remainingStr string + if math.IsInf(remaining, 0) || math.IsNaN(remaining) { + remainingStr = "unknown" + } else { + remainingStr = time.Duration(remaining).Round(time.Millisecond).String() + } + + log.Infof("%s - %.1f%% (%d/%d) - elapsed: %s, remaining: ~%s", prefix, + 100*progress, done, total, + elapsed.Round(time.Millisecond), + remainingStr, + ) +} + +func min(a, b uint32) uint32 { + if a < b { + return a + } + return b +} + +func max(a, b uint32) uint32 { + if a > b { + return a + } + return b +} diff --git a/exp/lighthorizon/index/cmd/batch/doc.go b/exp/lighthorizon/index/cmd/batch/doc.go new file mode 100644 index 0000000000..70e55009d5 --- /dev/null +++ b/exp/lighthorizon/index/cmd/batch/doc.go @@ -0,0 +1,52 @@ +// Package batch provides two commands: map and reduce that can be run in AWS +// Batch to generate indexes for occurences of accounts in each checkpoint. +// +// map step is using AWS_BATCH_JOB_ARRAY_INDEX env variable provided by AWS +// Batch to cut all checkpoint history into smaller chunks, each processed by a +// single map batch job (and by multiple parallel workers in a single job). A +// single job simply creates indexes for a given range of checkpoints and save +// indexes and all accounts seen in a given range (FlushAccounts method) to a +// job folder (job_X, X = 0, 1, 2, 3, ...) in S3. +// +// network history split into chunks: +// [ | | | | | | | | | | | | | | | | | | | | | ] +// ---- +// / \ +// / \ +// / \ +// [..........] <- each chunk consists of checkpoints +// | +// . - each checkpoint is processed by a free +// worker (go routine) +// +// reduce step is responsible for merging all indexes created in map step into a +// final indexes for each account and for entire network history. Each reduce +// job goes through all map job results (0..MAP_JOBS) and reads all accounts +// processed in a given map job. Then for each account it merges indexes from +// all map jobs. Each reduce job maintains `doneAccounts` map because if a given +// account index was processed earlier it should be skipped instead of being +// processed again. Each reduce job also runs multiple parallel workers. Finally +// the method that is used to determine if the following (job, worker) should +// process a given account is using a 64-bit hash of account ID. The hash is +// split into two 32-bit parts: left and right. If the left part modulo +// REDUCE_JOBS is equal the job index and the right part modulo a number of +// parallel workers is equal the worker index then the account is processed. +// Otherwise it's skipped (and will be picked by another (job, worker) pair). +// +// map step results saved in S3: +// x x x x x x x x x x x x x x x x x x x x x x x x x x x x +// | +// ㄴ job0/accounts <- each job results contains a list of accounts +// | processed by a given job... +// | +// ㄴ job0/... <- ...and partial indexes +// +// hash(account_id) => XXXX YYYY <- 64 bit hash of account id is calculated +// +// if XXXX % REDUCE_JOBS == JOB_ID and YYYY % WORKERS_COUNT = WORKER_ID +// then process a given account by merging all indexes of a given account +// in all map step results, then mark account as done so if the account +// is seen again it will be skiped, +// +// else: skip the account. +package batch diff --git a/exp/lighthorizon/index/cmd/batch/map/main.go b/exp/lighthorizon/index/cmd/batch/map/main.go new file mode 100644 index 0000000000..384e99ee80 --- /dev/null +++ b/exp/lighthorizon/index/cmd/batch/map/main.go @@ -0,0 +1,144 @@ +package main + +import ( + "context" + "fmt" + "os" + "runtime" + "strconv" + "strings" + + "github.com/stellar/go/exp/lighthorizon/index" + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/network" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/log" +) + +type BatchConfig struct { + historyarchive.Range + TxMetaSourceUrl string + IndexTargetUrl string + NetworkPassphrase string +} + +const ( + batchSizeEnv = "BATCH_SIZE" + jobIndexEnvName = "JOB_INDEX_ENV" + firstCheckpointEnv = "FIRST_CHECKPOINT" + txmetaSourceUrlEnv = "TXMETA_SOURCE" + indexTargetUrlEnv = "INDEX_TARGET" + workerCountEnv = "WORKER_COUNT" + networkPassphraseEnv = "NETWORK_PASSPHRASE" + modulesEnv = "MODULES" +) + +func NewBatchConfig() (*BatchConfig, error) { + indexTargetRootUrl := os.Getenv(indexTargetUrlEnv) + if indexTargetRootUrl == "" { + return nil, errors.New("required parameter: " + indexTargetUrlEnv) + } + + jobIndexEnv := os.Getenv(jobIndexEnvName) + if jobIndexEnv == "" { + return nil, errors.New("env variable can't be empty " + jobIndexEnvName) + } + jobIndex, err := strconv.ParseUint(os.Getenv(jobIndexEnv), 10, 32) + if err != nil { + return nil, errors.Wrap(err, "invalid parameter "+jobIndexEnv) + } + + firstCheckpoint, err := strconv.ParseUint(os.Getenv(firstCheckpointEnv), 10, 32) + if err != nil { + return nil, errors.Wrap(err, "invalid parameter "+firstCheckpointEnv) + } + + checkpoints := historyarchive.NewCheckpointManager(0) + if !checkpoints.IsCheckpoint(uint32(firstCheckpoint - 1)) { + return nil, fmt.Errorf( + "%s (%d) must be the first ledger in a checkpoint range", + firstCheckpointEnv, firstCheckpoint) + } + + batchSize, err := strconv.ParseUint(os.Getenv(batchSizeEnv), 10, 32) + if err != nil { + return nil, errors.Wrap(err, "invalid parameter "+batchSizeEnv) + } else if batchSize%uint64(checkpoints.GetCheckpointFrequency()) != 0 { + return nil, fmt.Errorf( + "%s (%d) must be a multiple of checkpoint frequency (%d)", + batchSizeEnv, batchSize, checkpoints.GetCheckpointFrequency()) + } + + txmetaSourceUrl := os.Getenv(txmetaSourceUrlEnv) + if txmetaSourceUrl == "" { + return nil, errors.New("required parameter " + txmetaSourceUrlEnv) + } + + firstLedger := uint32(firstCheckpoint + batchSize*jobIndex) + lastLedger := firstLedger + uint32(batchSize) - 1 + return &BatchConfig{ + Range: historyarchive.Range{Low: firstLedger, High: lastLedger}, + TxMetaSourceUrl: txmetaSourceUrl, + IndexTargetUrl: fmt.Sprintf("%s%cjob_%d", indexTargetRootUrl, os.PathSeparator, jobIndex), + }, nil +} + +func main() { + log.SetLevel(log.InfoLevel) + // log.SetLevel(log.DebugLevel) + + batch, err := NewBatchConfig() + if err != nil { + panic(err) + } + + var workerCount int + workerCountStr := os.Getenv(workerCountEnv) + if workerCountStr == "" { + workerCount = runtime.NumCPU() + } else { + workerCountParsed, innerErr := strconv.ParseUint(workerCountStr, 10, 8) + if innerErr != nil { + panic(errors.Wrapf(innerErr, + "invalid worker count parameter (%s)", workerCountStr)) + } + workerCount = int(workerCountParsed) + } + + networkPassphrase := os.Getenv(networkPassphraseEnv) + switch networkPassphrase { + case "": + log.Warnf("%s not specified, defaulting to 'testnet'", networkPassphraseEnv) + fallthrough + case "testnet": + networkPassphrase = network.TestNetworkPassphrase + case "pubnet": + networkPassphrase = network.PublicNetworkPassphrase + default: + log.Warnf("%s is not a recognized shortcut ('pubnet' or 'testnet')", + networkPassphraseEnv) + } + log.Infof("Using network passphrase '%s'", networkPassphrase) + + parsedModules := []string{} + if modules := os.Getenv(modulesEnv); modules == "" { + parsedModules = append(parsedModules, "accounts_unbacked") + } else { + parsedModules = append(parsedModules, strings.Split(modules, ",")...) + } + + log.Infof("Uploading ledger range [%d, %d] to %s", + batch.Range.Low, batch.Range.High, batch.IndexTargetUrl) + + if _, err := index.BuildIndices( + context.Background(), + batch.TxMetaSourceUrl, + batch.IndexTargetUrl, + networkPassphrase, + batch.Range, + parsedModules, + workerCount, + ); err != nil { + panic(err) + } +} diff --git a/exp/lighthorizon/index/cmd/batch/reduce/main.go b/exp/lighthorizon/index/cmd/batch/reduce/main.go new file mode 100644 index 0000000000..bff9f8216a --- /dev/null +++ b/exp/lighthorizon/index/cmd/batch/reduce/main.go @@ -0,0 +1,389 @@ +package main + +import ( + "encoding/hex" + "hash/fnv" + "os" + "strconv" + "strings" + "sync" + + "github.com/stellar/go/exp/lighthorizon/index" + types "github.com/stellar/go/exp/lighthorizon/index/types" + "github.com/stellar/go/support/collections/set" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/log" +) + +const ( + ACCOUNT_FLUSH_FREQUENCY = 200 + // arbitrary default, should we use runtime.NumCPU()? + DEFAULT_WORKER_COUNT = 2 +) + +type ReduceConfig struct { + JobIndex uint32 + MapJobCount uint32 + ReduceJobCount uint32 + IndexTarget string + IndexRootSource string + + Workers uint32 +} + +func ReduceConfigFromEnvironment() (*ReduceConfig, error) { + const ( + mapJobsEnv = "MAP_JOB_COUNT" + reduceJobsEnv = "REDUCE_JOB_COUNT" + workerCountEnv = "WORKER_COUNT" + jobIndexEnvName = "JOB_INDEX_ENV" + indexRootSourceEnv = "INDEX_SOURCE_ROOT" + indexTargetEnv = "INDEX_TARGET" + ) + + jobIndexEnv := strings.TrimSpace(os.Getenv(jobIndexEnvName)) + if jobIndexEnv == "" { + return nil, errors.New("env variable can't be empty " + jobIndexEnvName) + } + + jobIndex, err := strconv.ParseUint(strings.TrimSpace(os.Getenv(jobIndexEnv)), 10, 32) + if err != nil { + return nil, errors.Wrap(err, "invalid parameter "+jobIndexEnv) + } + mapJobCount, err := strconv.ParseUint(strings.TrimSpace(os.Getenv(mapJobsEnv)), 10, 32) + if err != nil { + return nil, errors.Wrap(err, "invalid parameter "+mapJobsEnv) + } + reduceJobCount, err := strconv.ParseUint(strings.TrimSpace(os.Getenv(reduceJobsEnv)), 10, 32) + if err != nil { + return nil, errors.Wrap(err, "invalid parameter "+reduceJobsEnv) + } + + workersStr := strings.TrimSpace(os.Getenv(workerCountEnv)) + if workersStr == "" { + workersStr = strconv.FormatUint(DEFAULT_WORKER_COUNT, 10) + } + workers, err := strconv.ParseUint(workersStr, 10, 32) + if err != nil { + return nil, errors.Wrap(err, "invalid parameter "+workerCountEnv) + } + + indexTarget := strings.TrimSpace(os.Getenv(indexTargetEnv)) + if indexTarget == "" { + return nil, errors.New("required parameter missing " + indexTargetEnv) + } + + indexRootSource := strings.TrimSpace(os.Getenv(indexRootSourceEnv)) + if indexRootSource == "" { + return nil, errors.New("required parameter missing " + indexRootSourceEnv) + } + + return &ReduceConfig{ + JobIndex: uint32(jobIndex), + MapJobCount: uint32(mapJobCount), + ReduceJobCount: uint32(reduceJobCount), + Workers: uint32(workers), + IndexTarget: indexTarget, + IndexRootSource: indexRootSource, + }, nil +} + +func main() { + log.SetLevel(log.InfoLevel) + + config, err := ReduceConfigFromEnvironment() + if err != nil { + panic(err) + } + + log.Infof("Connecting to %s", config.IndexTarget) + finalIndexStore, err := index.Connect(config.IndexTarget) + if err != nil { + panic(errors.Wrapf(err, "failed to connect to indices at %s", + config.IndexTarget)) + } + + if err := mergeAllIndices(finalIndexStore, config); err != nil { + panic(errors.Wrap(err, "failed to merge indices")) + } +} + +func mergeAllIndices(finalIndexStore index.Store, config *ReduceConfig) error { + doneAccounts := set.NewSafeSet[string](512) + for i := uint32(0); i < config.MapJobCount; i++ { + jobLogger := log.WithField("job", i) + + jobSubPath := "job_" + strconv.FormatUint(uint64(i), 10) + jobLogger.Infof("Connecting to url %s, sub-path %s", config.IndexRootSource, jobSubPath) + outerJobStore, err := index.ConnectWithConfig(index.StoreConfig{ + URL: config.IndexRootSource, + URLSubPath: jobSubPath, + }) + + if err != nil { + return errors.Wrapf(err, "failed to connect to indices at %s, sub-path %s", config.IndexRootSource, jobSubPath) + } + + accounts, err := outerJobStore.ReadAccounts() + // TODO: in final version this should be critical error, now just skip it + if os.IsNotExist(err) { + jobLogger.Errorf("accounts file not found (TODO!)") + continue + } else if err != nil { + return errors.Wrapf(err, "failed to read accounts for job %d", i) + } + + jobLogger.Infof("Processing %d accounts with %d workers", + len(accounts), config.Workers) + + workQueues := make([]chan string, config.Workers) + for i := range workQueues { + workQueues[i] = make(chan string, 1) + } + + for idx, queue := range workQueues { + go (func(index uint32, queue chan string) { + for _, account := range accounts { + // Account index already merged in the previous outer job? + if doneAccounts.Contains(account) { + continue + } + + // Account doesn't belong in this work queue? + if !config.shouldProcessAccount(account, index) { + continue + } + + queue <- account + } + + close(queue) + })(uint32(idx), queue) + } + + // TODO: errgroup.WithContext(ctx) + var wg sync.WaitGroup + wg.Add(int(config.Workers)) + for j := uint32(0); j < config.Workers; j++ { + go func(routineIndex uint32) { + defer wg.Done() + accountLog := jobLogger. + WithField("worker", routineIndex). + WithField("subservice", "accounts") + accountLog.Info("Started worker") + + var accountsProcessed, accountsSkipped uint64 + for account := range workQueues[routineIndex] { + accountLog. + WithField("total", len(accounts)). + WithField("indexed", accountsProcessed). + WithField("skipped", accountsSkipped) + + accountLog.Debugf("Account: %s", account) + if (accountsProcessed+accountsSkipped)%97 == 0 { + accountLog.Infof("Processed %d/%d accounts", + accountsProcessed+accountsSkipped, len(accounts)) + } + + accountLog.Debugf("Reading index for account: %s", account) + + // First, open the "final merged indices" at the root level + // for this account. + mergedIndices, readErr := outerJobStore.Read(account) + + // TODO: in final version this should be critical error, now just skip it + if os.IsNotExist(readErr) { + accountLog.Errorf("Account %s is unavailable - TODO fix", account) + continue + } else if err != nil { + panic(readErr) + } + + // Then, iterate through all of the job folders and merge + // indices from all jobs that touched this account. + for k := uint32(0); k < config.MapJobCount; k++ { + var jobErr error + + // FIXME: This could probably come from a pool. Every + // worker needs to have a connection to every index + // store, so there's no reason to re-open these for each + // inner loop. + innerJobSubPath := "job_" + strconv.FormatUint(uint64(k), 10) + innerJobStore, jobErr := index.ConnectWithConfig(index.StoreConfig{ + URL: config.IndexRootSource, + URLSubPath: innerJobSubPath, + }) + + if jobErr != nil { + accountLog.WithError(jobErr). + Errorf("Failed to open index at %s, sub-path %s", config.IndexRootSource, innerJobSubPath) + panic(jobErr) + } + + jobIndices, jobErr := innerJobStore.Read(account) + + // This job never touched this account; skip. + if os.IsNotExist(jobErr) { + continue + } else if jobErr != nil { + accountLog.WithError(jobErr). + Errorf("Failed to read index for %s", account) + panic(jobErr) + } + + if jobErr = mergeIndices(mergedIndices, jobIndices); jobErr != nil { + accountLog.WithError(jobErr). + Errorf("Merge failure for index at %s, sub-path %s", config.IndexRootSource, innerJobSubPath) + panic(jobErr) + } + } + + // Finally, save the merged index. + finalIndexStore.AddParticipantToIndexesNoBackend(account, mergedIndices) + + // Mark this account for other workers to ignore. + doneAccounts.Add(account) + accountsProcessed++ + accountLog = accountLog.WithField("processed", accountsProcessed) + + // Periodically flush to disk to save memory. + if accountsProcessed%ACCOUNT_FLUSH_FREQUENCY == 0 { + accountLog.Infof("Flushing indexed accounts.") + if flushErr := finalIndexStore.Flush(); flushErr != nil { + accountLog.WithError(flushErr).Errorf("Flush error.") + panic(flushErr) + } + } + } + + accountLog.Infof("Final account flush.") + if err = finalIndexStore.Flush(); err != nil { + accountLog.WithError(err).Errorf("Flush error.") + panic(err) + } + + // Merge the transaction indexes + // There's 256 files, (one for each first byte of the txn hash) + txLog := jobLogger. + WithField("worker", routineIndex). + WithField("subservice", "transactions") + + var prefixesProcessed, prefixesSkipped uint64 + for i := int(0x00); i <= 0xff; i++ { + b := byte(i) // can't loop over range bc overflow + if b%97 == 0 { + txLog.Infof("Processed %d/%d prefixes (%d skipped)", + prefixesProcessed, 0xff, prefixesSkipped) + } + + if !config.shouldProcessTx(b, routineIndex) { + prefixesSkipped++ + continue + } + + txLog = txLog. + WithField("indexed", prefixesProcessed). + WithField("skipped", prefixesSkipped) + + prefix := hex.EncodeToString([]byte{b}) + for k := uint32(0); k < config.MapJobCount; k++ { + var innerErr error + innerJobSubPath := "job_" + strconv.FormatUint(uint64(k), 10) + innerJobStore, innerErr := index.ConnectWithConfig(index.StoreConfig{ + URL: config.IndexRootSource, + URLSubPath: innerJobSubPath, + }) + + if innerErr != nil { + txLog.WithError(innerErr).Errorf("Failed to open index at %s, sub-path %s", config.IndexRootSource, innerJobSubPath) + panic(innerErr) + } + + innerTxnIndexes, innerErr := innerJobStore.ReadTransactions(prefix) + if os.IsNotExist(innerErr) { + continue + } else if innerErr != nil { + txLog.WithError(innerErr).Errorf("Error reading tx prefix %s", prefix) + panic(innerErr) + } + + if innerErr = finalIndexStore.MergeTransactions(prefix, innerTxnIndexes); innerErr != nil { + txLog.WithError(innerErr).Errorf("Error merging txs at prefix %s", prefix) + panic(innerErr) + } + } + + prefixesProcessed++ + } + + txLog = txLog. + WithField("indexed", prefixesProcessed). + WithField("skipped", prefixesSkipped) + + txLog.Infof("Final transaction flush...") + if err = finalIndexStore.Flush(); err != nil { + txLog.Errorf("Error flushing transactions: %v", err) + panic(err) + } + }(j) + } + + wg.Wait() + } + + return nil +} + +func (cfg *ReduceConfig) shouldProcessAccount(account string, routineIndex uint32) bool { + hash := fnv.New64a() + + // Docs state (https://pkg.go.dev/hash#Hash) that Write will never error. + hash.Write([]byte(account)) + digest := uint32(hash.Sum64()) // discard top 32 bits + + leftHalf := digest >> 16 + rightHalf := digest & 0x0000FFFF + + log.WithField("worker", routineIndex). + WithField("account", account). + Debugf("Hash: %d (left=%d, right=%d)", digest, leftHalf, rightHalf) + + // Because the digest is basically a random number (given a good hash + // function), its remainders w.r.t. the indices will distribute the work + // fairly (and deterministically). + return leftHalf%cfg.ReduceJobCount == cfg.JobIndex && + rightHalf%cfg.Workers == routineIndex +} + +func (cfg *ReduceConfig) shouldProcessTx(txPrefix byte, routineIndex uint32) bool { + hashLeft := uint32(txPrefix >> 4) + hashRight := uint32(txPrefix & 0x0F) + + // Because the transaction hash (and thus the first byte or "prefix") is a + // random value, its remainders w.r.t. the indices will distribute the work + // fairly (and deterministically). + return hashRight%cfg.ReduceJobCount == cfg.JobIndex && + hashLeft%cfg.Workers == routineIndex +} + +// For every index that exists in `dest`, finds the corresponding index in +// `source` and merges it into `dest`'s version. +func mergeIndices(dest, source map[string]*types.BitmapIndex) error { + for name, index := range dest { + // The source doesn't contain this particular index. + // + // This probably shouldn't happen, since during the Map step, there's no + // way to choose which indices you want, but, strictly-speaking, it's + // not an error, so we can just move on. + innerIndices, ok := source[name] + if !ok || innerIndices == nil { + continue + } + + if err := index.Merge(innerIndices); err != nil { + return errors.Wrapf(err, "failed to merge index for %s", name) + } + } + + return nil +} diff --git a/exp/lighthorizon/index/cmd/map.sh b/exp/lighthorizon/index/cmd/map.sh new file mode 100755 index 0000000000..390370f2cb --- /dev/null +++ b/exp/lighthorizon/index/cmd/map.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# +# Breaks up the given ledger dumps into checkpoints and runs a map +# job on each one. However, it's the Golang side does validation that +# the map job resulted in the correct indices. +# + +# check parameters and their validity (types, existence, etc.) + +if [[ "$#" -ne "2" ]]; then + echo "Usage: $0 " + exit 1 +fi + +if [[ ! -d "$1" ]]; then + echo "Error: txmeta src ('$1') does not exist" + echo "Usage: $0 " + exit 1 +fi + +if [[ -z $BATCH_SIZE ]]; then + echo "BATCH_SIZE environmental variable required" + exit 1 +elif ! [[ $BATCH_SIZE =~ ^[0-9]+$ ]]; then + echo "BATCH_SIZE ('$BATCH_SIZE') must be an integer" + exit 1 +fi + +if [[ -z $FIRST_LEDGER || -z $LAST_LEDGER ]]; then + echo "FIRST_LEDGER and LAST_LEDGER environmental variables required" + exit 1 +elif ! [[ $FIRST_LEDGER =~ ^[0-9]+$ && $LAST_LEDGER =~ ^[0-9]+$ ]]; then + echo "FIRST_LEDGER ('$FIRST_LEDGER') and LAST_LEDGER ('$LAST_LEDGER') must be integers" + exit 1 +fi + +if [[ ! -d "$2" ]]; then + echo "Warning: index dest ('$2') does not exist, creating..." + mkdir -p $2 +fi + +# do work + +FIRST=$FIRST_LEDGER +LAST=$LAST_LEDGER +COUNT=$(($LAST-$FIRST+1)) +# batches = ceil(count / batch_size) +# formula is from https://stackoverflow.com/a/12536521 +BATCH_COUNT=$(( ($COUNT + $BATCH_SIZE - 1) / $BATCH_SIZE )) + +if [[ "$(((LAST + 1) % 64))" -ne "0" ]]; then + echo "LAST_LEDGER ($LAST_LEDGER) should be a checkpoint ledger" + exit 1 +fi + +echo " - start: $FIRST" +echo " - end: $LAST" +echo " - count: $COUNT ($BATCH_COUNT batches @ $BATCH_SIZE ledgers each)" + +go build -o ./map ./batch/map/... +if [[ "$?" -ne "0" ]]; then + echo "Build failed" + exit 1 +fi + +pids=( ) +for (( i=0; i < $BATCH_COUNT; i++ )) +do + echo -n "Creating map job $i... " + + NETWORK_PASSPHRASE='testnet' JOB_INDEX_ENV='AWS_BATCH_JOB_ARRAY_INDEX' MODULES='accounts_unbacked,transactions' \ + AWS_BATCH_JOB_ARRAY_INDEX=$i BATCH_SIZE=$BATCH_SIZE FIRST_CHECKPOINT=$FIRST \ + TXMETA_SOURCE=file://$1 INDEX_TARGET=file://$2 WORKER_COUNT=1 \ + ./map & + + echo "pid=$!" + pids+=($!) +done + +sleep $BATCH_COUNT + +# Check the status codes for all of the map processes. +for i in "${!pids[@]}"; do + pid=${pids[$i]} + echo -n "Checking job $i (pid=$pid)... " + if ! wait "$pid"; then + echo "failed" + exit 1 + else + echo "succeeded!" + fi +done + +rm ./map +echo "All jobs succeeded!" +exit 0 diff --git a/exp/lighthorizon/index/cmd/mapreduce_test.go b/exp/lighthorizon/index/cmd/mapreduce_test.go new file mode 100644 index 0000000000..db529fd8bc --- /dev/null +++ b/exp/lighthorizon/index/cmd/mapreduce_test.go @@ -0,0 +1,232 @@ +package main_test + +import ( + "encoding/hex" + "fmt" + "io" + "net/url" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/stellar/go/exp/lighthorizon/index" + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/network" + "github.com/stellar/go/support/collections/maps" + "github.com/stellar/go/support/collections/set" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + batchSize = 128 +) + +func TestMap(t *testing.T) { + RunMapTest(t) +} + +func TestReduce(t *testing.T) { + // First, map the index files like we normally would. + startLedger, endLedger, jobRoot := RunMapTest(t) + batchCount := (endLedger - startLedger + batchSize) / batchSize // ceil(ledgerCount / batchSize) + + // Now that indices have been "map"ped, reduce them to a single store. + + indexTarget := filepath.Join(t.TempDir(), "final-indices") + reduceTestCmd := exec.Command("./reduce.sh", jobRoot, indexTarget) + t.Logf("Running %d reduce jobs: %s", batchCount, reduceTestCmd.String()) + stdout, err := reduceTestCmd.CombinedOutput() + t.Logf(string(stdout)) + require.NoError(t, err) + + // Then, build the *same* indices using the single-process tester. + + t.Logf("Building baseline for ledger range [%d, %d]", startLedger, endLedger) + hashes, participants := IndexLedgerRange(t, txmetaSource, startLedger, endLedger) + + // Finally, compare the two to make sure the reduce job did what it's + // supposed to do. + + indexStore, err := index.Connect("file://" + indexTarget) + require.NoError(t, err) + stores := []index.Store{indexStore} // to reuse code: same as array of 1 store + + assertParticipantsEqual(t, maps.Keys(participants), stores) + for account, checkpoints := range participants { + assertParticipantCheckpointsEqual(t, account, checkpoints, stores) + } + + assertTOIDsEqual(t, hashes, stores) +} + +func RunMapTest(t *testing.T) (uint32, uint32, string) { + // Only file:// style URLs for the txmeta source are allowed while testing. + parsed, err := url.Parse(txmetaSource) + require.NoErrorf(t, err, "%s is not a valid URL", txmetaSource) + if parsed.Scheme != "file" { + t.Logf("%s is not local txmeta source", txmetaSource) + t.Skip() + } + txmetaPath := strings.Replace(txmetaSource, "file://", "", 1) + + // What ledger range are we working with? + checkpointMgr := historyarchive.NewCheckpointManager(0) + startLedger, endLedger := GetFixtureLedgerRange(t) + + // The map job *requires* that each one operate on a multiple of a + // checkpoint range, so we may need to adjust the ranges (depending on how + // many ledgers are in the fixutre) and break them up accordingly. + if !checkpointMgr.IsCheckpoint(startLedger - 1) { + startLedger = checkpointMgr.NextCheckpoint(startLedger-1) + 1 + } + if (endLedger-startLedger)%batchSize != 0 { + endLedger = checkpointMgr.PrevCheckpoint((endLedger / batchSize) * batchSize) + } + + require.Greaterf(t, endLedger, startLedger, + "not enough fixtures for batchSize=%d", batchSize) + + batchCount := (endLedger - startLedger + batchSize) / batchSize // ceil(ledgerCount / batchSize) + + t.Logf("Using %d batches to process ledger range [%d, %d]", + batchCount, startLedger, endLedger) + + require.Truef(t, + batchCount == 1 || checkpointMgr.IsCheckpoint(startLedger+batchSize-1), + "expected batch size (%d) to result in checkpoint blocks, "+ + "but start+batchSize+1 (%d+%d+1=%d) is not a checkpoint", + batchSize, batchSize, startLedger, batchSize+startLedger+1) + + // First, execute the map jobs in parallel and dump the resulting indices to + // a temporary directory. + + tempDir := filepath.Join(t.TempDir(), "indices-map") + mapTestCmd := exec.Command("./map.sh", txmetaPath, tempDir) + mapTestCmd.Env = append(os.Environ(), + fmt.Sprintf("BATCH_SIZE=%d", batchSize), + fmt.Sprintf("FIRST_LEDGER=%d", startLedger), + fmt.Sprintf("LAST_LEDGER=%d", endLedger), + fmt.Sprintf("NETWORK_PASSPHRASE='%s'", network.TestNetworkPassphrase)) + t.Logf("Running %d map jobs: %s", batchCount, mapTestCmd.String()) + stdout, err := mapTestCmd.CombinedOutput() + + t.Logf("Tried writing indices to %s:", tempDir) + t.Log(string(stdout)) + require.NoError(t, err) + + // Then, build the *same* indices using the single-process tester. + t.Logf("Building baseline for ledger range [%d, %d]", startLedger, endLedger) + hashes, participants := IndexLedgerRange(t, txmetaSource, startLedger, endLedger) + + // Now, walk through the mapped indices and ensure that at least one of the + // jobs reported the same indices for tx TOIDs and participation. + + stores := make([]index.Store, batchCount) + for i := range stores { + indexUrl := filepath.Join( + "file://", + tempDir, + "job_"+strconv.FormatUint(uint64(i), 10), + ) + index, err := index.Connect(indexUrl) + require.NoError(t, err) + require.NotNil(t, index) + stores[i] = index + + t.Logf("Connected to index #%d at %s", i+1, indexUrl) + } + + assertParticipantsEqual(t, maps.Keys(participants), stores) + for account, checkpoints := range participants { + assertParticipantCheckpointsEqual(t, account, checkpoints, stores) + } + + assertTOIDsEqual(t, hashes, stores) + + return startLedger, endLedger, tempDir +} + +func assertParticipantsEqual(t *testing.T, + expectedAccountSet []string, + indexGroup []index.Store, +) { + indexGroupAccountSet := set.NewSet[string](len(expectedAccountSet)) + for _, store := range indexGroup { + accounts, err := store.ReadAccounts() + require.NoError(t, err) + indexGroupAccountSet.AddSlice(accounts) + } + + assert.Lenf(t, indexGroupAccountSet, len(expectedAccountSet), + "quantity of accounts across indices doesn't match") + + mappedAccountSet := maps.Keys(indexGroupAccountSet) + require.ElementsMatch(t, expectedAccountSet, mappedAccountSet) +} + +func assertParticipantCheckpointsEqual(t *testing.T, + account string, + expected []uint32, + indexGroup []index.Store, +) { + // Ensure that all of the active checkpoints reported by the index match + // the ones we tracked while ingesting the range ourselves. + + foundCheckpoints := set.NewSet[uint32](len(expected)) + for _, store := range indexGroup { + var err error + var lastActiveCheckpoint uint32 = 0 + for { + lastActiveCheckpoint, err = store.NextActive(account, "all/all", lastActiveCheckpoint) + if err == io.EOF { + break + } + require.NoError(t, err) // still an error since it shouldn't happen + + foundCheckpoints.Add(lastActiveCheckpoint) + lastActiveCheckpoint += 1 // hit next active one + } + } + + // Error out if there were any extraneous checkpoints found. + for chk := range foundCheckpoints { + require.Containsf(t, expected, chk, + "found unexpected checkpoint %d", int(chk)) + } + + // Make sure everything got marked as expected in at least one index. + for _, item := range expected { + require.Containsf(t, foundCheckpoints, item, + "failed to find %d for %s (found %v)", + int(item), account, foundCheckpoints) + } +} + +func assertTOIDsEqual(t *testing.T, toids map[string]int64, stores []index.Store) { + for hash, toid := range toids { + rawHash := [32]byte{} + decodedHash, err := hex.DecodeString(hash) + require.NoError(t, err) + require.Lenf(t, decodedHash, 32, "invalid tx hash length") + copy(rawHash[:], decodedHash) + + found := false + for i, store := range stores { + storeToid, err := store.TransactionTOID(rawHash) + if err != nil { + require.ErrorIsf(t, err, io.EOF, + "only EOF errors are allowed (store %d, hash %s)", i, hash) + } else { + require.Equalf(t, toid, storeToid, + "TOIDs for tx 0x%s don't match (store %d)", hash, i) + found = true + } + } + + require.Truef(t, found, "TOID for tx 0x%s not found in stores", hash) + } +} diff --git a/exp/lighthorizon/index/cmd/reduce.sh b/exp/lighthorizon/index/cmd/reduce.sh new file mode 100755 index 0000000000..1cfbca0ccc --- /dev/null +++ b/exp/lighthorizon/index/cmd/reduce.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# +# Combines indices that were built separately in different folders into a single +# set of indices. +# +# This focuses on starting parallel processes, but the Golang side does +# validation that the reduce jobs resulted in the correct indices. +# + +# check parameters and their validity (types, existence, etc.) + +if [[ "$#" -ne "2" ]]; then + echo "Usage: $0 " + exit 1 +fi + +if [[ ! -d "$1" ]]; then + echo "Error: index src root ('$1') does not exist" + echo "Usage: $0 " + exit 1 +fi + +if [[ ! -d "$2" ]]; then + echo "Warning: index dest ('$2') does not exist, creating..." + mkdir -p "$2" +fi + +MAP_JOB_COUNT=$(ls $1 | grep -E 'job_[0-9]+' | wc -l) +if [[ "$MAP_JOB_COUNT" -le "0" ]]; then + echo "No jobs in index src root ('$1') found." + exit 1 +fi +REDUCE_JOB_COUNT=$MAP_JOB_COUNT + +# build reduce program and start it up + +go build -o reduce ./batch/reduce/... +if [[ "$?" -ne "0" ]]; then + echo "Build failed" + exit 1 +fi + +echo "Coalescing $MAP_JOB_COUNT discovered job outputs from $1 into $2..." + +pids=( ) +for (( i=0; i < $REDUCE_JOB_COUNT; i++ )) +do + echo -n "Creating reduce job $i... " + + AWS_BATCH_JOB_ARRAY_INDEX=$i JOB_INDEX_ENV="AWS_BATCH_JOB_ARRAY_INDEX" MAP_JOB_COUNT=$MAP_JOB_COUNT \ + REDUCE_JOB_COUNT=$REDUCE_JOB_COUNT WORKER_COUNT=4 \ + INDEX_SOURCE_ROOT=file://$1 INDEX_TARGET=file://$2 \ + timeout -k 30s 10s ./reduce & + + echo "pid=$!" + pids+=($!) +done + +sleep $REDUCE_JOB_COUNT + +# Check the status codes for all of the map processes. +for i in "${!pids[@]}"; do + pid=${pids[$i]} + echo -n "Checking job $i (pid=$pid)... " + if ! wait "$pid"; then + echo "failed" + exit 1 + else + echo "succeeded!" + fi +done + +rm ./reduce # cleanup +echo "All jobs succeeded!" +exit 0 diff --git a/exp/lighthorizon/index/cmd/single/main.go b/exp/lighthorizon/index/cmd/single/main.go new file mode 100644 index 0000000000..7661b160dc --- /dev/null +++ b/exp/lighthorizon/index/cmd/single/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "context" + "flag" + "runtime" + "strings" + + "github.com/stellar/go/exp/lighthorizon/index" + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/network" + "github.com/stellar/go/support/log" +) + +func main() { + sourceUrl := flag.String("source", "gcs://horizon-archive-poc", "history archive url to read txmeta files") + targetUrl := flag.String("target", "file://indexes", "where to write indexes") + networkPassphrase := flag.String("network-passphrase", network.TestNetworkPassphrase, "network passphrase") + start := flag.Int("start", 2, "ledger to start at (inclusive, default: 2, the earliest)") + end := flag.Int("end", 0, "ledger to end at (inclusive, default: 0, the latest as of start time)") + modules := flag.String("modules", "accounts,transactions", "comma-separated list of modules to index (default: all)") + watch := flag.Bool("watch", false, "whether to watch the `source` for new "+ + "txmeta files and index them (default: false). "+ + "note: `-watch` implies a continuous `-end 0` to get to the latest ledger in txmeta files") + workerCount := flag.Int("workers", runtime.NumCPU()-1, "number of workers (default: # of CPUs - 1)") + + flag.Parse() + log.SetLevel(log.InfoLevel) + // log.SetLevel(log.DebugLevel) + + builder, err := index.BuildIndices( + context.Background(), + *sourceUrl, + *targetUrl, + *networkPassphrase, + historyarchive.Range{ + Low: uint32(max(*start, 2)), + High: uint32(*end), + }, + strings.Split(*modules, ","), + *workerCount, + ) + if err != nil { + panic(err) + } + + if *watch { + if err := builder.Watch(context.Background()); err != nil { + panic(err) + } + } +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/exp/lighthorizon/index/cmd/single_test.go b/exp/lighthorizon/index/cmd/single_test.go new file mode 100644 index 0000000000..58620d2ef9 --- /dev/null +++ b/exp/lighthorizon/index/cmd/single_test.go @@ -0,0 +1,279 @@ +package main_test + +import ( + "context" + "encoding/hex" + "io" + "io/ioutil" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/ingest" + "github.com/stellar/go/ingest/ledgerbackend" + "github.com/stellar/go/metaarchive" + "github.com/stellar/go/network" + "github.com/stellar/go/support/storage" + "github.com/stellar/go/toid" + "github.com/stretchr/testify/require" + + "github.com/stellar/go/exp/lighthorizon/index" +) + +const ( + txmetaSource = "file://./testdata/" +) + +/** + * There are three parts to testing this correctly: + * - test that single-process indexing works + * - test that single-process w/ multi-worker works + * - test map-reduce against the single-process results + * + * Therefore, if any of these fail, the subsequent ones are unreliable. + */ + +func TestSingleProcess(tt *testing.T) { + eldestLedger, latestLedger := GetFixtureLedgerRange(tt) + checkpoints := historyarchive.NewCheckpointManager(0) + + // We want two test variations: + // - starting at the first ledger in a checkpoint range + // - starting at an arbitrary ledger + // + // To do this, we adjust the known set of fixture ledgers we have. + var eldestCheckpointLedger uint32 + if checkpoints.IsCheckpoint(eldestLedger - 1) { + eldestCheckpointLedger = eldestLedger // first in range + eldestLedger += 5 // somewhere in the "middle" + } else { + eldestCheckpointLedger = checkpoints.NextCheckpoint(eldestLedger-1) + 1 + eldestLedger++ + } + + tt.Run("start-at-checkpoint", func(t *testing.T) { + testSingleProcess(tt, historyarchive.Range{ + Low: eldestCheckpointLedger, + High: latestLedger, + }) + }) + + tt.Run("start-at-ledger", func(t *testing.T) { + testSingleProcess(tt, historyarchive.Range{ + Low: eldestLedger, + High: latestLedger, + }) + }) +} + +func testSingleProcess(t *testing.T, ledgerRange historyarchive.Range) { + var ( + firstLedger = ledgerRange.Low + lastLedger = ledgerRange.High + ledgerCount = ledgerRange.High - ledgerRange.Low + 1 + ) + + t.Logf("Validating single-process builder on ledger range [%d, %d] (%d ledgers)", + firstLedger, lastLedger, ledgerCount) + + workerCount := 4 + tmpDir := filepath.Join("file://", t.TempDir()) + t.Logf("Storing indices in %s", tmpDir) + + ctx := context.Background() + _, err := index.BuildIndices( + ctx, + txmetaSource, + tmpDir, + network.TestNetworkPassphrase, + historyarchive.Range{Low: firstLedger, High: lastLedger}, + []string{ + "accounts", + "transactions", + }, + workerCount, + ) + require.NoError(t, err) + + hashes, participants := IndexLedgerRange(t, txmetaSource, firstLedger, lastLedger) + + store, err := index.Connect(tmpDir) + require.NoError(t, err) + require.NotNil(t, store) + + // Ensure the participants reported by the index and the ones we + // tracked while ingesting the ledger range match. + AssertParticipantsEqual(t, participants, store) + + // Ensure the transactions reported by the index match the ones + // tracked when ingesting the ledger range ourselves. + AssertTxsEqual(t, hashes, store) +} + +func AssertTxsEqual(t *testing.T, expected map[string]int64, actual index.Store) { + for hash, knownTOID := range expected { + rawHash, err := hex.DecodeString(hash) + require.NoError(t, err, "bug") + require.Len(t, rawHash, 32) + + tempBuf := [32]byte{} + copy(tempBuf[:], rawHash[:]) + + rawTOID, err := actual.TransactionTOID(tempBuf) + require.NoErrorf(t, err, "expected TOID for tx hash %s", hash) + + require.Equalf(t, knownTOID, rawTOID, + "expected TOID %v, got %v", + toid.Parse(knownTOID), toid.Parse(rawTOID)) + } +} + +func AssertParticipantsEqual(t *testing.T, expected map[string][]uint32, actual index.Store) { + accounts, err := actual.ReadAccounts() + + require.NoError(t, err) + require.Len(t, accounts, len(expected)) + for account := range expected { + require.Contains(t, accounts, account) + } + + for account, knownCheckpoints := range expected { + // Ensure that the "everything" index exists for the account. + index, err := actual.Read(account) + require.NoError(t, err) + require.Contains(t, index, "all/all") + + // Ensure that all of the active checkpoints reported by the index match the ones we + // tracked while ingesting the range ourselves. + activeCheckpoints := []uint32{} + lastActiveCheckpoint := uint32(0) + for { + lastActiveCheckpoint, err = actual.NextActive(account, "all/all", lastActiveCheckpoint) + if err == io.EOF { + break + } + require.NoError(t, err) + + activeCheckpoints = append(activeCheckpoints, lastActiveCheckpoint) + lastActiveCheckpoint += 1 // hit next active one + } + + require.Equalf(t, knownCheckpoints, activeCheckpoints, + "incorrect checkpoints for %s", account) + } +} + +// IndexLedgerRange will connect to a dump of ledger txmeta for the given ledger +// range and build two maps from scratch (i.e. without using the indexer) by +// ingesting them manually: +// +// - a map of tx hashes to TOIDs +// - a map of accounts to a list of checkpoints they were active in +// +// These should be used as a baseline comparison of the indexer, ensuring that +// all of the data is identical. +func IndexLedgerRange( + t *testing.T, + txmetaSource string, + startLedger, endLedger uint32, // inclusive +) ( + map[string]int64, // map of "tx hash": TOID + map[string][]uint32, // map of "account": {checkpoint, checkpoint, ...} +) { + ctx := context.Background() + backend, err := historyarchive.ConnectBackend( + txmetaSource, + storage.ConnectOptions{ + Context: ctx, + S3Region: "us-east-1", + }, + ) + require.NoError(t, err) + + metaArchive := metaarchive.NewMetaArchive(backend) + + ledgerBackend := ledgerbackend.NewHistoryArchiveBackend(metaArchive) + defer ledgerBackend.Close() + + participation := make(map[string][]uint32) + hashes := make(map[string]int64) + + for ledgerSeq := startLedger; ledgerSeq <= endLedger; ledgerSeq++ { + ledger, err := ledgerBackend.GetLedger(ctx, uint32(ledgerSeq)) + require.NoError(t, err) + require.EqualValues(t, ledgerSeq, ledger.LedgerSequence()) + + reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta( + network.TestNetworkPassphrase, ledger) + require.NoError(t, err) + + for { + tx, err := reader.Read() + if err == io.EOF { + break + } + require.NoError(t, err) + + participants, err := index.GetTransactionParticipants(tx) + require.NoError(t, err) + + for _, participant := range participants { + checkpoint := index.GetCheckpointNumber(ledgerSeq) + + // Track the checkpoint in which activity occurred, keeping the + // list duplicate-free. + if list, ok := participation[participant]; ok { + if list[len(list)-1] != checkpoint { + participation[participant] = append(list, checkpoint) + } + } else { + participation[participant] = []uint32{checkpoint} + } + } + + // Track the ledger sequence in which every tx occurred. + hash := hex.EncodeToString(tx.Result.TransactionHash[:]) + hashes[hash] = toid.New( + int32(ledger.LedgerSequence()), + int32(tx.Index), + 0, + ).ToInt64() + } + } + + return hashes, participation +} + +// GetFixtureLedgerRange determines the oldest and latest ledgers w/in the +// fixture data. It's *essentially* equivalent to (but better than, since it +// handles the existence of non-integer files): +// +// LOW=$(ls $txmetaSource/ledgers | sort -n | head -n1) +// HIGH=$(ls $txmetaSource/ledgers | sort -n | tail -n1) +func GetFixtureLedgerRange(t *testing.T) (low uint32, high uint32) { + txmetaSourceDir := strings.Replace( + txmetaSource, + "file://", "", + 1) + files, err := ioutil.ReadDir(filepath.Join(txmetaSourceDir, "ledgers")) + require.NoError(t, err) + + for _, file := range files { + ledgerNum, innerErr := strconv.ParseUint(file.Name(), 10, 32) + if innerErr != nil { // non-integer filename + continue + } + + ledger := uint32(ledgerNum) + if ledger < low || low == 0 { + low = ledger + } + if ledger > high || high == 0 { + high = ledger + } + } + + return low, high +} diff --git a/exp/lighthorizon/index/cmd/testdata/latest b/exp/lighthorizon/index/cmd/testdata/latest new file mode 100644 index 0000000000..9f53cd22d0 --- /dev/null +++ b/exp/lighthorizon/index/cmd/testdata/latest @@ -0,0 +1 @@ +1410367 \ No newline at end of file diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410048 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410048 new file mode 100644 index 0000000000000000000000000000000000000000..6eb8fee0ce56e3f4040293df7a501bffa7854e40 GIT binary patch literal 4160 zcmZQzfPmUN*Seg9WlQR9*WK*0K07}&+-_@44Kpw} z_C4@&G{~l;MaKgmHZm}R=&MDUH8swiW-o4VWe9GFVZ}(4L(3R3Q(Msm{q-NIUjS<%}4}4Www{m^j>zuFA zHD0|93p|oHWy?+wR%40R~=pM9l8=0*0dy7OG2YA4$7d0+Rxo4S0{Y!RJLerp-{og5fMo0kBU zMuF4=0VwVyvVy#QO!JNMLqoN*OmlN0j3O;UEFEoaq4F?w3|s*ZRcrK4H&?hU`tmdQ z1FziXq_5%o)He1W5UK0>ee6F-Aryeq1dz=Lwig)po5J1lPaooDk9n+fp}f0^>%+TO zr;8heXB_^|r+>bG(IREhnKe)H7bre&@V_^Azi+nF=6TP#c57{3bAU}XU#yHBXcpLy zyf;>7@0__-MnHa9!4&V`|K70fVqI{#QaU#(Rdp|~#4@lSxfSbx>L-Kj2V!ghBo3An zsJ)o2V`1#kq}M3-ebsr9i!-|N8A6IeF5AiFwuL>I1Jk^t{Iazo&*Ke2&)eM8uP8Rm zzp_WmI#W_li!t)|jN>~yf$GFP%3i>fGlAU#^!K5wpRabRA8Qc)VGuIu!k5aK>W4bD zUSEu2kh>rda#f`_apDdC;%A;uIAzo7W*zK!y>Exk9+Ms^`+&bm|2O`&<%GJyVY_=| z>HTdSiW7>baBRIBCgP*I;GE#|dHQGDESF5aDQf@l2ghwD&I|WXiMUnBR&BL@CR2KW z?@GzpfSjinByMnV0v!epKf~9K%riD9o|(p?n*B=UTM8$)vr?yzQl4kt$&;oNgbjh} zQWylpPBSoQXam_O;g_`NL^)K9v!J-Z%GlW0!~iG&6@$|${((=K%H#NsReLJ!xOdev zWq!cn`+gQ7R{JETeBRgW3slGy5E>NV;|kIO0qLi%C0ESiEA8F-D_?JN6o=cX5{uX6 z83Go@>$GMrzV;cUib;&`07NwdBh;-9jImi8`HW`Au2>_Pfx{E^xQSsr zAz=qTh3iX~-H;LdGykxgW9y#hJKt{Ch1v-&HGphbn80X|G$?GDfq4s_2MFdxpaxmD zko(a57zr{286X*s#DuE=$0eKxDFYZ__5;giZ>St2sLX=_BHdI?V>f}^4h^qC=Qc{h z3s$DWf*YPtfC41MAtX%D>Q@$^8Nl>Kc6iawZKS)Y3YxxXCGqTFX-U;jk{TJ~!J&0&R_1*Tw@069oZxC*k$BjVijc?*r* zgta^xByK}VYb3e}RDV+;4l&9jaHKC&Yx`EfDQG0FV$8*=!gHTT+5N9H`=t9$22&Tv z#YG!IQ$`Au#+7D(c{O(jR1E{cwjEHRwcN%p43PHSB$z%JjbsTD6DA93|G;_hIul}l z(xM>( z`s*Is>DwfU0@ZH-7IPC|`d~B`2ch_#2=mpR9jzk8d|;anQRfrg2SL&Ub32H};(oX^ E0IH<5$N&HU literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410049 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410049 new file mode 100644 index 0000000000000000000000000000000000000000..e253f8658a74075490d75c33bf3dce311e5fa836 GIT binary patch literal 5340 zcmZQzfB>Zp#fB4WC#ud-U%qJ5w)s~kuVL+Tb*NtTvhlQK&jO_speo_oJJ-6Lgk?+W zZP(rGvOYUMG~8}$P2}1@en!oyhQDHpZ~V%fr78Tj39c2%2|m$b4%HLHD^UfuUTS!c$L0UriG|&kN)hfr*l6T z1C=;D3R?eAdgqeuT4(D&G%|$Wc=cH^oZZ3XrQDpkGdcXN#fyXkYu7$V3sbSvNQs+R zHRaCX5Y62@2Cp0Uum3IT+Q|ClgqxT~hUg)UW98n5dXL3T_DErl{o`S&mfpW$%Hpuv z3z?d}r#iU|5AAfCwR7h=9>re!2zSn|9K}kPR}JxRKQM^4F!Dawws|=y6d0I~pIH!B z0%AeHsU#qs!r zc>dK5vHLchyZJDbb9(mtANP2+U5jb^|9i%VgI;&c!+SJLbb5b&Vo3S-H#I1xflK!E zoWvGohuPPPZ)Wh9yF9#}`fQa_*TSi9no5jB0#8m>J!O1VE4)j|<1u^m<82T(F)$E{ zE1)_SAZ7xoms;JpdjH4vsV6>vsco8lK+$Z5`dyKYZbk93w$lRd2RbnDJ2^0LE3O18 zoead_cmvYt03;5U6R5qItz%*A(WKWX_kGoQk&83B@)<&kLN43M<+g=Am;=+8_v~l> zSFX#|lLGd~t8^74tQ5SlNxfp(>6ahlZhJo~^8~6B_b7V-Q_ci-3ot#%G+p+eRu|W{ zVpILY(j`V;^ffPE3~&*9d)Oq~dd5w2VS!qOvVeyNE*o!k^LF^X-qdf0J=2N@iY{Lb zZDpgfH}gT=;85b|RqlBpaNEj<8Hp;VW+!iCsMN81_cFA1NmKTUn@<)sEm;|(WFe-0 z=H6SkEmsm}AqZ0TGFODppmQ<Jv+z9z0-~uJ9}|eith&{8lB)H$R$Kq*MK&>Y&b*YjaCXHD=kAHx<=qG|zvW zDYb6@G|Qi_C%Ts(JNumdeCLW89^v_$o%1{S_sBL^8vT0A4+}rT*N)6HHYlE%#-f`2 zO66M$C%3awr;k#eXWq$^rW1q>f$CBi1jJ4=Flgui*(m8RY0-%qs2FEKaeUUv1`tCP+t2bY}bLkuiBGsQtm$R?wT2KnCb0)vkiv3 z+YkIXbBGsc9yncDaMw*tZF;Gp!H?3x)O2Xf9AK$X~x9vYMDcF%Nd?7Zr!>nL1DeyoK`=L@5%8mxuOl_e%dJH zKN1RHtqH2WI>Yb6jVU3Q-!=YMOEf(T3OknXRvH}9$|2d#ld@HqbbO>s*NM+`EBm^p zG^yX8yM${ePz?xxOLQQOlBU2ikT_sw5X}Yo7Yqo-2T+yORxK7_SqX}hNf2EK63G%I zCR`pI=Wrgdq5-p^_9rb0vVzJng6bofI^x`nFf9LK#SIgGr(Ioe1;!=lM+{ z#e4?j_$IpCKuRMphk|GW*`H}ok&c$3LN-=wFRS+1*95X^$gOos`*hu~ zvAs&QPpD_xX^*9G>ow0oM z0!h&Rg2~bK{|$a!_IbDCxoYw*&xDLRciX?#)3sj-Tla1Ol_4O2$QuwoG6}6~a`(do z3HA+v3av#gIjEeE!S)05aXi!=C~-ran=GhZwxGKSYd#(%Zlff;23bBPIiI2Uorrw= UkT>BF+4-30F$@y&F)Z%^0PbzrfdBvi literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410050 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410050 new file mode 100644 index 0000000000000000000000000000000000000000..e4e5598abee9404b292accdb2a25effd7bc2e25d GIT binary patch literal 5136 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 HoX!9MR%3pG literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410052 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410052 new file mode 100644 index 0000000000000000000000000000000000000000..2aa528231a7f7af967bb0788b6cd99d683b1273c GIT binary patch literal 5052 zcmd5<3sg*L9KTZ;6&gwrF`Ay^QI^u^Wr`$^B$I5yW~LO`q!?6ccL=FXCy!DnGDu2k zYD9xWPYLZ7V$C3(FtK*Cm96ae-MM$%9($S==lIT=e*r26clO^BFLa$>#5>SOe&i4(_*ep|?+yd840?5?1_NQUilYt%5dVQniOV&l>j;GLK4}=+@Iundff=y%|rEOPSUc} zh~!ZwJjpR(zM=-~J7A=z;Wd()@T&7@;&GRCl@%Aw=0w+CD7$m^EqjpTmEs$Z+nCtcYfnhReD23; z#+2h5AFi?Tj5W2G9M}FQ!s+Vtb@z@^A9pL&XQu%(r7q;5KZQHz>3!eEOj=C(LFvz- z(?%|-NHnUPUOUkS*1d9c-YU5#XUxoZ37-@9O-Fr^kjSn_F;N zoNkg^#FmGj#`DoYyP za7WDde*pX`Ij~$@-GP9&wUu@3tw%IA#|b_JnI(Fu&aDh(yyp53XH3tYn0&JCFQ`ZQ zZVIkWLh}P0Q>SZFGIi!lS(}&-EUWw*g*H_t60hPR6Fm4?3{FW~aDK~`g&WGZdb3qF zs0Es*oDbW6fA0rLus@^JSz41y1`g4=+-RDxYvt09{50Lcv&k)+jco4fd&lr3nk&a9 zRW-YoYyoL_ZEf!GXXhn9ChlA0HnE`XrA9vQbr|CSjEBxE};sdc%Lx*s_qR zY0ogQnDROw6S=Dx ze%j9@U2ne644hG9);a2g2Kc<=IPpY970)(+$g0D`Kpbcqe1i zrGs*_H!dFve5tVJ*B9_u@nvc+%Pq&+8*Q8GH9Fwb%{K4HQFLk!-`^FB%J~QyjE(h4 zp!)%n;T$MqHAROM*l%=9`C@zE4(@R@U06~<$CeAq=gj_=K_+U-1M*Gkf zoFkar|DKB=0MnEDuZ#&|$LH%?!}dAz1<$E3QA2c22zM5=e1=o|AmZM;?F(=ag*t&6afSBO9@dauq+f#aS z)N?_xZ9F|#M>t63F25j!`6A6GAn2V$tmA%>b%|cnfYqBLue(fu=K$XDeL;>XW122)1q<9Uvx{SGPte96^+; z^wh`pnO+=)w#0X+F9onEen-I}eY&d<@xuFo{Jc{jF61@Np`JMsiB61xBj~A5%p3Ch z(;ECdOx7;k%s(e}0RCYPcph@ze1$ll zlSi*$+GX;qKl literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410053 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410053 new file mode 100644 index 0000000000000000000000000000000000000000..25b592c2eafa3346a294d62924ece5ff1aa8480f GIT binary patch literal 3896 zcmZQzfPjf_Kcv_(9FQ?<)R@MS(R8qS*&O*L)8l`f`*8V8x7jIPpekXuiB?m&`u(>b zPj=e1eokVml0vQ7(M8#z@$)ndd?WO~U$_&R8s4I7j#+O?9&uc6`o_pWw>)o2+x_R1U3kJy;bJuMS#fi7f~yZdrcqijLofanSLZ|+#)p*-XE6}z13eEv@PKYuy?3SO)}dg-L;6HSTy={&Rf zA8zfIS6^uIbDjRFglAjI+E@PBQvUy(h3@=}$n0Gx*D09$rs@Joe0#Yn{=e(8MwBKvV!+AV2^-WT~xE^8qa^H0NqfI(FCEF*Oqyp7| z^nvvfv=6H8rS0YNHvzVq;hc5Jj63W#ZI4ZUX0>$9@fiY7&fi~_aRX=`lVgadUl5Q1 z1L>!(C0ESiEA8F-D_?JN6o=cX5{uX683Go@>$GMrzV;cUj?qvTrkoM%KVTYr`TN^+ z-uv^@w}qVWesnU?V;cLaJ-hX!^Jjeg_qcoO#G8{X3f~^KdVBg^{>iQE8V#%FD(u#N zdcJES=kwn-3QyLu01X6(<(wre3gkALa6UUB|JP-i<_0!~MILL}HL|?c{7YZs`1TV6 zzmo$nJ%Pd=7LFiV!Yk6)Bdy9fyUf7MI54>?KiS*F)Z5Y47EK*PgGFG9YLHQKvM~F4 zla-zGUY~Jyjp|YCD1OrBzAStGKal!hS7#u_z(CL~KsDC0PJz_|2}ZE{fMJoea{Zci zeyPn3J#Jw;?iAh74PIFF(j41dia;$pBH{(f8Ps3QRuH1Ej zzaYFjRsNQv06Z+1GZ!}>4>~8iWqZ;3?_9^16|Q}&F7{KR+M?t4cd?n0+mONn9ClEL zz|#pq{lH`nayQJs`{Ih1%+CGO_fF_A+uOimU;c|J?;Mw$oqOwTm{qF&?OukBi}vUp zg?X2O5$pz_f7>z&*Pakh)9@7e{?jFjPqtEam7?lBcmL;Q(H7EkmfStB-Xnkc*U3)- z8|0;Tzj9`h*>R5ZGTX{PhsH@&8J+zg|1z)JTJkAeW^ZBof_E1lg+Dp$FK(B#K42xg zS%`#%t6;Gf*j8{pg0P^j0J$FqKw-cPOdBBcz<>zLh zpw+do_yx&<%_1T$iO*ZG1cJz8$nFKDYjWd>c40!9n~?oSt$1Q6t7=G_dj3WgyGz3- zVW-b+wb@y6VaM&+Ob%TB#5wm6B$R>W$bTS!#UqFYa(_Xi0G2P{

e=t(eBLd$)kb zfa-W z`imNI2o4jZas(XdBBEU0mzA$xoBDa0M}g7PM@FlzCd_VSDKehrd?-+0OC(qkW?hGz zc3@!+Nh?4N#I#kIetl?w)+=BukO^eNq2egvN0j+LGary(KGw8D1A8d(2S%_U2_P}y tAqnymGJxe}^mG8y3yVWgJ_nmc?Y1e{A=v8^SeVenUT~WjyS?zR0RXwZ)C2$k literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410054 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410054 new file mode 100644 index 0000000000000000000000000000000000000000..6515d892d672c3595c26bc913d3d5461422f65a6 GIT binary patch literal 2820 zcmZQzfB+v`d9EwwtuY0f$=X&EHt&4(Ra&Vy`L)#K8Ag--ewg+Zs7iR^+Yc#r3ZV7wf76 z+j=KO`NxB7N?LS04`L$&BZyw1a#mu`+)_4Q%~{dWYnE6aUZro8X(4Lcqd$A=>D&*- zKqU@U>>fs+F3f*yFUkF1P!OcCCFIF5=7OJ^ zk^6rnq^a$f!T??naX(}-i2|PJj^_1~dt?(`-kH_rMkGDbG#K1r( zu7K*8pqSx>Qd-%;v^DV)0xw8N&RpRg+uhVKx5rwyV}on$#C73282FtWfMEcNFOVJ> z0LLGUQ<_yh<4NV?YhOxccC2*2RbBnEfwA1;0LzQFZV5fPDtkfdWY4C;lrw_O1;(wl zcyPXI&HBWQNv03_Z~7leUo*w%NemN%Z?9req-#{yQP%L&8+QI%a&Uv8!m%fu3rc<) zN%uvWt-AX*%l5)zZ!VyLU^nfHD_$}?_fOwDp~Gx%1B-q6FQ&Y6Tyl2qt+!!Tsrt8j zp?+Wis)xB7L`!7ldb%Y#W+q#>`j;Aerst%j_@pJ5+uEY3V_?o)+oiW(e8AvfO5OfPrjrFWkV6{Mk5$rx-SWNN=st6Qu z6aL7eyE^~&w<%Hw{yBd9l4h=Jd*$?euLn|B#4dz{N_|}H;eFw|$}HD>t{Im9_p4{j zJblrkd~N&!KA>4lfr-i0JGU8Y=?2d|6FKQMpA}z3#WBH(2D_J0ZcUe*Z-6ZY#U}(H zhXqs!9G=X;_y&a)7!czhre7Z#koCayf@qi}NMekjJO$$uW&Z1Rpfm_~6HG6N#$rA! zZG-$lOM58s2Zpne1dy0;pCacykRUTy2I@bwauuW&+QiV#Z3EP zVJHC$H+WbREE^dZ#I;t|O8~&vnJTHKsr_ zS=(yD=AEy;N-Gs7zm}Rj!)Vgq57WMWud_)#v-o+>ewh;-uM?+TXc9LyXnM5&l2Fa# z9sk_~I6yWfEjnHdv5|ohL~o51*tEM=!|&<=EBB=jw@x_8AKo15E^{QHdS4UEC7W)b z5{H(`KZV;T{Ce`f+ezh~2T%FJnT{%#uJJd&$Xa)h=PmDasYmyBShmU7Nae5B*u7=h zDc#F!gB~8J{xmB>;__NWN0!gMsqRygI>VXo{JEcbx1?|)->O}26YI*j`*x}Yg>Q(y zm}vMRXxj2G`W8n5?*H-i&_xD9^Zk#V)5hpTAT7&tHzef)}fgUOH*|L{lPvI?rtW zhg-Yl)fbxlT&I63;n|k5_LYCOl>a|xp*z3eW|Db$kA{g(@6S&RDgXYa2E{aR$)28* z*rMz(`&#kM4E}PLhu2e|ty1b*IQ30aiIGU)$;qmxjIU~icPV*1W{-XhiYG826jwk+ zOi;{FnpHjHN#)~fUrJ_ntaQIsUH!6wvE1SS%Zs;e2|c+gdl~qh9DrfK22>A@GawBD zAU2p!P(K3$SZ}baGg#xCB`XT#HkxogJ0btqWtrv%HiktWYuPojyw?0nU*q`p6Ht-3 zN7)OoT96%JHvrRtmTBLby#9Up7nz=YY*?PLSX+7r-sroW=E4=t&RCjds zoxFXitKutuKiBJa`Y+k_;n~9(`<0o$S=X@u%>wzE;Sj5{s>`#7mKRrTuLYwv_ICfAIr( z#;mgmwwCLkyf!j!`SI!_Ywg40Bwqi-c{+XE&xG4#+iI#nd5Gyp?53b8T%R7E{5_3_%zrZ$v;tT>%+zk~2`HdMEr_4}3;V=QliPrKz2|zs{ zy%=U92_P|HrbEIV&I8#E0#N&bWkNhuju9xv4rLSPrWv~wXzV6fIsk>&U~?NK;RW&+ z3P6fDBqm%5x_Vd~g2M!@TnEWf5?<(bAtyM>+(u#?@|+HZr7v2!36u}u>5Axb1xXJ) zhJXxgfx~-__f1Ke_EXmlyYqw9Gh^SYty;eL!Q8XgZrl(~*3Mmhf R;U*NjkvK?9lHCN?003tm(hdLs literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410056 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410056 new file mode 100644 index 0000000000000000000000000000000000000000..728fcd2b229e23b205059769a2aa299f7d5153bc GIT binary patch literal 6084 zcmZQzfPfG0o^J|iIH%NRW1h6Bar^nztnK35J=#5?>K{(+-TJ2hs7hEnLgH^kVP}SS z*jMFBZJC;Up<+LgM9(TdPZ#+$K5MuAD?PH#LS+BZqC1m1wDl|066*9WMxJv1(PsW< z-E^UA`5>E;79FpE*vP;LqPNBhY}#F`;dk|bmHX0%TPK|44{r{2mpKwpy|0Pol1(>I ziG$=nOa94LG916fCUfT09l2Qe%soFK$T{y3!~A0}kEn@#D0BDj4ri}edgbh~0Kez6 zy(U$L*WMRDf3#5T?d9d$c)qWgDSmk;r*zHrX*c4{zhy7U>E?;>39C@>`?2cX)j4Nk zYpSl8#1}9*{@-Yp-E(%~w>^K1bRIi*T1kYisn6S}%plsr!TVs_=H)3M7c(C}^TKcf zhy?+sl7MsygO9fZh@No&=8h#E$}?_XvCFB>=kJvN^OxhV;Kk~rmrj~K(Ui!a&NG|; z;nr?>^@S!s*Xf^1c($diedV7m<^RuF=*};=nPeW`qhX@c`|}e+%D=yd7&Rx_NlE}3zMy{7H4$%Q9{N)iF7S_(leSbU;D+scXph4DJA znTxM|1}T#$kZOP`XN0(u!C|Sl{fVLlP4`ke{AT@4ui0bDAsQo7o6c%-vexMCcDZL; zUQ0#2_sV&{DA6FJR#t_v?*7X57l-C7KRu&qRqAXbPAuU0r)_M6ea5cjjn zbLw2h4n1AaV!$%->V~*#kOtYasUQjj7{TTO)BA}x(D#=?EvG<{}?aF^)d0H8P z0iiCH%@gD%ukG5UX6Wm=)6ru8j=gt3Oo($g|FWMY^628z8|HHxS}C(V;s=@q_QTQ| z8Rp=dD{pCc`^Z{W+ARF7!((!z{6S~yk=@&_-FWQ-^#eP|Zjb|@0K^9K3F>EHU<0WQ zc6A18-4|E9WOnYKzIQ^0+1>^g`|@8*dFQy~?A%*#!>m&EZ}$QfiF=g20ILP*1G@p} z-*EFK`bSqsykbp#8kI5q^j*D({WEKBc?TxFI{!i`#4DxV##k(YJ6hye1Mk;Fwp6tq zzIAsOgnnORSfKUoN6u+ppjlx58oqX9p0Ppk%rq9&>{lw^QaHJtl{$Tt@;viSo-~~x zYzS1B!XO}ant?$>7i2#W!$H!b6AOSGkQgT@9UB{)Sb!2T5Wv)d=@kFKr%dH>e8;Li zm3G{_>X|Y>;P8Dvix8`Q5>r0!YxV`IX9@@n3h;3S>je|Uq-91!kdwfG5$aZlZNm9QM^@wWv7HsSVQQj%QyNO7P{KT7CCZ%?exjm5Wl% z)4YB%z3Z~Ou|FS_F2Uh)>KkiVw`iAQvGmPC@t6M@y}j~>pr;ocz)|2?f~_{%kLX`xr22wFi^!#*ANe|acY06{*|ehnrW^6 zv&m>mQZxj@?rV7q^;g`t&$Iad7RZ^VzTd!A+iHG?Auh^O_ z^4{?9=0$T_MYAUfc^-Y6FZ`JMpN7#{ORfU;%ggua_4k9~omEcp)wET&6*s9d&dpt; zu)2Ewod`~LouyHf7q9=#M_p+gMy zv!HRD0!_QHG{cCLUSVMllVc#Feqdl<|2Y6!N9X|UV1?QPrr-u)FC*c30jdX*pCD}n zP&+9UDvlE7#JTCy8ydR_YZ@ISZlff;K$R*r;t*>Z{SozUP5+a%+txKm_4sA{emehT zUZ>^Zd2JR+20!AZwR55I4em7n*|6jTYBRwAERBNNU|>K*n+wur6m5a)f$0U&FiVic zP{NNm^Hb&k%TZ#@$C`F%U=Jn!zz7y30VF0o)Ntix^mG8y3(MyqIj~to_??tCGt5!2 zwllJOL3J?LO&~=?*b7WM@VG@b2W$tD01^{sJmZerLfWHbBc z+704|R#lwOHGVYd)^BCaSl`YcLVm*8>kgMSLQO=^t8j~e3|L-;=U;+-L|}i1=gjpR zz&HZA9X(AU=T%bt2DTqqpX5OuhmtRdbW;h9-GsGF7$k0^B)nj4WO%wjiZ~=DTpB&j zk;@A7u`rMvN`59WylCe(r2I^To5bGzFM_2nTDge*d2c4>oPN!o|K=os5fBu5XLCcHg-!QufkFzm|%lPeU zfR42#1NHI1^nz$4cOo(2DsZGVi2cCw!VIbsCH)iWCK(#L32S*VNZdwAc!BB@D#RhA Ljz{k!!D1c&OB8C1 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410057 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410057 new file mode 100644 index 0000000000000000000000000000000000000000..2ffa35e1d04a7a346665c2f659a30abf40515597 GIT binary patch literal 4876 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410059 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410059 new file mode 100644 index 0000000000000000000000000000000000000000..7911dde3ff96236dc754c033c8cea4ba3505a6d5 GIT binary patch literal 4876 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410061 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410061 new file mode 100644 index 0000000000000000000000000000000000000000..bbd1823295d4c4f2a435d19a9c2207e29812833a GIT binary patch literal 5944 zcmZQzfPgirSD!xOaLM0wic^fGj_2i0k#{pApBJ6|m-PCV$GL;PKvlw$Dlbza3OBCc z=g~eMwBz=_IUMO?-md=kV)21XFQHCABL?ph7Ms|T#ymp_m>G($e0+b4Z-eDtjy?#$|X8&)fyKag-_?ah|f)93o8UwlyFbo|Dd z4UL=UC+XLR`B$8I^YEt8*`2Rg+qoWW>JB&99dFpPfw!`@_4{JgcVE+Xu&|z+-x~Z^ zd}8{^rOPhwKcN^G8(cL>@nM^phUSHmKRoMrk|zG#cJi?!gJ=sc?}Kfdm#2VS%zXUJ z3yVJ>76hD12GS`EKHd%>dcysiJC=AT&$xZXE~h%5zf=CtUyi?m7psq6I%)buQzCyl z&uspOTf61e7n=NBr++Hp*_N{Qm4CLB|37D;JHOy&l6iQKhKWw^&rb{~|Nf>1#WZlq zo}QD~qU z9;62dK=B8TLxTDl7R=Q=V7!;fB%qC#GMo zF3Z_=YghcPeO&C`65M;%AH1sR=JmsacdkkOm1!-rxpTd&lC@ZY27<$YEpLXk_|G*9 zn~JukM8$1t@QeL*?(@nBA=TQ{U3!N8vEVS^R;&lApA5ucf5RMuM8m^Gp!Q<6j)k#D zlU}3T_f_XbF3#x6X9y_@xojtw+ZOg<4or{dk>HC}I=4dO8=qHNq#RvU?08tL=AU)q z0lqJhLjMxa0o93nl)Zo{X9Bwg=x>R;t81=nF5M>n%_vRxHam;gHNhuRVK&;zKl!pQ z-MsU+O6`M(oqC%4t{{eD;j?CyOJ8{H7r(}AZRFG1X0COf1?mQeT#-j*dh9$WuC95n zusfvfXyl1)%<+aC$98Hh>zO~(UncGB(@h06E_ygkK;R5?Wwfm-c`?(`2mOT`&ooo z?UR`Dd0#WAjA9B14GQpa1!;kR^i$W8D`xSP_HO-^uQxf0!|haw#q0760Sn`GS~C}4 z`wUXWv~H;bL^T5=)U6Jic#MAiDB1YljOCP-8egb)jKuf+)%~x6H!Hu{%G8(layge% zoOjWWH6Lv=m-tWVV=dfj^LSU#6sI$`{Ih1%+CGO z_fF_A+uOimU;c|J?;Mw$oqOwTm{qF&?Ou?fNO_GB>^@*xlgjgDxv8TPdFEtG-Z|BV z7KbbU`zLzG+Y3BMZ%bYI$T%;k^uP>O|C^OR-pH4}6zUYy;NSeZ^Oxt7?QY*X71cO^ z#vp|JbH&-<=K=LiD-hpXHwgi`baJ4`NBz~awCoKxHf=V)i>IaxYBHbiM zV>f})9tZ=&YtXrklJEkRy(j=F;*gkdCEz>==fUF;5+-PE5l9{sZGr0{Scb2Abn?i{ zTcSYoHvp6D1SEUFOe_vU@jDUbZ>lN!M2h(g$ni~d-GY=xko|zg{S3!)6CZ7up)9BL zN&ERJ^P?j3ohP1IA^PCOa?8bn;mJlbq54z)0|Af?GlCJw{RfqUrAt_wfPsj*gMoeh z#~5h+0jf(t^)?*9ECC82G2zni_=Ji;^KlEl%1^gvt|c7lRdo+y5XIyq+bdZ3|KlYu|#6K@hO`2DPogfQa@rgY4Oq zInXvbC=a0I10)U-6KXiLodV|}%1E&Nz_4wH%Av$Jk#4e}v74~wk3r%#O2P}|FO;}K z;vg~Mu?A9)3}9s(dj0_E1?4-Gb^(duMLV}4mA_=T2_+1W0ttx;4k8dAMJ+fyKvGcq zk;)TLIDzs3*bE}vL~0oTvK7|9Lv|-D?a<3!usuj^e~`Nn01__XFe4%ifcmIjCNgN1 zNglBN_IIgib=fTOKX)IPvGA*|SGmzLVSh3^^8-FDsAJLl56EE(az7|O;q?x|z8O%1 z)P=V-Ks})R1k#Ue9x?|-9H+`_k8mx&(H%ZXgO(3_!!VBK+7$k0^ zB)mZJh!R&w93&<5C0WgUuo$yx`>p(S7rci}vUp1vw4HAFwc? niM?Rku-gmrFQ|VHwwKyt5ny|XC?Bb9FSH!QRxU%G#J~UmR@7}m literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410062 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410062 new file mode 100644 index 0000000000000000000000000000000000000000..9b942201c7bf38440ff06066d82f176f6d49d0a8 GIT binary patch literal 6732 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410063 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410063 new file mode 100644 index 0000000000000000000000000000000000000000..2ff80dfa664fde3f1705f5218c0e55cd8c30a679 GIT binary patch literal 6004 zcmZQzfB1z!}MbNBmn zXZh8LJr-u7dK&jl&rIdK_mb&*_U9Y@GxClKIvOtdv3$Sa%VQJkm=bKwQ%a2rV=BOz>||zPZ?j;3hz?#c+4LCcpJn`3=D+g z3aE}1h?zj@Q~U#;GL^^i9jo?K+HvoyXUhD5!}t9xLag>lO!>U8*_VN_Z2$9@fiY7&fi~_aRX=`lVgadOAwF& z1L>!(C0ESiEA8F-D_?JN6o=cX5{uX683Go@>$GMrzV;cYPNJyh7EC!K*nhw@CUxrN z7N>|K@!?4d?L zn)xi}+H}j$te-E&4>S-QmYzp~FIMT?3XN}kUTKkXbXBqAVX>Nj)`_buhnX9Jfet!yx68t|P286|LP zT+Ui%<-qi2K1hS?*;Eh(0*qjDfqoFSnz%6NU74ig>A#1s?I{a2jGOqUPkYko!^WbO z`+H4g#q@h8R(Tk2lZk#2Imz_vflvna)odcO>b(0y&R5T40mTJ4K5}?|e|@C+{GXIz zt)b9|a{Dm8Du(FLI?rovafv^3q|Jnpi(VE${cG5|v7n=-kWHu~V zc=to@YEYRW?osvvW)~CKEkJ*FRGkV`?hTL<=6tkp-Ol%_XZTomePQejQn#PFf$ezO z%twy%_h0P%o}t8YP<1-*zB~OP>z4URWv+c;&*{7S;}>428yw^|-(Rk6U?2VbPWs1n z3-+@33(Rd36|SpdKbY(!F^etrvcUxBM_2PM&1;f7`XEz;(?WTF)WaOF3;N5iye%<$ z1&UK}_!+)-WS+4>@ys+9)$CU)-%>caos~L$l=3|DPM$QKAZ!R!m%<<*cA9}f!vx4i z3BRO8CyqeHI17pktc;CKOpSm7P%$`7M0vmz5E>NV;|kUbCWy%A4B)&5(ZUFItHX9h z{hh7P6vL&}-vnA#^e?T5&_Ac(bY=h9j2X@zU+-S{b@|weswX{{+^VgPIn94(QR8#v z>}0FJy|3S1O7}5$;07AUYIDQsy^u=Xy&kc`IvKOs%OZrDcZK#|-}L3r!Ts+4!Z@L> zO!*H4KsGE)7=hehP&rW8GDGtYScr%)5Yt#1ehp|Gs2o-WY66uZZ~(IeD1gL-OM~MQ z&I8#E0+755D#NEk0DX>o{212C3(uRv;*tWe5>qnng-?#F-CtAMxg6O+z%WhZ28a1PhV?5)&S3AU`1k zSe`~t4F92tSs;L(Cn2H`5)@v*aLC;O6C~Ka0V=eX&UIsev~ecE^ucJPa6n?hWFc(; zI1gTLK3)#7jgs&JwNp_5Qp6!K;Y#3n3@U=J zoB+w8v`YxKch^0dZTyEz6sQlF4;UxFZG|$hI0(h>M3~?Fx>bo3^Fi$zcsWOOI~++5 zvLCRxpTTAC1HS{2C%&n)*31m=oV~D9YJ=Wa?U1``FWvWilm5T~s-K8_4DLIDl@si1 zA?j36+Z+R6aVJVR6K6iSP9@fStoe%u_E6#vj9@_$Kw`p!lbpN)PirLl9m9Sk0VF0F zZbM4T#JEXoWfQ0^h2D<`Ta4roBqqsjLdlavj|m|818yUbfz2T^&!3UuT4`i9d2dB~ zXW9#|@IQ^)O;*pD>7BYXroA@kDpWtVKG{F094PKl%043cz6|W^KTLu4e~p3qL1Sug yfaFdjCR`ds3 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410064 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410064 new file mode 100644 index 0000000000000000000000000000000000000000..b33caa50d45ff3337366c34615cd62184331b5c5 GIT binary patch literal 5940 zcmZQzfB;2N-Mi}@R|w41GSe)n71;gzum6kxO51<;-I{o$?u@G$P?fN>e2~nxEw>hy zbaFUs)ZeICH|2sibHbH7PWv9T@5{VlI`dBM*O=PZ6A z+9|se_@hBKB`rF>5Mm<(BZ%G_E3j#It%l##16J-!A8ws+l0UpT)LrICK=r;RmP zKqU^_uheAR$``o%bRPr zd|P4fgLf>;%^wPx8d&{o)MVFx>{oty=27L4RX62h4#%W2$Ul5qvsJg#<1;fLzRc{LBmc z1t1m#oC1laF!*>ofanSLZ|+#)p*-XE6}z13eEv@PKYuy?3SO)}dg-L;6HSTy={&Rf zA8zfIS6^uIbDjRFglAjI+E@PBQvUy(h3@=}$n0Gx*D09$rs;!3$J(~)mK!6c! zE--G3w%Y#VeRg%j?1UU~lg{|81pDVZWNwu{>C8L#GS+O4e0957)#iig9e=B5P18_* zQZFp;aD;iis+7bOwfwvXGr56gf&HL(qq=_1{=I51iY2vnnC~`f~CE?CL{$*y##4M z_5;{lpdS`|3VwWdXR^HJ8lF$4sw!6YL9%AK7azm)nP8S_1eGjyFX z|6bl5KR1uqb78mdiJzFp^+T|L1!OGP4~DNDnP+TJJTr|&HT#vyw-iopXQfUbr998P zlP66l2pa;`r7#GHon~OrFa@$v+@7@P#0jVvXF+j+m9epj0Z0*43{I!`2R>ygkK;R5 z?Wwfm-c`?(`2mOT`&ooo?UR`Dd0(?HP$5%5Xi$KUD@Y3jq@TK$TrrEUw0G;Te7(t0 z9B!vdEMAvq2v``e)0(;X+Gmg|)~^qtsu>ueZgrS!@#d_jeo(yX>F-&^lJ{oW$7s%2 ze*VkuH4)EF*L``Z`dF{=zTGNq_Sf7w;o2YbxmEa7Gq*pCIVSDL>?8w&Vjqx=7B0tkK^+DY z=LM+=^@fOYsJ__IASs)$Rai!#k+00hLCDTR+1BX3Z_kb1y?4xYfvWboyb^T>sV2f5 zi~+7-r3`00h0<(9C+7ay8pW`>$Vu~}%&NrX$^%(lAurl(oH~9$YzMm)1RcQN)oqs(h=+t2r6s7a!MNs@}Hh z(VX+&S1~eNfAo2yqT&r5UQj{>#|ObQ#Mrg~=%uX;OyAW(egk4S0G9ut_+Yu?=PYJ& z!0+CZix(yJs-$ydidAe|K4!XK+EwG_rZB?+s0O4Dte2pDP<1bDFPFawu+Z1OX!rE8AQ5O{L_{<4f4K=YU!Lp)uAfD9NQB5g|))!c$9XN2Yv2a$EUmM@K; zW$Q?=RJ==T+~lTzkfHNmVAVm*6%4bq&R#OyXmI6EK0kli2v zsT)9b`y8koBdE@VvKfeU6R>QishdDNoNe14;}>w;+p^K3!oEeL?xKp=oC&7h=Fg6$0k262zF7w~ii zF&aT)u>>Xjh%^8B99X(Vm<(ZIF&}H%p@BV=_yfb)NCHSqxNmXgW%P6a(hJJxpg4r* z8xsAFVLy@p5|a$Kk?1C^l})gC$FLVk0EtPmn?UIn9!^BJi;?ueZ3Hr~IpojT;0p02 zF>0H0)mgr7TdJ;BuzLN`^EN5#zE`ev75Yn7o+Q|Q2Ifg<*a6FFOlKjv6Nw4;39hsT zYUAOoH;8nT1u(p6=_agse~`EhrMw`~P3Uog9I2$lAyMVUpm7^gJ|o6WJa=Sa>5Eq3 zg;HJ+-3LYT2RxSn8Q3DuF-GCWzW7s@1h#K(ObCrHX1uy|gKR{V(<}4pLmS*8wnFt| zYiIt0%7OfjQil`KS7l&d|G@y-Uo`>h2eo_9SQFLGB+gCm&(PRSAh*N9 z3tkTl61Sn07bLm~G@L+&UacZ}z=#`TYXeDi#!`7?FR&-a^ay=Tlj6Si&Q zW&Mv%F3^;Lo<@0reIfKT3hJkV0TKOmagWUUDbTTlUZ5sOI)my*N|;DYs3f#64Clf0 tinf652ezXTZDORnN~D_%XzV7eX>^dd4JD0|=q6CxhZ=E+HI2gJ8UWPuSVsT= literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410065 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410065 new file mode 100644 index 0000000000000000000000000000000000000000..13b942c6a252824830638d03b2607ebfb1828eda GIT binary patch literal 4992 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 GZan}7&ZU3= literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410067 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410067 new file mode 100644 index 0000000000000000000000000000000000000000..f093ddb04064f16254e209f130ea08a765cf2231 GIT binary patch literal 6092 zcmd5<3pkY78~}S(sJk|4bo@eH~=bZQQJMa5`=RF64cy0MH z(m-zW`JwD5&&o?2o-SNgml%6fi?p}c`Wo`5l7h4?(^kY zJc|$k?$cJ>mn}T!JIhk#?6z&!F6iA&SJU`eM_T`+T0(ZU`!E%-Fe)z&&^!inJr`e1 zU#jlmICM)aOUzaEUawzJ?WIn2tK%_@(i6HX?~LW^?%pE3gDgSTY8`Pha&Be$#9gQk zEhN@hW*0x}BigeH`fo7?r4DeyGmFbz*TqHDYJ^CgP8?BNsrw`=B~@%!HR&?tO@1FW zw^#EYQi3<-I+N>KeB?YN9U?ieBj@I27K~V^yr$1Y7N&Tu3Z7*_7C$oJKC-n7?XN*zSaMgrfJ77OzG99|%91 zKVcP^UaJ55j`GIBQ1jCeB$fvWk;5P%Un{_l#0{4FPZL7N^U$7XBi4P;IUwqyoESr~+`L~3 zx0LMI#H|TaDwhqcuO%r^g5&JG9vPRTHVM3VK#bvNw>~wy0`}S;61)nf+9sN4Dd%Pt z$8rMq9UUbfk@u_k=j^2&y||U(^=r0rW10P zRC-v$=kA2T((3#7)@B8$97x!7St~Zz7OWKvZ0Rjiktcg zxh@Mg1hlnem&zBNfaT=w!!AV{0nrQgu&`-m0b!BU@IWY`Br}owzn-qH!DMxriA?|C zF+=Sg-e--Ud3TT5Za=GZHT?Io{MtO3qy|#yNxkLk1I`H>8NuH!vIq6&hC=Z-6l>y0 zEB6kp7-fly87qZbD0whFDl0W3CDj2R3z{!e4MEB}fX43wob45l(K%61Z%;avN~Ns> z6X+bj7a#|SR<_nw7AA-+@+U}ai8A6Ps00DU%AjNnTV7-YYd6Kjt}bddQgo}77L1B49=+@p;i4o9!$x;Ii=4$ zCCoW*np!B|HO(32dfhu?BmU@Mz`Q8^xJSQ+ENHW2+4UQ5%Fqrderuv@xQ4ZTG<7xr zCTwdGSZ?TUKTCvq*fjFhY!TOJp}R@Us^5)YQm%%#HP`dN6DS`*12n&mfFT#%*t}2B zaBPIYHzkhp2~f)*i0xG5i`KJ^Kn{*Ezu^LhBl*inF7QVfR@6Q?56_@$1o+N?@d;A@ z*iMDauZ#)Cj`{0b!}dA;g6||>B8G@haQ9Ord}0cOH_kMNo@J}MYQL*Dq8)wM`GDeU#Hlno!cUHl&@EROL{@Fgmy?7Zt;2_$ zu+3(w@sA52%kYBd!K*ZswRfWOQ`HUZ&I9{*(l;r<-5i>IeTanK&$WR({LbSy98Mg` zU;b1)kuw40+BKAOQvM5uX)xt0V}h|`{^IBVH-hbR`~|KBbvNof~PD(cHhYy`vY6w*Ym+JrCuDrhO*?Mm>?l5Qz6yipJ!y9+#Ln z5?lS$-G*m>_jzkN$~npB0%B75%9y6i(QgFX=lCmzA058)`7<+!A+K(IqNefhCDXo} vGrHW^;&f?%;08FEBl$=^#)d6T+PeVyZMUAWo{9cRwMO9g{0wRhwvqn>J?*N~ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410068 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410068 new file mode 100644 index 0000000000000000000000000000000000000000..7329c0c54e02dfd794a2f967e62554069b812f79 GIT binary patch literal 4864 zcmZQzfB=t)ldku^%s$9+(%|07pO0()?OT3QRea^;yCGI{n(v#>0;&>LZfAM5{raN! zTkkwSejwtpko@^MQ#PBiWbX_5sq?33(N*=EDUQcoHci_6UES?(gZjjUchX$sc+{R0 z>wd|N+NEp?vMFiN@$C>B85lwI)uPOsNdezIbJ=GFrGUdXa4M(qAz#3s_$?ZgJ_Eg?}Kfdm#2VS%zXUJ z3%3;@76hCEiKa04csqdT3HNXASmL2PtUh|_r0Ek)iTvq2 zv-uxx?Uq+xX!3KN{;7m#Tguv3{@GIg|D1*H{DPZF=HWdWCOW-8KQW~I`p=Z0FzmXzE|@Q}GPt z&QB|BeqK{PsQOpzbo3PYdru=cfd+#8uszojReSUF(7AfY z7U6gMk7+^uAPiKG>~3T>L+;zRQ|?Vuzu}hn?9hDK6aTMXzj5hc!PA1%UV@F{o5W@{rXSb=ehcc_h;w$_w4#)@BHuDWoMzMiz}zN?N8Q@4~WQE zqU{!=ckx=h|Ma?59QkQlj_ys|H!ay%lJdp%2-}zY zrqds7(#a{=KG6ge0U&)~y#(!ps(WdBx%^Flt!6l9T{7bidrjM8lb=~FU2}Yfz?1X$ zmu1`ln#bfA;^`LzWWYfBscXph4DJAnTxM|2B~8- z)P*T$1p5ydmS_GrD{xC45Y;=gZ1q)--#kB?7aVo;S9<)aJ(;UjRK<1ke`#~^-K~6! zu3h>4{vrQMCV`~WbCy`>xrQ;Pu8_-T1{w%X4~DNDnP+TJJTr|&HT#vyw-iopXQfUb zr998PlP66l2pa;`r7#GHon~Orumm{;<`58_wCKcrs2FEKael8Pgh|IvAmDbqM~S5^(X%q<2&9ZCJW9`tGgUCr?^&-neUU`Fp^` zpI_#yRxDxa(pdKW-OiV67cK~fENh6}zwq~_UHMKfbv?)5TmXd&%OQ!0d?HU>KC{Lj zo>XpP=3vg&aPH}eS=*mSJ@_&=ZBqu+PH-s)WTS)$R1Orj%)qh$l#jrGVA;XIAg;Bt zX%4jfz%Ua@0Er1R9TJyt9>{JGfZ7jC^P8Y@j6g9FD4TG3262-GjokzaD_D39Hn&j{ zUf}!%3u_RK6mdvQxC(Ujus8&V30ivqBnL`gG<)%Nmwqun8`(=$xfETH;R{sRG$5sX0YFQ^YVlv6v@EH4)^h*okNt-0_mnSFxCHB? zVoRg{ps5BH=I}I1M0=iref_%y&^A3NAFx8r0#ir{6Nw2|0kRJnz{*3sX_QDe+0fWc aSkveraT`h+CDBcwE*CZ85NjHRMK%E0O>=Mn literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410069 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410069 new file mode 100644 index 0000000000000000000000000000000000000000..5b4dad0b871b3bbda8feb9716bdddc0d68a4c560 GIT binary patch literal 4084 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 E0Q!ruDF6Tf literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410072 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410072 new file mode 100644 index 0000000000000000000000000000000000000000..73d1d63c7e147277f16d51941eae375f406b7b6a GIT binary patch literal 5316 zcmZQzfPh11n)kG+DXjVuH$lx>Y27Aay?bqsO$~S*@0%AXs9U%KRSCC$*0JTC!;!3T zKuMn6+j{r@sJ*Ksb2zx>?8w?TFWTlqL#3~KnAUZ>%T0{CGqc`A$-G*OXUt{Fa=>L$|7!V~v@;`z4z)J<=3j*E9)uN?JrOymA^ zI7>y=CA8!W`J>mY%9ZNivXWYJGms6e3-zop+FUMcOi`7Rjoiu%-DUm;&XEy)C zt=;nK3r&8m(?6B)Y)e`D%0FAm|DUtaonLS>$vnJA!$ha|=O>1ge}7YhVj8$)PtQqg zQFfSpt@vgJf4R%U>#5IHDRnKJ`lhMGNF?y&WYtr~SGB^slsq1@M?c;MaT5arp|}F7 zV*z3&koqa_uDx|E<3DYOp#d z033f1CIf?<#Oj^WGv@wjX_U?ry!Ie_d(qDSD>G&E)^z{ln^m#q2uPjm*;JTvMzFcS zxGj{((2x+>RrmF%M3?*ArC}3$3)1vtFKM@IWz-cc%(i&nx5}%uh5h~Ax+{F)MRpgB z7`WMYF`C!s&HH2V=kt1gpn+gNbWM95Vm@*5UFQ|T2_X}1dkS+YElFN9M=;0f>VqY1 zK1UfC)CCxX{W2IB!!m$+P}~kolh2`I90kP%R)!`9rbueQYG2x3E`JkXs~OH&m&~}s zUeos2?r;q8jSK=ZU?Ba}wd9Ife5Jixf92~Ingl+=}%;^%H>@B}|}h0#P7wu$(~c#cUl5 zV~-}iM!D~+&Wl`}(Us2-QWSF8PA<1C?7bdnFJQ`NR$N^YrTO2y<9vSS^anBj&Hgd& zOpm+T{P!pi&|%>4t3CML$nJQN#p)js74mbe&o7yfcdzW#;)gFp9&321Cw+y6-%6nR z$v{lC@cYiR?TMl1ock-+hbr-~7p9f9L~IlNt0%%@DgNR>?k**uT`1uPb_+24?l_0V z7M;}(NLp>4*dx2SkA1}q&Nch%Rp$6~i)l!RpI>~fa8+x*lbhh9#h2C}yR)^yfn6$m zX8+R**<#ZZ9WJxO!mqoYz2$z%`vvE=y*Qt<$w6+IG4w$~c^%__Y!SZe0Bejz&n?!+%HvqFczVroi1B%~?FrUBC zXcj5v1JfrWzKJe(k@UbE3Zk(EsJB+5)dR80)1IZqGX*E|xx{TwN?BeawAgBe>Y7VV z!UrJwiOR>|HULOH!EzO-#+p+Ll#fAW>m-mKWPs#OBqm%1jx>-9wjY>}!R=6>YLvJk z&P~jqd`wF>Va>;b#BG#>7pQ(j0Z0*t#Dpus5r=s5F}zHsK|VwAJ7M!5{;=4+iR^q# zbp204K7MW;pLRD~V*R8?R+FxrzP_I|_T5ouId(7m?x(qPTmLSBggi0%xJ4WmO9cB3 zz;=q(EQbToG8QAAkpz&KFw=3>#}NC0W%+TaN|d-E&P|dVXzV7e`FN1Hjgs&}&zHz` z1}Sj}&Z{`;V|YF$F}!H!Hl%z;jGK7QY=NaOTDb|O-Y2^6jpPq_E(0>KMVygLRiftX z<3di3_XGcC&gNsx{4AQ&wIa8lx8&#F<8x~v`qc&g0|Al|j6m)$sM#RDgW6ZzP(B0U zK01TAjNhIQ(0+P4P#>r-1_wy)L}J3F(Zd?n_5<4wOl$2>l_=?-I5$mwKw~$7+ztyb Zc={hCZlff;Kw~Y`h(q-90v_og1^|-1y14)V literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410073 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410073 new file mode 100644 index 0000000000000000000000000000000000000000..12c2708a1742789404390329814afb6510fa7d77 GIT binary patch literal 5080 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 Nz-g^@C% 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 PpQsUs=;Z}G(m@OWU!ZU$ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410076 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410076 new file mode 100644 index 0000000000000000000000000000000000000000..cf94b43e9a064bb822a713af6e836a228a28053f GIT binary patch literal 3876 zcmZQzfB^9wJ-f5^PpoAK;{NsYshFVHVF|w}QhODJf1dVJck8zWsuJGsw!JS`X_bfL z9k1g&BL3D_CZ0UjwA{3@Fh4EV-(+d+tab^T^5PHS|4&zX#9lYd3!Hd9bL;#G;d zySmxxZ8XTHq(#RcLTqGU1ko#0&Pwc=Tgv9EIV(DP%@XUwtMrXBEktd5^k;89o%_KU zsKnue`wE-u_p39#mC`a~vg?0-X_#UN_e)VtbOI5E#?2uS?JC$xS3=g-lJin)BE!iL(0FusX;LfT(YO< zB(^9!%)VB9GlRd}<>B?zXRDOD7EXQBRAM9&cyhApDdVeJ;ay4|kJ+OiZ-cmrfq_t5 z0o5@BF%w9AWW=vMHx66r=qYEW{G(eF)W*ERd<%Gr`Nd>Hth92mG2R|1tz z24Zl$0cmsq5(mo()LzWiu`u>%(rc9azUsWl#Ti}s3?W4!m+jc z4YST%=UJPlJeh3tZMXA(8%w*O-!+Sz1AgC52C5VHD0=}@&IEP~Fg>U{du(Aiel9r6 zqg{2v%i@F2lMd?1{%khmT3i3bw`kcSu1lQLm|4TtdHnD>ZL6u6U?x=7d@w1!?B*|# zed3xS{7^SITt9i?yRM?9*}VDh9#ret{cGUzw-4aM{&5FDzSK7o*`giyiRN8;%lG5DrZb<0MkG>K;7yPZ_4sQ z>%qy}dveSJ8~i>s}IwsTi(G?=1wxt8zjl&oaEJM!1ccNYDcnej+Usc?eJ zvgXgN&0hb4B9pm*<}uq%oIT@q7K@PCG?vwUFK7L~e$?}3VN>Gdtt)Jw6dvNXhuWF) z9|(YKxL<+Xe^5D4*fIml4^Z5L0TJQMz`p)%2{f#&fto;Z1P3rnfC5NNxHLE};XFtl zXMoucEPL-jwLC=TKIhD5(( z*pDQD#3aLQB)Um!WfLskG3-SWKw^^YCQx|@4=1AQT_inl8-WaL4%yBjYOb}rB8vYt z(`5O+!;i z5Nxxrdw6pB*+-&4+cp4G{RFtJPzDwUq4=E$^EbtZ{UXJDlzdEdUxI{u4D%xZB*Kox literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410077 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410077 new file mode 100644 index 0000000000000000000000000000000000000000..3a04690232c4a82afa74aafdd64a9f988fa11bb6 GIT binary patch literal 3700 zcmZQzfPmvCPD?6OygO+%0@4)r7!$_4N#*Ewa21wryUX0&+3)@v{PJ zZh%-2a0(=v!rhw{{^UOPW(sY8bAy7#QgMip+1_ljV zAR8QSAblW^wCE%skOGNu78Dm)85^6Jn}Q?|>cHw#`~#mdmB;ZNtM*jdaqp^U%KU)C z_x&tFtoBJv`Mj^$7pR^oAT%hz#}%v>Or)Q>mRvE5ue5jTuYA49Q5L1NduGa`HII84^8 zIPIf1g*{^y`|dY?0_HwBTDY)NvF4X^%#Jm&>tval6dopXfQ$v1*?DGzxBeSuow?4l zHcxpn+34GD=l?dAc0s>u7C8s}zMag#@8keX8!|wp$YFxaW^l7PX!zph-CZ-8ws+{V zo-4W3Z71Hb_|CVr5(#zgbCShC_Q;-11yLZt2sRg(<|6}KuYAvrJCthN)THX>wNvPa zzG0Z(v($&5#fx6pr=Kgl#=@z@eC_%<{_UrFXP%BTV+PqdC!3({$isC`C^AEcy0 z#)nDSitNunI)4*~cf! z;~L?&BtkVL$o|9cEw$eheq{AMEaNyZ>C&c4xrZ(X=q~iPe?8AbW;ZBZF)MAJCjMud z_pjM|Zq%mz(XHC{SbazMddro9e={bvSAWh2*$o6K|A7EV!@`~s$OV;IAOH#nW@tQt z1&B!R4D9RQ3P97m4Nwy+SR<5xSpwuDG2tq};ST3P@)-lneqj0c5N-^xT!E=0&P{LD z(AZ5Nx5L6~u(^#9R1T9FUZ8S|8gU4=AE^!lJE!^GhvMr=a>DPeGu8%5{b~QYT&UFi z$&*C4`H=hp Mw-Ly|<`9@X0MuV)mjD0& literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410078 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410078 new file mode 100644 index 0000000000000000000000000000000000000000..72dd66b70905f66127d834cd482c9405bfc9fbf5 GIT binary patch literal 5132 zcmZQzfB;`TVU=qPx0PAe*fsu4?pkp%EkgM3ouEBe?M_^53tsyXs7mm{4K;r21XFQLglQ)p1GxLzM8Y5qt`64KDl#E|muZ)#9X1DEXS zIf*UG4zsTn-^}1IcX@a{_1P+=u7y+IG?f^M1fHC%ddm2!R(O|^$7A;B$J-!oVqhQ? zS3q?vK+FVEf9e}+Shr}GVzKniLh+aX8NI#kr0jNEcH!P34gWo@UQ-zuxOXrxYqc>j zmj(fqgX0aP4-GIw`Mgk?DTMn8m`T`turekyyLu1Ee`Sh^Q@Ftw3gpD@^S~M0RpPn=^EkzHcahL)xR?JQZud9e>NFSY3j0y zUjOs@>BU)FT>D-rY!ki&RzxK`Bb_3^#!XCfct7Rej=DXO2Vw*NooQ1#E>VBs9E*ZZ z^R%$S8_vg}b}~ZZgTcXc%WdUFM)@7vr^s1dnR-0vkFxOLFHR~RJ z{B&(U*(7<>eqD>X8sF)K5zhNIxvMTbnD;RM7$}Uvaqnhx(D230ySrvGZST-!Jy&w6 z+fKY=@ttpLB@*h~=Ol|U@H;sG)4wcGJ#xGwvl&kOvT00SB)7%$Ws+2TP)H@~+ub`l z=3T$Y_bNmG$QAF?Ah$F9`p^KPfq)TgE-+vFPFIVW_aVq>Q~Gh6kP%Odo@#H+TFH@~*-JryUST5{s#*VUl(2KIyDYe(i8 z8x+q>V^Ph1rSdI>liOLT(?==KGwUzw-4aM{&5FDzSK7o*`giyiRN8;%lElikOn3U>X>q zZgt>RO1C;&`Nir;|E1Td#}=7+NiKibU#WF(qKc#C6r*qYIc7;P3?2K&l z8*B@X^X48}UdFI-(H^~{P+Nfdz-|DhgS7U4E{!H;yIKE~Z!v6GwmE%%mP}!gRq zKi4A3Kb2xpZyndZj6D{*Y}>K$JWkc)s;~C1y?FG-T?1YoG0SPJKoeQ&cP{?tc{}r# zW5PHK+bAK2S{*bcQ7T!Mq#4CRB|4+EeuV}_=6uow~kXOOy7 zA_LR|3PYHgF!PWFFyw%Wz;Ogu46+*pAbAZ`Pd+*G`S#%_Y8F;I97 zHn&j{Ug&W`ZXANc1g)(Di(il&vH6Smyah`jh&+az<|v6L+Jy=EX$RycWd9*^P{g6Z zg6;?8xIk``GW;sIwqQF?Yktj}tE<-k2opPN_;Rsx?&i4@PTtyl>&QnzsCg;>fdI)3 zj6m)`B)d?`5hCg*2KM!D)j{&wA z$stHglHCMKx8N`Y=_I&q+kJ}ZUmb!vjGq+pdYo-n z&H436Ufvm^pHT-|Z$RoFU>_2x-oV9%m5-7zHxSdGUYxdJBDBvQ2-L?3(+i@J+>gYB wtH6~8!S(~&G#8;NQSt?GZd#v8V>e+f69$RfC>cB016qf?*IS* literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410079 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410079 new file mode 100644 index 0000000000000000000000000000000000000000..ff584b34d8666c142fdbd6d1f84121697aa0b2fa GIT binary patch literal 4852 zcmZQzfPfH@p9>!tZH~ROxhj^?J3#(?LivZ1A9tSnT~(@|_c}ohs7lyZPgvy|!);}j zHFk|Zle<=2Op6fydnahmRl5@x+k)4AOp<=6%sNl}(HRGh=Hp%a_uSH0Fyl#d#+JQP zZ@&4Ze(xU0rldv3KS69{UBFrPPV$F0hq}ug38>!J#B#}| z8>qx#`{Clt;`>zpG!_OphkWFp|KDzP2G_0GQ=e5D9aKuRI+(J^_T8eaK<8Kf*V9GL z)!v!Az+B~Q>)GS#=RZDNQ2T{rZZ!M6+?uFOCG{PX41Np7JdoWatL3&gKi#5gx5W8X z6)JHmg?%dWOFy2HpImbNy_>iUSHb^_{ZqEFOyVxSpux-_+9J>UVB6;9DIga!A3rOw z;Q)vQ0jEHsDGWZ|4j_8M{hK?Mcqq@feZ?-PI-kE&{?A{Izk(O5k6t=y`b1MAe>%@> z{)bz;<<%FO{9LDhD&g6dvi6mKwv_)rXQ4a4;AWC}c#npOPVdi83@QKqrUu0{aLJyY zlh~r{F#B5Z%?$o>mxtF=pRH2rS~&GhQ;CsC;K|9Vr;M*^g?A}=JZ6u6yba;P3?2K&l8*B@X^X48}UdF)hVG$BUijnGi+S6NAD=q7N9<` z8-V^5x8hlDw{fa&yuaaf_Sued(|E=SSvO1=~4Rq@-UhXEC0C zv|!4A(FJRs3H4t8@1>|U`vS;7u=`W|1D`UL$MGGj_Eg$&@2Y3Y{D8yv{VYPP_DM|n zysz1pfw64?(AupGOy6~ZdSLMn(htnbC%AzW%N;*wF_Qy+_nut5D5+N^og-7MV%zdD z)BVz}8ZS4684f@-@bDpMAJmSQwwKG_1lVeZbJis@?y%RiJvRB7)zUS`X9zqwe}7rV z4WOA!jv<~dK|lr!q@TK$TrrEUw0G;Te7(t09B!vdEMAvq2v``e)0(;X+Gn6TiK3cY zFy)M3{{h2Nr&*SF;!U1MO)pk$-#h)x)Rt5!FB9Vj8VC+c!`F_?Gd3umnZ}};{YvFq3MaR-Qm2nn zo@d_4lcp1d4T0)X7zD&lGcah_1KDU{c~TN8##vBYU}bDOb`(+3{2~mIzV+WLfz_Ml(}ky`vb-GUB&lUL>;6T8V0+5Jz&t282dBZ|7yD3 z+M2uhOK$DDzhb7Ga(rvx%ef!^t~A;FZth=^9Zx5$$U4shG>^sATk%B7(HWjg`|cP@ zIB|yXT5NbI-*coV?xI=SmHM}Lpmu^wRv;S|CNLT#4GLRO84N0az<^+R1ysfK>q7&w z9;j9@g={!foDo#U!_*LIz6=TGgZu#V2QBTP#2*;Jf+T>%goP3$&BJ-1I06A^IHJ|_ zFu%(IRe;T+cAWsW8&OswyB8KFG_e<47h$)TA%D;DxPW+w26PWpIqV*N-dVz;~u6cvhqx6>1ikLQ0rOOt=bMX%uWf(0^~CDpA6mI5)k1LSr{! zO{0UvZIpzUJuu9v5r?W*fbdb0WC5@8kCXl~S08$_!G2u$k)x+vS^fU_6i!28=iwL(7-v@*_341#OrM@G= nUSPV0ry+DdlG{GO3=^dGG#PF}i4Pqs_5* zHdnH%tu0~cFtuRYfqmKbMuy7TcYo;I^kMb*MzS14YqfUrm@>?+VTa7D^ucob* zL}s?Yc0Q3yiI+X@KD%Vd!*am+CfmF2y*3TkH?!ZqdEr!FjBMg*^H-fk^$wCH=`1=^ z7WUq9X%9S^di7EBLp8boM=Vq>UTvHwwR-p4%agqHN*P336nGzO+q^sl~gB}`8(zR{N?y7c(MBErIV&lG$rz<^UUUd zxV2keeWA(Eb^50go^2^>U-@TC`Tuhky7LQeCYgu#Xqf2q{`|y{^6zhIP)q}t?CCj) zEy@nFuNB|S;4gQ1cs=#mDy6Q4Q{Oa|7>NX)oUD4v_^MWTmy*Y0_UOmkAZ}t{AQV?X zbu2*41X6GK+L3w22E{YeSX8rLseDV}UeNu=*7Lz^6>*aeT+BJ(YIcyXu)TKj83v zKZ_8neG*eX?`!r2YGDcp4GQpag=hwm>8GwGSIpuo?cMq-UvF|0huf(Vi`V5D0v5*W zv}P{8_8FvxDJcp>fdC`atqya!_4Wxpp8f6qhT}YqZmKNnyEgBf@a|rX+1}9grzUpz zXU!4%wEl`YQ_WGYz0N)S@d4LbnYmz%;22cQ~|K4QZN ztnQ`l@{tVO@3yzbj|S@0#DB0UzTwLXdaVeh^I>skO2ckgr!7L z%`KR6MrfQk2(V1mYTn&lv~kbYBhR0l=KSIrc|^)4%(Q1lVM&NXN4@!rVp;FoNmrU} zH>^GQUoE%A<(XU8ryE>PjVE@N&++5{8VC+cDXCC-!;(dJc3KzYJ?EbBVORB1c1E`O z4Ymcxd2>2B~Kt=oX+F>shD3 zYJmhJ*nPlq=+8QX9g7}EWZYA@m9r@JrKHf=m+n_37$<~^bj^Nyu(-8!*Lk!1MVZTz z_dcqPau&T^9h?#V^VZ2QmpYHcvfENzK(oMMapIRvWAY-oEuJrvq}qc*Dp}v|-qA7d z`bEB18Tv=Ac%O!bg&fFks3X8(QEKiKWMLUq5o}Ou>XZ{2k`ZQR7>Fe-+-wdSzPNdJ z*G#7E9lEUNN-lNViFYi%^KGp}LY@1ZWHFGru&^NL7NBC}umHOc7#7zL*BkwuzG_S5 z?kAHOr&g>N0|rx!WiSFZ4J;j#N_O5F93i@+D2`SnT-&7A9anP}|;e(Utj?;@3N|Db6nARmbdlO-=-66vM}8oLQ=z8oZOgQY=mK$GYu z2cRZu#GwMna-@6-;xb4uY*7l4b}*cAilyL(#>5ucO%t5EPW@B+%Gh>S>Bl8cup(gH zfhCRpg34i}QG$IP1_m*WMILs*Bnay7=mO2*g_;GXkP;>m6RrZioFc03AkIzhz)lNI g-Gnub4idMaq)`&x1nRR=BMucnv5cNZL1G{P0Q?W1ga7~l literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410081 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410081 new file mode 100644 index 0000000000000000000000000000000000000000..fbd441566ec2d9a2a39ae33b54a0c3ea46c341ca GIT binary patch literal 6180 zcmd5=2|ShQ8vo8Y5wgu#FOscfiLsm*4jqoQB(ii%x2#h-*&<77Ib(vTM2H zpwJN6m#ZwPu{AVVr_w}(dB4y1nZtFjQ;pxR=l473-Ja)t{^$QZ?{*+)PNXLc-&E#} zK8d)%lYrl9UfJ2MpKDf9RwJFHIFhN5KmuBPov(FCQx{`TR*J-5l}$EVzhNV#!s=Ta zk^@22rUw^YVk~trH~7Qk+MnB0q+e3}!+Z|0WOLt5PMOu0qPogbbYM@8DOJ;R9w8iD zX2>)M7hgTfx?S#yon20I>#&RJW-Yh!Sj0m3T+rxLk6XHIDV=CQ)9ssq^|uwNf)9!s~vB4s*hl4Nm+l4IQ@{ zi|1=GXdlVsBjr^x2~3nT3!-_%+ww^_l}u-hp1v1&TV$1j|S9Q z)4O`874;|G&9*=SA(C7@`9GyQ!-xr04ZL{7 z4)owTz3#&_+w@n>fe#PJAw-&-(B|E7kjKxUx83DMmeiT#i^HdURQWAy zV%FEHwaV{0BW3dIYP(EZ~EO^<9m>Rxf*)J6m12_MW7C#&|;0eUkO+ z=i-s-Z#Rs7Cnpc_=#!x}k|%-c1@J-Q2Fk?(_akGxCz`2>nrbGel!C;^vzqvu)#>x#RD+4dx2hJqp*Nn+2q#!J&HOBCKSGH1f{%(GuU-*AqkYvz+A?yWbQ5qi~AsE<&}_ zoY~hF6G&x&08R#7TnPe9d9|A(9pE{z;XsgieZs^pfp`_nJ&sB!1z2J<1j zNE>)H1SiuKa_ZW?o7`dz;KZ$sIL8|bPpx*lmr%X0PM!!S^KITfD%bw%_@j%#cM94`S#G~=z#T2;FzMqD z40=r?1$eW9xsczyY@BdmKLWRO2tTe2`v;$SaZITe1dUa{m{SDROU=9bnw$TjbWHTH zn752h$ZJ>QNB53zHIj%Y10C27v0c_5syCqX*l*ZEM{XDU_fELNEpj@QF)y@k=a1A&4p&qvB{uVR&OFEuj~(M0(#7M2B+B?U|H0PR!omS2M7XS;Bn3I8 zi07FOIEsYtv<_eyDJqP(*51rLcu={bWm_GxFA0Q*WBQA6G z8f}dkeG$-1f3s}AA#18fDYl=&=MXH|Zv?qMFEF13NFX>6>tSN82$^_u($MwNgV|vv<^;4E zs@UjU{N`J*Z&*X*UDl0^!!>WOioU14B8wM7kVO(UX@?%S{H^! zzpJr zT8_^7cTCy21D$69=S*S}TVYH~=I9rK?Q{5o--`?6(_*;fnFH@Z;7$_)?gMkO?7Q=3 z@4xrkP6`0++hEU$!tAh;0oz|)4Ce`R#I#dH?y+bE+3-WVRdp}_%Q6JF=Pky;QfJK!Iybg=N}z3siT5tF`qmH) zee;rm9sFOzyfENosLl_={uqU+vxZ4bVk?Yk$#wjNU|X<`KN6E32*}?NLrFNWi_eI;jCe6lR>ZF#F&2WB_KN_4zy1VY?x~0X literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410082 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410082 new file mode 100644 index 0000000000000000000000000000000000000000..71ae40a074df6e2b389b7c4e4b7bf478c7134196 GIT binary patch literal 4884 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 N`8jepZS8WC{U;MeVH*Gd literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410084 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410084 new file mode 100644 index 0000000000000000000000000000000000000000..55e0a613568dd2f70608a15abfdb800c9903b283 GIT binary patch literal 6220 zcmZQzfB6#6)DQVFOUWknhj39c2%2|m$b4%HLHD^UfuUTS!c$L0UriG|&kN)hfr*l6T z1C=ofanSLZ|+#)p*-XE6}z13eEv@PKYuy?3SO)}dg-L;6HSTy={&Rf zA8zfIS6^uIbDjRFglAjI+E@PBQvUy(h3@=}$n0Gx*D09$rs@JoG2U0A1=e(8MwBKvV!+AV2^-WT~xE^8qa^H0NqfI(FCEF*Oqyp7| z^nvvfv=6H8rS0YNHvzVq;hc5Jj63W#ZI4ZUX0>$9@fiY7&fi~_aRX=`lVgadUl5Q1 z1L>!(C0ESiEA8F-D_?JN6o=cX5{uX683Go@>$GMrzV;cUj?qvTrkoM%KVTY*Xp4(& zSi5;DllU!8*8<^}`&Ym4xL5wa#>VT^+y(J`*F^7##|idJ&a>ovR{rG9jw=^maU>)x z*rBig_I&@h87J6)27<$K-@T>X4^N&jXg1(J_Dl20)P48mwT(Vy1x>cs;9-y2W5B@g zc~(d-RS%Z2{^7y8-Cm!Ue1HRwWzPi&oD6U+nLgm9AA%h?}jNe7wxc z(mnes<_UTjd;_Wz_b7V-Q_ci-3(()zzBk)0{7d8ExnnL>AU%1Pca7ZY+f|0z>zIt2 zMMWAi{`QCFiuSj1HRK7D%#CgQUR1s3>7^LO{XMytX1?4Y#sYPNgV5xd&gNw=ex~|z zguQ)ix#^zkhr5n>-Chil+!cl~)mvWigx2lc`~E813uTq$qKVC&i5CS!Rb`bDoxYlF zY2@YxIt(0shOZr&XKYYBGmS+x`<2SK6i#kurA{BEJkPw7Cru{^8v@m(FbIg9W?;~8 z0kToTFKN+96Q~$xL2-eVv9YlUNB}AZr->*tm;yqB0(@M-dcg!SWdXCh^#Q03MyOjI zn2%%|`yGw!Q<3bGpU8HZ<9fz}`0wB1zo{OXZK&ZpWx}!&HtuxI`+I!N7DuL?=(Aqy zf9Gnq`3K$unROXYy#jfF=CSI$JwDBAuU%MNt?OJDeT6?Zhg&2QjGqVCwJGPTL~Lz` z+L`hn2!L!@m@opl|DbZ9uw@386`;HW21JB21N-_{A<(b}6%e2@9u8oZ00oekaA|N{ z!g(OOK>%t$u$%|w7Z6|s)rSx!0|RkxdijCIZUVU-7G8tRZIpx;D8Eo64#8o9)b;{L z-~Xs@rr*qOrS>uHS@-vFhVS8P?KL6(J{xs*9!xAgZTcP>$0<-6SDFE)(cEKDH4FsH zQlLVsN%orL5h1j38D5x1KhZ5#Qy2+2mZo-;I2Z`G# z2`^Bchysuz4v7g@f~%ZCPop5cpfU(l1}edH6Rca;J#0RgsU-?jzX4cOO@Qfx(O4XW z;&&p<&uRVSM2h*KG8F7LkQ<1u^O5wx+zz6#xS!$be8KE*C0!fO8XJ3vG)`e#*U%(Y ze^lqplh$B8H;zMBp!(7CF{m901F*7F874qPJCs4{Qi%*SZZMpMB!I+(nT{(TL+UzQ zbq#TDD&9e3H(|}kgT!qpX_-Vfq328Fv_?uC;>yRcd;m6!2)7a6$AKj)L|+Hly`b^~ zp8trj7nrW$X^3`VLYbS8{YR~MV)(X3#YDvSQI2cS(u(%QZ@Lcm&PSfry1qdHV=fa z%)2+UV6x0#sD5ny$X`%7j66w1Us_CKk=r_;i$G(3p#D2-%oc1Uk~@)@P}`tm0&pI@ pjgG%=C(=#8acG*l32WXTByK|~FGzF~Xxx?>afn{GBSkX<0{}zyZ?pga literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410085 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410085 new file mode 100644 index 0000000000000000000000000000000000000000..c5b81a78f23c3dd0e4be063d4d63ae7c68db8ee6 GIT binary patch literal 5972 zcmZQzfPjqSYs(n^ZIk!ywP|^gnDJgI>}|%?&qWXKAH8ps8RB;qs7jbyH-l#z$J&Jl zrgSdo)-i2)SsuIasL-Na2RZI8sxMFXdi3(;kyo{kORnx+k*E1Uq^NdZ)u)!$SxMg( zao3&R)w2O)Q_`XnLJ%7n7(w*bSb3?9`f%%nll|(#CFtlkLXZ&kF3R z0I?w86i76M!N=PHL{GSXbH@@7-0}0Jlj&%zVgqO^8e>7bmte`OfnDe(J;~J{rQO@<=@}bpqK_O+0%0p zTa+DUUn{R5r838enZrqG=si-fs9T;0H-6r9T>ziQT^;|)F1J^L!=33?cOW0*0m0T>1BQy0R5i*tbZUM%w40)+YhF8ffFf99&`x!<1xqJ%$8T$!*+=hV!aiHa~i zVDo`-T(an<8*5Tb=;U*bAsRe+SqhO4%lai>)h)3wdCPEo|*b4DPLTVuzk62I{ncm zot%>G6HQW~`oMY#+6T4crS0YNHvzVq;hc5Jj63W#ZI4ZUX0>$9@fiY7&fi~_aRX>3 zlVgadUl5Q11L>!(C0ESiEA8F-D_?JN6o=cX5{uX683Go@>$GMrzV;cUj?oYn{0xj> z{{h4D?QNl^bBj){y%hWCtDmi<)~vi;C%>6X1-rdVyOMJ^Lbtze@vFVbye?^bVrv;D z71*qm2&?LBxy~t9IyrUXq1C)VW5HoL?O9h-#bcf=$tuo;mu+JU#5%Mic@1n?G#h8t z35OKwG4MM%0Mmss$QeKk0w7u<(%8SuKQqJ0FFQE0BFMl!O+To@(9zZwDi2e~uQ8|oARSQpoj-(7C0;feoU7(GGo*@yP#H)Ni*+m`A&m1io3(Rj_f^I z`Q}HBHBwlB)k7Tu3J-AD5Y*4Wzy?wa^RIZ$jQH<=wO9O>jEntjRv*C?{qb5%^joWG z$MT(cypkdqHZIzucNA(1P#@S0K>rH7eORmRXTi1Tz{j(S2NP~wJ(=6J)mTUJv;TRm zKPP=RoqXnEc`&ZaHumd^MRn(#HrWO`l?50ysCF@i*!?`#4>Az!e#6&}%riD9o|(p? zn*B=UTM8$)vr?yzQl4kt$&;oNgbjh}QWylpPBSoQxB}U*_y_3+mI)^OU~$b^c)X(I9tQ$T1?fDgz9C?F%%sjXyKmnM2a5*3YIW2<3nZc5fvK&-)gYr5EFaph2g)kWyh;vi% z4jQ`&nCIbT^%4-Jp^)Ej_%WM~*CRV6cFoon%Bqm%1di=oB8Q6YYY=flKyHVH7d(9q61Sn0BP6=X73eQŝLkjfEoq_@o6y>4b<(M|30a|ItvX3nt9 zNGMpH+W-5XNloBqo@2P`I^?ti3v*>yDk7#0$Mow1sILLn1Jetlkqw86ql6!k=ELez zWTU}sEaqcPJ2bF|5`SPg8%Y3(3HL6@Psjk4m(kM!NG~i7LHQhP7PZ^#V25C@Phep} z6MMmZ1?=`REY9D^Gj~G9R^D5e;++%rJqai~V$vr6L;J>qYkRJ|DEkf#yc8%63uAD< z7bqs~QTBWeR1P`(z(NH3+F<=J7=U>IT(&~Zfm1Nc-~ym{L8{}3Gau+a;?2jJ2WVgq zCH}w&79;^ACOp(g$pb7vT_|lE68(;0Kav0vlVrES{Xm3!K<<}<*+6J)hG*ICt8?0y zF9)gzjoD$i5kmki4H9AgnUM#=BXb!Bu1U3>W9FUk$+sN#D5$7h? gXEb&b);vE*+=fz4kmx4Rm>)Ib5WTL1M>>cB01P2%U;qFB literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410086 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410086 new file mode 100644 index 0000000000000000000000000000000000000000..53663361c20d6d1e8990f9089f3cb0830aa0b0bb GIT binary patch literal 4528 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410088 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410088 new file mode 100644 index 0000000000000000000000000000000000000000..388d4569e7bdcf1d7690ffaa5cc12dd719c1a05f GIT binary patch literal 4048 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 NI7F{o;gJqv0070G+ob>i literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410090 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410090 new file mode 100644 index 0000000000000000000000000000000000000000..86779c9e3ae619f3e76c48d1e787c73560e13054 GIT binary patch literal 3696 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410093 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410093 new file mode 100644 index 0000000000000000000000000000000000000000..0fd9c17e7b7113142fbd46186fdce5c7d00c9314 GIT binary patch literal 4696 zcmZQzfPiCnHK)B5a5%VC%`xlr1ePmPdlWP-tL?h8vhTs<4^4*zfvSWvT5UtS-!~Y2 ze*euTmvb3scF5$ul$EV__Zf->E$Te8j{B(HjrgxAA5TYL>p$7<$0R>z&7&v#gfG;k z3jJbk$lnICDQVFOD~OE@j39c2%2|m$b4%HLHD^UfuUTS!c$L0UriG|&kN)hfr*l6T z1C=;1i&#cYU|hqxq|-le!7Jf;X>Wu#h6H>rnsBkXU3~jR_AAZV6+u&fNE~L$S({NY z=|Q6Xt=NQq;ZJ?*)Kd4A(ob`B{1|)7*Qv?%Mu|YA8_L+jfybv_+Tq!M4rIQ$Q|eK7Q8U z{tt)+0jEHsDGWZ|4j_8M{hK?Mcqq@feZ?-PI-kE&{?A{Izk(O5k6t=y`b1MAe>%@> z{)bz;<<%FO{9LDhD&g6dvi6mKwv_)rXQ4a4;AWC}c#npOPVdi83@QKqrUu0{aLJyY zlh~r{F#B5Z%?$o>mxtF=pRH2rS~&GhQ;CsC;K|9Vr;M*^g?A}=JZ6u63=U(Ey9mV< zPzf^-GlA5)EMt#aUL$m{fpzJe4DXuv&sFy;JY|zP_Q>V)w=6c_iwwHQ4gkYI3#J}O zgW^u2G&{M{!pOKHBGlhY+odAc$<@ctB+%9tClEA$8n_)C z-XOPw!xfme`;yYu6zr5$k|;WJ&Up3>#@0U>!Z%zuKHR}=b3NBWV?s(J*N3`3j<9sD zk1tFu*cCqS{Q0!XXyWvj=8uY=cisk>2R1XUCo1fcR(Z&WZx8l8m8!{^ekG2fWv=li zmK)4_^_3YC82FtW(Bcs0PjGk=)X%`c268tv4j3AFj|ks;%jwtn_lk9}hQv(g38o3` zep?N4GPj;-3Q~E_uyN5Iy`vy2z`i*_n+sNaCKnyDgJ>^nabn%j#Yaq?YMW3?kD{T}!T*#aG(9^;f>$5db# zo}G2d5a^EB+L#gCE>(L;C;l@izcCBXU6lK0dbps(kJO*nUwc)>GCuWYTO8BJ?Nevg z8}0xrv%sYo$Zn7WfB+P)FaQc$W?(weh6xanE*PW^8^O{R%uJYexFtXV3^||}I4{@G%zj{9VE~n51d71}jW{2R4g{xFo*J0UHh^5M>VwObAGW(lxp9M7uDd%uOiqfy6;#!b1Sv z56E$W+%99VSRugYR*;~yEv2yQWuv)EC~IE%(;TJMOT4=}f*kzTLri4Uftn042U?Fq z#gOd>*Y8kqSbl+*FGSR_3{0=rVJly_L1rRZgv5l}MrQd!oSW8)U@2dy=_XKE!NLok zZU>3mP|6n)-GrPDDG4w1@&%L*Vc`Wzo8a_7?e-x!WnnL0U|~WJds)HmfDkZ0!^>0} z`x)78n)(^s7Dt3Nvb{9-GakF?>1X7&8N;p>Dye5vgqC*PpQN_#^D~~ssjWw^Y+Tp% z>)i1NTyq}ZhNg*>|3Cm_!^%TOAom|s4i;`|P(A|@?Pvz}^)EG`ZD}{4KIC|STY_XC z%xDk|vJV-+>Kw5Bz_x)nR1y}ZU@mcPdU1rtZUVU-7GCi3caXS^lJElcho}*U=;bdo HDj^gA@L7$r literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410094 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410094 new file mode 100644 index 0000000000000000000000000000000000000000..7e05d0f9bb48272b1414932159b8cabcdcbc6645 GIT binary patch literal 4628 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410096 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410096 new file mode 100644 index 0000000000000000000000000000000000000000..1ce5bdbd0597e9d1068a26b0fbf033aea0d65cee GIT binary patch literal 7196 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410097 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410097 new file mode 100644 index 0000000000000000000000000000000000000000..36c7b0d4e761c76a35574fde0f04d8c00b2e4390 GIT binary patch literal 6016 zcmZQzfPhEc2;LDP?hjR?dK(j989b3 zeY&2i;`y1cx8Y#zmg@T7XSY=0W0^V54TP@$sgVv>MnC6pn6{u%O#s` zpc02iLi$1Owvll=U+456yAaU-bSJ~B8!K+<1?~CAKQ(oShGFuW)R3p?5s~eolH!$; zm)~%OhD0|%7s}P}+?N*gfHQXThOV0p%RjBiS2=KwEx$AWV27C0?kOLZZ4+CxOyw-U z|NSX@ygQwi%I}zr>0<`b7DL_#+cqyx0lAp@_*s81 z6%Y#oPJu*I7<{}PK=g$BH+L-YP@Zx7id{~1K7XhDpT8V`1us?~y>!y_iKayUbe`G# z54U#9t1mS9xlaF7!m}-9?JNImDgS@YLU(?_%_Q^i9t{(n-k+ZsQvUr-4T@>tl07{q zu|?To_O;@h8T{og53i>_Tcy;saO#_;5+jkolap0X8DG^3?^5!3%pU!C8^lcv420qe zsE!4QnLz4wp4n9WShRFi@28#qms93RTQJ4X3pfAV;CcMo-`FXCLK*m-92mG2>w!uq z12H(>fHXP)iG$??YA%3*&=4r;=2C8cfk4?{+|E>MTJ=Pk->ARih0o93nl)Zo{X9Bwgm>&8cb^q4lxccz( ze)(kwUo2Xhry|yxH0xO0w{5Z)rk)f3mMe4idOXvY{tw&ctz{19eDwdAXqt4+#3Mhe zDpK?`kMKa<;GoW3aDXLJO7V-aP4fQv*0!M|7yQ@fer(QUy6U=Q>OAbzGKy%N;~dd^-P%`aQMET zMTpfti7B7=HTyC!wk-e#%~l4c@1{UKDB%ap4_QDlmOFmVVkQUt?mfA9QBto;I!C5h z#kS>Rru(H`HC}ECGaR7$z8GwGSIpuo?cMq-UvF|0huf(Vi`V5D0v5*Wv}P{8_8F*7 zqNwH;OgSUif513-yi}-V^Ph1rSdI>liOLT z(?==KGwtGFuna&eT6WIN>8subdJ<}AV@|?jI7=)rN+CcTpFh>AsP#y<|5kdXH@*Si$*wq=V zoe1>|X+2S4m$b@5K74zy@2ONx&h#sB3@vkwH?iDc-m9<7kN`4B{CW$BBGwJ;AjLbT z%FW1I>9=9+8Y#JDUz=*U59?mp&MC0ctRUs)uQJnoka`5U!AVozJ~ZhoLyEDH|2n`1voBBG*E?^lraqO@5H#Vgm@%WB(Yrq^plplKB5 zcaU~&5P@V75)&i?2FU6`0+2KgwI5i9I)GJyi~-iwP&ScnTHQipH-X#^3$MZEHcG+^ zxtyUS4#8o9)GmazOJL~>oF0fMYhYypQSoj)>l6dS#zlMdj)GGQ8!XKLX;_%h#9rmY zZn*4)r4a+L8-N70%QL7SVQnfU&j&{@3&(OL97S?q2 zGakF?>1X6T#qdACRVuL}s=(KxYtpVyxu&XS*9>@;tn2%>f=)*4zZ@uglCuA7arxF=zrw|>(z}~ z)9UL_`?1_e>#NYY_SbM9M>8}rq(Et0X$IJ~%H0oDLwNiLsL)#CgBWxSXc9~xj7G8q zi3yVh$1$7(WI)p>#Qvm3LGe&YlrSeaCI!@FE%AO6jopMbjSdpGQ4(IDaXb`&6mdvQ zxDs6D40;*`=>^3vsO=6f0||}?u6yu9dj2j^puP>jGG_wZRwx6DgHZfVg!vCHYg{75 Xe9%}5yi6i`oDxY7vLCRxA1)0585Z8r literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410098 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410098 new file mode 100644 index 0000000000000000000000000000000000000000..a05bdbacfc141b2dd8172dfcb8c94dbbe8343527 GIT binary patch literal 7080 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410100 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410100 new file mode 100644 index 0000000000000000000000000000000000000000..f4be81c033177eec4bdaad32f43a1882c05a96a7 GIT binary patch literal 4844 zcmZQzfPgesonKyR+YG(3_&UB=iJUsRc-mTLm+NoY4#t^od(N2$R3-e~)O^xov-z)c zn)WuxS6y~mrPmwHWOzLFe$|#`pZ|GUPukY{=bG51k1Kuz3m0+=Iah2-dT6!1=6YQ3 zy6|`N{wxC7l(gtXJj6x@Mi9L4_bmwQaU2WOW`cUQK%tAf?`#d_+l-t}oc*L)_^gX$M zzPx59$K~HI+h^%=Zd)_&>PIe7nNEA|Ys+J}XF7>@h%MA#vZTLX@@?6LWvStdS4nm2 zZS!<3o&NUAf=PR}-Z8P$+0t=XtcZwC-f$gJr+v-uxx?Uq+xX!3KN{;7m#Tguv3{@GIg|D1*H{DPZF z=HWdWCOW-8KQW~I`iwErpZYS*g=U zDbF+SXRNKe zEWXm-t-td1CP#6&ohq?-U7jIeVZ2Uj=HhFg!HQ-~Yk=xtgu2yXcHo?+4t1Ty67w@> zcz;_txq?aL5^L4&^%d58R%=e5u%g5#_2U&ux0?KJJD#Ka7L~MAp1r8Pk2}1`Tv;sD z{UAJCY~qyzA1pj(tTWG@N&TJn3FfD*Pfg3&&g2O!y!%nbt&@S@$pM%)jDhwehY2#9 zAz>2#t1Ifo-P`3hIM@{Qdi%~V;*XA-$+>dL+*twIIh#RlXZrP_0Yn1-=*L0W1ROAg}7Z&{Byn? z+x10szWs;hKR&a&Ie}(@aLmK{jTD_(+ zFmUf+VAg75U@i><*$;9A7QhS@2>EOv2`al`)yw)q@xy^BF^fU7SH24%vAQ zTe4aw+0BnS{cS{*|ehnrW^6 zv&m>mQXa6S&TlMxah3=X|foX^%r9(jDkVB+~nXYOlXoS||=i7$lLecQCd zAJf9^RVJ=KpsjK{VP(w6TkBRmGMY5?wON#x@ifopZVf31MEQW`f#beayoFKyy+r({ z8G3wQ@3M65_shJJnwgYA=SHA%w2X18>gdmxV0MB+m*B5Dx^$$m>0my+H)f2(|bwg=GLfX z4V!mw;FdFe_M&?vsSq#<5^it1#yT2 z7# z!Drst=KB*59Xn?9;F(j?LH8wHPYO2t`lPud*Iy4B$Dp6n&Hf`3PN+|m^mc=lktxvf9%dIKQW*~mZ+M;|qV3DTzW!MbwEYWe zry}P;c*X>Z!|a2549LJ0-eCKIX(0-#7$toY=_U>uy9wlWSa`wH=OA$#CE*3?!%!m* zv6kz5@@u)5Z@C;Y`wkm}$enFAb&uNVj}}1h`w5sc{3^CyJs%@>_;z5h0Nane{p$^9^3xa8=d6E|D?+R zl{ge6F8FruXZzD6rZeX1iL8H{V)Nx1E?f;pX>~iMgXZ-^w zK>YxuPJu*I7<{}PK=d|G*V5^4zbu%vXX_mkJDn{J2mk5JRAW8)`OD>~R>r7H(|Km| zKit|aufEXa=Q{mU3D35awXgiMrTqUn3*GqzH_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ9q4G0in18 zDq;p=CXiyo*N)6HHYlE%#-f`2O66M$C%3awr;k#eXWq$^rW1q>fl5*s1jJ4=FlhJ# z+2D8s=>vhJMJKy~6iAG-pt!)w*x1Ct6eNLA2UefrANZ81JdW>JwWrdKdsjVE<_8?U z?`IKWwNGNo=Y7q-K=n)kp+NyYu3)`jBK_30L%_m# zoz~37*FJ+3v3`9B)4&LIt3!TK_wn*8$DZpNJKg&m(!$ocUoE*`mCr1y#D0O9M#-@U z0S`{?cI~M}D&E4V{$3*f(+oYnuXkCx_WNaLOfoc? ztdy8iJB!_ZI>W|Ad-RS%Z2{^7y8-CmQjya!C*lKUMy^($T=<&n(tP&mtm(bWv zAh*N9Yp}VElJEkRfz*gYaF`&~hv4Wto^2mId+WhJvue!5D%rQ~pL8kB!}F9?W*fbdb1>lJElg3k4uW91;_*1YJF>oIy{cAic|no;{(}VaC;d^0EtPmo8TG%RgQh6 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410102 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410102 new file mode 100644 index 0000000000000000000000000000000000000000..c56cfdd6ad3eebfc6643b7ee7ef46e83214fda69 GIT binary patch literal 5768 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410103 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410103 new file mode 100644 index 0000000000000000000000000000000000000000..fe975c297124754068505a57ebe9276723b532b8 GIT binary patch literal 5580 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 QKmwkUIK-Ncq4CK8019JomH+?% literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410105 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410105 new file mode 100644 index 0000000000000000000000000000000000000000..8362ded3c74037907fbdd4e6fb742064ecab5181 GIT binary patch literal 4984 zcmZQzfPk$JbC0XqWG(s_uRN#gsmI*=W-UGXVRN!X4)Pg2j^lg`R3+S=YtpH1T6<^N zw^d&*vgoch^YUkVrvBnxyq8GIBa0VIUp}iCw7g8zdFrBg)~cqrrg!Pw?5U^4mvP$Z zt+;DcunJ^T(xMYp5E~g7LG;z4%$rF8-#v5LXFqyjDrENl|BK6W^w{>V-smJR{wG}q zsKkNUVb%@pIa}K&)=aA^o8~gVJ2R+H#j@huw=CC=DHezQD$j*HX|du+wZ7m#catM) z*W_=lU6W5utKZ$4)1H3iF;_?GV;@;v4HMHT-c3x254TtIoO1r~CQfTx>xGL;d>Yp+ zR=E6%Gr`i~9@94Y#Rum7xXK{fV#)hp+vepdAQv+qKkFX@ zau*bw0*R(D_;@>j=xv^^rPJSjSuknO);lJ4I$IhJ{?nPM#(MJem&;MDj8T`S^UUUd zxV2keeWA(Eb^50go^2^>U-@TC`Tuhky7LQeCYgu#Xqf2q{`|y{^6zhIP)q}t?CCj) zEy@nFuNB|S;4gQ1cs=#mDy6Q4Q{Oa|7>NX)oUD4v_^MWTmy*Y0_UOmkppIf76jwlX zEI`ZzQlBu1|J4=s;_mHo8ysv3dcA$;7x72O&E#CUWbUj0?VQaFwww&WFfaisWd^B- z0+4=i91_$IOgkX8!LH6=twgA2u!&a=e6aACvCceqCiQpPCzzkMJ~b_8JCi4{@a{(y zw@#2j;@4Y16tQk#2Pv+%+SGct*oP~1LGVh^4eR6U_T7DS>-9wMf6GpKvUNOK0WuHj zPO!N!^GVQ0I6Q%d(%9|bcm=r~9Dl$(l(cAX=IyYFtXdm(xGU6eTG5|-_*?d__=>42 zFY0ZiU-+)`^jEEUx7N5ipp3b$Dyc6>=HuevBi#k&S51zTYVTwNnguq~=FX8P-2ZrY z7@c}FZ@xm>qIZ>w`C&7TM=!8AU328FVGaYolLG^{A}Fs<2H6k9*Z@czEGJNVFsD$Rc*^cgxU}o9Qu@@duY8@~ zmv=ZM%h$mPTZsH1xz^;*eyVR*X33o)DTqz{Rx<0`4 z>iGyywzsFE{9^nrxp%B7NDph(WZI;`-uo7*dG>1CA^O?BQbB@RY=U%@pQxi#OWJ&nGBBtMFbMl) zFffJ{0rjASAF%9%g(*itaeumlP~#o%;`f8bN5 z@;JU@)t*W_?p^gvnICZYzMn;i)jo+SpZ7KU0u?d^ga!rpxPr7m05RdhFk@N+R5>Hm ztqyZ$UO&3%&H{$5c5!iW%qQlCPA%VQV^Wux_$PLoQc`}}4VOtNi=R9$vS8>ased-X zxcTXx1<_N4Zy&oVxqY?O6fU57Ea7sAMPVH3vwN9W-tCk3src8y?w{>`yluwYwbyu~ z9QH%)O!*H4KsGE)7=he>P&rW8G6TzP3n-s~h;U|LU;m^68rGmB!V1$1qG6VxGT|z~ zaS7+a>I$g+z_eQem16|er!aLyx=Dn_ZUVU-7G8tRZIpx;D8Eo64#8o9)OG_$U$~6* z#g$0~e@rKB@eDU@-zoU5;~OV7RDeq}K6bR0Liwe+3SnDHbd@=w4 D6+gw9 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410106 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410106 new file mode 100644 index 0000000000000000000000000000000000000000..4006a603f1e97908f69b71b22a9bafd090578a83 GIT binary patch literal 5080 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410108 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410108 new file mode 100644 index 0000000000000000000000000000000000000000..16f2f5f6da07622db56ca83939c1d46fa8d10770 GIT binary patch literal 2968 zcmZQzfPmwnT-Mi)+N~?io&59%i|F4oWp381oFDt%4x0HV%j~EvP?fN)Se1VK&C^rl z?2qIa&ra7d&Nx!TXZTd+#yRhePs-%~oL;BfBlJA5e!?el7tZioQaZ)AudZ1barE6h zmfv35XN^HNB`rG939*rZ5kzl|71*@9R>SY=0W0^V54TP@$sgVv>MnC6pn6{u%O#s` zpc04B2(F1IWtG4620uIM<>>RIF6qaXx3g2XU75$n{!A>mk4LYkaFa4yxa*9vr>5N& zb8`A#8oHeOvrl-1S>?$&%q)gNt0wzs2L&c?cZ?DzdF*H3rcZ8{P-oHZM;BxtRI*+1dNf zfLIW43M87s;N$H8qPKavmQH{BWx=F9Tkn|I>1=5@_)ll18tci=UoJ^@S!s*Xf^1c($diedV7m<^RuF=*};=nPeW`qhX@c`|}e+%D=y0QcS|5^U9zMD1YE9nubBmW3UN3XWm;9NOWE6Y1JcLxOzzFMBkVc0!QX5xwP{U0|hm?bBf znD;PqUkcA^+jqBuRbPJI333C-9}NA9HNrRIf^s(0*h~Jak~*K+`6(g9Aw-DZn}3h| z)`#byez1b62k8LMVeJi(_mh9tbMbF{eCW)*jo~qS>XZ8G z<2Ub{dZI^5@QXe3uQQKkbU!-$?81>Uwcn_R<6E{aW%r0+_+Jg5nGUVDSWUKMa7vj2RlIU@;>6&meW! z2$T*$VF)u5W*)Kth8$24IF8_oL3V=xBu#+ z1QcF_&25x~7kZqK8;9U9K`Yl`@e7h8Hh&SHw_phbk;jnJ93}BYyD%X??SR~b>_21< zia0b_(EWfM7s&N5!||ek>s+@Y&f1IF6-q3ax=z_^J@2k=d++DFCLf=)!1NK=Jj`+g z7WyC>WEV;~La;6YYLGpf3eWo>Ly!R$522)SqRii?bO2ViAe#+hV=*7(2be$L`I`pz zP~r~^XCnz9G2y-i`3D)`N&_IhAUTvgL!#d?>_-wnV#1w6yxWlS3^8tk)=%i|6tKle d4nbm)>?TmUg{Kjs+dN2m;5Gso*c<|r2LP<3&qV+L literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410109 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410109 new file mode 100644 index 0000000000000000000000000000000000000000..20e756d66d12a96eb7c3a23ec5503d5c58375003 GIT binary patch literal 5084 zcmd5=3p7<(7(N$Kl55;libN@Sl*CnWb%jJ}J;f{sdUSBPjQ4qrSU-^!tC*_3A?oanZCPnbB$Y{ zWZ0vFep4B=yAQJ(e)Z3`p4dPOROoWuGd0ep_prc^qU5faN7vMtl2pq#wq+MH)2O`X zaXmd&i|XpK?mjfV8Tw*Oa+$(-DH+Y&g|)stlGAwMn|3x*oF(mZ`gN0594|3yY!T~7 z_T00LlUlcU?MB`hL?MbX;d#O3Q(h3U_?`P{C9aSXjxxygLRPJ0!nwf8f_0>|BlA!Z zzfRvoJD+`{Pdjz?n9_G051pea&Xq@G(x?LdL6tdK`XBS=mwCL(4~{w0lONJwu}CK) z;IXHnQ=EprcKpY7#H+8@+s>6Ot$Z(iL)1)WTJ?^{o2en@f*S8vxoWXlhu=o*)SI}b zl&fC0uu9V@R&7P2luKiQ5EFt7mn$GA0e)gozPZJ?HSvVp`mL+|I}=mnI$E55nDw|< z!kSlU`U=S_ut!po*&qf+KosT-%^TbY2gv6sAw}@V!nL)8N`Wnn+IzAsXnT2;K}C1F ztuw8SbH*cAyDT-7qe|?7`azB-ols}SW<(~$%Zu-Q8D!^ni2w9*x6TaNr4>uGw0)+B zDNEKx?G!ILKaW2DnGE8SHkS3`_~wZZWINMJI2k-kX`lo34F&9;rHH%oJ?Lj4OX zvQGup1e?iW<-AU{fa?##Yp-825kHSyJYF>KHf9CvLoon<_dW2}vC%7P*dM5Mv{PA3 zE$-q{!`-|E=lhQp>@dIR8bqz9PhY5+ahda!`#wtW(URWUez<7C7T*BfCYN;e_fkM3 zihn`Nc=e>*apRDnAf#5iEM(S&Ch>&KX-RI*I}-cY`Q9Xd@w~x#$~rS!-CY(IA@r?I zyX-CK2D&~@7JeZN26#r6LlRU&X|?Wl+b)DIRqWe6_0KH_+C3bY4)WBM)FKtWpaRO{ zu^1K$5H{kKfxqaWp!h&6bedZHl#a#POpJ+H>M>zo{W@kFyMdwRaxSQvZLso&!Nz8< z_}Fagyl3GYv3Ns&9`bkdblsG~P5NS8l?@zeh!>4TfCeYp&A$I+rlORYWCu&#=z*$h zY~*u!w`nQmt*dVb13B=nt}R6n)kA>B{Y1flI?hFjPy7i`#&M;PJ665<3R_XxgHKbE z3zrRE#gsQ$G?SrMZxdG=f340?2S^p0zgDn>(jwrX*jk{XNV%1~kFf&BuZQo9oDEtw z_b;W~4I6(d@r`w8jxk=g|AWvviWT}e5m5DboI$BkLG_Hp87PZ*@ex% z)$X?3+W(Ph=}Vf0spBzgt{^%+yX#c{wn56>Ey!!PxHkbM$^L=tdB#YV`9kaLJ%54SMf`6hF+NJ57dTo2?g zfZrgDgBVbT6dw`vPh-7x-!lUJNH=TI<3u?X-i+eS(gKd*g(c7Q4Z1QTvN~PPy>iZG z&Bc5h_)fy}4j*_88{-TS&P!zKO+Ea~9ppBNK_YSO!oE(dU=6{c5QW+YHFf}d_F3#i z!?d6d=)vzam>S_k0zx7Q5u#==&i`S8a(KLmy8lkFeNDUu`cE$Ik;Ds)$$&nA#}Yn6 zb%_u!;{Gx0T?|j*r#l%L?`Qo<1iPTFi5eQcn2>tH7hod$_pS1T#Qe^u6~Eb3Am$p_ z&23wCGNY*Vv_phf+>dgnZFBZ1ti!_W)ei=qM=(zS%RcNG{$3FMRD?Uz>+kPBv$xdte=A#bR^V};w>7cu*^}$1Y?EAi|8ZY3AWGr$OtjP zJNUQA;lMtErt*0}Kt^G8OyEANbhB&;0i$FLmn)fa9_iDI%{SFDJ&FpU-vcOx*9<;G zCLVKmMxaBK?;EkMHa2#y;M<6qtQ=#07C#a7D~&+^khL=kcD}}sL17^=NF)(P=o5L6 l&y0cpU&3<;?+d7|Z~Z-s0vY-|!DBKicG2(Xq3sf}`49dG=CuF- literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410110 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410110 new file mode 100644 index 0000000000000000000000000000000000000000..e9ded78c9b1d6706db1aaa168a5b015bedccb3c8 GIT binary patch literal 2740 zcmZQzfB>nJG83wvY8m@PJ$(N(^M;*3qwB?I8?&vmKU{ps5a9I{%yW^oDE$f?fuaAZezkB$h#Z9~U zlcB#!aSzC*q(vvDKx|}S1ko#0&Pwc=Tgv9EIV(DP%@XUwtMrXBEktd5^k;89o%_KU zsKnvYFP~XFs@l5*+O4NPQB&>6|3C3+piw)^Sy`vGOQy`=5SC6pccAiT$|>FX5{>My z*LU6aeU-~OZI*<4`|QNqTKsPe_J--l_T9dDc7ulrbLYuZPFIw@wEnX%*WIw7-}>s2 z`yV~>vl5@Tp69dm*xMTY)Wzee!Q>+cmDu|=tl07{q zu|?To_O;@h8T{og53i>_Tcy;saO#_;5+jkolap0X8DG^3?^5!3%pU!C8^lcv420qe zsE!GW8Tu7#gm1(Jn>9)2|d6+r|!SADX~$-6aF=9Nd>77c6A1+Cq^xU`1KaBS|Gs)b{{Z}Ea%|7q;YoA?tWcq!{b&B z4{HCNa~Ck)Xa8gYcW_%d@BgEp`u5lM_^f$(i%+sZSNL?)w9<=Z68d{Ystk=5G^m68 z0}2lYoyo^upXuH)`PI_;kPZeu+3jo2%=)TSu>W%7js2VKnAbwX!UkkFNIejM!y-93 z*uyixBrGgc+aNME$2GDv-NP>sGb}dUuh9CCJoE6$Raa~Bwx3(PyzqLNOTOgKq$H!* zyX}EyAoE~hLC`Hg#n!Vg-7kk>Fs_R@<8;E^cXCl)D0p4l zfIKn@G25I8)Uf$0VmR$xFdodH!z zZT`RjO>3a|fSC){kHkS@f~7zNIR4-~SXzbJ4=i^&p>m9%{0vh^l$(~UJwRhO!SW&~ zyat=wCJCttfx-)3-VhO&@VrGtnFPsO8yD@-I|_Cm zvU_1+0v4mTy~>B(7#OhI%b+uB-<5f115DR`uKYgv&AvHV-n&0-unL@d`Si(vqNEgG zuuDnIm&BAaOus%f0K*!dm*Fu6WFQ9&R2&vQU@lSSzg`EbV~8~$Ykr}DJ(TzZBUq3G lkeKk0#8rL~mG-G!uYg^JJq^Ob1ndWD+Y7GGu-gj{C;OJe}t;=RN1V=Y4_#xn^k0vwqo>ty7ySMRWkAc?rvh{NDAp>eUHm-et4eahPmOM;M zsJD|f8|tbyxA%QLpy9qbT0JE{@j-z9SzqeApYzvU=-NgtvsO~bO0%{R3W7bkXV#^% zJwiCRXShk`F+)}CS}P1KvicbD^%5?0XA8M%hXZ91yv;wx0u__0i2?5%-}NEAzjtS4 z+-?l0in>{Fnc?X$R!CaM-1?j5T4jNu@0127pL&}fQ}dzw)liO%mL0@}yr9Wj5Sugj z#g6jC;LM)4slA@bCJD8owAR&8^YFqAdi` zx@9rt#Jig(ck=LgS3^>fYsGpCZe8+#5%bjEE8c7aOYGJF&w4;M)+TUYU`Y&0el>DF zv4qj7YfQ*LR5?jVRpzT79%*rn!nrmli=LI}W5oVz#Rc8*>lzK)2J`ntUzy0KO*QGS z%R1XHz(9t4?u+bZ@Y zQC6oRNTdkjQ9At6M3uGq*&5KVWg@l+>o(P%Tw<2h@TFykC z_Nj;%!%a8F+Z!+L$tT7=S19E+XVlJ7{;XwOgoCNddWm3ynGk06{)PrrPk<>U}e zKr59+ceEAHvn`+o#JyKCW4vjOTj3;Xrhb5rAJ{{vBqN`^lCiJ-4Ticmi+KX�!iOm_z7SH7^M2>`bb}NN zjbG^Z#C#dqS6mY^D$^}*Ff3nM3!xZH%5rpysWMWp&wSV~UrF8K{42#-Y_&AquWKbK zpz?jcMDb6BvVw~7)R5iyAB%!6I4-$No)lSwf0Y>)kU+@shw}}IpNBnp08iT@UUR@w zv8cP%(j&v6s>e>BVpt|~YPgJU3*nBO2VrAC_%VI3XIlKgyKoE4@s_5)lFuu*`fuMk z(Nqmxj}c-u|47fO&gByd;c)myS0X>NsFv3 z9@s=H9!_v(+OeIA6itB!WX@d!hLb2Du}{b_Pdwl*H$?TIx!gw=cP3py?>zW`!WcP? zi$pGwvy9{gXM|xz{R7UCOAf5#;5Qc2n;WL+*IyYEj2(;Dw}$OY;sw83^T8n!6Rvs= zp-)p>Vo0(lrnAazvEDsrT3o4?Y^Jxrr>lZN3LmMw45gR{yUehh!FqTdi#c3JAZ^zA z1Zc@tX?3DC3cR1&9`|6{Jli_^?m}g_W1lCfNW?$*ekakjX)&K0rj;XK8Pkk8`i)@w zl6b)!Oyi1cz$K#0pzjZ7^gj+i&QW+TmS4u0k2%Eav+C0|X$=sRn^~fM2SLP*7mEop zH{X7hs?0y-7aBdesL}M4a;=jmfA#PLtxayRbg^ni+mrm>{<4y)hzbNL#Pbw=Q!vap zFlMpWIC0ZK$Iu+!-$g2!W*Y#N@SCp(_F=UtVghpl23#U${gdYs@ej_WE2@db6dBG9 zlZnq)#x&!({6?^SNxa~?Z9X{s_*^2~*_a74jTSuU&V(%LD9sOrbp_lH)Ty@@CaDJ2 zqzS)<>om~*@9khuvp>`g6n7ae<4Vl^*7%2 zUV5&rwiHml3GSSDbQ`Ce0noJXJ_q~WUTs#NV;|zKwdefXldC@GfCs`1_&Lk}0E56T AcmMzZ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410112 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410112 new file mode 100644 index 0000000000000000000000000000000000000000..18b155faf91f7619d8f337bc538079e64bcae1ab GIT binary patch literal 4884 zcmZQzfPjfR9>*6Qmx(P?d}HL*_Ulhh``Wu#i)S`T8-%EapSbuAs7m# zxVnJ%>a+DAo01ltm=Cd$fe}PsEy}!^6!6_Mmwoo57p6jH@BhEJJV%dh|LToS^5TEe zWq?W?KL6>Nf7ESBO=n8M&m_ZD7rKMYmv{W=`6xeQd;%dU4AZFRk4Dvt4!<>`Q(a-Bfze zn(y}9^Hbhws7L7=_FijhxX*sB`;W$v$2+P{NG!R_xHRrLgJ_Eb?}Kfdm#2VS%zXUp zuGl*u76hCEiKa04csqdTZJw^B)8BqsFlo=$J0^BITN)1j)0wHpdh+v^%TcY2QJ1Fk z%;tZ%wOd|&p~=s6`lk|}Z7FMC`DaV{|8o|)^9yb!nTPjinCSHW{KSy*?{8{QOaqtf z={bol$_}%y72nL@FL!x(J@wftrLKij-!zpNi3FaUta{4$s#bWHlE-8A=*Qb2Zem~{ z6jwlXEI`ZzQoqvEsCed!Dvi{zDKE6EUBnISfS#{Bb6^@f*RB$g zT_$kNsCCU%O$94S2i~81=ByLs+k9-&J&ucq?SSgUJ<49dlrw?d0!$CDVpxwEiAn!I z{i&(TmSxZ99SmPAGOhFUm$z+-OZjA@o%LC1rHssixygSyrY+gl?mzw2N|nCX5)S(O)mS&)634c)JZHpf2aE}$ajM*02c2RaNKer&o#Ues(D3SRj%2U5X)?n9bt^2xP# z{OaU*)_i9*;5q9XnDYPg#j2TJmrn z&rE%jlrOGF*uLC1o&IQ(PEN`8i6$V&gY<#*60{Gh?xpSJ@;3ptn&F&v$&5SfHEoYg zerC0F&G8umPtM<8mT?1U9+P8;r(Y0|0R!o$t|eE@;w$al`YT^=aukQ#sS=CVj;07p9yM>_1>w@}JZBy5m*$Ojl>~m2E-!*S!-my~BT(h)us65p~}1 z@tV-;-!lUmq%JZyzM3Q3bEa_KRLg_2qN4P@Q;iaYU93TA0h}HTUpq3-*r0f38jEW7 zE0u34oZQYzojyu=o_QxvnobZl1gcA65D+`fz@QNVath2LAUbK$$)iv)&Vu3sD`R65 z3sZ;!m^xy^k0~HDD8R=Rq8UVz8ZJ<`I@~G1{{ zy9t&SLE$yn+(t=wf$|GA;t(7rNNo{t^er{EQ+pkJ$4sDUrlzY`Z0BLK_q?n4z1L=| zM=y13JzD`*1kAfw(&#U!94w83%6c#$qMQ@cSmclgOoE`Y)dHxA7pfIZAtg*CCR_!s zGzzvK=)ajzl_+6Oq?>^GmZomPnnnkS+b9VyQ29iSICKET9eNsFWA$|Y$w;Rz?Y&Qr zy~>R{<#$m#{9MG{Kh_&qEN8`ag4)X<0B)%QXcP1lxZ=HCAuTW^ddjgs&Jwc}9$Qp6!K z;Yx6oGY%jXNNE(L7gSGz>K%9)NU%S!?mVEJX4i}uRIb-U7pllJX`oo%f`5(^i4w1Q*5)B z{+SKcPeeXOgfqc@2&_Ma6gLFii4xAlng4taa@-RzA8Y=ifjyM?10(v71dy0;pOTYT z9ALUh^gD+ANCHSqnCZC6Vn|v?$;U`(nHV=|t!#qDJBGbT0!U1f-Gq`SiS92W`2%hv Kkb%u1FnIvu(vTzo literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410113 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410113 new file mode 100644 index 0000000000000000000000000000000000000000..13421a93a1f7021c1c097141559b6bdb12200ad1 GIT binary patch literal 4708 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 DLZM~f literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410116 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410116 new file mode 100644 index 0000000000000000000000000000000000000000..36387fe2ee8e536942fb848128c15a6ece89731f GIT binary patch literal 4696 zcmZQzfPlH|m)vAqFTXi1_W7`!=E2@yHj+Naryky~$?|Jsr)b$jpeo@nY);cmSFp=g zY?s;@K2dJQ4B1(2Cyat}9VV|WJ#Zj+^<1uG?{ph+SFWjdj7wq~8aHt72yS|E(M9l> zef^n@Z89L6k`|rV0S5kzl|71*@9R>SY=0W0^V54TP@$sgVv>MnC6pn6{u%O#s` zpc02QCu_qm@7v+t>V42IS8Yv}yvw$UhfF5O2Y>rLZThY~p4VlqDpJJSEVg+~EIRf1 z&f6CX^DMQm%AcLZGs$tUpfYoac|l)oRmh$Dam$UR=N4$UJmkpkKexqrOO}J%k~>bT z4m-vkT_&2o$l+m0`10$IBw3ucEK=BD!OX)q_x~v~MIHvx78l+J+cqyx0lAp@_}N`4 z4ImZ-oC1laF!*>ofaqhw{{^UOPW(sY8bAy7#QgMip+1_q4? zAR8QSAblW^wCLm=AO#ZREGRCpGB!3b2WbETm^v_>;ve{wsXUJFShc6pj(b-wNJtX0{ zo%Pw$g_~r;E-)KCGtk!A|J>`{)ZdGK%AM+Snbh86A`aXW97M|Y0p}9U`l~IxI zp^o7Oj<&WyIZ&8`!-JsN3=AN5Fv!mF(FkaGb+_@>L92;dehB5M-?et}ogmy-toQE! z+WvbW^MYNSK}x~q!e|oo5%wq0Q0rNz;C9f!?cneRxg8v?z&yXUb-rUYMBMYv~C(_eSZ$CDvdoA0> z%?CMvW`WK0)qas;D!+lncz)B@Ut3?OC8W*JoVnrU*#|=NS!b~AU&p}j zlc0VE1~!npp>e=4!OSAcP2c1(Yi7+mbF0g9Hu3IqIp&_V%u%+oQ{wCs7KV+B_UIi2 zSpf!&U^f8$>tR>@c3!#c_S-dc{5P(d*yzd{;pm=Hmz84UUwrQRe)q$?2gPDr`yaRD zyESvQ6|GPDEYcKWy02G0UMcuhdQoR5`+ap*3-3q8 z#>d)pw8{=Zt<-_g;Is&0g3}pP43>t_#X(`o3=AhHs5+w34AZX z;4ndI2f*qfQ2GKDWZ?8b?eYuka75l?fq4XMA0!RY!(LV-dy&%!z3qmj1De zO;=`^)oIcs#fNzGGolqwB7K{KAhDJ}=L7aecQ~)r|+LFa=73 z;{?b?HUpMcoM46$Q7oR}#K6Uy9#VmA^8i3tl7oOumBo)|cdkb4l&Xv IG%mpu0B?z+5dZ)H literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410117 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410117 new file mode 100644 index 0000000000000000000000000000000000000000..db5ec06fd7eaf71ea28faafc725a753250b323bc GIT binary patch literal 5836 zcmZQzfPmGLzfC^pb3|hHu5VkkJ(k_w#SwC;Lb1Xn=$TS0!}Z2RKvlwX*)O@twqAa7 zTMv!bKdEU`YkO5Z5cLe#cLfA-eXxgU&y zY8?!}oQrw-exchowI}g)vzujvTdReSvFd-n{wtv6eq$`l-6>)%pH@GN=aV|8@G?mL z+g+>KJj|Wio8juc-({Jq&gFm3t2oh=Oq|LM$BV?Fu#%jKw6#;8lvd1muJ z+}bU#zR=|7I{i}#&$g7cul%#6{Qo%%-T4JKlgz_=G)#1Qe|};}`S&+9D5il+_Vk>@ z7G;Oo*NSgu@Rz$hyq@}Ol~UKjsc)J}j6?!YPF6i-d{ryFOUdIgd-UUN5H~R}5Q;0H zIu;;i0;y*Up5FPZ_ULz$kl3otInUpD?qkvqYsh;vH>fPc@Y=K&>I+wJl|Rz8>u*+41gVofn+j9T2sRfO zw-yC2W$PwhvhOxCS$I;yI44m&ecr;;*KMMTI`2d?%=_TQmhz^v;p5I_&p#T@OV7N# zghx`-QhjyHiTwhve#Nfj2O0?WgW+pO<{29l&rD-c&3>iwErpZYS*g=UDbF+SW(gF4ioxj=|G=kA<#Bw+sy&r< z+`H(>k}GEMmG*A^m9IBBio@+x ziN)*k3;_${by_nQU;7MF#k6jz14K0gBh;-9d}?Q2=kEBL@U9{L^X9DP&ym^RBg`fS z7|+p^sm?9r&_CPd$kB9mji%q$Kw;HrEeCBzx>bW?R6(*x7)G{_YP_J?`id#%D}+AgMnGAje)r|3aA_vHlkDb4o&L6<=IZgiw>ZT2 z&$Ciq(OQ1r$jcq11_-EPr)!7@*f6y}RsYJ=OU<-a|Jh_TrK!s*di~Gqrx#~!aqWAh zuub?9SP_-%jC6_s8#gh{;r*0jC9*7P6ccxA0xJ3Phb1Vuv&C|jPZ#W-^+Q|rs z4+e)=MMkDWF2~oo_U(1I*6e!N)h#S$IH4o0(a+|CyNhv%&BhB=>l9xfzQQ@%QZ&y# znY;V6Q|Q7s$x^8jON>}-LE#0C`w3J~+`ex;}wm>;hIsm5;f@U)? zumjaH_-emMF_quIVm!a;>#waZ)DqHWXwKa5^6Uel`K&Y8_OAn(7wig74zRQbrb*Dp zK!jTutY@79Ifqtm2ZuMvd~mn|%e9zH*SqdBR5sMi@akIW(EaYW_3D7KrA3u9ME5xg zhPyA2Si1D7|Kr%Cc0K0m#H0&hEs}?uD??mE{#Bj2y7AjnP}s37TO05q#>GB!(|(qQ z|I1QEc@2~f>qYGsG&}V4RJ>%RCdflTD|H|=IBq~paJWOoU}1_b4stIuFz&$t50WG% z44Hm?kb$NbWOKPe`jG^Xm>?N2fQ1i?4@yT2F#CaRg7r`dMxYqH^d!zr?>^AjO(3_! z!fUX(jgs&}juT4a5F92*?K)80!otf1$OfkeYUgKgdfB*WkKR!*3)Gf@0a%#O!(LXH zr9c|yXHa<1+isfr8C-TE!kVsr#$z`<{mh0G&kS6mdvQxDs^rus8&j zH|YIpkQ^v|f#pE*B*rDWUC0TJGPjZFCZ0R8u=GVMH-XwA@Ngo!AB&_16uvNkEpQ_F zv!+_Uf2^=*K``5|!p~b)y{vk&W#z2C33Gg9g(D7ag6d8A4+KCq%m_vx_a9UaWCnh+i**O;xPLl217{nu!f~Iu>HXDVhdC;O8O_xO%M0b z*i9g}!@>(5uY<&Gl!O;(+=3c$2o4jZ_8K@UCx&qCn!P4g+Evc-fpx8_so^c*kj%R; zF4&%Aeb~~oU^XPa8Fip(6p|i*{bZyxii-`43sAiU2E>dREKY0x2pyLI`I{4}6-*%| xSR^J~1+KIUwjb#4nNXD|aY3A$+K$rLO<2?JAaNTd;RPC7qDCBIO}ns|2LPbaZOQ-u literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410118 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410118 new file mode 100644 index 0000000000000000000000000000000000000000..5d5234d0a0fc97954f0a4669138c027d73fd3803 GIT binary patch literal 4876 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410120 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410120 new file mode 100644 index 0000000000000000000000000000000000000000..892ef45d70c653a22339ebbfd1a5b34e3c6003ea GIT binary patch literal 6060 zcmd5=3pAA57yrhayd^4K%XC#LR6`i1Chx&`MGr}2JiA0i4=KHbFd~miDbGt86r~51 zi6U-!{!58UqT*KcuQYVe`R1Eho_4+OQ^&bWC!XQr07`*AMe?I zy8=v31{KI%G0yo-J*>uMnOM~R&0BP4lrsZGB8%?IJk)ul(IMfJvCG&r0nW5O^L8D3 zMg#}j)=hp7*4uxe1c)Ag+N&$S_|GrB&FNYq*Upp}t17X4Q~-p$c8i=Tm43eTk>j!Y zWr1hb;u&w$%$e8c2KRUlup*wQObQ6vJ?(%nOR9EtLfpFZw>yv%0|csouE@>AQ!-bk zi*(Zbl**3PtYFogtUJ(A7~2*{+-J5?OvY9xb6=K$gZIq-;?f0#h`PrsqXw9&7nADU z&iC!3RJG*~H-x>pmm7vC?zfccx^O;^1{n+9YPh_y1!`=mgS|Aw#+nS<3ly`E)c&DU ziC4=W=`5LF8gXNE{?S>&wSz;=E(dTfO{tQ{WuBBBQk{KT=MzJ_&b_BJ^k6=#bnBNt z^ffbr9<0|}89PU3e%z;b2yJx4%h5GLV#b|xkNq>r$J;hP@RiwW`s6=eJq7rf)5)** zZdo+hp*Bgqj?g-HWenc(ndq8l7ucK-{w4R+R8bR=p%8V$ zdupyR`?}=UPEV`vx2+BZ5b%vFY7s;&3Fv6tpdAzt&mOkM?Uw@2IA?2iub6sodBoHi z?qv!pvU{XMZOHOV^kLO*JbN$*aE8>t36z@AGH*+cx%R=}mb&??dSG4PKv2OKsY{till)V=^*HZ8yOGPdq>$M_`=L zSjmPrZkVr6u-vP)=a&x!cPBTd#cV{JHaRsf`6VE7MOQkV?>&N zjI&4yV0|{SRjRkM^gDWXX}|s^btg~5rtKD1A9H$=6VyY=$-P$?XWvS?4>4on6X?|T z1zJU$TA=Sx|1EsAOfaojtv*FaeZ$LN|4S2}WP*>j!3RB2|6A#)PL?jS&7U+TwtdHB$>>Qp8Y;}{a38Uc4AGcCy8TQOhTYV>DrSg%% z%6(J-6tH)8w6`&XCT#JkYxCT8Ogbd4YZ%v)TqBO8ET~ z42|G0uH;2q76q#fZ8M!l_kK5eKJ{SuRfABK>S@+?YbnP_Ph|;R9)qHDB7Rr!z7Wu;QP`@NB)^{=2)_ErEw{t*`*qnY&>auB#g@5j{8QY_k z8@h!Mk~jkPI%9wr!sz(E9 zou+ZLxUZp0K0`Q7_ti#^mtA# z+PmWpZlvpqA@<$g16?aGd{sYa05vWSAMFM?YE&j*eVp*=hky0Jbn zxP}(8mk7n0-|fuxy$|dKmN`*ZJHi1m z;b;9Q2ThB>;5ar)T-&pPCmYk;Q3jHBmC0df|TX@pdpxUCuCD3qOUOy@{wj#~8GnNye&N z04Fy%9qcuY%@1Nk$6nxIeZv9qaRx(9Mia~la_9K#XXExG{(|Q<@80@PV~F~Mrb^%M-_X<~T+Ax!~A7U{EwHzuu>4WufeI9U=)AP+* z1OSOmGH0=R1m@@%;hBU-vxK1G1}h)({*C$3<$>W=ES%-TvA8@FlVKiXk5v z+-1!E?`I8eoU?}OXFh6TM6{@D2&;?NfH~m(kSjbqc~tJ}ujK$bKga>jeljLDd~!;d zU{2$%RV7!pc!WSOKPy7AF^7uI~G+HaGEP{veO|KRx zu`>ggfYn1nSf(g|XBMWTbvmZ=<`i!~j1Al`xVI>NYHoqyfj7X$7})aW*4&qO>b ze1i%v9(HhQCUky~$DFk{CNBJO>UW=DPLMnNL4R@gr=Nt|kNAtXUiy(3j+@8$4G$HW zc4o~2BQM^abs#ac+D7QPf5u;EOEl@3l=uby=K*EBy>^)WitBUkUii!;_diDi`s!9R0t0Eq|Qk?oTkMadY%1;Wj2mzmpTZ1OFQ_95+Wfk^c*p C`l8PO literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410121 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410121 new file mode 100644 index 0000000000000000000000000000000000000000..48e472d68540518ec8ba5aad06587c753c1c0fae GIT binary patch literal 5948 zcmZQzfPj;~AF-J?E=_OR6rZQG`~Izb7q4TNIrkfcYJJn%%`n3Qs7lyf&AZ!EqWi4q zVwU7h7tR*@G?sq6HRHg`pLaLyocoxuS9{%~+I1f<9Gw6Dkh+6~oGMqrx)<*&oeszB zDE4a2Iy@U>Q_`Xn=OH#SFoNi-MVU8~0=|3Zvd@0>!c@rY{r?x2=jgHRU%k;uUi?qG z3{Z&!%SMYu2}^eRADC~ji*=hBtLm+Ta|GFbFKQChE&O-J|MxdJzr5XZI2~B79h!0d zeg8h0-@0?{GnPIsyI{TMSlBC`8!zs#s2#eaD__}hZZANYDqF2Y;( zUgN&(wAa3`)cjCU!bo8V>%`nW@Hl^7EI=QLT(om!|W~ z=6|@gTV8#k$l#E|muZ)#9X1DEXS zIf*UG4zsTn-^}1IcX@a{_1P+=u7y+IG?f^M1fHC%ddm2!R(O|^$7A;B$J-!oVqhQ? zS3q^FK+FVEZ}{4gdBz6CGt*d9vtOxvOX1{pR_gRo%Ja-SdD3)(upv-M3WI>yX$A(3 zSRfl5ZygH9&PRLfz`{r~KOaY0r)M&V+}WcgKb8J08C7YS5JHlGmqjY)oxG zb8tcFtH(?p`4)T&!)NeRU+!I~HQ|IONBQSMu{Wt^>#~`F=7GcIkL9zYL4kcsB9~kI z=)AjRDm$N@wuaww-!Ew{@?Py14KQAMA&& zX|F@fCr-ZWyh1o3WWsGvVJ@X5$&2O)<~Ut_u%ylBC3zATbV5+!~q~m_qp=C1ABLZ7-L<39!`+=d4R+++nY2du;MEtEFp>&k%TW{{FHI zkpGw*LwqBHKsum+n0S>akZOP`XN0(u!69hV0`}$KPk)d+x*$d~d`fmwq^`@QmENL3 z??S8MW<~wXbg-I}sdqp5^pbWlMoCd+ozt%!=i2hlExHjfZS!P(PM~?W4 zmYuQ9R?Wwoo@zbt-!360_`38_@X6z||F17&0HrC>7I&a}kRQ>*IjXLh{(bmSFN zp{sw6T%E_a&BvcE@Sj|Dbn(2;1wOkYtHe@$#Uv7vMLj&GeeStgdoQ76%ORmH|3x3vraq37jP}dAQ&l0ZG$a*EZ=Hd}awTIv|%B{ExsD3gKqxc2L zhXJ@h1!^y5>sT0jH0d?UeP4B6C3e1?#skjr**xou$&=D;*4b8b#rs=K4~RB!k` zt2+&Kk@*7sZ?4VVyzb5K3I2_;OMvRcJ<49dlrw?d0`#}Zb>`@Wk8eg~1<9swckItK zaD6YmPPS`55foK0Q(<+2DWk<9DJgd@I@y}FokbhaLSXCNk7BB>`M+s@=&u^V_RcI;qN@G3t#s_8B=kz!{ zfjU0rKM(-fu&`qUa{oc)KyknfEd#(pM3kir?CT#)fR?8*Kuw@>7!F{T00oekaA|Oy z!+9XPK>$()f$G*%P&r1Rm?xA?O!*FSlMapD1adnpyat=wCnnPjA4DH*-co%r_3NH%q06Ch3~Gab0IoEHl12&EYp}cv zO;-@35hNB%P{NNm^PkUwrCWr_5Ed5mv8Ejw*h7gwFr1AffW(CR7FS+IPX{2qpnMLB zLwLR+(eD`cBMBfe$#5HqZqi!W1dDeJdyxc?m?XOiR35^^iRkthk{-B?Kn6C4_?xsT z3GBEhT9IolbD#BQe}>_#fJaI%C;jJGYV0rJ3~S7x=Sgro0ca$!UIeCn2!NG`ptc4W z5Nw+u@+MplOfQJW;$D#8m^;ZvbKpH)05Y)F9O}2UWbFDIL+%|-Fh~G&dIT>#f(_C21P|NPiFsm^U|TS`}Q0wTfYaI4$$ij zs4$pc_WfD~~^Ot=zII3NR9IfI@?L3&~BOn4bca6Dk$1Aez( wt3-i%fpr1n1Z0aqY%C5!@jGGj?|=W-e25hDQQCh*k5eGkPso12;(oX^07wCJ%K!iX literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410122 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410122 new file mode 100644 index 0000000000000000000000000000000000000000..17c1509e02cc6da1833fb82660549b78794a1b7a GIT binary patch literal 4908 zcmZQzfPk0+*^gDSEAxs?%xeNBaLDX<7_7j}m1NAyYWM!*3f~%_D&dpAAF-J?E=_OR z6rZQG`~Izb7q4TNIrkfcYJJn%%`n5m>&F4kG^S&%%Ibfbj~1>@;z;3a7rC|Q`uDDb zKeJzaUjwo!Y0-%*5E~g7LG;#Gfla$>HTD5v0= zIa6=H=gpJdC!f3LHi!DuGEQH2lgBGJExPf+#(mGCoxiMnFSzdO8J<+d8<7m6Jz;NU zzwKZUxv26!V(ygu^DB4CZr&uNv4+8>vRl5H@y2|vTn5n=FWv{+HZM;BxtRI**j=xv^^rPJSjSuknO);lJ4I$IhJ{?nPM#(MJem&;MDj8T`S^UUUd zxV2keeWA(Eb^50go^2^>U-@TC`Tuhky7LQeCYgu#Xqf2q{`|y{^6zhIP)q}t?CCj) zEy@nFuNB|S;4gQ1cs=#mDy6Q4Q{Oa|7>NX)oUD4v_^MWTmy*Y0_UOmkAZ}t{AQV?X zbu2*41X8cexjAX6?vBz^z2W<;?ljaz<_q+{xi)w6x;MWk_&3TfVc>Ui0EU4lP$@Xh zKze`xna!{|_20fV;o&E;Wbemi_deUetg}#ik?^f6^QB`iFlswTgEYvVO$AXPzz8-M z7`Jy4N;MOz@6I>-8?)@+r^R<)XC1Zv5ZP_r(sL(SvaWhZba|wcxP1TjjR#M*wD|wA zZJOY5%;me?w}-nw&P{8u1i2sVhYefr-!YMzZ#n5>Y?6~S#yhC`94^>l3ny3$l*-CJ~V)6AYcTW z3-rU(Sf7iPCQGB6l!N~jJ~++KAhs>as%p;G!w;h-PgM42bKU)5aevu|Z1?_;ZgRTZ z25++bL>Z3W`SCevSN5(;$9RBdf&F0k+L3w22E{YeSX8rLseDV}sE0CMFPBm^v_>;ve{wsXUJF zShc6pj(b-l9-$6CTn$7&Aj%_!jOBc7Bb90v%ukU z>KkiVw`iAQvGmPC@t6M@y}j~>pr;ocz)|2?fE%*&IU%QBx@U6b+5j5=cAHbtN(qqtMmzov+7Zs z^EuJs{$qX4Pkt%Pf?P@Se>89U0E#P?%ZqaKgx|MeV_;wZKnI%7Vu6}ip=N<8m?c0C z5)-ZhoL=EPkli2vDLX-R-+8DUBdAV+vKfeT)BO`Pb`!|$u<#meZlff;KxHL0;t(7r zNbMJJ^!4(~Rt1O^wf`)S)nwt7IkLd|wLwo8Q~ru?9FNv5a^DY)V{pp>$i|gsP|_&D zx(sL#xGsgJE2tfC3X3Hu;YXbLDRW@y7H%+^(P~r~^XCnz9G2y<&m6y@e z0Z1<>pM&BMo^MF>JBIy80!U0U+(x3Cq%M`nK;s?5UL*k|CdqCBm51M2G5iTQnTC4sd%3m>qOk1+5zxb)fYIq>KjEmq_&n zE;g)ugtv}}X*(=VYdZ>UpF{zj%!$MO*t~)(4T9|lmPNauW})N@;@s3alg4hsS|$t< zw^0&apf)Bo;t;(Y0Y|0Ow=2qDPb^q2Ozx| z^$Cf7$FLts0EtP4+mOm(V%(&)vI!RN81^CwATddH6H0wT^f&~PKj1b38Q2^GlLr7R CB%zl8 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410123 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410123 new file mode 100644 index 0000000000000000000000000000000000000000..ccf93278b9e89a08a1ba72e5a45fedb23dedcf2e GIT binary patch literal 3924 zcmZQzfPf{keII?)oqqBNp3AFAdmh24*JT~lB%pCS)xiIJC&OBxD&d#{*^gDSEAxs? z%xeNBaLDX<7_7j}m1NAyYWM!*3g4Q#hh{Ujy}f;Lb5yM3)j6u_n%)e74nFFfT3Q}) z_qgpdKsF^UI&l+XBLgFd-Wn^gX?LxL-_-+F?n@tTop6#rygAfe=14&Gz9yDSHr+ra z4$k&v-$XwAP2VP;uxL_V(846&i4SK)yq#&W&$v>aX=BVU+Yg4_C#Hq+nXsw(rQKDy zaq{6?gEM`%K4s@`4>=jcacA$biQLo6Cu+L7n#B5UbBdHRVe49EX6HV8eefld`*NBh zB2u@7mxqQQ>$z)jDF4fo(>jG55nrzyJ+^#`Qk?2_2GJI8-Ur(@FHZrvnECkGU8O!C z76hCEiKa04csqdTZJw^B)8BqsFlo=$J0^BITN)1j)0wHpdh+v^%TcY2QJ1Fk%;tZ% zwOd|&p~=s6`lk|}Z7FMC`DaV{|8o|)^9yb!nTPjinCSHW{KSy*?{8{QOaqtf={bol z$_}%y72nL@FL!x(J@wftrLKij-!zpNi3FaUta{4$s#bWHlE-8A=*Qb2Zem~{6jwlX z%s|WpQolO&-@Y~B;U}_W@5g2LKHI>ovrv1H@U1KJrDHEJYCA_W@H;sG!@vuu6dY$D zJwSlWX6O@oy5?j33JLG2Mthe`&8jxrac!Z(&fH6D*ea^pXJ^a?X<+*Gp#ek#0VCL4 zVBGGvnX3Ks|NUH{h3V|4O<1g=c1=iieEwyBy1SN~$E&jkb{|~A>@hWcGxw)UXXW~4 z#&>3aH@T{F@4SKSmVJByQ#gQTf&H*y>-{?>Qu8h6zu8g6cg5z|>a(Xl`(2*izbw(# z=6k{I8&E%Z!qkHt0R$jFfc-&GKLZ0BNNupIGe{qU;R&DB_st^Yy*xQL2i8VUcAt8v z_hi=GqF25Tmab$My~nU|(H^~{V5J~}5$pz_f9=Co)J*MSevnkJ>}znFJ@4`38G`=u zAx@Ld7fs0R{CG(3X{0|7-)yc){{jyEOw7Nazr^82l5ND^VmE8My$w5AfM$XHYxvrc zdBz6CGt*d9vtOxvOX1{pR_gRo%Ja-SdD3)(upv-g3WI>yX$A(3c#!=-3NV;|e#LA^p^~L%_m#oz~37*FJ+(GKujWfa+j` zy47J#f4r=9r1__BC+@y;y;&UmIFx+; zzE?b{5rLhb8FeQFoV>BAbWeo)52*f>|3H9b1S6394=M*s%b@xP42UTE7}(d}KLIWO z;((e!M&}= zA$oZMj>=9G)tZ}zPOV28{A~W!?d>o$eikCAURd#RUEVDIXr80c_(rccKED!OfQR3WGyaQO~(9%s<)94^^8%i1_(M=$Kp#Y?a zLt?^}psNSfTk!H2OPvOj1Di#J+lX(g!kmO?vm(0}r7k7HUSPV0ry+DdA}4fYJ76wA eSC2bPkm?{Z+=OB`5(kM1a}v(HhP5mK=>h=DAf7=0 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410124 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410124 new file mode 100644 index 0000000000000000000000000000000000000000..0e792ff9b3253de4b667c3fc0a21d4cc12b8a136 GIT binary patch literal 3844 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 UWcR|tgeLZa>pAT9f*k@P0OOy)tpET3 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410126 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410126 new file mode 100644 index 0000000000000000000000000000000000000000..67f1d1a7a52e73eb1d403bdbde0724bd3873e67d GIT binary patch literal 2752 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 A=Kufz literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410128 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410128 new file mode 100644 index 0000000000000000000000000000000000000000..17a0790a93f7fd43198ebf0ec08f392a608d7178 GIT binary patch literal 4960 zcmZQzfB=5;+3G3mZ{ND?jACmEyb`VG$oIyr0b6Jn78$$6K!7eQv_uDzj+9qOYd|rng@2QE`38 zviezv7qgPBS^>9BFjGV0frxv_bN`ZcY)ja1e0jEHsDGWZ|4j_7)r)%l-w_g@a+OzeJiJi`thJ*ifW~#BC{QTu|R4ZfDrRhAg z`5$iWmRDbB@^hX3sf1@+%Gy`{*;4-hoQ3ZEf}2U^;XN8AI=w$XF{J$an;I0;z$JTn zPGXC)!|ZFtH#7LlT^?RfeYQ%eYvI&4O(jMmfhQ-co-)3w72c)f@t8gO@ivH?7#Ik} z6;K@u5Ho?)I~f}hU`oXs+y@KQ}=Swp1J2?Quz#pg- z9A_XsK!D6igmJxnDJK4*Wgv2loKQu(z#;ohgi8&88Y$ev9FQ6Rtw zHWwJT{?3^ON4Vc>FORfNy0EWN;6+Jr>znf5JK4q8uC!NKRA*{*|FQW??HTg2hkQ(L z7q!mU>TnfxX??l#=LG4Oy)Gd4gZ+@=ANZ81JdW>JwWrdKdsjVE<_8?U?`IKWwNGNo z=Y7q-42*3HfYxqhVES$cvLA@yAZgJ_ULc2M@0_=CoA!Gxc{q<}roKta7uO?fU+$Yu zf3!&_r)2v?6OiLU`oMY#+6Ptl()M!sn*dwQaL&49#vS&Ww#Ozvvs$|5_zZz3=kG7e zxB)be$uY#!F9^thf%H??k}GEMmG*A^m9IBBio@+xiN)*k3;_${by_nQUjyeKMnheg zaz?QKfMI#$g~G=lDh#~B63h0Te`vKUWpA17sltfE4<0|e|GRhkffOdgA2#++&SajR zaO&=s_^AhGtuwpf`~S!7Yu59POMmeL4Frefsc)=d-J)HJ#nLwm#b5qs^!B=wvfFLh zg?ooI{P(naO=V!<-oe1E)yBYF8UxgW5|%JW0cmC^8x*!6z!bv$1k508K198lT|I~a z4)f4p7iSQMLw26SmaNuEcJrf7e_K#<_4wXf9OC=uSt+k*Ex&K%qW;1;76qN=X<>yooR358WQ4>Ag9De!!F5*eYFQSBCx1F712u|ocd{kf?D2%~z zf5_+6#n&$mp3~iW!BTd|jn4%FYD>O6eQ+C6iGUi+)rYuUfsQ)bkag51vZ>q7&G1_DN~xuEo`Gj-DLNdY-47B5MAe{E&O zq9*3MHs_w`xlT6WO5gg=p||LP&9gY|;##p=`Rk78-#nl3cKyxzYXFv!edp*=l`D=gEU@WMF;)p|Pf2P&hC{%R8_# zf?)zw1uoklWhB&S1cfDoFoNn7mJBIy80!U1_YlwFniEfg*R4@Y??-=$X2_P{^b`t|~ zI1!z9k^BL-5y-&ikk#*w_J0Xu*kbuvNIJEJY3Fj*olM)C*TMj!*5L(Fz|oafeJnmzS^tiYweS{GMzN!HEv zv(9jicl5qA&F3{lKlhIRKmcUJ+j2neKd9NfIM}dy#|x^CftdE*;P#-5u zFNj8RKN1tJ0#_OY+YhW4u0U0yC;)y@RSm33KAy ev?YwjZo-;I2Z`G#2`|t%05#$eYZ`^dCj$T=AnmIF literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410129 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410129 new file mode 100644 index 0000000000000000000000000000000000000000..c942106cf9d08958a9610d20ffbeead5ebd91479 GIT binary patch literal 3976 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410131 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410131 new file mode 100644 index 0000000000000000000000000000000000000000..2bd09182e856e66328e3217653d245212092863c GIT binary patch literal 5880 zcmd5=3pkY78~!DQLRW(A{8aHR{O8aQhUxf-#0s5_Gx52kN0`LIk)$`=XZYZd*1oZhaf`P zvfj3JQJk_%t+VIoZ?fT8L?xd5U~0oQ_qHPp{@(2sKugvB9FP7mCzCfa(nverU$w)K z%60$Ar}6>EZehQr^Q&9JM@c}1HlH*4k)2-VoCC&Q z!6g~vn`I1mDPw`E0`9G?@tZ!se0zWX(}Tz8R^9DKY#WP1zHFk!5z2ZtPXA#}a z86j+Vu4O%$GXLCPA-C(Zp1JD(KX$WPJA2SY(?nx&@ zmS?H7d!GMRK@ee>^K#+NMnpZqP4P|*rz8*}mTJ5rduuVQ355@P0}-dsX|SxJ+ec@Q z{CW5Qx4N7BnaQbLbuUbE4P=_1{Mqi6NbqXQR>)WGt4{sMu#h}{!j$hja4IJ0$B9$1 zFRxIR6-0LVTX^pMm~66dd<+SEF&X6g)vmc(ErD;z^GO{>o+&zx_vF0q)d(>m$h&+6^rXQuw_8te+P7!yLRAaYceA$xg#=iD4mFRD>Vh8v ziVwudR!vjmJIkWfTSByq?*}C9m0e#{b)tg1kTAv`c4s!}sakHUYaLmf_`C1ov#Bm= zk7|eYt+$!1rlhadZ_^x{Q~;ZLNaa*&U&@l!-}FfDY2ciBb~<~UbRy^x&CQ?g zS6u=00uhx30Q1LwaDDKI#J`Ad3>vU+P9!VRl0+g?J{6AQdT1H=1*4xB>!WmWD_!U3 zo(_6ofy;&7wG?;DT8+#nweiki3en|DR~ILH;biz5%)cJ6B~K}OU$x+y=Fw_sI1pP=~N$87!a>Fsw8wkGrbBc$Y7*>_5lc%6+zb`<^@118&8k7qgug z-8>_=w5hhEESIrC14BXW#Zs?>B+47UFx0G zgZlYTBY&kHaE&oJ_S>PPN=`yY?kX%s*5djQ1Q2}RXd{?u(mHk|KJ;#%*@WtGvMw^T z$idB#nw4_**k=`gM54M{mxEAUI2W}Ge0$!QmZ#t7Xq`D3;K2MYq|HD%?QXAAj!tHn zyLVbcUAKGI?JClkhQ6}0!Ml*<5H=pKUF0-WMAAD%uy zRkL*HP0?R-F(bi}O;o61{r({FXlEHBrRa{R?t^hI2iFIWNI>4AU=7q3sLO(fU}L}5 z4fY#^dS*PbisGJevuyR{FSr)|J+Hg?7$=!|!($-1pZ#LzOQAqrAjbt*nYIeG1!0Qwik*ZcA{bJ(VcN#SM|9$zzs37!KHPZH->u{k0Kg38+* z_W?=kYG`7qtS!%y<(b*IvAn^dX~`8{-=H)?VaW}JV)L7@65l7xjkut>4r{=1DMank z8hr4bIT_>*haPP~c&%6kHw{Z%7cYgm16$v}fRK$Tzj78N!?dJ) zhA~0R@OkAd%o1$V;S2t^;{HJCe=i?mF)Wrl{^3jLU(~Dwd`bKt4o=8L8KrVW?7tkY zAqM}A_``Xc4OrmWnNBRtl;3cU&{&YbUmzcd&ffcCLf1n1fVDSc3Fm1x_zmZ0Ca_%n zam|vc#52gW`jGu`LacUi0#Fj{ZR8|)V_=`4HA%52xU;f&afpG=so z&>k)l!~j&C!nsA}9i_5y^N>eLxrB4z9wgl_;;}0r)@X;1nt8FY&*3+jbn9^sHE=fislD(}3X11+LpbQg$Y(Hv|lT8JCR`tg`Tn-bbz zJO%sz+fguuvD~5LZy5w*KpHASi~aBJT2E z(V943oRb+@7yb6~_H~iN#JId`gNi3!?${k$bBRcvA5-u4T<=r?aXjW1!-jrK6e);( zcoW8XfhB0Zz{YUioUk?`!S@(M?qn~5eUtG8^Wc47Bx1yfRYk-9=2LWiaJTHiG;vPR lrev6U4$Uwoh#lukbO)Fv*rvmmAK2G_;ltO@CFV+D@i!GJ<0b$A literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410132 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410132 new file mode 100644 index 0000000000000000000000000000000000000000..09032447acf7064e3eaf2acc5bc230ae3413ddbd GIT binary patch literal 6120 zcmd5=3pkYN9{SAr7TuQT1PPMY^`+hUuoX^lPj^layKhHex|NGDX^7~)j_j_j$ghPkU zeq0mR{5m2*Y*Y1c@3UNnVRK4~!W`p_%J*M3e89Y%WbGg6zFb%K9i-D zYU^KXHh);W!OhVBQFlN+ebo2f)9~KFVMa9Nho3;j@t%~R9w5S^02a+PlbZ2?6=a9_7?KyB&{`uPaTpTCEsW_(Fa-#>z9h_ptb-w z2oxJs6{#o72bd|Q28~pBAVuMp%kHb5_uabh=8go{zIZds?>}&&qnKch)4?x~ZYra|kIz2v;SHC%Jinmn$yUWQMpdJtJ&3T=6dHvEg{N? zjHLzGa~m;zutFNw7p9`_E>fLt-loo0OLe=@tRK8Om+gDG;_guV5esjbOgPJkT7y4a zAQ4c%V6Ww7hU#@Q(%%Ne4;-H#6+XaDRjc$;_}AG=x6%ue2V8!&+V9EO8(|ZcN7;Yy zjIxNf+>gH&ZoN^b8z_FJ8vhGQ?IjgXQA&q>i`nlaHIRuf5!NYxJXW0q@O z-KFA$eDwZcH;9KdwY|B`4T`=YU~KL)#E%iInK0-ntOrU0;?rNKr-oC#&$Yltiva{=Lcz1Q=|uQZV6^=QTKcbk7}H0Okgs*%R>N z1UMG+{>gfy@%{4aIQIkS7guk=FvVP+WlWQv37-kJnbeDa-tbMGP7l#@a*}go+j*2Fb7Ez<;RBfLZ`A6JJdjC1M z9-8Ruqovnm_m=eOaP6sMfN(CPz~dXt}3V|w&aIj&d!86`|IqrCg4 z0Kb(Hv5QaW+~3Q-AO^e(^9VPXIF|SIr}9JNgYyup)wr4qh>0}Im?oXk&ji~{>g5Nz z^Z)4Kq%&GRR397ewIn1U|6!W>FY(6{3B#Ty$A9k<=S~c`^Y+rY zJgh%FPvL(~C{6hv753W?XM@?ob&>%cS4YA81AKz}pLB+1Ai!dx`-5*Av_HFHOTg1|9hut%!VD literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410133 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410133 new file mode 100644 index 0000000000000000000000000000000000000000..6b6bafbb87c82b8556271791b296f5d547e96c2c GIT binary patch literal 5292 zcmZQzfB*rVtTsuTppP9suO~H%h3;fIy?(K#)4E;ljO$J$e%^Njs7kng#c$h|OYcj& z*dq@9dj5FkKBG$$Ch%PA%iXe?_uw1uxYfmaT|xZQ&nfEf`0@ENYc0Qa&(j%o9xVOd zW7*kDI{?pNhcA#&U7ZfI?@zO`}c zKVJsdUhtA0U|M_#$8L!PpMJ;V!&-fZq8^)BvAleej`(WGVe6(c z+58W;cFU_TH2Jws|5U=WEoJR1|7Q9Pl%<`dM-~(#K%qB3!OKyCc4AGt+#JLKEi?&l6`R898PBJ-vM5 zR)$}9fJ($Y%3grg0trU28-VFx|3r?PbCy2wx-l_6q(R5Hx`eC|u z+aHmu2L*E{Z*BY&v9;cE^<)F#NoK!!CJIDFZadvK@kTH=&@8Zj5Ba>h`1-}cbGlnE zSjz6W@wq@iZONCX56-S=ojv7G#+-{#|M~;fGeaE#@~1>-NvNl1MZQ^Pwpp&Zi?(07 zOG-&(psg)X4i+w8Iz3J2!l%W}nU5YC{SjVmqRjAG%01Y_dRo_`Wp-;n7J$^i!(#1k z?YCDWikJkW-@e>1(aNo2|0Z2SD!Q`@Ub^X>ow%6^M8&;bhI=FnEq$T6^>v~SMSuVbEvbkJy0&+!iw|7q~ z32$Nu)2rSBiVtvD7`}F7p0Ppk%rq9&>{lw^QaHJtl{$Tt@;viSo-~~xYzS1B!XO}a znt?$h8Du{Y!$H!bQ>H);NQ|?fxWLNT*u=sJA`4Rorc?X_pE8xl@g1x7RN8Uxs%Ogl zfW!CwEJCdINlf{?uh|!%{31PDdZEQVQQE z6=Rv?9plFwym;+hc7d7=bDq_E|52Eiv9bBTzVG{4?XN!Z0nG!4%c*ayVcnu#ipA15 z3&mglXY}^Eld{`w*@b(DH2n9pdQD|u;NHQ&tkuTATpA111M@G`IdBS=eqd!DQwaAH zuq0vg!OED-?CL=bkokEe`Sh^Q@Ftw3gpD z@^T03WMH6*ovtAsVB^&ORQ)ScFE!Iz{b!TWl%_7L==DFZpI)4`#kKF1!ZzVc)U-3w zDFSTV#59NZQ~vF!+Y@;pHt^q>Hl^bd^%u^uDCjg#3oE?gd>m>gBP2c;92mZ@yV@Eh zVV|^90?lLj9;(P;;Pd(T>D31dKBi`*%=lHJ9rkI;;-uU==DtA--5_={>ih=+ zAR8XPK<+=Ndw6lMLFtMaSdIoj)iDs01{SAPK7^*T7@$5*m|hT#l<1I{a24Qm4Clez z3bj9J(FsT%0vQ9W-(c#9b5q4u8oLSPc35}~Hn&j{UZApq8gU2?6Qs5jIQl+Zs%p9t zlrXhtfz3guJMWhAUkOlkICjsfC)$ds`k??UeWpOuE-cM3BBfVYn1kwiFd(9SU|?T= z&jMOUBmp&n@(~=sECC82G2zl6`;Y-7KSA0Wptd3hR1zi3iF4E4D>QZ!$nCK3f~V0z z;xpA52Rfks2q70e(Q4YLHs2hu3vN1XX7b4V~BYucfKJ(TzZ zBUq3GkeKjL1NjLV!16MBIsoZKmIIqbgx}$9W+M6zFn7TE5y@N$HhzPI(PQw$6n7wyqI3U(5*dtqS$7NfSkrb}MpvKQoE e_*e|J#~PpkkFDH9@h`RQg^qDxvlr?l1_l7IuRIh0 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410134 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410134 new file mode 100644 index 0000000000000000000000000000000000000000..7e60093de2e0a15438629d7633887443ae3fa2e3 GIT binary patch literal 5124 zcmZQzfPfD&(g$4y?guV1n*8D0!_8VpTp!+j-OJzq%a*vx z!n>~aRCDvsKwQ%a2rV=BOz>||zPZ?j;3hz?#c+4LCcpJn`3=D+g z3aE|+h?zj@yQaMkF`qd3uJa1vgpdihJ%zcHmLxBlBbeiK^}&)hpQ8*6>H-YHei;mm zVHH5-;CKV+0|8)s>HsN_7)L>Ift8_&fhm*^q#$Zv+FmYy6JVL%_m# zoz~37*FJ-kNfbymK$SB>+{xf@o_|Z)qON@*8b^MfO?n`D!v4g1y z)^~MwOtMV3wFSz-!VOHH6xGVHIH5H=;^9>%d5?l=oiPtJD_`lI{?gv$5XV2&0Hi+H z)fsHGjf-%(^6ZZIvdv8MJqk^nKRi#InPlXY_4oAhiCYlKY3{zZYozQ&_Q+&CEjq@XgLW#{L#Z`j;& zb*2k6Ec}4#VPOGs3(UVDHiPEfy!q44crMO(u;A0%?&wyFPV+g7R$G~GfrDUbht=5$+UX6P<* zS!1Kpg^ch&K|%X#otFHIXg$7t<)!t-i}x5T;0Kxo_JiSTN9GwD6wgd!QO$m(@-2mv z+gYj8M=8%U@8n6-3Brazbtw!2Vy77xG(h)|^&mzQXpTv~U`q7%n zIV04q4%0F=JYc#b@38Y`bxr58Q_3|Q+w9BE$j|lY4|bKE+WMu}K9TWLPz2uG-B|@`5DDe=Mgmd6xQox!P|$Z_aZ61NxeRl64&uo~$)a6z0kQ zaPwgi)XtRuKmcUJ!h{jX{Rfo;g)K8MAA~~r3`B%81N-{BSD;~?4AjR8(+i?umY_1> zD!_3G=fTn&)P7)D1WL0Yzz8a{Axs8h-BdthH-X#^3$MZEHcG+^lwYV3hu| zm7B1Z7lXuYl!OaSYQI>o@SanT;VqhQA&yB8KFG_luo$xB@J!ul~_d#T+&2m2D(uJkB-0b=6x RFSYH3jtgM37wRMi1^{gD(y#yk literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410135 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410135 new file mode 100644 index 0000000000000000000000000000000000000000..3106790205f245d344c4e9f5de54ee5e74b70c53 GIT binary patch literal 4876 zcmd5<3sj6*9RFq_)iyO_Ob_oS(=>@TGij8V%5+38ZKPVX8DwoLdU$QyE)|89=xs%z zlF*ASiWC~;ks=9U+o;gPE7acm&G*guhW0d_bdGyY-T%Gc|M9#3*S&)vl0g@6-n+z( zb#P73wF-5j(t~tYT}$m=c$vT0KH=A6b1VQC{nhAE=Y`aV^A1jldG)3t*Pu-Jq;3nP zY;$>8(Mk*+P|==~=;4u-W_}^lXv#`H!(zKpQvMa6S6sj%V871Z(zs&5i?OLw8!n~4 zWLIRAoi@`?`diGc;}wduhZS`c>G;GbyB+gfX);pP%Bh*^aW$+?DU$!tcjegVZE#BwSGwBJ#zJh8wN9G&2(pGM%!fSMa>NkppJVyJt9DLpNUS> zij$;MrQ;sd265P2gt5hy@~9y9hzKH9xKye5cq^nNLkxFCh}~QPoC|D)fpM*!DZ9_) z-{;LR%nLo=ZJ3}pw4$T4$~}zaek)EjNxdOIY8?9j?|rtB*t0P&C?exyUT{w(f6{)x zS}#-A?fN{!9q-!_QTL~1&Oe5#jJcBh(-vJpQq9_0Z}s3=4R@9`W-=oW>}~lYV9JPj z6)`L^x7xrpk~#ayFpnn%5=;ovSF8Zv5b#Ha{G9sL`(v-#rg}e0X>X142;*-vO+I+k z%rsw#=9c?tYa60&B>+BH09etOK^ZV0jjsEYM-WBG`-9LL$|1)-Ol+-QxQ0ygXnl4f z+RSEMNzNo=-B{y0^hEzX@gaeLMRRsg6Uqzzkx&dE5BB+xdBQTAi|jS^L2v8*g?xQ= zZEH$-5GUbend93Cqa3n!P>h~#?S>vw@tt@|c4tIFjfVF-x0Tg5XQZnv8U}Qs=Z%bQ zb}@~Pskct2d%EnZv!$yr)0Q4gru|}7{U|Nmt{g$wRD|x}jgS_shxTzGa0dBnjO{7; zt@SfA=6!F1t>I^s_nAdiaEp@#$5lCGDe3e~j>W~8Mzcd%m*S~UuRlEOeHZYNEnV#0 zoS_6ZR)}ksu1i!4+>zhqJ2}Q($wnN&Zw~gR^10at38^(5kWI^PWGKc?lHf!HQDjEO z(97&2%Wr06trR#r=x8OH{F-9vV9ECH>R8L8(zE? z>@_kTy49$qQYbs9iCZ)P&KX~ zi2gD_f94bMOl(gX`h?5nnL+_9FgKAsRey0l$9r!M0#aeqh ziZ|N>MzYgt9-Sw+HeH?3OEQb%lN0Vps<<6@>UC2)MNA3DzKzk zBg{y#hkJ-~y+~)u)DfGZ+?Z?7t>_@3dO^N7@bsH@+K=C%ERGEczrD9@Dv4A@sWh;1 znKqR=QkPRAM&b1bS{|@cU##p zw4`n>!d;Ue&~TiW^(jv^x7o+MA%2I4Q+ z2fs!PQJ)ar0BHCOBYS9bcH7wAJp937S@g@Uo#AIQ&ADkRB^1U);|y)I_W?%q_60MZ zGw@Eq2kvutN1;QGJ4sS=SYF|B_>3FD*I<_MpM3g7$;7%L=#n0Su-9wu41%43oZ}0( z5H<*;^i=Rn#3AXCSltrx0Ll`-p}OQadw<>(I1c=6LBtFDo3vi#unTfW8e5_s)COUL zK;q^l_lk5Sdv4kH@xU<2*d{Cpq!fj`aYE{*Ql?`!7)8Fbal(8a!--dX@u| z#!Iz4Jii7D6GH5Ja=%jv=a5DS5WNL1arC`gNGYW8b~%2lAj6K=`(eLVU@sbq=|U$0P68?A_`JX1xrW;RzxSt{ fm{bQDQ?LEv8^I>iFZziI{y6#?G3@pIiAVlF?%q{} literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410136 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410136 new file mode 100644 index 0000000000000000000000000000000000000000..f54a0d0366ae01af53c9b5d3c70d9eaa96c0cee9 GIT binary patch literal 5036 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410138 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410138 new file mode 100644 index 0000000000000000000000000000000000000000..d49827bbcf3615c94f6014bf600e412972c7f0c1 GIT binary patch literal 3876 zcmZQzfPk+$6JJM``7Db(8(sZV@>=P+4iUCF^GpISFJACua_Eh-KvlvI!Cqz1%B21XFQLglQ)p1GxLzM8Y5qt`64KDClq|1X~yG1a*xE@7Q4?+wnUCbPv>{p8#) ze!{)Kt6z)#nrqOX{NO)HZ_YA4oF~_}+h5wX!$?OdduDvK>IJKbed~L;V(z&mv_6dZ z=>6c!ngWSWVXyvAoPB>;qKuD=Y4(bm5KV^p4SFYyI1Cs>TcUU$Y}>p%1>|Dp-Hm;1ozSg~7+$0Yq=}bS<6!_RE4vd$!&&vD4YoaPXhbOf}Y%pTAs=YGsVNG@WNQ z|HG}_^6Cpsey-C$mGEp!S^LUATgv~Rv(TMia5Kp~yhp=Cr}yV4hLnGQQ-fj}xMWYy zNo-Men0>AIW(I$`%fsub&sHgQEu8wMsl-Sm@Z@CGQ^r@d!n>3_99^#+7|;B8LVGbsKp+FZlLG^{Vm(ml zBp?RI8<0i^AaSsqK<&kB9SdWRCcQ?v@2k#>T%6IB&k#}+a@kHUw=L|!9GFHMSqIUl zjQmkbN%_U@D(X&>dlJ7U9KNwq?#RybRlgYYfa=6O%3i>fGlAU#ObPHh$XadJpPS)U8a~!#yyD)v2R;kl-J1gH|{Xx0-6U7 zm+gHkw9{2(C%hD&%;abHpZCjovjD#(KFd#KN*hkU8q5w(SE4PEK=sIBg3M+(>}7b} z-a_E^J?07jxN776&Q++5nEtA^c|E`2EbXfQQ$TK)J(~)mK!6c!E-;PQHgfVV++xu^ zWoOaHLvkE9&({_FJioDRX^x6g?e?Ixacf&oo3U9XGkd!%H+Z>k{i`#=AFqeM^nEE? z`K)yF`AM8WvzT|DoA~LwO;3o2;zAAs9<4?9-5l41*47=HWZj;elmF)g*hWy?LI8@p zp<1z5Q^W4Fu!?a~gWW$VL1akjD zb_0dkO-a22@H0N8$D8c>3&M2Q>X+;sZ^ zjok!tJ1o55Wy~OP8ztcds=KKXhu|G#1 zX2+Hr-}Zz}w=;moHxYFWqAVa-|H9f=NaY9A9x#O^VWNZ|appgtgIqolFdu8$p@BV= z_yZ$YkOYvJaG!$wgbYAsHoVM3N(Uglu<{(9Z%FhzhW$taNKCK`Km_q_Ln?2Hag)}{ iCRn^<*o!29#3b2G@Gu1FB)Xl5D&*- zKqU^@yBwUnnbcVhih9kQ_VW6gl}o?x5!UZ}e7xa{vG0=67!v z^)Hf_*jKypm)^;a%@T@t?(}kf=dFp&({5}K?T_Tho#1r)(abBs^|8CBF~8J&s?4Kv z{;;~sDkJ6}*}LXz9jJOSKcoF=$>-uz^X=;;zB`~dOdA{lw zgB~${$P=EvrYd>YBwa1@B@IELjM6M}sb7w&FK)Y|Ecxup0}qhHWzVL9C=g%-n+x=V zZ+sm0N8|jwo#nrB95QBQOmO4h`0t{6V@0Y!@3NoQHaXV{oV(R{tj$;7&Uni8*hZ&s zO9Q$#hPj`RIQf!S+K?G&7C0VGePa#l7VT0jmcCgi{_;Pgx7VGN-EPY++&iSIFS7yM_>UU^FV+XLNPFfa6bVv37ZdA#$;w!4`P7KXABK?aRzZX zWal|-$!eWsH$Uq1w*@s1Jp!TQgUzvKT znbzt*n~bJ3by-EP|9SoN;;b#MeXkU@315O(NgX>Qog%=-O-yrmKjq(!x;>ExVgvu3 zX;V5bQGekai-Jz`w6MY(&c~s4GD6~m!6C#n?rnDd^Pa%JukO1`1@M>87u{*drF}JD zTq`7AcGB9=;5f@;*3bTzUr3$Vv_St?Pr~GuknSU|#Mbmpt7f)j2bu?td&Ad`%riD9 zo|(p?n*B=UTM8$)vr?yzQl4kt$&;oNgbjh}QWylpPBSoQWCGbJai6s4R47!8v!J-Z z%GlV%z!WF|6@$|${((=K%H#NsReLJ!xOdevWq!cn`+gQ7R{JETeBRgW3slGy5E>NV z;|kIO0qLi%C0ESiEA8F-D_?JN6o=cX5{uX683Go@>$GMrzV;cUiuLP5sA>jAs9PP% zu6fTm8o288gN0U!v(BBopT(w@7xF+T$lBhHDv{h5VMV?6=|g0&eF3wUU%43AUYq27=p+Ncot6J5kay zaptGM;s)7hFdIuaV9k>>u!j_96)&F-dk4N}eRTkB8(BxQ##tHivM&W?lZ{vQ>FUYV6V&X2lq3p|*V&Hu5E{ zZ_C~MZE|%wRDTMzjR!M=5vg4VihGoLl8Cl61N-{h51{So44{5is6Ai`W(kmk#DuHB tmDa%ZG_W0@0ab~T{)u!`4UOG|HSZ4+w^0&apgs{b;t;(KhetYy0RZDfY+?Wa literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410140 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410140 new file mode 100644 index 0000000000000000000000000000000000000000..d08b2214abec7e3739690691077187461c63141e GIT binary patch literal 3924 zcmZQzfB=#Ftv{ud=KQK*erR^g^YFK+d)FF@`+FK*h|b?>KIudiP?d1{QP-nZOH=f& zXO&#D(bXuNy?kXOQ{JXUl2!G)yXIasTXN4N@*;fLzRc{2be> z2oMVbPJu*I7<{}PK=d|G*V5^4zbu%vXX_mkJDn{J2mk5JRAW8)`OD>~R>r7H(|Km| zKit|aufEXa=Q{mU3D35awXgiMrTqUn3*GqzH_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ4fsxFc69> zpgLwCW&)`{>}7b}-a_E^J?07jxN776&Q++5nEtA^c|E`2EbXfQQyBQ29DrdE1yc{w z0|cP>1IHmj{R|9jAhp4+&LDjZ@5R)A{{QL_7kfMUcWh3RdVALK4k1}dN8RSXqU@?7 zR~R-f+M{?d`Ux4 zD5ErsTWka%C`&%O^1uV?-)NBCAO}DJGMnLV+3vTCol{Sw?)vAoV4;j1*Y0Xr zLkFeL1^>>x-oY;M52T0b*M|m}Ua+}9KeY7pls*mRnx)M@W!IW=v4`J9rKPOvBfX%riD9 zo|(p?n*B=UTM8$)vr?yzQl4kt$&;oNgbjg8QWylpPBSoQWC7VIZckct3g$P?g5m-z zV`CEoXr_dz1Jfz~flryrweBdf+;MPFoMbxm>S~DPnko4`5-^Q z{6R~5DDekIupkK_F=3$uN%L?Xr0fEPIoN+_bqc6l1H~b^e#fvMNdSoncMUi{!g=`I zMxvXfE|thY;~m3ZBmpEQ+!e&S2_A+ZokW*uNdAD^2xMS$h^$7%%{@&?tQVhlibvdb z`A{8u{^W`&p_#`l<`!{Pbb!hb5J>qC1V9>Q1S60OE<=H0u(S-%lSGt#4D9P~*Fej^ zOrRN{auW_UG84bN69M@n)!AH2mf`*XGb#g1z+0!8Bn}c2<|LeX4Qp8f(ggr2{*EgE literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410141 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410141 new file mode 100644 index 0000000000000000000000000000000000000000..71202b875420d43e9d4f28581fbc3af340557301 GIT binary patch literal 3844 zcmZQzfPkr2?K*f)@WeV8cfMbAu6p?k{!>AbHJ9D)-T2UGvRl&-s7hEQf9p>vr8&Q9 zm>-%Q^E~`*>fW`6;{Kk77ozibnol}W^<(~tUb~rFuALK&m(N+JsTbVN)4xt3SoPWG z7q`Texe`D&B`rE>0iuC`5ky=q%DkBr@ZB?)efFalrb1@#|G&6AM~`j)>WxnF;(yX* zfJz+xs<>aV*^>S04lDmVp+_MTawX$D9^EQ5X1zJrtZ;g^UZtdSSU6pgb z?%!**4T~DQwWe6s6x}>y)}6*<|Nr@&8|(gOmCtNrtvbLU+7iqAVB6;9DIga!A3w+T zx&_37fKwpR6b2t}2N1o@)3tQ^+b;_y?b&+A#7<{R!@++#Gu2p6e*SVfs+BS7(sZ8L z{13Nw%d0Om`MFO2RKl|@W$i2fY$^YL&O!ObM|@E#2lo!+0H7*hWIO$~}^;F3K( zC$UA@VfMA+n;HD&E)TD#K3k>KwQ%a2rV=BOz>||zPZ?j;3hz?#c+4LCcpJn`3=D+g z3aE}5h?zj@^Mt3bsY>28Nmt8!NkdR5qcn?L>X)PHi`(ufOFp~uz=MI`$pIJ!(J=KO zJwO18KX4op)X%`c22vaB>I~Ay@V9LD+r`eQCsKF)^IEV_#*S-uwXC6o(&vJIXI}4M zm-xr9anT;VqhO^Vf)VTnU^?ItUr=$7C3f?UYucg1^j{Y5+ z)1=;>b-Y7JR?<l^2|APGoasv=c6z2O?WJH;{6&M&5m`CP?hx%KFhNauu zLgj%JBs>m#8D6)y5V(DhdBQ)g+PJ@S6>1}Bk@Ocw&Mfx^nFS6D!`F_?Gd3umnZ}};{YvFq3MaR-Qm2nn zo@d_4lcp1dLHRy~K|t&@1A|63$bOJU7)V-lDgnp_iE$Pb7g!k^8=IIwBw^~nbc%oA zQ>OAbzGKy%N;~dd^-P%`aQMETMTpfti7B7=HTwd!Fa?AL1^Bo^G=s?WQ`eF!X7QEw zZvB<7H#v&K?No`y>+%c%3*&WKGZ$a`3{u1FZhZhm0|6t{tqyPJHGQ~Y5wYb}Ygdo& zryf_UiQUd;Uv8CTm{!Q48XqdL#i@14-IIM=i#5GZDxOJtwd9M`)P+?&C-WJO3jN&i zgB55N^V-W+=eVz3yxe%>YgJU>%{R7_XL4|AFJkvub~A0$bE~&t8-d~S9|%BpL-~wA z?ms9S6t>L3JQD-uGY}EZ4D9P~OF+Xq3#bp|7C3-e0u(@E!ll7+3FkrbHUrFlV3}(S zm16{zWiWNbx#`vx8oLSPc35}~Hn&j{UZDI!jW`5{2~xcYj=tNccqUKgwB|6{Bt3oV zyy7!+TUlge*g6uvf0#NY=!mNlG>*YF7LW}~GcXz?4GVKnxef-zltoOxJ~Tk<0+=>9 zjchnn5GDMGG+%}U^RcEK8rVaLKQMv?NdSon4@QumkO8Qk2IXb+bO6!|i$hR312&7= zbq~mDr1}`yy|6H$iM`-D577=lwwEF2OzgEJE1y;t%_Bz{=j`NIp%3Soc4{C$J0CKp2#fWIbFi2f0fu&6p_kavQ5mPjC$YlX9Ma literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410142 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410142 new file mode 100644 index 0000000000000000000000000000000000000000..2add5746e716a3faf975ab28af9704f06d4b27b1 GIT binary patch literal 6092 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410143 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410143 new file mode 100644 index 0000000000000000000000000000000000000000..1291873dc681e1f5764efea22b5c8ccc9025917e GIT binary patch literal 5960 zcmd5=3pABk6#kJs$|Jenj|fT9gr*x^E^Z;x&5)9s(v`eLU6qu#LCw&WM|v462CY1A zuPH-CbB)9;lA4f;avL?8Q7Yz~-~ZqB6U()l)@rYH@7d>^z0dy6-us+?|389^5QR5q z*r%|(bn~)xHKf*V*4mzwZ2B|0^VK%h`lXbUZ9r^$RDzxMeS6`1sy^=veZFyF~OepwJ5jNaF zUU(|_zUNZ6L;9wMhu{5;For&QIeSD(s{e6(YwTQVc1!5}9Wr6G&U;?Z3bm7Rj`E^+ z8OyKbuypH8nho5NwC%dYHg=sK!Gs`_#R|xYf}b#yUl-KY__3?#UC*kTLoYvE_s!jH z(#1+tUgX`;Ow~9fc@vSbrXn-6B7rCi{Lr|8b}YehbWS#+$>i#CPleL^FK#nztc`HK6A*-?msgxih-(lG36fo8aI>f9;qFfT8|ltXcb1IBc_Y zZMePm#YfHdj8pXM?M`GDvr473{>qruh*11#;2Jg}JYWrUM)?n%hs|JY+CwKB1=A|0 zM^BoXl#TYO`m2TNP-0*E*m5uLU$t^hCKb~|btkol*?3O56UL)c=DSH8*d}z*az3S& z^~~(d*_)ZNPj0v8?PvvS3R}6@)0_c=lXqRqwmrwBcOPaC1{ty3#aGttBKL;vkR_X( zGswzq=?8L3A^YKVfRYfpKM)i1$mP5soy#913i950ac%qx*5BHx?{m(tW?eR?+Rw#) z$<+uo-o|pZ^*fw`W68!>HGNp;Igd41=4Sg=wsB5LgVj)9F7CAw$t=;TPoJji_d@4i zhQtgT?LI)rXGzNJ#XydM$SVASAo>A-ALGk9crM|XL~v-ZnF*j}paTe(6V!jg zbXRA(YX9N3u+tpbSo$dq$IG3T z>c8B5#%uU!7nXX1Vr{qa-(+!V_mG5p6&B`-77I)$*Cj|Zz}^S6%2#Zeqq;R=vV1!_B#x z1)fLRX3mfwVy^PgVYosC^;=%Bfh2^k3u60#vf&bsGLYsuEb4#lgMK;@~yPy z;16Zi+z!QmUW9wL@1&&~NoZD)@)EXGGBd;PBrXojmAO@3sdSrvPJ;DQ<2HRaoOfDT z%x3zV-MZ$DS{_L;9desgGwWYE)i8lgaF?#*Ac)pcXdi;W2|FjSi;qh|-qm45LR`1! zVx+SA7WQm4*(hnIBUR3VJeOkJzWPw>;j0gqE(B7Qv=<~>C@laEA^Hkb6sfnD+rFHX z8t{SbjueGh&u>yI+`4UVm2Z+uPr`ERAKs2wM=`-TO$E=CZD2m4zCSqRKaci4er~CY zuK4hkx2i`?-8IWXF8-a9t6o=09z1Z{!7r0pwTE)$p@bW+`Qf&M*KD?~Nq*7H^zQVL z+6(c*f<-l6v`k?!mst`>=g6h5^BSVSORc9mIIJ zUlH(ZggJ+j0QZuD{)xOYupGP#t$-55!YAAa7$C6xY4jZMd(bu{%s$8u3v5mZKCdwz z5fGEp7sdp!+= z=L^{RoPB%(n=n9N`MZUCA`*U<$L0alg>x{5{BtwKy?=9^v6Coq`-4y2%Cfch4FO3`hq-<743ZXgL)OM4bo73Tp6?koCKCHFi|{F`{2$I z$0SGPzW_A?#I)-RV}jUmfAQn`m0wr4 zvU7DIbKSGm*lfuV(+ulnc{PUqOBptcG49nYdkzvBQwbJJHT*teV`xE|gT z5|hBe|A2M&5iQ8fS3%wFs5;H^ee&kIsSs*<3(H H@IC$qz?p%+ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410144 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410144 new file mode 100644 index 0000000000000000000000000000000000000000..75866b93dbf7b8d36c0046c8da4896bcd5f43ddf GIT binary patch literal 6080 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%0AVftD24Jr3I(A^bf~knNOD2 z0+@5-zsdxzH94i`ZT!WU=hXy3{-C{wSr@N9-ZZ~}=hm~MoInG?VcEu){Gs5|MZwUg zSCb`F^6kwNb~9NVPS?_Niphw1S<4PhI|)Gb$l-|0X4u|q#TvwPDCV~3?tZao#n7*! zYgWwA<6v^KXZTz@J?<6A@l3xyG=OLzU<8{B^n;$t8lex;hpmksZP?At_bup4Lec|-1tuZs8Oj8Q12ZsxB)|lS z$PWxsmkMS;^At*0fXqM=Kw`p7hlD$v2g%0_F#Cb&(-SJk2oy_%sUylw7he9Lv710? z282Q3HQ3xnNqB+t0xYaSG*ZMNG2trE)q~1nP#l881g(xlmIIqbL|hVI*20{GD07kB zOG!MT`;mHKLVnr-xe3{S$Q%@LXt1FB0XZ&^+cFGOpFS=teUN{_BHZlw5~Vk8Q-mHi z9r;n~IfvcliM4Eq6WBapz49LjklerseSZi9mBu(kF;- zHu1rym)rUrL_9gREpqD?y?o(h_>#ix3zH8YC{SH57!6W`rLF@DLkU=zC%^;=*8f0- zvS)!!f28^mrWZtGNth_%N0j-e?YpEEAyHXLyPsuDh`*ttV|=`rQZ z+lQvP?Rj{e_w*@^x3d=6muO4N%nG^fEo&T7xzYDo?PJj!I>}edqkk5zJSkjQEiL)| zokD0_!WNKCNsCSfgJ>XN1QAtoe@`pEvy2~61sNUDaa>=F} zsKi0aNJMo(qpI}$D|>nuoxHiGHgCV&B)Qe!jxXCUt#V}PPW3~Qt}?z~Ro||$vnJA!$ha|=O>1ge}7YhVj8$)PtQqg zQFfSpt@vgJf4R%U>#5IHDRnKJ`lhMGNF?y&WYtr~SGB^slsq1@M?c;MaT5arp|}F7 zV+LX-koq>hMa#HZ5iGY3o3-fo6f-uW>i>+oP$|syhE)$*A#t zH02VL5BnY7qpGV*71g3bcpZ@Z3-%w#4L~fBZe`Q`VDk-5Lb+E7?=oX-2>shBj zW`h7D*nPmTU_Yw)(`bL=#)JKylIs_^E@@PJ)Yc)IK7FU*+Rf+gyjc>>9XU(fMS!Dn z$A!OV*j}#AdZYTtQQ(K*tsSe5w{>pf1DXX6ixmIBr%dH>e8;Lim3G{_>X|Y>;P8Dv zix8`Q5>r0!YxZSeY+C@db}IwZcUO@8Knw@KyqpW zgzd|H)9H^k>Ex7bpJ{f#X_mkw5( zH+Oxt;(D;fhnp#qC$4Vxf44vC`i{S{>vcxZitJ0=tiIm)YOZ6qelh zMe-)+Yd7YH+asi2g@7#uhb4psbp&#Fg3W>EGq5x;Vmp}B8LN5Kav0v6J{Nx?1S?VaheDc1IDQzR1OxVU@lQ^ zI-l+U%`X`CA_*Wdp*BIwV>l1Dn?PX&3$G-Y`a$A0O2P}|Clr7bNJvb$5_I+W@+Gny z*eoK#3tmSPY_Gtaggt*S!^#SfG9v6{kV2#(bUz|jz?6jvQawP1n^5dV;vg|$fr2xy zfzvKh`N`lpqkNL{wY@(+ZTxe9WzyVF6OLy|NqwzZ!gS6o>JrD~Ua*P4a^yb{0NF6R z7=hehP}@M^1q$y(D4&6d_J)|o9O+{~<3Md37oa{~m|hSKvjmk1SAiZsuyh8tA6Gd- rl$&NvRim++KyHVHR}#!egT!qpoR?Se(SACaq}ic1t+MCgen3S%~8?Q@Ke%Rl*ALg~?X)uTFT} z7b2Pz-{U@2@5jcT!&=%)KWALZZ8+j^)M(9nk>7U%1$s)>@9^w+?C|K7gk;vT&mo+@ z_exdl3U~Wtmu$L$ zN*oG5TKeAkW_F|FyV)hVdBNKM;zWL__1%fSVph_t^nc?Dm1QnhWw*>(rZvOL)oGRO z`+_A~{`F0owXONU&Am8krdz7wmy zG5_DNPR69ulX05@+er!Ao3o#lIsGzJiryj7BK&;cv`7ZgmSo-s+cqyx0lAp@_&L4p zEFcyHoC1laF!*>ofaq0|bg+p|%J(vU2m~*J*rt@t( z=6t3VOx%+>=Jpk`sGU2LsKHym@z?KfDZW5;;vQu$V9J@mZULqTj@Jz5mGe22fA@3y z71oMLzxLVu`tcQ>wSFzzI|c5so>w~aqGYCP94(*LJM@cVhFinC4c? z73=w+ZgA)ds8N)RikMOjmY!2gM$F4vc98mDS7#tagj;}WtY@79s|6B_VD|yT zbg#e7!mRW;er9LwqpE8vMLm}YM@Z&uFY9?#rv2P~TA#hs0s(DFtxpRDB-9pid^XNM zdyRGCN4>`r-%GsPJh_DpXcp6!-M+!X$?+GJ{{K1C@Fv(vw)gh&x+96a8GkR%_^z;| z7HlafEkOWsSU`oq;mHilZ=kRO1A<`-RK@h`Lj$rNs8%oqvjj)Y3;L>;lD;xON;AIprtZ7BQjefo9Ro!i%44DbfK z6f^CEg`osA+;Y#r1PPYQK!sLcrMsbJGpLS60Z0LZ#DpsW*@q0EaSE|NX;IK3s3a^* z!Ca!;^z^1Tjok!tJ2boyd2Eoljgs&J)vqW3DdLcra3wh65E3S6`4SYrpnMq*(@n6R zUU%Pi*O?|!p!yBKtULjx4@P5g5Q^W4Fkh`GbQUS*Ga#3#M7JN1%3+w>K{OWk!=(X6 C9_Y&e literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410148 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410148 new file mode 100644 index 0000000000000000000000000000000000000000..4e22f6aa659456e7cdb4413c7231fb7b6eaa3749 GIT binary patch literal 2920 zcmZQzfPhsOjz|UHQhj%jRs2eby;~piQ}xBK*mnMJT6M~v-}mzapeo@MX5-YiTUN$e ze^{Kvr6#Rt?{-T##wO%MURj9aF;lro$-DENm3w1tmTE75_54tK>ecX9*`LFY`P^Ty zL3_=SnM*)6B`rD`3!;I55k#y|IV-VeZYi6u=B()GHA}1yuhKWlv=FuJ(VxBbbnXXZ zpc02v##tMdeC0l5|74cF=$aV53r!!QH@y6D760rIx7qCxzv)Gnp<=yB@Zrc{ z4>MhWbFm7p057p5OYKZMUSGaMoc4(Uuh62irC;PXW1@`S>|s z>lP3T0#1QMQy6@_9YFLpPuJ4vZ@(;J!O1VE4)j|<1u^m<82T(0Ubgh zu7K*8pqL@&P|Ho{+jh+POe>hUCv(j0D`ZhScP3GTw|?WV-``Sv8Tg$XfMJjfR1c0b zAPoY@Y=+9KGkEw8u9wC7_Oq-y zec}9ib;l~f%91jfqLB2*oItZc{$Qw8ZNGkMiSgMy@vws_fh@mQ8GEkak>XpBJyE|` zQvJkxuphV;R{@ny24WPqgY-iINE|FDPOLI7iVGRvKm|O430IC!BD0=}@&IEP~ z(BBElw;pdPmp#>d)r{Zqh{3;}-iu!nBkq0^UGtz*$;kV6Y~8ZkcKj(hx_y~xi9&**%!M+6g4*+YsCGCIMu=ibQn{%siET62L=8MgY1fK zg!#)K4@{Stsxz}_#{bIT$OwUxV8?^f00dwQJ5U@jL(>RY6~QzG3l~Vbf*OsWuvo$f z%7-u&M46wy^Z>G(V0u9`7V}~G0pt%_+CzyyFr1AffW(CR6r4BUJWw2g0MvhIWid#O z62D{Ek0gM^1iJu45brh;<3nqf11#P#>_rklVv_78co>3o5?%Ho`2%hvkb%u1+h&PQ z__aBsE4JZYjO-SN`w~;Em{jy;9SX6CyP*B44U#^I%9FWgV0IBKBY_I7F4j(FfRvMy zVESM*k~@)@Fj-vj39&zEQP3i&N|f|Zl$#D&2h-S1So8iMaT_J!1*+>%08+#uG2u#x z&y$d{1f|R;SZA)guT~T~OB86^24Je60Jjy&z~UejzY}3TzvjJ7q?nJAkBP3!Nyx`A GKLP+K7trYd literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410149 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410149 new file mode 100644 index 0000000000000000000000000000000000000000..7e28da78388291b99ebb85d7d4284abef1d5f67e GIT binary patch literal 2744 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410154 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410154 new file mode 100644 index 0000000000000000000000000000000000000000..b873382d7429f70568184602c6002a95d50fcc08 GIT binary patch literal 5896 zcmZQzfB-jDdycgNos}C3es8&Z@K5FUt^I!_c+Pf7KI)6(zUXres7m%6{qr8{6&%K$i@mf)@q;2kk-A|th%2{gO z^W{0naVX7l%ba%U{ExDEddGO1L}gC||E=*=PFZ^-bRmOiOBU~gZJU>;fLzRc{9N|Q z6Cf4@oC1laF!*>ofau%>7g`#=Nu01PtjK#mH)x%qgJS(+t4ijFKkc?}QMe#9oo6=x z!>!%&>I+SNuG2r2@N7$2`^rCC%Kx9U(4Aj!Gs!%>N5e#?_va^ulz)FygJK%EWKYjY zY*BWYeXaOr27kHB!|SQfRw;EYocgAz#7HFY3EK&9X~ z1L*+*WH!Uz8p$2C_Nv^eyT4DdtYJGiCFPi7gNFV2#{pi;9(?If1!<5y3#Pz&!R7+v zcJGX)WeX1VR4zJvw)E7jTSd|${}^Lg9`C!xUv{~;Z1tn&{S5B97B;Rs!=2KX-|2sB zw!OnQa5aNn%sl4$yZSFUfQEtnaL}Z($LaVx-p!$gR}an#uJ7J&TzF%`$`f0Z6!Wic zwphn7V_E}}A5h#5Op`M~jsgOhILKTEFBbW20mA%$mwhP8KXcXe-0x2TQNkZ4u1r{^ zb82SIL`9GqAYcTW5A@4wsZHUnegAfNh^PFxE213TB%ps~`ek{pMXE(Ra-R#Wad~;^ zZoPlmr{G<_U;;bqyOmi}4rEWT$=BSGUd;zI3+$Hyjft(X!T*;nkYsme`%Wdu> zlClX~g=GX9`O178gzPMoZH?~x_T1>*d&gWCsA`|fD^YilY9idh7~l$4%5cV0D9uK6 zV(y=`B-;TEBZmv!u>~4VCHB8o z>t%kLl@rE(H^Tq$iV1q=C7ZjXw==DoaOdXR-H#UiQvB$=QNBQqYx7FyDbv*t=LOZO z7sz~n%?@-YI6e$tJ2KDMpm=5)i)!{Om2WAW+|Ej!K1z9>c_&YrP7pQ(s!L%I5IfDl zpiuQ$Fu&_64eE3J47f@Nos}1rzC~t|eE@;w$al`YT^=aukQ#sS=CVQ;v{YZtsvw14uUKDKG^L6f!#vyJn6+%COZIGOvAm9L{` z*tThzT}f|Si;B_?$zNp)IQZXCcjL#EscSFoxwmE8?x&#i1P+&rn%(mX4=Kzr6Fq2E ztHqJDLQL{Vw3oxZtGSlWdgZ@Y5mRpLjIw?c6nH3s$J_AKsn=r5mwy!M*5i1)yR*V= z7ysfD9biBF`p^LN1S8m7U|zeZ($QDq?|N-hcjuGx{ny`W|Jil#7xxv31Nnz9aZ57) z{4;-MTGuY6U)jRVs}vXu53|h?mDAaOF^OsUo0)eCUULG?W8K)fPwIbP!LNO5*<@Lt z?aSiKJaAw|QL(p_>lKOr9(R+WcBcFX0w5a}U%Wu>Kd2li9GHRSN<5TLOr8<<$gC-W z=HIDc{V>fy8fFP96RrRp?r?V-gVc|8{+(t=w zfy!V=Jir4V7V^l^2^Yf^hu|M89L$k0gM^B-w3nKM>&_ zkoyy0HW2Cu@GRSt=pQHuZ2vKA0AdU`VhDiLAcYSR=2uKV{xh8f^BLAX+Hdg6RH(Qs z{?Pw#tR0X1B^c`t3SQb^Ts~n#RdO$^500KEVO|E&pzs2P13X_5>?cx~7l|{U^1Mg` zdr;z&MDsC{2a*616CO42bPN@NwE@w~W{_Tx979a6L*Qi=(c>9Ndf+w!8Q2^$_4B6a8Lv%V=NHR7@mpPerGa&M!mNaF8Sl8VuSPe2 znnU%IlqU)H!GMN>$6b)}F#&g?q-EmFPl3e^ve95RmT&;2Us%}!4{sXSLy12yoQ))a z#Dsg7qC82W-!be*5jw;bkpz&KB)bVEPZB*=faDLjjX(xA Hhrr|kXAow~ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410155 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410155 new file mode 100644 index 0000000000000000000000000000000000000000..52c73b5891ea1ec43514c52576ba76bef67d52c4 GIT binary patch literal 5132 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 D5wlI^ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410160 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410160 new file mode 100644 index 0000000000000000000000000000000000000000..048e5e4c93189dfda55507c10b22771c6ecd9767 GIT binary patch literal 4644 zcmZQzfPe;`C*PVCx{SqEeytVKI&m<3-tyHmyi%@wSevi;p34@bNZ3nZl_iVl*3t^) zbwYoC>XtXlSZywLUHCcwk>;#^@ki4OHPWuMO-i#V(VZ`PdeZ+t*I&Jgywp8q@AsJB zZ~JbBd4LQ_T6A&_hz0^i5V18@VAJke4Zo`gtlXDA+&bYTe|U4KyUdY*>U~Wtmu$L$ zN*uTY-n}^b(D*^rmq`|GGBS*PdS~ONKI47lxO&~qNt4bri#cBSI6-Ym^NPpyGxlk) z-95eKefZMklIh_#VWG|}F}zg^*cC3->^`T#^y%{{4%@#~4GwZAA4z{+r8;3}@r`H| z@1==5AGKc0_hXuHv%2NdX_=&0-edDP+W&vH*ww5UyMRHorGWRrw#~~^KrUuJelGj6 z28aa#r$C}93_jitAUb!!g_eeI5+`g6EArmY4O(aDpjf}ys*?HPPrL0~6fOu&=b6p_ zaBH```a+YR>-0}0Jlj&%zVgqO^8e>7bmte`OfnDe(J;~J{rQO@<=@}bpqK_O+0%0p zTa+DUUn{R5o738X&7KkzA2c^u!dYEPvd_pW-T%nvwx-_Ih%YM;cE&-h%<&K}Tn8^XZdrvN2l+>$|&XFlrv2FR7>3(TfjhCCk3sE0CMG}us2H3kB8@Nwga!rpxPtY92_oX2f!W>q08|Gf)U6H?KGH9lG7lWxIq&~M z$Idegy&P;M#T9a+wh1)N+`{=PRf2Tj=#4n7|OiT{js+i{l`OBU= z$;%Hk4;(Htdz;tH5mh?OkZrub?=KD$eLT$z_82j#-C}mmZtK5_T8>i2gg0ruMZ6{KY+~z#%=yI*&SzEUq)QK zDOaU(X$A+cnaad1zv?u9R*SRmvQe_W!1CH6e)ZyV+bLf)!mOjU{Iwoi{Sjrf>MeM= zvLeY86t^s0y!S3$ZqE^2Sdz2SXnH9JYC=xma7%#VF#EvfLJ4rVBMC5r*^vANDx0Q5 z8ZnJoQ{sS05L5@c12yqN zwSp<6go(t2s~{_l66dDLKtIvcO<2?DAaNT?8YR(9pmq#3;t*>ZwLTh~KUexg^GvsZ z?;$2P4kbn(`B(V+`jPXpZn^Kw7eqi)2B_T#0KHSN&A9!mUy5iCdoNKCk|aOGw6bO6!|i$i$XPNLs2 z>_-wnVv^xD^5Y%ZUU0h;NdSpSvYQyry7N4K!K=E}S|&*$MDBET_sIk1egF8gq?xAt zy&JOeFw{i!JOj4~$bf}6yj&;P9s_EUJ)6ovLYhWNYs8tKGKU27vE~^X*h7gwFya|W z0Er2Y8dCBMsQm_NKf?12iGIhhA4vd-Nru} PAnAeI2xMS$2uvOTGRH)Z literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410161 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410161 new file mode 100644 index 0000000000000000000000000000000000000000..0904b1bce9751d64d0d7f18eefc12cc5b3bf9e37 GIT binary patch literal 7296 zcmd5>3piBU7e8aX38_3PZyBjv@(7ojJToC9rCaKcT!m5-lDS?rbU&0H^poDX zN{?G)l$4A|AyI$aic+Ko|9#G!GxrdG)0OYDzi*s%_E~HHetWI8_g?3KAWq{?DRgx` zS3+v zcs}jQ-Zr;W)C`h7BD*(mu-jeBcKO@5-_IHNP--31sTa>rEpqQUf2^hhM)F82OXDCy z1o&FL!@JJIvDbF1(5aVg2GWL|gKbrb+JZN;E}JXKvNz2JDrD<5XAbv+117^G^)T~~Km`WG?N#!aq*F4R5mbF5Wcn1y_0BbgOd z5PI58K|6b}_->*xG$WEWbP zwJd%PQ?FXfB}R2wi_p530uCn#8Y@;nPZ0e0V13=cmlBU(Qh9Kce~xd5dXJ}wn1yPz zovNR$Us;*K!D+fc#S@Z}DTg4|`bBXJ8+26!k}@|GsuL(SfX`4nQC0WOGr+<(B-UjliUU%GNYA5qP8 zZnU+A^@97(+S%FfLIVwhi)wWQHmWKJRX;p+Fu?(}Lu`4#6T{&FQ;2}_Ae#v;w2Cli zgt|bQaV?sMMCw0HRsPLJm_PFUnZ!e>xo2i}gv%r*|1rO|ap%F^H=m{lmk8NVw|Cw? z=J)<~Ww5#k7z@dT1!u!);W{?D3!0|i@;SXT0=n){%RU-vY@oz0eilX&fF#YyU_ESy zvB3{(u#DV~X%9gHm=S6N=JbCX0l@|P0EK<;KB<{@LS9tUGMrafnE5ms7RAf^w*US#GrWEM(a z2vDNXS!et|A9-S~IDLvqzNp=5H{Uksa$1r0Jro7_5#a#lYj6K%g+W%)9e{ehKdsMJHr~-(Q4~cv0zqPh5K(I{L=5x*Hu$)5NdbX~ z-oWF+jn7%E6%ocBpQCTMI2;xbKdxG7i8x1CA&H#l6LolT^4jvAl#uGu8}-7nHG}Ovs=Ppdx6JxUOVqZ;px=)@}T!(VOv?o6j z^B40q$Z&_e$ejrTg<(a%+*(f^c(7NsR4%--f?s()XKX+dcTvrpV3)im22uxv)=9uq1ewTd3R@;1Q<{-ZC_rKcpxD; zI;M7^{_bkSQo4M~zAcC6P-3N%b5u5EprD?EpaB{4BLUnFpwHlV5YfF8>Ehwsg@i6v zr=Vx=E?|V;JGmgBAHze&MgJi2$8~VlFK6$D>##)h83C?uFg+eQnNBb#m^uC%9S~37?_4hSZc^#@cvii?(#-nyxZaqawebX(2x!(6D&m&76nE7^z}` zjO7ge{Kb5(g6cv%)FiUY{KY|?j?_WTm>qV-KyY~2BltR6jw1LTX{G(B@(`c%#tEEh z6U%AD9GxWG#^bM-_yEE3zl`DV9EFF$@{7Bsztdk6#|^g!P7+R#PIW1<=d%1AJHlt< z&1ZJFhTkxd6LI?>R%fJkpu_mo0yIv;*E4>9!Tb%^>qz6FK8N_ebVcCLDa;nW;&K7& z9+GQ3jz9Z5#)s#C&4Zga6M!?Gd3@s*TnA3XTsG>y$J3hm&bfbskL$pR|AHHC&5VN+ za{l0m;Kc9d1O=oSeO(*dpm@(GKWT86Xn3D++=h*vq-SmFin4`e<;XV!FzQHk+7}cJ z%Qfzr;i0CL(L5Os?2$See#yYzSCAob6T*oo+URE>fC{6B8DU8Zhjf7eXF*sIXhh|GUM)%xd!qz3BDcz)1|LO zK3eu+xjq{2;WY{TO%-ccj&8#`c=(|NG+_{0}<4brJvo literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410162 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410162 new file mode 100644 index 0000000000000000000000000000000000000000..a8a09edf7b5c752ccc66d008751f2cb9ab568e92 GIT binary patch literal 7176 zcmd5=2|QKVA3x6$vL>pjhI-XRcHXnaM3FRhSwb66Q9}%Rme63L#ozK;kSK~?Go!`G5NAx#Mv038lx>x#3Xrk)1MaY=8b*rt-gNM zlo_2Onx@vT(`hH@b(y~%w+T5|kiX?+KOW6`Frtsiwc5sQdKF=TjD_uU@zIMUsB3a`)PsLXHizW0PAp1-Y6!Dk?qp zy|r@8?~e+zIwX6iHiz2+`M94XADlS0kAV;iUaMMjPYCrmtsIRqfYlEs=<2uUM%;$Z z38lt?q5dy-I~>+BnHrsI7$(@%{r$<~Q*Oxal-MQR#NMJfb1$v?utxdv*5m8q5A+^q zeyGw{%MQ5bspqm)iKd#^-3}P<`@9`IHjMuEdgh9_X(qesg6{c9GtHXrc()c%H}Bp4 zEGAf6&c2K_qg)v2OX*)LHD=0Wt6X5PE`Pg23%fqCJm&>M= zOm=$E0#fyK0)rWmtEbw>SIf1gfxfMI`(0vHN)0YXCuOthqa$U~glQtH*1sO_Ee7>r zei;%oW#b;WO*RndS_sz+@JrbRI3{!^^SMINxrM(rWl5xK&=?)&mq&Fp(swo&ss0#z zGKJ=iujAE6nA{WNTg+#6#>y^YQC^hdIm*UYS*?b72d|~cJgjca_qhvo3Yadkv~hqP zqVeKOik2E7PQhbhqVYgQu2S&tHY=gX3YUFZidMUs$}6Qx6zZ9p%MwJd)TJM)x2Adh z;E|N@YAs#p=km_<+1`Jp)Hr7>)R^D(I)m%W^u*YMnex<264v?kwW&6hlbDTpOXJ%^ z%UJuEqhxRN0O`3V@Htxx0bstzfuQ|jdmeH<0Hctsqxn2+sak$?V6>)%mRh6S`PIAE zZ2%=UcYAk48qgJc&o)HO6b46gPOD$Rt-y5{2AFG3>~R%;Co*wDcB-3|cJ5P|?}Av{ zbT+?sU)A@}HFWWndZy1R-A>2b@@MA#B5xF`E3Zxkdy_qdv*WK-Ef>kZA^|mG_)q_= zamQ`b7B^t@2Sb{A-}>~at}x{#F{hYfR+THf98Cb2DFe+`1M!IQa1FLc0u9WOm+&$C znbOtN|Pz{tF?`U>2+*DdDs4!^Jy&k+UlE9AEK_Oa5=mgUmVVxYOb|xXPxwB`H`?>btGwd&DFJ~7xg#0nb(xsgl=YA<5;aZ|NdcK5@ z0`;10)NK7%!HdPk3iZ4F^qggmWtbBaFZSQkOeT_<%uN1Vv@RCiGRgW9x-Y2 z8PlK~9U<8Iar6r@p_*Y`D>02{PP<{sf2zS`Fd6!?_=e*5QBz1`OQwrEN;w)@ZTpI`PLxk`M>J}7XH z4O8@>8PT8YbQfiBtd2zJ=sZZ~1E!NSK8h;IZ zcaVenB{3ZME=6mJ_@X&X7Z3g-&UawQ?*uqmNZ$)kUL;z-x||2Qkk?!e@y{ouv%S8&a3p{wJTcfpG$#?eSo z#Wizcil5i@CpqYOwqwj&za?k-E4w{5uBs?j!#`n_qat^y1YZ z`DHjU4Vqs@2)2It0Pk>vbB_~gH^OFoYi ld*vqZ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410163 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410163 new file mode 100644 index 0000000000000000000000000000000000000000..67783f0c097567624047aac8693f24b3ce1624bb GIT binary patch literal 4700 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 TB)SRIXQoCRVojsaG{^t|*4tNU literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410170 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410170 new file mode 100644 index 0000000000000000000000000000000000000000..bc33e9d5e459ac5486407b67174c370e3cfc1e6e GIT binary patch literal 3876 zcmZQzfPe^dj?~#d_qXQ%QC$_9xy3xrJ{m8Vk@T#ZzfeSpY>nyGs_se&zb)0irKiBEblaf0e$HM=Z z2gltxcZuIAK~Ab-0}0Jlj&%zVgqO^8e>7bmte`OfnDe(J;~J{rQO@<=@}bpqK_O+0%0p zTa+DUUn{X?C;38cPO_3Y~JcQ=rqyNgn{45fq`3b6;SD9 zAO^=9kVXd}aj={~?Zs>z3uBKay+*n3tImsDoY9rf5K4K&EuSqvp#hBazsuTApdjV6<1a=EBJxr_FSAOyqH&@|+K_2~-mQI0eQlrET$$)@BG`Rax6gE^*k6sW3sRr&|9NCLkB*J1 z+Ih7!cBmU1qOxB8c;;bhz3MUd?m*e43LS~+{*hnv_vijSvMtxd{=%e{OB*d7t-q*K z)L>_+x8rGAl$unVO2eDkmlAv>Q^8399DX8)QYMSLHYHyuBtOH6Vxkv)z)N+V!jEyqDYQbH~5_PvEkh9qt82 zYB?Z1vS(AFS{WF@<^sdweXvR6hIavZ6F0@)~TPg->9Csd5Hpt!)w*x1Cv2q*v* zgVQPgflryr8GwG zSIpuo?cMq-UvF|0huf(Vi`V5D0v5*Wv}P{8_8F{d#!!v2M_m<`x?Q5Jb|5@=nVJXy3aH#-fql5`m z4ivV`45GOgpnSr44XDuSj>Z*eewzf-2cwZJL1MyWA#n-kLGm~Q%>JZBLEE8njG!_N zrjB6#2Py=*ses0A0)-Vcyat`yCQR1mH?DC}jb`b_uL~g;ah(%?DFhEI|oB;>>?O2f2JAU_REgLj!v# z@drk*APFEb;XcKcm(kM!NH45Bhvyp-{f=Qjk^mBu47VYbx5T(fYh@EG-ZAV&5?V|YgXs1%l0V=!0vXsG0+R;-*%Yp* literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410171 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410171 new file mode 100644 index 0000000000000000000000000000000000000000..996be09a5357574c171f48275a4c98135b2216e3 GIT binary patch literal 4908 zcmZQzfPjS6UU9P??CsgHdL>)&J*O3u%H&pPYP|iWIYaKdr4y$QP?d0mIY;X3pZi<$ z|ER7C&D>%hXL6{&KX<|}qsZrfbkem0m$Z0CHun5xdC)Vf@a@{o>6T(ECu~rj9;5lv zVPI9O#g9|&te+D2EbLRBxxI7Dx&`@3zaI&`eRRgmMrZCNSB1%IR=le8cH1tY$@R>6kq1iBW8M3^X53k>#@S?WNjkY*UUcj3rVs|vmTKMy+cqyx0lAp@__+hF zFF-5^I0X_-Ves*G0MWS%F0?d!lQ>~pSdsUBZqPbI2gUluR+Y>Tf7)%|qHsZII?rtW zhg-Yl)fbxlT&I63;n|k5_LYCOl>a|xp*z3eW|Db$kA{g(@6S&RDgXYa2E{aR$)28* z*rMz(`&#kM4E}PLhu2e|ty1b*IQ30aiIGU)$;qmxjIU~icPV*1W{-Zn4dNyS210QK zRL26uOd$0xcbqRzF&3|suIJT1{N&l&Q#tOQ;?o67_g|B4u!=Fc#lY|601SgFpi*$0 zf%E_YGMk}p*7wkbA-_I2O`q*?s(R1Aj~z;{3LS%Wf2i4CELkuo8>B(@Y$}KX0Y*HiuV<|e?%8r_O^hVV)+^=tew-Ub?_6rhn5E8obKm1f7MuC*sr}|GhlO!>U8*%zpVDIhc`z{eG$8APU^x|Uoqi?6hI>#uyh z$x$3`r%EhdmuCoA7_ZZsx%k>=kQ%069~wY35HLdB>hNwJ4@0x$`j8gUl5po5jVBZA z9k)$+^RDmBKC!929=t~Fb8ImIK!zBzhI68GVf&9-r%cn8P*?KKgp0Xf{C?UwA+u3t6hz1&WpJO1^5 z0+;RVa4#@Y%ONITPfVV|(77^G&wRuGjsux-mb^5W}GDZCGzNo89Qz~Tf-Z% zQ_S_j|4Sd{9m-f36f~b;d*yTI{nr?Up!!M5lLXt&3=HBPWx(zN znR${p^Pf|kCuv|0N}eRqe2nNr56SiEC|36cO3ljQJ1 z$&*C)@sRuhw-Ly|=8%^hZQnPZIdSXxtP9i2+gXnGM(3Z-o3N1gf$YcGiAk-Xej5m+ zK-+jQBN&m|b)dLMsV9l(3o@{;zq|(8AFKuHX9ZaT129W~d?Y4J7Sgtc^AK%0a6Jud n2V8{8p`?Ez-Q+=IH({-h2Z`G#2`^Beo*Hq8UWX$^GXnzvm1dWs literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410172 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410172 new file mode 100644 index 0000000000000000000000000000000000000000..98d8c4e6a57dd4c5a6621440c30bcb2fc2025c0b GIT binary patch literal 3924 zcmZQzfPlT)vCgW(bz83UD|;Vyn#OElVZ!em8pIU%L(Q=DhUzS!D&d6HUU9P??CsgH zdL>)&J*O3u%H&pPYP|iWIYaKdr4y%5(ai$UT+XcwD|tQ~I)3_Ny~Ar0rS$Jhx@E6> zZhXo3Ga6)5(xQ`hAT}~Eg6OTW0-JW%YWQ6}VCBB_;noQ!`NNw--DQphRPSqIxn$D~ zRO0ZJzj(*HeA~XSA`yyrIbGv-TKPCRX<2A{l*h5|KJ8=2b0#MF%7<9~?ZWfE%)Kn% zrTl)+Uq3Z1JuQb@SIYHQvvbxQyq3l9E*&G7<@a}GPh{xAqQceD@-4>YllwGH`~w8n zS=a0~nfvun=;0J+*QjMiW*M`DrtB?PFBSjxX@GGTgJ??)?}Kfdm#2VS%zXUZ0e2A) z3j$7oL{k`iyd6Mv?t%*~4c{bA*cMjgy`LMj&d@=zez8?0^TVHZ+qWoO5Sq?2oB!d~ zZh7^ECO_BdpGtVPrL2ABpDpG8&spfsFSwax9^Rv2qSO2H6GO_szo|hn4P3IP=Onf$ zJIuaTd^3Z;+~wi*)Mu-dx)x4-(^O(45_od5>M7%^TH#$v9*^0hA8&)WiGhJoTmjWF z12Gdwz2R#|<{29l&rD-c&3>iwErpZYS*g=UDbF+SVa%< zyn*zAK+>YqtUwAR##vBYU}bDkTmDN}hI-?3^>r5*RKdZx?|IDFsF zBE)K+#FWqbntg%lnF2zC0(@M-dcj2cscXph4DJA znTxM|1}S1nih^ligu2zCJ>&82l@2$a+=_L_*)fE@16c1w0@*RPuM zUT&w)9sl}2fy;JwxEC0yJ`NwD=^_>@rHGqsO*cXBE_uUPb=$sf}dyd~1+sPmOibNucu{Os0wR>vHLM4g9Q zEGPX~%W}_^7ibpPzjd>|hb|2H^}%WSY>!jbd;WdwPUb!D`rm^OVOOzf%(JpEJV{>1|{XDtzpyk1n(eq~+vzW){djJ1kb8MdY1ijO$ z3tyxd#kO{yT)5T!CFlD9P+DfbvFF;^8K1WNeK`5g{M8IE;+7udoiP1B+smWP%68Tb zOIV2aVkXayu-%2AkU`2`}_GL5?_5;t(7rX!RW^or2;Q zBnLK&h`1!ad;l8`BoO5UvU@@4n%sDzU6@ejCS?CnE1nouFmS!zntDH)`DeruO?KV0 z?=E&cnzzvN?t?#*Y%Xx|O@Rh;%6}k$#UqFYa{oc)VEF=Gju27DGO(||>;bK3>wxBf z>Qy*E@+cA$E{z^Puyh8tA6Gd-oSQB^p|P7lZij^zJbexlx1p3HB)SRIR-#56qL(A! zNaqykOzgg4c3ELX_41#a&TKs4<&x=f{lee*>|Mgno@xBhl!0E?L50B-EX?6)lwkc2 z)Fyj26<$9f8wO%yNth_%N1XX7bAaU(+)c=KU@;$S+M$6xl=uU~*+>FNOt?=$enJMY zyo{a>KzhOL9^`VJM89L$k0gM^ggb|Lw;`45#JCCCh62SqhP_AvNKBI51gm$!8i{VF OBKZSuBQ{4ONdf?bpsJYw literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410173 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410173 new file mode 100644 index 0000000000000000000000000000000000000000..bed9cbede3892e4c6a865e90700b56a1546629fb GIT binary patch literal 3844 zcmZQzfPjgQvoF8QXMKCU&Pz0XJ;(AXD|r4t%H-kL*vs?dsm7$WKvlwfwPT%Ch3mFl z=U4VV>@kZXeDn9Auf{VZRU(P<)a_qXHh zIRAb>(Gz4-(xQ_OAvQ8Fg6I`0XC?N`EoJl7oE06tW{LITRr*Gm7NWL2`m?v5&i!Bv zRO0X~>*Fmmxs7V;F3-!fAIcyr3*EMQt|1Q=N%YCTWWb9Y}>p%1>|Dptl07{q zu|?To_O;@h8T{og53i>_Tcy;saO#_;5+jkolap0X8DG^3?^5!3%pU!C8^lcv420qe zsE!$knLz3%CQo7LT$!n7zTtnzfy_9|U0d&`HrWP?PJ47_YVU#kYzBTOP#9DLm4f38 zN=pQ!>iZOFSCqQAI=h%vI68)wl_!^`+uEY3W4OI0A~hg~`?KAWo!a%Q=De5N>2t@w z{!ietogMB4Mrt`A^}(*rK#B;r0M%H}It5k>BpAW&1E!JVN6vqXJIkY7Qj#na*^+d; zYHIx3SxX<;nFn?S9CTs!*wxLER9pJNdJkjFpSq_)x&QtZyRUPOIG(eKt4v4x94H*X zVPW{%k$J`j#WT}bRI^{Hd`scvc2?^2QOfhoJ9*M{g0LY_T?&JM*l7j^jRuhYKnw>- zi%xR^IUq64g5m-zV`CFDONcB?9hgq>4}8j09>;gA+EZ!Ay{n!n^8*gw_p=DG+9xsP z^S)+ZpcbZp(4YVxSBPd1nSSb8a>XpZ(%!AV^7ST1ak!l-v3OmcAz)#=PHX1kYo9@C znARE-@o4CX4lUd9SX0zi3!xg|2A z-{IZfO}Y_l)^eI#es%3t*OXh#GEd)ijZ0aUQX|w>aA^Q?Gn5abL0nK6FhlbUSejrS z18Ms8p#fPBSR<4`HXJSlESF$vh%{e@1oJ_Dfcb-#_Amn7M6UT5!Ga`!#Ds+sBrU^v zkTQ?~WQb1&~w(Bi!3uWf>UG*3&g6~BH-^`4#)SP`h6 zh5%Ta0o5-s01I<)8UYCsQ9m(AT`GZifJN}QXDcRWh1kn* fP`iWNc%ogHQ069-_(0+yF=2s%Gq0h?6I=rTdWh8b literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410174 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410174 new file mode 100644 index 0000000000000000000000000000000000000000..395f0a84e345462bf2db5c4d58d5d2bdf0638089 GIT binary patch literal 4704 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410176 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410176 new file mode 100644 index 0000000000000000000000000000000000000000..b0e4cf4dee13c8e2a5b02be14762967771b72ca0 GIT binary patch literal 6248 zcmd5=3p`X?7e6z4kn!k2ggl~DQZl+8VLU^)V$he$qY;u6{{ zr6jl8%cNVlBy~v*z1)x_Bkp(han9T`eSD^`{C=(9Z)UHv_kXYTKWneG_c;fGMrFCi zs#PZ8^sXBBn1Veorj{)+TBatv;?pppcIlY|a{iZK)a(q`zs|adD5akyyk1$HCDUP9 zF)Kr_RbIwB;G?eRvGmrDT6%)?^{EBxZf;mCA-bQj7QonjX458#nJa$#vUebTL zOgD_mYBn+(%6&DTNuT-nSx7jPSF>EC@}|Xwr-^^uQ5RLXo>)};`s zoNF`fVV7L%MX3^}g_$Rn6$_d#T9tamO=&c3*qguO$-x$um>YAEe6;4jnsV1056^3M z8GJG0Nl=6#yJG=yk&fsJRsbXs&k%W9R&tgG7z^C~z1oHfv{7M!UK(V*mI7M8(+}bq zAEfT+2kr9jK4D*|K~dbDqZ=mBK4|busltO{Q?*j~|>5sFKx^2H!!nkzr5!jf@#`caOWrFQ8;L1wZl@qnoxr^z1yRv@h#O zS8T}2IqPatRalyhOPu#9Sv;Ta^1N)669i4PRxoZV{38OqGxP+xisCYxg~pR`hv9X5lGCK0tqWJU}y^bL)Hy$KNrxCjEP6p`KoDaTEwcy%pW*+ ze@ysPf9-)yWgCifo4wTxI~*2ktdxGpB=59% z1>@#ggucY!M1%{R4~==wR~}RxSykjhE6Flbyhuzu;yF?nEjSpnDyH0~cbdO-*d3)> z2bZFxhaoD}7ZTsP$DBVSTPUtsBwg7NqDn$JP)@|$Fi43jbj#^A`t`0}UfQPA zTMfOIz4tS%&EB@?MFnLUbWE2!_0iaxDoGu@ekK1!wdMjwu(fOHl5n`2$Tz*hFcswUWQ3e@%_nq1hTRjs}TK-i095I9TV2pGWpxDYQ>0FgQ?G zQ_>Ta>0j*2%LHMeyAb;x;yh0Ci{m;_H;Fj8p$ z7v)upf|RXj!D?N>zD@gPCHoe1?^JII+IrIcXHuQUfUD*e*d|Y2Aza|R%8eSC*8RcA z^{Mf?-=rzcK0^%_y6##kn`4KG?ZtaN&3Br<6Vorw?qHkS9aHwYY#T&VH+z(voL(g7Q*t1C|a8@(&TF`Xz;LK?f#!6j=4|5jYsSH;yUgfVXf|?3^QMaMRQ*EpXFQ) zuec_c%+X}i$fDY1=Sn^Z#dSrHV17tmIcWUQk*pdJvz{7ob*O1t<+H0J%H&(F4yE&M zpS$arAc(|IB0dtsqp_0G@n5Rrzs)R!?zq<4%84AZ#>&{n-%DQ~9fzCISVw0JIA&2f zJ;p~fEm#qiBfW`AuY-&nmVI1kSQm2oxG%stn3(`PG$9ulqnE)#u<#!N;RnZ*PIXJz z;vvu6JVbgYDtP$(C8tw{Z26uTu>)+gnIc*5`%l#CtQPAkJ@5D`*ud1a_L#SzwhD)A zWBUG@IXihHHNf5M*Auv;cX9IyrcF(RP#$Yu${&6Fp$CX(c8+g5g6owRzm%y!ve zW&QYGpEdQDBdyF8(kUEi7B zZF%0!usxy?)zZ`+RxshCE~R!vwqS4qTHloZXE$ndMhXS8$^XtuuF zsvZ-DlLJ{Fuvdv-b3uza;Z?Z^q!zeWz&8-oKm0uL1|1`Sa~jq8<+OE@IRWliyuLSX zUlT7-W8Y#8k(l6~31PDkM6Y@K(ovhT(AHhCK{%{5yu;T?f>0roQYG7RI&Qvf1_AZoApgSDU+i+MDP;cNdQa(ZOp90L0(-WFm4*MU

!bTM_v)VfLYV${ zU83DcgT~Ddeij~;37-*Rn4Z=l2i*GyHWg`cfx)R<$T({{>fa;V{sfA|Vv1by$0@mc zk~snHSe`LZp3rx~?Q7x%p7nVIw{SR>3mIn(;~4<)SzUsf1@GXc(a0`c1xsY+zFrEPrN~+IPfuW5T>e)|Nw7F60 z2d*h@^(zH&gJrCDxslN!ZeDWV<@orP1x+uiD@VU)#lm*LcSNq>79OW^hsVnS#6O$^ uW4_Dr$%#72oB((51Lq3^ZV~xTxbfuj7jlY&Z{%C7Ap-;iSI6LzEB^ua!|s>> literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410177 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410177 new file mode 100644 index 0000000000000000000000000000000000000000..794bec0f8a84eb857c9895cbb0ef3dcedf87a379 GIT binary patch literal 7168 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410178 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410178 new file mode 100644 index 0000000000000000000000000000000000000000..16fc67540660f379295b74e16b08ac0cafbb002f GIT binary patch literal 5828 zcmd5=3piA1A3x*T)GTr-8$OrSx)jn(vtnfL$x&;RoK|K9&O^PYjA z*&*iATIhzv3i62pXY8o$J(hOTkCGBcIAl9-kOy&78p`5lI z4~s@?8V(TDM!k$f*WD2DZW(I!oT8(rGj;zv3)A~8elY>rgPIlJT;B*X zIVV^CKARCDfH_^YM)rQZx5$?o-`m?~UDq$Ysk8Bjo{T}bPFz9Nv0*Z7VwB-Ash2KG zY+%>rdj8Vpml^7;>Npdsa5ZX~x}rd1hX*m=tGw@@H*${1KI!Y!?xJ`!bA}lTO7$y> z?l2vpbHZ|8Ia{fEbklm7EgM?mR<3J)=R{4vBK>U#Rkg&;fBNZ3-(oHHM6uoLu0Wa{Y@nq>7GD^F`PdJ2 zA%PRJ6$YNZ?nCi5w{(q^15&7df?bn_ck`9%B%?`j;#~#j71fd`lecu(PJQ_&LNngw z`@FB!(@OMs;k;#ExH@Hh#N;SMpP6RZI0P&7YG(A^R0&JE zG7!w%C~aLCwU$k8)Py(1v`5swr*IY%1TC~zu$?gc5dijbg^|m1Kd*Od@sQEceY|t! z+wI+L#Uqmmv_k*NA;zxNkoYzun1dkL6xlQ20}s3mMUtgP{Y7;(?H)W|UCn@ON&|a1 z*{3R6?HeM3l^M|pE`h1xK*SUFMdHGH8+ftDqv4=mhNM(mV?XP{YSJ^qpM?l!H*?On zclD8bQwNh~$o7HtH1i_dpa@wpKW+A!lYGQdLZQEOoZbhHS8}6FHe?X%7uqA z`tmZjXkPg-Syha@$ad=VZ1FtP^^0ZSWs?|24nfl|YKsq0q-o zo`{ThY>MvATYr>!HqvENf#V4n)690;zydb@!)`k4`@NWVM8VW??WhCGcCwjzZ+u)O^vJg~V^f;OfE__s z3o|Dm^}$@qZ|=-$bC=d~gnA)*e>foPxn86D1eWze@==xqqkzqo65!Goj za57GE%Gh>uq3_J#cMbi6vjUu1g?&l_Jh*uc)I%xJ?qz z1fEJx&C-kbboiNYt_wjw!dAOgE#^eDm-VG^>#-xyet=2Xe{$uSng%^Lr@N;04w-7= z#__AHGE!pmc5G9>t#EK^6PgowV#6|42Z9$~uQol=e+{((ya=K-BdP^CLi}Gtcpo$t zGGWPZIs$APjIH^L596EN2h5E-&l4ZifYq^-c|N8N9@sbqPUMye>cI~|WP$Sd!8t+* zz6SR2&(_Y9AE&^HTsZt<>es+TYoou12M#j49O4o*srL54tIQF3SVMO z06WH4Fjmpu1lz~(1%8kJiakU)!96DsiWupV<}90%jlTDu7zSUGgItr|rAc3|D3KxF z3AjyJE06LxUyn|q*D%hxJ<7*9YL$BI3fK=hyII0e`1j1}=W!8WHy7l{d+zyFFoL^%1dM~!Smt5tu_b{*9v+9?iQ?tl6AryYZ? z+a3;B}X^ z^D%Ys(1KIoM2tnG9_|9E4Xn`KSm>TyWbHipaSEKsh4ZKGNq#VCW&~r;VxGJ>1x{Qv SCN77-lkfl57I9Bvi2e`CUuHu9 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410179 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410179 new file mode 100644 index 0000000000000000000000000000000000000000..91f91e0338c6685da2475667ff4ab8c8cb9852d8 GIT binary patch literal 4932 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 XEZ9}p^C~P%Xkst8?Tp=Cct8OF7+J`O literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410181 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410181 new file mode 100644 index 0000000000000000000000000000000000000000..e9e3eb84c82432be9791b07ce75fe406b4ded425 GIT binary patch literal 2840 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410187 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410187 new file mode 100644 index 0000000000000000000000000000000000000000..f2df1e2779f1af5b9c617cb3a645837e8a9062ad GIT binary patch literal 5164 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

B2P zU%_|5guTnd|5tf!R$ual0M`FG(;ti6(B?FH&~*C)Y$vMg*NRsmx{!5ma2Wy zBqm?K+RbiqM|KzU!&3*Nn>-$#cTibe(fo;N%Dt!FUsvXG|kG6o|AR9Guds@=lG1jAM*XL?*9Gvsgz*7%}Jh_ zGTjkR&zsjY`D(j1Y_Mhnn#FSa_lnBhpTxY+{q2~1*!kBB8NR)>i>p66N6TFD{Crw` z2iQhX+(H0Kyh6o5;lKKxeq+L|e4 z!hFvuVJ>Gs3D!9_sfXk)-UQXosPi8PfE;*z3grHSO7h}h!_s&=R2>5`b?)M{NI_`5 zo(9y%3DXOrk=&2OgsZ?6reOPlX;2iZ5+z>{=ce$dGJw?Bv@tp!qyRbCF zh?HJoVGe2wg8>n34+i%2=M$jqj}D+FR;X4m1+xUmL1MyH;7X%l`+@$`f~rIbbK=}| z?hlRKgf)#061PzjUZ6H4HR2F!8jW1?UrKV~rS<9ATjY;%m1Q-W?lal;^3k>jH%r{Y z7dDweBhLI3Slx#(7|Oz8KGw8D1A8d(2Zpne z1dy0;@8ZhK=;;8Y7o*KXqTey>M-o6{lHoR_wgNG3lDbqP18wVK*o!29#3b2GC~Y31 R`zuKPfZGUUU~>pe9stQedNu$6 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410189 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410189 new file mode 100644 index 0000000000000000000000000000000000000000..d5fcee359b5f559a9376510bf95e124f2b049447 GIT binary patch literal 7128 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 DG+}yq literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410192 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410192 new file mode 100644 index 0000000000000000000000000000000000000000..e6d5af2aa516f3fbc0eb79c1764e0deebd533c86 GIT binary patch literal 4708 zcmZQzfB@x=A9EY0-MOf6=GD>8w^{*FfoIn|dt2A7VLMslSh<2MP?d11)SBy-@otmX zg_ft^4nEE#<;tt;`ovr>Bl&xzNC)@I&nX>S_@7VRz_(dF)#|K`z3$|zH%qxP@60TH zQhIZL+3K%7U#Xw;pqzuTl8^WwX_XtqTi_tP*Ey#GalS%C+8m|0-#R`oq;m zY)PrN@6O>q!f|fR)-#JHgap2QvRg{VPnhFPu+lO0s7ve&qAe46A8gyaJO$)p=Huu7 zJrDu0Am9{8G=;&(+W|!9F1XOr@J-@`ZDB>;`?*2u3>_5f7h6>_Km2L8eT%{cq3Jxc z`5$iWmRDbB@^hX3sf1@+%Gy`{*;4-hoQ3ZEf}2U^;XN8AI=w$XF{J$an;I0;z$JTn zPGXC)!|ZFtH#7LlT^?RfeYQ%eYvI&4O(jMmfhQ-co-)3w72c)f@t8gO@ivH?7#Ik} z6;K@u5Ho?)pK5yS>#8y7A1CkguS;gNU(Y@3WzfYU`_fn{YVwzwBhC!`P7c5@=m#nV z#~DZu5FoP|x{k2FX5P17pW*a-YgXOD;_9!QMtNm=YiA^`|Gj5Y1uIAc)2|N=AQ}i5 z!R7+vwnjZZ=xdbC*Ap$<;}iWXH|z*sxnPNPwKT7*+m(y)*0cSa7nyxlxaXhe*tMi& z-k+O`F1%X3RY;*pOZND$?O$i{0nGyYVSW0N19Kmm*4o;>k7CRH_{e9Q(XN{F3!m)p z)D3Yc^6w(X5B+x)`YiO?<@a7yYhA37Z2y3xuY0|s@->Cu!6DrSx?umxo=pXN0OSX- zxj;WyE4;Y+IaB@7#fwLu&MW&inR%gaq_@GV%A|{n_s`!@T))3sou^n@?!IT!n!4Ze z+?o!JQf|{0s5PoBPfjXK+r|v@LyCXkQ>OAbzGKy%N;~dd^-P%`aQMETMTpfti7B7= zHTyC!wk-f!yOn|Idnm|$Acg~AzKR8MSnl{ai${u~4mjMdN?|-jycCiu*hrE^=G2B#XY!HRUSbC-mpZrm0;w+kd?&jjwzz zn<96lSWdBRjiL)%kc8adW43=l=?t763|~7k&)A@NW*UoX_A8ZdDV*HSN}WDRd7gPE zPnu2;HUz3mVGs~I&A_113uL2(<>_pw7-vCoft9hbiJ2u(04fHji3mTYfY6`-A6Kwm zFhNAPFfgrK>HyWj2z9H&C-I%`^^F3Zz1A)I2^$jT$1p4V9Q?qO#F3({y@^9Ib6V5E zIrHX}v{&07S|n9`&OT{lMsHryY2m$l-3r#!>azjOV-Y)C82In?eHkmmH=@s;WYsL< zyU4ZwWr4QSkFQ!A*3MCZ+L`hn2!L!@m@opl|DbZ9uw{mp1z;f}!kK}6{n-o9uPd8gU2?6QuS4IQm|D{!G4fU%gnVV#m>$erfw={?~j{{FpIv#ukm;Yv;av42@%K zY4jIV4i@I1`U(t)$fsf&GdY2E3Ak(x0czrfY6VkB2@{D4S3y=9CC*J8t7z;dtZ8(R zxD6$ZlISK-{YZ^C#F|ETZ#D_~ba7h7)jc<-%O)7A{8|}%WX8rn*)x_#Pwl>>@D7?X zz^z3f8&{e^DMtymUl_-wnVv^xD65XV=vI!RN81^CwATddH6DZxn z!-?p2Hj*B=jX(xAhfFft@0UnJ wM-o6{lHoR_v`mbfq%M`nK+9qbdyxc?m?XOiB~KFFe@F5M+(sY+n?qpo0Fx44m;e9( literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410193 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410193 new file mode 100644 index 0000000000000000000000000000000000000000..02cbfec4d72a3ca47c808158688620d1d24dc42f GIT binary patch literal 4884 zcmZQzfPhB~3OP5<#k_rEW44Nied)Vtk*_|i?^;^D=UZ0&+NTQJfU1O*JATYCln2B{21XFQLglQ)p1GxLzM8Y5qt`64KD_T$?f=SS^A^Yf_-berR^^(2CzBQa+MWNFka(#`f|AK-}Bq9%r`5KD%i$c zw*J`$i63P$`mtwiawQbAn9o!$+ z|F|u*b$?E(QGpR>@NUvM+YJiJH4M5p)XCx(=Ne^Y~E8n|Rn z&q-`ic9?yw_+|!wxy!@rsn1p^buFCwrm4h8B=F>9)lG($@YmRsX#R# zePF!=?Sra&X?wZ+O@OUtIA>il;|_aG+hdcTSuI_2e1^c2^Y@o!+yI)#Dd`$rZEsN_)5d%GaA5#o>0U#Nu^%hJc0fI<1+DuYCroV>Hx-DQ5)x517Vw{EL>3 z64bl!mt*RO#?QT5R-N>?aVYyzrDk1RPuR2f&q8X~C?uykvHaL`E2^dWd9L@Ay2C+d zRCd{}xl(`aU_Cd`KyX+ZzIJ4uu|e_7G#1tDS1R99IJupbI(?M#Jo8STG@T%92vnEC zARu;{fkC4W2o9J2y9#|4dhPOiud1~!)=0L0z|q&eUQzj) z!tdaaZUbFL27V_8U_P4wR1fnj$Sufh27@0xn*NKzGz&Na##r#LVr`!E!u!)d*_4Biy)9l!e0pql(S%`H z9hc6Lo2x`Wh3s*nzj=Gqqe!x5W%k#a=49*#I zkM^#e`5LHB+@tITOgR(SEkJ+s%PD^MZOq!rcqPi@WM;$`Pt9W6qg*LFL(V%Uonf{p z&HlJWU+QI}_^sUfcH{E~E(e45rkoGEQ}DVw@TVff3=XIp9F7Pd-YB?o%AQNE%Dsm_ zvi>yC4SDP{DPybjLo-h8`iKv{2d+;kdnmM)XM;p=NBWd@lifY9IeymuRAXJ+qZZW3 z26Pw;w~nlGyZ`qa)sI=z<{ZEC_3o|+$?8PAf5*Se@IUyZ-w1Vl%6}jLvSDGz2;}~P z%7Nm58Jb4GLPV4!Vj43!RsoFzm0h7gO}tREz!c09AP0#FR{@T5I1gku2td*(sN8dg z$}xiK2Pm6xS<1iwa+4d4-2`$wEW8Gr+b9VyQ2j%VI0T0YQdJ2bF|5`SO>3z7g56CRMb@-lil0O#1P}tG~vzv%KnR^*#7r}ZQsL<+}@goLET|Nn>4@M)o6Nw3vg|rpm zJb1kUu|H{1&@reSO8O_#O?5PO6V|*xNZdwAc!AnuC;%zqkeF~K>(b?ffh zHcXi$3RJ%VnCd}oUKD`EK`4GF!hAIg*;S;NkCKmxZf}y1kN-W0oh`pL#cHnCw`0-z zPD{e}6|jlr&Az{L-M>hNUE0^60f4Rj_YW!uiW8K&m59C;1N-{3Ine%AFHk=#)GRQC w6wXLYxC&fp032Vq+J8j4iG#*&!kUii^_i&=hv@Y_Jkmi703C{k(f|Me literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410194 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410194 new file mode 100644 index 0000000000000000000000000000000000000000..3c3da42cdbad01c35ded372655189f13a703056b GIT binary patch literal 6092 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 p57=U~Wtmu$L$ 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 ZrqMy-Hk33|KIlOz3eBqgF{!c&*V7Bv5jpnpiH` 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 J)HN{A0RWfCZ?XUY literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410200 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410200 new file mode 100644 index 0000000000000000000000000000000000000000..336b92a9f09d896655f160ee3817e49c934a69a6 GIT binary patch literal 6368 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 C`QUQ^ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410201 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410201 new file mode 100644 index 0000000000000000000000000000000000000000..f1aba9d150fa5471099a95a73b784417402a7f64 GIT binary patch literal 4928 zcmZQzfPhm{pD*ei-0iYs&#^_(R&yJts)X;Hary7xxznb+5_@uVGfkZo7ZyU zeL{W93Rd2wn)`om<4Y}BVSbU}VThiXX!UdT4?+g(pENCH5N(;s`(WGV@fRU@y!hWa+inKQ=hF;>RLGUO;d@HNZ`rIs;7*vYK3n%p^}qJ6-)eN5b8R4hnqz0U_?I{AJAZHF&Cq)~Lp0W0-o=}N-^l?O1~Y(4 z!EpxC0|dxyhNH(Hu$|#%TYH3MqVBINyu2;!evQTa5L9Yk;D_=x{G{~M!1yLZt z2sRfOw>tOuCn{?mTed59eT`zU-`u4>>;2f}Rpu_|>^rDmI@fVa@rku7bTcpRVQKF2 zb11mIWYd}G-PbS6DW;zkYW-`%4m1nwhZJX{FG<;XKRG8kl+Nkx`fjS1K1*#{xT<6A z_Uwql9llUMOoyolIRXe!{E+;5TKZN)_Dy^zP1TZ^-|Gi)$oLy39|)>WoMaFsXZsVR z0oe~=bAf(1J@ICTw?x|+^$E^lDFs3*VmG(TSY}`KIv`?i#@k_19=)yS?ihFaRY!hjPfWP;6&?>=(_V*|Pn>+$d4+I7$b{RT!dyyAk{8Vp%yGK< zU`dZ1OX!rE8AQ5O{L_{<4f4K=n+HA-<78Kn4t?pSqS@F^jLXck8cwy~$A=Zl_8t zUYBPGSQxLO<%>qTeZS}9~z=uDPceec$%>pG>se{Wj8dXBP19be82b{?R4;CL<2nAjQ{ z{D0X3Np@$pFQuy17ZhW7607*>h*GKM zSFcooYCr%~9)bWY%pp7o3Gy#9v@8LO5G;Eb7{oowUN9i*K{gPJC5)i*8m5Lg^PkTl z!F-S(VE&+`J(TzZBUq3GkeIMgg5)(g4-`ir01Zd9IvXShibHb!j$uEN01^}I0uTWT zYh-}iZ6vx$Yh@EG-ZAV&5B;`qhbty1UYOSo70Hy&@eTv~MBmpEQ+$Xrg z4%EH?wGXqQDpAruac-*HLSr|9+z!Gh>3@*8jgs&}j}zobB_$5QWdfn{g2eEmo!gM| z88L3+xg!ZnU$k-)N_jzadlbnZ(7Xetuti+0WtpGWmlUlZKgIs%2QR;S)NSFHZ>!Fp zoOg61`{CD`p!^I1DgS{0NW+Za1#l0 zi^PPPj}&W(Emd&x@(Dqa857~7kjKvlx0q&{ENJGk3r z$DU)0qOImOPE`rtIpgx*zjLQec_sGb=;kLITFh=g5Bi$7r`zU`F^IOz;(f4f^YRpsi7Pn?wxz6n<)1C(|IbV!t&H8o zy64*GBcJ4UGTd_&zxcPYb@5mGvx>_uKb6NJ ztI=)FwSoL;j-BD+U*53q{JoJkL+|Mf(O7eN7jFiBCkJ2}oB>l0(gOr2@n9oeS$XPN>3R2c(<~;Z2|3UHt%-tYb!qHee z(X7DJ-QU39#W<`g+taMLvOL|^7UV`C1_3ag{CZmYRzvnpd?!uSl9=D?2XV;w8zvtJ zs!p6_5G7~(6QUHM9;6R!FVK(E8o2MQ)Vg1h_}OFeq~lS`KR;DE_E&NJrAIf?-KM|) zCL+PkzGfjqm-WhXhjdTxUYPkL%)#q|)g7sewTmB^TP*^`6EpulL)AVur%a)o%@^-5 zy-I4--@j+d0&mx|xlgQWzeR_FZ3e{?1i;)5<$@_tI57j`aVAWVV7xOhh-!{(Gl)-=n`Sx0(AZ6|bOj2p!R9tf!V8>N zkm3ZM$bn*{#38o26jYv55?-`(8;NnqbLKWIebLHIpnL#NS45YcNP2L@AzYY4aMtwh z8}{G6`HQrQO6JYW-SsEKf%%G+z5I1=@r|+bzzQ+T3*7P~s1aU_lZM-o6{f?WV2h<6)OS|-v>`=RlUVK0&Z5|d;%fx-!1Hxg6dL(@83 z58Or|1B*jY(g=~}gY*zr zYF_rGWFm8~``cRc^^Yk)(&Tr{!_0lf>xy4p~u)uzv>*TVuRKc0Gr$X0t z)%BES-u(-*DQVHEYKV;tj3D}IQRdC0fbX8U?6V)eFcmU;|Nq71IeKjSS8sHZ7ypwk z161Ph+^a|ZS39ei{Es9~dHtIrVjuST=N?Snb!%&>I+SNuG2r2@N7$2`^rCC%Kx9U(4Aj!Gs!%>N5e#?_va^ulz)FygJK%EWKYjY zY*BWYeXaOr27kHB!|SQfRw;EYocgAz#7HFYEQ~#x^cv;9uR1SsaYk1@Lr77`Wjndtwy+0tU>cveEZh-! zWcA*nl8aoWTisv#VZEOICc?Q zU;eX}vO?Y9Q1;}5!d8#{I>yJ7tb)G1o$N1ia_x+0$@E2`FTE}@-ZAVy#`*s3CzGA3 zlL9l+PFg;{$~pJi=aT<(Gq!ElT=ZlI571%Y@H?F~`OQPO9oEmX?&=1IX6XH@i~U{o zUFSkm>DApw@4eZ_Fk@N+Fb&NBs)vUW%u~SpFcHdzg)0Y$@5Lg&EkKz6@3Id?`Dd=0 zp8NeNAWHbd#FYuFbWY8znWzZU12!KRCOb`{#m~-RZpm7cSQ@V4rg&$gee;)S=}l*u zjOtEVeDX}uUv1*jBksLst=XEVrb`=sM{~=ttXd>3k+}JJ$eu`cpm|`w96kPk?F={D z+9NCzb$?yy<@z*ty-LQCg=$L=dOgrz`63dW7DQWS0@cIZ3!)`F3{&!*%6#1|UERyl z4JsV{vPw!k(rsAPOcycv7n;`LdSH4%G!}=Tq!A*` z2k9Z!e1^UgcLOhY9-Q)Hy{S^;j9oJXE;2=~Tg0Zw;O>27#jVH!u=_CcJh&VN%87fV zFFgQOgUB7l|=I~f(1zci3txz zV$(86FGk)UByNNIfe812@??D{z7>yJTNKBY4uKERHf6}6$(@>QtVMmmkc=$AE>?W*v zevr70lJElcM^FG##33=^O5k}6D#8q=v9}RGa-e($FY5{RYu4SF)f!?Y3e*S8XN(h| ocEBkt4npxe5#~4B+zlede0ci@Wdwg3PC literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410204 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410204 new file mode 100644 index 0000000000000000000000000000000000000000..ab8afac47c7c054944d0e0e87476ea1533ab23f6 GIT binary patch literal 3992 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410206 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410206 new file mode 100644 index 0000000000000000000000000000000000000000..cd1d9c6e8b2e8843de4af72d922d8ab60559cbf6 GIT binary patch literal 4884 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 E0EyglGynhq literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410208 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410208 new file mode 100644 index 0000000000000000000000000000000000000000..cf0ee7f8acfd158854afbad539346e177386db76 GIT binary patch literal 4992 zcmZQzfPjz3cHBt*bWQR0Gy%O&kvw+}99XkV{rHY~zZpgBj$OLC9H>e-uIu+ojXj(1 zWrc2B5uy6>t^eA#$G%7YY(KWM)}q_rc3J82me;8o&1|_{r~b^kGxv-2;?;_ES*^B8 zb@S$2dUkRX$fl%4r=~${WMBl*TVn+_?XK1EyL!ONed)uk6HfAnH;1~*90{o2*Tizk zrW>flVW%vY`{GG#ddCw*cCHYJh>n``C6R5q>c6Ova$f!T??naX(}-i2|PJj^_1~dt?(`-kH_rMkGDbG#K1r( zu7K)TfS3uS-t6vEOP{-ymltd*IIVHum`usN-XA&ohs+#YCNqRxyz!2K-^l?O2J?VQ z!EpwqCDKgt3%bmTv1ImiBD*Xd(ZPC;*v~4-QVC6);KYgq@YAUOGTK@K5 zs=b!2%H?=rFTeXYj=wA?%twH`{Lx+pDN#)vGZie&A2PvK!d@4Oz{tV%2Xc5 zcdXh|X~(^*o+G}pnXs~UfNzRe-mJ<8O~Xk z%(%l|)ArcpXI4ws9G@ZZUzw-4aM{&5F zDzSK7o*`giyiRN8;%lFQ>LiM4Zo!l@g8c^!OGD`mLD9V8tQk_XpWhPs;dZ+ExqfHj z1sBPXS06(Ebl%jE~!UG0uYG z0xM%<6LV9b08|W46A^w)0ii(wKCWQBV1kHnVPN|8p#iFc5$aY4SH-z?xAs+btv%wq zOCW?Jxp?;ayrq9u_^N2AUYz!U=bA=x=KTuaySpnoCLG=MOt*Tso=?C_kyhzP99zHV zFi&Czngsy>e%-$UE3d=YeReSVZQ-&q)*H7FTNnRgfC6V$GSUt=gP#R3# zRy1h3c5g-drtRkThOc!`RchSdH%p3T_TKrzMxSGDM}ic|o=t^mVFa5Cj9aIRbG&&9 zb*HWnzp=2f?*r@V1&>oy&5yDz_-lE?Bw^mQHM}$L{%&2kquN@2{r!I3$#!d>X78{$ z#xDK->FrfZia})oi~Y&wo^_hff>pdidDir7d~dUGOYEfJ*Z3osiBW4e^H03`K z0NJp3Wdw46LFGXHVg{BK^PzkOBH~d@Vf}^4hyfr<~B;g3sinmBM!k~g4A9B8q2^io#ViJ zhRqp*tI}mE<)h4XnYGHVZZ>>3ZSM`mrDq&ZtN|+ml|K*wOFl3fBnk_2P<;jl1nWPb zDsY~I)PYc42nyM7s3=PK5odnN9B3VjFc@wah7i`YLj!v#@drk*APFEb;eG@82^qlh z@&b?`QaS+X1(jEza%KUL1_KiPj$uEN01^{sI;4Du^AP3C0+1Mr+emlQY-qe=*o!29 z#Du$o*ziIrkBMoELDM?C42RnYWMFX!N*W>3e2^Yu&1bN-KN7GY{#g7Xo7(;B<03x9 zH#3^Fbn9^)iw;jWudloSbsx5R=^s=MmgnJi648cXU|)Z-1=^084m5`qY8IG63I`-6 zTm`PU0o#wOULw*>3N&^T$nCK3S^)FWAaNT?S|ia-ptdMA;t(7rNO>L{M2l`Po4xp( zu)0!3D9%0ql{6E_?iOap>g5X=nEqyMD${|6BYM3A6$VqVFo&m6g6&BL263&GO|W_c zBOM|MATi-ql9fh@bdv>@%QW> literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410209 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410209 new file mode 100644 index 0000000000000000000000000000000000000000..57eaacdd4c78bf1c68ef5120bbebc6d10b1d7621 GIT binary patch literal 5012 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 TbQ5U&ff{iL4ioe=3JWIy?z*6E literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410211 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410211 new file mode 100644 index 0000000000000000000000000000000000000000..622e4eacc073d5a0f97e702bd5ce4d347752bf26 GIT binary patch literal 11720 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 ArT_o{ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410213 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410213 new file mode 100644 index 0000000000000000000000000000000000000000..0339effab32dad40113e28dab617ebad5c7946cc GIT binary patch literal 6184 zcmd5=3p7<}8{WrV7s@feGk=UymXZ+>3d=zWiQ{&ZOcNam;pG z$!KyiGX}qd(@4bsPZvZc#L%eZ@_*m9_nwVfj^9|TZ>_!dyTAQ@@B6&Z`+o2K&OwmL z5gsqZ8SrO*EA=95^{;JR^`54Y>xJBmhcc)eAC|IL*#IqSSyBE5TdiLjT0D7bJY6o+ z>P%zolN3|kyxy_j-%_p^{L+%4-2E_kMa>18J$IP8HT>t0p^`|NN9^4|hX<`pLFmaP zn|ou@BSL}qCO?l>7seYiFVVF2?oCo!{S)1d>8c{-g*S}kWkwIk0u}nZ?#w>tCFSdw z4UvN{rERr&OC$WQpLS-8%XuNSpC>5QzYvoS8}U`}$@}*oih&Y|?CP))!KAe8!%s7R z&|GXFbcp_n@m(>;!l=T}zU`fu+r>3fg&SNiH|7c8h4&bsbq78*4ytC7FW zQQFu*uvzs%;N}}qh~M(+*AB>M5qV3WmE8J{1rZBY-*+rkgVN;S!dVt#v4alp{q?tW z4WnfZV+tFYDJ7yVb-#t|`B*Wrr9A(+<%+9VL^P5M;Jo0X<=G2|#^^0Js12actjYzCP1#(SNBr zMDK2~67`a%v~Y*J>B}H-MAC>3ILOBJ!5!v~wtc&+jkUKHW1GE$Yml~h!?!xa7Uf#rIh?fnvj{mJGA>{W6|sTV1du7HUyyst z;?#GEj`GS{tg#i;)E^HHSX`4By%{YL%FZj{6dC8J81^UJ4)stC&8)R8zn|(n^p(3_ zq@nrlt7A8XHAJFBfh{z~6_?LR$DEVwYhKfTDuk`Pc*jqrF-3AJEII!{iCrb9#IP81 zpl=cu+Ez;|TQ7hAgFEaUe5^u(Z8g2TcR3i&jB#bN=EhJx8uNsRolm%xjKi0~n;*Hg zMkhC~Ju&vY+G_-}32%gb)Gvt9Y@MATZTPBIPq5tj2ey})8-8;hj@+Xcsh*S;vgh&X z*IU1CYZLMASRt9bY5b)1LC1%cZO8pmUR4#p`Z=KAYO5HqiN;tSeb%$Z#MQ=Jpvf!h zW^gBUz@#-)UB%kdOgh*p+OHN#PCN232w_f56YD zu4xnR{UF1UIuKW|?}`dHB{Xgoz6P}qV(D+SHr9wvPtnWLVC$rPqv)Y^KDI+OqW7|L zXT$dy-pe_H<%bQp`p3flbaTx)I-aB~Az`k?qf_D@pE$lKVYMW%hsF|<*kh+17ysD! zGIgKbh5wpS7b)cK&rV-{$f)UAZj?nOf+#IRsFv;sg?SdNnI20WHYeum@4H1^Q*9GT z2i5&uzcHwnp})>SJZwLu)L@-{ReaZ$ysOp8%bwqVn(y8MaTwa|+zlHaJ`Ql3_a9A_ zI1+n%#78yWQOtxJpwkoNzD!5snsQ1`^AOad`Tp}Hrcar~iO?15sGIwo*FBC4JlApR zkWYJ=&K^7VdBuU2$2_$$X~Er}%bJw_>Xh7bc1Ojqj{8q0cWP)AEe2@^GjjG8T@>+< z0QOK$)O!pClZ#f>B?>8ezFafR5?^er@QsCnx0!cM&87=fb)d&Wmi?m^LDpsfc^W6R zFeW%B4mr`#m?VXyhw8JetUCpQ_DWP8GgByi(qzWUWK=x0)Un%gk9KkB-szn{Uw~n6 z!?4%}By8}*i}0UqxH<*HN{=bADOg>9UCAkVHeO`nl-w5(>{8xs z(pzyn`zz1Tb6t6|1tNR>JpmwLozdE5k;Xh7iRxQ}rF|8Pgj$1nI$-1awaUDg^~jJOS{MiTi__zPT8d?B9^NC{yEu;yONU#QD zA`g5|Pp=2KRtQ|M-w^v}j`5qf;!f%wyK53`*SmgJW;-MMK09_SZ= zYX-&{6Htf54o4T#cr{as}?OE=-j;(ptxIUV5RgCRLG{ zfW`Kq?*@LBgM2|AQZQX)_A?72BfIlJFLrKWgLO;rweU{jfg}GcEh3TIt7BM7}*-xU^z=_{N;)49;e@*R5)A$}ANR>pSjywFl@_t-BU9kQBRPm!1m>L@Tck*q$!%*1br>{X&Au6bZIZ%jIe~8rRhN3{=$3cfF-oUq63f!RQCWq88&QkgnMwV{R^1L?0|wESWxNlzZC;)NaxwGq^HDnw zfLIW43M87s;N$H8qTiq0Z0*=C`7U-@TC`Tuhky7LQeCYgu#Xqf2q{`|y{^6zhIP)q}t?CCj) zEy@nFuNB|S;4gQ1cs=#mDy6Q4Q{Oa|7>NX)oUD4v_^MWTmy*Y0_UOmkAZ}t{AQV?X zbu2*41XABM?RALx#L0J^R|qGBOt|eS%%!v>dC?rf9H*-fmbCdCWnfSjU=a4pU|@{tVO@3yz zbj|S@0#DB0UzTwLsE)}o#5Xbsqyq}lPhCr{n8jDxyY*MT-sC6_w^Jn+ugfz8ER5G_ z&0Ku#Gf0_4fm8!jIU~fK3=S+xDLF-v6&lZ?(ihez_Npvl{dDuIy?vs6u~Wu}{`p=) z{V{j_-+Wp+=V^<^HS-e>#p~<^thr}Ri{B)6aDMUwE}(hfFvp|%J(vU2P+$Lh$9hpgcAF5so2qv<3$$voY)mVEo4p}e zH^N>2TRBjjxJTIwm~tktTY!1w?f*9^ve|25eE9vh8Q2_NzGA*xcwn6t)5e7@0kvmX zO@$Aq@3Z`B%=^Nu@sZ5>&exS{Q#mJ0h}_%|T^Xs>*v+anHbgBNQBk1h2%H+i-+r0T3%1P z**@c%M*ZI>$8&#y6hZ+kZWzJ#0>kS5%;hht3txU>*V)4Q>BOcL4R>6Y|2!|$X1C|+ zOXURF2`y=t@_xEqx%lAt%cB~7doG@N*(RQ|TyImU!^=BW?aiz}bHRQzeC^0QV}s(E zX)LPQuT;LJaB@2reF=rl@ee^Ax4?0M(JZGC=K7uc3P&rW8G6T!prBFTt5#h|hzW%rl zG_2eS^c_6z%0BS$5eA@_>V+7R$P&ScnI(CA_ZUVU-7G8tR zZIpx;D8Eo64#8o9)aC$3-|_zBQgvgE6z9Foid(uid8B?;<=W;~S8@1Vt_<6rclFRX z#+F85m*HAZ0vS~rVMb4 z6v)PvW`N~r?lo8{BG{$@DztWI7K65HCc*Tv*AVnM!6RreTIfI@?L3%;;B&ePSwc)^kU>kSc?eAT} zk3@lrHvp6D1gJJRg~dTAeka2G2aa=okzzimo`=_)M7O<>^uQetWMFYW!;GW{+2-t* zJG#P7P0?-&oL{@eblbIcz3 z7T7u@0VF2WCD67loCnXxkoFj^_7IV7GN-Ydu;$}I;x?4DOro36^CfaxBP9;8)dzU_RV#00DlK AF8}}l literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410215 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410215 new file mode 100644 index 0000000000000000000000000000000000000000..3db4b13f69ab8ee575f74e961bb887aba6e4a61f GIT binary patch literal 6116 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 Cx}jzO literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410216 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410216 new file mode 100644 index 0000000000000000000000000000000000000000..e699e0ff178dc0172181ab326db2972a97af16a7 GIT binary patch literal 6016 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 K8?iYONfH21GI31+ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410217 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410217 new file mode 100644 index 0000000000000000000000000000000000000000..a0e424ac4da1a452667dd2405b8e009489105094 GIT binary patch literal 3984 zcmZQzfPlbxj!XXB_o`h}5YlmT%bcrk=5lY(DdK;zYRTirYXkDCfU1P68M~Q`S2?`A zl2GapULX2VId$3xr@85ekFX`=UKiaxX9Dy8E&jRp0i{w;q|(SmNv7~ z(k?Gt|9}%@Q_`YS$00T{FoNi-MVU8~0=|3Zvd@0>!c@rY{r?x2=jgHRU%k;uUi?qG z3{Z(f)(wwCfo^>QA0BG`RGj87yLDz%mX_YnL)k7DPTBrX=I^PJ+cHbL%jtu+OXdAJ z<@P(w7G>MxzIwTa3VU75{3h4us!t=g8zkRddThd@HUHg$W4|$owyflRux<156p)LVkDrg) zKL^BufKwpR6b2t}2N3=K>}G4nb{WU6)tAF&Zeve6bEhgHe($kG2c`uy&8l2tFr8;M z|HG}_^6Cpsey-C$mGEp!S^LUATgv~Rv(TMia5Kp~yhp=Cr}yV4hLnGQQ-fj}xMWYy zNo-Men0>AIW(I$`%fsub&sHgQEu8wMsl-Sm@Z@CGQ^r@d!n>3_9B9Ai zo2AcItooxE}L3nV{+)k7Twa}StX&M4a;q_!o7Gv$|ZZCT00 ztIIrMPk;0Kzh%X$cuS3n7?2{_v#C(642)oNfqoEfE;3%HUZvsC{D0a;lc%go?^R;^ zLU&0tHu5u6$UJd)>0I|qVfF3HKe8(J@Hf7@BFL~|JKqJH4@WbOc7J)U266+~O@^-> znP+TJJTr|&HT#vyw-iopXQfUbr998PlP66l2pa;`r7#GHon~OrSO{dJxIJmn>32{u z&Vu3sD`OzA1PVaK;B<M7IyJ?Tk+t$^twrfi0$b+{gwUJ#ANAt-5tNb^B@h&7*q z+q+=d#K;M+I1c!{@-RPEA0*J*xc71KO|zn{#XRfyRiW-f&-2L6hUFEwodoL#1_p7h zl})fbiQ#P|0VF2eN?hp$Y(Fq9AB3tz2|FU)WI^Thg6<}e+hO4a&tHSYZIpx;dYmAq zWm4i0942Vx5vXm2QXY{QUbJ%?Qh7v-n|SWX!qOM5+ypPvL3x1aHWrdUK>A<+Tf|wv z?fd!i)ZRUvuNPEZ`y#-_m3%Ayg(2gYwhvCnwWL#TL-nTo2Ld1)W&|UU`wuDyOaJil zf{3;l1N-`84AA!20-!mdwkRCHECC82G2znaVGT-8pm0LZn<(j@I5!=gLt{6A+ztyb ZczH2M+(t=wEd=_D8gYnTUVvg24ghU=mQerz literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410218 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410218 new file mode 100644 index 0000000000000000000000000000000000000000..9ace28860431465090334bf3ee74f58bdb37ebc9 GIT binary patch literal 4336 zcmZQzfB@l=&L#i3Ik&Bs zkjwVmm^>9^Q_`YSXCO8*FoNi-MVU8~0=|3Zvd@0>!c@rY{r?x2=jgHRU%k;uUi?qG z3{Z)K-de+XZsI4@I`^8Z%`iW|C2yaL_NhOSUz_;uPktKq?owCXnng*T>=u*m|LXhq z^7Y)-37J{O`-+{9O5J%A8@Ggs*Ej57*KxDdUtHPWHT9kgU+E3&x^Kj15;s#=O~OyM z;?to6M{AgV{Ac?(b=K*)HX)|(Z!av++UyzZvG}m%xyTO;qAjaJ!O1VE4)j|<1u^m<82T(F)$E{ zE1)`NAZ7xoZ>p`Y>5)`hGDlLG^{;%cDM z$v_N_Hz17;K;mFIf!d4NIu^zrO?r)T-&dU%xj3UMpCP0u4o3ZNZW3yX&alobX+miQW@`_ZfGwZP8>2002M*P1ExUPXt>N;< z-M3RiuQ5X1;9wck+I~>w@*mCwTc)&glT5<4ez|M-+S4xmriU1lQ-BCpY-0QoZ8^(7 zixPXQBbS}relUFQtO}P^@=Lk4tYqTVWgfAozxn;&vSL-drA7r(T11!+wig&yMjwrA6|(}{Cwh?L2a*Z_c+r zy{p+_etdFOz~WYz^W<{^QDtVIjf5hn{qd?cc&zK`{YS5C!Q5VG8fgTor)nDEN&S*^ zQt0>V4GX8=k;$L+(AqCVhtV)=$61Zou#43N>w)&6q!F-Nfc{Q?E4jHPBBgDU@+wZ1 z?UCoJZ|an)$xhWZpT!-fzs}yxcmB&KZam#T-x(Y||CJ*l#q~PVo|SJunVe|f-*MyM zWIk9Ld3^1OU|~U_9^b=~K_oRYTx?DWjjbD5Jm zgS>PX@~yn1xxnX}`8jnNZ-x?9p!=AANo@Z0^Zpt8SKD8#t}xoxZ?iIpe}_@+v+ON9 z-aYQft^%rI0EZu#0gpon4?=>{88d@u?nRg&!T10wwA$$81`>_-wnVv^xD(%rNl8t)kP zA_*WdNp=$`oL0hg5z~%=rggX;m|hT##UUtZgh=y2dWbcj!DRaup4WGO74f-xp(hq>?W*b%OG(ZCE-Pnx*J{|(V%Wc@jDUbH_IQ4BD?M;x?hM~-oY{# Kkj7F@z-0lx(Grya literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410219 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410219 new file mode 100644 index 0000000000000000000000000000000000000000..19b9595b10257c2e37cfcb635b88026e7239a66a GIT binary patch literal 2920 zcmZQzfPi@$tU3Hof9caa;5M=H_U<`$>h>uHLe;e~4-Q4Vn{QhRR3$82(z)b6H|Mrh zwJN8blqc=h+q!TEt2EPJH~SvjxrPsfuauQYtd-ttdUsVy^Ze@6-Hb1%uQ;`{a=YC& zqqVPlW?u%`l(gv7MTm_Ij39bztiYz-wHkg`4_LV`eYkbPN&fKWPX$gwYO|5ZL+Sa`dNS^kf8ZpNALf{>YaBxWa9Yw63D zUQ=K1m$KC5vf$mAOYJN>Y*`cjWS7csI#_9Na_g7>G2t_&+(lry$@x$|A(}z7Wi{`EZJU>;fLzRc{QPpR zJ0KPWoC1laF!*>ofav#UH(NWl%Q$wez8p4l8++23J5>qsdyg$TFfE{IR^<|d={&Rf zA8zfIS6^uIbDjRFglAjI+E@PBQvUy(h3@=}$n0Gx*D09$rs)|;oPI|pf7U~5zYrZp!>k==HDbdqRu`;i;CFHWhQTVJdT^Wp zX%IkWGu$$J+#PDXcEZGCitDq_rs>}NGdn3@x9Lr>)^kn%zbx878f4F=f+!GR1e*(t z+uR+Nr}xZUw(8!Ew5y9hTtB?QQKhqZ$!=FBF1!5nlJXnZH=WxQaLW32-fF8O&-GP8 z=JhD~?%grVba`JI&(V9I*??w&{K4@3-pps`-_)kaI4zR-q;ccYn)N!__F+9m0Y>W4 z9Y*VBf&IX(*a%cQ8HiEb4$=<=AaSsqK<&kB9SdWRCcQ?v@2k#>T%6IB&k#}+a@kHU zw=L|!9GHfjS{C6Y(?k1mtO}#0yUmWtI86Gq)04||>X(uY3Nu^yf$GFP%3i>fGlAU# z^tbf44z^|1Q(RvB{&dkumcQVh?9tZ0ou+XM$}dh|B#||v<6+Z9*>5&w+N$r(mqg5! zD9xGT%uh(xRY)P&r0W-i5M>bJG>&RWx=JEZ+gcYtXrklJElM zD-?hfaY#(K5^%nS^Wbp^2@|w>0u;ZX@B)=VU_dZ`uDjhVe=tfEsCWY~OHY7mgHu=> zgyMH1%+J2=RYZ#U!1RfTZ=%Z$Bt3A)0~uJ{&mfr0SAFwid}VXWwUyCTm7!%`u}+E) zjb}ZmF7f(eJ68>?pQwC{2xo$I1FU>QiW>s%L06Fdnn2$Ao(ZC)`{DBdD zNCHSqxKGK+EAVoUM89L$k0gM^B*Se;X_*)|Y0Ywg#XE+*NCHSqlHG)oCy8#uAo&As LBang3AuxFWuc_>B literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410220 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410220 new file mode 100644 index 0000000000000000000000000000000000000000..1d2bdc322dcc7cbc66ba61ea06b83efa983a1056 GIT binary patch literal 2900 zcmZQzfPkW!XIGRXymkcHKWRFuWqLyR=K2Z$U+NzH;OcBt^6gR%P?hk!4b~j~r@!=R z9&npjd3*O9J9YaM1EK2Lm-e^pi)bK1{O$DIDGdBZO=b#lNFw<@+* z4x8K;r%nUel(gv7HHeK2j39bztiYz-wHkg`4_LV`eYkbPN&fKWPd^#Es=H z1$wHLt19J=H7sY+s{XX`K`KLa$O7}FG3ok?C6}z-WcEPUdQB4By|*d`dWGK~h~1lY z?Rik=Rp-YOUL>Sz>2#cF6U{%=6foPk`h7%Uz2HZ8ej5hSmNmQ&wryUX0&+3)@$*Of zV?ZnjI0X_-Ves*G0MYNyZnk!8mvQV`eK~CAHuj`5cd8QN_a0kxU|K-atjZ+@(|Km| zKit|aufEXa=Q{mU3D35awXgiMrTqUn3*GqzH_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ4fsxFc69> zpgJZfX2_{!5neJqv@gf1Fj~6X?3j$hq)$6NxlE^iDcPVfvxT35-^l?O2CISU!Epwp zK>(S}P;y+%YjJ7C3iITnYL`XX`b2wH?B{ta?Y;JX$}8txk%k}*vS(946bLYa%>~Bo zPmVMCx0XFOdEO__n3Y%q+HqDRHtb?` z!Fs44R>9PR^Z)^hAL4H9Pg&eGm8a(QosQKWEw`5i&snh1S-HnpnU�T3i4~1F|2$ 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410224 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410224 new file mode 100644 index 0000000000000000000000000000000000000000..72fd61b53c6fd43ec19896a51de4066567bd089e GIT binary patch literal 6136 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410226 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410226 new file mode 100644 index 0000000000000000000000000000000000000000..b9d6071107a8c88b6d2ee8f322e292fbf45128dd GIT binary patch literal 5920 zcmd5=3pA8zAAiS?r5KfnNThFLrDQ~C7nL=JNfMn+#h8dyTd^+nkxEV2-PV%pl-!nf zq>NjfsN9X@ERxPwZXrxkES44D`@H6T_jNl)t#kG{XXg38&-=gp{*6Fv@M{ajX>s4qdl=7;eVdU=PJg>5;6TY$E$6Woo6I7EeF-*4Y;UgA5GF@=J3ic< z?BIN%wwZE3;u+T!S{2BVXvxV;VF1L$@|qrMDxf9@7pyWM3#t*A`>HbyjF}3?!R$KQ z=p0e{;}>Tfjuez7JdCiq5aXM)C30CTsWm%9>GOErlm z({=5B*htp+aikw&jDI+4Z66@6SjBP-TyGTH;PLzz$$MApv!k8AsRYMgdvn2atMuN= zunk=8r|NdWDrT=l55CHo!~{X}fkUyElCw>_#u46&sIWXu6k3o*Wn%GH?llt1)WWBB6#ov4gKV#3{ z4k7Qul4WNpD)*Y6QW$Yo#mzK58~wu7;RA&iEn$8ktNqqi7P}Ey2jRt52W-EqoRK4ZML`4mmuP68G zw&d2mZyh*$TG6?2LVWqi^(!PE?M$Nvrc3iN5kDSH5Wemmb>45RcBt8j`8yHreWkXeEUZ&MIjuZOs=cQ9*JF*aP9b9(3e6fO&>=`H z30j*_b^<>qMyKv>x9MPZ?DZe28&T^G8DM#wN=z(uNUb1*$NjWQDbR21(&K>M|N428 zMTT~j#5HjgA7$VVR@L30ry!7Jmcet(#Tw@t;@c+%H04`}x-- z|1+vMBta!q5LV}I|S`}MpgHI*I!T(KII zH!=u_Uf74UqXFiBji;K&%XPkVrLT=%)pGaFr88?9n`9|b{dWf1nI_e1g{ohK#Af!^ zo^_-*TfQNNRRoqC>1#0~_luv+t6s>hk29*W#EI_8zh#>lD?v)^~g6fAZQgnXqq2>)& zwls?`*NKd_V~2+gc6Jeib{4+ePlt^8_!txLz(FLykE8r|Xs>|185#4Q?+_%jG13Rd zh@A0YGn3$pA`>nSI1=NSD>sOJxR%jYFxO`nyLEdSuPa=7NRQ)@_vC7`uTx8kEqb|{elB4JgNr#< zqx0T3U?Jq3oxF44bs_XDg*hdDg6U^~{RzW_H!_RV2i;TCWtmhn&O$p!j8!#-{SM}7o0TrCTNN&VIZbT6J^7q@^T z7mR62PYV1y59bGF5hi8>(p&n=2c;~|l}lxV63-5a9p0Nsf66F!eWA;v@xm%n;QIqI zX1mLc^WTBogFP0je*yl(f<*6>)bszxN`&RX?_q445k@huC<{ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410227 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410227 new file mode 100644 index 0000000000000000000000000000000000000000..8a5b0da8c9b76b221c9811060e3d2546327dcda8 GIT binary patch literal 6140 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410229 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410229 new file mode 100644 index 0000000000000000000000000000000000000000..a46124cc12bf45b481a7e657105b98c3c00a5b74 GIT binary patch literal 4728 zcmZQzfPl<^RcBmo{c+2%bB&mC*M3!VS3g(Q%7158^j@F5;_jIupekX}sPtPWf40>7 zy^ybCb5myjc6F9>#XCi&5B0fx>ca|xp*z3eW|Db$kA{g(@6S&RDgXYa2E{aR$)28* z*rMz(`&#kM4E}PLhu2e|ty1b*IQ30aiIGU)$;qmxjIU~icPV*1W{-Zn4dNyS27+5mn_WhB<|q&aOt8l|JU#g~=Hi`GK~!XzCbhWBhW%)~#;YTK39fBWv*_^`+-; z%uoFE)_>2H*Y`A)UV+qycpCyKBHRLWa|rhnuv#F&2zDPZjp%xOO>$`5YwNMM(&?^q z?y?OFR2Y-rKYelf&v&<#O!?o~)^9oc<+6(U?xq*7t-`f^k1qPxIs00OU*Dal4(?NL z^8w8QhlSy5N9GwD6wgd!QO$m(@-2mv+gYj8M=8%U@8n6-3Bn*prZ5PIon~OrSPrru zh~Xe<(HR{e2PDQ>P+VYTY;0m?36X`V1Jfz~flryrB1kSiVEMUp_SCZf@gmsgn5rht419d#uqRD#3SZa*W%>su@*R*fSC|nFp3{0VX zkP@)km$sM7-vrodhI7^>Gw!h0v^_TYnbp!Y$7cvUIe&jy#too4CdUxp$RLmoC?FJD8&vm4;&`1BR*!GU(j94zk6vO>%>JW1%{vg zTCc0|S9&9KTjQ}u9yH%C(v7{8hSwryjcR{0NFymVXutw|tYj^vL0{gL|Za zz!{rnoA4U(r7VWq8Lghk^O`^{1($k2HY`qIG)Nj04$RQ90mLU=tOu+S zN+25!7Xp^QFf|0rDxgB3`7$J!5Ap-dAGEZG5$Gmz&Bq89BmpEQER-PW6V3z05ePu- zN2`Nje%}IA0XB=um#7pnVWG(ZXIxJ%Qa`2JutBv;EYXy}ho5 z++Vy#*k9Ct_ry61_sH8;E!E}n_&pEmzLft!faGRIAom|s4msSwLPXRZ4D9O?V-gVc`YO1B1kEl!O~@KmrOV0#2h z8vO;8gM~S`?F|wmT#qw|%lM|nKI*jg|ToNCZ72cnGck4w+)sz>4Ulsf1-CO(| z-+t7C#y5H!0-j)i3|Jc72oogOu3}&i*IL;$2UvH2+Ey6p5J>=u2{RpM-3YcnY0;_o zP?acQPNbVGXzV7eX>^ddjgs&}FE@}Qm6SL{Pop5cpn4M2Mg_}(;+DklqMh4FbQ8}V dSy=j_m7CygL{K;p-8Vt<2S^_bV2e1I90271gLnV{ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410230 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410230 new file mode 100644 index 0000000000000000000000000000000000000000..4a9b313035e3a49eac9079881c1fedeac6128241 GIT binary patch literal 4808 zcmZQzfPl#F*VU5}q~3G=>1DZfm(lXVSzYm4zv8*vY%}_k0^XhjsuIo&Sartb)*rVF zJJ*OQckNd-clC2+t^9XpMep^=EAE~tdi7oT$n+zj-g*SY=0W0^V54TP@$sgVv>MnC6pn6{u%O#s` zpc02fhVCL}Inyl`$0~xkoJD;EByQbV``0QK4t>8A_?G+57E!%h>Y}>p%1>|DpBbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}*}54TrNX%BV_yHXNnvv>Bc_Ts~ahZy*s9Drf45vCrb z2MECN2VpWWSk}zYOTRRE;j&z%NqJAS^f@KVnWyi4l(+lXw2lZ7_EL~K*|VuI<&0o+ zfpNRK_p0K@*#X9$-*weDi>`fc8k~G7%%iY=X%`bw+LsSsNSdUa$?g-i&J7u z?!7BgN>iCaLtek#;PKzIga>FK*bnalm#n_RxJ755xnAX(9>vD_^`_UJZU1MnG2MjY zx!#3ls2{ch)g!wbnawbAx!XUB_DwDQ_xEQao1NA_iQ8s3 zN%EpOf;moCA1rC}Im*DGF2Eq{m%+doHW6e$$Pq9A%vZ`lHb{)4pt!)w(8RzL$_FU{ zt9@yEx%^Flt!6l9T{7bidrjM8lb=~FU2}Yfz?1X$mu1`ls$+5t@r?`u>41XtQ`eF! zX7QEwZvB<7H#v&K?No`y>+%c%3*&WKGZ$a`3{oahAk_d>&IoZQgM&eT`bEFX6ZP2q zqi61&bT_T#kd5b?EsTGbPTbd&dtu7^44LD)t^HbW%g*R&KehdH@ZKsOW4*Nd;mbaE zdFfeAhR3VnYe(i88x+q>V^Ph1rSdI>liOLT(?==KGw`vTQ71%w6#__%`gf(c^6g()ctrhyUaR)_N1T@N(gC_8?5-;>wUt`PTrMQwYc z$0Ju8%g?a~3|4k=&%HBWMU_R*O^ty)HuSgaHN);di&rXc-%}-cQ83^c8_+JxCx&NSYp!CKJ%+sL! z00u;aGXwkj!!FRUUJlg63e^gxV3q(mNKCj2aQcSxKz4%w)P7*vg_*?&Dr;fth;!4S z2Q+pQ$nCK38f|{bU?qZ&_cFZvp=T1- z7`V;m=aZ}(`Cp%LiL+^D$;!9oXeQi)nu9Hk{({QE!W^DP373xy;xfLeKcHpg1fV&f zas&>LJcY!BOOusGiFDHx8oLQ=8XY8VLrJ3~x(QUzQ6mnqrqSLMmHgSawSUiho}@l! zPiV{C1*Z)&!(y&J{80Jr+VgD@(3Amg^#R$q(hR5^-2_WT1lv*!4B}cVo8|!ZY{X_J zk|jt?nCUq4AlUw-MW?<(RicDBk#4e}v74}_(Lv%iO2P}h+(1q!q{Jb58U^VE)svv~ z1+RBV3@_TbjYK!`+>wQ)FIu??)SiHc6Vd$?V{vNp$}j$sceVfedU8fyn~^u%%Vx literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410231 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410231 new file mode 100644 index 0000000000000000000000000000000000000000..ea5db50f215e33d5ecb6bdad13cb505218a1b1ca GIT binary patch literal 4876 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

n) z1u+QfXJB9hsSois0x4&h*xP6C+B%UnqI27&)$>`Nox8ODncI!ztbDn!GPakkjC3pZzP->&^4=QQ(!iH8!BXNm|H z1}V-wDSz&FzgoY+qfJj7RUd_ah?|@DyI<}G7tkzlIu?5gcR@yK%Y^2OXa9Mk&$i|zW|+Wk7`2J>0*-z?fESPvS>bDVp6 z2&e`K7kA2s-kSZ=%$;-{C{=WX#`U&j3Gb8?flU{H`;vJ}+0s=4^mS#ZZ5DdV= z9G*srD90G2E*0;9mW3ewFmu6XB5{zIFzX;?51dD28YR+AAvAUq)-*au+=h}yNpusa zOhbt)Bn}c29&4a*Kn9R95n84orBRSxWI3=|M7RxJFAz~j!rTF?E0Ns`DtqB&2@&>! z(lsm%(Jo9Va}!F~BY75y2@4XOc@3j1aTl$baCRE|x1GV)IS%#S(b%Tau=vKLXFsZL zT))+J{tc+E1_AVP1ldlIyFlR$YA=8R5p_BP)2|N=$a-LUK{T@AP;rzrPNexVB$$si zf78GoO8kKlEJy-KOn6A5q!*wla(+bbo5SL82Q2uh+=hcVYU83kdPl)bWcR|t1T02n zdm(K{?DjHnZaTQ0U%4ir@%x>n%s+Y3e;IF=*dljE?eH}}&SLJQWQd~~QlRZWn41}q z+JDI523Af)AB%x~{UHfxKWi0G6D!mnFa@&&$U$PlRp5$KP`rWSR1B&T7N%e>ac(-e hg~o0Ixg8c>@UnT3xQ&wV0`--t5r-IMGc+#26aZ4)>!|<$ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410234 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410234 new file mode 100644 index 0000000000000000000000000000000000000000..f72536d0d6d2a3a5ad34db123fc395c917ee2a4a GIT binary patch literal 5436 zcmZQzfPj>3*I(?or5KyW5v%mEd3)IHKV`T1Klti)-G9`pcVY2cpeo^-1+_b5ySHc< zR10vV9ox@(h#}zO7m54Z%Vei+RBGL;w*1KR-0yq(tj`_$y2$Lyv=_V?b<&yF-5-ed zUdX<5kevf$Q_`Z-3LqK?7(v9=Sb3?9`f%%nll`ZknvGIVThBGT z5_3Z?a{bE9|2Chk{Gs*tuI5EvW`^wQlQO|;+BfW1;pW=Od3tBTqO(D(+^RJK&xPGl zP;q;&v_wFeXwow@)VGZnU9}; z6}bb%f`C&X(G&(BZwCrW^Q9oI&-HgA%5?%MF*w@G|j49VlbU& zHvhw|-SX-SO@6M^Kb7!oOIiEMKU>QGpR>@NUvM+YJiJH4M5p)XCx(=Ne^Y~E8n|Rn z&q-`ic9?yw_+|!wxy!@rsn1p^buFCwrm4h8B=F>9)lr5*RKdZx?| zIDFsFBE)K+#FWqbntg%lnF2zC0(@M-dcj2cscXp zh4DJAnTxM|1}S28w>|(?473mGR)+-T;OP_hMu&aNRTlUy|GlDP*Pa}v8E4tz9&b_> z{bwEGB{xI6ig}oJ)xO>$vtS|*1HY341Gi!$Q2k^eMhO=n9|l0;U^#)>i`hCB z#vV<2jdI^tofo+{qbr{wq$uRFom_5P*n>GR4d3Sfkw0x_yWO4nf8nXzj%AD-dTQPqi`8li+AT$jl9QG z&TtuT(>f~mX_Mu~lXiEL^FBuBhNm6dHs^tgp6-h$3|g}{I_>$y4RwPCz`jn^=BLE%R_6uzi6!0u3#@J z^05fd*5Cs=3><#7F@CvW>sGgHEqi6Lk+pb|`qJ|^<|lr7>%Zs9>wB6?ufX9a+Oi3# zo*CvDAPo;Mg8CU4*g)z-yp6!xt=&J1&JDHMtX9;m)cVN4c{|IW=QFH(*gt$cX1;X7 z8cU!ej3{K6AawH9d-r^XpBo zJ=^}zVq>}q$8)_4%~1bt1**sDUxNDK{sm=81_mvKIgj7`RS_{;6Jgz@Tk$pIh2p`+ zBYTS~C$~@YHhomS@ioV#A3PmIGp?C^s?+l!}?*Ibx>X5mkj&Bm4~ zO!{)+HdDBROXhB!68Tg*bA6j!-7_QAgQ^dn&YmE6|B@&d&|H?F%MXj&xkT`JxI z)B_4bkbaoCU?Y$?NKBaNkT`<#U~vGoA6PF5K;;-gjV1g&iW$zzbX1e--fTms7yqVpEaQLsFQ>|Rj1CO4jF7bcXs z36%ce0LimROt>_PTA(O$Tp+gr8Tc5LRdQ9b|DALTzHpoIj@TNB?>ApAyt}K7RrI56 zNS+4NMD%h5ZV`|H%@?_sVS)tfZJ+VS~eJ~oyLr6@REUq*Nu|H{1&@re= zlzc&?oBC+%CQw*G!wZo<2Z`G#2`^Cl3k4uW91;_*gxK^6(hEwvp!~cMrkh|pZr!c# zIp1H20@ZH-7H<<^`d~B`2ch_#2=gDPG%}H5J_B-jM0EQSsl0%>9YkYsKf|$08;gt1 zH8gG4_IZ>2{u6U|qk$KT$+O?BA+I;yliqy`sz2pF5Fi=B2;}~Q%E8j*PAH$4ehCBn z`h#1beUsHdeXKCOAR5V?NKCj2TxkGoKd>&5hN?t~8zTBjAU6fj*i9g}!@>(*&kPc` zQ4(ID{v|cy5WPJBj>?mg{8sC}rff{M-V?dU`ScS;Mb{vov?J|XH#~T@sxmSS8sF%3 z4YoGx7N}YVBKjz>HY=!~1Bx@0w1~t(V!}+vRoAqD?FXh&m_n2=C(=!zHY+XNgf)#0 z61PzjUbJhUp|+F2>4AvwBBlQeOD_c5tVGxgO4qP7M7uCSs)NXI6DSSB0a6A-V#1|S M)B;6uwpn310JgjT%>V!Z literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410235 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410235 new file mode 100644 index 0000000000000000000000000000000000000000..33a57241a9793962ca7efccf50622e371abd054f GIT binary patch literal 5156 zcmd5=2~ura%)S5byZ`0){kQwwK@i?3ociLe+S#Cw z9aD0Ks4Jqw+NECkC=ArgGq()&?`qq-3vi8Rs{_3=4f3Z*6ShuLqB6Kn4X{w-P+C0sU zTG(MG`Y?M-+GVyVM~oTAvFo|2uDbck%!4moY}eUwPmr{#Wge9c7~Z{hDVn?H;>J9P23u3xS!Wm-Ay>|h{bsgb8v zcPhy?|F>%9l4*$2ov1Sg{hf^XEit; z&6D)1xfkr=UR04%5=-CB4mzP1Hzq-$E%(5b>BkMn{n zG~dcfql4q;0Tu=_R=FBMbg}`B#tq8B;gwQj4Q_Bv+Hb3$v4Mesp&oqji4Wyx*skmn z30y5-@du4u@Zc7WkwpKe#fD;UTs-mci{enILxk>NN4Hvr%A%KbRgK=;*zy69x$igY z#;lTBUgb}D9OyHaVsK{W(bUElkV!;Yz650k_Aso};^eSk)8&O_!9I>})`l9J6b?!@ z1j~l_TXd&otWo;QWzoWj)05Q$YIAHd3|33OJvX20oaa*=|ejQ*74hu<%6`%&8zZIDl0Y@?%55Kg_AJVOGNFum?Hax;su$$Ee&wX}`|h058;b9TbX4z` zx;%O2T$&nEUYhhOebW1vLw@@VZGyXNH8!3IySTQKQF1&9@oaOhaTfnjnpvt{BncFv zd+^hV{?qOEq4Z=`$t4mm1GG)*HC)3(TNT}YiLdFmDQ5vbkiU+V2%>Qe&;r;&tuDrZ z;p@;hBnM00s+4w5!7f#GkKFNUV?*RwR#e5s6wO8YE!GitB8%=Vm;<HZUJi zD}BA$lPgxWew0m-ajm=Qlj*+TXo;-$k_#LOUCmf-x!FEfYjTtQ!Dk0_iaVcD_1i0M z!{Gv2c65?z$T>$I~~F5BYY^oGYywC>mGP?ps(9yU%%oWB`^^ zu#Si46dHrC$|Hyb^ohMCEL#NWAuSW%AARUU&K;H!cgM-sI~x-8O{6uIvK%ZM$^y9k zfJMcE)r0ba3kk&l@-K9pcwcDp$&AEVtRt6Vx5>_rF`a?vk@Oa|MunO+@272KI<{=f z?Mle5j0j+FF*<*1MUV#F>Yb*S9Vdp>@T7rdJ`7XG`f=zd8-%#Bo`8$BhV* zd=4~5{23kt%s!}xsn|0Td?(<1LiA67sibd|G2uA|{q?h9`yPK~13&+W7@|HQoIlX; z4WwQ%bs&w=VwO+beL;7&Wk~Lz>R$iVQe91Q(Z4D-NnbMz;{_C5ZB zy;>kvgb9Ju%2oV=)6Y;kn9(xsfZ!s7a}i<4|H z#^GmUxi6fJVeO+sh;J+vi}gX^oPd7sB?2J<{}Gx5fy9)5-d}jKyxs%;2dL`BI0f=w fNKE#ljOmO0;wQoOJ^q5<>^~xg!}|q4=#cmiV{D=7 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410236 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410236 new file mode 100644 index 0000000000000000000000000000000000000000..fd88249d7b337d3596e968320c699174dd905710 GIT binary patch literal 5044 zcmZQzfB@m++vooba=M?w{qKskwc*USzm<+$W^vGa;FtM)=K24cKvlxK6+ZuvSzr0@ zvHZsW4(=1%A8~xm;Qw@iuc+|<+lGg24Z+Jlnj5vTeSc8ezWv@T$*bpji^R_d-{8G- zDE9jC>90dTHYF`Otqrk}fe}PsEy}!^6!6_Mmwoo57p6jH@BhEJJV%dh|LToS^5TEe zWq?W?797i&)GGVIlEeLU(v}H6j}|2==5iYT2rZa=xI<=I_-gft`gDQX;9WMCQvaLe zIXyKnd3ZHwgM8D9>$~%|{�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 UNO_eEH=)>##6e<`>?XJd04GlWh5!Hn literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410238 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410238 new file mode 100644 index 0000000000000000000000000000000000000000..93c5b7b42fe4fd34580b7b8a0883d2cc61ab6d95 GIT binary patch literal 5196 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 CdX4n} literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410242 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410242 new file mode 100644 index 0000000000000000000000000000000000000000..340c16c7baa03be9be2cb5f14df96e86946a5d58 GIT binary patch literal 6556 zcmd5=3piC-8{X#}IiZm;xlDt^_`{&wa+eg7T%r#3Hzqj|WiYNMCJj2>h{7m3$|X&I z7ue0; zwXW0Eo$*8W`db4(t6eQusahc44ruyV%@JU+ZDE~ zsfT{nN2-;p(j*zF9{Wc0-}fV{b~01UbDhsEGjKZLwAz^Zn$3J;nW1g!ZY|MMmV5(R zVrE^p;aS8eu)hFCy%T>5OhyCF?WxFVNpxj<&rOAd%wICwfRsren* z8LXy5w2djnj>m7EFwM|bF>3!qDEyUghG+Keyaacan56izoQ?k9YoC+|JoMd#Yss8E zbvM1hv%(%9&yG`2eDwuKc3zd(Rch-#2RW5Q$%5Sn9$vWVf7^EVfCjzeh`Z|H8tYEg zP#FgGKacC0LzZlaulJP7(K<7B7L#IM{-~{_@U#a+OslDvHBf{jUaR#6y$5DzYXSRb zzZIG+-zT>`Cbz*QwS>;R@gRJ&+okHP$|H`^$HKCg9G%0N+g=>IU{R*taQ@=j&0R&I z`_7COvBqy2YNmgCmuay6psJom-0%y`@R6 zlwvZId!vJO<~dzUQmNIwqrN^yY1Je8SC2|~m@sTIUjaE9xTsLxV~x{eDr=K?^>;Q( z=UeXBc%(c2(&}KyFfNz1k|M`2c62i%HnGJ zL@Qw8Jq#P$R;cqtt9ln5EQq+Yh>~ujbq(guUK$=FV?Pu& zUvW7tw0A(#O2b%XTs$fcn`>!-i7m_p@=4(0;}YBxZGtO_h+94yhPtbwwW1UTbL!^Z zN{R8u7=eria|PeME^TRqf|B^zmqW2!x09T?sSgwb!uGIBWeirD$GRv9Uk@nre*)xW zR)uyG;#7nMytj*rv~N+|>kZ0LW0~18zZCqO+h6K181Q@x)zqb=s*Vv+$q9Ao6y`pS z*UJ5DomIK`o`>eW){5&~)ISMsSwA`p$6#SHtoiu9^t0DS{ceqVE%vISYLACq+5I)l zdRx_?uezOo;sncmJ*aYUuU~o3@SER7DWz^{BU|;@Eu|kX3~O952LMC-Da%-hG)0@s z*{v(G^OGp4zcwePZ|vUvJryfbY2AmzWe|T->@pw^;}5r>bSFM;lBc7mG*QjSy3u9`Xt5VApLoN zwZW;wc}7SG=%9B4{`hj#D|yImwL)6a9g(FfmYNj{(;rsmsi@zQ*uK!#&y3j@Z|T53 za9&qe?NCc=))OOMcrb6zL#?OsD-JGE?B!BIBbb@m#ht0g4_(N82kZb1gpO6r$JG>G4<^RDs z_q|I@Pnvsypvkx9@p%4CB=m@WdNIng&&*oeg|EZMxj>lwO8+;*m>^d;Uw_8v2ZHTg z_<}W*uT9T~JWfb43YUR72=3{Ngn>zGasIuw&!ebPK>i%aIfn^7q9 zO%}Yi3dge|dAbvIAK8m=b?|k`&t(^fJ~iad3QAmbs;)WR{YBrJvj6t9s3|?g?QI49 zw9Ww2!8k+b5%?;^-Y@69mq2G9Ouk&98N?4fYe9X&XJ`phERcr8>in``mZ;~*KGV;tds2h8r)nSKt^j(-9|F{w zsni!LL#z|SxLIXdzao^-1}Ow4{)3{2=ov5mX9SB~XX8I&Awb~$GvgoPQ>bm$&%gA1 z?uNOAjEUe0tt|*9kSC{luANHE5#PaJE`{Iq$PHqgDo*%*mw+DPNPrqXm3&HIBkxsW Sj_~!2*u}pU{+(UIKK6f0_IW7) literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410243 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410243 new file mode 100644 index 0000000000000000000000000000000000000000..d224ba2805e671e678a98469cbd21df0e5216615 GIT binary patch literal 4884 zcmZQzfPe!V_zq3bofr0bf%*0fjnDe%$(a zzexI&m&^f;tt=Hw{0|;I$M&#omB5ywLz1<>3M{&+Ux)r&k<2t{KDVD*QO3o2*BNKr zTED!3?9`f%%nll_%z6-|H5=&X%e&Hxzs7H>YHmfw`Q@e^0~x8P8uDM%sHX z`<0&(D{r3=8L})p^u^yGPH{e=Zt2%+KQc%)oi-1UUpk#ZwB<1GgKe9ar+{3{eEfoh z#Rd=y0#0QB=@bSZZwCrW^Q9oI&-HgA%5?%MF*w@G|j49VlbU& zHvhw|-SX-SO@6M^Kb7!oOIiEMKU>QGpR>@NUvM+YJiJH4M5p)XCx(=Ne^Y~E8n|Rn z&q-`ic9?yw_+|!wxy!@rsn1p^buFCwrm4h8B=F>9)l;B*s}#TwrBvY+_*wk%g%P(<%OePnpW& z_>NV3D($#;)iY&&z~TFT79m#qB&K}c*X#?_!W0l16yW0u(F`KfPhCr{n8jDxyY*MT z-sC6_w^Jn+ugfz8ER5G_&0Ku#Ge`}ip)QC50Y<1>9cGIg{+ux-<=nx;w+h|Egr5Ja z7E+&8y-Tj=jBSMWjdWADeBbKFmnUs6dTPD5uOR)w&G&3?K09{bcG>9rv*NTaC(tZ# zxDa$RW7`6twObjOzUP4K2Vyt?=GAl{hh^`aw{n~Ido6i5k7uU7Ny-=3BWz#pn@)eU zNhhad`$Q8^q=NJj8%AJtFKsWEzX`C_4Cky%X53+~X?tw)GpnU*j?WNya{m6Zj2l4n zm>feq{eplD7$7w)p>g6cS$&>je><<#>(#%1>AsHP{989eDN$$I?!Te24o`$0$tUic zkof-5y8J5cdYdozg>3p0L<$e}-#uV$@;O03=K(X&esEa&?#T#mHg7O0bJ{v{UCWBr zweeNrAsXE3`TOrp^)3hejjR0EuONl;(&IXnZAsvlpvO8!ZmP0=E<4l>4pW-t-n~9nHaFl;sa~qefubBS z`@kg&SPuNS^fKd|Y1m4g@~biOt{cx5%vR)#HsN`mKj+T3)hW|2>qG@uK3(R>19TXR z=RK2%nka^OiDo4%jx3vUihhZAi%9-uJ?VT`^2((d_E5)zOIRQqTiAi(fEij2ft3?1 zw_xD{DZ8LnASf)BFoMc@mjEZ#BfMG`<_lI$jU7=m;X zUB4pv18yUbfz2VGcmHkNueL)%`?`OQ!`2#)t1Z&Z z&7IG3=YaU+${3T{)I_FRN_xuvuTS^I-P&X5@@eU*9lxRRO+?!!_cBy11HrZ?P@&bn ztRoDN_U0s*J{XOZFp-!rSzPrZ#Qvm3LC2sfQNo-^Hzm;6O<2?DAaNTd;RWh5pa7(Z zLt?^}fWiS8z{(l)Gz!uSl0#`b6YP7eyLI9Ymy{?_A8@RLaRRbMAT|~Uq4=E$^EXbO UY($FrDD5Gl`$QzPhhTmL0Qx$T-2eap literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410244 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410244 new file mode 100644 index 0000000000000000000000000000000000000000..9699ceb49e667d0fa7e3b20a74341d4ff5cb04f6 GIT binary patch literal 7236 zcmd5=3piET7eANshzNO1Gp3ZTR2U&D3Ps7A zFdC2IuWqO$c~**l425C#InKS`!S%UDzOVg#>~;28kKbNv?X}lE5Hv9?pGki*OO^C{A}jw6|q2%&W;MDJ|Hi_)`5K;1Vdg!&7=eG0v(#e%%8XdCU0Y#N|A^p3?Z& z4|a3cwhk%>UzG6WD|uPnoZGiTH_gsxRV9DeSiWwuzv<1>j~@_nU`xcr>Rl5SAsjqY zkL+(EnhqJcvd6sb*5KFd9q)b`tHf4(>#p7^;ZbKXz@n2;y1UoG$r&R09L!BCkKw}0 zN<}qhzUo>0@mTY)XxNXopAOw0kj1+;?X~f;HpyMuKu&b-&C6QytdnZYyO?K7|5{#JjQJu(!I+&UmRW4vAK_)-xz;o~8tPQ#WV!FY% zo?2bWW+&FX^4<4KVMSs|lqosdCrRZJcMNaq{g7313HYz+%2oS1?;Qyfnk`m!2G=|nfpSlW86&z^ZZV_r4F=tEU5ePzG3$n87k&U}tOM=EJLoaeeg#gASdCqMTM~+f6Od4PHK(gzM_mL z4H%TO4YX)ndYifSkmrLiyd&U&kqW&4FauE6>bUseq+wh9ppmhcd5au@OV_(QB-M_?#4AX8>m^m!>dvsiGoBjoJ*5jGahO($Uj@ww$L z{VnhV$I3XJdj}{NH(mX?dNBSs>Z8IJ9KywMT2ePD-YsgzhAk1%~Bn|t(jBkIDObb2Gka!wTNDcNf_ zR^u=ENaSSQlSSfd&qP_dH*R@^+Qd!71Jc3$t!eq^m`5!^?1+O>9K{B&r=(g@;(*6p zI(wrm=ys)IJ@vejzFMc9W=s3t15krvVAKcut`jS}T1N-SzTsRz9}@dgiq$IWQ(}7SBvld9wikQl?GXVsTakc@PRUNhf~Z7 z$$3&|OQg1EdMDiUO=s2Edcr*S3}1)S5`rK1*zzx>+&~+$2h|U{te5Xd)I=?kIrv_B zki^NgMKZ`l@>io@D=RfF2&e)+60}UH8iM5S0D4mX5)*6u&^1ms4>v7!bv!{GETC)j znThkyVrpq_YGQ!MBLA2<*U;stQ>X+E#i~>Adhyw$fWSzLYH+3ruqAe-@j5z%Mkx`O9g1Y9!Wj$OBGlA`_9R~vt^qK`8$BViC#*F;myBImvXOwbLEQanfYr8RKvaN#{UfNPnVXMeTz-Oapy~gZmX4A9I+b za~BvBjGgAM?+x30`~}~C-y(*HPcXeP625GF$tY#${9FE%TOL@*7^=X!rP8^Ls%J_EIczXGRc3;>FQ+UxC;&N~F=AB?H(%@2v@IG$S)Wo+;N5`=I9RKrv6sf95b1mn<+Q zhG+jf!8RX%!F}hqh~dYcPTCVD+^Z-BZ!(LZ8RazX=U0YlQXPxPnsCt7cV+9$@1dZ6zn z`J72igayXLFh{==Z1eHg)VhJKhs?(it*w~e29}+6r_Iq{3x^GJ7chg#z;7@I^P35i z!jtZg`S@$noyPQUDa<~wpcnx;UpH``U?`B~0Gax2`62WSk79EKw5hmgu+dO+Sb4uE zDnEO@!1S53ep3WWk#7JPnOSg7%4O#ClW6@0Ry9I1@B_nhya0CQvyW-8F$)-yzTK(k z1U`54e46b{&T;S4>|+)%pl;USW+&Ww@ylHO5nds-E(ChD$l6T97f;4v7hwYkBM(q zLh@m@;JXL%o%|4xr+r67RAO*qNYonojfwt5yTj4*JyV~_B&O&E#>DXKe<#=`)r&b| Oa{WdzWY}lYV*M`yQN#}b literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410245 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410245 new file mode 100644 index 0000000000000000000000000000000000000000..c05484d31ab4add8d50459bd2e8149954cffbf0f GIT binary patch literal 7088 zcmd5>2|QHWAHOrGXrb&8@hC~z3n4<0Z7_IxmN1q!Z?Zq0CB3LAC1ii8(JGV>m90pY z5VA)2S40b?r_l1h_s+fZ?ud`^%IDMhe8xHVoZs^OE$4U6y#RpLbg4d-V@_|Wp2Ux1 zi~4IoK2m*w} z@tZM2MMe|cEZtpy$}3!=s&cs7j80VVlDQ+X$6E8W|3HvM?HBuy+jqUYDeWSM`W{vZ znw8#rHm(Tp{&jD0&yAdmBnUCf{f7dYQqYLjs@=h!1Q;1=LH$r|&L-^$VQtElCac67 z^PL`a2JCbws-Tv}5yF!GI42jsD4DZ2@2KeNvr1!Gvei5La(yE%f6DdyTBouumE7*M z(fXK_l1${-JAm}%vx~WHD94JL6vw}Q(MoRdYIo=K)9r0_=}Q-E?kyJVZ~4dr91QGNw!C68BDG)MEo~4)Jr&)Rd)r zw>0;h3pct40IRtGZWDI^XB7*Q!*PT7pa#ZsKiENY3&>t%HF+gDW#k#`XtVlv z9SgQrd#k+kbLUb~%#uz_YZ-ya1jx%n5po<2C!m!gyfw^7^?m-1PwWcLD~09CI|Yo( ziidkuisZV3lk%y-xxs7W#5*>=4w5R0E#cTjaZ~NMnNcYD_ldg34m_RZCOIU{b_e)f{9WXp5HgcQsNnjkX#_;4sfj)Sp)dt_;Q<4P-| zP(CSJ9iF!?uJ6tzi|tt^c_=C*yRC$DakoE@+l9TGUktRBW$6m_cNaQ(Oq@3?iv4W( zEYE9nSX+JxBo>k>S0AR~K9rf`a&=zWvevjtUiSW^6tNosWu55P3THY0`#W!#2bWM{p*!j`y$T>J%P~;_Q`W8gAAo3x-o~I#!@;gBEBsOV^>ds)OA2~y8(aT#&d`+xl9?PUT|zH)q1XxfKz(=@zIrz(rsfb^}_$9|8L8guXLrBo!AR)`+a^|vYIt+bj@R!^zD zZd+qs6h?NlXFin2u>Xee*LNPZPTo)8y%iWjPJiD^G5K{{{kuc1_a2@Jx6Kqn za-a;QM+~C-Yes%r?ZFWZS^;kD|97 z3h@E&7HuKi3YOt?D&;6{2qX#M;;R7wscdk}kS|Wu2cgIuJ5>J^6_r#JzzJlI-jhttp5y)OmsA8A zh!;F*HXx-E%DPQdh-$?Gu_MI?jX^>@!P1;yqzAFk8Z$KzNQetX$Z-f(EkPV^t82uX zi%-}BjxB6TxwQU30JX{7kr%-7MsHz|2g)r}CMpj*@y!}Y1m`ZDm{ykoR6lr5Ss-gTaE_i$ z%wh5>m}5*3cGO=#8@BK9*W{S|*BHV+nRIuA>J=1UXbsK>CSu8a&4Go3fBvmK*qnp% z`oG3$?tJonn6UQ!s5k**qKi$fmY1=XBt=yk-dI)8X8Tzk(Cy(!Fj|@G=6(soB>+CN_i>?l6X8hIER_@j>$p)<0bxVG@(= z9Akp8qyD1j=TCx-p^nTF6LjwWh#0~?!PF5r=nBlB3q315l$n^Uj#typ1sX&5rJ2q1e+1TBC*Tun7tVX$g*i9krxrwwx zN^MtK{|@8mwFo~Gdvad~s%ysgLXdEifZ8BHdpC?m`OqHYFw78Lo$mfg)9*wze~C~s z-%aU3hX(%#OwNaQ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410246 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410246 new file mode 100644 index 0000000000000000000000000000000000000000..2e785b821951e458e92323ab3f4ca960c8de246b GIT binary patch literal 7160 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410248 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410248 new file mode 100644 index 0000000000000000000000000000000000000000..f074d88542ddb802ddf9d1a82b6523417b262da1 GIT binary patch literal 5928 zcmd5=3piEj8eSW&HNKJCTj3Ugq z49O+u86?%tEh;fQ$St>Fv`0dHSDc@Bdrh|6Shi`~GjOe=P)I zkD1mQJyXqrn@^jM89R?m&0Y=U@Q=}m#$ zewmbs%Idt?f3iINRc%gAwnPydZ0lcDGRiiDWd3eRHxyN2)tl|JPFB_&th1e34?R&+ zYn|905dxf>Je*po1OeTI;o6%?>suZCiaeJAaR?)Vt%-KtzA_DZPh7R_f*+ zLMa8&FIb~Og!APYk_Rnq&(*nKUsuGEc^z(140b{AceRY=i%o4dWnuD{UxyUcfm z^x1lyt=HXJsXGpbD{87-emjQHW~Lo1YyyR3YLkwIe4(G%?AhulN;m5M)uBI&6qXV_ z66B@6!Mr+Bu12Ft`EVG?xR>~CZ!sGaf-F@lAh!zq_@I0xrHwIF`ELi$s1%=yEl0Jt zOKTZ4ymIStoA_j>p2aSM?=IUYY{}T5YTh5Q&a0AFrJ{Br<<;ZhNkvT zz`%Iinf8r&A0PebT85tqspLhI3GF)he&+!lif;MFsPXb(Ga$%EK4?KU-3wLN<9JQ8 zy-&j0)0c}TUAILZS@Vw?FP&k!qnM6Hu5$d%=5Z*+mz%E7mPQB|R(eJ_8$+ut1RqSP+tGBImVpyy0HSIN--2d(ynQi(9Yop5K1G-3uj+f-B5Y-dq^NT!( zHSfas(7bdcT+=CcIdRG`GjNkm?tQOjt3Rp&Jk3L^ZCYL039cbnZ65)7d`|E*(jlDJ z{-mi%!T9=wN__W7np)l1h?aYg@CBPB%U^t4tsr*3NoF9N!62Y^L2Z+Iw^wBfSw!w> zDBtE}qD2T|#5Q<`ju$HPnOv?H_WvxW#FI!dGU_}sAmdXjNb^t>$&$KC889ki3{h*s zc0pTc3?!LA%G!x5uR~O~x*Q8`_RTt>;(hPG623;4OkearW0W9qF@a!zGN5nl2X)Z_ z?l(ag$%$zVyxu1uTTB)goz7r-xYjjk)lZF)kBBoPGVQWdYU2GPKIiD5_CYL#KOqm~ zf;E_?IjQp6w`A%4kr9Y<{9}qNBh4|cod5KUR>Ht0we`tTpM?gfs0XWkx!PtTE<{SRPfc3qyr-!v?Skn;1W^z}#1A+k zghR1Fhl3e~gT(@QYv4Xr(^S>M$)K`ByBg^u(1d)ArnXi&Ids-&do91xe|yp)MWC2^mkWtOoX036i@s-hP!W z>08{#-0vZ~vy@OQHhWvq+)QD#$IYU<3-d{v1q0BWd=S9pB(?_g$`9%f_b@t^&M_h- zZDxN1%X17+62Vl_iQ@!Z;7G0nT1V~`^bH5*AJhnaN&pT4@T$-?@W&%2y%oj;v2*hE zv0?j=e8FD%D{6@51b0S3%NO?i;1|chk;}#e?}vZp ziP!T3PKWCe5Q_o1IAJL%rqN9OE#ZtoU(wAlo4?nprHBssYlL2teQixt!u03&; z$<5?)dK12n&FRf$#=gKmj)5bWjg1=v4|pK{QjUkE&Zi<7Wt*@!#sv&0 zgX=FI?5nrAPMlnZePk^&y!1a_VH%7J5}j4glBDuCzwGbq$@zZjhtp2?hjf`(Z1Z~( zW1|zN7awq#OTL?eUR2Hug1rLYPZxxZW8g@Rb@rK@d-dm84)qVt0hZX>oZRt7fd4UGMxRjQkdpE z!+HL`gNuV(AwVovxGU+S5ej|DAx}g}rQLp814{kG>e4ID>RK6O#wt;$2QdA`-u-5< zH5mJxb2Sg&fe>O|c@*|7r~v50?_k`4!-*rg%ky*1)4Ly!m{eC76U5HR7x!%Tkzkv1 VzgQ+F_&x8hsNwwk1-Iys_#eRif7Sp1 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410249 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410249 new file mode 100644 index 0000000000000000000000000000000000000000..a96bed5f947cedbdfe496f9a79a7f26eace8f5b5 GIT binary patch literal 5132 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 U8ztcdYGYF)4l&XvJl25>01i*~Z~y=R literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410251 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410251 new file mode 100644 index 0000000000000000000000000000000000000000..3b0f0f86fcfd1fd235a4126d0e0828aad80f0274 GIT binary patch literal 3844 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 Yo*N`?qa?gQ{WEIBAx8Ry#wC~n0IpW#!2kdN literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410253 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410253 new file mode 100644 index 0000000000000000000000000000000000000000..dfb813015cb042c177467e0afd9cfbd2bdf05442 GIT binary patch literal 5088 zcmZQzfPifV>i=}!`|?>Fd2%9;F(pA}p+d^i-n7>%-v7MgJ zf7#!e&S-sq{p$V}rs)C){nqF;%AFQ?*8W7m)7H`Gsp>w##%DU!0Mh~ z*-z%Mx}}3`N?LTf8Db*?BZ%G_E3j#It%l##16J-!A8ws+l0UpT)LrICK=r;RmP zKqU_HrW|{OJ=V-f;G8~L=;h?%#?os~HRkLRaJ-tH{C%5sm^te+@&b^bHW zh11_{S7G#7XEJ|jLEMjjw{>~a%{~<>ls6}tte^C5&Uckqoz0s+Xzsi8JkgOg?XzCi zl%Qhw5+}!Fg4x0|+D;kB-qB{sm_Chl`|EV2f4k}3rKX#_ey!JzzF9&`%lkUa5BB}_(_@d`_h>=>>ghbQ z`5$iWmRDbB@^hX3sf1@+%Gy`{*;4-hoQ3ZEf}2U^;XN8AI=w$XF{J$an;I0;z$JTn zPGXC)!|ZFtH#7LlT^?RfeYQ%eYvI&4O(jMmfhQ-co-)3w72c)f@t8gO@ivH?7#Ik} z6;K@u5Ho?)EAg%W@}qlW{NmaCGw&!AR<^cm7dLmY>kd<5J6*UZ|2YG{lLIge4gr-i zgVaL-IR0Q92QO=nOTUEpPEURtvr8&j_RWW;i?$yQ3ZM6KPZ4cBe*~mX_G~IlIV0Fy zVBD6fZIP~9x#D1>&?@unBFmpXh6@+&shhki{+p17`sL`gFWEx3FMc|4nt>$0d-&Q0 z_qFS_1>wt~dFq%-6i5&Edcf0)lV2qdPJMsJDr)cJbw@afA6@p=*5`%@+ximi>Q z+WXqp^5`Dp%iJllHfAyv>-qSB27<%V@U@!pZHd)aj#?=b3l% zr0E1 zOb`(+49xD<2cS9_p>B2fdswvOU}9&{UuK2eGw~-4Ogx_bib%;=e<9Yi|MJ8;e5|Mc z2Nk4O+Ai6us$8WvBlqgd>)v+^dYca_*ghAtc?I$xI9zTXwD{5#|L^Yv4doBzNpA}J zHgeT}IAEaid%@B)melUI(0p-oz5_5w;GWD*hTQ0Y7kw73&iMU7E3$R)s!3cH(Fdcm7IMAGXRxmfdX!(j1 zg_7AzUpzNSj*>{@VZV28i@{p)q_dgt^|!Y@PybWe?wvE~ll<2g0{>bA{m&_%{>-Y+ z$O$xyCFS>bUYi@!e!MUcZRI(3@pMA=;*T-GUNP0L<=*ywDUpX-n(`kAKz2js=J%)oNv43y76M7m;NU%zJoG;Qqw>SKlJ1<^1|P?>NQ;5dTwU~vGoADGAMpmL0$ z@*Jj)NH^Kg*i9g}!@_H@xs8(W0+kQch(mCgAhjL9(YJBUa*h3V71hqOql7Aasw9MMh*iBf|=pb<$CE*1st5M<#iG#$1#~LUc zkO8Euhn6WwX%wUvSq^L#5#dEj`44jktR6siFR1K=mnB5l3rg3pG(@{Fq0CJvVUOfl zBql6KaOO3PvgC^lLu@KX?jkndSAWuO%u_kZd}ikFRCzY@Ck<|;fgZt76Vb~NxJ5t) z$X%fDJ`NKk*ro>7-8^?>Er5CsLG{D5f@vfVAu-`fP{IKy3QK2T`+;c?;Z~&ZCeBSV zS7_`eklSJ51y7%Y#BC_$2#Icj!~r~tPzn`N;t)N3g7kvSM#;~E#%-jfFR}N0+hOU8 zR^i2fT<#IwFGcbPQWTFVrkBg_OXMm~a)i$_udl!14lC m#-XHtBHaWW8=|S3u$C8t#BG#>7ib)U8gYnTUce(A!~g(J;GgCI literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410254 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410254 new file mode 100644 index 0000000000000000000000000000000000000000..5338e11b07a31e897a02b63ba0122a862daca410 GIT binary patch literal 4932 zcmZQzfB>E4$2wiT{&%19uCbXr=OxqD9ShC2U9^t;tWbCD*yK4GKvlxq4AlSWy!Yj^ zIP&B~9%D*^%tD2frM+pdSG@n5m$y>UtnISwm52?D>X8rmCkoAd>t!~jZ*9kAzF(YP zuC1J5c6UKGB`rGL0kM&R5ky}t%DkBr@ZB?)efFalrb1@#|G&6AM~`j)>WxnF;(yX* zfJz(`vhGJ*;|W{5=HCa?6+cdTJStGToOeUx+hzIVuUs^y|6F+4k)uO@kzJ(W-5dM% zv%LOxCSWF8>e(Y4pX*MqzM8|OapSkq8C8L0*J?J%KdJt$(H-IPE+#U^rf~1+ZJF)` z+k$HIowYo6ES>y!-HaLE?lkiMSh?wlW9ardjB2Z-+72r+h_;;LeXwow@)VGZnU7zP z@C7lT;1noqQW$)^9YC~##%B*LHQnU(YrS^#%@SH#-q&G%u-Pv@D< z|8Q%!y!t|upX>BbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}R5o738db^%i818FCo6uli$Ydl1i3+^P%aY?T3TH=e^uhL|e}vVc>Ui0EWR?pi*$0 zf%E_Yhz;fw)X%`c22va1Z3xoGAh7MbuV+cpU5)uo=Uw-eTBhGZeD(>*QFZbWv z?OXsNV3D($#; z)iY&&z~TFT79m#qB&K}c*X+x{*tP&@?N$b+@A)A6ffx>e>30#3!*a*ZS$9@fiY7&fi~_aRX>3lVgadOAwF&1L>!(C0ESiEA8F-D_?JN6o=cX5{uX683Go@ z>$GMrzV;cYPNJyh7EC!K*nhyV?3nBrYjE((!Tm8$n7Dujg8g9l+L3w2 z2E{YeSX8rLseDV}G_(WWp6h@EC&(AWiJqqse3(U}cUG0uYG z0xM%njqt-a#?49V6!v~NVs|dPi@&llv7-CNgzL{< zh@J!akHtmO!X{32R=59w`+loM;+l>s&n~%te8E}!wxF~LpR3)WcBcFX0w5a}CX7Jt zFQ^~z zR(v}A%n7xgvR&Woa`!)o*m7zMG>);Q(SJ}mSeS$AHZUNfEMQs(;_iRX^21yB#D-bGJy z$d=%;4`vmJhSb|o`+<2d6Do-k=ES*4<_eA7gf)#061PzjUdU}HaPT8V91;^6GnjD> zi$nA@3epRb1Enu`y+dMn(avq8xk>Cj-*#B~qLrH%ki&`Sb}N!U;4uVbV2e1useJ2A zPfOj~QrU66)jewGTD#C01>4>ixQBTQ#$0nPhU!NzFF^e(7y#t~ly(;p{WD1W8roKZ z83d!TxDzEU6K8(P9AMbN>sgrDAR3GLSo1Lr?4iUT7|uo#Kw`puie3&A%9F6P1~!Wb zzmw9xhB*q>&qj7HN?AaJy}+~sk6UDOz;+-BATeRqW7JK!!vrZ0k>Ms3yOB6ZOp@IM G*8l*&y`n4t literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410255 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410255 new file mode 100644 index 0000000000000000000000000000000000000000..b089316285603351a4aa78543a163cac98fd3f3d GIT binary patch literal 6208 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410256 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410256 new file mode 100644 index 0000000000000000000000000000000000000000..0f076b8752f11f1c11b1056a874c1de967f62eb4 GIT binary patch literal 2864 zcmZQzfB>m&RVjXa8>R)g#0d00RM!suK2mviD@sxAV@g zZm_#cIQ$4JJ)ojt_?K~|(sQlodzHbVXX1U=uQYeKB)Iz2hyF?y2j+P0wbBB;mH!&A zrDQ8wrGacpT6B6c#6|{25WPa>ti+zVrEI>Mv!bKdEU`YkO5Z5cLe#cLfA-eXxgU&y zN*pwge%Lkj0PmbIo6emK=6=f`AKDb=m$TJlYxb<<(5(tQ59_@46x1xfT)F6c$exlT zChN_sLf3BCymybz*RU@6P_~5^8NId~E{T1?F7Vj-Gy4VSofog=e3--5zQQfyiR`wP z&pG>Foaqv|cX^j*pxyG@j~EP2`W^SGdv&j#eV2XuqkIO@mJ7TOwryUX0&+3)@e9&1 ze?Tk-Pv@D< z|8Q%!y!t|upX>BbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+})|^&mzQXpTv~U`3x|g<>%ijdpYKC*xB{S}@*R(x0`I*(yHOFTNJUM@VS;h^Zc}$KWo-RQ^1`MR1 zx|Uoqi?6hI>#uyh$x$3`r%EhdmuCoA7_ZZsx%k>=pgM`7np-gCj9~u((^$f^m(wLS zI9-1$e{-|8N37D7kn2kNtNuGZ-|(gC(runQ)>D*zYxXcSbqd_{kt%p-{p3ui({stB z&)VEx?+wq%a{~YH0)a7QYpx%uWZ-vl z0H&vNF!dlkKmZRLg8CU4*g)z-yp6yb_3r*#^~uubMv-mA+9jL#&xLwNEPY`WbKg$X z`{^;L4>nNRw!#f00pK4zB6__yq+>q?h@YEl-je+;ylr6EfY0&h3>s65;BRsVo`v)&~yt(ava`c9~mz42w%HL#`Nv;$#*Nr?NQOmLVn1HD08)`o=E%!p@7(sa+rj95#oxb~r#%_Y;S5SBjHn&j{ zULb#=#1#?;i3y7hoN)*a6SVpT7QY}luvtXJB`J9e<|tSmLv}AE@kF~YAwTWF(jUyv z$dQKZZeHLyBPu=dKeAvUtqG>Wo)Kot%7iM)CT6?ltLE&x2pd$a{ee z0hS~GfdI&c*~JLt{(@QnvJ0ggA)<~E)0h~Y2Q&^;uN46G^Fqx6Q!q<_93&=O1unmV t%Q0N#2vKh8zXGfqXzC_dJfft}LE<))a)d-Tf!Ze2h(q*p1Rm)i1_1ji>u>-7 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410257 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410257 new file mode 100644 index 0000000000000000000000000000000000000000..6ae2724bdc3b942b7c5f468073e91103d6bd2392 GIT binary patch literal 3892 zcmZQzfPl#Br6Ip3H*1~9eqmuWC%Td4U|Y&n-RIvf8i;UTu-@nnR3$96tt!QjZ^N_z zml%QGhYH)LI0~8-sT`Dy^xAl4|HIey8#;UEhW39KEPi$Ay72ca@}Cy)9di7zU8Ul^ z*83e#-<}8Al(gvdOo)vPj39bztiYz-wHkg`4_LV`eYkbPN&fKWP_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ4fsxFc69> zpgLwCW&)`%(3sd78~lIS0!emfwlAfs))y3Gx|$yd#Y)dO^VaWZ8Bht(=V2!q7!>CN z+2D8s=>q{^e8TL6iSq)58AH9n(hMA`FLpFY$|h_TmJw*=EAw#>va?XOHM;NHbE9|f z9dlivs(mi6L_z6`7{gH;NpTO!c0j|(!SaL8d8s(2%Irkbc13wdCPE zo|*b4DPLTVuzk62I{ncmot%>G6HQWqYC!tHdI{PGRrk{Na`~G8Tg`CJx@5*3_L{cG zCO@-Uy5{%{fhXthFUz^$K#M3Vb$bf+%c% z3*&WKGZ$a`3{uBv2ucQEzzFsqFwbw^bv$N;hU2m82YRQ=tmr<UAhRLk zL2LG`X@2~in?)l}>H0Jq{tMia2Cqc}82FtW7`PQ1f$Ap%F-lkh`7i(vD}maJ**X@+ z9!+|Ua^F{-7r8j2E1w~xDCDx8Ty9&~gE=q_*8evzj-B|g&f-d&$H6e+IXj~xYn7*d zH@s*wYxS@8eL_HW;vQu$V9J@mZUOrHjmKZZ%kQ1JRVFd*zZIR4MEAD!Ve?m8K zFFSgCnYI5>QSCsj-oM?-O^&`CH&mAF_@DeqN%6<)k34Noxr`P|r%X3xxL$01mL*~J zUzLjO4V*xSF*nBi*!w{KpZDGmUf%*wN9#nM^p;Ibw!0`Rm(syK7b0Xe&ppg@J{XN;2@(?~3yE_$4^qZ4!0b<26m$V9#|SFZ zVd{uM3|qwORtI)^MUCT5#L0Y8%TO!ZU@m=+|OWs z_pr~)_czn`S6c+^y|T*WcyCa&nAvGf_zGyt|Am4i5@Mh_P5*%`Y~t@Pi%Urnjv!8hbvgx@J& z;)13O^fbx~Or!sxaD?UKmw)24IfXWj%KuVZMOt>_zGzzvK=)X9q hN|Z1s(oH=yb`#b#I!N3`NqB+!C)9{TtZ5V$^8hJ|*@*xE literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410258 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410258 new file mode 100644 index 0000000000000000000000000000000000000000..75f90f17a7039dd3d7d765d69d3bfbe6af34cba7 GIT binary patch literal 1528 zcmZQzfPj}-JHJoyI@U3Ttv5b)xA4W?8}=UiAJmY<#}Sy^cj)RCpeo_W>!l&TCpT-I z$bMmAG$*=|{V9|pL_huT&yrU+^%!d{9`k_&W8kY zUlb}7ZvPLmDQVH^`4Af!7(w*aqRg8~0pC4y*=Ij`VJc+y{{M^1bM)Buuioe+Fa9T8 z2B^eAd_&C_UQwkOx&2Qad&B&1U&{;+%(XY)U%oke+tjd~=}+GDi`&oDUJ!pF+;fLzRc{DRDr z6Cf4@oXP;wDGWZ|4j@`V*!R~@k3D+dqXqe^r}NC_ zf4H?H#YbvCdgO^eSl({6_eYAS%>;8>tO=3#<>nT6;_j7%P?hk@texMdcpdAQ z!qyufyIc6;?hSj7{SRtL;^PQR?mKjK3)A2K;gK^O6Q{k}(cWJz%J*XN(~WI4{}ZP+ z$27c5K41v4DQVH^B@i1K7(w*bSb3?9`f%%nll zC;6@}l*hA)MeVlUyT5sF{k-36FON8Oh_+njeXwow@)VGZnU7zP z|HJ`eLBOdDAf3YCeb((=9z^MieV{q)$Q_dQyWzj`{) zZ2pH^yXDmvn*3a+e=6bGma_Jhf3}qWKWCvkzu;z)d3cY8iB9j&PYfyl{-y@SG;qnD zo|D+3>@fRU@y!hWa+inKQ=hF;>RLGUO;d@HNZ`rIs;7*vYK3bH6_YL6g;HzN_3yy)5YM_~EDeukt(GK=VNUVMy^0e9BZF$9JsSQ)$P&tDY(I0}kK!vk0-; zCo$#ozGhzr#$9@fiY7 z&fi~_aRX=`lVgadUl5Q11L>!(C0ESiEA8F-D_?JN6o=cX5{uX683Go@>$GMrzV;cU zj?qvTrkoM%KVVq;6!2DFU1hVU@5yR0YrdDiRNBt#ci22&R(-$y@BT}xlm6@qUpCRQ zW!v`~0>9sVcyc1${0--`ppWmrN8S9se{(h$&_JeDXZhb>3P1RG&V|($JJ%oeix0W2 zDsXB4k*M_4ZYJkBK4441VF_VD9RbT1;vS`m2~aUmyf8!44_Js`dShS^_b7Y8fUE~< z7MMbbG>8NvD38L_5M@5deULN_(*~!pm=E#;%pbJ0hZ28aI2%a-i3#^6$Un${8O(u( zBU(Al0+a*AA-R6XupdbPi3xWOv2KH_BO=a0?mq#ufl&Fxvuv-??ffs&dYN_lug`oG5@BjwKV5b5FU=VXmZxrdFoWgyFR41P z-++1YKM)|fnGwkS1(rjk;Y(0H;qsD!K}@61_Y}~3pfa-7eph40}>Oi0vVMj!nDyGqMT#LqT0=XT8QPTY&aT_J!1*-R`5r^Qi0I8gSMkRy- E08psyga7~l literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410260 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410260 new file mode 100644 index 0000000000000000000000000000000000000000..d04eb76a42d14dba7466dfcb084f9e61c6d99cae GIT binary patch literal 2736 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 Khgj1nEam~_hjCB< literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410263 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410263 new file mode 100644 index 0000000000000000000000000000000000000000..8effaa7aec85513ba74c93f6630cdc5e40ca403f GIT binary patch literal 2428 zcmZQzfPmf)qVI0%GVbk6-6*yB^o|ZW*SB`Y8U~Gf((da1D9cj;suB*p5xS`U$EC}9 zduE;&I%vyOZ4;q-)Jx#N^2)SC$*U`RjJ^miUBCPPo`WWD?=`Gy6MImi*!nXg(Ufyp z^i;z*#VH`0k`|rb39*rZ5kzl|71*@9R>SY=0W0^V54TP@$sgVv>MnC6pn6{u%O#s` zpc02A`!|>TiCOBm{-dhKg3i6`kK}PWb>UQzA8rDx}K^}G!0nEREy^KNPH`TTC$ws#M*vI9al zYVZ10kfXPhf7Z-Lv9q0I&TN-tb4W{9UH0VD3&!lJx%LdAE!TM;Y}>p%1>|Dp;};ZI zKny52l>ua=F!*>ofM^Ac&mLN8y2ufzOc-(NpH_UL_&7UZv<&NG|; z;nr?>^@S!s*Xf^1c($diedV7m<^RuF=*};=nPeW`qhX@c`|}e+%D=yXpZ(%!AV^7ST1ak!l-v3OmcAz)#=PHX1kYo9?% zB?_b(Kr|3ALfpyVaGK-5okt%6?lOrdY-=jaoXo$T_5CehW0}d%qO%GkiwErpZY zS*g=UDbF+S|nkCfABh^s*%5A|1HoZ)T%rh*pyX`AYeE%x9$S+D3ukywG2_KT^ED3cE8-&y)c2|< zHgiJlO!*H4KsGE)7=hehP&rW8G6VC|H7K8mw8tPWL3td?Ce}@`JV7fr!SW|4yat=wCU8%SzwA5X_QDedC=HRSkveraT`h+CDBcwGMyT6h&7GE(gpzfvTPav literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410264 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410264 new file mode 100644 index 0000000000000000000000000000000000000000..b1e18e3af2857bec570265a0680f14b85518f5c1 GIT binary patch literal 3588 zcmZQzfB=6hp0tx$s%I{_thM!WS=-)zxzgwRSJhCzr&d$;DB4v6RSEZg5Pf%3mvL`r z>PD%}r+0M7xxTeC)-Y(?lXh44M_HcA)Bg^S_g;@t3py|49DKCxeBfMzz9@ynzb`^v zxt9swtOD7TwCMCch>Z-4AbN$$S&2P!OWAxiXGKS^Sz>*7mA+A?g{Wd{>x1jpH7?khzop4x1PH7cioka zS=KvmFaBWVcFttQtdJ{{_H&+KtXs#i|H7dS+I1q^^(yLD{C|=;W1=h%Q{t(=YBA~4 zb^hl5nBElpCZT?Vtl{(KFukkUiw*4dz09`TYC6?oXAgsD%MIQK+cqyx0lAp@_yq-4 z9S{owPGtb;6b2t}2N12G@!3O5O*eV{TCW{_vxJtG_jQ;b?ECAd#~!`!(SrQd(|Km| zKit|aufEXa=Q{mU3D35awXgiMrTqUn3*GqzH_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ4fsxFc69> zpgLwCW&)|dTWY#*YL>^Ojs7og+Mjy1`E&Zl@;FsB{Ue*3q>X}JUS?oyTLlb*BS58~ zI0MHUNFN9QowRmE6Alca|>O+>v%Y z>{-Xtl7IF8Apm9u*gjyKt8Q%(nH=V-8glMJ%<*1^rAeNv*8C4HcHerj$l>dq2Tq=a zch}u}H&K#NrepU8uDOzG^UaM9D_XzrywZ}(JM#`d&^)l;x~9DjF`qd3uJa1vgpdih zJ%zcHmLxBlBbeiK^}&)hpQ8*6>H-YHei;mmVY7jH(EWA;D#lSzTwrBrVql7-2CVj_ z?d9?}0k)dqoOQ{JJM1-Wk4=7NwRFw#83Iqv-(Qvi($C}=;u{$RWWYfBscXph4DJAnTxM|2C0)MkZORcXN0&Dm=6Tl4soq6sy_aqWlO^r z?b}m|d8%%(bFntqByoLvlDg}H^mf*TrxrV0{jsy(_hi0*>DLz-DVH0nk`A9Oocs7Q z$i3h&F?{XFJY$36nQ1Jl*{@W-rEqdPD|PxP<$30vJZU;X*bt~Lg+V~JwWrdKdsjVE<_8?U?`IKWwNGNo=Y7q- zK!r>Jp+NyYt{^QCKuowW{rb=VRn7=?tAhd4?+M$ocWkb<$^2ZWa{HD-FW=l%>(W&; zHC|}g>iV(8yS%fI@O8@EB`bYoPQ{{}*=u4=1^#dswep@=&urZf3KM2#C#R(oa_40~*$#a*-9L7evD>L1n^Kfa4O*gXCuhnEk+fxf3eK2owXQLoguDO*@~^*i9g} z!@_H@xs8(W0_7KK#349Lkm^cs+;M6cauv2p%`Ka2|1|2x)5m7%+0Xxe6@Gnz@9#m; z>=nXbMZmm^C5`@q%E7`Mo<<3mj|}26zDck$au(1W9;jJhiWX^IqgQ0&)}^mA-Z5pPc#ivV!)8mX$EklFr&!%RDN$DF(e)jg zcEBwuARCr^Ky3yXfTdATdjkvzwoibn!1V&8jRMt$pkS6DM1b}pm5D@}F9XyAcN3CX za8DqKVNE+Uu!jh9M~)({7!V65o|Azs0S5v zKmcSSyO$Z}CXhNJ>;V!Z literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410265 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410265 new file mode 100644 index 0000000000000000000000000000000000000000..50350cf53eeb6a0b0c5a37d13c85dd2780b4c114 GIT binary patch literal 1476 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 MHUb&g90HRE0CkqsPyhe` literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410269 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410269 new file mode 100644 index 0000000000000000000000000000000000000000..194edc9193121525934d5d4ee7840d8dbaba3177 GIT binary patch literal 7072 zcmd5=2{@G78~?_dtmP^du6=8aY-NZhRMyEd(WNA&t`^I+4RIw@62i2M<^EAxEJ=UK za{DV3F{#KBEvQu779mpM|9#(lGkqgH#x2j&d7f|1d(L^6-+ABnEOP*WQF`-YX+l@A z?WPWV0V7n(lKlMhS;P8GBtAtE(~QLVOF=I2(UIPV`$w9KEMs_z++qmw0u?0hC4sRR z*Pf?yV(L=qkz1F))2-KW$$Asg==MQ8sdQhe1P~ZMx6E>M8K$G=C}hd~B>g_aB7lMF zbBYa;SN;m*vr$WRammj%tj^Wc-k~pH6sMV1O!w;70a*w*fKo#0Zgzf=?BzNV=X9Q& zyRZB4hay9Zayg6JqDgPfmIw4KeN|F_cCE8!?7P}y3+IigoG`y;yx%fDFm}9y%jmDX zuHcoAWHLfG9cwGfv)5W)?5pDaYGHQrEpB3r3>J7oQz2r+l#|M#gj~*yc0rcqTR)Ze^trS9WBtTcYA&*=wQz0OO%P*`WhL& z?d~g_;#769@uLF(d3J+1f$_y3 zeQ;FgIcw_!^#7rFis|bVsHdE29fX*8-*+X8$tX=nag5G?wS<@IMT_{YK%9NR;MNAJ@o2DKmA5#296_$XXUi%s^BX8A=~Qm|d3 z>@3!&?c(VgvwC8(RkQ+i)S$y{gUQBz+0AKys0jfOl*tnK~zpChyeK(ng_jPvU9;$z;zOe1&*)26RK&@NeZ#ylUQS^@pn~Gy|;P$b)0Vd zz8taIV6q$bNCEAncSF;>O(~V!qr#uk7Tc`dlYL0|c8eYlf`L#MS%4WAT$PO*PHYxT z64;VvQ>N!o%cERiv(;({pY~hf{+kUaD+DfPFLg*Bk`?MOCMu1ktqJKK8mx3o-S~$9 z2n^1v<@or()YETU5|t*#+e<#ri z31ma+VBKk#uu)g82nneqQ$zBVERNJ3Q))O=?$@+8%k$IrYDx(wa}a+PxJ%|8->LO+ zjsx+^Qr;Shs=GY6ZloW_JE#_dI7?=8-#GGfv|SZal4#qpD?X( zT+Av=zS*D&T|J8!U9-v@KsIP zzu95pOy(kfQ@)`ssRAeRZ*2R~K;_v-v&P0nxIIgD zOZh7rMZepzuBklCa;+?lfXKnJ)7nF9^cvlxys|Y?a}o&mz?801GpeCJH#nVNOA~u` zr}K;_bZ&F4CRv!-!$=rE9PH7OeqkL<2@JB=6Z#bnSikdH`Ej5*xA(-rsw*bNnn9N* z+>Q`7`0{#_)Ra24)}2zw&Bfi!mbFu=i&Wj??Cn%?Gs=v9PF^y||%kdLN{dwbhI zW0gg#MV0H(e9LW>RRYECZ6f}9d!N0J5aL0Mg1HSBHa{YLQ0{Oedl1|;{ho!aRmQ%B zsQFS&VFUx+ADA4R=_jY7F)?MKdJ6r}9GLT%O*`K+kFQ{(I#48QrqF!Qg&S@lew=iN zgvOv4{@b~KWgpdnB3W=a$2LPui<<(Zkhp(k7uA6xXN(Cz`-$VT6*Ui*MUa>Zh_||Q zl)}Y7j&-hmPJi15RLK68C-*7!33f*`<+XHb0CdlT%8b_J^k*<~ANrSTk~94;zI#p1 zf*rOsIYa$lg%8z%A{hY#e>u;4Xgy9^lRphMR4#LZNj)c$8TYSzg6cq#GxitTn&kMd zh?)n>B1lXH^+X=9-^mRaZ04#X6II5^QYy#SAfJg7f+jF@$}BI>X^mHd=Nl?%XQa`o?FAXibr<`udbNd47op z%J&bERH`45S$DZfIf?<>A7KSweBO+-0S@lH3smE(O~6$H-L(TjIq1I)tDq7SMY4vW z7^4frHxkjXeQrQAJ_@~gz=eaiH-P^_DG7Ohs|JO55mC58 zB_@hwaeOOBVf$c?-a|Oqe9jrBlKxr7G-ZzdB-p;kU)MpO|A-h)nWJ)j_V300SJYHF z^liM2%Srk`oWNF;xh3Y>I_)U^&mKi2-W=S&y&f>LXxevBkkJq?(}lc+CNt%jH7YSt zBy;pzXFbEd`$_wLgpvY$ z&1Kqm@q!K`HA1cgC|3xci-|0&2r&g9wz+elU#Y9EY9{*G?(7`D*Q4&L=s8S*pEdk{ Da7K%L literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410270 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410270 new file mode 100644 index 0000000000000000000000000000000000000000..5ab3583ffebc20586f1357eceaa8ca6a88753361 GIT binary patch literal 6052 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

GHP2rQMY*0>pSe3pBxS@O3HySWUrfK7Hz z9D6n!dynfO3Y`Jo^IuO*vyI&FCQ3`icd-WD+nszj`HZn6J?Gq({ZQY@3Z^K6jEw~$ zJ5BFIb-|4!!iSie1>ab5c-@~X+|64Aw+>zqnhf5IbZg0)zIVRRvtRrCk=+$?JJf0x z?q2*?yG7{{zqKW49kMgJiUnSz$ID#?Ho);vxM%nNxZLHRA;PKsDSNgyHB8MsVV#mJ z&$hpA(MDxvU{eCjBaz@-V2@}4`J4jQ-6p(o>~$KMqjULqGPYKKVw2E7sN&W|@e@Bm z-%v3B;JXd;3HkzqxN3# z9n3HE9eh8|SdR83p3uP5#ITJMlU$Q3o?nxN3C;sCPZIBS2z?;GxO|Qr&0|VMa@kk9 z6Hnga+ATY#xMySk&68Xw*RAn?c<7b9#=CGo0s(gLXA*cE#O%Vc!+5sfekAzDD$yf< zRRZSVzUTnxHv{hrOWeuGs#s$**Lbd>{z0y_W11uKUouSP*CrVg#E$dTijU|k!8Q@T z;NJF2#1Q3#uwI}PU0vlel)m&*g~9<96P`694C}x2G2`}XZeM$X_pSA_cQ5VLDgyT>@*rK+Gg5auBiE+Lli8GcjIa}ucI-w5$H zRf4lIBAr`A$Ii!vCo_3HnAQzgSxcxRF^eRjS2O0eNMh;lnl zOzFB(7EnSdrm8epmR@a3p!T&8H z!E**LVJn`a@Jx-01m|9aJlGcu)&p@Lye}+?#&->XCmYHgcd>r zp(oad;mFZ`baxb21rzMgLbm?-vCEZ~p}- C140J? literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410273 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410273 new file mode 100644 index 0000000000000000000000000000000000000000..92710228022088d31967d5a7a3629f3f836f72eb GIT binary patch literal 4864 zcmZQzfPiLAO$nd)b>`;wwpVVgpCH?t9J~2Ju92G8bPcc4(*ACsD&gBs!s|MF?v`rJ z^nDr5Z=P`M9mAU7Jr-$tldU2iT)LK_wn0fMUqWfllJ2sT%}uj@rkaa0mArkF7`paT zcGZF~dyq{@i%!3R*vP;LqOTTZ-b@Pk?wQLz`_T(iA+z`YUtFG}$F_gI-;jtsx&)WWqV`BwiEA8esl_vSez00#?hR6UGKX$&vF{yxVrZoQMenq!o9$x z%TdG1zASh0OP@8Lwm+zf71RkElnf(a#yB_4ufdRL*575HZM;BxtRI*1qI;+ zAQl9i$^go7mq_t#I4J$m1x1^KI|^UUUd zxV2keeWA(Eb^50go^2^>U-@TC`Tuhky7LQeCYgu#Xqf2q{`|y{^6zhIP)q}t?CCj) zEy@nFuNB|S;4gQ1cs=#mDy6Q4Q{Oa|7>NX)oUD4v_^MWTmy*Y0_UOmkAZ}t{AQV?X zbu2*41X4ePm1~;lst-vM{A;9@{$*x8=au~C(|6^^jRVH93tLMo85qJwWrdKdsjVE<_8?U?`IKWwNGNo z=Y7q-42*3HfYxqhVESGKvLA@yAZgJVb|8mk@0_=CoA!Gxc{q<}roKta7uO?fU+$Yu zf3!&_r)2v?lT@G@kUp?pg7!hxy|let{wBayGn}(7nQ@1`rtPuG&#acNIX*++$@%-s zGHw9PV{#1f^a}zqU?Ba}wd9Ife5Jixf92~gofi()Hgpn>49G<@yIJY$36nQ1Jl*{@W-rEqdPD|PxP z<$30vJZU;X*bt~Lg+V~vj8L~agj(LWs?2!!(x$;^QqDSo=9>KA7GPCHz_9hy(>z|_Os0j0sz2EIqvQ}vInw|-$f><@|!`5WY2<<8W=D_%w=#$_+NLuFaJrTy+Em>*4|6=cnklo;Oyby z8a@dOmNEI)rg-P zp0{ge+^ddjgs&}FE@}Qm6SMq2#PqQaulQ&quoVfc+t*nNbMD3 f+{AN77M8wfGZeenI_O}CbfvSX?H8mxC;@6p* z+uL5bwSIzZZ*uJB2f0RSUeh(aN=y5@f7|;kTC~C7)%1DBWp0mD6h1UZZ*t3eqw_v4 zb^gDm4^<$Wk`|r*2(giY5kzl|71*@9R>SY=0W0^V54TP@$sgVv>MnC6pn6{u%O#s` zpc04PC`Ofq^F$I#rp{H}WxIG{XQ9Q2X1sLR$g7Z z&g8P1lz-{*--~SZ|9rW;fLzRc{DMNq z1rQ4YPGtb;6b2t}2N12G@!3O5O*eV{TCW{_vxJtG_jQ;b?ECAd#~!`!(SrQd(|Km| zKit|aufEXa=Q{mU3D35awXgiMrTqUn3*GqzH_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ4fsxFc69> zpgLwCW&)|-!1w5Ss{XO{)-Q}_+`IW-3Yx{Q=DVh z600p^Wh2p5bm*Yh4*f?$f9`|K0{bDwKkzA2c^u!dYEPvd_pW-T%nvwx-_Ih%YM;cE z&-5n$)3x|g<>%ijdpYKC*xB{S}@*R(x0`I*(yHOFTNJUM@V zS;h^Zc}$KWo_;|<1`MR1x|Uoqi?6hI>#uyh$x$3`r%EhdmuCoA7_ZZsx%k>=kUB;~ zU6^u4u>XMhBwE=u`sv2Jr@WdX9*bYUIlph-Y754VQ+^nFJy>&ub)z@KzGstT|9(2H zR#BZ|a#i(T^zzKJlmGO7D%4!{XQx;nC(uA}Sk7SOnkKsHL(&BQ8fm3}nOVsvRkp&|0Xg})P3 zZYy7uuvnJwFFNDe?&nE-$`%Y87wyqI3bh5O59|h@f7zx#Zf?{U-}B_{v(xcA_fGVj zrRJHclWFjLeUms}SFhs>J<*k+ZAD#Yc)V6t-qha5@0RS?C-sTf+dy<(^V+4XKoglG zVsc(dyinjg{7d(*c=mMno`ALAA0KefoXZh$?T_VdUx=;1^bY~BbOUlf41mIn8Ja)9 zVnq0#LF!V83{cMlpaz(kV7(v$NdSonmj=fXoCnFr3^4nF_2L`iJ{bPs;KljrT#+`M#W@9TzX zD_30%fCMu}IRXoP5Df}1lyZb{z0ANMdlpzNLF#5?v%qW=k3wWo(l~ME+kPOye2^bt z{s6}t$XXiMLy12yf(1zci3tfXsDI!bAOkr+qPJ&3awvI*M89L$k0gM^ggXZm*2n<2 z+mP}MF>aE&p_CUSx(U=@ Rphg^m!vv{rhXoOs1^^LD?>+zk literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410275 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410275 new file mode 100644 index 0000000000000000000000000000000000000000..5799bbe692ee6c991d517d5b51b372541390c640 GIT binary patch literal 3896 zcmZQzfPmNWWy~wCHm3?9`f%%nllxNh5}izS7%fwN9m&oW@1V4<`7p@g%=n>}~h;to7G zo|oC>claXLotx$?R$mydeO#+wWouP1d&}Xy|81HNNqZ_Wh_*cDeXwow@)VGZnU7yk zmDvDdLBOdDAf3YCeb((=9z^MieV{q)$Q_dQyWzj`{) zZ2pH^yXDmvn*3a+e=6bGma_Jhf3}qWKWCvkzu;z)d3cY8iB9j&PYfyl{-y@SG;qnD zo|D+3>@fRU@y!hWa+inKQ=hF;>RLGUO;d@HNZ`rIs;7*vYK3Xu z%iF1@{cYM=?K@{HNjut9UFJ8-|CpI_b63#^2ldrG0(vKp$)`{M;oYe;BUC&3qeZF& z$Y0>FNbwJR%2Xc5cdXh|X~(^*o+i~x{g**oX0+@}3rOCHYSnW=A*^2PNC+n4*M(;scp$tl@B(Ige92BZ(Hm!N%6 zbuVo%m%j|*uW5U1@-wTYYmUzlcyj*!vWy!*^Ozh%JpF=z3>Zj1buGDK z7GG)a)?fL0lcPA?PL)`^F3%9KFkYuMbMdv$Aa#s}x-jL8VE+Nba>wLO>*){M*S~S? zy860)cg+1?uisDlKYiA`le?^W&TW`@d&cker8O7)4ffu8@p0=yTh3FHoTR7g{P$=y z(P+!N%nvjW9F`mS9$in>Kepcbh4GAgH$SXN^4T>*^6Zz@AB;RD0>8>{MoL#;^-zbv z!-k-K1_m~e7hq{;;|JuNdQSAN-&w4-0AeLpz)=+{r0tOwssUs+wyXcQKav?lGCk8b&aqnpiS zWc%+4OQv1!xbpX>mD%L0UH82hP2+xZ0Zn9Hw7BEi_4j=`6UE&Q?3J!qW-F|#_2yWQ z--a1>zXTk%e*oJG%100YOE)0*!vH7@n1T815lnyx|1(Hk@-0Bin=tLji4$fYTn@-U z4s(z=s61eR*$*tIKZ5z7^am{eplk-B+;rjC1sb~v^e99z|lpRiMWYES-Vv$5oCH<)&`oc{Fwt$n7AEl0FBC+fd3865Rx9 zn@}ST!Fd*`905oAnM8$tP22aYw=GSWTHN`TRjRUlcdSj(#o4RH9`NZe7X~ZBtm}}| z4lK+cg4F^Ef^ATs8m3<#8ld$GOdFg=HXJI55`ILP|1J2bF|5`SO>3z7g5 v6CR8pKOqBHUPey`Aic0Sd<0~J&7yKUAL0(|^$9FYXkjm;e}Uazc-Q~{^J^so literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410276 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410276 new file mode 100644 index 0000000000000000000000000000000000000000..d1211ac0b1cc9bbb18676519a04f03eccbc13457 GIT binary patch literal 3896 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410279 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410279 new file mode 100644 index 0000000000000000000000000000000000000000..da3e54d5b6027ad34ed38ec76a728b665fedfe19 GIT binary patch literal 4876 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410281 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410281 new file mode 100644 index 0000000000000000000000000000000000000000..02192f689091107bf59937492e26ed11c418df0e GIT binary patch literal 4100 zcmZQzfPnYL?045Om{uhUJa7^d2$$1y(kSkH5;|kv$|YYmda*tLsuIq33zmJH`sk#R zTkieH&(im^H~q}=c{i(_)kn8?@qN|CWE-0s%`;jjV-smJR{wG}q zsKnvJs+!n%PTfZnW|$~{xGz$rdvav~^NDv%JyQkOKTVaq6#iE`S4O6$_Lf(g_T9z# zdiV9qw>vC|WB4MK=2*U5oOk`M*2`=1nf|F&f15n%aA3KEjaTvc9tG3ApAYqStGB33 z)ZNxS!RS!eJhr9b>6dDlcd8t@%<0B=kx$>QofM^Ac&mLN8y2ufzOc-(NpH_UL_&7UZv<&NG|; z;nr?>^@S!s*Xf^1c($diedV7m<^RuF=*};=nPeW`qhX@c`|}e+%D=yDr4NZo89>>kHLxatBx#Xo)gR6Ex-qIt(0skz4P0^BhgS@#WNZN2b;4DQl0dcNLrx zb$9)i&~tm_XO%E8wk-grq31C5P!GYwi=ci61~#A`#t?5Ku-1(?JWmHH={Z)tVae_P zx%={s%;1&KdOBEe@e5LFOzfMA$IW0FB4b&$6+6T+VQ3ph3TkRO5K~3*&4f(6=*KlzlN_J znP+TJJTr|&HT#vyw-iopXQfUbr998PlP66l2pa;`r7#GHon~Or0AkeilC;gA+EZ!Ay{n!n^8*gw_p=DG+9xsP^S)+ZkXr&m zg93b9fh;IUKXom+VisR%@77=WdXu9#+)kBPye`iWurOYyHFNQ`&tPRUrZqs7GeX_! zps%GDcx7f!Lda?hwVF?}F8VykIA-am`R}c*yp6&cCO$k@?V4EmfKS)9^8UIs(9>u#w&IC)4EsD1-5$xeXjgV9(VgyMH1%;%dCbc+=8f$0+w-$d6R zNP1vy2hmvE&oJ$wMnzumG%ZuEtZCcVJ*iMEW$X2};#{A*Iy2mF@u62x{VD%}0LchO zAom|s4wf$A6&DfZB?J5Vtuvry<|&{#pt2SYklcyHgiGT}17Q1sX+Q|75+!bkbdw2< z-2`$wEWF@l%ph?aCE*3C_oxwv;4neT$Ka@(82znJ$dz@E%)K2hT~oL9T>g9D+LNyW z@|P?FHvNn>R)oekdR+r*L%;wm%;9O2h&Bj=?Aeq#&~^+c9ipT~Bn}c2W*x4&rUh>r zCDKh6G!(lsm%(Jo9Va}&s2aDe1lBqm%MMJ-SiIWCa<-*628;630J literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410282 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410282 new file mode 100644 index 0000000000000000000000000000000000000000..8fe66297d17eca696c3b51b690eccace1a07ab45 GIT binary patch literal 2684 zcmZQzfPf8WeRq3QceEV4{cSGq3a9tdG8ghz7Ck*PJxBXU-h@k0Kvlx;joI(6V=%2s z6nNkyCJ-*C=cG~G`6P74yp>D7Z1iG%z~H|1cigFPwyz1b-6a%j(6_0yKL zILm2%VVYb$oBQ6{g2(YS9!EIjc!k&N32)msh3jgM_X5XczbbvTvK{+3xEoIVQK%6R zzQI6PW+G>k(%pkivv$8eZ1u}t^{__W2PF;(yY+JycMCFzw!Gqfux<156p)LVk6+Mo zE&#D0;8X^XPGRuzb^y@|8lOG1)O3^Aul3r|H%n+~d0&V5!M?wKdhF5r9xcdUJ)LJZ z|HG}_^6Cpsey-C$mGEp!S^LUATgv~Rv(TMia5Kp~yhp=Cr}yV4hLnGQQ-fj}xMWYy zNo-Men0>AIW(I$`%fsub&sHgQEu8wMsl-Sm@Z@CGQ^r@d!n>3_9s;z9L3>ws>I@Td4_<6 z@j9)Ui?4kKDPsEdp#iKIXdl$A4$HP`T{-dVkW;eh%>V4yv_&1fHD%7JgiolSyQ%re z$y?bEoKpn23$Cd4DeleT`0({a2j2wt4@G-a)l}2|Zf?}z02vE1^VB!iux`;V#bW83 zh2k&&GkSa7N!jhT?83c68vc7)y{0lSaPMGX)@oy5F0BMAM+p~@ekfpu(!5ZbDTMn8 zm`T`turekyyLu1Ee`Sh^Q@Ftw3gpD z@^S~M0RpPn=^6sgx(sT6s{WO!mzrs<{i6r^!lIIPcP2e;@bC0VVm$Jh?Uf_ zGtwynY}~{&hxb$d?Wo%mc_23M-)IgGMPE`E&rdPi9{@v5}Aq5?|316r$jNSJDYG=xS zAONyq@y-b3{)5Va(hoDR+;|D)6O+dn*w=40f#x${USnW|=>^d+OHi3`72xy==Yi}7 z0jT}JGFKcb#|SFRpll-DwB-Vg-2`$wEW8Gr+b9VyP+3WhI0T0YQoRX|zN)w43$Oq4 z5#MxnefnNug$o`3!&`M1n@6ZDx4j#wsB{k!$Ba7AGzu?+kkTkG4mK<xutBrnzN##*DSFIY~mRl8U zd-x$meexsmW4E_Y^?Lf^rY_GTpEasm59I%ed$b^b^>m)u z{13Nw%d0Om`MFO2RKl|@W$i2fY$^YL&O!ObM|@E#2lo!+0H7*hWIO$~}^;F3K( zC$UA@VfMA+n;HD&E)TD#K3k>KwQ%a2rV=BOz>||zPZ?j;3hz?#c+4LCcpJn`3=D+g z3aE|=iWwSmzy9HNw<_;X_C2(ER_qU>chRigw;pjV*E2|bwf=h&!;EPSz%Y0TR1b;34fTlGGUd@shKqs6=8b7 z<^$vS_q`vk2iMj&{bCHi{!U)Lwr0D~ua67XWhfO?Y$~37bJ^FaoIh5#7g{lw^QaHJtl{$Tt@;viS zo-~~xYzS16!XO}ant?&%ERYRzGe|2iZJzalig6Yc7g!k^o0x+%K*iv6ihtl!rt&zx zW7VEYJMLZeOqm~W_`aV-h}AxcDWCT>`vMg*1%w6#__%_!KtTGbYsnR}_)2@X{>s;z z9L3>ws>I@Td4_<6@j9)Ui?4kKsbWfsf~saGTYXs?_VmVkrk18@i5*%&dcIoX%ImK`S@tM1`|*As%ZCQJi@QK^!SvvR zMPIb;A;&szaf#DDyDt04-e4>XElofDb-^<0*{Y6EOH=*>0gw#~6GkBSA5;z$w#>jZ z@CwQ&CY%}A*KfH14eK*NeXKCOAR1-~Dif{(9G7q&$ZimT+7C=~vQRljP~L>HiFH#B zjok#xSD^43Y;L0@yg>Pd8gU2?6Qp_o9DUk1j|pD`-nFmYZ{_~J(TzZBUq3GkeKjL!f2Z8}i>1|Y_8BZdG-4O00+g!vU)E~|pdQDV)9Ih_Fj DrSPoa literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410284 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410284 new file mode 100644 index 0000000000000000000000000000000000000000..96ae96bfd67907fda52328f4baf8ff52d32ef2a3 GIT binary patch literal 1476 zcmZQzfPjLa{I!SgsP2()U}|w=m=&x2C+Ykh$E~Gj)o*$E7qfZ;RSC0tKEK2J?q2Mi zVzs$HzUNg z`xNu9Z=SyVEq=~KyX3+FYd+omEwi`tHa0y_ttiu5uk`+TVcqv&gGIhfPOa9fCi9)) ztj%g#c-(EW#xB49pzJ+=?(xU-{GMX>qG@~S)XZFKD^U z0kI(9R0fbvVes*G0MQB>pFOnHbd%Sw_1e)lOK53%Ux)d@zQ2BY?9uxkEy!Ozoo6=x z!>!%&>I+SNuG2r2@N7$2`^rCC%Kx9U(4Aj!Gs!%>N5e#?_va^ulz)FygJK%EWKYjY zY*BWYeXaOr27kHB!|SQfRw;EYocgAz#7HFYV^Ph1rSdI>liOLT(?==KGwvhJMQ8nh6iAG-pt!)w*x1C(5+s382UefrANZ81JdW>JwWrdKdsjVE<_8?U z?`IKWwNGNo=Y7q-K=n)kp+NyYu3)`jBK_30L%_m# zoz~37*FJ+3F|Awb09FjN59(Hjd@+I2PhWhlZu$PX`rH3VrT(kZJHmWy4!wT&HC=!9 ztI2H(r`Ddm<`!#X{PolSVs-alDNG9-xvhD>TUW=%7mM=(&12j@fBJ{wU+SlsnA%LI z?{o2Z`Fs7TW%>_-CVi_HwyNg;54AJpKM;V_!`uMm{)4hXVap6mJFlU9V#1k$ef^dk zXjq>G>SKlJ1<^1|P?>NQ;JAeIKz4%w)P7*TQ-aDdg3>dTO{|+ZXzV7C+hO4~*xW`* Uc!BZ@HR2E)CP-x`Ec(DS0IENq0RR91 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410285 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410285 new file mode 100644 index 0000000000000000000000000000000000000000..d63e8309f7a8e293a059a1edf1d467063e00a30e GIT binary patch literal 2484 zcmZQzfPhs^`%|)63pDc0oF$)VO|r?oo4U$iHjo zm*k(c6=YM=qBA-W8yOfu^wwB`O}lF~{H`9ba$ovz>x7g1;mx7$GDiZc_cgIxvgrmY zaX85xD4-_Bv#&WScbCd@+ZKCS%@8gf^Ce3sg%t3Ys3TfmQZMe-+PJ5MJvh_<}teXwow@)VGZnU7!4as{(M zJO+kS89*Y1!N=PHL@Q`~_Rvz(O$NAG*IAb<6Ap4t2l zw|2{`FEshNPXAQGvn^%qEB|aM|9{RxcYeXmB=hhd4HKQ-pPv{~{{2l2ifQ1IJv}F} zMcHBYwc?u@{N*kWuctm+rPQ@>>YJt#Bay(9lT}X{U)2ilQu27r9{qS5&<$WfD6W8t zn4p-!@U@!pZHd)aj#?=b3l%r0E1u>cDi0f8bN5@;JU@)t*W_?p^gvnICZYzMn;i z)jo+SpZ7KU0<|y&ga!rpxI#38$n;a!k}GEMmG*A^m9IBBio@+xiN)*k3;_${by_nQ zU;7MF!;};SqCkKV>Q;vy_ms6kzGfS&O3Q-puFt-;$*ldp!=on$B%jL^?U^@K`NaI( zeG_$MTeAc8c0Rmz-a>=l``z+SoD-dscV66}RmBN33lugC1l`Qowg71DRtBc;bwK4X z|AMrF(mBW{EO-2z#Y_(P-FtHJqNHAxbdF51ifzlsO!rH>YP{SOW;g)Vfb zZ7-L<39!`+=d4R+++nY2du;MEtEFp>&k%TW{{FIz8$k1z978-^f`AMdAR;U!ifV4b zlruu(#NpGT=#AS~%OBNQdds8R_Za`@ aKTIo)hIdflKdc-xEy))7aZ{G7uJ9^FX zg7CtyhspNFe|J1vTl@3)rE9CcKLm&6S)siW%H0nqSQq=by|UFU+{vN1t1C&Qe(mcJ zfnR@OA44ro`40p@HY^+&f!u#kIZ%8s1M}eH_@v72D|8WdiG&25x~7pUx^MjV2}1gRbZ zM_;DBU-9#ak2bR!-q`S(>s#@S)05{Oy?$}3^|9Aq`ONL@p>d2YjsAkl!P4jxD4%fo z!N4G<(b)q`e+Yvsbj&>;Kc$Dya34ufyDUN%~`UKEukXKX}s*RDQn5S9t7~Td|a9 z_rH@Xg0F*YN?LTr2x21xBZ$6QlzB5L;Jaro`|L+AOohze|9^3Ljvm|o)f=7U#s8$s z0F^j=KEO0x^LL7QU0tmU->rrxp+TQ_8_HK--h46AERNNvm{;)6-0l$3tb-h~ovY5g zTxz7(E-|P4h0gT%-O3ARxH0B?hNTkLA|3CA5NiyMW1K31??%EqG|)5jp%@{aexw#~~^KrUuJenHDk z2gHJaQyD-yg~7+$0YockeD=^%(@kE#)@w)KETN_4eI4cp`~Ldru}ANFv><==be`G# z54U#9t1mS9xlaF7!m}-9?JNImDgS@YLU(?_%_Q^i9t{(n-k+ZsQvUr-4T@>tl07{q zu|?To_O;@h8T{og53i>_Tcy;saO#_;5+jkolap0X8DG^3?^5!3%pU!C8^lcv420qe zsE!GW8B+WMpE8xl@g1x7RN8Uxs%OglfW!CwEJCdINlf{?ui2M@v26hZ1LIZ(rtkGY zJ>Yl)(qI6LPj4WD<&K}Tn8^XZdrvN2l+>$|&XFlrv2FR7>3(TfjhCCk3Y#WTS=U*+{4uXF+j+m9epjDKxXd)DatgOaY-m0Y0uk)1ZKeaA9DWF|7fr zoDu3)hbuM(u6F&^()VlYZ}h)by0=n$$E)`KO_n!XW=+5Ru(|5T3F}2WvRIZHy*a#2 zVezN5oJd6_z318bt4~`jSX;XUWE|7m|2y|BsAGB&70vYJKmQD^|A8X^7kF=PjaV@E zz6Em_Ff70ziL_H7L9Wo7*S}FHqS-jW`5{ z2~s@-j=rWROFCQrv?{QrE%+F^{M$csiD~vaum3Pte|0|;s=x!TyMPL@rO|)T_{T`2 zM3e;#?CUo#ftCm7f#$G6%>q-jNTWo$$$`dh!kR`0iQ7=pD2Z+YmFd)oL#$~OmNo!Y Cl#wg| literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410287 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410287 new file mode 100644 index 0000000000000000000000000000000000000000..b4b099a2e37601e07375229bebfa8a90d1465c61 GIT binary patch literal 2484 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 CbB{a# literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410290 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410290 new file mode 100644 index 0000000000000000000000000000000000000000..57f569dcdec136e2ad18c23dfef6c7f0f6b6c2b5 GIT binary patch literal 2484 zcmZQzfPn5*L5KXWFy7pvyI#q{?%aHrCegEhu3ngITHR+ApSyiEP?fMs-(D&*- zKqU@aZJ&1+#9cnG?Kbsz$iwBHk1I8fM+U6(3cMupaQ;b-ll@1k4yvuY!KpxVQyD-yg~7+$0YockeD=^%(@kE#)@w)KETN_4eI4cp`~Ldru}ANFv><==be`G# z54U#9t1mS9xlaF7!m}-9?JNImDgS@YLU(?_%_Q^i9t{(n-k+ZsQvUr-4T@>tl07{q zu|?To_O;@h8T{og53i>_Tcy;saO#_;5+jkolap0X8DG^3?^5!3%pU!C8^lcv420qe zsE!GW8B+WMpE8xl@g1x7RN8Uxs%OglfW!CwEJCdINlf{?ui2M@v26hZ1LIZ(rti%_ zJ>Yl)(qI6L&jcWYW$&D~a+~&hEqOSPXQsYM$`{uoY+vr1PJgsXC#PilM3YpY8jwD) zUV`>P)xEU6T>d7&Rx_NlE}3zMy{7H4$%Q9{N&0}&5@$?GcW&Wg8c_fV>`BTZ@(vHu7H^Gq2LX1F>epukNVXn%NO9P!qQ#EaCqUSDzOL8 zK9?M7{OrG<2WTKDJQ)mMJ2KDMpm=5)i)!{Om2WAW+|Ej!K1z9>c_&YrP7pQ(DoJ4w z5IfDlpm7DrMhnZcc~CLVg5m-zV`F0z1E2s@3{DdfeoO(OK>cN)>ihSRM@oLpGzTLp69=u@l^cOzXlExiY zJ12`SPu?}_`0aPw`1q9fnWS%K2b#z9D`sWuEY>%+yK3j%d$ax`XXMrQapv6%R?OL0 zcsBGxdMwn=l>a~gWW&OQ5y<@ql>>z>GcX^1gz||AX9o85n?#^teHo~a6{Z(N!z@8% z!c~Cd63zqJ4FXX6foa|eD#r*a8=!0=-L!E7jok!tJ1o2go7*S}FHnA=MjV2}1gRbZ zN8fY}qtf09_k!{T8;{PIbn5W>>7VbnxXPKqCA=WesOB(=Z Ce2|I& literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410291 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410291 new file mode 100644 index 0000000000000000000000000000000000000000..f0233c2ee231eab584649e65b140ea27232060f5 GIT binary patch literal 3772 zcmZQzfPlRpHwrp)a-MD8vSQk76^UOfQd(b}6FHW7sn7z zTB7;cRW*=JNsG?-Kx|}S1ko#0&Pwc=Tgv9EIV(DP%@XUwtMrXBEktd5^k;89o%_KU zsKmj1g`4e^XsvX)^*JUy=0{VVrk|GkcwY2@^I{&I9Sl=VSHF6Yx{|S1=jF_dd!7Hp zj(q97mUif}@zk%L;{Ok{c(8rCayWSR$3GE;D-4gAe|=`UkvFcY@3Lrm?7H9FyUPB~ z-Dsn3@h@ajbPa!7&gWbERK4tqLb(|uPcl7yvgJcq;ZFw9me0HowryUX0&+3)@e5ks zOF%3LIF$jUQy6@_9YC~##%B*LHQnU(YrS^#%@SH#-q&G%u-Pv@D< z|8Q%!y!t|upX>BbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}qtBOgDY4_e3I=Td(_&L^{**rAXVh!1 z{{CqnJJbyhUtE_}%y=mKMZzWH;x!k=tMd-M?SD1z|Bswf#`1*48~Xa0<{xCSyQlS_ zSejXCQn{LcMCaNKy&F0nZd+%z-{2<5ui)@Y@eh2;R3687tlCp)$GxkbDf0sk-}kc! zvDzmw<@3H~Uk1ju1;C)$%E0u!1*iul{DAo(4JgL4cg|b6P5Zr;JerIa0a646E@wz-iz`}T)*38A%K7-UT z8tTH7GlKmGj1%!Y-Z#Y-CfV`Mk3O@bAnoeo&%OccAG;P-*_>uGx{%$@t`W(2D)nC@!!v1_Dcn0+>2t!;dK- zG$_Ew6`~nL5)&>AGp02_budER>cI7K&m=jn1k7oa6pSRN~D{dXzV7eX>^dd4JD0| z=q6D8NR2qenntfq^E|-BXp#Nzn*X~cC#OW6ZfCe)&$4%B;!DA5*Q>g>p$P}v$^){I zk`FvdLDOjNHJAv&wi8gH)xP#c(Du_Lm_8VdWC;=zCJU((;XHUb3b8+FQ4q`>C}B>d zo9t-pCah_6khqPK@S2PiA7Ca@#33=^szBj@3_$H}P+f?gMnQT(a-e$pB}fSf5Nu1Y zyTvyn=$0r@=>}kuodDK^L|}0cir)#FfAhQl?N6kb5BD3$8AP}7k=g-B&c~VwVe$aL C$GHIj literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410292 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410292 new file mode 100644 index 0000000000000000000000000000000000000000..b7e3e9ee0c68dcf807a403fa817da3f70b823878 GIT binary patch literal 2708 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410294 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410294 new file mode 100644 index 0000000000000000000000000000000000000000..7bf15567d2abab732618ef533fe74388433f8d64 GIT binary patch literal 3920 zcmZQzfPlNM>hrd{a3wk3k-l{*GH?FubrIgHRy^WT;}5EO=@BCXR3*H6@*Lie(sEN8 zR|s61VZm}Q&EaHb{z07!&88|>l}`A4)%#o5yQ_J=yqc?@z;e|BL&a`a=aoWVPBcp` zHOY9K^BrVU(xNlb5E~g7LG%ihvl4sema_S3&Wes+v&8!FDt)6&3sKu1{n=Yj=YB8- zDsf-Pv@D< z|8Q%!y!t|upX>BbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}N%EpOf;moCA1rC}Im*DGF2Eq{m%+do zwh*Ws9B&|fAOMWd5+DT<<0vRDurf3;Fop7g6h!Sy+sox|0&F$IIqQ-cci3y%9-I8k zYU!HeGX$QTzrQTw22dT7V~B5L5J(3Uq@TK$TrrEUw0G;Te7(t09B!vdEMAvq2v``e z)0(;X+Gmh5i2|txsB%V#I~g2ar!Rgbxv$J2^XQR%o}G!6%NOUZ)4I=Re}=tJnIePnooam+vqDhNorc1`WU|?)p08AI4bO2Tla|e(HhYvyh3=C`_wISYyV66$i zrpZ3mvN}^AdbQjn9EIRfw(dV~8fZ>5`&beg~7dLj><%L)tTzzemLXFm}RRJoJ`yad6BhH|9%Ck zM~;7FHbaY~C+DVrN2fYjHfUV$xn0EAH+N|`>!(jG(eofb53w85n`wUr;tE9GHP|3i2Zu5Y872;xfKQEYQ5M0H}!vq7y>GECKS6 zm~a)y=>{YS%8v{%`+@l?49o}V1(rWhHW7Ih^dd4JD0|=qB_y zAvX@u(N4as2P%8vWeE}X0@F1-4bd)4D036C|ELvD z3~4WBP4D+|N!HiLkz(OBHgTf1?t|M6g zGcbsIl)Zr0PsoOW*eD)_NTQ^1;>M-o6{lHoR_JVT6|v{pvL;vK_YBmpEQ$!-FrTX@+*bUPJE N58Or|1Diu&@&I;3rf&cM literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410295 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410295 new file mode 100644 index 0000000000000000000000000000000000000000..c2a15d1d09c22c35f5b5f79320b98a73e13c3a06 GIT binary patch literal 4736 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 P{-Q=4qL(A^NCzvn*jgQ1Wpeo_T#;aGHbCzxN zm+8C6sJKe~7~6`D7s>)F-saWbv^+j@OYYw4Z4K+%x3F0*H{AEi<+6ouZ0r3==?qt2 z{m|-2l@9{hl(gtf7Q{vdMi9M1<*dY>xutBrnzN##*DSFU-c~e+V*Bo z1@B-cth){I^hSaSIJxYY9(_zs_}B+EdmTW_xYdsUr-cEkAi5Y}>p%1>|Dp;}`UP zF9ES2;8X^XPGRuzb^y@|8lOG1)O3^Aul3r|H%n+~d0&V5!M?wKdhF5r9xcdUJ)LJZ z|HG}_^6Cpsey-C$mGEp!S^LUATgv~Rv(TMia5Kp~yhp=Cr}yV4hLnGQQ-fj}xMWYy zNo-Men0>AIW(I$`%fsub&sHgQEu8wMsl-Sm@Z@CGQ^r@d!n>3_9N)Wst3mz z)DC1eL#B;$59`d8FN<`}Em4Yan|Y>n+R4{y2Nf#QyidL`Nv;IxVfyu<0Yn1nZHY@dud5okYS!tRCbP|tu2~5h89Ur&Q1T0PIah8KnD!jl;q-(UfP=>w=r_G~HxvL3KTD1pTiMo^xDsUgbz)AtfcFdvq-LH?kn zJ(TzZBUq3GkeIMgf~0de4-`ir0QDbQxeAh_#P1mPBMBfe!7cz1;5-QD;d2{_@ga5L z?E`4MW7vx%fW(Bmf><}f!w{sC=yD9nA8;Fi3~UbBx5@VI-+14ZJdL@hE^Ou0pHUkV zq8nS$rFCz1?DvI&AHn)D^CYrSusjJWzrcWCc?VPlE^kF!;Ci51!4$ILP;r#BOqBUQ zGeL1ftoc~;Bn|AL#2*;Jf+T>%goh-tX$+(n7KiY9l*)Aq#8Dd;?a@05W+J;67A9aZ OD%%UG%dp!E4=4a>9qqLM literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410298 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410298 new file mode 100644 index 0000000000000000000000000000000000000000..717df905de6b899430dac5a3b5ff350bdf98c65f GIT binary patch literal 4080 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410300 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410300 new file mode 100644 index 0000000000000000000000000000000000000000..e8c2d1b8420721e16b6ae891e05f4c66d44a7891 GIT binary patch literal 4148 zcmZQzfPm^UX20(Cr?sIOzpQULGk&c#`SIt@!ri?W|2nUAoo$~0R3-dC_+fmHj84Pv zmz!pZ=Q+$2v~OI`;iI ziNnSRu`4E}H@v!YqPd~#)=W*kE!PX4?M(2G47zvGC9__mQc5jtWXZFX%Ib zfLIW4Dg#KTF!*>ofM`NyP3M`-|8Q%!y!t|upX>BbB|O_w*1q!3mh%7SEOh4=+)OeL z@6j;P>HYbMA?4rS)S#FKF4@y_5?hoVW?w75nZaM~^6+}@!pZHd)aj#? z=b3l%r0E125 zo#G$(l&L(9?^v~`(vEvqJyYff9KP>o5n{DZV#?=z&AveOOaY-m0Y0u^ydO~>OOi8T z*tlqq-chJ6Kz(300Q1M^1??^Q``#4ZYZUNded)5_AisN#Y2k6JEh}>ltCT)6uN0l{ z+j!jZ=kub?ms84JY_F~^ITco^R&^|iC9K3Dj|*rb*uNg^J5pa}=4K=x+UT*zz_B*w z=fbLw^`Nu^10cVG{Yy|kntwBGoO@Vju6$Xfb8d-JgxkzB zt;|BJd)Z3=91-z&nkf{pPA>hT#M?;@*XB5G zt6l!wB;eR#dleq7B&TlO$O(L(OXh7^xIutNJwVMi>2kQEb=vc7_wMil4P-v&6c{vH zqv&11ax-4U9nU zKd2lmU%<-|BI;uX_VpWDpmp+fpgF8iv%nP05+DbO30Hw0Kd^KLwjWnHLZq7%XzV7C z+hO4aPoIOtZ7AgkiEaY5tEdr&=;a7F($gh)nA!jDn5uYkzkWf)hEHXo`N2 zV`Sc{5(}~^Y0;TFh>Z-4Ao^-i=FOyl@1D8rvmd=M6*7DO|Hb7wdTjewZ*-Cu|C25Q zRN^3f(Q(3?HDa-e%%YvbANx4w3*PpRoACYWrSmH~H}BM&`fa=3-f2yHml%3mvv0Po zxNW#Lp7)2@>E1IoF9J1domiUgrAD%O*cHs)>F1s++ho2x#!=s*%|CYL*{x1ZNA)bu z$S+ZmnC!>a%x75_yz`TxM&CqteM|lJlM2%ecyF(}$spSDm-oT8&C63jE@nP{L7%Y! z#DaiR89+LP!N=PHL_b@5Pt9Vdi&yBsIi)+#tTx)5_9VD?+4iqBmTvc-nI8^@S!s*Xf^1c($diedV7m<^RuF=*};=nPeW`qhX@c`|}e+%D=yAxIxX!mnwvkF~7M)Q4UzH&0C$y)CeUFSvzWv9&q$vA7MZ zIZ%nXN7)OoS|Gs)b^|aS_<4s^TZZZgbc{lw^QaHJtl{$Tt z@;viSo-~~x42rN61_80t3=A4KLH5HO0-}=^om~nQ<18pHurf9_F*k)MfT;u1DgJ>^ znabn%j#Yaq?YMW3?kD{T}!T*#aG(9 z^;f>$JC;tvdGBMBfe z;XVcV2N{6MKn7SiqSZ|>zyATM0GmbSd=0jmVdJ7bdPl)bWcR|tgckNf$`kDNGSn<} z72R&Ax7=TvF{gy-h^|;nSk_nL?vI-ftaqce2~`OTQ!tk}H?7}8V>f}^4ht`M z9vdWXqa?gQbp7NMufBDt(%2i90fvSYL-Q1Snn=;#c zVaP+KgL20o0{^IQLMb8_wy>J_bsc%gT(hd!%dzQb?3rImO5Qho zB_sm(#s`CJN?LTL8Db*?BZyw1a#mu`+)_4Q%~{dWYnE6aUZro8X(4Lcqd$A=>D&*- zKqU^pg5_Rb{nyeteSb1n(Hssz?H!`8e&s7vrO&-k!9AOYC1iH=92>K#Tryq{4=w+s zxAmthqp7uyj?@jCTcQS;|CxU^%n>MvcyzBzYQg^L=bbqM9lahbepD+|VH&=^O?1Ac z(45UBe_oeJIxsr?oYgG+yGq&Z$8N!2e($x^pNak6Eyp0*@{jkyw#~~^KrUuJenFpE z0K|fTQyD-yg~7+$0YpDrdr!?`r;AtUzd5Bluoc1KRc-i)^HI{DopP3&FoX#_w z|KZkddG&=RKiBD>N_e)VtbOI5E#?2uS?JC$xS3=g-lJin)BE!iL(0FusX;LfT(YO< zB(^9!%)VB9GlRd}<>B?zXRDOD7EXQBRAM9&cyhApDdVeJ;ay4|kJ+OiZ-cmrfq_t5 z0oAbpF%w9=;cG|c85p)g_GM^snbU(&ol4jNz)0!hCn4L3<6@O85lHf z0omYq1L*^Sq(x^}0V$9eXF+j+m9epjIYQ$Fu&_64eE3J47f@Nos}1rzC~t|eE@;w$al`YT^=aukQ#sS=CVQ;xzT2WrEK~r>@R;DcYtJEIM!nMX-$d88rk6odS zhd0a%2)tu$zES#|nD-LD?9Cmi`+Qs5_AULt?`EHdAT!8VkfAM-o}8Qh9i8fA*`RT~ z=XMce-`u6)te-xaaYb!7w*TdE2FA7pz_jrVs1&3J20;D=`;+gYNFUe@!2Gd0_1uF; z;+YN-Q7>Z~ZvB2Yaf$Wp)Ebr5sc+^5gnyePpT_FElk?#m<8zBOf9Ut=uj%=*{FF+c zzzyT6jkE5o-~^cm_AkLW0{NGLaVrDU_g0YoAdN5p%-0KmY?eEI&SEA9{O&!ucu`WX zN;*fTSjD#GW2XD1T{T{A3NsvlYT)5R&_1XgFKsWEzX`C_4Cky%X53+~X?tw)GpnU* zj?WNya{m6Zj2l2RnH)nrU4noN7$73;NEFq8k~0`ELerqbamNVtyN6X9b@dkoe zaq}GgStXAO7yLW2v0`a$^DFa)umjrXzpI=&fA&L94UhQ3m9iK4S@cbQ%KCR1|4HHk zng$Nbj&EJJU37F!&Ei;0+$L;kU*jfcl6mCFA+b{@-CM$#_@QYBl!npM4#Y)3(&5{= zU9%chL-VgKb2fi0!kHCSC(QFlZ)e)x{Zm{0#REYKWzVL<)H8z31!^-r=J|Ww7ooe+ z>vr)SzjiB1%ITes!I{Nv;?It^#zfp|zkXF=L%wu{F!Sre4H^f}^-oJ?`KoogB;k8r zQ}SxoxBNf@S$rF&AMk9vcem4%vMHUf!*#Dtj+33oUT7OqhHf$0+#P>i7R z9Hx#qH>v)iv711Cg@xB(a~mb$1xni}afQS|V!~nrXB>jV1g-4=$v2R=1e--fT#{1O z!W;!FbCKN(O4sDZ6Yau;GB<(J9~>Zg7KsU$Mo|kCMUD&PHXg&7CD)8k<*k_h2R3bviSf=Wal6?v2O8hl(kLv9 zVPXCks+O2GH3R$l^?RW0>YG4)tT4SG8Yy8SG2tq3rBSf`z%*J1Rf!VjM6~lkZi=9> zo3N(QLE<(_!VA=xq(&TKO{47cXF2x0@kdv!)Zj2Zy2rj4 z8sF&cF?fOjGGJ*G-X0^^Mg{gacy52|0oHY(vJXAYk()-r_Ty@g5$Prk8oLQ=8XY8V zLrJ3~x(QNGBc%$Y!Uc&5SAZVpxaxV3UXUD0yJ*n34XIs3gqy_P^G$@MFIt5c19JP3 S=zboOKj1M0WMGRpm^=V7RGQ8J literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410303 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410303 new file mode 100644 index 0000000000000000000000000000000000000000..4e4b63d9f24353c9cf47315a6ef0178b47812cb9 GIT binary patch literal 6076 zcmd5=3pkY98ve&+qawNF5*5XvV!9e;(;ksqXk3OKHb)sH6-utTgwjFwsYFhxEoDeZ zM^n;6ITejdxoo$zCt{SQX=`l`up1$W{eQW*STHpJ9-}|q%{y`9) zWLJpo;_TY|i{Ny<(|F^H-q75OOQN5@vmc=kB$sa=8haDj7!*;&icGU1U$;=^98U7}(uAJ2 zW!A*=JR&66vfZTreJeuCw&@PlJgbv^j5F6cmSL`PkA9#mMx?vIT-6yKY)Zk`Jqo=0zt!H{x5&fh`i;g{fYf!75TIfRMG`AWHuQ)>C=5qF4 znMa|DpS)V3P||oe+OEmv?CsTocHtYfa(=z_Dru!>`l7af&iy{aabKO}Pme<0jqe_c z8{D#gU{K{@or$)m&q^)RT?*$C5&1*ok{#EJvY8OEaAh4?R0L`~sqzLb6S3YzgKbYf zS5yC{sTJcxdf?5P%d3mFcRK{IZoCaQFniiVDYrW{=gizrcH*MHonJd#K(6xWxfXQf z(&)9|@rU|4Kl`_Bqc|VcTDu}?_!YwZFy>|d

~)#+OgEuo!h4rue98*JLy;?;KW6`GIXh0WM%9|8WzGre7F}YlUDtV-5M-)a0XtFfCj{;NwT>Tla~Nl3D2bVT z_a;!|`#QBB6Jk4L+!mzO{9$`P2$+D#oj3@hb^+*U-QYYZpq|-04Ih^TeG+36Z&X~P zulS&nss~$EMLJq4$eJdzUVn+v>ZdkGTa!wkP;`J*ndu98b7(C94ibGMYKqiY%4|23 zKfY&_?Sfq1w?VsJC3mZ5=pCN~$G#(m8~!oG^NwPIF-`~1w0-b%M9Yb=Oy85&(9@!L zkXB^o9PUm(rJ2AwOAozuZI5vI$}$VG&Le|7QR3^m@76**>dE6x<5!{x}&BlDb|Z;GDSqJm1Quz*4Tg)8H{r6)MZE z{P>{Rqm-PAvxRaS$l87LJnqffL&oD}ZnpbPDA--9;((=kF}KN*`7{0QQ(JvUYMJ86 z*JZIbXuJrCjR+0KLH`&K*aH4#f=1+tt;zhgcWk-c_4P`+VGDMv1S~Z=G_cdWp(NbO zY;hV5*x>n@(jVW)bEC%Ke!7vivqYpPsrUzNql)AnYF1ulnsoQ0r!4PQu%?i)qot`m zP%zn1)$AE^S}HJ!J>sXE>>_SfwO{}DU~g&tH3iH5b)oq+uv6R@{unbSq49xSF4FH~ zD#x)@b*x?s9eHqGEB^Iny;8lsx!&bwa~Rsr!`ue~#`4z5g6Em7!Kuc84 zvuoK`b~otNNPz`W?!>ZBT$n4Lpx86!cfC=!^gLatqq)i9YVpe9fmdA`a}xR783Fdf z7zohfxtVhQN8Gu{PRY2;a#-BZ>t$WxyQ8J$gf_*)$EwQTr;cVY77uYv;0}rb#H%bS zW9$@@*BN|Ka*LqIX$ z5acgmLe2%mnUW;3%9+e1@(iK)&!hl>p}=h66hviFo1Oi|-`@_=UX2I5cxP`7%rs za!&}BpkgWc?7Q~KUh-b-*(Hm()tpZ30K9iRr>us^O#@6@6k=Ep;O=%BVwr-mLDdUw*ZT zCd;U{j$d}ok~5TYJIb%OAonqhvxoq{Lm;ZPUI;kr@HsdGfiej(C*)7C^_l6@SbtLP z%z&RSiQ|*B5C#Y;o=F%JQLyvoV;;b{#AoPQ0>qBrc?9?!3Bxhvoksw_fIE0?KZ%<# eKu|IFpWWX+%?V+D`#&&E@J;9t)M;bFC;S(-vdDx0 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410304 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410304 new file mode 100644 index 0000000000000000000000000000000000000000..1c8773292b757f2661a6bf78c7f0511d767c5e40 GIT binary patch literal 6376 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410306 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410306 new file mode 100644 index 0000000000000000000000000000000000000000..94cfc57359a85e1fc2c91ac11e94f1dd98a150b7 GIT binary patch literal 6692 zcmd5=3piC-8{P*;m4!mPE=-g~w*zwIyo(|(?F*4k@*-}>I~{l0bC3qgdB zMCmk{zLa$Z(<|d`^CZplwPzR)Fp-MikC`-I?%U@i0i@&`m(=#JE_q|y@yj7W&B|q) zF9Zt84vUw%^eSh&=f5gu6?8sI@5q0*xZ_*C@*O^FR@3ejdG)Q$XE|X8=hIA;T%e^* z$))i^iwF&tKdY4~-1sp-{40%w_3JODnBG3GtMAE}W*)73GW)?cfdLRvfYZi9(ze8@#bDncU@vqq1qPfz5H^Cne>vjIJrg(?{4~*WL}Yr zfr?t70^K>(r2YO}`84`M*T|x)8n)hx%wDf-^@^;JTl--3dD~D~!{aj*w+gh9id&i! z3@YOElsGS*GrpT^$ZUGewM?)=rbURzyvWH+V?o5C_a5m=OT(J*;dz0Qg;=j)!m=s7 zZayP-#cHR)#DLt=3;LPv4Qu^UuXP14S;=oQF1C-8J~^pA`v;W;Nrt_bba}2#S%Le` zy~_$3Drab)_O01u>>RCbsCBUS4Z<26aC2}8ov8F6abx&g=E-Wm8jndqE9#%PHJw*I zkQCb<=C421o_lN_&!9@v`GD$|FDI;jnIpu6Afv?!$kD-11j+|D#kh$xxOKPlXhY5QYtBVDOcMe~pBo*YNrxc#DFJk*!${G}4B z!ecXXqqj+TW(jf?H(pMORJ8I-x+3Q{`}cdD2rcpqGA;a1ATNetfCb^#sQL&3V^4Ej zqmQb4nk|o}bI!eDYn7*{e!Y@Y8gYW#HotiGF84Lu7Q@ZEP$BSVn!tlqXlEj@;a6P3rX5FCuEk09behu}cskeK8I`ormGtFBkGGi7-5wazh^s|q`_7o;V3Z&Ee{ zvNOI6Y{ASRG!z5y_o-=1{LzRl|4FrQF;3Arxp7aeRqeypsFcUgncI2x3L1)hmYEfTe}MD<#H1M?!)N;EP%98!1ISwJ39{487hMv$nTGk@V@)PMUAz}Rx#1x{)#E7` zQ^dm2%Ekdy@MjCJdh^Z%*?@!DecN@8t&>>E^JlaLc}!*)T+-y6t?qjH@b3cdz%TU6ZimOwW@DX9kK@9Y_J#sgBUc+`6 zUj(-ga{d_l4t2rahSe!zO20qOm>?dUug?wJ$M6N`%cqDT$_eQlg@#Yr{^S=E95ZI9 zx0LWU@|U^BC?DM5sp-AAymiXdixGp@uo!QVc_{*SJ$`@uLe z*jQ*m4D<!udku0UiEKuno)6F=B%I_`*Oxof}SMqZBkEoW5C7wQ{vo_j~P&lIjLG)ZsoKdKr)37}R zo-J_uLZd{y!w2vNDmZ36>QKPWo5UWA>yr$Ss(7#PY(u;eYq-!p;5G#78y0VBnA)?( z8Pf;n$7h1=WB7vK48(kg{F4Gls$!9OPZqS|`|$ho1M0%P0iF?v@yE#5_+k4vOhTVA zN=PK3RqVS6>mAN5$_MPh?330Lz*BfX=t0Mzz|&aP0TD;31_Zg9d_(6BG)@jNs0*?D zzwDCe0txc>`4$C+VL|hS0tZ0nldp+LrQ`C*Ds}A{+}GlLr@BJ78!PBU`{!*twP2Dt zGW)){s}SgjH66Vp0Da)QusIQvfquaJ$6hJiOW@y?BppH-RECi!?7-uPzKweCf#ah- zKJ)?i1C0wbG!Aw?#*Y!SkQzu;lp^>?_<^1o|LBrOtG5S8jKB<9J#gI_PM)*Jh z4_sS%a0{c|V@anL#0*$9!te4c;5P!}hz2M_T2CpApJsWpEM^SjBUggiJ&&~yzq*~0 zv8U8breCpg=bi&gzaKo1uW4~WMJ^rljfIssM0lrzKX?w~eF%j+Aw;Q+m59w>s7L6+ zK}j#9DsJu{a}1RQ>;4D7S1Dp*j58+470wrtXP*hSVYxO&OwgBMu_C>Ys-wmb<%G0% zLtXM3f{sN28?lE|xNpQk;rEVYyu;Qf3U*;$<2f{boRHRp{{a(`chGT?v!p6c$v@7u J_v1t;`~{ckG!Os) literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410307 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410307 new file mode 100644 index 0000000000000000000000000000000000000000..28c6d05d2a6b118fd8059eea35a3a4294ee82398 GIT binary patch literal 5688 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 s4b%7 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410309 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410309 new file mode 100644 index 0000000000000000000000000000000000000000..884a3c530b065a5c2881d1302381aaede943f0c0 GIT binary patch literal 2428 zcmZQzfB@xnl8Mh);<-|GzurAbV$w9}`yBk$g-_?y=-gXp{N&?Ppeo_Np3jRX?Qh&&lv_`g>?ZtK@vEEW6g zALDT?hLbiRo01ltSpl(;fe}PsEy}!^6!6_Mmwoo57p6jH@BhEJJV%dh|LToS^5TEe zWq?W?7M<ITvarwKEm9Oc&Z+ZLextackCug>#tyor#^Uzq5~d zcR=>ONgIq5p7^*=kvWC2c-O85qd7&Rx_NlE}3zMy{7H4$%Q9{N)iXJU_(lf73-Se%;GEU-TEtEZ*mle+o=+Z*X0=k7RKweW-h+= z8KhLAK&k;m0|6t%oeT~ececFilQ+!&E4JwEoA?(y|HZSOiHZJh;U1T@;i>4xyanCI zSGqdqPkXuRr^u#Iu4fV5%XsES?VGelrm-g5#Ge^x7ARa83|~7k&)A@NW*UoX_A8Zd zDV*HSN}WDRd7gPEPnu2;HUuh3VGs~I&A_1X5XeRgld~6~Vw|8bF*Y_fF#rlc#o%;` zf8bN5@;JU@)t*W_?p^gvnICZYzMn;i)jo+SpZ7KU0u?d^ga!rpxPr7m05RdhB*u3D zs+?we7+u{s8kl7@a%&Btd6xi6v|CSGz$NeW&D2c8&w|rYr2IJw~3$I zFWZZAGE|!pg~v{`ew{`{FUXYP_4!80GoOw=v5 z+B=IGSC9tQS%eAeub?LF@YsD>?`1+^bv!t=U2Ghpv24r<9M)=w%RXW#fIYZS(RJkc*j*U(gq9 z0kI(9R1T0%Ves*G0MXCZ-cz&K>EadoZ%*mXGpmg@r#%TSUbg*fjiuZDXXXb3r}NC_ zf4H?hw{{^UOPW(sY8bAy7#QgMip+1_q5s zKsGquK>9!+Y0=rMKnf(rSx{VHWefzCAPIyzu=*7Lz^6>*aeT+BJ(YIcyXu)TKj83v zKZ_8neG*eX?`!r2s%Hua4GQpa1?vS9>8GwGSIpuo?cMq-UvF|0huf(Vi`V5D0v5*W zv}P{8_8F{b#Lw5c3TF`Dg-{GkA>2>E zOv2`al`)ywfs6;6&lno);tb+&$j)=vlGQrNZhqA1ZwqR!9^ZS5Lwx@{E9Dif<@b%e z+(Bx9fGT#nhIoJtQ~Oi(uS~tvOl$R@O-56ix~!tt|Ga*Ban=^szE=v{gfD>=QOV9o zrwFid6Vn{tPx-f_ZcpTa*uZ~h+LVq<)L%HqqM*|}Ev)c{^Kq!1jF9+Xa5%|i9Cq%d zm%gUrg!F0tpCpP*fSCLsQ6SX-RnG`RoU@#^NpC^%X?-&aFQ^WS!1L3#qd&_`I_C{sX|80X@z~^vaF%*oIvxK zd(X?Sl6TZgepyn&%C52Cv`!||RH6Y4}tpNdO>EuEJ0<0WWWF%k1!69hU7!2{lGX{0hMC}mG3Zh zM3jXfH(AiwO(3_!!fUX(jgs(s1oRg*;t(7rNNovl^ey<#si0-eQ*_cUXNQ)^jJ~2N zxvSKIdXk;QdwH|UT0cSJm{A9sM&ac$QX1vO!G^^JOb-JwW%uIL@0rm0p$=#_C?CNA znQ0enKhWQEp$4JE1#xcrW=mr?VNJV(#BG#>7pM}ZMjT>IyAP~#7%kKDzA&8nsrE!# zIcz4A_5B5ThiB9Uu}-@CsM;PH-`L96Ur;$%8inUC!gUyfxQx%90%$$91ZWNq)GRPX mW*PMgQ6g*ZBR3*&q+Amn!Qt{(c zFl%;FN}7y*5T{Vd{G%EBpR4^%($tQ1ey^Qj_HcsF;@z)ewlCmpFaM$zt(nxayC{0r zonJz+dqFlOEjqIaVj}}1h~642uxWR#hTqi#R_;q5Zk=$FKfF2AUFJwY^}Z&SOE%p= zB@Xt<4?`Yk_|CuK{=~$Ke?_|Z*O;JxkB;n6Tr-c)Hb-Xf`h&51mx&esbhS|o*sy}h zk?V!x3*!s3S)Zqb))nt$V>}(98TEeA_B$(D>b(WGabCV0cS8ATQ~7C==!(r-Qg2Pz zzwrM~zxPjjB(&>psORZuM4#pfdsEbKBbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}41XtQ`eF!X7QEwZvB<7H#v&K?No`y>+%c%3*&WK zGZ$a`3{oahAk_d>&IoZQgG1P>6#^R=_>4LWf6 zwX-cqUca$5TKMNBX(waz^ZhsGS_*A@5_zSfWV^^$W}tbD-6t3yiypbLqsBJ%oew3H`9SPc7x)hZAoVae0J*=QY*4r|1Jex~lutw$Glr2;HwZxO2gcD_s2n3GEkoHvx@pQE8oLSPc35}~Hn&j{ UUZA`}jW`5{2~xQUi#{+70EVfcbN~PV literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410312 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410312 new file mode 100644 index 0000000000000000000000000000000000000000..c56b241904a673d288dcf37130360093ea2c895e GIT binary patch literal 1472 zcmZQzfPjDDZ#K)W`L%SXZbQnoo6J3(|MoYm{;)k(``KCfGdBY(fvSW*9FUtQWuMWs zK!{T@>*mWHHM@dEx1?7sT@=1OtWoe-{oN$(NAsCXwiVd^xT@t4s5I-Otze|JT-Ow? zkGIwH-YHei;mm zVauWVK-$0n7@rq`43JqI1;qtch9(B4P(DZ=9LFzhFPFawu+Z1OX! zrE8AQ5O{L_{<4f4Ky^%xA-<78ARSPUe(G9s#Vo$k-mSm#^(IGgxScAocwL?$U}3yY zYv$r>pFzqb3ZxpK${8Wj?U|J_uQR&XDM&a{byg!&o?h`c+U+qkMYFZGwcr*`kU0x(v6c} zFn5c5@b=FWvg4Lr(ofA|uTD&a*r_h?9|%C|VQv6&e?i%xaAgLj8+It4h%jalm+_hM z2O7@Hfckh~dOzH!1vnnzJdoWW0JR?&N1LE>jG(j(WfSS9+&46K6Ugnb@EUAx Wqa?gQd4(Er2o4jZauXJPU>X2WT&zU^ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410313 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410313 new file mode 100644 index 0000000000000000000000000000000000000000..2f97426227a57e031390eba64f1aeb85fbd68687 GIT binary patch literal 520 zcmZQzfPnvQ>-cBv-Z>-KfBhe(q?IOD%MbMiZJD?@S1~3zXO*@JP?hk%@Hd-f*Zf+# zQ@0`I+D+!3&VTzGR)5$YtNrY({F$49m7lLnS6&lvcw4Ks;psK%6OMCN8Zd=SX#ebS zp+d>mtL+rXrldt@_Cai9UBFrPPV$F0hq}ug38>!J#B#}| z8>qxVd&bS_M;4ZD(q4D@?NhDEGyZ6rfA71a#`H*JSKR6PIgL$U_MaAKU38XP@Amf2 zKP_TUAN@%Z=F2YMe)dL1PYyJ5N+k;eXwow@)VGZnU7!4 zk6!{}LBOdTAf3YC zna%%jYqz}mLX)5C^iL%`+fvrP^3Rs?|K}`p=NH^eG7s<3FwyD#`H3Or-`~`rm-cBv-Z>-K zfBhe(q?IOD%MbMiZJD?@S1~3zXO*@}#oGfila4QFU1YFzZ-4AbM-8z^2``8h%#~Sh+8KxOKuw{_y5dcbOvr)%%)QF4=Sg zl{l2Qt}0jEeBfu{mG8;S8d!T)YpqZC;)NaxwGq3;GEh zAQl9i$^p_T3_jitAo|(bdukRtUA#j7%_-e^X0_4gv?syE%eH^5v2?rt%=}>Bbe`G# z54U#9t1mS9xlaF7!m}-9?JNImDgS@YLU(?_%_Q^i9t{(n-k+ZsQvUr-4T@>tl07{q zu|?To_O;@h8T{og53i>_Tcy;saO#_;5+jkolap0X8DG^3?^5!3%pU!C8^lcv420qe zs16kVOd$0I8WUS%ga0pEAj$5`_N7$S`hsFiSMvj*Sm_yO-ufLa11bUfJnSR`gW?h( z8ys&SeINjg&qojzT$~rkXAJd*h;pdD*wG*?!X1nOu3)7MXFP?{Y(yvK{@EJEu)4@e^PDC9?B*u#oLHN#&D#$3SIU1N0I7%ad4b%2P&Oz|nHfY|K=NQfM7WB3WS0Gb#_?jH zCT6HsFa@&&$U$PlRe;kFoCnhjwI7&1I-zompnL>VN2Hr((AZ5Nw?o5g(7BD0@B-yi QD#RfqOpwZKSoDEu09c~O)c^nh literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410315 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410315 new file mode 100644 index 0000000000000000000000000000000000000000..166968ec0de3c4931acb5e84ed9f60017018f0ce GIT binary patch literal 1728 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 Tp!`COI0T0YQrQWMJ}?ac?kJsL literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410318 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410318 new file mode 100644 index 0000000000000000000000000000000000000000..d51a3f646882f4ebd9cfbf7d9d26bc1b4acf47a3 GIT binary patch literal 4892 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 BU?2bh literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410320 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410320 new file mode 100644 index 0000000000000000000000000000000000000000..dd8d016d948bb5b4c0a5826c237b30541ddcc245 GIT binary patch literal 4884 zcmZQzfPg!x`fhU*HI>%2X@B`6+OjU=t|-getozcplnnV>b2_#IRS7RLIntVvzauBQ z;^9OKfxMIo!$)!!)iN?N;lU7`DT!A6grQ=i*}Wc*6;QatYTW1Uf4 z_;z5h0Nane{p$^9^3xa8=d6E|D?+R zl{l@%%%O!-fKe^|ET$nsrJ zZd7tQJkqh+Um~oac{41xo8hC}?&MS3xfmZp%1>|Dp;}`S` zEkG;?IF$pWQy6@_9YFN6wfEF4cDi_l{+m;}^UP|a&1p}9i{lw^QaHJtl{$Tt@;viSo-~~xYzS16!XO}ant?&% zIgkyGH;_IMNLqA`6G(x?I17pktc;CK%t0D}0HzL1r}zgxWh#&3J67$fwBz1Y&y@KA zhwuAYgjns9nDTjFvoBCRQ$T1?fR8I!FPKO_buGDK7GG)a)?fL0lcPA?PL)`^F3%9K zFkYuMbMdv$AVo|`Q7{dRP`5fLeL7kkvpOnIz@$%S7V9=cg_ovWT~X~muRe)rWOnT; z^X^D<;>Cig@k=M@^FOAmE(m|I%+~Prrd`iwdTm_4sp;cu zW3CSm%0+!Ma~}R!{b*;f5YH`*C}#fVZ?etmVOuOt=Dpset+vTl!DRDuR-k!cKeP)a z-R0s|S!A@YXo=&5aP4pMG93J0KdU`De4{RTedTAcAGj6Q0M$a5Qfa=6O%3i>fGlAU#^!Hs)k!68vx_Uh~EIle(w$}OZa`w3O zcTciiu+_PBzhs%;bW1r_vw1hJe_J8p^0D5S^Ol#JaN5*~-wZRIeX%^p=Hl_|8-PY0Yt`6fg2=iRK$#n2@wsvE$?dF+qu+PLfJ`8%5n(8_F+uG%XHT z@N9X!f9Eu{?Q6Jz4g-fD!F0>mwg4D3TN#+X_X72xgdec1`vVkXx#Q<7W^%yq-jj!vU%fte2pDP&;1QUM_zVV5=Fb$d6c zoMDMwclUQa??R>8_mjnE@9;2q{%@jt5S#CcB%3FqhkxF5gIWqMVS#LvaD>W%;)9t% zH1`ga&p)S#5d7pFp?ga+d(uI_cJ`}{ryXN zpN#bR1M_1p_8soh+&Q~Hfw}yz#ZzUL#SZ)rp!!q(0|Al|j6m)$s2nU^g6d5$Afn6@ z)97qh0=fuP&h`K`@j|tNDI|9yG2tq3r2(-0z%+0dsuCq`h;vihJ{r3T__Z$FeVf(+d2z!Li|CN5+?1?ce4lwO>m<(X+Z9 z8sFH`=s&0&EX?6)l!!Jm1N-__E1>P4mj9;AK0Be#fvMNdSpShTD+Z1jM*W>QadeG~O}nMG`<_lI$jwwhht! QY9xQaZ3Hr~IRqvT0Le9fVE_OC literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410321 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410321 new file mode 100644 index 0000000000000000000000000000000000000000..9be013ba346500d092b18a5b0735035fe4a4cb2d GIT binary patch literal 6140 zcmd5=3pkY78~ab zEA_ZnxvX*{({1lh|Jk7tMBB7$D6t_SB`7TY1WkeHCkwP>+kJE?r@b{*3!<058j*`E z@O>yJnor{?x2ft$NBPHGX2qMv-t{Y8`FVhsQs}JySsJ0Y3WK;!feY3}#$RyqZ-$<> zWYs+$^N0{&pR=@HvF!BjPaHHEo}TCax3VfvSKrS>&isgON^$MZ7Y0CuENfXblFa3> zyLsK$8Hs~$^zL5&Wsylsie@om^?Z_^+KS|2I~zL(mMrJ{4JX|Q^wZ~t16(bK z2iT{EiC%>x_NDX94nLqBU@%{ov#t!6=*IuzXZ40+6VLKa_U6t{{gSo5H@YQW{Ha>g z@Nni}e^Ka3lZ01YrTI*%hbcHH>CWQ#&($*xhYECSyn2d459f{) z(cj)S(M}J%zs1=7h=!ro(V>?Jb@cx}>oy;lv#2)JJ8}s*r6u^juQc7Vv(=|3Pc<$x z>FL1`{ZC!k32HS4%_R3YRhzC^o?Vy5Fd@ifxdM7(;ExFP_4-zeocd{TU9za!j^3q% zRB;Il)mVGgZ`XdyW`A*9Ru8C95gGYf1kv~n$Y|c+JlM8m-IfL&d?y|l9B5`>U}$6t z2maPW^{GxSkBIbdq;7tP^>&82}B=+{P zojWZYsI5+?J^{ASHB1_JLUaajTZ|C@JO=%PKSA4)RS%y<+Z`?B4J#eF-HnY(I{TFU z7X~jikNm^Wx~X)(Ek)%N8R$Sgbnc}7_ro(U`+@D06XNVz!%3jB0jiy6)_Y?sP)h!*wVRq2~*2Q&6JSO45LTcIM&Y)MWOou_YW z4Rt`97N1tGHmI>>Bl|`})pr|_&W>EvPdpfB=6QaowU$XMvO3nQa!IxcRhlNB9&0wD z=1y!QKTjMj2`lrc{Mu!-E?Tu(F}&fnsEXF1UG`ls)qdRYGs7iTkH7IxR5e#60_pP(thMZR+-KX zR2BXZPz<1M#BDFO?=E*z@fgaFMA9PWy7V~@oNW)6IqP_Ke|Uyyr)lAHM`tn5sQAk_ z=6|z!KrXL5*J!yqySA7Tnctrs1u=+8M(Ef+__4Yo;J~eRy+dio`r4Gq_HT!aF54k35V&4#|&eF*s*whY}h^|UJ+nj(~(0oCb;t?nm&yr zjwM^6lyjn`gW%=UqMn0159eiCQNrfZ?;Ng{I?m5AmCs#_GX(JbhChbI9PaCA6Jk%3 z*NBZS2W*7<=ut2UtF!2Q%n5kFiRcjg`6rH|{=xcw3I~FeiM?Vs5 z9}+Kk7MhM6PRLQ2TiQ3DOQ>5(aI6@=-%RsL5f8ZH-S^tqUe@E`acSr8`1wZv+W<1g z8CaXKhQ(aKQ6cshWIgd06R5(n*B{Pg^LXLhU z*xupj6fup@2|jUC$>Dg8!ns&};TUR52yDWiT`(s7eTKo~h35l7EfK;m;I)85GZ&K& z!-QYobma-LeiYJF*h6afzr0c7?dY`?#VnGwL)U35iF(m9Ijl{$`C*=Dufxu1FkUd; zf;u9^T>vuce4Zu$J^%w3nG6f{p(L*rS|_ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410322 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410322 new file mode 100644 index 0000000000000000000000000000000000000000..a27642a4367d01c08bea87be95b9955a317b9079 GIT binary patch literal 4628 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 V2`^B4oEmWm4iluh5+3Ow1_1bCZ?XUY literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410325 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410325 new file mode 100644 index 0000000000000000000000000000000000000000..26e0997a8d17472f44492f56854ff512ef8e94dd GIT binary patch literal 5032 zcmZQzfPnq$?nUjLnc{wG+4cIQ(`Bqrn`XJMTi-ra^g!&Mh2E7{fvSWz{_R^g!RKj?^*ixYdKH0 z*%f)d>dzXGO-YN+{DIiWzzCwZ#tLlOU8~`D^?;T8(uZ3ooa7I04t19~5>UObiRF?_ zH&BT~*fdU^w_oHFHTRcJ-EJTh{Jgey?a%X$my(vO-*x1{!~O$SAq@WZ3V9niH;EVq zNMC$0;k&qTT#OFujI`gqn|E>Wc#5$Wg`~@CDAq4D{k7II@_63Q6PL_v4=jJV&+G7- zOTG!a{>Qw&-!kLG+@5k1*QC`^@}E;oj(AE(Y@3yF@`e?IXsamigKe9ar+{3{eEfob z@fRU@y!hWa+inKQ=hF;>RLGUO;d@HNZ`rIs;7*vYK3n%p^>%CJD*kCIzMXt;v$tO7h1o4n-O?6JYm~~mb$s{!$i+7p_?;YpVITlg z57Gk!;P`_u85n~8Cwt0I=95TM&2r;DmUFr1N~M^tVbAOdMV4$GlfET`)XAPrg(+tQ zn+uFv_5B8p3pZDu7O;^qm^EoabJo@FoXX0Y8qxi+OLp&2kKd#A*4)y<@^qZd3#o&4 zQ#20lJHp`N_sjL`zQ{>ZuM1g$27>)C*F}%rMJD7|{Q8xL{%tLb{dYX4Zd!-wj!ja` z3@RLJra}E63{wwv2*?j$e-PBqz`zDl8{%yQ)~d(vd)HR@z@q@IyR7?^;*>wkwQ{Y# z-nj2*2BrSd-rMkw(9I!%Fyz+Ucg=CFvvi#e+^$dGSAqc zcxD=lYW6FYZz-JI&Pts=N_n1nCr_GA5HsCA2F5@Es2H41@eh2;R3687tlCp)$GxkbDf0sk-}kc!vDzmw<@3H~U!X#!fY6`- zA6JkT2uMG5ExBSAUuo~wU-^2Iqd447l~}wk&k(RMUZ*v4@wLxjRWqhFK$SB>-RjW1 z^X3|+?k4_pcFq4{Q(v3j5Wc_Rx^T4g#l=@cep}6(ba1}#N5SM^qv_js|I-qZ+92NE zVz?u7xyoXt)X#@x7s1m(*Rd7&Rx_NlE}3zMy{7H4$%Q9{N)iXJU_(lc+88ARhm`D^zH9*xfLfpyVu4AvwBEH=QOIC>X8*-XMDW{0A z7nrW$X^3`Vf|S3>a1*lskTV~OI5b$${eT=7$bCD89h_E~i?@5birjbLpY^Tv(^1ug zuz*JG%#VBLEH7)cx(qchf}^4#FtubC9?Vr5qvAO`t|BHR2Gx-2smDIKNl%>l+vT ziR`!_y1Vn+_OIvdm%ZI%*6_UJ=831eY4jIV4i@GdP(B0U{s)7&j8E{8gYm< IjlyCc0J4>EGXMYp literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410326 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410326 new file mode 100644 index 0000000000000000000000000000000000000000..a09965dc8d64f9d19681d88d6e338bc30c3f7a44 GIT binary patch literal 5648 zcmZQzfPlWeZ(Y6^R2_KveX+w-Rh?U3;~RsoGTk=P(zX+^TWI_js7iSMx_eQ3XQsHH zT6VoY>2w+E)23PO>(;kV6+IBUXQ6lH)tSfYV(+g{UOe$kuAmg(6W+-UQy5~OC+KWm z5S#aK9;Fc;=b&0y{R$jySc~<2M{pAmq_U&@8?>ebFm*K+E)@iS*>Yjw82gG=<|Fvzk zdFIiS*`B% zUiLS0=EG@Cw^g&kokO_ZSUd8E?>K95@bBHJx|Xja^ENSvwul#E|muZ)#9X1DEXS zIf*UG4zsTn-^}1IcX@a{_1P+=u7y+IG?f^M1fHC%ddm2!R(O|^$7A;B$J-!oVqhQ? zS3q^FKn#ux!`F_?Gd3umnZ}};{YvFq3MaR-Qm2nno@d_4lcp1d4MF~55D+`fz@YIK z$Ogw7kOqUKMd!4D43HRSL2-eVv9Ym<2}BB}4os)`2R>ygkK;R5?Wwfm-c`?(`2mOT z`&ooo?UR`Dd0(?HPzzH)Xi$KUD?~GhOh0ukxndSyY46rw`FfM1INVN^SiCOJ5U?;_ zr!{l&wa*|m%2)CXfKC3nA)^d2b6lhFrjSc?4Y=I=ZGuxL^ zRqG3iF}ZgbP1q_dBhbiK=Hnn_XQ6Csbl*RMSE zZ);iXzvDS|(>hFdY?5MTP~liJ4WvHA+X$qdfuLJ}ZVuso0#*wo7{TrXmSv(X3{E9i zRTLYpO}p*$l)K*VpnQhTY>#gz+4Kq`GxBHgJ(TBApPQM#Z>lt7i{iv1o9>>PE%&)h z?e*uKA>6Pu;`lhI>db9WF)FovxJ*$;CFhz6!j8K@WsD9;+27??u&AZf7Lm$sM7-vrodhI7^>Gw!h0v^_TY znbp!Y$7cvUIe&jy#too4CdUxp$RLmoC?F=kN)$*nK$SB>+{xf@N#^b6qg@iFn@a5p zQ_Nz{U0YVATewqBXIsX-q>?|XY8u^s)!wd*`%X=NZj>#WxKQHA%gaGej-y?5u2hQY8urYdP-My0G3i?}G<}Ey)gy-oGMnL$ z+pk+jU)fIUynWi5mRvsni@fdKzq|I&c~$;;Q{m$zDUjQletl>F(LlfmHWwJLx38Zt zX12?fYW#g){m1GY(XuBqy#x$@O5cc8C}j|5W&fpWFuiEslhh5DxK4cSf6rx@?x|nz zyW)$>&5hNod9}HLX0dwc-<)LWebV4!n6Hk}4;}4=HS?p+$rygMa(TLS?%qis!8U@+ z954&!ZWs*`1cd`LG!K9k5s^+I{(I>hBd9)vsUgyQ8DuxX^nz$C=ELF> z_wqx&OO`t%B0t6d+Y5(kE*7g-?Ze!rWaG>GxnA#syVNOuZ@Yky$naT8A$I>Vw1!)BvP_L1My8hm<*R9!#%jE7<;| zMQ31bL|B-Dl@d{xgWLpcLV?>Av~m;HJT^$&MoD;q+F{g)LvXo+lrObG?#-h%o_Ab_4m8G&i^7eor4MnQQS3<%fb4B|3A zxgF5Dd<9Sw4^%6dLQ0rOOt=bMU>f}cRf!VjM7l|Z#%{uzMhA)8C(d+Y^glwN$$ENFbEz~ddtK%!vfC_If4 zY*zs_%braI)n{-$NamqbFi>%n@FULrlsP1rk2USkz#dBcfstR41dy2Uki?aj(bEA) zFQ^PeX+M$ZcMSWH1dy0yxDBaIM~s`KE|q}Vj-YgfVK0&Z5|d;%!P{XVokX|)k^BL- z5y-&ikX_EI0ss0F!&23lu4nE!&zbf$cK5X^hJTh1MD=<6mlQztW2-mN+cxn0MZ}m5 z1N-_F0nl-qH$XdBakvwklX0aru>H8|4dUFi{0)uWgf;IE61Sn07bLprEtTRBy}Uq9 G77PGOi7g!f literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410327 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410327 new file mode 100644 index 0000000000000000000000000000000000000000..9f1ccd3ec30a627c95dc0fba75b99ab2eabfcbaa GIT binary patch literal 7596 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 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410329 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410329 new file mode 100644 index 0000000000000000000000000000000000000000..5a9e0f15d510beb5bd3009ff2f4271ad334f077d GIT binary patch literal 4244 zcmZQzfB?aS)PL_zi3zjM33^%I0~(ZX7cg&8$AF zw9d=ytOUrWq(x_iAvQ8Fg6OM7nKzRHzI*1f&wlj6RLJc8{}-3%=&|izz0pZt{7I9dk^n#JrC;8^={@vs@r)TyJF5*iS8P9c^2q9wmM(FfMeeGrI9?lc7|G4vq|CJO zsO#dbr>^PP>EadoZ%*mXGpmg@r#%TSUbg*fjiuZDXXXb3r}NC_ zf4H?Pgq5e`b}Tr|Y5R3^p3tKu8zLBHOlts!fjCep zD9*s~2GR!t!1&aLu;AhxAifui{I&pL{=dsU6y=|}YI^SXr+_Ho4-;1=tkO9(vu2_q zOb^(6U>wVZytv;rdF|pYy8x^`^qdNm4*X;vQu$;P!ys0QB#6!;J^)8}xShzMgV1q`O%`WVgYQ zSG&aR7p%3AweDa-wMRfg}lbskyPA=71E+o=t_RX9SxI^n=F6KeAg5Q<+5~d462^ zIQi}TmG>fJ?f1?}+~>DpU4-}DyG{A+Rw1Xf_kLdWoOe@7e$CF$SKHF_9xc7>Zo2zb z2^Y{nX7xf1dC&hGZS$+n2u9?^ZSDOVwV~E5;Dyn|@YGGg<*i^#L2(8Fu(StGV?ZHs zkMyMnpmLz_V1}ksun@s?4T~E{+J+i~pkS6EM1W}=SQfzeM41nAAMxgc`~dR@E$yMi z9~i-cB!I+(g%Tv4!g;WC2n|QH`T&%!L2*c~-!be*5v711Cg@qS9-47DCQ4(ID`UWMgkT^(8 zf^i5A6SQ&yQkOyU0oW`e!i$tT8|Elj-Hq&Cl=6uPdl{qb7u;U-Y}g99Yb zA~E68C~ASC$Z>((zGqN4ct3LK`8j&#C3QR4bXu20Go(e zj=(JfGGO@vUXBoK?*Qv`o;$J@&@vd^t;h+I6u;puM~HIMnOi?->?V+3Vc`WYM+S-8 zP|6V!-2_QrNT~uT;*gkd1?X{(D}93Wg5*&0^Pq7XQhp}FO=9o&2TRsVG#&s{_1={gT*p4Qi%r#5Sea*xb`LvO9KYTRmBw>13-j>&B( zYwKLQLjYt`(xS5x5E~g7LG;z4%$rF8-#v5LXFqyjDrENl|BK6W^w{>V-smJR{wG}q zsKlW$mUq%aPS>^5Hmo|vvS~qr!O1N*Z(WzoTD_N1`pMRdCubBaTdHKY?)l9}QrlHJ z)?B*8^|WYxpW>+(J5RXG{awW9%y4ww>zbU73rC)Q-&RoV6!|^(5A%)MrQhcy3NL&2 zclYg+CMUkG3O=*{`~Q$vUyuI(d0c&EeMsQC9P$4<(jr{a7(`p8cpq%rygUWuV&>x) zs!wkLu^`}74vU-@TC`Tuhky7LQeCYgu#Xqf2q{`|y{^6zhIP)q}t z?CCj)Ey@nFuNB|S;4gQ1cs=#mDy6Q4Q{Oa|7>NX)oUD4v_^MWTmy*Y0_UOmkAZ}t{ zAQV?Xbxcsq5cEIUQ+_g^M4D=r8~3rC%RN^r#dHmOW=|-xWb2spEt!Ge$pIJ!qCoY` zKnw$*_yflwLH!I2Y#_BE-iBbk!NRKl`p){suU1Q6FMmF))$L%vf#MeeVEzTg7Yu;Hj2RgJpzr_#BK*%F zdo~pq&Q4l!_qp)AGEZG5`SO>3z7g56BbI4v<&CL z@+Q=OXyq&<-$LZTW)Ts_@cc?d`h__ImX49#OG%g`n*+83NdSonvz~-7AwL~}>_xE~ ziG#!>*-Z>zTRq%N5v74}# z34_FKl!O;3ucO2j5(kM%Fb=`xB%yMIi131!BSh5uFh>zAM~JYOK?;$EXcs0(

#W b0;NGXK#CkBCR`dtEl?D>Oh;~4!!-Z^NQu`g literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410331 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410331 new file mode 100644 index 0000000000000000000000000000000000000000..2dd6f71e8861f310ded735d26e1b1340a3a5cd80 GIT binary patch literal 3124 zcmZQzfPlwmHpRbx+;V;G%y0c4%QCCtUiM#Wt5c752n?5d_O5axP?hkeJ+Av1Cs{?u z&fuDTGL-35!Jq7!d8%?^{r>rPv!tJREZe zKqU?fUu@X9@l=$4c+8&YGrRiK73*3WUaN0X47_(O>hV|8Q#s+B*ZfQ}`=tCu3zobt zy8Cvr%@pBdRT&fhD3@I0cV|&JX!6pYjol~i$jYxDGWZ|4j}s3+IwmiJ6*g&|II1gd1kfI=Cmil#mlyTt+8~w|IGYg;B=nZ z{13Nw%d0Om`MFO2RKl|@W$i2fY$^YL&O!ObM|@E#2lo!+0H7*hWIO$~}^;F3K( zC$UA@VfMA+n;HD&E)TD#K3k>KwQ%a2rV=BOz>||zPZ?j;3hz?#c+4LCcpJn`3=D+g z3aE|=iWx+0DsOf;o@tO_on3ufYxTG9it8^YiY6aOl3VY%} zfZ`7vhXnOAFtCBthIku+^fBo1``)z`KJX|&>n`g)r8wmebFEyfFS#{--M8_!#>N|2 zKqcZHWiP;LfdnJi4Zw7eS=jNP)#reZhG13kzo@3$jtjG;Hdn>i{Na^5&djtk`Ngyk zzM@v_(KQQBzIrHhDempQvD*#l=3{npTV86mRSNm+sITIr|KWhu|IL%vi=Y7;X z|H}e8pE-X|sI$+T{0gK__G~IlIV0FypdVUfPK6p+ulyIdz|lfAtSEq)B|ScP?Y;L0 z-qb4ByXS;W>7OV!t$WZw zV>e+fF9wO*C7>BsZ3s^n?n?*!;ky&Hb}eqNq0F4VH*Br|))!HSP>pO&`G`?WOa_rs0lZ(lEH zJRq0+q1K`E;Gxqq4_XWGUy;h19xQ0Sui%rdO{i~JpXsi6@5hny{9KM^;jb>Hsa&`_ zRdi15aV{z4A4gYi+O|%&-l#cg{?%K$PyIyrlZ_J?L|bKeA8gyaJO$)p=HnM;CGP>T zAmCIkkWOLn@pb^w&(_{kv)Jk475Z;Z>CQ8&jW(w}2`*l?{cDY-+x=(e2Lq?`%;tZ% zwOd|&p~=s6`lk|}Z7FMC`DaV{|8o|)^9yb!nTPjinCSHW{KSy*?{8{QOaqtf={bol z$_}%y72nL@FL!x(J@wftrLKij-!zpNi3FaUta{4$s#bWHlE-8A=*Qb2Zem~{6jwlX z%s|WpQh&A2wwyCDg7dSs5RcQmRd?P;&GWx3p!1pY_k=q8yveT^_?;YpVIU1u3XU_7 z9v}d*!F+=H85r0=YD2sYLHZbig;oFco%M}ht(LxC{(M-+EzJVOrN$pr47BUBGp5ep z08}FGQT76?7DzCH-2hAn9qUD$RySVUe8u?t-P3dWnqXG6 z*oEuv61v}9=Vku%3gqTDf%=ypsGbGp2#`CVn1P^v1_mCGT9|*+CrnJAyprD8c(tnwsAZ-*cF( zbj+mX)8R0+-~_X__kFugitImN)aqnXYbBCW{NcLXyykW}qH$dO&s%lnqL2 zAixWu7??u1pMaT!%?B%EGPA1(F~D&U8tmc>;&8~$bJ&vAI>~N+)ah>vYOWsNdy7MS z|2!+@6|Lp>jlA4JYJh+$cDjamfDKdoQ}wS*z0^!=^`A{fQ<}Q0qSybtetL1%7T3O4 z3fqJsg&dXajC6_s+c`1K;r*0jC9*7P6ccxA0xJ3Phb1Vuv&C|jPZ#W+ZD*}cM zBt94%UT2+F5-Hy#uBdk8sHyxzqn@n8H!K9=7tM4##+UR;dgXGVYzw>Ce*c?Y@_!|p z{r~+F-Lr1W{v65m8jMeJBD_FpirM<%t^W%D-HJnv9-K?PB66s8)2c6xadtn7SdJE? z^@>h_*vSmbq_B7g<#QMSr5|Q!xd9d|c)3SJIS+FN zth`5dFDUJh8|KL7fbBpMKw`qICm~EIa}$c)NE{?4$!=n}?8>Y3yM4~Q!~=fYbq}i~ zxSOdu%sVsjvBbap?Yv2^=0i-x$g80G0|gLKuL0GA+dJ?$rgq)NuFeN?6Q~SAcONVt zih~q@05R^Bx>OtjOh=&l43w5ZX21bbpd&Hi(kNJIG`#~@-A_1%Fm&( zn?P=dg%>;z4HCCe5?-M87)rb#MH~_n8Z&5WfugWD#8%d^gXNL(H;LgzJGW8hCWxP* zL4p)WNKBH$3sg1>0d;}RAR?SdDK8*x!;Opf=p6;S3)!8pv_mg@k?m%HmQC=$g4R#q z^hiV)0QC)6*$Ij-P`O1yxDnqUfd&Y+I-HF7qR3vb|DgRNL_9%R%!eQ8C_OxRbB56F z;~$#ZHzt_A)8AGhEGunS<)l`S3vEYp@AwY{KrOI*!wcm8gBpR04J&KmbrUiDn#HLv zbfJBxCZPG8P&>dBQo=)G!d0M@fk07MxrVoHBF;^BN@?sSklSJ51+SY1iQ8c56&%nc Sx~Unci5hW;UN?c=0wVzQ5z(Lk literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410333 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410333 new file mode 100644 index 0000000000000000000000000000000000000000..96e0cb0c76c969778c57e6d5a7cabc1fe316e678 GIT binary patch literal 3608 zcmZQzfPlhRYa`S;6D7At?lUm)kBxn>Zh64vdgC(grOq-MQu73Ws)V1;f8WR@vNw{6 zP4#!7UhI0Qvy8g}a-*Yt%)UOhX$fcCKmWrmx9{IJ>ZY-!%<%H72t1@2*YoSec9*W= z4a@76e+JozeTCjfPq1@RhIX`w#~~^KrUuJeqqN8 z8xRWuPUQjV6b2t}2N3;i?L9S%oi1LX|K^nLJhR$pbJ~;O;$_>v)>yjTe`bC#a5~Ry z{)bz;<<%FO{9LDhD&g6dvi6mKwv_)rXQ4a4;AWC}c#npOPVdi83@QKqrUu0{aLJyY zlh~r{F#B5Z%?$o>mxtF=pRH2rS~&GhQ;CsC;K|9Vr;M*^g?A}=JZ6u6ybaREso z20-x#jzfa_85np#YD2t@z#m)sh^?%Q}=PMc(%A-(CCXyefaasqk@<6i}_WN7)OI9U#C6b_3AA%k~CObg`{jXW^Kj zeiYk4guge1~NhJ zhXGKSF$2>CD1U+h5&mb8J)0;2)B_4bc(_5eLlP2%1G5h%1){-mge1TWW<%`C9aS-NK9C4;EY3Xn4r}Qu=oYZfz2W! zE=kE-Fh{}i7_xgo>6+YlqFtC!<|dH4-~h?9NKCjiidvv3a$F$S-wgWqH+-IR@Y$Dy z*^-ZhGOzfAC8ZUMFdG(6Q#F>{DEP(8Swg~u_mbqHHuc>_>0 zsQv)eBgg>cPf%P-zyyhLht#Fw5TG7VT?h&nl(0eyOe7}EbX2ua{h+o5*nVL8=Y^_7 z$&*C6>9C;)jok$DCoH_+d1H{cjgs&J*N5;J0y2;y4v7g@ffC<9QCxWgq?Z*ekCbmo z3@_TbjWRbu{0t2eq(A~Qp=~y(EY38KUgm?_*g#F-@FgakXiy%2$^tQ<8^Hb`!VRR9 nMNA>wPZ$_BF507a6dW{2!3MMs5+C%k7ujwGX!!_>FE|YV+EPsx literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410334 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410334 new file mode 100644 index 0000000000000000000000000000000000000000..cb69dbd7c2dfd50b10eb0cdda083c6f414debeaa GIT binary patch literal 3660 zcmZQzfPi49gXWg!|L%4b=Jr$ZT@!MaC27msc!n~itG-OL)Mm8+RS6fqS{tF(nJBqE za-V^Te{Af7b;|=T*Bh5{FLjpDkeVmJai{(N49^J-=2QMG_A=TX_VHK6Q{UssFIrFU zn8sAJ&=zD<(xS7v5E~g7LG;z4%$rF8-#v5LXFqyjDrENl|BK6W^w{>V-smJR{wG}q zsKkL?SYywY&QFd4Q?@7W)U=WnD6Z-O?l9$2%wO#uvtyLE4*Vv9%=svh(xmnNe-fCgq#}iL)TD0)M zm$yAr68bp(*iUZDJ!~$R%)O&U?#tQ(Ti!OuzxGvos@TdP+A7EUVB6;9DIga!AHT5s zYYm760jKhTbP9uyw*!cNw)UQy#ZDKm(0_AEcb-{ov^nibaPhM3Uu!Jg?msg>7&x70 zHvhw|-SX-SO@6M^Kb7!oOIiEMKU>QGpR>@NUvM+YJiJH4M5p)XCx(=Ne^Y~E8n|Rn z&q-`ic9?yw_+|!wxy!@rsn1p^buFCwrm4h8B=F>9)l_e9Uyew5!I;O<{%uPz^{Q zST8~Qpz2=QUM_zVV5=FpMmNmifV4blrw_;2TWrND(-d~ zzI?juoWSPf__s$--4e1kce4`wBGI>K?&Wt^jr_ADI@IO2A4%HP7 zt0f}dyXm*w&tnD}2ntVz-KyKf^)#Z59TPw8xqmgrQfO-O;VGLY^iDH+bZ5DkSTqB_ zlLIh42?CX}fYd_)JZuQ+XJFs~>R}A=HUw)87FPY&ch)z4wOaam`SW2Nw=@eBml}Ui zG0?8h&X_uX14OM6SnVOVU$>0DvYpm>`?NJJxqSW?dE2{xckQ3^s{Hk)!pBKcAVq9_ z*(EaLp*@LOH{m40B0f$8o)5CGY*G{*?!{(?FR6yD6huoQ;!38!NQ1~HA!)=5C) zKxugbP#-T$FNlU&g35%e0LL$!2eKOkp!O#%I`auC#|SDfplssY)cy~sm!@ul<#kYa z4K}w?5?-M4h#GMS4iluh2^@XDH0GD@owUkzu6#(AdNJoQY3WZ677ghwHLtiPR(Ugg z04oBQjSv=?gybD46G9P@r-2H=^$5-KHM=?=$T6V22g=*X0G5v=VS>cCSL(1~1u!jv z$}xD^2h&exJ_6ehj00GHMoAY$x#{4l8#Hzk)_gQb+(t=wf$AcZ@{P+2#yP4!f61@4NzSI%Clgv6X8~1*+zUj1RAX3 z9%U~;Or+oivLRtaFMGlEK-)!7XTd2@7>K~!M_d>X-ELzF;eNuvuyN5Iy`y0JC=3IN Q>_xVl0a};90|CYW04PMe8~^|S literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410335 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410335 new file mode 100644 index 0000000000000000000000000000000000000000..5df72efbf5438932bbd3077042e02bf9047c18bc GIT binary patch literal 4236 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 T1gev$5r^O~K~JNgFaiMp@E?(Z literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410339 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410339 new file mode 100644 index 0000000000000000000000000000000000000000..334ae0a8358552e08aee2bcce97900ae596321d6 GIT binary patch literal 3772 zcmZQzfB?1BYnT6aT;6fm)5Sor|=I^~0%ug!K<+U!Ec>W3jsuK2aUOUy*?s&N6 zHFXExMH?S5{$dj2ea!Za$Ap&iX5Qu>8Ub`+t6GvZC{vesBFV|MZLUpq>r-M`VTE7@x?dNJlNM z5cl?Pb-T!NviJSOEzb+;d06iXcHHTkxp~t@_9Y1ytFGT#tY>sJIBrNJ z2-fOvsC2Ely@tU?AwjsM+~l$FxyU`A7}IY}^|Nl5EoKmHRpx!LZS(RJkc*j*U)cS7 z4~PW;rwV{{3WJZg1BiaM_MV!>P8YAxe{)KAo>^_QIqgYs@v`k-Yb@RFKQliVIGtxU z|HG}_^6Cpsey-C$mGEp!S^LUATgv~Rv(TMia5Kp~yhp=Cr}yV4hLnGQQ-fj}xMWYy zNo-Men0>AIW(I$`%fsub&sHgQEu8wMsl-Sm@Z@CGQ^r@d!n>3_9b{0|c2EGRCpGB!3b2WbETm^v_>;ve{wsXUJFShc6pj(b-y}7ENv$6aCW7sw_9=Nm_@otvder$+b4G zgN;|ZC$n*We2`kVVaoOgoprR#g}S57m>H4c!mAVUednXzpF(3@Ksn7&T~ zDu?+Oq!pCT6M+=V-Z^jOHtqLX@^Bu{OnsA-FRn+}zT7vR{%DgR$&g+&{qsG!Pt?oJ-iRWd51j`BS`M_wjkYk5nGNOKtsE zSv7-m{>8H8H_kHfJ2^0LD>eevPX=PJKY=tl0EvU;1Zpp4>sT0jH0d?UeP4B6C3 ze1?#skjr**xou$&=D;+X_aEI7BR^Y`Ym?X2B9V}$l7k%K!9AzS{LeNDDE)b~6R1wy zqwEDtITP3|z%r+?K3|>X$V67h^ttipA6ZE z@!!3dk?rqEzeQhtZDpTEa>JE? zckgyhzwk~mOT$X<&?>g<<181y9<8+2xwCHJPUFYsAtxAZdiGoF&D`C@3UnCr*L5=A zLpDB6zOnUW-@#vxb>HZ#6n0MZa^Gv(`tDf1rytbuDgS{0$cBX-Bar(GDhG-KW?(s{ z1mzP>+YAh18ts3RfX0E+_f()hUYK4G4YLH530DD*b2txD)-b^A2j&fDs2n4xY=x;K znBTx|`ty&*ZUVU-ghAmo*xW`*c!A0&YQ!NpOpxkvaP+CP_T{zKS?i_8U$x);Nvh<) z)h?;MJEKia{}wZrZc%&%Rs<|kv7}L8e1ZWi%$1;gV(KLZ_Vr7rK`=a+3j#-Gnub4idMaq)`&x1ZrPUBMz~qQJoVhyJufq`BtgVuyq?-=;Vj;Z!7|T z9qZ*ytynX4iGMFN;ecCtKsHkHfhQ?w8qEdOv0y;3UIVJK`f+UnUqM!#*Nt7@r(oIG*b`#b#I!N3`NqB+UCnx|Z;*gkdB{<>`Qnw(L zqaeMY_yyHF;xOF=+d1oQ+VZGu5(TQ?08FwIVESM*76+mDoe1+)53fB%ius^=9-RL` TZXmk-g`@}Ob`Xul{cvdjVS=py literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410340 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410340 new file mode 100644 index 0000000000000000000000000000000000000000..fe15d2001e013d07a703fc6214c48781f6dbfaf9 GIT binary patch literal 1476 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 kLJdNR3*y{#OP0oN!kTsmiQ6a%FHqZq8gYm5 zUjHZ%6ad+jwCHRy#6|{25Ph{M^JY@Och6k**^geB3YoqC|Kjo-J+}R;H#*6S|4Ekt zDsfQy#d*QdMltJ|n{9#Otg^>NW};_A;%`-~73Y%U`T3eJ-Y@)!>8c~mOQJ76diyHx zZ0!S`sjEHLE8O^bd!E+^O_uqeOY*kZ@XZFYIBs z0AfMFsRAIK!rBbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}PBSoQ`~^nabn%j#Yaq?YMW3?kD{T}!T*#aG(9^;f>$VqY1K1UfC)CCxX{W2IB!`1_p!~6@<3QFfSP%#crxEPukm_qp= zX|URtwwKG_1lVeZbJis@?y%RiJvRB7)zUS`X9zqwe}7rV4WK$E#}MDhAdn6yASO&C z3ZxpK${8W8{fYPUXj(ki*N5iyW4dW z{w??`Ui)nGf*VqWKR>MEWLWpvdNK3MC^iwx-r zV0u9`%o0>4Tm?8D;XIJtAON)=7)NnXIYv+(hq8%uQ`Q+8y9t&*LE$yn+(t=wf$|D9 z;t(7rNc9Og`oeyh?wpf*W=m3C{=IoubiAVeai+XjGVju>ZEqeVIQdya;}}~S{Rfr9 zNTWoQj|}YV7cYR8kw1XuutLoOQ?y8h8fdBvi literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410345 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410345 new file mode 100644 index 0000000000000000000000000000000000000000..e2533afc30ecfc1ecba5ab83055037d492c51124 GIT binary patch literal 2764 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 aCv5(W??(H7kzzhdT|;zVf`qyT=0^b1i?_J| literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410347 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410347 new file mode 100644 index 0000000000000000000000000000000000000000..8d9b81b6f86fd67869dc097de9917f76643a8f0a GIT binary patch literal 3700 zcmZQzfB;4hj@U1OD;ivW?6JPEq*kWb@2*tl-qKqMCdXLU*6z~-suG^E?|pOr$_bhB zH;w|q+uCdJg zbo926wJ^x0q(x_oAvQ8Fg6OM7nKzRHzI*1f&wlj6RLJc8{}-3%=&|izz0pZt{7I7LDJPS!uhQ@G3w;eVk8=F`CS6P`f2Cn2U--*PrE~o%J*#YFSEXLnEH;tY`R|Hb zuuv_}xBGjNvKsa0-*~=w4HKtxU3T)dOMYodzA`!Wsvn=editmNVCbJzO^pScXZ)Gm z?zn8tXZFYICd z0AfMFsRAIK!rBbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}@!pZHd)aj#?=b3l%r0E1L;8WiB;3f2oI(obDWu9(GF+Pn2vzTV_04!2V!7O%@Q1T2i# zY0X@G?K4=>jA;!}9gI-7I#j=3VRZfe+1F~Z46ToM&A1z@uq~#Xug|USg6#3B=Jn?; zNB`kt_{a3zGlyS9KK+u6sn2S;m1$P`@z(XKp&g$E#RQ~60_DPkSj;lW6dKBj}gPSwa`T$RN@H1lkaQl^*m3HNyYaLUM92fd#&#Ir? z?(gJOGfk+`D30MIiv!5vOus%ffM_6KgqX|VaHfl+Vv}xmmsxJ)_vb;=YG3Pb-xICH z@}~FgRhvHM3r<&GZ25HaijSoC)>bu#L=La-9viYVWMf@UGo#9P zZ@;*B^(JYS|L&DdN4jHP|C;TX=_PwvSKBcbY$GUrLI6tGL&ZShzzmHiun^&RWf1o$ zd%=LL2Wl3W!eR*{sEmNAAJ}=v~@n68YY<=DT zJ3Z|Me4z17QoTXAE``?RkTwC-3Iv5EVWNZ|aptGM$`6FWP!<;Rv8Ejw*h7gwFr1Af zfW(A*7gt_JPX{2q81)8;e#fvMNdSpShTD+JTVmWKb*V%K+Fro07fAq#NwS+z>J6gX Qd`SL)+X!S}a|lcx0Q4D6c>n+a literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410348 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410348 new file mode 100644 index 0000000000000000000000000000000000000000..98c5b3fe96b553f8d5f91a1545e09b1aa21a8b36 GIT binary patch literal 5252 zcmZQzfB=1wj`;I9#BlGVmE)VkA*%bAU;l+v8vi>!Zt;e@tXyCi(A1ar0d)s7ombUlzr!4*vt^QSJ z)f`uuC%jCTtkwmv73gf~Uc)2ZY`?DP9N)7J6D#f+Z7vfh{1KO2<0-PvR{8b)FB`(w z*j)N^GCH1pk5}2t`P=iCttwY)dDoS?a#PMprGJYml&&&}w(9Uc*tU6j3dqIG$1m(* z^8m3R;8X#SPGRuzb^y`O*4|UI*y-XG`fpC@&NHixHm5xaE?&0%YmKGb{b%L}1E=%M z=6|@gTV8#k$l#E|muZ)#9X1DEXS zIf*UG4zsTn-^}1IcX@a{_1P+=u7y+IG?f^M1fHC%ddm2!R(O|^$7A;B$J-!oVqhQ? zS3q?vK+FVEU!XCuH8%MFvIUat&TL;wRjn^5#&k755Q>$aaptYx(K4VCpwGijGB7AE z2eQHO2GR!t!1$a7VZp_DfqceLZ-^*|>Wdu>lClX~g=GX9`O178gzPMoZH?~x_T1>* zd&gWCsA`|fD^YilY9idh7~l$4%5cV0D9uK6V(y=`B-;TEBZq*PkAYJiuD{5UJb%GGLhk1AH2#ENORCOFTQgj$p8G>~ z$JYy7VjD^Y*Xg`5=Q4wvGtv40Pj~P$1_pHjV4Bo|sYmiFIBp2*XJ7+`Nr<;0SnKUqVpiIf zf39^*J#t*=n?0+3a=X8iQ_VD?Mx!`}lPnGl8yD@-I|{W0s1M=>1_!5Kn}wnaURJy%*bNO6f&sBDIA z6<<*m+mI zA$?Y3W~0L?evq*sGf#bE4eJ)|QY@CfSt$PUKclzTos`{f%P!nIq~X7()oUsP1NROF zX00{`=F&Exa%6viw8H=>4S)bB4}kzv2=@~(gRuD!^=5YUAO^^M#?W9FXAp-&cAmqQ ztky|(^P^6GTTpZL_}*I_;``@WDX(ZPzi;FP$xu|X(=`NAG^+in`d6l2YNoaN&nBZO zOwjK9y*O)&Yu_t{ZNis8?g9d;_%qTe0&LjCG>7+7{_UvS6L}yu@ZXs>rQ;Iy z7tXOL=rm6YE4<-+9IOZ!E|BCzO|2>7Ea4{ z%vrFbUTu?U;j)FwEs{bj@|f4>FilGrT>s?srRkC%rb*6Y1)9h5yFNeeC)P1#2h)EjgK^T z6DX`;;WgOYMoD;q$_i@4AvjEs+Fs!3doG{OA-?Z*l-bQos%pWhXN&Id2g?&+BVd*QIY>;H={VCU z*nXh@Y@sSq!kmb91;|Y~GZy?*yL!(pMX+b4NhzWOIA zzm;bz*Ca)|dsR z{$cS8k^`p)BEpOKwl^##A===`X%5sr0J{mKj0k&y=^CDfXcs1wxe3{S)QTsDJr~pu z>oFXAENeJPAZl$ai)a1h5~i>-f0bt0FaNjAsSX;<=;a73^g%Qzyg=a%FGmRXTNoH* z&!#dU>p?aU#iI}jlr&D9`6+WqFdu9Frhz?__yZ$YkOYvJ@Q_4L$H@5+eY^=IhmvPV z^gD+ANCHSqGTergXNYkVw0=VG6N4>AatIQWWH*7*Exc?Yx_^zN2W}&ffz2T>c>oKb B%Xk0) literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410349 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410349 new file mode 100644 index 0000000000000000000000000000000000000000..26069739ebbff90e84d7a7706147876961d00ebc GIT binary patch literal 2888 zcmZQzfB-GFgo8)+-V`zYbNgZNS%Yax?ll?jf33d0Df3y9?9R&(Kvlx}A|3M!R=?T! z&3F4ZJ{E~227#pRJvn8iGV7oI49l6qxX$^Ze#)~g`XWnHypwHQx4NB*nBc#x&e|u0 z{U&4EeRq&eNsG?bLu_PV1kqb#1vc%j)$qG|z{-8;!>toe@`pEvy2~61sNUDaa>=F} zsKnva${oFHPei50#--joXW{(7X2PYs#3g5DpF1hMVpr@L78Uo)lNKzfT&{3A@2~gq zQyE{M&*p7>cIxl8LvsQ*luqT77pSSs3z&L-%Q=T@oHx8aPkZq1j!T8f-(ALBx0dj$ zR$Vp!GpQlNC-;!hG_GHVw|}YJ&SxW^ob+$uet$`;#a^BaqOH2T54LSyo&s_)^YIIN zLNY)s2sl*$q*EAtyd6OFv$glsEOxqhh5nmUy7SCxqs?hgf{T}J|5{_|cK@0A!NBP} zv-uxx?Uq+xX!3KN{;7m#Tguv3{@GIg|D1*H{DPZF=HWdWCOW-8KQW~I`tYT;5X$A&$0bm$t!_-6V z0L33T4hidLU<0WQ@iqi&?KbY}Y4>8zHT=sTvo?0d|HH}eSGg;PKTF%ZNhH#clapcN zqCI*?p|$|^LEOOLpy1thdqs1=0?USi($1H^qHQ+^G#7F&ds@!>F30`xSFKGqS3dt? zZ+%Io!F;)Fm)AqXug;mbmoN7Gd^KxQfcrX-TS0zixcy4ZO1tvUwT`JrjthOWXVp(` z_jhutnI_a|6vuFq#R18`VE+NNfM^LHORr={3s=`t*GhNyur#kSw;X>bM_XH{JdlEf z$Mk8wC-`_in9BeB%|5Ae({a^DT#w>hW^i*RS|8x)4t@qw2MY_rZed^w;eG-#8w403 z?qhIR94d4C(GgyUrwUwqSC#Qe&;9mK^o8vn9 z!M+`}%DY~Q&v;h+;7O**hA41YT+P{Z;uzc236oxUf5>51Qe`N9ueUrbmCu|;?DBeKzY9&-9EKI>%;@srRLt{5#&0~Ya zZIpx;$X_S`DdLcra3$#K@#RZoIj~togcrPCAy~)4oP<4ppp=C~*b7Y8@HB+(N8~b{ fvM@m^pUH3&irq*YBql6SaOO2|{eV<{!ZiQ@RJq?c literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410350 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410350 new file mode 100644 index 0000000000000000000000000000000000000000..0a8b820ef0c159a91e162aa550f28f165725e6e9 GIT binary patch literal 5600 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 LyS1C5T6N literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410352 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410352 new file mode 100644 index 0000000000000000000000000000000000000000..3a972c16b1235ad33e1209c86f0f0d89a7239ea8 GIT binary patch literal 3936 zcmZQzfPl*Fvy+$JofqF>9CcUd`_W68^Srs{&pu)G{?#@vU%y!fKvlx8E8qS#etfgQ z?9bzM1(Qv7Z`vvDvv|7CY&ktsRm0s`N~ww0=M*j39c2%2|m$b4%HLHD^UfuUTS!c$L0UriG|&kN)hfr*l6T z1C==34C*>AJMCT~@0ERRVZAMU65q58zehY)SALWhXeMA`qxk%LSJkEaH5+u!z1FZ6 z=_*R-J^0^j!o+zq_cY5txxvY5Q<^w;=lbG5U-N`srQCk$u*xta_p%1>|Dp;};HI zS^;7~z^Ni2oxe1lI7}h-4Kka$KT!Xj3RJ!|WODzns()zO&UnI1)N#)^wiP z{13Nw%d0Om`MFO2RKl|@W$i2fY$^YL&O!ObM|@E#2lo!+0H7*hWIO$~}^;F3K( zC$UA@VfMA+n;HD&E)TD#K3k>KwQ%a2rV=BOz>||zPZ?j;3hz?#c+4LCcpJn`3=D+g z3aE}5h?zj@)qA$iU0mnH{5$4s|I*KLeldmxk0w>~X_~)GzFd&F@-M@TX$`K>9!c7@sR4EVwuai0{QBzb!zR|L?L7Mfqp0nx6apDIiMt!^D*dt8`AyteL0? z(*rgi7{@QBJPbSkN!oO6`SLwUiQ=5v$usHeUo3_<-ht{gRYFSK`sWy=E6mKGb%ZZTuj=T(z+3V7Gkh zbsz2fiOYU7@H;s$a4R+f)lUXu6u$uZFaQz<%L&w8%+|3m_Gr>;l>5HwyvW5FUHJ?l zMIo2%9?XGh@G}-UYd#v%YJH_tZZIrQ_yg zkK;R5?Wwfm-c`?(`2mOT`&ooo?UR`Dd0(?H17q6)V9;!3VER52s0StdplN~Sj-Rub z$pOE6PcB}R)T@%tkttTOZTXn#erZ>Ymz%;22dF-P?RaT>x%^Flt!6l9T{7bi zdrjM8lb=~FU2}Yfz?1X$mu1`ln#trC;^`6uWWYfBscXph4DJAnTxM|2C9=Ns<{PI&ItA&C{8Yjf7I^xirxAq`rab>C2wwDU|oAT%iZB? z>pQ_u*Mi;he};=4J)c!KY4@8?$EO-UIWpakbMDF6q1ka-BA+jO1*Ju1-Zm@wN1R_j zf9LHx%l_cV^WYhOA7&PRkvTYLVtntMhB&aL;IM?SppF3N9UxoWqckxADh7%dW@!Ec z3lYq(u(SrrvruCY6iTE)gc(6)983jK=7Zcvy!jwM!2Cf=dnoY-MzA0WATeQ~1j#RO z9;7^FfQ2JkJ;wr62Z}>-{f=Qjk^mAD?ix^7BLh%bkIijxbwtED$o;A?LkX4dJj?dJ zl^1hr2dduy#29YG5CEw`3LnDe?_){tUi_Q{^BI;s@u(E(Q(omIcF?C*Mn3pk@4PcS z=5wM#=5NrJ4u+P^z&!aM2$0;&2;}|(@<9NWhV>y_V1-YpykuYy(`a9J59mEmnK=Wf zj~AvFL?eX*5)-Zh9zReKSUiI52d1$us5na45#^>f4LcgU3FLNIc)`>CAaNTd;RUMq zs1b+YFhMFOz)^W|mhSC&&wn4z=(LeEZ;YNgAwK18&~(?vvo%WJ(j~bS!HO{J4X`kj zfQET4ykaBRCIV`-`g-pUv|Tg_*)R|rDPbZp;i_7pQHE0+1pOi3wMNBMu?;6;c`n=>?TRDD4q~ZRB+~?!WRB5(Vl5)&-0c o;I={;SR91ncOuL`vG9@>DdxlLLXZ=PZo4DZPso12;(oX^0O`BwGXMYp literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410353 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410353 new file mode 100644 index 0000000000000000000000000000000000000000..5fc02221c38e07e6bba4d8bae3a1d17d7ea56139 GIT binary patch literal 6092 zcmd5<3piBi8$W}|wGc&FLbi~|tt2!?a!rUNmC{6uN^Vmdt;DdTTsQYzD=JC`tD>7r zl$~WU+HR_?fATLjOG?xK`_7!1|FMn7^r+|QeV%i^_nh-ye&74P-*?^*LD*z+@ZAx* zouz(s9bL90+%HNw^1Z2r^vhOhE$ik7`K8N&l+6C>tnJ13b3J4AH{PE)($M}xuASJ4 ztY+iaeN|#>95R<^A0HglCg1Cg6>;)&v#nQ3?3`IhDwj6&xtJZwxVYHU^9OHaHd>2}!F>bskc2kIh)+D+nt2+3!TxuQ$7Z^wm%z+I;b zesu_Jcb;#wM>WW;&g%(FZtII5Wi{^}d$sy)x=nmdsiKcfW6nDFbKZJqUY$=HP7^vK zT65oLba$$Y@~ZGQ=YgQ&Gig2U+er6T*1nIJQJ*++xpMY)AMxjR|M9%i5|~&N`^L82 zGicLD-G>>~CqQ1pKFq4Kcdo`@VzyY}(ms<*;bi5FHw>O!F;x&2Hr>fwT6>x}n98BC)iv4ghf$7G3b zRhM^72e#0-v z&_4vhg9Nd6&@Lna7xAYkGr4`X{8F=@AOr0aT-U9#fDa;yxuiocQp5t)+=1s2!W0WjAR0xak&TkbUAn@bS3-0N)`3CMZLKa z89D}(ASy42I(9-iFE1f~ScU2Ghjp;3%JcO7Btk<{0c(?u-Ze`xrhmq=j8)9fbVv6*Lg zXiF~3^Q{e!3NlTn54HLvoW-yPuYk zzICMbA*GSh-(-JJkK2Q|1iEyXDea=If0WJ9Ng+r}$XHhmFlsf!{~miS|JHxE=^C-V zC8e=DqZy+ObAg4u>V__>HiNroDkrDo+H7Z{pZj;$Gm`&&I@!A=Ko%x5CYX z>k%-#fRH?@^Q3gfa_ROFS0Wu@(~cu1OYs$^Lw=OOzBdV>K8(hQA zH$e-lQTE;gdN*8+PQ-I^p~bcVPH~g=$p})ogE@%x$~2%S%F%@*u3&L2S7M^tMg4<$ z@5dy^<$^y<-3?zE6VKZHMzBpKUhus0C348Kc0+vgm0OEOG$i`J3lVL4&ye`&%Xj_B7DbRr&q+Yg_(3fTJ1Zq~^f)7~FmWvB>qIrm z_U~2TJb+1#i#eZ|GQTn=o;CW7V4F(3;CXi(;Sz^qxgrzoGykqpsLMGsaQDDT&&_4b t=KOZeSwQzZ*mDxOUNMFTpmB5O!~O+#5Ax^OAK%OKd!GM{UY;|@{{g&}sdNAU literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410354 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410354 new file mode 100644 index 0000000000000000000000000000000000000000..942e9e3aa1a1ebbaebf67bdd4a15022c9056e5b5 GIT binary patch literal 4708 zcmZQzfPmT7b30c^@}E4(+u<~8_wDpMeMKi)Bp>aMb!I?@N?LSw7Q{vdMi70qDD!4gz<1AF_SuhKm41bN)=h%DHi_%~$7k6rv0-pfz>AB}nC6>p-EXaDZmq>TM;pC@fRU@y!hWa+inKQ=hF;>RLGUO;d@HNZ`rIs;7*vYK3n%p^^+L|d4xT-eYmhotM=2M_YyyH@@##%UKyx~0ur8}Z*fhp&0OeE3V-bdrb7#W@w9B0f*o3zL<#axOX3 zx@JpkwC|J$E3R6+^*(r%VYVqJ&@6}_78UIljBlTPc-xd~%L6z+OKNgCy~+P+F!Ruv zu0AQ*iNyHf$jfg9tN%sHuG+$TsrQ*ld*m($-ak`I#2wDPw_5Df&3 zU~_?fxXjg6SCVDall%4bzpRD1a}%R}pM9aSs4B}yxqPy}Ov0MU>v*rNHLH+Fuh|iF zJ)HaYq>LiRHZi@Zii}HBPCnoVng#ZQ;cG|c85p)g_GM^snbU(&ol4j zNz)0!pzu#&5D+`fz@YIDWIqtYLDHgghkzWA7-vCoft9hbiG?vl7N!nNr}zgxWh#&3 zJ67$fwBz1Y&y@KAhwuAYgjns9nDTjFvoBB!Q$T1?fR8IgGl)z-buGDK7GG)a)?fL0 zlcPA?PL)`^F3%9KFkYuMbMdv$V3jkbHGpZL8=!7=h|7K3_scom`_}g6O1?y)+k27U$Aq4<;@l#gJtiWw{n~Ido6i5k7uU7Ny-=3BWz#pn@)eU zNhhad`$Q8^!UX9fHjKdPUfNzRe-mJ<8O~Xk%(%l|)ArcpXI4ws9G@ZZ%7Nm88Cn*Ag(efqcR*F(d=Du< zpvE95ES4~W%3GKU;>=H(gX|`lUJ#ANe2^bt{-C8jl=uU~*+>FNOt??M`2@}b#SsWV z!x62{1<8TpkX*lG*pDQD#00wlL=f*b65S+qsYC`E?-=$X2_P{^b`v}dK{|=9ACUY3 zw-Ly|<`CCcnNeCBR`oEHE>J5H7rfhk>Y7HHm80owxkf_|e@R%_k(MV3)#nTh;vQu$ zKz;+&DHs5YJ5kdzQRY9VJWo>J9!mUy5iCdoNKANe!r~4lKw6$8&F>iYBMBfeVNM}4 zPZHrKt(8r%c*n38NdSpSvYSxyB+>0o67r<(frdTx#{cH(t`6vbQ`W=p@~2(aEJys^ zU+;=v>BgZype|1N4+KCq%rA^U?k}huDDF|}NrG({1_m*WHuYOT<3Q~gP#+1@{(%FS zB|rfrCR`d!d;AHcG+^)E1>i9D>6HsSXE6 zrAI=`%)%#L$E71WHmNy_l{oq_`ON;;)1sGr-)ir?-Y#f-V@so;z5@ur!W>=)5Yhi& zU|+v*4YUvP7ib16*d!={lrWK)a1~^wQ6k;sL1Q;zO{0UvZ76A!L^pxb0yW|gYZ`^c FJOGf3S9$;d literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410355 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410355 new file mode 100644 index 0000000000000000000000000000000000000000..09230a1709e844319d636ea44049afdc603a2998 GIT binary patch literal 6060 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

B21UADOKR@L&*AKHsf z^D*2Ez4l_uw{HeJ)#h=2<(QW=`)_HK_Q6;2Hx|9fxx{|@&C;8HTYXPdFEHR*v?e@c z&Y>oFxJ={Q_%FY{xUtOsbi405Tc!<1gIwRs&P$%T{;BNccuPeF#<*a3YwRE7U_Zlm&F#l?!`dUYHvH80w8=U1lJk4};w92s zCN6aT+S(DhxcKWonIp9yCI;jfwDeCXekRMr!3{JK?EXiE(PH&$TW|Zk^WzkE{W76M zD0j}T;%whP+Y_XNMI7!S`4^T3K(s_|sCikkeq>}xsDGe^M{;ILxJQIbpsg*)Z&25Q z=|8)dPQNDi&T#I>ybG?s>c3`QV?4i7BJ+WT(?@S%$7w1c^{}uY=oX-xL%5$njb~s4 zyAK!^3zN-fuT`Rf0G3f;fZ|7}7$`iMp=kpwM1+4Bn0|d|K-L2_3rxW*K@wvG)e|s2k><;g zU_QtXFn`d}9!mUy5iCdoNK9BLLDDZ|#QfQBPlTLI>GbD#>aSyWEHV5^bxG_reP zVL}UgA^8STmLS{9aA>NC!-aO_Pch4)mAI3S)SYa_j&=u(HLnT zRF1&_a=3xTh$sgcq%M`f(k6;~Kn5TQATeRq;fzyIyn)&ao1rRUVG8CF=ceKv(ENg7 zFOmQf6KWG`eE<@Kr89IlVa;QM#BG#>7sy{I04b1=m~bWN>S5spE_cxKC9)jYEF!`S zUat_WV_{ChoZspW_XEFsmolz@V4ap~e(c4sFNJ5hS8RfsnDQS8fNYpuyg=?hs2nJ~K;f+e zi0DIr+{8g+H-X#^ z3om&393*a|B)mZVKS(^l10TvjiB7l}t~kUr^>wD(xFoM{F371&6nTo zet6$PX1NdA~TJm*DEMt gPMn+OeW0Iqp-9A0K%7{w*UYD literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410361 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410361 new file mode 100644 index 0000000000000000000000000000000000000000..198df6f33e92c1e7785c7c42754189c79cd012c4 GIT binary patch literal 3844 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 Tp>bQF`O+Jhx|>NbA09XWrOuMQ literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410365 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410365 new file mode 100644 index 0000000000000000000000000000000000000000..e989965ff7636746574cc2e6a5810dc486c57550 GIT binary patch literal 5000 zcmZQzfPl5^fz>IRsZCo89}5bJH}BeQUU;Lt=GcOd-5wo1YtKIcsuE_Kv%+_WdyUM` zl!*;uH(G3qnbXfLX)(*2e(+*c(ImGOt>>nu1#0E>EHB?&x_|dcK4aIA1K}a3zIpE3 z>zL(vU^U35q(x`1Kx|}S1kqQEGH)gYeD};{pZ(~CsgT+G|1U1j(PP`cdZUxP_@8tc zpb`h2Z89t$UmJaJ6mK;6sG?N;-GAMl$SEhx*i^1A3A8)*q{9AJj`rK!ST+UME8@H< zf(xy-uIM@Z<;p+rgipUxPcna|xp*z3eW|Db$kA{g(@6S&RDgXYa2E{aR$)28* z*rMz(`&#kM4E}PLhu2e|ty1b*IQ30aiIGU)$;qmxjIU~icPV*1W{-Zn4dNyS210QK zRL26uOd$1!uN|3ZY*0KijYT#4mCCmiPHtzVP9LQ_&%Bc-O(zH&0+pmN2#B3#V9?|Q zvcd5N(gy-bi_ZN4QXnzTg5m-zV<4~uNg&jL)u;FeK4mJ8<2zREskGzXRnL_90f+DV zS%g^albG^(U$ZYzJySqvP=JpsSTC4JKXom+VisR%@77=WdXu9#+)kBPye`iWurOYy zHFNQ`&tOF}rZqryFhbqxaMS0YRfD#x{I*k#dv-lpE|`2S>$Py*SH|tjFGkzcoA0r{ z^x%=Q-%rho0D;I|%#3eSxA8GKp6i|L(fw3EoYjUGXdXCR2)dcEZ2{1mTN#+X&jac~ z3zxHRfnqFo{G7#14*1=Ba`B?1UX^r?OtFe>%g0RjOS@{k+!SUwK=lzDMqqU>Z7-L< z39!`+=d4R+++nY2du;MEtEFp>&k%TW{{FIz8$dIe978-^f`AMdAR;U!ifV4blruu( z#6dXI)cC|C;Ya_R731u$^Zow9yFxHgQ9g6u+Urw!ue7$T@m_k*UUAxMg-+AZ{Ssvf zQ%+S{sjiZcQ9RXgw~W1v4`?7bEKfuxY^o1kx;cFFJc*t28(z+8&uDsaar%u%k$b#s zEZ8j>Kw&A`Y7J8lbqL76U_TSo4@~kPwISX{V6Be|qs8jiw%+!6=f^4T`ej0iQ0|;v z#o4}pwkJpji#Xf^DiZf7djVDp(g$_}u&lZBY=6MR{wv8k2Zg^>-2RYRJ0;jEP(k74 zu|w+)-7B3PbLvNafNE-Lce2vK!_IC>zWts2}cMQ1%37(52d610LCF z9a=ssW}>i9Xz%oH)webBk=pIK@hVAsK@p4MU$7g1{ymlCY}a4#o8|uM?vFgzel6N? zUHNiqf61NC_LeF=F7jeN`R(f~IBx#zedseMXv9`kku^T zuAYbrz9Qsz%&7Zn-Xl!~kt?5GWLSqBDL!@iwvCd-V~`>sNcj&0KpN&>Mj-bWSO$_t zn1S(c0_8Ih%zF$BVj68~>ww0A^50ybK2W}e1DGX10VF0|8k{EKJYb;?W<%`<)=^8K za*Uv|AEu5-H+j+6O(3^}FetnRo7*S}FHjjujW`5{2~s-)9DPAoQvKcUJ~nD~3yF_w z)_7diwzHho?Z5?(uP3<wYCszeEMBHd&`V>e+UQtCUHqhR#_vU^d=5+dvcrE6FkqFtCEm2YIY2_^p^c@~KY3lf}p z4LzP1UeC=>{%4=$)*toFp%to;@ykx literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410366 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410366 new file mode 100644 index 0000000000000000000000000000000000000000..9a1eb601e6eaa7c04c6662fa3cf0e5c48d7895c7 GIT binary patch literal 4996 zcmZQzfPi{WB!mRC9N-ObX4)n>T;%yX%#QfvSYpvIkbDXr?x8 zEqp8}B;LGhw|U`>@|t4{K6ZO_^sGJq#Kf)dn$@|0N6`i94+2)1O-|&|S4hwC~}jw#J;l4Xcfq-ius+87lbM zZvBLGw=St7pLLVxO-amX{J`p$vA(qRaL9FA9pmE_O)+y%^te0`{%erBb;fLzRc{KB!i z7!V5rP89>`6b2t}2N1m^u!iSGB&%SN;}X3?n?iXRX3uE<<)ohQovrr4kFSe`oyfr0wg4CghCrn( zAoWlHia&5164cMYzynen;teu_f#Jx@Zw0IWMar()!h5OrnMr%(E(hK}Q%l4h;<%SH zYaA|ws5JtseN-4NR=>9Ow$D31PI1>S6H0_~=j(_@MiW`Wz zKnx_n2zEa(4JjLZ7JvWQ{;>V>3IW%o#rs^(yWig$d(~I6nDNcLRIjTQixle{5|5tO z-?VU|!LeJK&3^qpA2d&L1otk#`_W-NKhR8YSmm4fsfb_HkiY&VQ%?TTEuL9RV~+|R z$Sj``XFtb3B0m8dRyHv8P`ALtilBZ51~#A`U|2!?|0{j!bE_qqvG?Da{dh2u+kYFg zrJeOg<_XvJCau|*>6iypEACPD0%{EdBiIc<|0Z6sd~UG+$@;d%9~b1S1bUUZZcp_4 zYH0R4bq5kMh8^LQZUFl?#Xs;V zQ+XWUv1(7H9rvz!rpymGeBaL^#A=_!l+XK`eZl@^VBE^U^nE_a-7trMXkcFd45V1@ z_&JN29PqpMd7&Rx_Nl zE}3zMy{7H4$%Q9{N&17;6@pK6SGGHM6)V1V_S$w6vTYu&2O^)Jl zJ5^%wx;#U`!g!t5%*EG0Zj~sixdl_s2=*T^EMMronW3eS&Hd`{beRc$>WU(JLbsGM zEMZ^z?$etmf`#$+;YPnW`2OCXko@DN(}iwdlPFGwyCOf@)s}qF8S)ofScKrQzG)2U`ltXAl4jM^L^&0Yv0Apn7nbM6>+H z)>qyDv;>sbKzR-sfYJ;zu&l6z2@>O8sY}Hn(DDUdCc*T>0s_cKV!~u0X%xu`3l@gpxF9CY4`8_gD>K0UCBm)n`jMFS z0W?@)^(1nb!qPdt>;>BcZLdI`1*bq^U;^Zk;1;6WQ%oV;PZ$_BF507a6l@=bVL*|+ z$aXV8>k@b%tX`Xvsm7kO?5gq12B{NkgjfQ#@>f_Ct(kE~Z%($I^(2VvG3rTBdj$qy zWvVSqfQU91gY4OqIl%A-wc9}MLvbS#2Z;$Y9cTFkwjWr2U4g2Er8O{@C^wzH`-jGE z0)-VUyx?`vAaNTd;RW&&N?ajvkeJYzL94HTqWJ0{WI3=|M1&W-ydt7K4|5c@@(NT& zkQ-053lqxR1acP~AVm%m6E2OS7AT4w7sz7^3_8z#+Huc5cIndUYd>dM>V(hjaClhv zB8~gzkId@49jsTuCIZ{r|A7F=hS|jkf}^4ht`M`Wz%~Ln%i{bQ7pgON}@LhY3 literal 0 HcmV?d00001 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410367 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410367 new file mode 100644 index 0000000000000000000000000000000000000000..68ee1497fa693d5940f67a5470fabe7b2a78f7bc GIT binary patch literal 6884 zcmZQzfB?<8egX{V-`skpTvUE*$DW{A_hqRyFK@YJ8aOZYI?i7PR3-fG*9MK$Ck-bx z-A+vTw10*~n`-V(mPw&mdGqGaeRsX`usxul(gvVJ&27Aj39c2%2|m$b4%HLHD^UfuUTS!c$L0UriG|&kN)hfr*l6T z1C=;%SAUqpKKshM*u*a(e0i&8H(lG8^uu&}ocFqaZieaMR~E`!Ghd%}=0ej_=Lw!0 zrA%x#OzC+TVt7#V+I#V}tWQ`!NWK5URH-|6O^w#qsQt&Y{;+ku+q&FcUjM>2>7=h> zdc02gdoOL-r+M#$b-FA^ai5>4>V)MX)1&X)xu4V5>#oNj+G@}HVB6;9DIga!AHQ&1 zY7U460jElVbP9uyw*!b?5?I4?Ba&6H$Z?6@p-rK@46|pn|8i1K_|8`Q;7I6{S<`uD z^FQ3$Ew8@NJ!O1VE4)j|<1u^m<82T(F)$E{ zE1)_SAZ7xoPw@|Y%2Xc5cdXh|X~(^*o+tCG%&k%TW{{FIz8$k1z978-^f`AMd zNI!KgxndSyY46rw`FfM1INVN^SiCOJ5U?;_r!{l&wa-9x5=Aw)V9FW6{sX45PIJXL zOT)R}=QmDXym8uRao>qQr%mrH)zH#7Tj#lbm!XK>kJ{F@#bL_-*DJZZwaWA@5{!Py zw8n?Kbk~oQ6Ixh-27<%#&+etuugSeLocl5Fg6psPubI~v&##ood|=`9(OcMYnhH29 zMO#gQ>OtWT1MsjRsGos>2dIfL#M=n0_fcWASpC}8+dl97IK^GROehh`owKVr+xO4* z1nFQA2UuQXVEy_Kq@Ejyxxg9`1S8m8z%Y0yv-W(MNs;A-QqCB=dy96z?SJJTX^<=y zcRS}#W7DcA=nowOSQiSJhIa|w0u^~ zL}8!M-s#<{Z)@ZuwcB&!Rg(6CA{!oFztX2Zw_2hZd;hK3j|UUE{kJh&+F5U8o^V}n z(wcplj(H#h@r4!G{lKtVw>G`|?)Bw8W}+-Y=alR{s;PkSmQU7{WXLl5X&7Bl}rMtd+ zc7LzbjBVOo&gpYDF*(?Qfk9mW==}2x+|?35J>dKUq`?51_dsHt1;qtc#>Qp_kPv~Z z1FI)4AB6_HK+OeFMC2m|h8fcupjsFqZe?(o)idkwj)aUUcUj+**l9mH+O|mOxH^Mv zj_yP2X@9)$$nAK-ap!P)(bYR5`;<1`KYv0^Ytue|`)$XMulm^Lzi~ULj$nx^-R9jM z`G)nw?3zSFHA3phFOBjgsT9@C7cJc8w8;C1LNr?RE`l;mq6J> zx~XCojok!tJ1o2go7*S}uLVGVQ6mn)VS?0d0Y~3)|1z!Oe+_|p5|+K66(sI$$-b=3 z=ECH7T~a-p~qV`>g$|`8c&r|Hyqg*)_m&Mvf8OjqAkRfIU;*j zzmWvS7Z8Bj7+?U4JCrmDk|Cl^22^#b1V{keTC{1GvGtWV0L=%rUqS6yWB{wbtYCt~ zxL4{@aR{(J2DK+Z^)*WQ2nz_HI1&?PI;vWzeo%c6wjY=ek3dzz;vdW<&P_fpGnfPGcw|fB74DhL;LnnXCbJ@dy#K)vQ4-Xew)ma zd+r>+`bbfg)6<=&WRHs|*Kb+Z2~kP7o+M)Y251y`%n&{%LF^a~DfJ&Q?ts>Rpu9?# z`j0p_f$Kk7x(RFjH%QzD%M;+_MxvW&SH}Q{#K36;sje9`Zllaic= 0; i-- { + if beginOp, ok := operations[i].Body.GetBeginSponsoringFutureReservesOp(); ok && + beginOp.SponsoredId.Address() == sponsoree { + participants = append(participants, beginOp.SponsoredId.Address()) + } + } + } + + case xdr.OperationTypeRevokeSponsorship: + op := operation.Body.MustRevokeSponsorshipOp() + switch op.Type { + case xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry: + participants = append(participants, getLedgerKeyParticipants(*op.LedgerKey)...) + + case xdr.RevokeSponsorshipTypeRevokeSponsorshipSigner: + participants = append(participants, op.Signer.AccountId.Address()) + // We don't add signer as a participant because a signer can be + // arbitrary account. This can spam successful operations + // history of any account. + } + + case xdr.OperationTypeClawback: + op := operation.Body.MustClawbackOp() + participants = append(participants, op.From.ToAccountId().Address()) + + case xdr.OperationTypeSetTrustLineFlags: + op := operation.Body.MustSetTrustLineFlagsOp() + participants = append(participants, op.Trustor.Address()) + + // for the following, the only direct participant is the source_account + case xdr.OperationTypeManageBuyOffer: + case xdr.OperationTypeManageSellOffer: + case xdr.OperationTypeCreatePassiveSellOffer: + case xdr.OperationTypeSetOptions: + case xdr.OperationTypeChangeTrust: + case xdr.OperationTypeInflation: + case xdr.OperationTypeManageData: + case xdr.OperationTypeBumpSequence: + case xdr.OperationTypeClaimClaimableBalance: + case xdr.OperationTypeClawbackClaimableBalance: + case xdr.OperationTypeLiquidityPoolDeposit: + case xdr.OperationTypeLiquidityPoolWithdraw: + + default: + return nil, fmt.Errorf("unknown operation type: %s", operation.Body.Type) + } + return participants, nil +} + +// getLedgerKeyParticipants returns a list of accounts that are considered +// "participants" in a particular ledger entry. +// +// This list will have zero or one element, making it easy to expand via `...`. +func getLedgerKeyParticipants(ledgerKey xdr.LedgerKey) []string { + switch ledgerKey.Type { + case xdr.LedgerEntryTypeAccount: + return []string{ledgerKey.Account.AccountId.Address()} + case xdr.LedgerEntryTypeData: + return []string{ledgerKey.Data.AccountId.Address()} + case xdr.LedgerEntryTypeOffer: + return []string{ledgerKey.Offer.SellerId.Address()} + case xdr.LedgerEntryTypeTrustline: + return []string{ledgerKey.TrustLine.AccountId.Address()} + case xdr.LedgerEntryTypeClaimableBalance: + // nothing to do + } + return []string{} +} + +func getIndex(ledger xdr.LedgerCloseMeta, mode AccountIndexMode) uint32 { + switch mode { + case ByCheckpoint: + return GetCheckpointNumber(ledger.LedgerSequence()) + case ByLedger: + return ledger.LedgerSequence() + default: + return 0 + } +} diff --git a/exp/lighthorizon/index/store.go b/exp/lighthorizon/index/store.go new file mode 100644 index 0000000000..de5f4f6f07 --- /dev/null +++ b/exp/lighthorizon/index/store.go @@ -0,0 +1,377 @@ +package index + +import ( + "encoding/binary" + "encoding/hex" + "io" + "os" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + backend "github.com/stellar/go/exp/lighthorizon/index/backend" + types "github.com/stellar/go/exp/lighthorizon/index/types" + "github.com/stellar/go/support/log" +) + +type Store interface { + NextActive(account, index string, afterCheckpoint uint32) (uint32, error) + TransactionTOID(hash [32]byte) (int64, error) + + AddTransactionToIndexes(txnTOID int64, hash [32]byte) error + AddParticipantsToIndexes(checkpoint uint32, index string, participants []string) error + AddParticipantsToIndexesNoBackend(checkpoint uint32, index string, participants []string) error + AddParticipantToIndexesNoBackend(participant string, indexes types.NamedIndices) + + Flush() error + FlushAccounts() error + ClearMemory(bool) + + Read(account string) (types.NamedIndices, error) + ReadAccounts() ([]string, error) + ReadTransactions(prefix string) (*types.TrieIndex, error) + + MergeTransactions(prefix string, other *types.TrieIndex) error + + RegisterMetrics(registry *prometheus.Registry) +} + +type StoreConfig struct { + // init time config + // the base url for the store resource + URL string + // optional url path to append to the base url to realize the complete url + URLSubPath string + Workers uint32 + + // runtime config + ClearMemoryOnFlush bool + + // logging & metrics + Log *log.Entry // TODO: unused for now + Metrics *prometheus.Registry +} + +type store struct { + mutex sync.RWMutex + config StoreConfig + + // data + indexes map[string]types.NamedIndices + txIndexes map[string]*types.TrieIndex + backend backend.Backend + + // metrics + indexWorkingSet prometheus.Gauge + indexWorkingSetTime prometheus.Gauge // to check if the above takes too long lmao +} + +func NewStore(backend backend.Backend, config StoreConfig) (Store, error) { + result := &store{ + indexes: map[string]types.NamedIndices{}, + txIndexes: map[string]*types.TrieIndex{}, + backend: backend, + + config: config, + + indexWorkingSet: newHorizonLiteGauge("working_set", + "Approximately how much memory (kiB) are indices using?"), + indexWorkingSetTime: newHorizonLiteGauge("working_set_time", + "How long did it take (μs) to calculate the working set size?"), + } + result.RegisterMetrics(config.Metrics) + + return result, nil +} + +func (s *store) accounts() []string { + accounts := make([]string, 0, len(s.indexes)) + for account := range s.indexes { + accounts = append(accounts, account) + } + return accounts +} + +func (s *store) FlushAccounts() error { + s.mutex.Lock() + defer s.mutex.Unlock() + return s.backend.FlushAccounts(s.accounts()) +} + +func (s *store) Read(account string) (types.NamedIndices, error) { + return s.backend.Read(account) +} + +func (s *store) ReadAccounts() ([]string, error) { + return s.backend.ReadAccounts() +} + +func (s *store) ReadTransactions(prefix string) (*types.TrieIndex, error) { + return s.getCreateTrieIndex(prefix) +} + +func (s *store) MergeTransactions(prefix string, other *types.TrieIndex) error { + defer s.approximateWorkingSet() + + index, err := s.getCreateTrieIndex(prefix) + if err != nil { + return err + } + if err := index.Merge(other); err != nil { + return err + } + + s.mutex.Lock() + defer s.mutex.Unlock() + s.txIndexes[prefix] = index + return nil +} + +func (s *store) approximateWorkingSet() { + if s.config.Metrics == nil { + return + } + + start := time.Now() + approx := float64(0) + + for _, indices := range s.indexes { + firstIndexSize := 0 + for _, index := range indices { + firstIndexSize = index.Size() + break + } + + // There may be multiple indices for each account, but we can do a rough + // approximation for now by just assuming they're all around the same + // size. + approx += float64(len(indices) * firstIndexSize) + } + + for _, trie := range s.txIndexes { + // FIXME: Is this too slow? We probably want a TrieIndex.Size() method, + // but that's not trivial to determine for a trie. + trie.Iterate(func(key, value []byte) { + approx += float64(len(key) + len(value)) + }) + } + + s.indexWorkingSet.Set(approx / 1024) // kiB + s.indexWorkingSetTime.Set(float64(time.Since(start).Microseconds())) // μs +} + +func (s *store) Flush() error { + s.mutex.Lock() + defer s.mutex.Unlock() + defer s.approximateWorkingSet() + + if err := s.backend.Flush(s.indexes); err != nil { + return err + } + + if err := s.backend.FlushAccounts(s.accounts()); err != nil { + return err + } else if s.config.ClearMemoryOnFlush { + s.indexes = map[string]types.NamedIndices{} + } + + if err := s.backend.FlushTransactions(s.txIndexes); err != nil { + return err + } else if s.config.ClearMemoryOnFlush { + s.txIndexes = map[string]*types.TrieIndex{} + } + + return nil +} + +func (s *store) ClearMemory(doClear bool) { + s.config.ClearMemoryOnFlush = doClear +} + +func (s *store) AddTransactionToIndexes(txnTOID int64, hash [32]byte) error { + index, err := s.getCreateTrieIndex(hex.EncodeToString(hash[:1])) + if err != nil { + return err + } + + value := make([]byte, 8) + binary.BigEndian.PutUint64(value, uint64(txnTOID)) + + // We don't have to re-calculate the whole working set size for metrics + // since we're adding a known size. + if _, replaced := index.Upsert(hash[1:], value); !replaced { + s.indexWorkingSet.Add(float64(len(hash) - 1 + len(value))) + } + + return nil +} + +func (s *store) TransactionTOID(hash [32]byte) (int64, error) { + index, err := s.getCreateTrieIndex(hex.EncodeToString(hash[:1])) + if err != nil { + return 0, err + } + + value, ok := index.Get(hash[1:]) + if !ok { + return 0, io.EOF + } + return int64(binary.BigEndian.Uint64(value)), nil +} + +// AddParticipantsToIndexesNoBackend is a temp version of +// AddParticipantsToIndexes that skips backend downloads and it used in AWS +// Batch. Refactoring required to make it better. +func (s *store) AddParticipantsToIndexesNoBackend(checkpoint uint32, index string, participants []string) error { + s.mutex.Lock() + defer s.mutex.Unlock() + defer s.approximateWorkingSet() + + var err error + for _, participant := range participants { + if _, ok := s.indexes[participant]; !ok { + s.indexes[participant] = map[string]*types.BitmapIndex{} + } + + ind, ok := s.indexes[participant][index] + if !ok { + ind = &types.BitmapIndex{} + s.indexes[participant][index] = ind + } + + if innerErr := ind.SetActive(checkpoint); innerErr != nil { + err = innerErr + } + // don't break early, instead try to save as many participants as we can + } + + return err +} + +func (s *store) AddParticipantToIndexesNoBackend(participant string, indexes types.NamedIndices) { + s.mutex.Lock() + defer s.mutex.Unlock() + defer s.approximateWorkingSet() + + s.indexes[participant] = indexes +} + +func (s *store) AddParticipantsToIndexes(checkpoint uint32, index string, participants []string) error { + defer s.approximateWorkingSet() + + for _, participant := range participants { + ind, err := s.getCreateIndex(participant, index) + if err != nil { + return err + } + err = ind.SetActive(checkpoint) + if err != nil { + return err + } + } + return nil +} + +func (s *store) getCreateIndex(account, id string) (*types.BitmapIndex, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + defer s.approximateWorkingSet() + + // Check if we already have it loaded + accountIndexes, ok := s.indexes[account] + if !ok { + accountIndexes = types.NamedIndices{} + } + ind, ok := accountIndexes[id] + if ok { + return ind, nil + } + + // Check if index exists in backend + found, err := s.backend.Read(account) + if err == nil { + accountIndexes = found + } else if !os.IsNotExist(err) { + return nil, err + } + + ind, ok = accountIndexes[id] + if !ok { + // Not found anywhere, make a new one. + ind = &types.BitmapIndex{} + accountIndexes[id] = ind + } + + // We don't want to replace the entire index map in memory (even though we + // read all of it from disk), just the one we loaded from disk. Otherwise, + // we lose in-memory changes to unrelated indices. + if memoryIndices, ok := s.indexes[account]; ok { // account exists in-mem + if memoryIndex, ok2 := memoryIndices[id]; ok2 { // id exists in-mem + if memoryIndex != accountIndexes[id] { // not using in-mem already + memoryIndex.Merge(ind) + s.indexes[account][id] = memoryIndex + } + } + } else { + s.indexes[account] = accountIndexes + } + + return ind, nil +} + +func (s *store) NextActive(account, indexId string, afterCheckpoint uint32) (uint32, error) { + defer s.approximateWorkingSet() + + ind, err := s.getCreateIndex(account, indexId) + if err != nil { + return 0, err + } + return ind.NextActiveBit(afterCheckpoint) +} + +func (s *store) getCreateTrieIndex(prefix string) (*types.TrieIndex, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + defer s.approximateWorkingSet() + + // Check if we already have it loaded + index, ok := s.txIndexes[prefix] + if ok { + return index, nil + } + + // Check if index exists in backend + found, err := s.backend.ReadTransactions(prefix) + if err == nil { + s.txIndexes[prefix] = found + } else if !os.IsNotExist(err) { + return nil, err + } + + index, ok = s.txIndexes[prefix] + if !ok { + // Not found anywhere, make a new one. + index = &types.TrieIndex{} + s.txIndexes[prefix] = index + } + + return index, nil +} + +func (s *store) RegisterMetrics(registry *prometheus.Registry) { + s.config.Metrics = registry + + if registry != nil { + registry.Register(s.indexWorkingSet) + registry.Register(s.indexWorkingSetTime) + } +} + +func newHorizonLiteGauge(name, help string) prometheus.Gauge { + return prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "horizon_lite", + Subsystem: "index_store", + Name: name, + Help: help, + }) +} diff --git a/exp/lighthorizon/index/types/bitmap.go b/exp/lighthorizon/index/types/bitmap.go new file mode 100644 index 0000000000..171115938b --- /dev/null +++ b/exp/lighthorizon/index/types/bitmap.go @@ -0,0 +1,367 @@ +package index + +import ( + "bytes" + "fmt" + "io" + "strings" + "sync" + + "github.com/stellar/go/support/ordered" + "github.com/stellar/go/xdr" +) + +const BitmapIndexVersion = 1 + +type BitmapIndex struct { + mutex sync.RWMutex + bitmap []byte + firstBit uint32 + lastBit uint32 +} + +type NamedIndices map[string]*BitmapIndex + +func NewBitmapIndex(b []byte) (*BitmapIndex, error) { + xdrBitmap := xdr.BitmapIndex{} + err := xdrBitmap.UnmarshalBinary(b) + if err != nil { + return nil, err + } + + return NewBitmapIndexFromXDR(xdrBitmap), nil +} + +func NewBitmapIndexFromXDR(index xdr.BitmapIndex) *BitmapIndex { + return &BitmapIndex{ + bitmap: index.Bitmap[:], + firstBit: uint32(index.FirstBit), + lastBit: uint32(index.LastBit), + } +} + +func (i *BitmapIndex) Size() int { + return len(i.bitmap) +} + +func (i *BitmapIndex) SetActive(index uint32) error { + i.mutex.Lock() + defer i.mutex.Unlock() + return i.setActive(index) +} + +func (i *BitmapIndex) SetInactive(index uint32) error { + i.mutex.Lock() + defer i.mutex.Unlock() + return i.setInactive(index) +} + +// bitShiftLeft returns a byte with the bit set corresponding to the index. In +// other words, it flips the bit corresponding to the index's "position" mod-8. +func bitShiftLeft(index uint32) byte { + if index%8 == 0 { + return 1 + } else { + return byte(1) << (8 - index%8) + } +} + +// rangeFirstBit returns the index of the first *possible* active bit in the +// bitmap. In other words, if you just have SetActive(12), this will return 9, +// because you have one byte (0b0001_0000) and the *first* value the bitmap can +// represent is 9. +func (i *BitmapIndex) rangeFirstBit() uint32 { + return (i.firstBit-1)/8*8 + 1 +} + +// rangeLastBit returns the index of the last *possible* active bit in the +// bitmap. In other words, if you just have SetActive(12), this will return 16, +// because you have one byte (0b0001_0000) and the *last* value the bitmap can +// represent is 16. +func (i *BitmapIndex) rangeLastBit() uint32 { + return i.rangeFirstBit() + uint32(len(i.bitmap))*8 - 1 +} + +func (i *BitmapIndex) setActive(index uint32) error { + if i.firstBit == 0 { + i.firstBit = index + i.lastBit = index + b := bitShiftLeft(index) + i.bitmap = []byte{b} + } else { + if index >= i.rangeFirstBit() && index <= i.rangeLastBit() { + // Update the bit in existing range + b := bitShiftLeft(index) + loc := (index - i.rangeFirstBit()) / 8 + i.bitmap[loc] = i.bitmap[loc] | b + + if index < i.firstBit { + i.firstBit = index + } + if index > i.lastBit { + i.lastBit = index + } + } else { + // Expand the bitmap + if index < i.rangeFirstBit() { + // ...to the left + newBytes := make([]byte, distance(index, i.rangeFirstBit())) + i.bitmap = append(newBytes, i.bitmap...) + b := bitShiftLeft(index) + i.bitmap[0] = i.bitmap[0] | b + + i.firstBit = index + } else if index > i.rangeLastBit() { + // ... to the right + newBytes := make([]byte, distance(i.rangeLastBit(), index)) + i.bitmap = append(i.bitmap, newBytes...) + b := bitShiftLeft(index) + loc := (index - i.rangeFirstBit()) / 8 + i.bitmap[loc] = i.bitmap[loc] | b + + i.lastBit = index + } + } + } + + return nil +} + +func (i *BitmapIndex) setInactive(index uint32) error { + // Is this index even active in the first place? + if i.firstBit == 0 || index < i.rangeFirstBit() || index > i.rangeLastBit() { + return nil // not really an error + } + + loc := (index - i.rangeFirstBit()) / 8 // which byte? + b := bitShiftLeft(index) // which bit w/in the byte? + i.bitmap[loc] &= ^b // unset only that bit + + // If unsetting this bit made the first byte empty OR we unset the earliest + // set bit, we need to find the next "first" active bit. + if loc == 0 && i.firstBit == index { + // find the next active bit to set as the start + nextBit, err := i.nextActiveBit(index) + if err == io.EOF { + i.firstBit = 0 + i.lastBit = 0 + i.bitmap = []byte{} + } else if err != nil { + return err + } else { + // Trim all (now-)empty bytes off the front. + i.bitmap = i.bitmap[distance(i.firstBit, nextBit):] + i.firstBit = nextBit + } + } else if int(loc) == len(i.bitmap)-1 { + idx := -1 + + if i.bitmap[loc] == 0 { + // find the latest non-empty byte, to set as the new "end" + j := len(i.bitmap) - 1 + for i.bitmap[j] == 0 { + j-- + } + + i.bitmap = i.bitmap[:j+1] + idx = 8 + } else if i.lastBit == index { + // Get the "bit number" of the last active bit (i.e. the one we just + // turned off) to mark the starting point for the search. + idx = 8 + if index%8 != 0 { + idx = int(index % 8) + } + } + + // Do we need to adjust the range? Imagine we had 0b0011_0100 and we + // unset the last active bit. + // ^ + // Then, we need to adjust our internal lastBit tracker to represent the + // ^ bit above. This means finding the first previous set bit. + if idx > -1 { + l := uint32(len(i.bitmap) - 1) + // Imagine we had 0b0011_0100 and we unset the last active bit. + // ^ + // Then, we need to adjust our internal lastBit tracker to represent + // the ^ bit above. This means finding the first previous set bit. + j, ok := int(idx), false + for ; j >= 0 && !ok; j-- { + _, ok = maxBitAfter(i.bitmap[l], uint32(j)) + } + + // We know from the earlier conditional that *some* bit is set, so + // we know that j represents the index of the bit that's the new + // "last active" bit. + firstByte := i.rangeFirstBit() + i.lastBit = firstByte + (l * 8) + uint32(j) + 1 + } + } + + return nil +} + +//lint:ignore U1000 Ignore unused function temporarily +func (i *BitmapIndex) isActive(index uint32) bool { + if index >= i.firstBit && index <= i.lastBit { + b := bitShiftLeft(index) + loc := (index - i.rangeFirstBit()) / 8 + return i.bitmap[loc]&b != 0 + } else { + return false + } +} + +func (i *BitmapIndex) iterate(f func(index uint32)) error { + i.mutex.RLock() + defer i.mutex.RUnlock() + + if i.firstBit == 0 { + return nil + } + + f(i.firstBit) + curr := i.firstBit + + for { + var err error + curr, err = i.nextActiveBit(curr + 1) + if err != nil { + if err == io.EOF { + break + } + return err + } + + f(curr) + } + + return nil +} + +func (i *BitmapIndex) Merge(other *BitmapIndex) error { + i.mutex.Lock() + defer i.mutex.Unlock() + + var err error + other.iterate(func(index uint32) { + if err != nil { + return + } + err = i.setActive(index) + }) + + return err +} + +// NextActiveBit returns the next bit position (inclusive) where this index is +// active. "Inclusive" means that if it's already active at `position`, this +// returns `position`. +func (i *BitmapIndex) NextActiveBit(position uint32) (uint32, error) { + i.mutex.RLock() + defer i.mutex.RUnlock() + return i.nextActiveBit(position) +} + +func (i *BitmapIndex) nextActiveBit(position uint32) (uint32, error) { + if i.firstBit == 0 || position > i.lastBit { + // We're past the end. + // TODO: Should this be an error? or how should we signal NONE here? + return 0, io.EOF + } + + if position < i.firstBit { + position = i.firstBit + } + + // Must be within the range, find the first non-zero after our start + loc := (position - i.rangeFirstBit()) / 8 + + // Is it in the same byte? + if shift, ok := maxBitAfter(i.bitmap[loc], (position-1)%8); ok { + return i.rangeFirstBit() + (loc * 8) + shift, nil + } + + // Scan bytes after + loc++ + for ; loc < uint32(len(i.bitmap)); loc++ { + // Find the offset of the set bit + if shift, ok := maxBitAfter(i.bitmap[loc], 0); ok { + return i.rangeFirstBit() + (loc * 8) + shift, nil + } + } + + // all bits after this were zero + // TODO: Should this be an error? or how should we signal NONE here? + return 0, io.EOF +} + +func (i *BitmapIndex) ToXDR() xdr.BitmapIndex { + i.mutex.RLock() + defer i.mutex.RUnlock() + + return xdr.BitmapIndex{ + FirstBit: xdr.Uint32(i.firstBit), + LastBit: xdr.Uint32(i.lastBit), + Bitmap: i.bitmap, + } +} + +func (i *BitmapIndex) Buffer() *bytes.Buffer { + i.mutex.RLock() + defer i.mutex.RUnlock() + + xdrBitmap := i.ToXDR() + b, err := xdrBitmap.MarshalBinary() + if err != nil { + panic(err) + } + return bytes.NewBuffer(b) +} + +// Flush flushes the index data to byte slice in index format. +func (i *BitmapIndex) Flush() []byte { + return i.Buffer().Bytes() +} + +// DebugCompare returns a string that compares this bitmap to another bitmap +// byte-by-byte in binary form as two columns. +func (i *BitmapIndex) DebugCompare(j *BitmapIndex) string { + output := make([]string, ordered.Max(len(i.bitmap), len(j.bitmap))) + for n := 0; n < len(output); n++ { + if n < len(i.bitmap) { + output[n] += fmt.Sprintf("%08b", i.bitmap[n]) + } else { + output[n] += " " + } + + output[n] += " | " + + if n < len(j.bitmap) { + output[n] += fmt.Sprintf("%08b", j.bitmap[n]) + } + } + + return strings.Join(output, "\n") +} + +func maxBitAfter(b byte, after uint32) (uint32, bool) { + if b == 0 { + // empty byte + return 0, false + } + + for shift := uint32(after); shift < 8; shift++ { + mask := byte(0b1000_0000) >> shift + if mask&b != 0 { + return shift, true + } + } + return 0, false +} + +// distance returns how many bytes occur between the two given indices. Note +// that j >= i, otherwise the result will be negative. +func distance(i, j uint32) int { + return (int(j)-1)/8 - (int(i)-1)/8 +} diff --git a/exp/lighthorizon/index/types/bitmap_test.go b/exp/lighthorizon/index/types/bitmap_test.go new file mode 100644 index 0000000000..c5e7864872 --- /dev/null +++ b/exp/lighthorizon/index/types/bitmap_test.go @@ -0,0 +1,382 @@ +package index + +import ( + "fmt" + "io" + "math/rand" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewFromBytes(t *testing.T) { + for i := uint32(1); i < 200; i++ { + t.Run(fmt.Sprintf("New%d", i), func(t *testing.T) { + index := &BitmapIndex{} + index.SetActive(i) + b := index.Flush() + newIndex, err := NewBitmapIndex(b) + require.NoError(t, err) + assert.Equal(t, index.firstBit, newIndex.firstBit) + assert.Equal(t, index.lastBit, newIndex.lastBit) + assert.Equal(t, index.bitmap, newIndex.bitmap) + }) + } +} + +func TestSetActive(t *testing.T) { + cases := []struct { + checkpoint uint32 + rangeFirstCheckpoint uint32 + bitmap []byte + }{ + {1, 1, []byte{0b1000_0000}}, + {2, 1, []byte{0b0100_0000}}, + {3, 1, []byte{0b0010_0000}}, + {4, 1, []byte{0b0001_0000}}, + {5, 1, []byte{0b0000_1000}}, + {6, 1, []byte{0b0000_0100}}, + {7, 1, []byte{0b0000_0010}}, + {8, 1, []byte{0b0000_0001}}, + + {9, 9, []byte{0b1000_0000}}, + {10, 9, []byte{0b0100_0000}}, + {11, 9, []byte{0b0010_0000}}, + {12, 9, []byte{0b0001_0000}}, + {13, 9, []byte{0b0000_1000}}, + {14, 9, []byte{0b0000_0100}}, + {15, 9, []byte{0b0000_0010}}, + {16, 9, []byte{0b0000_0001}}, + } + + for _, tt := range cases { + t.Run(fmt.Sprintf("init_%d", tt.checkpoint), func(t *testing.T) { + index := &BitmapIndex{} + index.SetActive(tt.checkpoint) + + assert.Equal(t, tt.bitmap, index.bitmap) + assert.Equal(t, tt.rangeFirstCheckpoint, index.rangeFirstBit()) + assert.Equal(t, tt.checkpoint, index.firstBit) + assert.Equal(t, tt.checkpoint, index.lastBit) + }) + } + + // Update current bitmap right + index := &BitmapIndex{} + index.SetActive(1) + assert.Equal(t, uint32(1), index.firstBit) + assert.Equal(t, uint32(1), index.lastBit) + index.SetActive(8) + assert.Equal(t, []byte{0b1000_0001}, index.bitmap) + assert.Equal(t, uint32(1), index.firstBit) + assert.Equal(t, uint32(8), index.lastBit) + + // Update current bitmap left + index = &BitmapIndex{} + index.SetActive(8) + assert.Equal(t, uint32(8), index.firstBit) + assert.Equal(t, uint32(8), index.lastBit) + index.SetActive(1) + assert.Equal(t, []byte{0b1000_0001}, index.bitmap) + assert.Equal(t, uint32(1), index.firstBit) + assert.Equal(t, uint32(8), index.lastBit) + + index = &BitmapIndex{} + index.SetActive(10) + index.SetActive(9) + index.SetActive(16) + assert.Equal(t, []byte{0b1100_0001}, index.bitmap) + assert.Equal(t, uint32(9), index.firstBit) + assert.Equal(t, uint32(16), index.lastBit) + + // Expand bitmap to the left + index = &BitmapIndex{} + index.SetActive(10) + index.SetActive(1) + assert.Equal(t, []byte{0b1000_0000, 0b0100_0000}, index.bitmap) + assert.Equal(t, uint32(1), index.firstBit) + assert.Equal(t, uint32(10), index.lastBit) + + index = &BitmapIndex{} + index.SetActive(17) + index.SetActive(2) + assert.Equal(t, []byte{0b0100_0000, 0b0000_0000, 0b1000_0000}, index.bitmap) + assert.Equal(t, uint32(2), index.firstBit) + assert.Equal(t, uint32(17), index.lastBit) + + // Expand bitmap to the right + index = &BitmapIndex{} + index.SetActive(1) + index.SetActive(10) + assert.Equal(t, []byte{0b1000_0000, 0b0100_0000}, index.bitmap) + assert.Equal(t, uint32(1), index.firstBit) + assert.Equal(t, uint32(10), index.lastBit) + + index = &BitmapIndex{} + index.SetActive(2) + index.SetActive(17) + assert.Equal(t, []byte{0b0100_0000, 0b0000_0000, 0b1000_0000}, index.bitmap) + assert.Equal(t, uint32(2), index.firstBit) + assert.Equal(t, uint32(17), index.lastBit) + + index = &BitmapIndex{} + index.SetActive(17) + index.SetActive(26) + assert.Equal(t, []byte{0b1000_0000, 0b0100_0000}, index.bitmap) + assert.Equal(t, uint32(17), index.firstBit) + assert.Equal(t, uint32(26), index.lastBit) +} + +// TestSetInactive ensures that you can flip active bits off and the bitmap +// compresses in size accordingly. +func TestSetInactive(t *testing.T) { + index := &BitmapIndex{} + index.SetActive(17) + index.SetActive(17 + 9) + index.SetActive(17 + 9 + 10) + assert.Equal(t, []byte{0b1000_0000, 0b0100_0000, 0b0001_0000}, index.bitmap) + + // disabling bits should work + index.SetInactive(17) + assert.False(t, index.isActive(17)) + + // it should trim off the first byte now + assert.Equal(t, []byte{0b0100_0000, 0b0001_0000}, index.bitmap) + assert.EqualValues(t, 17+9, index.firstBit) + assert.EqualValues(t, 17+9+10, index.lastBit) + + // it should compress empty bytes on shrink + index = &BitmapIndex{} + index.SetActive(1) + index.SetActive(1 + 2) + index.SetActive(1 + 9) + index.SetActive(1 + 9 + 8 + 9) + assert.Equal(t, []byte{0b1010_0000, 0b0100_0000, 0b0000_0000, 0b0010_0000}, index.bitmap) + + // ...from the left + index.SetInactive(1) + assert.Equal(t, []byte{0b0010_0000, 0b0100_0000, 0b0000_0000, 0b0010_0000}, index.bitmap) + index.SetInactive(3) + assert.Equal(t, []byte{0b0100_0000, 0b0000_0000, 0b0010_0000}, index.bitmap) + assert.EqualValues(t, 1+9, index.firstBit) + assert.EqualValues(t, 1+9+8+9, index.lastBit) + + // ...and the right + index.SetInactive(1 + 9 + 8 + 9) + assert.Equal(t, []byte{0b0100_0000}, index.bitmap) + assert.EqualValues(t, 1+9, index.firstBit) + assert.EqualValues(t, 1+9, index.lastBit) + + // ensure right-hand compression it works for multiple bytes, too + index = &BitmapIndex{} + index.SetActive(2) + index.SetActive(2 + 2) + index.SetActive(2 + 9) + index.SetActive(2 + 9 + 8 + 6) + index.SetActive(2 + 9 + 8 + 9) + index.SetActive(2 + 9 + 8 + 10) + assert.Equal(t, []byte{0b0101_0000, 0b0010_0000, 0b0000_0000, 0b1001_1000}, index.bitmap) + + index.setInactive(2 + 9 + 8 + 10) + assert.Equal(t, []byte{0b0101_0000, 0b0010_0000, 0b0000_0000, 0b1001_0000}, index.bitmap) + assert.EqualValues(t, 2+9+8+9, index.lastBit) + + index.setInactive(2 + 9 + 8 + 9) + assert.Equal(t, []byte{0b0101_0000, 0b0010_0000, 0b0000_0000, 0b1000_0000}, index.bitmap) + assert.EqualValues(t, 2+9+8+6, index.lastBit) + + index.setInactive(2 + 9 + 8 + 6) + assert.Equal(t, []byte{0b0101_0000, 0b0010_0000}, index.bitmap) + assert.EqualValues(t, 2, index.firstBit) + assert.EqualValues(t, 2+9, index.lastBit) + + index.setInactive(2 + 2) + assert.Equal(t, []byte{0b0100_0000, 0b0010_0000}, index.bitmap) + assert.EqualValues(t, 2, index.firstBit) + assert.EqualValues(t, 2+9, index.lastBit) + + index.setInactive(1) // should be a no-op + assert.Equal(t, []byte{0b0100_0000, 0b0010_0000}, index.bitmap) + assert.EqualValues(t, 2, index.firstBit) + assert.EqualValues(t, 2+9, index.lastBit) +} + +// TestFuzzerSetInactive attempt to fuzz random bits into two bitmap sets, one +// by addition, and one by subtraction - then, it compares the outcome. +func TestFuzzySetUnset(t *testing.T) { + permLen := uint32(128) // should be a multiple of 8 + setBitsCount := permLen / 2 + + for n := 0; n < 10_000; n++ { + randBits := rand.Perm(int(permLen)) + setBits := randBits[:setBitsCount] + clearBits := randBits[setBitsCount:] + + // set all first, then clear the others + clearBitmap := &BitmapIndex{} + for i := uint32(1); i <= permLen; i++ { + clearBitmap.setActive(i) + } + + setBitmap := &BitmapIndex{} + for i := range setBits { + setBitmap.setActive(uint32(setBits[i]) + 1) + clearBitmap.setInactive(uint32(clearBits[i]) + 1) + } + + require.Equalf(t, setBitmap, clearBitmap, + "bitmaps aren't equal:\n%s", setBitmap.DebugCompare(clearBitmap)) + } +} + +func TestNextActive(t *testing.T) { + t.Run("empty", func(t *testing.T) { + index := &BitmapIndex{} + + i, err := index.NextActiveBit(0) + assert.Equal(t, uint32(0), i) + assert.EqualError(t, err, io.EOF.Error()) + }) + + t.Run("one byte", func(t *testing.T) { + t.Run("after last", func(t *testing.T) { + index := &BitmapIndex{} + index.SetActive(3) + + // 16 is well-past the end + i, err := index.NextActiveBit(16) + assert.Equal(t, uint32(0), i) + assert.EqualError(t, err, io.EOF.Error()) + }) + + t.Run("only one bit in the byte", func(t *testing.T) { + index := &BitmapIndex{} + index.SetActive(1) + + i, err := index.NextActiveBit(1) + assert.NoError(t, err) + assert.Equal(t, uint32(1), i) + }) + + t.Run("only one bit in the byte (offset)", func(t *testing.T) { + index := &BitmapIndex{} + index.SetActive(9) + + i, err := index.NextActiveBit(1) + assert.NoError(t, err) + assert.Equal(t, uint32(9), i) + }) + + severalSet := &BitmapIndex{} + severalSet.SetActive(9) + severalSet.SetActive(11) + + t.Run("several bits set (first)", func(t *testing.T) { + i, err := severalSet.NextActiveBit(9) + assert.NoError(t, err) + assert.Equal(t, uint32(9), i) + }) + + t.Run("several bits set (second)", func(t *testing.T) { + i, err := severalSet.NextActiveBit(10) + assert.NoError(t, err) + assert.Equal(t, uint32(11), i) + }) + + t.Run("several bits set (second, inclusive)", func(t *testing.T) { + i, err := severalSet.NextActiveBit(11) + assert.NoError(t, err) + assert.Equal(t, uint32(11), i) + }) + }) + + t.Run("many bytes", func(t *testing.T) { + index := &BitmapIndex{} + index.SetActive(9) + index.SetActive(129) + + // Before the first + i, err := index.NextActiveBit(8) + assert.NoError(t, err) + assert.Equal(t, uint32(9), i) + + // at the first + i, err = index.NextActiveBit(9) + assert.NoError(t, err) + assert.Equal(t, uint32(9), i) + + // In the middle + i, err = index.NextActiveBit(11) + assert.NoError(t, err) + assert.Equal(t, uint32(129), i) + + // At the end + i, err = index.NextActiveBit(129) + assert.NoError(t, err) + assert.Equal(t, uint32(129), i) + + // after the end + i, err = index.NextActiveBit(130) + assert.EqualError(t, err, io.EOF.Error()) + assert.Equal(t, uint32(0), i) + }) +} + +func TestMaxBitAfter(t *testing.T) { + for _, tc := range []struct { + b byte + after uint32 + shift uint32 + ok bool + }{ + {0b0000_0000, 0, 0, false}, + {0b0000_0000, 1, 0, false}, + {0b1000_0000, 0, 0, true}, + {0b0100_0000, 0, 1, true}, + {0b0100_0000, 1, 1, true}, + {0b0010_1000, 0, 2, true}, + {0b0010_1000, 1, 2, true}, + {0b0010_1000, 2, 2, true}, + {0b0010_1000, 3, 4, true}, + {0b0010_1000, 4, 4, true}, + {0b0000_0001, 7, 7, true}, + } { + t.Run(fmt.Sprintf("0b%b,%d", tc.b, tc.after), func(t *testing.T) { + shift, ok := maxBitAfter(tc.b, tc.after) + assert.Equal(t, tc.ok, ok) + assert.Equal(t, tc.shift, shift) + }) + } +} + +func TestMerge(t *testing.T) { + a := &BitmapIndex{} + require.NoError(t, a.SetActive(9)) + require.NoError(t, a.SetActive(129)) + + b := &BitmapIndex{} + require.NoError(t, b.SetActive(900)) + require.NoError(t, b.SetActive(1000)) + + var checkpoints []uint32 + b.iterate(func(c uint32) { + checkpoints = append(checkpoints, c) + }) + + assert.Equal(t, []uint32{900, 1000}, checkpoints) + + require.NoError(t, a.Merge(b)) + + assert.True(t, a.isActive(9)) + assert.True(t, a.isActive(129)) + assert.True(t, a.isActive(900)) + assert.True(t, a.isActive(1000)) + + checkpoints = []uint32{} + a.iterate(func(c uint32) { + checkpoints = append(checkpoints, c) + }) + + assert.Equal(t, []uint32{9, 129, 900, 1000}, checkpoints) +} diff --git a/exp/lighthorizon/index/types/trie.go b/exp/lighthorizon/index/types/trie.go new file mode 100644 index 0000000000..b5fc39c0ca --- /dev/null +++ b/exp/lighthorizon/index/types/trie.go @@ -0,0 +1,345 @@ +package index + +import ( + "bufio" + "encoding" + "io" + "sync" + + "github.com/stellar/go/xdr" +) + +const ( + TrieIndexVersion = 1 + + HeaderHasPrefix = 0b0000_0001 + HeaderHasValue = 0b0000_0010 + HeaderHasChildren = 0b0000_0100 +) + +type TrieIndex struct { + sync.RWMutex + Root *trieNode `json:"root"` +} + +// TODO: Store the suffix here so we can truncate the branches +type trieNode struct { + // Common prefix we ignore + Prefix []byte `json:"prefix,omitempty"` + + // The value of this node. + Value []byte `json:"value,omitempty"` + + // Any children of this node, mapped by the next byte of their path + Children map[byte]*trieNode `json:"children,omitempty"` +} + +func NewTrieIndexFromBytes(r io.Reader) (*TrieIndex, error) { + var index TrieIndex + if _, err := index.ReadFrom(r); err != nil { + return nil, err + } + return &index, nil +} + +func (index *TrieIndex) Upsert(key, value []byte) ([]byte, bool) { + if len(key) == 0 { + panic("len(key) must be > 0") + } + index.Lock() + defer index.Unlock() + return index.doUpsert(key, value) +} + +func (index *TrieIndex) doUpsert(key, value []byte) ([]byte, bool) { + if index.Root == nil { + index.Root = &trieNode{Prefix: key, Value: value} + return nil, false + } + + node := index.Root + var parent *trieNode + var parentIdx byte + splitPos := 0 + for len(key) > 0 { + for splitPos < len(node.Prefix) && len(key) > 0 { + if node.Prefix[splitPos] != key[0] { + break + } + splitPos++ + key = key[1:] + } + if splitPos != len(node.Prefix) { + // split this node + break + } + if len(key) == 0 { + // simple update-in-place at this node + break + } + + // Jump to the next child + parent = node + parentIdx = key[0] + child, ok := node.Children[key[0]] + if !ok { + if node.Children == nil { + node.Children = map[byte]*trieNode{} + } + // child doesn't exist. Insert a new node + node.Children[key[0]] = &trieNode{ + Prefix: key[1:], + Value: value, + } + return nil, false + } + node = child + key = key[1:] + splitPos = 0 + } + + // Key fully consumed just as we reached "node" + if len(key) == 0 { + if splitPos == len(node.Prefix) { + // node prefix matches (or is none), simple update-in-place + prev := node.Value + node.Value = value + return prev, true + } else { + // node has a prefix, so we need to insert a new one here and push it down + splitNode := &trieNode{ + Prefix: node.Prefix[:splitPos], // the matching segment + Value: value, + Children: map[byte]*trieNode{}, + } + splitNode.Children[node.Prefix[splitPos]] = node + node.Prefix = node.Prefix[splitPos+1:] // existing part that didn't match + if parent == nil { + index.Root = splitNode + } else { + parent.Children[parentIdx] = splitNode + } + return nil, false + } + } else { + // leftover key + if splitPos == len(node.Prefix) { + // new child + node.Children[key[0]] = &trieNode{ + Prefix: key[1:], + Value: value, + } + return nil, false + } else { + // Need to split the node + splitNode := &trieNode{ + Prefix: node.Prefix[:splitPos], + Children: map[byte]*trieNode{}, + } + splitNode.Children[node.Prefix[splitPos]] = node + splitNode.Children[key[0]] = &trieNode{Prefix: key[1:], Value: value} + node.Prefix = node.Prefix[splitPos+1:] + if parent == nil { + index.Root = splitNode + } else { + parent.Children[parentIdx] = splitNode + } + return nil, false + } + } +} + +func (index *TrieIndex) Get(key []byte) ([]byte, bool) { + index.RLock() + defer index.RUnlock() + if index.Root == nil { + return nil, false + } + + node := index.Root + splitPos := 0 + for len(key) > 0 { + for splitPos < len(node.Prefix) && len(key) > 0 { + if node.Prefix[splitPos] != key[0] { + break + } + splitPos++ + key = key[1:] + } + if splitPos != len(node.Prefix) { + // split this node + break + } + if len(key) == 0 { + // found it + return node.Value, true + } + + // Jump to the next child + child, ok := node.Children[key[0]] + if !ok { + // child doesn't exist + return nil, false + } + node = child + key = key[1:] + splitPos = 0 + } + + if len(key) == 0 { + return node.Value, true + } + return nil, false +} + +func (index *TrieIndex) Iterate(f func(key, value []byte)) { + index.RLock() + defer index.RUnlock() + if index.Root != nil { + index.Root.iterate(nil, f) + } +} + +func (node *trieNode) iterate(prefix []byte, f func(key, value []byte)) { + key := append(prefix, node.Prefix...) + if len(node.Value) > 0 { + f(key, node.Value) + } + + if node.Children != nil { + for b, child := range node.Children { + child.iterate(append(key, b), f) + } + } +} + +// TODO: For now this ignores duplicates. should it error? +func (i *TrieIndex) Merge(other *TrieIndex) error { + i.Lock() + defer i.Unlock() + + other.Iterate(func(key, value []byte) { + i.doUpsert(key, value) + }) + + return nil +} + +func (i *TrieIndex) MarshalBinary() ([]byte, error) { + i.RLock() + defer i.RUnlock() + + xdrRoot := xdr.TrieNode{} + + // Apparently this is possible? + if i.Root != nil { + xdrRoot.Prefix = i.Root.Prefix + xdrRoot.Value = i.Root.Value + xdrRoot.Children = make([]xdr.TrieNodeChild, 0, len(i.Root.Children)) + + for key, node := range i.Root.Children { + buildXdrTrie(key, node, &xdrRoot) + } + } + + xdrIndex := xdr.TrieIndex{Version: TrieIndexVersion, Root: xdrRoot} + return xdrIndex.MarshalBinary() +} + +func (i *TrieIndex) WriteTo(w io.Writer) (int64, error) { + i.RLock() + defer i.RUnlock() + + bytes, err := i.MarshalBinary() + if err != nil { + return int64(len(bytes)), err + } + + count, err := w.Write(bytes) + return int64(count), err +} + +func (i *TrieIndex) UnmarshalBinary(bytes []byte) error { + i.RLock() + defer i.RUnlock() + + xdrIndex := xdr.TrieIndex{} + err := xdrIndex.UnmarshalBinary(bytes) + if err != nil { + return err + } + + i.Root = &trieNode{ + Prefix: xdrIndex.Root.Prefix, + Value: xdrIndex.Root.Value, + Children: make(map[byte]*trieNode, len(xdrIndex.Root.Children)), + } + + for _, node := range xdrIndex.Root.Children { + buildTrie(&node, i.Root) + } + + return nil +} + +func (i *TrieIndex) ReadFrom(r io.Reader) (int64, error) { + i.RLock() + defer i.RUnlock() + + br := bufio.NewReader(r) + bytes, err := io.ReadAll(br) + if err != nil { + return int64(len(bytes)), err + } + + return int64(len(bytes)), i.UnmarshalBinary(bytes) +} + +// buildTrie recursively builds the equivalent `TrieNode` structure from raw +// XDR, creating the key->value child mapping from the flat list of children. +// Here, `xdrNode` is the node we're processing and `parent` is its non-XDR +// parent (i.e. the parent was already converted from XDR). +// +// This is the opposite of buildXdrTrie. +func buildTrie(xdrNode *xdr.TrieNodeChild, parent *trieNode) { + node := &trieNode{ + Prefix: xdrNode.Node.Prefix, + Value: xdrNode.Node.Value, + Children: make(map[byte]*trieNode, len(xdrNode.Node.Children)), + } + parent.Children[xdrNode.Key[0]] = node + + for _, child := range xdrNode.Node.Children { + buildTrie(&child, node) + } +} + +// buildXdrTrie recursively builds the XDR-equivalent TrieNode structure, where +// `i` is the node we're converting and `parent` is the already-converted +// parent. That is, the non-XDR version of `parent` should have had (`key`, `i`) +// as a child. +// +// This is the opposite of buildTrie. +func buildXdrTrie(key byte, node *trieNode, parent *xdr.TrieNode) { + self := xdr.TrieNode{ + Prefix: node.Prefix, + Value: node.Value, + Children: make([]xdr.TrieNodeChild, 0, len(node.Children)), + } + + for key, node := range node.Children { + buildXdrTrie(key, node, &self) + } + + parent.Children = append(parent.Children, xdr.TrieNodeChild{ + Key: [1]byte{key}, + Node: self, + }) +} + +// Ensure we're compatible with stdlib interfaces. +var _ io.WriterTo = &TrieIndex{} +var _ io.ReaderFrom = &TrieIndex{} + +var _ encoding.BinaryMarshaler = &TrieIndex{} +var _ encoding.BinaryUnmarshaler = &TrieIndex{} diff --git a/exp/lighthorizon/index/types/trie_test.go b/exp/lighthorizon/index/types/trie_test.go new file mode 100644 index 0000000000..8745296429 --- /dev/null +++ b/exp/lighthorizon/index/types/trie_test.go @@ -0,0 +1,297 @@ +package index + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "encoding/json" + "math/rand" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func randomTrie(t *testing.T, index *TrieIndex) (*TrieIndex, map[string]uint32) { + if index == nil { + index = &TrieIndex{} + } + inserts := map[string]uint32{} + numInserts := rand.Intn(100) + for j := 0; j < numInserts; j++ { + ledger := uint32(rand.Int63()) + hashBytes := make([]byte, 32) + if _, err := rand.Read(hashBytes); err != nil { + assert.NoError(t, err) + } + hash := hex.EncodeToString(hashBytes) + + inserts[hash] = ledger + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, ledger) + index.Upsert([]byte(hash), b) + } + return index, inserts +} + +func TestTrieIndex(t *testing.T) { + for i := 0; i < 10_000; i++ { + index, inserts := randomTrie(t, nil) + + for key, expected := range inserts { + value, ok := index.Get([]byte(key)) + require.Truef(t, ok, "Key not found: %s", key) + ledger := binary.BigEndian.Uint32(value) + assert.Equalf(t, expected, ledger, + "Key %s found: %v, expected: %v", key, ledger, expected) + } + } +} + +func TestTrieIndexUpsertBasic(t *testing.T) { + index := &TrieIndex{} + + key := "key" + prev, ok := index.Upsert([]byte(key), []byte("a")) + assert.Nil(t, prev) + assert.Falsef(t, ok, "expected nil, got prev: %q", string(prev)) + + prev, ok = index.Upsert([]byte(key), []byte("b")) + assert.Equal(t, "a", string(prev)) + assert.Truef(t, ok, "expected 'a', got prev: %q", string(prev)) + + prev, ok = index.Upsert([]byte(key), []byte("c")) + assert.Equal(t, "b", string(prev)) + assert.Truef(t, ok, "expected 'b', got prev: %q", string(prev)) +} + +func TestTrieIndexSuffixes(t *testing.T) { + index := &TrieIndex{} + + prev, ok := index.Upsert([]byte("a"), []byte("a")) + require.False(t, ok) + require.Nil(t, prev) + + prev, ok = index.Upsert([]byte("ab"), []byte("ab")) + require.False(t, ok) + require.Nil(t, prev) + + prev, ok = index.Get([]byte("a")) + require.True(t, ok) + require.Equal(t, "a", string(prev)) + + prev, ok = index.Get([]byte("ab")) + require.True(t, ok) + require.Equal(t, "ab", string(prev)) + + prev, ok = index.Upsert([]byte("a"), []byte("b")) + require.True(t, ok) + require.Equal(t, "a", string(prev)) + + prev, ok = index.Get([]byte("a")) + require.True(t, ok) + require.Equal(t, "b", string(prev)) +} + +func TestTrieIndexSerialization(t *testing.T) { + for i := 0; i < 10_000; i++ { + t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { + index, inserts := randomTrie(t, nil) + + // Round-trip it to serialization and back + buf := &bytes.Buffer{} + nWritten, err := index.WriteTo(buf) + assert.NoError(t, err) + + read := &TrieIndex{} + nRead, err := read.ReadFrom(buf) + assert.NoError(t, err) + + assert.Equal(t, nWritten, nRead, "read more or less than we wrote") + + for key, expected := range inserts { + value, ok := read.Get([]byte(key)) + require.Truef(t, ok, "Key not found: %s", key) + + ledger := binary.BigEndian.Uint32(value) + assert.Equal(t, expected, ledger, "for key %s", key) + } + }) + } +} + +func requireEqualNodes(t *testing.T, expectedNode, gotNode *trieNode) { + expectedJSON, err := json.Marshal(expectedNode) + require.NoError(t, err) + expected := map[string]interface{}{} + require.NoError(t, json.Unmarshal(expectedJSON, &expected)) + + gotJSON, err := json.Marshal(gotNode) + require.NoError(t, err) + got := map[string]interface{}{} + require.NoError(t, json.Unmarshal(gotJSON, &got)) + + require.Equal(t, expected, got) +} + +func TestTrieIndexUpsertAdvanced(t *testing.T) { + // TODO: This is janky that we inspect the structure, but I want to make sure + // I've gotten the algorithms correct. + makeBase := func() *TrieIndex { + index := &TrieIndex{} + index.Upsert([]byte("annibale"), []byte{1}) + index.Upsert([]byte("annibalesco"), []byte{2}) + return index + } + + t.Run("base", func(t *testing.T) { + base := makeBase() + + baseExpected := &trieNode{ + Prefix: []byte("annibale"), + Value: []byte{1}, + Children: map[byte]*trieNode{ + byte('s'): { + Prefix: []byte("co"), + Value: []byte{2}, + }, + }, + } + requireEqualNodes(t, baseExpected, base.Root) + }) + + for _, tc := range []struct { + key string + expected *trieNode + }{ + {"annientare", &trieNode{ + Prefix: []byte("anni"), + Children: map[byte]*trieNode{ + 'b': { + Prefix: []byte("ale"), + Value: []byte{1}, + Children: map[byte]*trieNode{ + 's': { + Prefix: []byte("co"), + Value: []byte{2}, + }, + }, + }, + 'e': { + Prefix: []byte("ntare"), + Value: []byte{3}, + }, + }, + }}, + {"annibali", &trieNode{ + Prefix: []byte("annibal"), + Children: map[byte]*trieNode{ + 'e': { + Value: []byte{1}, + Children: map[byte]*trieNode{ + 's': { + Prefix: []byte("co"), + Value: []byte{2}, + }, + }, + }, + 'i': { + Value: []byte{3}, + }, + }, + }}, + {"ago", &trieNode{ + Prefix: []byte("a"), + Children: map[byte]*trieNode{ + 'n': { + Prefix: []byte("nibale"), + Value: []byte{1}, + Children: map[byte]*trieNode{ + 's': { + Prefix: []byte("co"), + Value: []byte{2}, + }, + }, + }, + 'g': { + Prefix: []byte("o"), + Value: []byte{3}, + }, + }, + }}, + {"ciao", &trieNode{ + Children: map[byte]*trieNode{ + 'a': { + Prefix: []byte("nnibale"), + Value: []byte{1}, + Children: map[byte]*trieNode{ + 's': { + Prefix: []byte("co"), + Value: []byte{2}, + }, + }, + }, + 'c': { + Prefix: []byte("iao"), + Value: []byte{3}, + }, + }, + }}, + {"anni", &trieNode{ + Prefix: []byte("anni"), + Value: []byte{3}, + Children: map[byte]*trieNode{ + 'b': { + Prefix: []byte("ale"), + Value: []byte{1}, + Children: map[byte]*trieNode{ + 's': { + Prefix: []byte("co"), + Value: []byte{2}, + }, + }, + }, + }, + }}, + } { + t.Run(tc.key, func(t *testing.T) { + // Do our upsert + index := makeBase() + index.Upsert([]byte(tc.key), []byte{3}) + + // Check the tree is shaped right + requireEqualNodes(t, tc.expected, index.Root) + + // Check the value matches expected + value, ok := index.Get([]byte(tc.key)) + require.True(t, ok) + require.Equal(t, []byte{3}, value) + }) + } +} + +func TestTrieIndexMerge(t *testing.T) { + for i := 0; i < 10_000; i++ { + a, aInserts := randomTrie(t, nil) + b, bInserts := randomTrie(t, nil) + + require.NoError(t, a.Merge(b)) + + // Should still have all the A keys + for key, expected := range aInserts { + value, ok := a.Get([]byte(key)) + require.Truef(t, ok, "Key not found: %s", key) + ledger := binary.BigEndian.Uint32(value) + assert.Equalf(t, expected, ledger, "Key %s found", key) + } + + // Should now also have all the B keys + for key, expected := range bInserts { + value, ok := a.Get([]byte(key)) + require.Truef(t, ok, "Key not found: %s", key) + ledger := binary.BigEndian.Uint32(value) + assert.Equalf(t, expected, ledger, "Key %s found", key) + } + } +} diff --git a/exp/lighthorizon/ingester/ingester.go b/exp/lighthorizon/ingester/ingester.go new file mode 100644 index 0000000000..21bb400b50 --- /dev/null +++ b/exp/lighthorizon/ingester/ingester.go @@ -0,0 +1,55 @@ +package ingester + +import ( + "context" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/metaarchive" + + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/xdr" +) + +type IngesterConfig struct { + SourceUrl string + NetworkPassphrase string + + CacheDir string + CacheSize int + + ParallelDownloads uint +} + +type liteIngester struct { + metaarchive.MetaArchive + networkPassphrase string +} + +func (i *liteIngester) PrepareRange(ctx context.Context, r historyarchive.Range) error { + return nil +} + +func (i *liteIngester) NewLedgerTransactionReader( + ledgerCloseMeta xdr.SerializedLedgerCloseMeta, +) (LedgerTransactionReader, error) { + reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta( + i.networkPassphrase, + ledgerCloseMeta.MustV0()) + + return &liteLedgerTransactionReader{reader}, err +} + +type liteLedgerTransactionReader struct { + *ingest.LedgerTransactionReader +} + +func (reader *liteLedgerTransactionReader) Read() (LedgerTransaction, error) { + ingestedTx, err := reader.LedgerTransactionReader.Read() + if err != nil { + return LedgerTransaction{}, err + } + return LedgerTransaction{LedgerTransaction: &ingestedTx}, nil +} + +var _ Ingester = (*liteIngester)(nil) // ensure conformity to the interface +var _ LedgerTransactionReader = (*liteLedgerTransactionReader)(nil) diff --git a/exp/lighthorizon/ingester/main.go b/exp/lighthorizon/ingester/main.go new file mode 100644 index 0000000000..a93636c67a --- /dev/null +++ b/exp/lighthorizon/ingester/main.go @@ -0,0 +1,87 @@ +package ingester + +import ( + "context" + "fmt" + "net/url" + + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/ingest" + "github.com/stellar/go/metaarchive" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/log" + "github.com/stellar/go/support/storage" + "github.com/stellar/go/xdr" +) + +// +// LightHorizon data model +// + +// Ingester combines a source of unpacked ledger metadata and a way to create a +// ingestion reader interface on top of it. +type Ingester interface { + metaarchive.MetaArchive + + PrepareRange(ctx context.Context, r historyarchive.Range) error + NewLedgerTransactionReader( + ledgerCloseMeta xdr.SerializedLedgerCloseMeta, + ) (LedgerTransactionReader, error) +} + +// For now, this mirrors the `ingest` library exactly, but it's replicated so +// that we can diverge in the future if necessary. +type LedgerTransaction struct { + *ingest.LedgerTransaction +} + +type LedgerTransactionReader interface { + Read() (LedgerTransaction, error) +} + +func NewIngester(config IngesterConfig) (Ingester, error) { + if config.CacheSize <= 0 { + return nil, fmt.Errorf("invalid cache size: %d", config.CacheSize) + } + + // Now, set up a simple filesystem-like access to the backend and wrap it in + // a local on-disk LRU cache if we can. + source, err := historyarchive.ConnectBackend( + config.SourceUrl, + storage.ConnectOptions{Context: context.Background()}, + ) + if err != nil { + return nil, errors.Wrapf(err, "failed to connect to %s", config.SourceUrl) + } + + parsed, err := url.Parse(config.SourceUrl) + if err != nil { + return nil, errors.Wrapf(err, "%s is not a valid URL", config.SourceUrl) + } + + if parsed.Scheme != "file" { // otherwise, already on-disk + cache, errr := storage.MakeOnDiskCache(source, config.CacheDir, uint(config.CacheSize)) + + if errr != nil { // non-fatal: warn but continue w/o cache + log.WithField("path", config.CacheDir).WithError(errr). + Warnf("Failed to create cached ledger backend") + } else { + log.WithField("path", config.CacheDir). + Infof("On-disk cache configured") + source = cache + } + } + + if config.ParallelDownloads > 1 { + log.Infof("Enabling parallel ledger fetches with %d workers", config.ParallelDownloads) + return NewParallelIngester( + metaarchive.NewMetaArchive(source), + config.NetworkPassphrase, + config.ParallelDownloads), nil + } + + return &liteIngester{ + MetaArchive: metaarchive.NewMetaArchive(source), + networkPassphrase: config.NetworkPassphrase, + }, nil +} diff --git a/exp/lighthorizon/ingester/mock_ingester.go b/exp/lighthorizon/ingester/mock_ingester.go new file mode 100644 index 0000000000..62c377ce78 --- /dev/null +++ b/exp/lighthorizon/ingester/mock_ingester.go @@ -0,0 +1,44 @@ +package ingester + +import ( + "context" + + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/mock" +) + +type MockIngester struct { + mock.Mock +} + +func (m *MockIngester) NewLedgerTransactionReader( + ledgerCloseMeta xdr.SerializedLedgerCloseMeta, +) (LedgerTransactionReader, error) { + args := m.Called(ledgerCloseMeta) + return args.Get(0).(LedgerTransactionReader), args.Error(1) +} + +func (m *MockIngester) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { + args := m.Called(ctx) + return args.Get(0).(uint32), args.Error(1) +} + +func (m *MockIngester) GetLedger(ctx context.Context, sequence uint32) (xdr.SerializedLedgerCloseMeta, error) { + args := m.Called(ctx, sequence) + return args.Get(0).(xdr.SerializedLedgerCloseMeta), args.Error(1) +} + +func (m *MockIngester) PrepareRange(ctx context.Context, r historyarchive.Range) error { + args := m.Called(ctx, r) + return args.Error(0) +} + +type MockLedgerTransactionReader struct { + mock.Mock +} + +func (m *MockLedgerTransactionReader) Read() (LedgerTransaction, error) { + args := m.Called() + return args.Get(0).(LedgerTransaction), args.Error(1) +} diff --git a/exp/lighthorizon/ingester/parallel_ingester.go b/exp/lighthorizon/ingester/parallel_ingester.go new file mode 100644 index 0000000000..133b0a37c4 --- /dev/null +++ b/exp/lighthorizon/ingester/parallel_ingester.go @@ -0,0 +1,141 @@ +package ingester + +import ( + "context" + "sync" + "time" + + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/metaarchive" + "github.com/stellar/go/support/collections/set" + "github.com/stellar/go/support/log" + "github.com/stellar/go/xdr" +) + +type parallelIngester struct { + liteIngester + + ledgerFeed sync.Map // thread-safe version of map[uint32]downloadState + ledgerQueue set.ISet[uint32] + + workQueue chan uint32 + signalChan chan error +} + +type downloadState struct { + ledger xdr.SerializedLedgerCloseMeta + err error +} + +// NewParallelIngester creates an ingester on the given `ledgerSource` using the +// given `networkPassphrase` that can download ledgers in parallel via +// `workerCount` workers via `PrepareRange()`. +func NewParallelIngester( + archive metaarchive.MetaArchive, + networkPassphrase string, + workerCount uint, +) *parallelIngester { + self := ¶llelIngester{ + liteIngester: liteIngester{ + MetaArchive: archive, + networkPassphrase: networkPassphrase, + }, + ledgerFeed: sync.Map{}, + ledgerQueue: set.NewSafeSet[uint32](64), + workQueue: make(chan uint32, workerCount), + signalChan: make(chan error), + } + + // These are the workers that download & store ledgers in memory. + for j := uint(0); j < workerCount; j++ { + go func(jj uint) { + for ledgerSeq := range self.workQueue { + start := time.Now() + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + txmeta, err := self.liteIngester.GetLedger(ctx, ledgerSeq) + cancel() + + log.WithField("duration", time.Since(start)). + WithField("worker", jj).WithError(err). + Debugf("Downloaded ledger %d", ledgerSeq) + + self.ledgerFeed.Store(ledgerSeq, downloadState{txmeta, err}) + self.signalChan <- err + } + }(j) + } + + return self +} + +// PrepareRange will create a set of parallel worker routines that feed ledgers +// to a channel in the order they're downloaded and store the results in an +// array. You can use this to download ledgers in parallel to fetching them +// individually via `GetLedger()`. `PrepareRange()` is thread-safe. +// +// Note: The passed in range `r` is inclusive of the boundaries. +func (i *parallelIngester) PrepareRange(ctx context.Context, r historyarchive.Range) error { + // The taskmaster adds ledger sequence numbers to the work queue. + go func() { + start := time.Now() + defer func() { + log.WithField("duration", time.Since(start)). + WithError(ctx.Err()). + Infof("Download of ledger range: [%d, %d] (%d ledgers) complete", + r.Low, r.High, r.Size()) + }() + + for seq := r.Low; seq <= r.High; seq++ { + if ctx.Err() != nil { + log.Warnf("Cancelling remaining downloads ([%d, %d]): %v", + seq, r.High, ctx.Err()) + break + } + + // Adding this to the "set of ledgers being downloaded in parallel" + // means that if a GetLedger() request happens in this range but + // outside of the realm of processing, it can be prioritized by the + // normal, direct download. + i.ledgerQueue.Add(seq) + + i.workQueue <- seq // blocks until there's an available worker + + // We don't remove from the queue here, preferring to remove when + // it's actually pulled from the worker. Removing here would mean + // you could have multiple instances of a ledger download happening. + } + }() + + return nil +} + +func (i *parallelIngester) GetLedger( + ctx context.Context, ledgerSeq uint32, +) (xdr.SerializedLedgerCloseMeta, error) { + // If the requested ledger is out of the queued up ranges, we can fall back + // to the default non-parallel download method. + if !i.ledgerQueue.Contains(ledgerSeq) { + return i.liteIngester.GetLedger(ctx, ledgerSeq) + } + + // If the ledger isn't available yet, wait for the download worker. + var err error + for err == nil { + if iState, ok := i.ledgerFeed.Load(ledgerSeq); ok { + state := iState.(downloadState) + i.ledgerFeed.Delete(ledgerSeq) + i.ledgerQueue.Remove(ledgerSeq) + return state.ledger, state.err + } + + select { + case err = <-i.signalChan: // blocks until another ledger downloads + case <-ctx.Done(): + err = ctx.Err() + } + } + + return xdr.SerializedLedgerCloseMeta{}, err +} + +var _ Ingester = (*parallelIngester)(nil) // ensure conformity to the interface diff --git a/exp/lighthorizon/ingester/participants.go b/exp/lighthorizon/ingester/participants.go new file mode 100644 index 0000000000..ebc49173cf --- /dev/null +++ b/exp/lighthorizon/ingester/participants.go @@ -0,0 +1,35 @@ +package ingester + +import ( + "github.com/stellar/go/exp/lighthorizon/index" + "github.com/stellar/go/support/collections/set" + "github.com/stellar/go/xdr" +) + +// GetTransactionParticipants takes a LedgerTransaction and returns a set of all +// participants (accounts) in the transaction. If there is any error, it will +// return nil and the error. +func GetTransactionParticipants(tx LedgerTransaction) (set.Set[string], error) { + participants, err := index.GetTransactionParticipants(*tx.LedgerTransaction) + if err != nil { + return nil, err + } + set := set.NewSet[string](len(participants)) + set.AddSlice(participants) + return set, nil +} + +// GetOperationParticipants takes a LedgerTransaction, the Operation within the +// transaction, and the 0-based index of the operation within the transaction. +// It will return a set of all participants (accounts) in the operation. If +// there is any error, it will return nil and the error. +func GetOperationParticipants(tx LedgerTransaction, op xdr.Operation, opIndex int) (set.Set[string], error) { + participants, err := index.GetOperationParticipants(*tx.LedgerTransaction, op, opIndex) + if err != nil { + return nil, err + } + + set := set.NewSet[string](len(participants)) + set.AddSlice(participants) + return set, nil +} diff --git a/exp/lighthorizon/main.go b/exp/lighthorizon/main.go new file mode 100644 index 0000000000..f7c502d465 --- /dev/null +++ b/exp/lighthorizon/main.go @@ -0,0 +1,183 @@ +package main + +import ( + "context" + "net/http" + + "github.com/go-chi/chi" + "github.com/prometheus/client_golang/prometheus" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/stellar/go/exp/lighthorizon/actions" + "github.com/stellar/go/exp/lighthorizon/index" + "github.com/stellar/go/exp/lighthorizon/ingester" + "github.com/stellar/go/exp/lighthorizon/services" + "github.com/stellar/go/exp/lighthorizon/tools" + + "github.com/stellar/go/network" + "github.com/stellar/go/support/log" +) + +const ( + HorizonLiteVersion = "0.0.1-alpha" + defaultCacheSize = (60 * 60 * 24) / 6 // 1 day of ledgers @ 6s each +) + +func main() { + log.SetLevel(logrus.InfoLevel) // default for subcommands + + cmd := &cobra.Command{ + Use: "lighthorizon ", + Long: "Horizon Lite command suite", + RunE: func(cmd *cobra.Command, args []string) error { + return cmd.Usage() // require a subcommand + }, + } + + serve := &cobra.Command{ + Use: "serve ", + Long: `Starts the Horizon Lite server, binding it to port 8080 on all +local interfaces of the host. You can refer to the OpenAPI documentation located +at the /api endpoint to see what endpoints are supported. + +The should be a URL to meta archives from which to read unpacked +ledger files, while the should be a URL containing indices that +break down accounts by active ledgers.`, + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 2 { + cmd.Usage() + return + } + + sourceUrl, indexStoreUrl := args[0], args[1] + + networkPassphrase, _ := cmd.Flags().GetString("network-passphrase") + switch networkPassphrase { + case "testnet": + networkPassphrase = network.TestNetworkPassphrase + case "pubnet": + networkPassphrase = network.PublicNetworkPassphrase + } + + cacheDir, _ := cmd.Flags().GetString("ledger-cache") + cacheSize, _ := cmd.Flags().GetUint("ledger-cache-size") + logLevelParam, _ := cmd.Flags().GetString("log-level") + downloadCount, _ := cmd.Flags().GetUint("parallel-downloads") + + L := log.WithField("service", "horizon-lite") + logLevel, err := logrus.ParseLevel(logLevelParam) + if err != nil { + log.Warnf("Failed to parse log level '%s', defaulting to 'info'.", logLevelParam) + logLevel = log.InfoLevel + } + L.SetLevel(logLevel) + L.Info("Starting lighthorizon!") + + registry := prometheus.NewRegistry() + indexStore, err := index.ConnectWithConfig(index.StoreConfig{ + URL: indexStoreUrl, + Log: L.WithField("service", "index"), + Metrics: registry, + }) + if err != nil { + log.Fatal(err) + return + } + + ingester, err := ingester.NewIngester(ingester.IngesterConfig{ + SourceUrl: sourceUrl, + NetworkPassphrase: networkPassphrase, + CacheDir: cacheDir, + CacheSize: int(cacheSize), + ParallelDownloads: downloadCount, + }) + if err != nil { + log.Fatal(err) + return + } + + latestLedger, err := ingester.GetLatestLedgerSequence(context.Background()) + if err != nil { + log.Fatalf("Failed to retrieve latest ledger from %s: %v", sourceUrl, err) + return + } + log.Infof("The latest ledger stored at %s is %d.", sourceUrl, latestLedger) + + cachePreloadCount, _ := cmd.Flags().GetUint32("ledger-cache-preload") + cachePreloadStart, _ := cmd.Flags().GetUint32("ledger-cache-preload-start") + if cachePreloadCount > 0 { + if cacheDir == "" { + log.Fatalf("--ledger-cache-preload=%d specified but no "+ + "--ledger-cache directory provided.", + cachePreloadCount) + return + } else { + startLedger := int(latestLedger) - int(cachePreloadCount) + if cachePreloadStart > 0 { + startLedger = int(cachePreloadStart) + } + if startLedger <= 0 { + log.Warnf("Starting ledger invalid (%d), defaulting to 2.", + startLedger) + startLedger = 2 + } + + log.Infof("Preloading cache at %s with %d ledgers, starting at ledger %d.", + cacheDir, startLedger, cachePreloadCount) + go func() { + tools.BuildCache(sourceUrl, cacheDir, + uint32(startLedger), cachePreloadCount, false) + }() + } + } + + Config := services.Config{ + Ingester: ingester, + Passphrase: networkPassphrase, + IndexStore: indexStore, + Metrics: services.NewMetrics(registry), + } + + lightHorizon := services.LightHorizon{ + Transactions: &services.TransactionRepository{ + Config: Config, + }, + Operations: &services.OperationRepository{ + Config: Config, + }, + } + + // Inject our config into the root response. + router := lightHorizonHTTPHandler(registry, lightHorizon).(*chi.Mux) + router.MethodFunc(http.MethodGet, "/", actions.Root(actions.RootResponse{ + Version: HorizonLiteVersion, + LedgerSource: sourceUrl, + IndexSource: indexStoreUrl, + + LatestLedger: latestLedger, + })) + + log.Fatal(http.ListenAndServe(":8080", router)) + }, + } + + serve.Flags().String("log-level", "info", + "logging level: 'info', 'debug', 'warn', 'error', 'panic', 'fatal', or 'trace'") + serve.Flags().String("network-passphrase", "pubnet", "network passphrase") + serve.Flags().String("ledger-cache", "", "path to cache frequently-used ledgers; "+ + "if left empty, uses a temporary directory") + serve.Flags().Uint("ledger-cache-size", defaultCacheSize, + "number of ledgers to store in the cache") + serve.Flags().Uint32("ledger-cache-preload", 0, + "should the cache come preloaded with the latest ledgers?") + serve.Flags().Uint32("ledger-cache-preload-start", 0, + "the preload should start at ledger ") + serve.Flags().Uint("parallel-downloads", 1, + "how many workers should download ledgers in parallel?") + + cmd.AddCommand(serve) + tools.AddCacheCommands(cmd) + tools.AddIndexCommands(cmd) + cmd.Execute() +} diff --git a/exp/lighthorizon/services/cursor.go b/exp/lighthorizon/services/cursor.go new file mode 100644 index 0000000000..8f2d2b0b5c --- /dev/null +++ b/exp/lighthorizon/services/cursor.go @@ -0,0 +1,102 @@ +package services + +import ( + "github.com/stellar/go/exp/lighthorizon/index" + "github.com/stellar/go/toid" +) + +// CursorManager describes a way to control how a cursor advances for a +// particular indexing strategy. +type CursorManager interface { + Begin(cursor int64) (int64, error) + Advance(times uint) (int64, error) +} + +type AccountActivityCursorManager struct { + AccountId string + + store index.Store + lastCursor *toid.ID +} + +func NewCursorManagerForAccountActivity(store index.Store, accountId string) *AccountActivityCursorManager { + return &AccountActivityCursorManager{AccountId: accountId, store: store} +} + +func (c *AccountActivityCursorManager) Begin(cursor int64) (int64, error) { + freq := checkpointManager.GetCheckpointFrequency() + id := toid.Parse(cursor) + lastCheckpoint := uint32(0) + if id.LedgerSequence >= int32(checkpointManager.GetCheckpointFrequency()) { + lastCheckpoint = index.GetCheckpointNumber(uint32(id.LedgerSequence)) + } + + // We shouldn't take the provided cursor for granted: instead, we should + // skip ahead to the first active ledger that's >= the given cursor. + // + // For example, someone might say ?cursor=0 but the first active checkpoint + // is actually 40M ledgers in. + firstCheckpoint, err := c.store.NextActive(c.AccountId, allTransactionsIndex, lastCheckpoint) + if err != nil { + return cursor, err + } + + nextLedger := (firstCheckpoint - 1) * freq + + // However, if the given cursor is actually *more* specific than the index + // can give us (e.g. somewhere *within* an active checkpoint range), prefer + // it rather than starting over. + if nextLedger < uint32(id.LedgerSequence) { + better := toid.Parse(cursor) + c.lastCursor = &better + return cursor, nil + } + + c.lastCursor = toid.New(int32(nextLedger), 1, 1) + return c.lastCursor.ToInt64(), nil +} + +func (c *AccountActivityCursorManager) Advance(times uint) (int64, error) { + if c.lastCursor == nil { + panic("invalid cursor, call Begin() first") + } + + // + // Advancing the cursor means deciding whether or not we need to query + // the index. + // + freq := checkpointManager.GetCheckpointFrequency() + + for i := uint(1); i <= times; i++ { + lastLedger := uint32(c.lastCursor.LedgerSequence) + + if checkpointManager.IsCheckpoint(lastLedger) { + // If the last cursor we looked at was a checkpoint ledger, then we + // need to jump ahead to the next checkpoint. Note that NextActive() + // is "inclusive" so if the parameter is an active checkpoint it + // will return itself. + checkpoint := index.GetCheckpointNumber(uint32(c.lastCursor.LedgerSequence)) + checkpoint, err := c.store.NextActive(c.AccountId, allTransactionsIndex, checkpoint+1) + if err != nil { + return c.lastCursor.ToInt64(), err + } + + // We add a -1 here because an active checkpoint indicates that an + // account had activity in the *previous* 64 ledgers, so we need to + // backtrack to that ledger range. + c.lastCursor = toid.New(int32((checkpoint-1)*freq), 1, 1) + } else { + // Otherwise, we can just bump the ledger number. + c.lastCursor = toid.New(int32(lastLedger+1), 1, 1) + } + } + + return c.lastCursor.ToInt64(), nil +} + +var _ CursorManager = (*AccountActivityCursorManager)(nil) // ensure conformity to the interface + +// getLedgerFromCursor is a helpful way to turn a cursor into a ledger number +func getLedgerFromCursor(cursor int64) uint32 { + return uint32(toid.Parse(cursor).LedgerSequence) +} diff --git a/exp/lighthorizon/services/cursor_test.go b/exp/lighthorizon/services/cursor_test.go new file mode 100644 index 0000000000..2112ae3715 --- /dev/null +++ b/exp/lighthorizon/services/cursor_test.go @@ -0,0 +1,96 @@ +package services + +import ( + "io" + "testing" + + "github.com/stellar/go/exp/lighthorizon/index" + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/keypair" + "github.com/stellar/go/toid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + checkpointMgr = historyarchive.NewCheckpointManager(0) +) + +func TestAccountTransactionCursorManager(t *testing.T) { + freq := int32(checkpointMgr.GetCheckpointFrequency()) + accountId := keypair.MustRandom().Address() + + // Create an index and fill it with some checkpoint details. + tmp := t.TempDir() + store, err := index.NewFileStore(tmp, + index.StoreConfig{ + URL: "file://" + tmp, + Workers: 4, + }, + ) + require.NoError(t, err) + + for _, checkpoint := range []uint32{1, 5, 10, 12} { + require.NoError(t, store.AddParticipantsToIndexes( + checkpoint, allTransactionsIndex, []string{accountId})) + } + + cursorMgr := NewCursorManagerForAccountActivity(store, accountId) + + cursor := toid.New(1, 1, 1) + var nextCursor int64 + + // first checkpoint works + nextCursor, err = cursorMgr.Begin(cursor.ToInt64()) + require.NoError(t, err) + assert.EqualValues(t, 1, getLedgerFromCursor(nextCursor)) + + // cursor is preserved if mid-active-range + cursor.LedgerSequence = freq / 2 + nextCursor, err = cursorMgr.Begin(cursor.ToInt64()) + require.NoError(t, err) + assert.EqualValues(t, cursor.LedgerSequence, getLedgerFromCursor(nextCursor)) + + // cursor jumps ahead if not active + cursor.LedgerSequence = 2 * freq + nextCursor, err = cursorMgr.Begin(cursor.ToInt64()) + require.NoError(t, err) + assert.EqualValues(t, 4*freq, getLedgerFromCursor(nextCursor)) + + // cursor increments + for i := int32(1); i < freq; i++ { + nextCursor, err = cursorMgr.Advance(1) + require.NoError(t, err) + assert.EqualValues(t, 4*freq+i, getLedgerFromCursor(nextCursor)) + } + + // cursor jumps to next active checkpoint + nextCursor, err = cursorMgr.Advance(1) + require.NoError(t, err) + assert.EqualValues(t, 9*freq, getLedgerFromCursor(nextCursor)) + + // cursor skips + nextCursor, err = cursorMgr.Advance(5) + require.NoError(t, err) + assert.EqualValues(t, 9*freq+5, getLedgerFromCursor(nextCursor)) + + // cursor jumps to next active when skipping + nextCursor, err = cursorMgr.Advance(uint(freq - 5)) + require.NoError(t, err) + assert.EqualValues(t, 11*freq, getLedgerFromCursor(nextCursor)) + + // cursor EOFs at the end + nextCursor, err = cursorMgr.Advance(uint(freq - 1)) + require.NoError(t, err) + assert.EqualValues(t, 12*freq-1, getLedgerFromCursor(nextCursor)) + _, err = cursorMgr.Advance(1) + assert.ErrorIs(t, err, io.EOF) + + // cursor EOFs if skipping past the end + rewind := toid.New(int32(getLedgerFromCursor(nextCursor)-5), 0, 0) + nextCursor, err = cursorMgr.Begin(rewind.ToInt64()) + require.NoError(t, err) + assert.EqualValues(t, rewind.LedgerSequence, getLedgerFromCursor(nextCursor)) + _, err = cursorMgr.Advance(uint(freq)) + assert.ErrorIs(t, err, io.EOF) +} diff --git a/exp/lighthorizon/services/main.go b/exp/lighthorizon/services/main.go new file mode 100644 index 0000000000..d391fc8baf --- /dev/null +++ b/exp/lighthorizon/services/main.go @@ -0,0 +1,216 @@ +package services + +import ( + "context" + "io" + "time" + + "github.com/prometheus/client_golang/prometheus" + "golang.org/x/exp/constraints" + + "github.com/stellar/go/exp/lighthorizon/index" + "github.com/stellar/go/exp/lighthorizon/ingester" + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/log" + "github.com/stellar/go/xdr" +) + +const ( + allTransactionsIndex = "all/all" + allPaymentsIndex = "all/payments" + slowFetchDurationThreshold = time.Second +) + +var ( + checkpointManager = historyarchive.NewCheckpointManager(0) +) + +// NewMetrics returns a Metrics instance containing all the prometheus +// metrics necessary for running light horizon services. +func NewMetrics(registry *prometheus.Registry) Metrics { + const minute = 60 + const day = 24 * 60 * minute + responseAgeHistogram := prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "horizon_lite", + Subsystem: "services", + Name: "response_age", + Buckets: []float64{ + 5 * minute, + 60 * minute, + day, + 7 * day, + 30 * day, + 90 * day, + 180 * day, + 365 * day, + }, + Help: "Age of the response for each service, sliding window = 10m", + }, + []string{"request", "successful"}, + ) + registry.MustRegister(responseAgeHistogram) + return Metrics{ + ResponseAgeHistogram: responseAgeHistogram, + } +} + +type LightHorizon struct { + Operations OperationService + Transactions TransactionService +} + +type Metrics struct { + ResponseAgeHistogram *prometheus.HistogramVec +} + +type Config struct { + Ingester ingester.Ingester + IndexStore index.Store + Passphrase string + Metrics Metrics +} + +// searchCallback is a generic way for any endpoint to process a transaction and +// its corresponding ledger. It should return whether or not we should stop +// processing (e.g. when a limit is reached) and any error that occurred. +type searchCallback func(ingester.LedgerTransaction, *xdr.LedgerHeader) (finished bool, err error) + +func searchAccountTransactions(ctx context.Context, + cursor int64, + accountId string, + config Config, + callback searchCallback, +) error { + cursorMgr := NewCursorManagerForAccountActivity(config.IndexStore, accountId) + cursor, err := cursorMgr.Begin(cursor) + if err == io.EOF { + return nil + } else if err != nil { + return err + } + nextLedger := getLedgerFromCursor(cursor) + + log.WithField("cursor", cursor). + Debugf("Searching %s for account %s starting at ledger %d", + allTransactionsIndex, accountId, nextLedger) + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + fullStart := time.Now() + fetchDuration := time.Duration(0) + processDuration := time.Duration(0) + indexFetchDuration := time.Duration(0) + count := int64(0) + + defer func() { + log.WithField("ledgers", count). + WithField("ledger-fetch", fetchDuration). + WithField("ledger-process", processDuration). + WithField("index-fetch", indexFetchDuration). + WithField("avg-ledger-fetch", getAverageDuration(fetchDuration, count)). + WithField("avg-ledger-process", getAverageDuration(processDuration, count)). + WithField("avg-index-fetch", getAverageDuration(indexFetchDuration, count)). + WithField("total", time.Since(fullStart)). + Infof("Fulfilled request for account %s at cursor %d", accountId, cursor) + }() + + checkpointMgr := historyarchive.NewCheckpointManager(0) + + for { + if checkpointMgr.IsCheckpoint(nextLedger) { + r := historyarchive.Range{ + Low: nextLedger, + High: checkpointMgr.NextCheckpoint(nextLedger + 1), + } + log.Infof("Preparing ledger range [%d, %d]", r.Low, r.High) + if innerErr := config.Ingester.PrepareRange(ctx, r); innerErr != nil { + log.Errorf("failed to prepare ledger range [%d, %d]: %v", + r.Low, r.High, innerErr) + } + } + + start := time.Now() + ledger, innerErr := config.Ingester.GetLedger(ctx, nextLedger) + + // TODO: We should have helpful error messages when innerErr points to a + // 404 for that particular ledger, since that situation shouldn't happen + // under normal operations, but rather indicates a problem with the + // backing archive. + if innerErr != nil { + return errors.Wrapf(innerErr, + "failed to retrieve ledger %d from archive", nextLedger) + } + count++ + thisFetchDuration := time.Since(start) + if thisFetchDuration > slowFetchDurationThreshold { + log.WithField("duration", thisFetchDuration). + Warnf("Fetching ledger %d was really slow", nextLedger) + } + fetchDuration += thisFetchDuration + + start = time.Now() + reader, innerErr := config.Ingester.NewLedgerTransactionReader(ledger) + if innerErr != nil { + return errors.Wrapf(innerErr, + "failed to read ledger %d", nextLedger) + } + + for { + if ctx.Err() != nil { + return ctx.Err() + } + + tx, readErr := reader.Read() + if readErr == io.EOF { + break + } else if readErr != nil { + return readErr + } + + // Note: If we move to ledger-based indices, we don't need this, + // since we have a guarantee that the transaction will contain + // the account as a participant. + participants, participantErr := ingester.GetTransactionParticipants(tx) + if participantErr != nil { + return participantErr + } + + if _, found := participants[accountId]; found { + finished, callBackErr := callback(tx, &ledger.V0.V0.LedgerHeader.Header) + if callBackErr != nil { + return callBackErr + } else if finished { + processDuration += time.Since(start) + return nil + } + } + } + + processDuration += time.Since(start) + start = time.Now() + + cursor, err = cursorMgr.Advance(1) + if err != nil && err != io.EOF { + return err + } + + nextLedger = getLedgerFromCursor(cursor) + indexFetchDuration += time.Since(start) + if err == io.EOF { + break + } + } + + return nil +} + +func getAverageDuration[ + T constraints.Signed | constraints.Float, +](d time.Duration, count T) time.Duration { + if count == 0 { + return 0 // don't bomb on div-by-zero + } + return time.Duration(int64(float64(d.Nanoseconds()) / float64(count))) +} diff --git a/exp/lighthorizon/services/main_test.go b/exp/lighthorizon/services/main_test.go new file mode 100644 index 0000000000..a8a3958214 --- /dev/null +++ b/exp/lighthorizon/services/main_test.go @@ -0,0 +1,250 @@ +package services + +import ( + "context" + "io" + "testing" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/stellar/go/exp/lighthorizon/index" + "github.com/stellar/go/exp/lighthorizon/ingester" + "github.com/stellar/go/ingest" + "github.com/stellar/go/toid" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +var ( + passphrase = "White New England clam chowder" + accountId = "GDCXSQPVE45DVGT2ZRFFIIHSJ2EJED65W6AELGWIDRMPMWNXCEBJ4FKX" + startLedgerSeq = 1586112 +) + +func TestItGetsTransactionsByAccount(t *testing.T) { + ctx := context.Background() + + // this is in the checkpoint range prior to the first active checkpoint + ledgerSeq := checkpointMgr.PrevCheckpoint(uint32(startLedgerSeq)) + cursor := toid.New(int32(ledgerSeq), 1, 1).ToInt64() + + t.Run("first", func(tt *testing.T) { + txService := newTransactionService(ctx) + + txs, err := txService.GetTransactionsByAccount(ctx, cursor, 1, accountId) + require.NoError(tt, err) + require.Len(tt, txs, 1) + require.Equal(tt, txs[0].LedgerHeader.LedgerSeq, xdr.Uint32(1586113)) + require.EqualValues(tt, txs[0].TxIndex, 2) + }) + + t.Run("without cursor", func(tt *testing.T) { + txService := newTransactionService(ctx) + + txs, err := txService.GetTransactionsByAccount(ctx, 0, 1, accountId) + require.NoError(tt, err) + require.Len(tt, txs, 1) + require.Equal(tt, txs[0].LedgerHeader.LedgerSeq, xdr.Uint32(1586113)) + require.EqualValues(tt, txs[0].TxIndex, 2) + }) + + t.Run("with limit", func(tt *testing.T) { + txService := newTransactionService(ctx) + + txs, err := txService.GetTransactionsByAccount(ctx, cursor, 5, accountId) + require.NoError(tt, err) + require.Len(tt, txs, 2) + require.Equal(tt, txs[0].LedgerHeader.LedgerSeq, xdr.Uint32(1586113)) + require.EqualValues(tt, txs[0].TxIndex, 2) + require.Equal(tt, txs[1].LedgerHeader.LedgerSeq, xdr.Uint32(1586114)) + require.EqualValues(tt, txs[1].TxIndex, 1) + }) +} + +func TestItGetsOperationsByAccount(t *testing.T) { + ctx := context.Background() + + // this is in the checkpoint range prior to the first active checkpoint + ledgerSeq := checkpointMgr.PrevCheckpoint(uint32(startLedgerSeq)) + cursor := toid.New(int32(ledgerSeq), 1, 1).ToInt64() + + t.Run("first", func(tt *testing.T) { + opsService := newOperationService(ctx) + + // this should start at next checkpoint + ops, err := opsService.GetOperationsByAccount(ctx, cursor, 1, accountId) + require.NoError(tt, err) + require.Len(tt, ops, 1) + require.Equal(tt, ops[0].LedgerHeader.LedgerSeq, xdr.Uint32(1586113)) + require.Equal(tt, ops[0].TxIndex, int32(2)) + + }) + + t.Run("with limit", func(tt *testing.T) { + opsService := newOperationService(ctx) + + // this should start at next checkpoint + ops, err := opsService.GetOperationsByAccount(ctx, cursor, 5, accountId) + require.NoError(tt, err) + require.Len(tt, ops, 2) + require.Equal(tt, ops[0].LedgerHeader.LedgerSeq, xdr.Uint32(1586113)) + require.Equal(tt, ops[0].TxIndex, int32(2)) + require.Equal(tt, ops[1].LedgerHeader.LedgerSeq, xdr.Uint32(1586114)) + require.Equal(tt, ops[1].TxIndex, int32(1)) + }) +} + +func mockArchiveAndIndex(ctx context.Context) (ingester.Ingester, index.Store) { + mockArchive := &ingester.MockIngester{} + mockReaderLedger1 := &ingester.MockLedgerTransactionReader{} + mockReaderLedger2 := &ingester.MockLedgerTransactionReader{} + mockReaderLedger3 := &ingester.MockLedgerTransactionReader{} + mockReaderLedgerTheRest := &ingester.MockLedgerTransactionReader{} + + expectedLedger1 := testLedger(startLedgerSeq) + expectedLedger2 := testLedger(startLedgerSeq + 1) + expectedLedger3 := testLedger(startLedgerSeq + 2) + + // throw an irrelevant account in there to make sure it's filtered + source := xdr.MustAddress("GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU") + source2 := xdr.MustAddress(accountId) + + // assert results iterate sequentially across ops-tx-ledgers + expectedLedger1Tx1 := testLedgerTx(source, 1, 34, 35) + expectedLedger1Tx2 := testLedgerTx(source, 2, 34) + expectedLedger2Tx1 := testLedgerTx(source, 1, 34) + expectedLedger2Tx2 := testLedgerTx(source2, 2, 34) + expectedLedger3Tx1 := testLedgerTx(source2, 1, 34) + expectedLedger3Tx2 := testLedgerTx(source, 2, 34) + + mockReaderLedger1. + On("Read").Return(expectedLedger1Tx1, nil).Once(). + On("Read").Return(expectedLedger1Tx2, nil).Once(). + On("Read").Return(ingester.LedgerTransaction{}, io.EOF).Once() + + mockReaderLedger2. + On("Read").Return(expectedLedger2Tx1, nil).Once(). + On("Read").Return(expectedLedger2Tx2, nil).Once(). + On("Read").Return(ingester.LedgerTransaction{}, io.EOF).Once() + + mockReaderLedger3. + On("Read").Return(expectedLedger3Tx1, nil).Once(). + On("Read").Return(expectedLedger3Tx2, nil).Once(). + On("Read").Return(ingester.LedgerTransaction{}, io.EOF).Once() + + mockReaderLedgerTheRest. + On("Read").Return(ingester.LedgerTransaction{}, io.EOF) + + mockArchive. + On("GetLedger", mock.Anything, uint32(1586112)).Return(expectedLedger1, nil). + On("GetLedger", mock.Anything, uint32(1586113)).Return(expectedLedger2, nil). + On("GetLedger", mock.Anything, uint32(1586114)).Return(expectedLedger3, nil). + On("GetLedger", mock.Anything, mock.AnythingOfType("uint32")). + Return(xdr.SerializedLedgerCloseMeta{}, nil) + + mockArchive. + On("NewLedgerTransactionReader", expectedLedger1).Return(mockReaderLedger1, nil).Once(). + On("NewLedgerTransactionReader", expectedLedger2).Return(mockReaderLedger2, nil).Once(). + On("NewLedgerTransactionReader", expectedLedger3).Return(mockReaderLedger3, nil).Once(). + On("NewLedgerTransactionReader", mock.AnythingOfType("xdr.SerializedLedgerCloseMeta")). + Return(mockReaderLedgerTheRest, nil). + On("PrepareRange", mock.Anything, mock.Anything).Return(nil) + + // should be 24784 + activeChk := uint32(index.GetCheckpointNumber(uint32(startLedgerSeq))) + mockStore := &index.MockStore{} + mockStore. + On("NextActive", accountId, mock.Anything, uint32(0)).Return(activeChk, nil). // start + On("NextActive", accountId, mock.Anything, activeChk-1).Return(activeChk, nil). // prev + On("NextActive", accountId, mock.Anything, activeChk).Return(activeChk, nil). // curr + On("NextActive", accountId, mock.Anything, activeChk+1).Return(uint32(0), io.EOF) // next + + return mockArchive, mockStore +} + +func testLedger(seq int) xdr.SerializedLedgerCloseMeta { + return xdr.SerializedLedgerCloseMeta{ + V: 0, + V0: &xdr.LedgerCloseMeta{ + V0: &xdr.LedgerCloseMetaV0{ + LedgerHeader: xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + LedgerSeq: xdr.Uint32(seq), + }, + }, + }, + }, + } +} + +func testLedgerTx(source xdr.AccountId, txIndex uint32, bumpTos ...int) ingester.LedgerTransaction { + code := xdr.TransactionResultCodeTxSuccess + + operations := []xdr.Operation{} + for _, bumpTo := range bumpTos { + operations = append(operations, xdr.Operation{ + Body: xdr.OperationBody{ + Type: xdr.OperationTypeBumpSequence, + BumpSequenceOp: &xdr.BumpSequenceOp{ + BumpTo: xdr.SequenceNumber(bumpTo), + }, + }, + }) + } + + return ingester.LedgerTransaction{ + LedgerTransaction: &ingest.LedgerTransaction{ + Result: xdr.TransactionResultPair{ + TransactionHash: xdr.Hash{}, + Result: xdr.TransactionResult{ + Result: xdr.TransactionResultResult{ + Code: code, + InnerResultPair: &xdr.InnerTransactionResultPair{}, + Results: &[]xdr.OperationResult{}, + }, + }, + }, + Envelope: xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + SourceAccount: source.ToMuxedAccount(), + Operations: operations, + }, + }, + }, + UnsafeMeta: xdr.TransactionMeta{ + V: 2, + V2: &xdr.TransactionMetaV2{ + Operations: make([]xdr.OperationMeta, len(bumpTos)), + }, + }, + Index: txIndex, + }, + } +} + +func newTransactionService(ctx context.Context) TransactionService { + ingest, store := mockArchiveAndIndex(ctx) + return &TransactionRepository{ + Config: Config{ + Ingester: ingest, + IndexStore: store, + Passphrase: passphrase, + Metrics: NewMetrics(prometheus.NewRegistry()), + }, + } +} + +func newOperationService(ctx context.Context) OperationService { + ingest, store := mockArchiveAndIndex(ctx) + return &OperationRepository{ + Config: Config{ + Ingester: ingest, + IndexStore: store, + Passphrase: passphrase, + Metrics: NewMetrics(prometheus.NewRegistry()), + }, + } +} diff --git a/exp/lighthorizon/services/mock_services.go b/exp/lighthorizon/services/mock_services.go new file mode 100644 index 0000000000..be573489e0 --- /dev/null +++ b/exp/lighthorizon/services/mock_services.go @@ -0,0 +1,32 @@ +package services + +import ( + "context" + + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stretchr/testify/mock" +) + +type MockTransactionService struct { + mock.Mock +} + +func (m *MockTransactionService) GetTransactionsByAccount(ctx context.Context, + cursor int64, limit uint64, + accountId string, +) ([]common.Transaction, error) { + args := m.Called(ctx, cursor, limit, accountId) + return args.Get(0).([]common.Transaction), args.Error(1) +} + +type MockOperationService struct { + mock.Mock +} + +func (m *MockOperationService) GetOperationsByAccount(ctx context.Context, + cursor int64, limit uint64, + accountId string, +) ([]common.Operation, error) { + args := m.Called(ctx, cursor, limit, accountId) + return args.Get(0).([]common.Operation), args.Error(1) +} diff --git a/exp/lighthorizon/services/operations.go b/exp/lighthorizon/services/operations.go new file mode 100644 index 0000000000..1236bcdb01 --- /dev/null +++ b/exp/lighthorizon/services/operations.go @@ -0,0 +1,90 @@ +package services + +import ( + "context" + "strconv" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/exp/lighthorizon/ingester" + "github.com/stellar/go/support/log" + "github.com/stellar/go/xdr" +) + +type OperationService interface { + GetOperationsByAccount(ctx context.Context, + cursor int64, limit uint64, + accountId string, + ) ([]common.Operation, error) +} + +type OperationRepository struct { + OperationService + Config Config +} + +func (or *OperationRepository) GetOperationsByAccount(ctx context.Context, + cursor int64, limit uint64, + accountId string, +) ([]common.Operation, error) { + ops := []common.Operation{} + + opsCallback := func(tx ingester.LedgerTransaction, ledgerHeader *xdr.LedgerHeader) (bool, error) { + for operationOrder, op := range tx.Envelope.Operations() { + opParticipants, err := ingester.GetOperationParticipants(tx, op, operationOrder) + if err != nil { + return false, err + } + + if _, foundInOp := opParticipants[accountId]; foundInOp { + ops = append(ops, common.Operation{ + TransactionEnvelope: &tx.Envelope, + TransactionResult: &tx.Result.Result, + LedgerHeader: ledgerHeader, + TxIndex: int32(tx.Index), + OpIndex: int32(operationOrder), + }) + + if uint64(len(ops)) == limit { + return true, nil + } + } + } + + return false, nil + } + + err := searchAccountTransactions(ctx, cursor, accountId, or.Config, opsCallback) + if age := operationsResponseAgeSeconds(ops); age >= 0 { + or.Config.Metrics.ResponseAgeHistogram.With(prometheus.Labels{ + "request": "GetOperationsByAccount", + "successful": strconv.FormatBool(err == nil), + }).Observe(age) + } + + return ops, err +} + +func operationsResponseAgeSeconds(ops []common.Operation) float64 { + if len(ops) == 0 { + return -1 + } + + oldest := ops[0].LedgerHeader.ScpValue.CloseTime + for i := 1; i < len(ops); i++ { + if closeTime := ops[i].LedgerHeader.ScpValue.CloseTime; closeTime < oldest { + oldest = closeTime + } + } + + lastCloseTime := time.Unix(int64(oldest), 0).UTC() + now := time.Now().UTC() + if now.Before(lastCloseTime) { + log.Errorf("current time %v is before oldest operation close time %v", now, lastCloseTime) + return -1 + } + return now.Sub(lastCloseTime).Seconds() +} + +var _ OperationService = (*OperationRepository)(nil) // ensure conformity to the interface diff --git a/exp/lighthorizon/services/transactions.go b/exp/lighthorizon/services/transactions.go new file mode 100644 index 0000000000..42d3964614 --- /dev/null +++ b/exp/lighthorizon/services/transactions.go @@ -0,0 +1,76 @@ +package services + +import ( + "context" + "strconv" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/stellar/go/exp/lighthorizon/common" + "github.com/stellar/go/exp/lighthorizon/ingester" + "github.com/stellar/go/support/log" + "github.com/stellar/go/xdr" +) + +type TransactionRepository struct { + TransactionService + Config Config +} + +type TransactionService interface { + GetTransactionsByAccount(ctx context.Context, + cursor int64, limit uint64, + accountId string, + ) ([]common.Transaction, error) +} + +func (tr *TransactionRepository) GetTransactionsByAccount(ctx context.Context, + cursor int64, limit uint64, + accountId string, +) ([]common.Transaction, error) { + txs := []common.Transaction{} + + txsCallback := func(tx ingester.LedgerTransaction, ledgerHeader *xdr.LedgerHeader) (bool, error) { + txs = append(txs, common.Transaction{ + LedgerTransaction: &tx, + LedgerHeader: ledgerHeader, + TxIndex: int32(tx.Index), + NetworkPassphrase: tr.Config.Passphrase, + }) + + return uint64(len(txs)) == limit, nil + } + + err := searchAccountTransactions(ctx, cursor, accountId, tr.Config, txsCallback) + if age := transactionsResponseAgeSeconds(txs); age >= 0 { + tr.Config.Metrics.ResponseAgeHistogram.With(prometheus.Labels{ + "request": "GetTransactionsByAccount", + "successful": strconv.FormatBool(err == nil), + }).Observe(age) + } + + return txs, err +} + +func transactionsResponseAgeSeconds(txs []common.Transaction) float64 { + if len(txs) == 0 { + return -1 + } + + oldest := txs[0].LedgerHeader.ScpValue.CloseTime + for i := 1; i < len(txs); i++ { + if closeTime := txs[i].LedgerHeader.ScpValue.CloseTime; closeTime < oldest { + oldest = closeTime + } + } + + lastCloseTime := time.Unix(int64(oldest), 0).UTC() + now := time.Now().UTC() + if now.Before(lastCloseTime) { + log.Errorf("current time %v is before oldest transaction close time %v", now, lastCloseTime) + return -1 + } + return now.Sub(lastCloseTime).Seconds() +} + +var _ TransactionService = (*TransactionRepository)(nil) // ensure conformity to the interface diff --git a/exp/lighthorizon/tools/cache.go b/exp/lighthorizon/tools/cache.go new file mode 100644 index 0000000000..0290fcb164 --- /dev/null +++ b/exp/lighthorizon/tools/cache.go @@ -0,0 +1,270 @@ +package tools + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/spf13/cobra" + + "github.com/stellar/go/metaarchive" + "github.com/stellar/go/support/log" + "github.com/stellar/go/support/storage" +) + +const ( + defaultCacheCount = (60 * 60 * 24) / 5 // ~24hrs worth of ledgers +) + +func AddCacheCommands(parent *cobra.Command) *cobra.Command { + cmd := &cobra.Command{ + Use: "cache", + Long: "Manages the on-disk cache of ledgers.", + Example: ` +cache build --start 1234 --count 1000 s3://txmeta /tmp/example +cache purge /tmp/example 1234 1300 +cache show /tmp/example`, + RunE: func(cmd *cobra.Command, args []string) error { + // require a subcommand - this is just a "category" + return cmd.Help() + }, + } + + purge := &cobra.Command{ + Use: "purge [flags] path ", + Long: "Purges individual ledgers (or ranges) from the cache, or the entire cache.", + Example: ` +purge /tmp/example # empty the whole cache +purge /tmp/example 1000 # purge one ledger +purge /tmp/example 1000 1005 # purge a ledger range`, + RunE: func(cmd *cobra.Command, args []string) error { + // The first parameter must be a valid cache directory. + // You can then pass nothing, a single ledger, or a ledger range. + if len(args) < 1 || len(args) > 3 { + return cmd.Usage() + } + + var err error + var start, end uint64 + if len(args) > 1 { + start, err = strconv.ParseUint(args[1], 10, 32) + if err != nil { + cmd.Printf("Error: '%s' not a ledger sequence: %v\n", args[1], err) + return cmd.Usage() + } + } + end = start // fallback + + if len(args) == 3 { + end, err = strconv.ParseUint(args[2], 10, 32) + if err != nil { + cmd.Printf("Error: '%s' not a ledger sequence: %v\n", args[2], err) + return cmd.Usage() + } else if end < start { + cmd.Printf("Error: end precedes start (%d < %d)\n", end, start) + return cmd.Usage() + } + } + + path := args[0] + if start > 0 { + return PurgeLedgers(path, uint32(start), uint32(end)) + } + return PurgeCache(path) + }, + } + show := &cobra.Command{ + Use: "show ", + Long: "Traverses the on-disk cache and prints out cached ledger ranges.", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return cmd.Usage() + } + return ShowCache(args[0]) + }, + } + build := &cobra.Command{ + Use: "build [flags] ", + Example: "See cache --help text", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 2 { + cmd.Println("Error: 2 positional arguments are required") + return cmd.Usage() + } + + start, err := cmd.Flags().GetUint32("start") + if err != nil || start < 2 { + cmd.Println("--start is required to be a ledger sequence") + return cmd.Usage() + } + + count, err := cmd.Flags().GetUint32("count") + if err != nil || count <= 0 { + cmd.Println("--count should be a positive 32-bit integer") + return cmd.Usage() + } + repair, _ := cmd.Flags().GetBool("repair") + return BuildCache(args[0], args[1], start, count, repair) + }, + } + + build.Flags().Bool("repair", false, "attempt to purge the cache and retry ledgers that error") + build.Flags().Uint32("start", 0, "first ledger to cache (required)") + build.Flags().Uint32("count", defaultCacheCount, "number of ledgers to cache") + + cmd.AddCommand(build, purge, show) + if parent == nil { + return cmd + } + + parent.AddCommand(cmd) + return parent +} + +func BuildCache(ledgerSource, cacheDir string, start uint32, count uint32, repair bool) error { + fullStart := time.Now() + L := log.DefaultLogger + L.SetLevel(log.InfoLevel) + log := L + + ctx := context.Background() + store, err := storage.ConnectBackend(ledgerSource, storage.ConnectOptions{ + Context: ctx, + Wrap: func(store storage.Storage) (storage.Storage, error) { + return storage.MakeOnDiskCache(store, cacheDir, uint(count)) + }, + }) + if err != nil { + log.Errorf("Couldn't create local cache for '%s' at '%s': %v", + ledgerSource, cacheDir, err) + return err + } + + log.Infof("Connected to ledger source at %s", ledgerSource) + log.Infof("Connected to ledger cache at %s", cacheDir) + + source := metaarchive.NewMetaArchive(store) + log.Infof("Filling local cache of ledgers at %s...", cacheDir) + log.Infof("Ledger range: [%d, %d] (%d ledgers)", + start, start+count-1, count) + + successful := uint(0) + for i := uint32(0); i < count; i++ { + ledgerSeq := start + uint32(i) + + // do "best effort" caching, skipping if too slow + dlCtx, dlCancel := context.WithTimeout(ctx, 10*time.Second) + start := time.Now() + + _, err := source.GetLedger(dlCtx, ledgerSeq) // this caches + dlCancel() + + if err != nil { + if repair && strings.Contains(err.Error(), "xdr") { + log.Warnf("Caching ledger %d failed, purging & retrying: %v", ledgerSeq, err) + store.(*storage.OnDiskCache).Evict(fmt.Sprintf("ledgers/%d", ledgerSeq)) + i-- // retry + } else { + log.Warnf("Caching ledger %d failed, skipping: %v", ledgerSeq, err) + log.Warn("If you see an XDR decoding error, the cache may be corrupted.") + log.Warnf("Run '%s purge %d' and try again, or pass --repair", + filepath.Base(os.Args[0]), ledgerSeq) + } + continue + } else { + successful++ + } + + duration := time.Since(start) + if duration > 2*time.Second { + log.WithField("duration", duration). + Warnf("Downloading ledger %d took a while.", ledgerSeq) + } + + log = log.WithField("failures", 1+uint(i)-successful) + if successful%97 == 0 { + log.Infof("Cached %d/%d ledgers (%0.1f%%)", successful, count, + 100*float64(successful)/float64(count)) + } + } + + duration := time.Since(fullStart) + log.WithField("duration", duration). + Infof("Cached %d ledgers into %s", successful, cacheDir) + + return nil +} + +func PurgeLedgers(cacheDir string, start, end uint32) error { + base := filepath.Join(cacheDir, "ledgers") + + successful := 0 + for i := start; i <= end; i++ { + ledgerPath := filepath.Join(base, strconv.FormatUint(uint64(i), 10)) + if err := os.Remove(ledgerPath); err != nil { + log.Warnf("Failed to remove cached ledger %d: %v", i, err) + continue + } + os.Remove(storage.NameLockfile(ledgerPath)) // ignore lockfile errors + log.Debugf("Purged ledger from %s", ledgerPath) + successful++ + } + + log.Infof("Purged %d cached ledgers from %s", successful, cacheDir) + return nil +} + +func PurgeCache(cacheDir string) error { + if err := os.RemoveAll(cacheDir); err != nil { + log.Warnf("Failed to remove cache directory (%s): %v", cacheDir, err) + return err + } + + log.Infof("Purged cache at %s", cacheDir) + return nil +} + +func ShowCache(cacheDir string) error { + files, err := ioutil.ReadDir(filepath.Join(cacheDir, "ledgers")) + if err != nil { + log.Errorf("Failed to read cache: %v", err) + return err + } + + ledgers := make([]uint32, 0, len(files)) + + for _, f := range files { + if f.IsDir() { + continue + } + + // If the name can be converted to a ledger sequence, track it. + if seq, errr := strconv.ParseUint(f.Name(), 10, 32); errr == nil { + ledgers = append(ledgers, uint32(seq)) + } + } + + log.Infof("Analyzed cache at %s: %d cached ledgers.", cacheDir, len(ledgers)) + if len(ledgers) == 0 { + return nil + } + + // Find consecutive ranges of ledgers in the cache + log.Infof("Cached ranges:") + firstSeq, lastSeq := ledgers[0], ledgers[0] + for i := 1; i < len(ledgers); i++ { + if ledgers[i]-1 != lastSeq { + log.Infof(" - [%d, %d]", firstSeq, lastSeq) + firstSeq = ledgers[i] + } + lastSeq = ledgers[i] + } + + log.Infof(" - [%d, %d]", firstSeq, lastSeq) + return nil +} diff --git a/exp/lighthorizon/tools/index.go b/exp/lighthorizon/tools/index.go new file mode 100644 index 0000000000..e37a7eb38a --- /dev/null +++ b/exp/lighthorizon/tools/index.go @@ -0,0 +1,356 @@ +package tools + +import ( + "context" + "io" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + "time" + + "github.com/spf13/cobra" + + "github.com/stellar/go/exp/lighthorizon/index" + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/strkey" + "github.com/stellar/go/support/collections/maps" + "github.com/stellar/go/support/collections/set" + "github.com/stellar/go/support/log" + "github.com/stellar/go/support/ordered" +) + +var ( + checkpointMgr = historyarchive.NewCheckpointManager(0) +) + +func AddIndexCommands(parent *cobra.Command) *cobra.Command { + cmd := &cobra.Command{ + Use: "index", + Long: "Lets you view details about an index source and modify it.", + Example: ` +index view file:///tmp/indices +index view file:///tmp/indices GAGJZWQ5QT34VK3U6W6YKRYFIK6YSAXQC6BHIIYLG6X3CE5QW2KAYNJR +index stats file:///tmp/indices`, + RunE: func(cmd *cobra.Command, args []string) error { + // require a subcommand - this is just a "category" + return cmd.Help() + }, + } + + stats := &cobra.Command{ + Use: "stats ", + Long: "Summarize the statistics (like the # of active checkpoints " + + "or accounts). Note that this is a very read-heavy operation and " + + "will incur download bandwidth costs if reading from remote, " + + "billable sources.", + Example: `stats s3://indices`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return cmd.Usage() + } + + path := args[0] + start := time.Now() + log.Infof("Analyzing indices at %s", path) + + allCheckpoints := set.Set[uint32]{} + allIndexNames := set.Set[string]{} + accounts := showAccounts(path, 0) + log.Infof("Analyzing indices for %d accounts.", len(accounts)) + + // We want to summarize as much as possible on a Ctrl+C event, so + // this handles that by setting up a context that gets cancelled on + // SIGINT. A second Ctrl+C will kill the process as usual. + // + // https://millhouse.dev/posts/graceful-shutdowns-in-golang-with-signal-notify-context + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + defer stop() + go func() { + <-ctx.Done() + stop() + log.WithField("error", ctx.Err()). + Warn("Received interrupt, shutting down gracefully & summarizing findings...") + log.Warn("Press Ctrl+C again to abort.") + }() + + mostActiveAccountChk := 0 + mostActiveAccount := "" + for _, account := range accounts { + if ctx.Err() != nil { + break + } + + activity := getIndex(path, account, "", 0) + allCheckpoints.AddSlice(maps.Keys(activity)) + for _, names := range activity { + allIndexNames.AddSlice(names) + } + + if len(activity) > mostActiveAccountChk { + mostActiveAccount = account + mostActiveAccountChk = len(activity) + } + } + + ledgerCount := len(allCheckpoints) * int(checkpointMgr.GetCheckpointFrequency()) + + log.Info("Done analyzing indices, summarizing...") + log.Infof("") + log.Infof("=== Final Summary ===") + log.Infof("Analysis took %s.", time.Since(start)) + log.Infof("Path: %s", path) + log.Infof("Accounts: %d", len(accounts)) + log.Infof("Smallest checkpoint: %d", ordered.MinSlice(allCheckpoints.Slice())) + log.Infof("Largest checkpoint: %d", ordered.MaxSlice(allCheckpoints.Slice())) + log.Infof("Checkpoint count: %d (%d possible ledgers, ~%0.2f days)", + len(allCheckpoints), ledgerCount, + float64(ledgerCount)/(float64(60*60*24)/6.0) /* approx. ledgers per day */) + log.Infof("Index names: %s", strings.Join(allIndexNames.Slice(), ", ")) + log.Infof("Most active account: %s (%d checkpoints)", + mostActiveAccount, mostActiveAccountChk) + + return nil + }, + } + + view := &cobra.Command{ + Use: "view [accounts?]", + Long: "View the accounts in an index source or view the " + + "checkpoints specific account(s) are active in.", + Example: `view s3://indices +view s3:///indices GAXLQGKIUAIIUHAX4GJO3J7HFGLBCNF6ZCZSTLJE7EKO5IUHGLQLMXZO +view file:///tmp/indices --limit=0 GAXLQGKIUAIIUHAX4GJO3J7HFGLBCNF6ZCZSTLJE7EKO5IUHGLQLMXZO +view gcs://indices --limit=10 GAXLQGKIUAIIUHAX4GJO3J7HFGLBCNF6ZCZSTLJE7EKO5IUHGLQLMXZO,GBUUWQDVEEXBJCUF5UL24YGXKJIP5EMM7KFWIAR33KQRJR34GN6HEDPV,GBYETUYNBK2ZO5MSYBJKSLDEA2ZHIXLCFL3MMWU6RHFVAUBKEWQORYKS`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 || len(args) > 2 { + return cmd.Usage() + } + + path := args[0] + log.Infof("Analyzing indices at %s", path) + + accounts := []string{} + if len(args) == 2 { + accounts = strings.Split(args[1], ",") + } + + limit, err := cmd.Flags().GetUint("limit") + if err != nil { + return cmd.Usage() + } + + if len(accounts) > 0 { + indexName, err := cmd.Flags().GetString("index-name") + if err != nil { + return cmd.Usage() + } + + for _, account := range accounts { + if !strkey.IsValidEd25519PublicKey(account) && + !strkey.IsValidMuxedAccountEd25519PublicKey(account) { + log.Errorf("Invalid account ID: '%s'", account) + continue + } + + getIndex(path, account, indexName, limit) + } + } else { + showAccounts(path, limit) + } + + return nil + }, + } + + purge := &cobra.Command{ + Use: "purge ", + Long: "Purges all indices for the given ledger range.", + Example: `purge s3://indices 10000 10005`, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 3 { + return cmd.Usage() + } + + path := args[0] + start, err := strconv.ParseUint(args[1], 10, 32) + if err != nil { + return cmd.Usage() + } + end, err := strconv.ParseUint(args[2], 10, 32) + if err != nil { + return cmd.Usage() + } + + r := historyarchive.Range{Low: uint32(start), High: uint32(end)} + log.Infof("Purging all indices from %s for ledger range: [%d, %d].", + path, r.Low, r.High) + + return purgeIndex(path, r) + }, + } + + view.Flags().Uint("limit", 10, "a maximum number of accounts or checkpoints to show") + view.Flags().String("index-name", "", "filter for a particular index") + cmd.AddCommand(stats, view, purge) + + if parent == nil { + return cmd + } + parent.AddCommand(cmd) + return parent +} + +func getIndex(path, account, indexName string, limit uint) map[uint32][]string { + freq := checkpointMgr.GetCheckpointFrequency() + + store, err := index.Connect(path) + if err != nil { + log.Fatalf("Failed to connect to index store at %s: %v", path, err) + return nil + } + + indices, err := store.Read(account) + if err != nil { + log.Fatalf("Failed to read indices for %s from index store at %s: %v", + account, path, err) + return nil + } + + // It's better to summarize activity and then group it by index rather than + // just show activity in each index, because there's likely a ton of overlap + // across indices. + activity := map[uint32][]string{} + indexNames := []string{} + + for name, idx := range indices { + log.Infof("Index found: '%s'", name) + if indexName != "" && name != indexName { + continue + } + + indexNames = append(indexNames, name) + + checkpoint, err := idx.NextActiveBit(0) + for err != io.EOF { + activity[checkpoint] = append(activity[checkpoint], name) + checkpoint, err = idx.NextActiveBit(checkpoint + 1) + + if limit > 0 && limit <= uint(len(activity)) { + break + } + } + } + + log.WithField("account", account).WithField("limit", limit). + Infof("Activity for account:") + + for checkpoint, names := range activity { + first := (checkpoint - 1) * freq + last := first + freq + + nameStr := strings.Join(names, ", ") + log.WithField("indices", nameStr). + Infof(" - checkpoint %d, ledgers [%d, %d)", checkpoint, first, last) + } + + log.Infof("Summary: %d active checkpoints, %d possible active ledgers", + len(activity), len(activity)*int(freq)) + log.Infof("Checkpoint range: [%d, %d]", + ordered.MinSlice(maps.Keys(activity)), + ordered.MaxSlice(maps.Keys(activity))) + log.Infof("All discovered indices: %s", strings.Join(indexNames, ", ")) + + return activity +} + +func showAccounts(path string, limit uint) []string { + store, err := index.Connect(path) + if err != nil { + log.Fatalf("Failed to connect to index store at %s: %v", path, err) + return nil + } + + accounts, err := store.ReadAccounts() + if err != nil { + log.Fatalf("Failed read accounts from index store at %s: %v", path, err) + return nil + } + + if limit == 0 { + limit = uint(len(accounts)) + } + + for i := uint(0); i < limit; i++ { + log.Info(accounts[i]) + } + + return accounts +} + +func purgeIndex(path string, r historyarchive.Range) error { + freq := historyarchive.DefaultCheckpointFrequency + store, err := index.Connect(path) + if err != nil { + log.Fatalf("Failed to connect to index store at %s: %v", path, err) + return err + } + + accounts, err := store.ReadAccounts() + if err != nil { + log.Fatalf("Failed read accounts: %v", err) + return err + } + + purged := 0 + for _, account := range accounts { + L := log.WithField("account", account) + + indices, err := store.Read(account) + if err != nil { + L.Errorf("Failed to read indices: %v", err) + continue + } + + for name, index := range indices { + var err error + active := uint32(0) + for err == nil { + if active*freq < r.Low { // too low, skip ahead + active, err = index.NextActiveBit(active + 1) + continue + } else if active*freq > r.High { // too high, we're done + break + } + + L.WithField("index", name). + Debugf("Purged checkpoint %d (ledgers %d through %d).", + active, active*freq, (active+1)*freq-1) + + purged++ + + index.SetInactive(active) + active, err = index.NextActiveBit(active) + } + + if err != nil && err != io.EOF { + L.WithField("index", name). + Errorf("Iterating over index failed: %v", err) + continue + } + + } + + store.AddParticipantToIndexesNoBackend(account, indices) + if err := store.Flush(); err != nil { + log.WithField("account", account). + Errorf("Flushing index failed: %v", err) + continue + } + } + + log.Infof("Purged %d values across %d accounts from all indices at %s.", + purged, len(accounts), path) + return nil +} diff --git a/exp/lighthorizon/tools/index_test.go b/exp/lighthorizon/tools/index_test.go new file mode 100644 index 0000000000..6d42f88f30 --- /dev/null +++ b/exp/lighthorizon/tools/index_test.go @@ -0,0 +1,58 @@ +package tools + +import ( + "path/filepath" + "testing" + + "github.com/stellar/go/exp/lighthorizon/index" + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/keypair" + "github.com/stellar/go/support/log" + "github.com/stretchr/testify/require" +) + +const ( + freq = historyarchive.DefaultCheckpointFrequency +) + +func TestIndexPurge(t *testing.T) { + log.SetLevel(log.DebugLevel) + + tempFile := "file://" + filepath.Join(t.TempDir(), "index-store") + accounts := []string{keypair.MustRandom().Address()} + + idx, err := index.Connect(tempFile) + require.NoError(t, err) + + for _, chk := range []uint32{14, 15, 16, 17, 20, 25, 123} { + require.NoError(t, idx.AddParticipantsToIndexes(chk, "test", accounts)) + } + + idx.Flush() // saves to disk + + // Try purging the index + err = purgeIndex(tempFile, historyarchive.Range{Low: 15 * freq, High: 22 * freq}) + require.NoError(t, err) + + // Check to make sure it worked. + idx, err = index.Connect(tempFile) + require.NoError(t, err) + + // Ensure that the index is in the expected state. + indices, err := idx.Read(accounts[0]) + require.NoError(t, err) + require.Contains(t, indices, "test") + + index := indices["test"] + i, err := index.NextActiveBit(0) + require.NoError(t, err) + require.EqualValues(t, 14, i) + + i, err = index.NextActiveBit(15) + require.NoError(t, err) + require.EqualValues(t, 25, i) + + i, err = index.NextActiveBit(i + 1) + require.NoError(t, err) + require.EqualValues(t, 123, i) +} diff --git a/exp/services/ledgerexporter/main.go b/exp/services/ledgerexporter/main.go new file mode 100644 index 0000000000..03c4f53b32 --- /dev/null +++ b/exp/services/ledgerexporter/main.go @@ -0,0 +1,181 @@ +package main + +import ( + "bytes" + "context" + "flag" + "io" + "os" + "strconv" + "strings" + "time" + + "github.com/aws/aws-sdk-go/service/s3" + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/ingest/ledgerbackend" + "github.com/stellar/go/network" + supportlog "github.com/stellar/go/support/log" + "github.com/stellar/go/support/storage" + "github.com/stellar/go/xdr" +) + +var logger = supportlog.New() + +func main() { + targetUrl := flag.String("target", "gcs://horizon-archive-poc", "history archive url to write txmeta files") + stellarCoreBinaryPath := flag.String("stellar-core-binary-path", os.Getenv("STELLAR_CORE_BINARY_PATH"), "path to the stellar core binary") + networkPassphrase := flag.String("network-passphrase", network.TestNetworkPassphrase, "network passphrase") + historyArchiveUrls := flag.String("history-archive-urls", "https://history.stellar.org/prd/core-testnet/core_testnet_001", "comma-separated list of history archive urls to read from") + captiveCoreTomlPath := flag.String("captive-core-toml-path", os.Getenv("CAPTIVE_CORE_TOML_PATH"), "path to load captive core toml file from") + startingLedger := flag.Uint("start-ledger", 2, "ledger to start export from") + continueFromLatestLedger := flag.Bool("continue", false, "start export from the last exported ledger (as indicated in the target's /latest path)") + endingLedger := flag.Uint("end-ledger", 0, "ledger at which to stop the export (must be a closed ledger), 0 means no ending") + writeLatestPath := flag.Bool("write-latest-path", true, "update the value of the /latest path on the target") + captiveCoreUseDb := flag.Bool("captive-core-use-db", true, "configure captive core to store database on disk in working directory rather than in memory") + flag.Parse() + + logger.SetLevel(supportlog.InfoLevel) + + params := ledgerbackend.CaptiveCoreTomlParams{ + NetworkPassphrase: *networkPassphrase, + HistoryArchiveURLs: strings.Split(*historyArchiveUrls, ","), + UseDB: *captiveCoreUseDb, + } + if *captiveCoreTomlPath == "" { + logger.Fatal("Missing -captive-core-toml-path flag") + } + + captiveCoreToml, err := ledgerbackend.NewCaptiveCoreTomlFromFile(*captiveCoreTomlPath, params) + logFatalIf(err, "Invalid captive core toml") + + captiveConfig := ledgerbackend.CaptiveCoreConfig{ + BinaryPath: *stellarCoreBinaryPath, + NetworkPassphrase: params.NetworkPassphrase, + HistoryArchiveURLs: params.HistoryArchiveURLs, + CheckpointFrequency: 64, + Log: logger.WithField("subservice", "stellar-core"), + Toml: captiveCoreToml, + UseDB: *captiveCoreUseDb, + } + core, err := ledgerbackend.NewCaptive(captiveConfig) + logFatalIf(err, "Could not create captive core instance") + + target, err := historyarchive.ConnectBackend( + *targetUrl, + storage.ConnectOptions{ + Context: context.Background(), + S3WriteACL: s3.ObjectCannedACLBucketOwnerFullControl, + }, + ) + logFatalIf(err, "Could not connect to target") + defer target.Close() + + // Build the appropriate range for the given backend state. + startLedger := uint32(*startingLedger) + endLedger := uint32(*endingLedger) + + logger.Infof("processing requested range of -start-ledger=%v, -end-ledger=%v", startLedger, endLedger) + if *continueFromLatestLedger { + if startLedger != 0 { + logger.Fatalf("-start-ledger and -continue cannot both be set") + } + startLedger = readLatestLedger(target) + logger.Infof("continue flag was enabled, next ledger found was %v", startLedger) + } + + if startLedger < 2 { + logger.Fatalf("-start-ledger must be >= 2") + } + if endLedger != 0 && endLedger < startLedger { + logger.Fatalf("-end-ledger must be >= -start-ledger") + } + + var ledgerRange ledgerbackend.Range + if endLedger == 0 { + ledgerRange = ledgerbackend.UnboundedRange(startLedger) + } else { + ledgerRange = ledgerbackend.BoundedRange(startLedger, endLedger) + } + + logger.Infof("preparing to export %s", ledgerRange) + err = core.PrepareRange(context.Background(), ledgerRange) + logFatalIf(err, "could not prepare range") + + for nextLedger := startLedger; endLedger < 1 || nextLedger <= endLedger; { + ledger, err := core.GetLedger(context.Background(), nextLedger) + if err != nil { + logger.WithError(err).Warnf("could not fetch ledger %v, retrying", nextLedger) + time.Sleep(time.Second) + continue + } + + if err = writeLedger(target, ledger); err != nil { + logger.WithError(err).Warnf( + "could not write ledger object %v, retrying", + uint64(ledger.LedgerSequence())) + continue + } + + if *writeLatestPath { + if err = writeLatestLedger(target, nextLedger); err != nil { + logger.WithError(err).Warnf("could not write latest ledger %v", nextLedger) + } + } + + nextLedger++ + } + +} + +// readLatestLedger determines the latest ledger in the given backend (at the +// /latest path), defaulting to Ledger #2 if one doesn't exist +func readLatestLedger(backend storage.Storage) uint32 { + r, err := backend.GetFile("latest") + if os.IsNotExist(err) { + return 2 + } + + logFatalIf(err, "could not open latest ledger bucket") + defer r.Close() + + var buf bytes.Buffer + _, err = io.Copy(&buf, r) + logFatalIf(err, "could not read latest ledger") + + parsed, err := strconv.ParseUint(buf.String(), 10, 32) + logFatalIf(err, "could not parse latest ledger: %s", buf.String()) + return uint32(parsed) +} + +// writeLedger stores the given LedgerCloseMeta instance as a raw binary at the +// /ledgers/ path. If an error is returned, it may be transient so you +// should attempt to retry. +func writeLedger(backend storage.Storage, ledger xdr.LedgerCloseMeta) error { + toSerialize := xdr.SerializedLedgerCloseMeta{ + V: 0, + V0: &ledger, + } + blob, err := toSerialize.MarshalBinary() + logFatalIf(err, "could not serialize ledger %v", ledger.LedgerSequence()) + return backend.PutFile( + "ledgers/"+strconv.FormatUint(uint64(ledger.LedgerSequence()), 10), + io.NopCloser(bytes.NewReader(blob)), + ) +} + +func writeLatestLedger(backend storage.Storage, ledger uint32) error { + return backend.PutFile( + "latest", + io.NopCloser( + bytes.NewBufferString( + strconv.FormatUint(uint64(ledger), 10), + ), + ), + ) +} + +func logFatalIf(err error, message string, args ...interface{}) { + if err != nil { + logger.WithError(err).Fatalf(message, args...) + } +} diff --git a/exp/tools/dump-ledger-state/main.go b/exp/tools/dump-ledger-state/main.go index 1523cbbacb..26f59348a7 100644 --- a/exp/tools/dump-ledger-state/main.go +++ b/exp/tools/dump-ledger-state/main.go @@ -5,7 +5,6 @@ import ( "encoding/base64" "encoding/csv" "flag" - "fmt" "io" "os" "runtime" @@ -16,6 +15,7 @@ import ( "github.com/stellar/go/ingest" "github.com/stellar/go/support/errors" "github.com/stellar/go/support/log" + "github.com/stellar/go/support/storage" "github.com/stellar/go/xdr" ) @@ -307,16 +307,20 @@ func archive(testnet bool) (*historyarchive.Archive, error) { if testnet { return historyarchive.Connect( "https://history.stellar.org/prd/core-testnet/core_testnet_001", - historyarchive.ConnectOptions{ - UserAgent: "dump-ledger-state", + historyarchive.ArchiveOptions{ + ConnectOptions: storage.ConnectOptions{ + UserAgent: "dump-ledger-state", + }, }, ) } return historyarchive.Connect( - fmt.Sprintf("https://history.stellar.org/prd/core-live/core_live_001/"), - historyarchive.ConnectOptions{ - UserAgent: "dump-ledger-state", + "https://history.stellar.org/prd/core-live/core_live_001/", + historyarchive.ArchiveOptions{ + ConnectOptions: storage.ConnectOptions{ + UserAgent: "dump-ledger-state", + }, }, ) } diff --git a/exp/tools/dump-orderbook/main.go b/exp/tools/dump-orderbook/main.go index ce54252c46..7121590caa 100644 --- a/exp/tools/dump-orderbook/main.go +++ b/exp/tools/dump-orderbook/main.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "flag" - "fmt" "io" "os" "strings" @@ -12,6 +11,7 @@ import ( "github.com/stellar/go/historyarchive" "github.com/stellar/go/ingest" "github.com/stellar/go/support/log" + "github.com/stellar/go/support/storage" "github.com/stellar/go/xdr" ) @@ -109,16 +109,20 @@ func archive(testnet bool) (*historyarchive.Archive, error) { if testnet { return historyarchive.Connect( "https://history.stellar.org/prd/core-testnet/core_testnet_001", - historyarchive.ConnectOptions{ - UserAgent: "dump-orderbook", + historyarchive.ArchiveOptions{ + ConnectOptions: storage.ConnectOptions{ + UserAgent: "dump-orderbook", + }, }, ) } return historyarchive.Connect( - fmt.Sprintf("https://history.stellar.org/prd/core-live/core_live_001/"), - historyarchive.ConnectOptions{ - UserAgent: "dump-orderbook", + "https://history.stellar.org/prd/core-live/core_live_001/", + historyarchive.ArchiveOptions{ + ConnectOptions: storage.ConnectOptions{ + UserAgent: "dump-orderbook", + }, }, ) } diff --git a/go.mod b/go.mod index 952e274f91..f3d11b5abc 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/stellar/go go 1.20 require ( + cloud.google.com/go/firestore v1.14.0 // indirect + cloud.google.com/go/storage v1.30.1 firebase.google.com/go v3.12.0+incompatible github.com/2opremio/pretty v0.2.2-0.20230601220618-e1d5758b2a95 github.com/BurntSushi/toml v1.3.2 @@ -17,7 +19,7 @@ require ( github.com/go-chi/chi v4.1.2+incompatible github.com/go-errors/errors v1.5.1 github.com/golang-jwt/jwt v3.2.2+incompatible - github.com/google/uuid v1.3.1 + github.com/google/uuid v1.4.0 github.com/gorilla/schema v1.2.0 github.com/graph-gophers/graphql-go v1.3.0 github.com/guregu/null v4.0.0+incompatible @@ -47,27 +49,31 @@ require ( github.com/stretchr/testify v1.8.4 github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8 github.com/xdrpp/goxdr v0.1.1 - google.golang.org/api v0.143.0 + google.golang.org/api v0.149.0 gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0 gopkg.in/square/go-jose.v2 v2.4.1 gopkg.in/tylerb/graceful.v1 v1.2.15 ) +require golang.org/x/sync v0.4.0 + require ( - cloud.google.com/go/compute v1.23.0 // indirect + cloud.google.com/go/compute v1.23.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v1.1.1 // indirect - cloud.google.com/go/longrunning v0.5.1 // indirect + cloud.google.com/go/iam v1.1.5 // indirect + cloud.google.com/go/longrunning v0.5.4 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/creachadair/mds v0.0.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/gobuffalo/packd v1.0.2 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/s2a-go v0.1.7 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect @@ -77,20 +83,19 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + go.opentelemetry.io/otel v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/otel/trace v1.19.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.13.0 // indirect - golang.org/x/sync v0.4.0 // indirect golang.org/x/tools v0.14.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) require ( - cloud.google.com/go v0.110.7 // indirect - cloud.google.com/go/firestore v1.13.0 // indirect - cloud.google.com/go/storage v1.30.1 // indirect + cloud.google.com/go v0.111.0 // indirect github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/goreplay v1.3.2 @@ -99,10 +104,10 @@ require ( github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect - github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/golang-lru v1.0.2 github.com/imkira/go-interpol v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -131,18 +136,18 @@ require ( github.com/yudai/golcs v0.0.0-20150405163532-d1c525dea8ce // indirect github.com/yudai/pp v2.0.1+incompatible // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.14.0 // indirect + golang.org/x/crypto v0.16.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d - golang.org/x/net v0.17.0 // indirect - golang.org/x/oauth2 v0.12.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/term v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/oauth2 v0.13.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb // indirect - google.golang.org/grpc v1.58.3 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 // indirect + google.golang.org/grpc v1.60.1 // indirect + google.golang.org/protobuf v1.32.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index bb1175e120..9ca895487d 100644 --- a/go.sum +++ b/go.sum @@ -17,26 +17,26 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= -cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= +cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM= +cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= -cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.13.0 h1:/3S4RssUV4GO/kvgJZB+tayjhOfyAHs+KcpJgRVu/Qk= -cloud.google.com/go/firestore v1.13.0/go.mod h1:QojqqOh8IntInDUSTAh0c8ZsPYAr68Ma8c5DWOy8xb8= -cloud.google.com/go/iam v1.1.1 h1:lW7fzj15aVIXYHREOqjRBV9PsH0Z6u8Y46a1YGvQP4Y= -cloud.google.com/go/iam v1.1.1/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU= -cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI= -cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= +cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw= +cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= +cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -138,7 +138,11 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -178,6 +182,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -194,8 +199,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5 h1:oERTZ1buOUYlpmKaqlO5fYmz8cZ1rYu5DieJzF4ZVmU= github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gopacket v1.1.20-0.20210429153827-3eaba0894325/go.mod h1:riddUzxTSBpJXk3qBHtYr4qOhFhT6k/1c0E3qkQjQpA= @@ -220,10 +225,10 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.1 h1:SBWmZhjUDRorQxrN0nwzf+AHBxnbFjViHQS4P0yVpmQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.1/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= @@ -448,6 +453,13 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -460,8 +472,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -538,8 +550,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -549,8 +561,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= -golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -613,13 +625,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -628,9 +640,10 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -693,7 +706,6 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -713,16 +725,17 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.143.0 h1:o8cekTkqhywkbZT6p1UHJPZ9+9uuCAJs/KYomxZB8fA= -google.golang.org/api v0.143.0/go.mod h1:FoX9DO9hT7DLNn97OuoZAGSDuNAXdJRuGK98rSUgurk= +google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY= +google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -759,12 +772,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb h1:XFBgcDwm7irdHTbz4Zk2h7Mh+eis4nfJEFQFYzJzuIA= -google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= -google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb h1:lK0oleSc7IQsUxO3U5TjL9DWlsxpEBemh+zpB7IqhWI= -google.golang.org/genproto/googleapis/api v0.0.0-20230913181813-007df8e322eb/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 h1:N3bU/SQDCDyD6R528GJ/PwW9KjYcJA3dgyH+MovAkIM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:KSqppvjFjtoCI+KGd4PELB0qLNxdJHRGqRI09mB6pQA= +google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos= +google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY= +google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 h1:EWIeHfGuUf00zrVZGEgYFxok7plSAXBGcH7NNdMAWvA= +google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -781,8 +794,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= -google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -795,8 +808,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/gxdr/xdr_generated.go b/gxdr/xdr_generated.go index 67270fcbbd..b66e0a2c7c 100644 --- a/gxdr/xdr_generated.go +++ b/gxdr/xdr_generated.go @@ -1,4 +1,4 @@ -// Code generated by goxdr -p gxdr -enum-comments -o gxdr/xdr_generated.go xdr/Stellar-SCP.x xdr/Stellar-ledger-entries.x xdr/Stellar-ledger.x xdr/Stellar-overlay.x xdr/Stellar-transaction.x xdr/Stellar-types.x xdr/Stellar-contract-env-meta.x xdr/Stellar-contract-meta.x xdr/Stellar-contract-spec.x xdr/Stellar-contract.x xdr/Stellar-internal.x xdr/Stellar-contract-config-setting.x; DO NOT EDIT. +// Code generated by goxdr -p gxdr -enum-comments -o gxdr/xdr_generated.go xdr/Stellar-SCP.x xdr/Stellar-ledger-entries.x xdr/Stellar-ledger.x xdr/Stellar-overlay.x xdr/Stellar-transaction.x xdr/Stellar-types.x xdr/Stellar-contract-env-meta.x xdr/Stellar-contract-meta.x xdr/Stellar-contract-spec.x xdr/Stellar-contract.x xdr/Stellar-internal.x xdr/Stellar-contract-config-setting.x xdr/Stellar-lighthorizon.x; DO NOT EDIT. package gxdr @@ -4412,6 +4412,37 @@ type ConfigSettingEntry struct { _u interface{} } +type BitmapIndex struct { + FirstBit Uint32 + LastBit Uint32 + Bitmap Value +} + +type TrieIndex struct { + // goxdr gives an error if we simply use "version" as an identifier + Version_ Uint32 + Root TrieNode +} + +type TrieNodeChild struct { + Key [1]byte + Node TrieNode +} + +type TrieNode struct { + Prefix Value + Value Value + Children []TrieNodeChild +} + +type SerializedLedgerCloseMeta struct { + // The union discriminant V selects among the following arms: + // 0: + // V0() *LedgerCloseMeta + V int32 + _u interface{} +} + // // Helper types and generated marshaling functions // @@ -29159,3 +29190,208 @@ func (u *ConfigSettingEntry) XdrRecurse(x XDR, name string) { XdrPanic("invalid ConfigSettingID (%v) in ConfigSettingEntry", u.ConfigSettingID) } func XDR_ConfigSettingEntry(v *ConfigSettingEntry) *ConfigSettingEntry { return v } + +type XdrType_BitmapIndex = *BitmapIndex + +func (v *BitmapIndex) XdrPointer() interface{} { return v } +func (BitmapIndex) XdrTypeName() string { return "BitmapIndex" } +func (v BitmapIndex) XdrValue() interface{} { return v } +func (v *BitmapIndex) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *BitmapIndex) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%sfirstBit", name), XDR_Uint32(&v.FirstBit)) + x.Marshal(x.Sprintf("%slastBit", name), XDR_Uint32(&v.LastBit)) + x.Marshal(x.Sprintf("%sbitmap", name), XDR_Value(&v.Bitmap)) +} +func XDR_BitmapIndex(v *BitmapIndex) *BitmapIndex { return v } + +type XdrType_TrieIndex = *TrieIndex + +func (v *TrieIndex) XdrPointer() interface{} { return v } +func (TrieIndex) XdrTypeName() string { return "TrieIndex" } +func (v TrieIndex) XdrValue() interface{} { return v } +func (v *TrieIndex) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *TrieIndex) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%sversion_", name), XDR_Uint32(&v.Version_)) + x.Marshal(x.Sprintf("%sroot", name), XDR_TrieNode(&v.Root)) +} +func XDR_TrieIndex(v *TrieIndex) *TrieIndex { return v } + +type _XdrArray_1_opaque [1]byte + +func (v *_XdrArray_1_opaque) GetByteSlice() []byte { return v[:] } +func (v *_XdrArray_1_opaque) XdrTypeName() string { return "opaque[]" } +func (v *_XdrArray_1_opaque) XdrValue() interface{} { return v[:] } +func (v *_XdrArray_1_opaque) XdrPointer() interface{} { return (*[1]byte)(v) } +func (v *_XdrArray_1_opaque) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *_XdrArray_1_opaque) String() string { return fmt.Sprintf("%x", v[:]) } +func (v *_XdrArray_1_opaque) Scan(ss fmt.ScanState, c rune) error { + return XdrArrayOpaqueScan(v[:], ss, c) +} +func (_XdrArray_1_opaque) XdrArraySize() uint32 { + const bound uint32 = 1 // Force error if not const or doesn't fit + return bound +} + +type XdrType_TrieNodeChild = *TrieNodeChild + +func (v *TrieNodeChild) XdrPointer() interface{} { return v } +func (TrieNodeChild) XdrTypeName() string { return "TrieNodeChild" } +func (v TrieNodeChild) XdrValue() interface{} { return v } +func (v *TrieNodeChild) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *TrieNodeChild) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%skey", name), (*_XdrArray_1_opaque)(&v.Key)) + x.Marshal(x.Sprintf("%snode", name), XDR_TrieNode(&v.Node)) +} +func XDR_TrieNodeChild(v *TrieNodeChild) *TrieNodeChild { return v } + +type _XdrVec_unbounded_TrieNodeChild []TrieNodeChild + +func (_XdrVec_unbounded_TrieNodeChild) XdrBound() uint32 { + const bound uint32 = 4294967295 // Force error if not const or doesn't fit + return bound +} +func (_XdrVec_unbounded_TrieNodeChild) XdrCheckLen(length uint32) { + if length > uint32(4294967295) { + XdrPanic("_XdrVec_unbounded_TrieNodeChild length %d exceeds bound 4294967295", length) + } else if int(length) < 0 { + XdrPanic("_XdrVec_unbounded_TrieNodeChild length %d exceeds max int", length) + } +} +func (v _XdrVec_unbounded_TrieNodeChild) GetVecLen() uint32 { return uint32(len(v)) } +func (v *_XdrVec_unbounded_TrieNodeChild) SetVecLen(length uint32) { + v.XdrCheckLen(length) + if int(length) <= cap(*v) { + if int(length) != len(*v) { + *v = (*v)[:int(length)] + } + return + } + newcap := 2 * cap(*v) + if newcap < int(length) { // also catches overflow where 2*cap < 0 + newcap = int(length) + } else if bound := uint(4294967295); uint(newcap) > bound { + if int(bound) < 0 { + bound = ^uint(0) >> 1 + } + newcap = int(bound) + } + nv := make([]TrieNodeChild, int(length), newcap) + copy(nv, *v) + *v = nv +} +func (v *_XdrVec_unbounded_TrieNodeChild) XdrMarshalN(x XDR, name string, n uint32) { + v.XdrCheckLen(n) + for i := 0; i < int(n); i++ { + if i >= len(*v) { + v.SetVecLen(uint32(i + 1)) + } + XDR_TrieNodeChild(&(*v)[i]).XdrMarshal(x, x.Sprintf("%s[%d]", name, i)) + } + if int(n) < len(*v) { + *v = (*v)[:int(n)] + } +} +func (v *_XdrVec_unbounded_TrieNodeChild) XdrRecurse(x XDR, name string) { + size := XdrSize{Size: uint32(len(*v)), Bound: 4294967295} + x.Marshal(name, &size) + v.XdrMarshalN(x, name, size.Size) +} +func (_XdrVec_unbounded_TrieNodeChild) XdrTypeName() string { return "TrieNodeChild<>" } +func (v *_XdrVec_unbounded_TrieNodeChild) XdrPointer() interface{} { return (*[]TrieNodeChild)(v) } +func (v _XdrVec_unbounded_TrieNodeChild) XdrValue() interface{} { return ([]TrieNodeChild)(v) } +func (v *_XdrVec_unbounded_TrieNodeChild) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } + +type XdrType_TrieNode = *TrieNode + +func (v *TrieNode) XdrPointer() interface{} { return v } +func (TrieNode) XdrTypeName() string { return "TrieNode" } +func (v TrieNode) XdrValue() interface{} { return v } +func (v *TrieNode) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *TrieNode) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%sprefix", name), XDR_Value(&v.Prefix)) + x.Marshal(x.Sprintf("%svalue", name), XDR_Value(&v.Value)) + x.Marshal(x.Sprintf("%schildren", name), (*_XdrVec_unbounded_TrieNodeChild)(&v.Children)) +} +func XDR_TrieNode(v *TrieNode) *TrieNode { return v } + +var _XdrTags_SerializedLedgerCloseMeta = map[int32]bool{ + XdrToI32(0): true, +} + +func (_ SerializedLedgerCloseMeta) XdrValidTags() map[int32]bool { + return _XdrTags_SerializedLedgerCloseMeta +} +func (u *SerializedLedgerCloseMeta) V0() *LedgerCloseMeta { + switch u.V { + case 0: + if v, ok := u._u.(*LedgerCloseMeta); ok { + return v + } else { + var zero LedgerCloseMeta + u._u = &zero + return &zero + } + default: + XdrPanic("SerializedLedgerCloseMeta.V0 accessed when V == %v", u.V) + return nil + } +} +func (u SerializedLedgerCloseMeta) XdrValid() bool { + switch u.V { + case 0: + return true + } + return false +} +func (u *SerializedLedgerCloseMeta) XdrUnionTag() XdrNum32 { + return XDR_int32(&u.V) +} +func (u *SerializedLedgerCloseMeta) XdrUnionTagName() string { + return "V" +} +func (u *SerializedLedgerCloseMeta) XdrUnionBody() XdrType { + switch u.V { + case 0: + return XDR_LedgerCloseMeta(u.V0()) + } + return nil +} +func (u *SerializedLedgerCloseMeta) XdrUnionBodyName() string { + switch u.V { + case 0: + return "V0" + } + return "" +} + +type XdrType_SerializedLedgerCloseMeta = *SerializedLedgerCloseMeta + +func (v *SerializedLedgerCloseMeta) XdrPointer() interface{} { return v } +func (SerializedLedgerCloseMeta) XdrTypeName() string { return "SerializedLedgerCloseMeta" } +func (v SerializedLedgerCloseMeta) XdrValue() interface{} { return v } +func (v *SerializedLedgerCloseMeta) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (u *SerializedLedgerCloseMeta) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + XDR_int32(&u.V).XdrMarshal(x, x.Sprintf("%sv", name)) + switch u.V { + case 0: + x.Marshal(x.Sprintf("%sv0", name), XDR_LedgerCloseMeta(u.V0())) + return + } + XdrPanic("invalid V (%v) in SerializedLedgerCloseMeta", u.V) +} +func XDR_SerializedLedgerCloseMeta(v *SerializedLedgerCloseMeta) *SerializedLedgerCloseMeta { return v } diff --git a/historyarchive/archive.go b/historyarchive/archive.go index 2d470a8026..1679d2210f 100644 --- a/historyarchive/archive.go +++ b/historyarchive/archive.go @@ -21,6 +21,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/storage" "github.com/stellar/go/xdr" ) @@ -37,20 +38,16 @@ type CommandOptions struct { SkipOptional bool } -type ConnectOptions struct { - Context context.Context +type ArchiveOptions struct { // NetworkPassphrase defines the expected network of history archive. It is // checked when getting HAS. If network passphrase does not match, error is // returned. NetworkPassphrase string - S3Region string - S3Endpoint string - UnsignedRequests bool // CheckpointFrequency is the number of ledgers between checkpoints // if unset, DefaultCheckpointFrequency will be used CheckpointFrequency uint32 - // UserAgent is the value of `User-Agent` header. Applicable only for HTTP client. - UserAgent string + + storage.ConnectOptions } type Ledger struct { @@ -59,15 +56,6 @@ type Ledger struct { TransactionResult xdr.TransactionHistoryResultEntry } -type ArchiveBackend interface { - Exists(path string) (bool, error) - Size(path string) (int64, error) - GetFile(path string) (io.ReadCloser, error) - PutFile(path string, in io.ReadCloser) error - ListFiles(path string) (chan string, chan error) - CanListFiles() bool -} - type ArchiveInterface interface { GetPathHAS(path string) (HistoryArchiveState, error) PutPathHAS(path string, has HistoryArchiveState, opts *CommandOptions) error @@ -114,7 +102,7 @@ type Archive struct { checkpointManager CheckpointManager - backend ArchiveBackend + backend storage.Storage } func (arch *Archive) GetCheckpointManager() CheckpointManager { @@ -378,7 +366,7 @@ func (a *Archive) GetXdrStream(pth string) (*XdrStream, error) { return NewXdrGzStream(rdr) } -func Connect(u string, opts ConnectOptions) (*Archive, error) { +func Connect(u string, opts ArchiveOptions) (*Archive, error) { arch := Archive{ networkPassphrase: opts.NetworkPassphrase, checkpointFiles: make(map[string](map[uint32]bool)), @@ -396,40 +384,36 @@ func Connect(u string, opts ConnectOptions) (*Archive, error) { arch.checkpointFiles[cat] = make(map[uint32]bool) } + if opts.ConnectOptions.Context == nil { + opts.ConnectOptions.Context = context.Background() + } + + var err error + arch.backend, err = ConnectBackend(u, opts.ConnectOptions) + return &arch, err +} + +func ConnectBackend(u string, opts storage.ConnectOptions) (storage.Storage, error) { if u == "" { - return &arch, errors.New("URL is empty") + return nil, errors.New("URL is empty") } parsed, err := url.Parse(u) if err != nil { - return &arch, err - } - - if opts.Context == nil { - opts.Context = context.Background() + return nil, err } - pth := parsed.Path - if parsed.Scheme == "s3" { - // Inside s3, all paths start _without_ the leading / - if len(pth) > 0 && pth[0] == '/' { - pth = pth[1:] - } - arch.backend, err = makeS3Backend(parsed.Host, pth, opts) - } else if parsed.Scheme == "file" { - pth = path.Join(parsed.Host, pth) - arch.backend = makeFsBackend(pth, opts) - } else if parsed.Scheme == "http" || parsed.Scheme == "https" { - arch.backend = makeHttpBackend(parsed, opts) - } else if parsed.Scheme == "mock" { - arch.backend = makeMockBackend(opts) + var backend storage.Storage + if parsed.Scheme == "mock" { + backend = makeMockBackend() } else { - err = errors.New("unknown URL scheme: '" + parsed.Scheme + "'") + backend, err = storage.ConnectBackend(u, opts) } - return &arch, err + + return backend, err } -func MustConnect(u string, opts ConnectOptions) *Archive { +func MustConnect(u string, opts ArchiveOptions) *Archive { arch, err := Connect(u, opts) if err != nil { log.Fatal(err) diff --git a/historyarchive/archive_pool.go b/historyarchive/archive_pool.go index 590988e483..e4e24e0853 100644 --- a/historyarchive/archive_pool.go +++ b/historyarchive/archive_pool.go @@ -21,7 +21,7 @@ type ArchivePool []ArchiveInterface // If none of the archives work, this returns the error message of the last // failed archive. Note that the errors for each individual archive are hard to // track if there's success overall. -func NewArchivePool(archiveURLs []string, config ConnectOptions) (ArchivePool, error) { +func NewArchivePool(archiveURLs []string, opts ArchiveOptions) (ArchivePool, error) { if len(archiveURLs) <= 0 { return nil, errors.New("No history archives provided") } @@ -33,11 +33,7 @@ func NewArchivePool(archiveURLs []string, config ConnectOptions) (ArchivePool, e for _, url := range archiveURLs { archive, err := Connect( url, - ConnectOptions{ - NetworkPassphrase: config.NetworkPassphrase, - CheckpointFrequency: config.CheckpointFrequency, - Context: config.Context, - }, + opts, ) if err != nil { diff --git a/historyarchive/archive_test.go b/historyarchive/archive_test.go index 9f5f4fc0c9..4f90802bf7 100644 --- a/historyarchive/archive_test.go +++ b/historyarchive/archive_test.go @@ -17,6 +17,7 @@ import ( "strings" "testing" + "github.com/stellar/go/support/storage" "github.com/stellar/go/xdr" "github.com/stretchr/testify/assert" ) @@ -35,11 +36,17 @@ func GetTestS3Archive() *Archive { if env_region := os.Getenv("ARCHIVIST_TEST_S3_REGION"); env_region != "" { region = env_region } - return MustConnect(bucket, ConnectOptions{S3Region: region, CheckpointFrequency: 64}) + return MustConnect( + bucket, + ArchiveOptions{ + CheckpointFrequency: DefaultCheckpointFrequency, + ConnectOptions: storage.ConnectOptions{S3Region: region}, + }, + ) } func GetTestMockArchive() *Archive { - return MustConnect("mock://test", ConnectOptions{CheckpointFrequency: 64}) + return MustConnect("mock://test", ArchiveOptions{CheckpointFrequency: DefaultCheckpointFrequency}) } var tmpdirs []string @@ -54,7 +61,7 @@ func GetTestFileArchive() *Archive { } else { tmpdirs = append(tmpdirs, d) } - return MustConnect("file://"+d, ConnectOptions{CheckpointFrequency: 64}) + return MustConnect("file://"+d, ArchiveOptions{CheckpointFrequency: DefaultCheckpointFrequency}) } func cleanup() { @@ -381,16 +388,16 @@ func TestNetworkPassphrase(t *testing.T) { } // No network passphrase set in options - archive := MustConnect("mock://test", ConnectOptions{CheckpointFrequency: 64}) + archive := MustConnect("mock://test", ArchiveOptions{CheckpointFrequency: DefaultCheckpointFrequency}) err := archive.backend.PutFile("has.json", makeHASReader()) assert.NoError(t, err) _, err = archive.GetPathHAS("has.json") assert.NoError(t, err) // No network passphrase set in HAS - archive = MustConnect("mock://test", ConnectOptions{ + archive = MustConnect("mock://test", ArchiveOptions{ NetworkPassphrase: "Public Global Stellar Network ; September 2015", - CheckpointFrequency: 64, + CheckpointFrequency: DefaultCheckpointFrequency, }) err = archive.backend.PutFile("has.json", makeHASReaderNoNetwork()) assert.NoError(t, err) @@ -398,9 +405,9 @@ func TestNetworkPassphrase(t *testing.T) { assert.NoError(t, err) // Correct network passphrase set in options - archive = MustConnect("mock://test", ConnectOptions{ + archive = MustConnect("mock://test", ArchiveOptions{ NetworkPassphrase: "Public Global Stellar Network ; September 2015", - CheckpointFrequency: 64, + CheckpointFrequency: DefaultCheckpointFrequency, }) err = archive.backend.PutFile("has.json", makeHASReader()) assert.NoError(t, err) @@ -408,9 +415,9 @@ func TestNetworkPassphrase(t *testing.T) { assert.NoError(t, err) // Incorrect network passphrase set in options - archive = MustConnect("mock://test", ConnectOptions{ + archive = MustConnect("mock://test", ArchiveOptions{ NetworkPassphrase: "Test SDF Network ; September 2015", - CheckpointFrequency: 64, + CheckpointFrequency: DefaultCheckpointFrequency, }) err = archive.backend.PutFile("has.json", makeHASReader()) assert.NoError(t, err) @@ -500,7 +507,7 @@ type xdrEntry interface { MarshalBinary() ([]byte, error) } -func writeCategoryFile(t *testing.T, backend ArchiveBackend, path string, entries []xdrEntry) { +func writeCategoryFile(t *testing.T, backend storage.Storage, path string, entries []xdrEntry) { file := &bytes.Buffer{} writer := gzip.NewWriter(file) diff --git a/historyarchive/mock_archive.go b/historyarchive/mock_archive.go index fc8095bbb4..9f8bab69de 100644 --- a/historyarchive/mock_archive.go +++ b/historyarchive/mock_archive.go @@ -11,6 +11,8 @@ import ( "io/ioutil" "strings" "sync" + + "github.com/stellar/go/support/storage" ) type MockArchiveBackend struct { @@ -83,7 +85,11 @@ func (b *MockArchiveBackend) CanListFiles() bool { return true } -func makeMockBackend(opts ConnectOptions) ArchiveBackend { +func (b *MockArchiveBackend) Close() error { + return nil +} + +func makeMockBackend() storage.Storage { b := new(MockArchiveBackend) b.files = make(map[string][]byte) return b diff --git a/historyarchive/range.go b/historyarchive/range.go index da79827e24..a81523b2b3 100644 --- a/historyarchive/range.go +++ b/historyarchive/range.go @@ -41,6 +41,7 @@ func (c CheckpointManager) IsCheckpoint(i uint32) bool { return (i+1)%c.checkpointFreq == 0 } +// PrevCheckpoint returns the checkpoint ledger preceding `i`. func (c CheckpointManager) PrevCheckpoint(i uint32) uint32 { freq := c.checkpointFreq if i < freq { @@ -49,6 +50,7 @@ func (c CheckpointManager) PrevCheckpoint(i uint32) uint32 { return (((i + 1) / freq) * freq) - 1 } +// NextCheckpoint returns the checkpoint ledger following `i`. func (c CheckpointManager) NextCheckpoint(i uint32) uint32 { if i == 0 { return c.checkpointFreq - 1 @@ -124,6 +126,10 @@ func (r Range) InRange(sequence uint32) bool { return sequence >= r.Low && sequence <= r.High } +func (r Range) Size() uint32 { + return 1 + (r.High - r.Low) +} + func fmtRangeList(vs []uint32, cManager CheckpointManager) string { slices.Sort(vs) diff --git a/historyarchive/util.go b/historyarchive/util.go index b2a7c96778..0753decabb 100644 --- a/historyarchive/util.go +++ b/historyarchive/util.go @@ -7,10 +7,10 @@ package historyarchive import ( "bufio" "fmt" - log "github.com/sirupsen/logrus" "io" - "net/http" "path" + + log "github.com/sirupsen/logrus" ) func makeTicker(onTick func(uint)) chan bool { @@ -137,23 +137,3 @@ func drainErrors(errs chan error) uint32 { } return count } - -func logReq(r *http.Request) { - if r == nil { - return - } - logFields := log.Fields{"method": r.Method, "url": r.URL.String()} - log.WithFields(logFields).Trace("http: Req") -} - -func logResp(r *http.Response) { - if r == nil || r.Request == nil { - return - } - logFields := log.Fields{"method": r.Request.Method, "status": r.Status, "url": r.Request.URL.String()} - if r.StatusCode >= 200 && r.StatusCode < 400 { - log.WithFields(logFields).Trace("http: OK") - } else { - log.WithFields(logFields).Warn("http: Bad") - } -} diff --git a/ingest/doc_test.go b/ingest/doc_test.go index 7ac0719df3..cd266f37d5 100644 --- a/ingest/doc_test.go +++ b/ingest/doc_test.go @@ -8,6 +8,7 @@ import ( "github.com/stellar/go/historyarchive" "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/network" + "github.com/stellar/go/support/storage" "github.com/stellar/go/xdr" ) @@ -18,7 +19,11 @@ func Example_ledgerentrieshistoryarchive() { archive, err := historyarchive.Connect( archiveURL, - historyarchive.ConnectOptions{Context: context.TODO()}, + historyarchive.ArchiveOptions{ + ConnectOptions: storage.ConnectOptions{ + Context: context.TODO(), + }, + }, ) if err != nil { panic(err) diff --git a/ingest/ledgerbackend/captive_core_backend.go b/ingest/ledgerbackend/captive_core_backend.go index aa9414b5d1..a8acb19182 100644 --- a/ingest/ledgerbackend/captive_core_backend.go +++ b/ingest/ledgerbackend/captive_core_backend.go @@ -16,6 +16,7 @@ import ( "github.com/stellar/go/clients/stellarcore" "github.com/stellar/go/historyarchive" "github.com/stellar/go/support/log" + "github.com/stellar/go/support/storage" "github.com/stellar/go/xdr" ) @@ -174,10 +175,12 @@ func NewCaptive(config CaptiveCoreConfig) (*CaptiveStellarCore, error) { archivePool, err := historyarchive.NewArchivePool( config.HistoryArchiveURLs, - historyarchive.ConnectOptions{ + historyarchive.ArchiveOptions{ NetworkPassphrase: config.NetworkPassphrase, CheckpointFrequency: config.CheckpointFrequency, - Context: config.Context, + ConnectOptions: storage.ConnectOptions{ + Context: config.Context, + }, }, ) diff --git a/ingest/ledgerbackend/history_archive_backend.go b/ingest/ledgerbackend/history_archive_backend.go new file mode 100644 index 0000000000..331f43032d --- /dev/null +++ b/ingest/ledgerbackend/history_archive_backend.go @@ -0,0 +1,51 @@ +package ledgerbackend + +import ( + "context" + "fmt" + + "github.com/stellar/go/metaarchive" + "github.com/stellar/go/xdr" +) + +type HistoryArchiveBackend struct { + metaArchive metaarchive.MetaArchive +} + +func NewHistoryArchiveBackend(metaArchive metaarchive.MetaArchive) *HistoryArchiveBackend { + return &HistoryArchiveBackend{ + metaArchive: metaArchive, + } +} + +func (b *HistoryArchiveBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { + return b.metaArchive.GetLatestLedgerSequence(ctx) +} + +func (b *HistoryArchiveBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { + // Noop + return nil +} + +func (b *HistoryArchiveBackend) IsPrepared(ctx context.Context, ledgerRange Range) (bool, error) { + // Noop + return true, nil +} + +func (b *HistoryArchiveBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { + serializedLedger, err := b.metaArchive.GetLedger(ctx, sequence) + if err != nil { + return xdr.LedgerCloseMeta{}, err + } + + output, isV0 := serializedLedger.GetV0() + if !isV0 { + return xdr.LedgerCloseMeta{}, fmt.Errorf("unexpected serialized ledger version number (0x%x)", serializedLedger.V) + } + return output, nil +} + +func (b *HistoryArchiveBackend) Close() error { + // Noop + return nil +} diff --git a/ingest/tutorial/example_claimables.go b/ingest/tutorial/example_claimables.go index 161e538aae..409602f960 100644 --- a/ingest/tutorial/example_claimables.go +++ b/ingest/tutorial/example_claimables.go @@ -8,6 +8,7 @@ import ( "github.com/stellar/go/historyarchive" "github.com/stellar/go/ingest" + "github.com/stellar/go/support/storage" "github.com/stellar/go/xdr" ) @@ -15,10 +16,12 @@ func claimables() { // Open a history archive using our existing configuration details. historyArchive, err := historyarchive.Connect( config.HistoryArchiveURLs[0], - historyarchive.ConnectOptions{ + historyarchive.ArchiveOptions{ NetworkPassphrase: config.NetworkPassphrase, - S3Region: "us-west-1", - UnsignedRequests: false, + ConnectOptions: storage.ConnectOptions{ + S3Region: "us-west-1", + UnsignedRequests: false, + }, }, ) panicIf(err) diff --git a/metaarchive/main.go b/metaarchive/main.go new file mode 100644 index 0000000000..7d06a46f9a --- /dev/null +++ b/metaarchive/main.go @@ -0,0 +1,62 @@ +package metaarchive + +import ( + "bytes" + "context" + "io" + "os" + "strconv" + + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/storage" + "github.com/stellar/go/xdr" +) + +type MetaArchive interface { + GetLatestLedgerSequence(ctx context.Context) (uint32, error) + GetLedger(ctx context.Context, sequence uint32) (xdr.SerializedLedgerCloseMeta, error) +} + +type metaArchive struct { + s storage.Storage +} + +func NewMetaArchive(b storage.Storage) MetaArchive { + return &metaArchive{s: b} +} + +func (m *metaArchive) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { + r, err := m.s.GetFile("latest") + if os.IsNotExist(err) { + return 2, nil + } else if err != nil { + return 0, errors.Wrap(err, "could not open latest ledger bucket") + } + defer r.Close() + var buf bytes.Buffer + if _, err = io.Copy(&buf, r); err != nil { + return 0, errors.Wrap(err, "could not read latest ledger") + } + parsed, err := strconv.ParseUint(buf.String(), 10, 32) + if err != nil { + return 0, errors.Wrapf(err, "could not parse latest ledger: %q", buf.String()) + } + return uint32(parsed), nil +} + +func (m *metaArchive) GetLedger(ctx context.Context, sequence uint32) (xdr.SerializedLedgerCloseMeta, error) { + var ledger xdr.SerializedLedgerCloseMeta + r, err := m.s.GetFile("ledgers/" + strconv.FormatUint(uint64(sequence), 10)) + if err != nil { + return xdr.SerializedLedgerCloseMeta{}, err + } + defer r.Close() + var buf bytes.Buffer + if _, err = io.Copy(&buf, r); err != nil { + return xdr.SerializedLedgerCloseMeta{}, err + } + if err = ledger.UnmarshalBinary(buf.Bytes()); err != nil { + return xdr.SerializedLedgerCloseMeta{}, err + } + return ledger, nil +} diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index c3c5705464..619c1f40e7 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -333,6 +333,134 @@ These fields are now represented by `preconditions.timebounds.min_time` and `pre * Check if there are newer ledger when requested ledger does not exist. ([4198](https://github.com/stellar/go/pull/4198)) * Properly check against the HA array being empty. ([4152](https://github.com/stellar/go/pull/4152)) +- Querying claimable balances has been optimized ([4385](https://github.com/stellar/go/pull/4385)). +- Querying trade aggregations has been optimized ([4389](https://github.com/stellar/go/pull/4389)). +- Postgres connections for non ingesting Horizon instances are now configured to timeout on long running queries / transactions ([4390](https://github.com/stellar/go/pull/4390)). +- Added `disable-path-finding` Horizon flag to disable the path finding endpoints. This flag should be enabled on ingesting Horizon instances which do not serve HTTP traffic ([4399](https://github.com/stellar/go/pull/4399)). + +## V2.17.0 + +This is the final release after the [release candidate](v2.17.0-release-candidate), including some small additional changes: + +- The transaction precondition record now excludes ([4360](https://github.com/stellar/go/pull/4360)): + * `min_account_sequence_age` when it's `"0"`, as this is the default value when the condition is not set + * `preconditions.ledgerbounds.max_ledger` when it's set to 0 (this means that there is no upper bound) + +- Timebounds within the `preconditions` object are strings containing int64 UNIX timestamps in seconds rather than formatted date-times (which was a bug) ([4361](https://github.com/stellar/go/pull/4361)). + +* New Ingestion Filters Feature: Provide the ability to select which ledger transactions are accepted at ingestion time to be stored on horizon's historical databse. + + Define filter rules through Admin API and the historical ingestion process will check the rules and only persist the ledger transactions that pass the filter rules. Initially, two filters and corresponding rules are possible: + + * 'whitelist by account id' ([4221](https://github.com/stellar/go/issues/4221)) + * 'whitelist by canonical asset id' ([4222](https://github.com/stellar/go/issues/4222)) + + The filters and their configuration are optional features and must be enabled with horizon command line parameters `admin-port=4200` and `enable-ingestion-filtering=true` + + Once set, filter configurations and their rules are initially empty and the filters are disabled by default. To enable filters, update the configuration settings, refer to the Admin API Docs which are published on the Admin Port at http://localhost:/, follow details and examples for endpoints: + * `/ingestion/filters/account` + * `/ingestion/filters/asset.` + +## V2.17.0 Release Candidate + +**Upgrading to this version from <= v2.8.3 will trigger a state rebuild. During this process (which will take at least 10 minutes), Horizon will not ingest new ledgers.** + +**Support for Protocol 19** ([4340](https://github.com/stellar/go/pull/4340)): + + - Account records can now contain two new, optional fields: + +```txt + "sequence_ledger": 0, // uint32 ledger number + "sequence_time": "0" // uint64 unix time in seconds, as a string +``` + + The absence of these fields indicates that the account hasn't taken any actions since prior to the Protocol 19 release. Note that they'll either be both present or both absent. + + - Transaction records can now contain the following optional object: + +```txt + "preconditions": { + "timebounds": { + "min_time": "0", // uint64 unix time in seconds, as a string + "max_time": "0" // as above + }, + "ledgerbounds": { + "min_ledger": 0, // uint32 ledger number + "max_ledger": 0 // as above + }, + "min_account_sequence": "0", // int64 sequence number, as a string + "min_account_sequence_age": "0", // uint64 unix time in seconds, as a string + "min_account_sequence_ledger_gap": 0, // uint32 ledger count + + "extra_signers": [] // list of signers as StrKeys + } +``` + + All of the top-level fields within this object are also optional. However, the "ledgerbounds" object will always have at least its `min_ledger` field set. + + Note that the existing "valid_before_time" and "valid_after_time" fields on the top-level object will be identical to the "preconditions.timebounds.min_time" and "preconditions.timebounds.min_time" fields, respectively, if those exist. The "valid_before_time" and "valid_after_time" fields are now considered deprecated and will be removed in Horizon v3.0.0. + +### DB Schema Migration + +The migration makes the following schema changes: + + - adds new, optional columns to the `history_transactions` table related to the new preconditions + - adds new, optional columns to the `accounts` table related to the new account extension + - amends the `signer` column of the `accounts_signers` table to allow signers of arbitrary length + +### Deprecations + +The following fields on transaction records have been deprecated and will be removed in a future version: + + - `"valid_before"` and `"valid_after"` + +These fields are now represented by `preconditions.timebounds.min_time` and `preconditions.timebounds.max_time` as `uint64` UNIX timestamps, in seconds. + +## V2.16.1 + +* v2.16.0 rebuilt using Golang 1.18.1 with security fixes for CVE-2022-24675, CVE-2022-28327 and CVE-2022-27536. + +## V2.16.0 + +* Replace keybase with publicnode in the stellar core config. ([4291](https://github.com/stellar/go/pull/4291)) +* Add a rate limit for path finding requests. ([4310](https://github.com/stellar/go/pull/4310)) +* Horizonclient, fix multi-parameter url for claimable balance query. ([4248](https://github.com/stellar/go/pull/4248)) + +## v2.15.1 + +**Upgrading to this version from <= v2.8.3 will trigger a state rebuild. During this process (which will take at least 10 minutes), Horizon will not ingest new ledgers.** + +### Fixes + +* Fixed a regression preventing running multiple concurrent captive-core ingestion instances. ([4251](https://github.com/stellar/go/pull/4251)) + +## v2.15.0 + +**Upgrading to this version from <= v2.8.3 will trigger a state rebuild. During this process (which will take at least 10 minutes), Horizon will not ingest new ledgers.** + +### DB Schema Migration + +* DB migrations add columns to the `history_trades` table to enable filtering trades by "rounding slippage". This is very large table so migration may take a long time (depending on your DB hardware). Please test the migrations execution time on the copy of your production DB first. + +### Features + +* New feature, enable captive core based ingestion to use remote db persistence rather than in-memory for ledger states. Essentially moves what would have been stored in RAM to the external db instead. Recent profiling on the two approaches shows an approximate space usage of about 8GB for ledger states as of 02/2022 timeframe, but it will gradually continue to increase as more accounts/assets are added to network. Current horizon ingest behavior when configured for captive core usage will by default take this space from RAM, unless a new command line flag is specified `--captive-core-use-db=true`, which enables this space to be taken from the external db instead, and not RAM. The external db used is determined be setting `DATABASE` parameter in the captive core cfg/.toml file. If no value is set, then by default it uses sqlite and the db file is stored in `--captive-core-storage-path` - ([4092](https://github.com/stellar/go/pull/4092)) + * Note, if using this feature, we recommend using a storage device with capacity for at least 3000 write ops/second. + +### Fixes + +* Exclude trades with high "rounding slippage" from `/trade_aggregations` endpoint. ([4178](https://github.com/stellar/go/pull/4178)) + * Note, to apply this change retroactively to existing data you will need to reingest starting from protocol 18 (ledger `38115806`). +* Release DB connection in `/paths` when no longer needed. ([4228](https://github.com/stellar/go/pull/4228)) +* Fixed false positive warning during orderbook verification in the horizon log output whenever the in memory orderbook is inconsistent with the postgres liquidity pool and offers table. ([4236](https://github.com/stellar/go/pull/4236)) + +## v2.14.0 + +* Restart Stellar-Core when it's context is cancelled. ([4192](https://github.com/stellar/go/pull/4192)) +* Resume ingestion immediately when catching up. ([4196](https://github.com/stellar/go/pull/4196)) +* Check if there are newer ledger when requested ledger does not exist. ([4198](https://github.com/stellar/go/pull/4198)) +* Properly check against the HA array being empty. ([4152](https://github.com/stellar/go/pull/4152)) + ## v2.13.0 ### DB Schema Migration diff --git a/services/horizon/docker/verify-range/README.md b/services/horizon/docker/verify-range/README.md index fb55a0b9ab..3e9ab2d7e6 100644 --- a/services/horizon/docker/verify-range/README.md +++ b/services/horizon/docker/verify-range/README.md @@ -1,4 +1,4 @@ -# `stellar/expingest-verify-range` +# `stellar/horizon-verify-range` This docker image allows running multiple instances of `horizon ingest verify-command` on a single machine or running it in [AWS Batch](https://aws.amazon.com/batch/). diff --git a/services/horizon/internal/configs/captive-core-pubnet.cfg b/services/horizon/internal/configs/captive-core-pubnet.cfg index 8b7322a667..f8b9a33985 100644 --- a/services/horizon/internal/configs/captive-core-pubnet.cfg +++ b/services/horizon/internal/configs/captive-core-pubnet.cfg @@ -1,7 +1,9 @@ # WARNING! Do not use this config in production. Quorum sets should # be carefully selected manually. NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" +FAILURE_SAFETY=1 HTTP_PORT=11626 +PEER_PORT=11725 [[HOME_DOMAINS]] HOME_DOMAIN="stellar.org" @@ -190,4 +192,4 @@ NAME = "FT_SCV_3" HOME_DOMAIN = "www.franklintempleton.com" PUBLIC_KEY = "GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" ADDRESS = "stellar3.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" \ No newline at end of file +HISTORY = "curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" diff --git a/services/horizon/internal/httpx/middleware.go b/services/horizon/internal/httpx/middleware.go index b2212ce257..cdcd7f4e3c 100644 --- a/services/horizon/internal/httpx/middleware.go +++ b/services/horizon/internal/httpx/middleware.go @@ -4,12 +4,10 @@ import ( "context" "database/sql" "net/http" - "regexp" "strconv" "strings" "time" - "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/prometheus/client_golang/prometheus" @@ -24,6 +22,7 @@ import ( hProblem "github.com/stellar/go/services/horizon/internal/render/problem" "github.com/stellar/go/support/db" supportErrors "github.com/stellar/go/support/errors" + supportHttp "github.com/stellar/go/support/http" "github.com/stellar/go/support/log" "github.com/stellar/go/support/render/problem" ) @@ -130,51 +129,8 @@ func getClientData(r *http.Request, headerName string) string { return value } -var routeRegexp = regexp.MustCompile("{([^:}]*):[^}]*}") - -// https://prometheus.io/docs/instrumenting/exposition_formats/ -// label_value can be any sequence of UTF-8 characters, but the backslash (\), -// double-quote ("), and line feed (\n) characters have to be escaped as \\, -// \", and \n, respectively. -func sanitizeMetricRoute(routePattern string) string { - route := routeRegexp.ReplaceAllString(routePattern, "{$1}") - route = strings.ReplaceAll(route, "\\", "\\\\") - route = strings.ReplaceAll(route, "\"", "\\\"") - route = strings.ReplaceAll(route, "\n", "\\n") - if route == "" { - // Can be empty when request did not reach the final route (ex. blocked by - // a middleware). More info: https://github.com/go-chi/chi/issues/270 - return "undefined" - } - return route -} - -// Author: https://github.com/rliebz -// From: https://github.com/go-chi/chi/issues/270#issuecomment-479184559 -// https://github.com/go-chi/chi/blob/master/LICENSE -func getRoutePattern(r *http.Request) string { - rctx := chi.RouteContext(r.Context()) - if pattern := rctx.RoutePattern(); pattern != "" { - // Pattern is already available - return pattern - } - - routePath := r.URL.Path - if r.URL.RawPath != "" { - routePath = r.URL.RawPath - } - - tctx := chi.NewRouteContext() - if !rctx.Routes.Match(tctx, r.Method, routePath) { - return "" - } - - // tctx has the updated pattern, since Match mutates it - return tctx.RoutePattern() -} - func logEndOfRequest(ctx context.Context, r *http.Request, requestDurationSummary *prometheus.SummaryVec, duration time.Duration, mw middleware.WrapResponseWriter, streaming bool) { - route := sanitizeMetricRoute(getRoutePattern(r)) + route := supportHttp.GetChiRoutePattern(r) referer := r.Referer() if referer == "" { @@ -237,9 +193,8 @@ func NewHistoryMiddleware(ledgerState *ledger.State, staleThreshold int32, sessi return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - chiRoute := chi.RouteContext(ctx) - if chiRoute != nil { - ctx = context.WithValue(ctx, &db.RouteContextKey, sanitizeMetricRoute(chiRoute.RoutePattern())) + if routePattern := supportHttp.GetChiRoutePattern(r); routePattern != "" { + ctx = context.WithValue(ctx, &db.RouteContextKey, routePattern) } if staleThreshold > 0 { ls := ledgerState.CurrentStatus() @@ -309,9 +264,8 @@ func ingestionStatus(ctx context.Context, q *history.Q) (uint32, bool, error) { func (m *StateMiddleware) WrapFunc(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - chiRoute := chi.RouteContext(ctx) - if chiRoute != nil { - ctx = context.WithValue(ctx, &db.RouteContextKey, sanitizeMetricRoute(chiRoute.RoutePattern())) + if routePattern := supportHttp.GetChiRoutePattern(r); routePattern != "" { + ctx = context.WithValue(ctx, &db.RouteContextKey, routePattern) } session := m.HorizonSession.Clone() q := &history.Q{session} diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 2cf067441e..f6c9e23f9f 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -22,6 +22,7 @@ import ( "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" logpkg "github.com/stellar/go/support/log" + "github.com/stellar/go/support/storage" ) const ( @@ -233,11 +234,13 @@ func NewSystem(config Config) (System, error) { archive, err := historyarchive.NewArchivePool( config.HistoryArchiveURLs, - historyarchive.ConnectOptions{ - Context: ctx, + historyarchive.ArchiveOptions{ NetworkPassphrase: config.NetworkPassphrase, CheckpointFrequency: config.CheckpointFrequency, - UserAgent: fmt.Sprintf("horizon/%s golang/%s", apkg.Version(), runtime.Version()), + ConnectOptions: storage.ConnectOptions{ + Context: ctx, + UserAgent: fmt.Sprintf("horizon/%s golang/%s", apkg.Version(), runtime.Version()), + }, }, ) if err != nil { diff --git a/services/horizon/internal/integration/db_test.go b/services/horizon/internal/integration/db_test.go index f54ae04568..f6c32f8cc7 100644 --- a/services/horizon/internal/integration/db_test.go +++ b/services/horizon/internal/integration/db_test.go @@ -494,13 +494,16 @@ func TestReingestDB(t *testing.T) { }) t.Logf("reached ledger is %v", reachedLedger) - // cap reachedLedger to the nearest checkpoint ledger because reingest range cannot ingest past the most - // recent checkpoint ledger when using captive core + // cap reachedLedger to the nearest checkpoint ledger because reingest range + // cannot ingest past the most recent checkpoint ledger when using captive + // core toLedger := uint32(reachedLedger) - archive, err := historyarchive.Connect(horizonConfig.HistoryArchiveURLs[0], historyarchive.ConnectOptions{ - NetworkPassphrase: horizonConfig.NetworkPassphrase, - CheckpointFrequency: horizonConfig.CheckpointFrequency, - }) + archive, err := historyarchive.Connect( + horizonConfig.HistoryArchiveURLs[0], + historyarchive.ArchiveOptions{ + NetworkPassphrase: horizonConfig.NetworkPassphrase, + CheckpointFrequency: horizonConfig.CheckpointFrequency, + }) tt.NoError(err) // make sure a full checkpoint has elapsed otherwise there will be nothing to reingest @@ -641,10 +644,12 @@ func TestFillGaps(t *testing.T) { // cap reachedLedger to the nearest checkpoint ledger because reingest range cannot ingest past the most // recent checkpoint ledger when using captive core toLedger := uint32(reachedLedger) - archive, err := historyarchive.Connect(horizonConfig.HistoryArchiveURLs[0], historyarchive.ConnectOptions{ - NetworkPassphrase: horizonConfig.NetworkPassphrase, - CheckpointFrequency: horizonConfig.CheckpointFrequency, - }) + archive, err := historyarchive.Connect( + horizonConfig.HistoryArchiveURLs[0], + historyarchive.ArchiveOptions{ + NetworkPassphrase: horizonConfig.NetworkPassphrase, + CheckpointFrequency: horizonConfig.CheckpointFrequency, + }) tt.NoError(err) t.Run("validate parallel range", func(t *testing.T) { diff --git a/support/collections/maps/map.go b/support/collections/maps/map.go new file mode 100644 index 0000000000..49417dfbfb --- /dev/null +++ b/support/collections/maps/map.go @@ -0,0 +1,17 @@ +package maps + +func Keys[T comparable, U any](m map[T]U) []T { + keys := make([]T, 0, len(m)) + for key := range m { + keys = append(keys, key) + } + return keys +} + +func Values[T comparable, U any](m map[T]U) []U { + values := make([]U, 0, len(m)) + for _, value := range m { + values = append(values, value) + } + return values +} diff --git a/support/collections/maps/map_test.go b/support/collections/maps/map_test.go new file mode 100644 index 0000000000..89b8d84df8 --- /dev/null +++ b/support/collections/maps/map_test.go @@ -0,0 +1,26 @@ +package maps + +import ( + "testing" + + "github.com/stellar/go/support/collections/set" + "github.com/stretchr/testify/require" +) + +func TestSanity(t *testing.T) { + m := map[int]float32{1: 10, 2: 20, 3: 30} + for k, v := range m { + require.Contains(t, Keys(m), k) + require.Contains(t, Values(m), v) + } + + // compatibility with collections/set.Set + s := set.Set[float32]{} + s.Add(1) + s.Add(2) + s.Add(3) + + for item := range s { + require.Contains(t, Keys(s), item) + } +} diff --git a/support/collections/set/iset.go b/support/collections/set/iset.go new file mode 100644 index 0000000000..f379d322d1 --- /dev/null +++ b/support/collections/set/iset.go @@ -0,0 +1,12 @@ +package set + +type ISet[T comparable] interface { + Add(item T) + AddSlice(items []T) + Remove(item T) + Contains(item T) bool + Slice() []T +} + +var _ ISet[int] = (*Set[int])(nil) // ensure conformity to the interface +var _ ISet[int] = (*safeSet[int])(nil) diff --git a/support/collections/set/safeset.go b/support/collections/set/safeset.go new file mode 100644 index 0000000000..a2fa648682 --- /dev/null +++ b/support/collections/set/safeset.go @@ -0,0 +1,51 @@ +package set + +import ( + "sync" + + "golang.org/x/exp/constraints" +) + +// safeSet is a simple, thread-safe set implementation. Note that it *must* be +// created via NewSafeSet. +type safeSet[T constraints.Ordered] struct { + Set[T] + lock sync.RWMutex +} + +func NewSafeSet[T constraints.Ordered](capacity int) *safeSet[T] { + return &safeSet[T]{ + Set: NewSet[T](capacity), + lock: sync.RWMutex{}, + } +} + +func (s *safeSet[T]) Add(item T) { + s.lock.Lock() + defer s.lock.Unlock() + s.Set.Add(item) +} + +func (s *safeSet[T]) AddSlice(items []T) { + s.lock.Lock() + defer s.lock.Unlock() + s.Set.AddSlice(items) +} + +func (s *safeSet[T]) Remove(item T) { + s.lock.Lock() + defer s.lock.Unlock() + s.Set.Remove(item) +} + +func (s *safeSet[T]) Contains(item T) bool { + s.lock.RLock() + defer s.lock.RUnlock() + return s.Set.Contains(item) +} + +func (s *safeSet[T]) Slice() []T { + s.lock.RLock() + defer s.lock.RUnlock() + return s.Set.Slice() +} diff --git a/support/collections/set/set.go b/support/collections/set/set.go index 0cad14dcd4..7c76a465a6 100644 --- a/support/collections/set/set.go +++ b/support/collections/set/set.go @@ -10,6 +10,12 @@ func (set Set[T]) Add(item T) { set[item] = struct{}{} } +func (set Set[T]) AddSlice(items []T) { + for _, item := range items { + set[item] = struct{}{} + } +} + func (set Set[T]) Remove(item T) { delete(set, item) } @@ -18,3 +24,13 @@ func (set Set[T]) Contains(item T) bool { _, ok := set[item] return ok } + +func (set Set[T]) Slice() []T { + slice := make([]T, 0, len(set)) + for key := range set { + slice = append(slice, key) + } + return slice +} + +var _ ISet[int] = (*Set[int])(nil) // ensure conformity to the interface diff --git a/support/collections/set/set_test.go b/support/collections/set/set_test.go index 798aeea7d0..74c6ecc1a2 100644 --- a/support/collections/set/set_test.go +++ b/support/collections/set/set_test.go @@ -7,7 +7,18 @@ import ( ) func TestSet(t *testing.T) { - s := Set[string]{} + s := NewSet[string](10) + s.Add("sanity") + require.True(t, s.Contains("sanity")) + require.False(t, s.Contains("check")) + + s.AddSlice([]string{"a", "b", "c"}) + require.True(t, s.Contains("b")) + require.ElementsMatch(t, []string{"sanity", "a", "b", "c"}, s.Slice()) +} + +func TestSafeSet(t *testing.T) { + s := NewSafeSet[string](0) s.Add("sanity") require.True(t, s.Contains("sanity")) require.False(t, s.Contains("check")) diff --git a/support/http/logging_middleware.go b/support/http/logging_middleware.go index 0a2f784051..2cc957ac68 100644 --- a/support/http/logging_middleware.go +++ b/support/http/logging_middleware.go @@ -2,6 +2,7 @@ package http import ( stdhttp "net/http" + "regexp" "strings" "time" @@ -57,6 +58,50 @@ func LoggingMiddlewareWithOptions(options Options) func(stdhttp.Handler) stdhttp } } +var routeRegexp = regexp.MustCompile("{([^:}]*):[^}]*}") + +// https://prometheus.io/docs/instrumenting/exposition_formats/ +// label_value can be any sequence of UTF-8 characters, but the backslash (\), +// double-quote ("), and line feed (\n) characters have to be escaped as \\, +// \", and \n, respectively. +func sanitizeMetricRoute(routePattern string) string { + route := routeRegexp.ReplaceAllString(routePattern, "{$1}") + route = strings.ReplaceAll(route, "\\", "\\\\") + route = strings.ReplaceAll(route, "\"", "\\\"") + route = strings.ReplaceAll(route, "\n", "\\n") + if route == "" { + // Can be empty when request did not reach the final route (ex. blocked by + // a middleware). More info: https://github.com/go-chi/chi/issues/270 + return "undefined" + } + return route +} + +// GetChiRoutePattern returns the chi route pattern from the given request context. +// Author: https://github.com/rliebz +// From: https://github.com/go-chi/chi/issues/270#issuecomment-479184559 +// https://github.com/go-chi/chi/blob/master/LICENSE +func GetChiRoutePattern(r *stdhttp.Request) string { + rctx := chi.RouteContext(r.Context()) + if pattern := rctx.RoutePattern(); pattern != "" { + // Pattern is already available + return pattern + } + + routePath := r.URL.Path + if r.URL.RawPath != "" { + routePath = r.URL.RawPath + } + + tctx := chi.NewRouteContext() + if !rctx.Routes.Match(tctx, r.Method, routePath) { + return "" + } + + // tctx has the updated pattern, since Match mutates it + return sanitizeMetricRoute(tctx.RoutePattern()) +} + // logStartOfRequest emits the logline that reports that an http request is // beginning processing. func logStartOfRequest( diff --git a/services/horizon/internal/httpx/middleware_test.go b/support/http/sanitize_route_test.go similarity index 90% rename from services/horizon/internal/httpx/middleware_test.go rename to support/http/sanitize_route_test.go index a71a7aa0a7..5a79b5377b 100644 --- a/services/horizon/internal/httpx/middleware_test.go +++ b/support/http/sanitize_route_test.go @@ -1,12 +1,11 @@ -package httpx +package http import ( - "testing" - "github.com/stretchr/testify/assert" + "testing" ) -func TestMiddlewareSanitizesRoutesForPrometheus(t *testing.T) { +func TestSanitizesRoutesForPrometheus(t *testing.T) { for _, setup := range []struct { name string route string diff --git a/support/ordered/math.go b/support/ordered/math.go index a07f7064c4..1bf2a40031 100644 --- a/support/ordered/math.go +++ b/support/ordered/math.go @@ -19,3 +19,29 @@ func Max[T constraints.Ordered](a, b T) T { } return b } + +// MinSlice returns the smallest element in a slice-like container. +func MinSlice[T constraints.Ordered](slice []T) T { + var smallest T + + for i := 0; i < len(slice); i++ { + if i == 0 || slice[i] < smallest { + smallest = slice[i] + } + } + + return smallest +} + +// MaxSlice returns the largest element in a slice-like container. +func MaxSlice[T constraints.Ordered](slice []T) T { + var largest T + + for i := 0; i < len(slice); i++ { + if i == 0 || slice[i] > largest { + largest = slice[i] + } + } + + return largest +} diff --git a/historyarchive/fs_archive.go b/support/storage/filesystem.go similarity index 76% rename from historyarchive/fs_archive.go rename to support/storage/filesystem.go index 3a241076b8..22928fb7e3 100644 --- a/historyarchive/fs_archive.go +++ b/support/storage/filesystem.go @@ -1,8 +1,4 @@ -// Copyright 2016 Stellar Development Foundation and contributors. Licensed -// under the Apache License, Version 2.0. See the COPYING file at the root -// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 - -package historyarchive +package storage import ( "io" @@ -13,15 +9,21 @@ import ( log "github.com/sirupsen/logrus" ) -type FsArchiveBackend struct { +type Filesystem struct { prefix string } -func (b *FsArchiveBackend) GetFile(pth string) (io.ReadCloser, error) { +func NewFilesystemStorage(pth string) Storage { + return &Filesystem{ + prefix: pth, + } +} + +func (b *Filesystem) GetFile(pth string) (io.ReadCloser, error) { return os.Open(path.Join(b.prefix, pth)) } -func (b *FsArchiveBackend) Exists(pth string) (bool, error) { +func (b *Filesystem) Exists(pth string) (bool, error) { pth = path.Join(b.prefix, pth) log.WithField("path", pth).Trace("fs: check exists") _, err := os.Stat(pth) @@ -38,7 +40,7 @@ func (b *FsArchiveBackend) Exists(pth string) (bool, error) { return true, nil } -func (b *FsArchiveBackend) Size(pth string) (int64, error) { +func (b *Filesystem) Size(pth string) (int64, error) { pth = path.Join(b.prefix, pth) log.WithField("path", pth).Trace("fs: get size") fi, err := os.Stat(pth) @@ -55,7 +57,7 @@ func (b *FsArchiveBackend) Size(pth string) (int64, error) { return fi.Size(), nil } -func (b *FsArchiveBackend) PutFile(pth string, in io.ReadCloser) error { +func (b *Filesystem) PutFile(pth string, in io.ReadCloser) error { dir := path.Join(b.prefix, path.Dir(pth)) log.WithField("path", pth).Trace("fs: put file") exists, err := b.Exists(dir) @@ -86,7 +88,7 @@ func (b *FsArchiveBackend) PutFile(pth string, in io.ReadCloser) error { return e } -func (b *FsArchiveBackend) ListFiles(pth string) (chan string, chan error) { +func (b *Filesystem) ListFiles(pth string) (chan string, chan error) { ch := make(chan string) errs := make(chan error) go func() { @@ -117,12 +119,10 @@ func (b *FsArchiveBackend) ListFiles(pth string) (chan string, chan error) { return ch, errs } -func (b *FsArchiveBackend) CanListFiles() bool { +func (b *Filesystem) CanListFiles() bool { return true } -func makeFsBackend(pth string, opts ConnectOptions) ArchiveBackend { - return &FsArchiveBackend{ - prefix: pth, - } +func (b *Filesystem) Close() error { + return nil } diff --git a/support/storage/gcs.go b/support/storage/gcs.go new file mode 100644 index 0000000000..07a675a1bc --- /dev/null +++ b/support/storage/gcs.go @@ -0,0 +1,137 @@ +package storage + +import ( + "context" + "io" + "os" + "path" + + log "github.com/sirupsen/logrus" + "google.golang.org/api/iterator" + "google.golang.org/api/option" + + "cloud.google.com/go/storage" +) + +type GCSStorage struct { + ctx context.Context + client *storage.Client + bucket *storage.BucketHandle + prefix string +} + +func NewGCSBackend( + ctx context.Context, + bucketName string, + prefix string, + endpoint string, +) (Storage, error) { + log.WithFields(log.Fields{ + "bucket": bucketName, + "prefix": prefix, + "endpoint": endpoint, + }).Debug("gcs: making backend") + + var options []option.ClientOption + if endpoint != "" { + options = append(options, option.WithEndpoint(endpoint)) + } + + client, err := storage.NewClient(ctx, options...) + if err != nil { + return nil, err + } + + // Check the bucket exists + bucket := client.Bucket(bucketName) + if _, err := bucket.Attrs(ctx); err != nil { + return nil, err + } + + backend := GCSStorage{ + ctx: ctx, + client: client, + bucket: bucket, + prefix: prefix, + } + return &backend, nil +} + +func (b *GCSStorage) Exists(pth string) (bool, error) { + log.WithField("path", path.Join(b.prefix, pth)).Trace("gcs: check exists") + _, err := b.Size(pth) + return err == nil, err +} + +func (b *GCSStorage) Size(pth string) (int64, error) { + pth = path.Join(b.prefix, pth) + log.WithField("path", pth).Trace("gcs: get size") + attrs, err := b.bucket.Object(pth).Attrs(context.Background()) + if err == storage.ErrObjectNotExist { + err = os.ErrNotExist + } + if err != nil { + return 0, err + } + return attrs.Size, nil +} + +func (b *GCSStorage) GetFile(pth string) (io.ReadCloser, error) { + pth = path.Join(b.prefix, pth) + log.WithField("path", pth).Trace("gcs: get file") + r, err := b.bucket.Object(pth).NewReader(context.Background()) + if err == storage.ErrObjectNotExist { + // TODO: Check this is right + //lint:ignore SA4006 Ignore unused function temporarily + err = os.ErrNotExist + } + return r, nil +} + +func (b *GCSStorage) PutFile(pth string, in io.ReadCloser) error { + pth = path.Join(b.prefix, pth) + log.WithField("path", pth).Trace("gcs: get file") + w := b.bucket.Object(pth).NewWriter(context.Background()) + if _, err := io.Copy(w, in); err != nil { + return err + } + in.Close() + return w.Close() +} + +func (b *GCSStorage) ListFiles(pth string) (chan string, chan error) { + prefix := path.Join(b.prefix, pth) + ch := make(chan string) + errs := make(chan error) + + go func() { + log.WithField("path", pth).Trace("gcs: list files") + defer close(ch) + defer close(errs) + + iter := b.bucket.Objects(context.Background(), &storage.Query{Prefix: prefix}) + for { + object, err := iter.Next() + if err == iterator.Done { + return + } else if err != nil { + errs <- err + } else { + // TODO: Check Name is right + ch <- object.Name + } + } + }() + + return ch, errs +} + +func (b *GCSStorage) CanListFiles() bool { + log.Trace("gcs: can list files") + return true +} + +func (b *GCSStorage) Close() error { + log.Trace("gcs: close") + return b.client.Close() +} diff --git a/historyarchive/http_archive.go b/support/storage/http.go similarity index 66% rename from historyarchive/http_archive.go rename to support/storage/http.go index ab2b8c2c5e..8f4e7d23f2 100644 --- a/historyarchive/http_archive.go +++ b/support/storage/http.go @@ -1,8 +1,4 @@ -// Copyright 2016 Stellar Development Foundation and contributors. Licensed -// under the Apache License, Version 2.0. See the COPYING file at the root -// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 - -package historyarchive +package storage import ( "context" @@ -15,23 +11,22 @@ import ( "github.com/stellar/go/support/errors" ) -type HttpArchiveBackend struct { +type HttpStorage struct { ctx context.Context client http.Client base url.URL userAgent string } -func checkResp(r *http.Response) error { - if r.StatusCode >= 200 && r.StatusCode < 400 { - return nil - } else { - return fmt.Errorf("Bad HTTP response '%s' for %s '%s'", - r.Status, r.Request.Method, r.Request.URL.String()) +func NewHttpStorage(ctx context.Context, base *url.URL, userAgent string) Storage { + return &HttpStorage{ + ctx: ctx, + base: *base, + userAgent: userAgent, } } -func (b *HttpArchiveBackend) GetFile(pth string) (io.ReadCloser, error) { +func (b *HttpStorage) GetFile(pth string) (io.ReadCloser, error) { derived := b.base derived.Path = path.Join(derived.Path, pth) resp, err := b.makeSendRequest("GET", derived.String()) @@ -51,7 +46,7 @@ func (b *HttpArchiveBackend) GetFile(pth string) (io.ReadCloser, error) { return resp.Body, nil } -func (b *HttpArchiveBackend) Head(pth string) (*http.Response, error) { +func (b *HttpStorage) Head(pth string) (*http.Response, error) { derived := b.base derived.Path = path.Join(derived.Path, pth) resp, err := b.makeSendRequest("HEAD", derived.String()) @@ -66,22 +61,7 @@ func (b *HttpArchiveBackend) Head(pth string) (*http.Response, error) { return resp, nil } -func (b *HttpArchiveBackend) makeSendRequest(method, url string) (*http.Response, error) { - req, err := http.NewRequest(method, url, nil) - if err != nil { - return nil, err - } - req = req.WithContext(b.ctx) - logReq(req) - if b.userAgent != "" { - req.Header.Set("User-Agent", b.userAgent) - } - resp, err := b.client.Do(req) - logResp(resp) - return resp, err -} - -func (b *HttpArchiveBackend) Exists(pth string) (bool, error) { +func (b *HttpStorage) Exists(pth string) (bool, error) { resp, err := b.Head(pth) if err != nil { return false, err @@ -95,7 +75,7 @@ func (b *HttpArchiveBackend) Exists(pth string) (bool, error) { } } -func (b *HttpArchiveBackend) Size(pth string) (int64, error) { +func (b *HttpStorage) Size(pth string) (int64, error) { resp, err := b.Head(pth) if err != nil { return 0, err @@ -109,12 +89,12 @@ func (b *HttpArchiveBackend) Size(pth string) (int64, error) { } } -func (b *HttpArchiveBackend) PutFile(pth string, in io.ReadCloser) error { +func (b *HttpStorage) PutFile(pth string, in io.ReadCloser) error { in.Close() return errors.New("PutFile not available over HTTP") } -func (b *HttpArchiveBackend) ListFiles(pth string) (chan string, chan error) { +func (b *HttpStorage) ListFiles(pth string) (chan string, chan error) { ch := make(chan string) er := make(chan error) close(ch) @@ -123,14 +103,34 @@ func (b *HttpArchiveBackend) ListFiles(pth string) (chan string, chan error) { return ch, er } -func (b *HttpArchiveBackend) CanListFiles() bool { +func (b *HttpStorage) CanListFiles() bool { return false } -func makeHttpBackend(base *url.URL, opts ConnectOptions) ArchiveBackend { - return &HttpArchiveBackend{ - ctx: opts.Context, - userAgent: opts.UserAgent, - base: *base, +func (b *HttpStorage) Close() error { + return nil +} + +func (b *HttpStorage) makeSendRequest(method, url string) (*http.Response, error) { + req, err := http.NewRequest(method, url, nil) + if err != nil { + return nil, err + } + req = req.WithContext(b.ctx) + logReq(req) + if b.userAgent != "" { + req.Header.Set("User-Agent", b.userAgent) + } + resp, err := b.client.Do(req) + logResp(resp) + return resp, err +} + +func checkResp(r *http.Response) error { + if r.StatusCode >= 200 && r.StatusCode < 400 { + return nil + } else { + return fmt.Errorf("bad HTTP response '%s' for %s '%s'", + r.Status, r.Request.Method, r.Request.URL.String()) } } diff --git a/support/storage/main.go b/support/storage/main.go new file mode 100644 index 0000000000..061f3471c9 --- /dev/null +++ b/support/storage/main.go @@ -0,0 +1,118 @@ +package storage + +import ( + "context" + "io" + "net/http" + "net/url" + "path" + "strings" + + log "github.com/sirupsen/logrus" + "github.com/stellar/go/support/errors" +) + +type Storage interface { + Exists(path string) (bool, error) + Size(path string) (int64, error) + GetFile(path string) (io.ReadCloser, error) + PutFile(path string, in io.ReadCloser) error + ListFiles(path string) (chan string, chan error) + CanListFiles() bool + Close() error +} + +type ConnectOptions struct { + Context context.Context + S3Region string + S3Endpoint string + UnsignedRequests bool + GCSEndpoint string + + // When putting file object to s3 bucket, specify the ACL for the object. + S3WriteACL string + + // UserAgent is the value of `User-Agent` header. Applicable only for HTTP + // client. + UserAgent string + + // Wrap the Storage after connection. For example, to add a caching or + // introspection layer. + Wrap func(Storage) (Storage, error) +} + +func ConnectBackend(u string, opts ConnectOptions) (Storage, error) { + if u == "" { + return nil, errors.New("URL is empty") + } + + parsed, err := url.Parse(u) + if err != nil { + return nil, err + } + + if opts.Context == nil { + opts.Context = context.Background() + } + + pth := parsed.Path + var backend Storage + switch parsed.Scheme { + case "s3": + // Inside s3, all paths start _without_ the leading / + pth = strings.TrimPrefix(pth, "/") + backend, err = NewS3Storage( + opts.Context, + parsed.Host, + pth, + opts.S3Region, + opts.S3Endpoint, + opts.UnsignedRequests, + opts.S3WriteACL, + ) + + case "gcs": + // Inside gcs, all paths start _without_ the leading / + pth = strings.TrimPrefix(pth, "/") + backend, err = NewGCSBackend( + opts.Context, + parsed.Host, + pth, + opts.GCSEndpoint, + ) + + case "file": + pth = path.Join(parsed.Host, pth) + backend = NewFilesystemStorage(pth) + + case "http", "https": + backend = NewHttpStorage(opts.Context, parsed, opts.UserAgent) + + default: + err = errors.New("unknown URL scheme: '" + parsed.Scheme + "'") + } + if err == nil && opts.Wrap != nil { + backend, err = opts.Wrap(backend) + } + return backend, err +} + +func logReq(r *http.Request) { + if r == nil { + return + } + logFields := log.Fields{"method": r.Method, "url": r.URL.String()} + log.WithFields(logFields).Trace("http: Req") +} + +func logResp(r *http.Response) { + if r == nil || r.Request == nil { + return + } + logFields := log.Fields{"method": r.Request.Method, "status": r.Status, "url": r.Request.URL.String()} + if r.StatusCode >= 200 && r.StatusCode < 400 { + log.WithFields(logFields).Trace("http: OK") + } else { + log.WithFields(logFields).Warn("http: Bad") + } +} diff --git a/support/storage/ondisk_cache.go b/support/storage/ondisk_cache.go new file mode 100644 index 0000000000..c8997d7e13 --- /dev/null +++ b/support/storage/ondisk_cache.go @@ -0,0 +1,260 @@ +package storage + +import ( + "io" + "os" + "path" + + lru "github.com/hashicorp/golang-lru" + "github.com/stellar/go/support/log" +) + +// OnDiskCache fronts another storage with a local filesystem cache. Its +// thread-safe, meaning you can be actively caching a file and retrieve it at +// the same time without corruption, because retrieval will wait for the fetch. +type OnDiskCache struct { + Storage + dir string + maxFiles int + lru *lru.Cache + + log *log.Entry +} + +// MakeOnDiskCache wraps an Storage with a local filesystem cache in +// `dir`. If dir is blank, a temporary directory will be created. If `maxFiles` +// is zero, a default (90 days of ledgers) is used. +func MakeOnDiskCache(upstream Storage, dir string, maxFiles uint) (Storage, error) { + if dir == "" { + tmp, err := os.MkdirTemp(os.TempDir(), "stellar-horizon-*") + if err != nil { + return nil, err + } + dir = tmp + } + if maxFiles == 0 { + // A guess at a reasonable number of checkpoints. This is 90 days of + // ledgers. (90*86_400)/(5*64) = 24_300 + maxFiles = 24_300 + } + + backendLog := log. + WithField("subservice", "fs-cache"). + WithField("path", dir). + WithField("size", maxFiles) + backendLog.Info("Filesystem cache configured") + + backend := &OnDiskCache{ + Storage: upstream, + dir: dir, + maxFiles: int(maxFiles), + log: backendLog, + } + + cache, err := lru.NewWithEvict(int(maxFiles), backend.onEviction) + if err != nil { + return nil, err + } + + backend.lru = cache + return backend, nil +} + +// GetFile retrieves the file contents from the local cache if present. +// Otherwise, it returns the same result that the wrapped backend returns and +// adds that result into the local cache, if possible. +func (b *OnDiskCache) GetFile(filepath string) (io.ReadCloser, error) { + L := b.log.WithField("key", filepath) + localPath := path.Join(b.dir, filepath) + + // If the lockfile exists, we should defer to the remote source but *not* + // update the cache, as it means there's an in-progress sync of the same + // file. + _, statErr := os.Stat(NameLockfile(localPath)) + if statErr == nil { + L.Debug("incomplete file in cache on disk") + L.Debug("retrieving file from remote backend") + return b.Storage.GetFile(filepath) + } else if _, ok := b.lru.Get(localPath); !ok { + // If it doesn't exist in the cache, it might still exist on the disk if + // we've restarted from an existing directory. + local, err := os.Open(localPath) + if err == nil { + L.Debug("found file on disk but not in cache, adding") + b.lru.Add(localPath, struct{}{}) + return local, nil + } + + L.Debug("retrieving file from remote backend") + + // Since it's not on-disk, pull it from the remote backend, shove it + // into the cache, and write it to disk. + remote, err := b.Storage.GetFile(filepath) + if err != nil { + return remote, err + } + + local, err = b.createLocal(filepath) + if err != nil { + // If there's some local FS error, we can still continue with the + // remote version, so just log it and continue. + L.WithError(err).Error("caching ledger failed") + return remote, nil + } + + return teeReadCloser(remote, local, func() error { + return os.Remove(NameLockfile(localPath)) + }), nil + } + + // The cache claims it exists, so just give it a read and send it. + local, err := os.Open(localPath) + if err != nil { + // Uh-oh, the cache and the disk are not in sync somehow? Let's evict + // this value and try again (recurse) w/ the remote version. + L.WithError(err).Warn("opening cached ledger failed") + b.lru.Remove(localPath) + return b.GetFile(filepath) + } + + L.Debug("Found file in cache") + return local, nil +} + +// Exists shortcuts an existence check by checking if it exists in the cache. +// Otherwise, it returns the same result as the wrapped backend. Note that in +// the latter case, the cache isn't modified. +func (b *OnDiskCache) Exists(filepath string) (bool, error) { + localPath := path.Join(b.dir, filepath) + b.log.WithField("key", filepath).Debug("checking existence") + + if _, ok := b.lru.Get(localPath); ok { + // If the cache says it's there, we can definitively say that this path + // exists, even if we'd fail to `os.Stat()/Read()/etc.` it locally. + return true, nil + } + + return b.Storage.Exists(filepath) +} + +// Size will return the size of the file found in the cache if possible. +// Otherwise, it returns the same result as the wrapped backend. Note that in +// the latter case, the cache isn't modified. +func (b *OnDiskCache) Size(filepath string) (int64, error) { + localPath := path.Join(b.dir, filepath) + L := b.log.WithField("key", filepath) + + L.Debug("retrieving size") + if _, ok := b.lru.Get(localPath); ok { + stats, err := os.Stat(localPath) + if err == nil { + L.Debugf("retrieved cached size: %d", stats.Size()) + return stats.Size(), nil + } + + L.WithError(err).Debug("retrieving size of cached ledger failed") + b.lru.Remove(localPath) // stale cache? + } + + return b.Storage.Size(filepath) +} + +// PutFile writes to the given `filepath` from the given `in` reader, also +// writing it to the local cache if possible. It returns the same result as the +// wrapped backend. +func (b *OnDiskCache) PutFile(filepath string, in io.ReadCloser) error { + L := log.WithField("key", filepath) + L.Debug("putting file") + + // Best effort to tee the upload off to the local cache as well + local, err := b.createLocal(filepath) + if err != nil { + L.WithError(err).Error("failed to put file locally") + } else { + // tee upload data into our local file + in = teeReadCloser(in, local, func() error { + return os.Remove(NameLockfile(path.Join(b.dir, filepath))) + }) + } + + return b.Storage.PutFile(filepath, in) +} + +// Close purges the cache, then forwards the call to the wrapped backend. +func (b *OnDiskCache) Close() error { + // We only purge the cache, leaving the filesystem untouched: + // https://github.com/stellar/go/pull/4457#discussion_r929352643 + b.lru.Purge() + return b.Storage.Close() +} + +// Evict removes a file from the cache and the filesystem, but does not affect +// the upstream backend. It isn't part of the `Storage` interface. +func (b *OnDiskCache) Evict(filepath string) { + log.WithField("key", filepath).Debug("evicting file") + b.lru.Remove(path.Join(b.dir, filepath)) +} + +func (b *OnDiskCache) onEviction(key, value interface{}) { + path := key.(string) + os.Remove(NameLockfile(path)) // just in case + if err := os.Remove(path); err != nil { // best effort removal + b.log.WithError(err). + WithField("key", path). + Warn("removal failed after cache eviction") + } +} + +func (b *OnDiskCache) createLocal(filepath string) (*os.File, error) { + localPath := path.Join(b.dir, filepath) + if err := os.MkdirAll(path.Dir(localPath), 0755 /* drwxr-xr-x */); err != nil { + return nil, err + } + + local, err := os.Create(localPath) /* mode -rw-rw-rw- */ + if err != nil { + return nil, err + } + _, err = os.Create(NameLockfile(localPath)) + if err != nil { + return nil, err + } + + b.lru.Add(localPath, struct{}{}) // just use the cache as an array + return local, nil +} + +func NameLockfile(file string) string { + return file + ".lock" +} + +// The below is a helper interface so that we can use io.TeeReader to write +// data locally immediately as we read it remotely. + +type trc struct { + io.Reader + close func() error +} + +func (t trc) Close() error { + return t.close() +} + +func teeReadCloser(r io.ReadCloser, w io.WriteCloser, onClose func() error) io.ReadCloser { + return trc{ + Reader: io.TeeReader(r, w), + close: func() error { + // Always run all closers, but return the first error + err1 := r.Close() + err2 := w.Close() + err3 := onClose() + + if err1 != nil { + return err1 + } else if err2 != nil { + return err2 + } + return err3 + }, + } +} diff --git a/historyarchive/s3_archive.go b/support/storage/s3.go similarity index 68% rename from historyarchive/s3_archive.go rename to support/storage/s3.go index 3504ca9f23..eae46b1d7b 100644 --- a/historyarchive/s3_archive.go +++ b/support/storage/s3.go @@ -1,55 +1,109 @@ -// Copyright 2016 Stellar Development Foundation and contributors. Licensed -// under the Apache License, Version 2.0. See the COPYING file at the root -// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 - -package historyarchive +package storage import ( "bytes" "context" "io" "net/http" + "os" "path" log "github.com/sirupsen/logrus" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3iface" "github.com/stellar/go/support/errors" ) -type S3ArchiveBackend struct { +type s3HttpProxy interface { + Send(*s3.GetObjectInput) (io.ReadCloser, error) +} + +type defaultS3HttpProxy struct { + *S3Storage +} + +func (proxy *defaultS3HttpProxy) Send(params *s3.GetObjectInput) (io.ReadCloser, error) { + req, resp := proxy.svc.GetObjectRequest(params) + if proxy.unsignedRequests { + req.Handlers.Sign.Clear() // makes this request unsigned + } + req.SetContext(proxy.ctx) + logReq(req.HTTPRequest) + err := req.Send() + logResp(req.HTTPResponse) + + return resp.Body, err +} + +type S3Storage struct { ctx context.Context - svc *s3.S3 + svc s3iface.S3API bucket string prefix string unsignedRequests bool + writeACLrule string + s3Http s3HttpProxy } -func (b *S3ArchiveBackend) GetFile(pth string) (io.ReadCloser, error) { +func NewS3Storage( + ctx context.Context, + bucket string, + prefix string, + region string, + endpoint string, + unsignedRequests bool, + writeACLrule string, +) (Storage, error) { + log.WithFields(log.Fields{"bucket": bucket, + "prefix": prefix, + "region": region, + "endpoint": endpoint}).Debug("s3: making backend") + cfg := &aws.Config{ + Region: aws.String(region), + Endpoint: aws.String(endpoint), + } + cfg = cfg.WithS3ForcePathStyle(true) + + sess, err := session.NewSession(cfg) + if err != nil { + return nil, err + } + + backend := S3Storage{ + ctx: ctx, + svc: s3.New(sess), + bucket: bucket, + prefix: prefix, + unsignedRequests: unsignedRequests, + writeACLrule: writeACLrule, + } + return &backend, nil +} + +func (b *S3Storage) GetFile(pth string) (io.ReadCloser, error) { key := path.Join(b.prefix, pth) params := &s3.GetObjectInput{ Bucket: aws.String(b.bucket), Key: aws.String(key), } - req, resp := b.svc.GetObjectRequest(params) - if b.unsignedRequests { - req.Handlers.Sign.Clear() // makes this request unsigned - } - req.SetContext(b.ctx) - logReq(req.HTTPRequest) - err := req.Send() - logResp(req.HTTPResponse) + resp, err := b.s3HttpProxy().Send(params) + if err != nil { + if aerr, ok := err.(awserr.Error); ok && aerr.Code() == s3.ErrCodeNoSuchKey { + return nil, os.ErrNotExist + } return nil, err } - return resp.Body, nil + return resp, nil } -func (b *S3ArchiveBackend) Head(pth string) (*http.Response, error) { +func (b *S3Storage) Head(pth string) (*http.Response, error) { key := path.Join(b.prefix, pth) params := &s3.HeadObjectInput{ Bucket: aws.String(b.bucket), @@ -82,7 +136,7 @@ func (b *S3ArchiveBackend) Head(pth string) (*http.Response, error) { return req.HTTPResponse, nil } -func (b *S3ArchiveBackend) Exists(pth string) (bool, error) { +func (b *S3Storage) Exists(pth string) (bool, error) { resp, err := b.Head(pth) if err != nil { return false, err @@ -96,7 +150,7 @@ func (b *S3ArchiveBackend) Exists(pth string) (bool, error) { } } -func (b *S3ArchiveBackend) Size(pth string) (int64, error) { +func (b *S3Storage) Size(pth string) (int64, error) { resp, err := b.Head(pth) if err != nil { return 0, err @@ -110,7 +164,14 @@ func (b *S3ArchiveBackend) Size(pth string) (int64, error) { } } -func (b *S3ArchiveBackend) PutFile(pth string, in io.ReadCloser) error { +func (b *S3Storage) GetACLWriteRule() string { + if b.writeACLrule == "" { + return s3.ObjectCannedACLPublicRead + } + return b.writeACLrule +} + +func (b *S3Storage) PutFile(pth string, in io.ReadCloser) error { var buf bytes.Buffer _, err := buf.ReadFrom(in) in.Close() @@ -121,7 +182,7 @@ func (b *S3ArchiveBackend) PutFile(pth string, in io.ReadCloser) error { params := &s3.PutObjectInput{ Bucket: aws.String(b.bucket), Key: aws.String(key), - ACL: aws.String(s3.ObjectCannedACLPublicRead), + ACL: aws.String(b.GetACLWriteRule()), Body: bytes.NewReader(buf.Bytes()), } req, _ := b.svc.PutObjectRequest(params) @@ -137,7 +198,7 @@ func (b *S3ArchiveBackend) PutFile(pth string, in io.ReadCloser) error { return err } -func (b *S3ArchiveBackend) ListFiles(pth string) (chan string, chan error) { +func (b *S3Storage) ListFiles(pth string) (chan string, chan error) { prefix := path.Join(b.prefix, pth) ch := make(chan string) errs := make(chan error) @@ -190,32 +251,19 @@ func (b *S3ArchiveBackend) ListFiles(pth string) (chan string, chan error) { return ch, errs } -func (b *S3ArchiveBackend) CanListFiles() bool { +func (b *S3Storage) CanListFiles() bool { return true } -func makeS3Backend(bucket string, prefix string, opts ConnectOptions) (ArchiveBackend, error) { - log.WithFields(log.Fields{"bucket": bucket, - "prefix": prefix, - "region": opts.S3Region, - "endpoint": opts.S3Endpoint}).Debug("s3: making backend") - cfg := &aws.Config{ - Region: aws.String(opts.S3Region), - Endpoint: aws.String(opts.S3Endpoint), - } - cfg = cfg.WithS3ForcePathStyle(true) +func (b *S3Storage) Close() error { + return nil +} - sess, err := session.NewSession(cfg) - if err != nil { - return nil, err +func (b *S3Storage) s3HttpProxy() s3HttpProxy { + if b.s3Http != nil { + return b.s3Http } - - backend := S3ArchiveBackend{ - ctx: opts.Context, - svc: s3.New(sess), - bucket: bucket, - prefix: prefix, - unsignedRequests: opts.UnsignedRequests, + return &defaultS3HttpProxy{ + S3Storage: b, } - return &backend, nil } diff --git a/support/storage/s3_test.go b/support/storage/s3_test.go new file mode 100644 index 0000000000..e1b5f7c1e0 --- /dev/null +++ b/support/storage/s3_test.go @@ -0,0 +1,114 @@ +// Copyright 2016 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +package storage + +import ( + "context" + "errors" + "io" + "os" + "strings" + "testing" + + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3iface" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +type MockS3 struct { + mock.Mock + s3iface.S3API +} + +type MockS3HttpProxy struct { + mock.Mock + s3HttpProxy +} + +func (m *MockS3HttpProxy) Send(input *s3.GetObjectInput) (io.ReadCloser, error) { + args := m.Called(input) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(io.ReadCloser), args.Error(1) +} + +func TestWriteACLRuleOverride(t *testing.T) { + + mockS3 := &MockS3{} + s3Storage := S3Storage{ + ctx: context.Background(), + svc: mockS3, + bucket: "bucket", + prefix: "prefix", + unsignedRequests: false, + writeACLrule: s3.ObjectCannedACLBucketOwnerFullControl, + } + + aclRule := s3Storage.GetACLWriteRule() + assert.Equal(t, aclRule, s3.ObjectCannedACLBucketOwnerFullControl) +} + +func TestWriteACLRuleDefault(t *testing.T) { + + mockS3 := &MockS3{} + s3Storage := S3Storage{ + ctx: context.Background(), + svc: mockS3, + bucket: "bucket", + prefix: "prefix", + unsignedRequests: false, + writeACLrule: "", + } + + aclRule := s3Storage.GetACLWriteRule() + assert.Equal(t, aclRule, s3.ObjectCannedACLPublicRead) +} + +func TestGetFileNotFound(t *testing.T) { + mockS3 := &MockS3{} + mockS3HttpProxy := &MockS3HttpProxy{} + + mockS3HttpProxy.On("Send", mock.Anything).Return(nil, + awserr.New(s3.ErrCodeNoSuchKey, "message", errors.New("not found"))) + + s3Storage := S3Storage{ + ctx: context.Background(), + svc: mockS3, + bucket: "bucket", + prefix: "prefix", + unsignedRequests: false, + writeACLrule: "", + s3Http: mockS3HttpProxy, + } + + _, err := s3Storage.GetFile("path") + + assert.Equal(t, err, os.ErrNotExist) +} + +func TestGetFileFound(t *testing.T) { + mockS3 := &MockS3{} + mockS3HttpProxy := &MockS3HttpProxy{} + testCloser := io.NopCloser(strings.NewReader("")) + + mockS3HttpProxy.On("Send", mock.Anything).Return(testCloser, nil) + + s3Storage := S3Storage{ + ctx: context.Background(), + svc: mockS3, + bucket: "bucket", + prefix: "prefix", + unsignedRequests: false, + writeACLrule: "", + s3Http: mockS3HttpProxy, + } + + closer, err := s3Storage.GetFile("path") + assert.Nil(t, err) + assert.Equal(t, closer, testCloser) +} diff --git a/toid/main.go b/toid/main.go index 8149803d3b..77616ab688 100644 --- a/toid/main.go +++ b/toid/main.go @@ -124,6 +124,9 @@ func (id *ID) IncOperationOrder() { } // New creates a new total order ID +// +// FIXME: I feel like since ledger sequences are uint32s, TOIDs should +// take that into account for the ledger parameter... func New(ledger int32, tx int32, op int32) *ID { return &ID{ LedgerSequence: ledger, diff --git a/tools/archive-reader/archive_reader.go b/tools/archive-reader/archive_reader.go index c5b03694d1..a07bacb7af 100644 --- a/tools/archive-reader/archive_reader.go +++ b/tools/archive-reader/archive_reader.go @@ -3,12 +3,12 @@ package main import ( "context" "flag" - "fmt" "io" "log" "github.com/stellar/go/historyarchive" "github.com/stellar/go/ingest" + "github.com/stellar/go/support/storage" ) func main() { @@ -63,11 +63,13 @@ func main() { func archive() (*historyarchive.Archive, error) { return historyarchive.Connect( - fmt.Sprintf("s3://history.stellar.org/prd/core-live/core_live_001/"), - historyarchive.ConnectOptions{ - S3Region: "eu-west-1", - UnsignedRequests: true, - UserAgent: "archive-reader", + "s3://history.stellar.org/prd/core-live/core_live_001/", + historyarchive.ArchiveOptions{ + ConnectOptions: storage.ConnectOptions{ + S3Region: "eu-west-1", + UserAgent: "archive-reader", + UnsignedRequests: true, + }, }, ) } diff --git a/tools/stellar-archivist/main.go b/tools/stellar-archivist/main.go index 060490bd18..01bea066a5 100644 --- a/tools/stellar-archivist/main.go +++ b/tools/stellar-archivist/main.go @@ -51,7 +51,7 @@ type Options struct { Debug bool Trace bool CommandOpts historyarchive.CommandOptions - ConnectOpts historyarchive.ConnectOptions + ConnectOpts historyarchive.ArchiveOptions } func (opts *Options) SetRange(srcArch *historyarchive.Archive, dstArch *historyarchive.Archive) { diff --git a/tools/stellar-archivist/main_test.go b/tools/stellar-archivist/main_test.go index 55a200562d..7cb67ccd6c 100644 --- a/tools/stellar-archivist/main_test.go +++ b/tools/stellar-archivist/main_test.go @@ -12,7 +12,7 @@ import ( ) func TestLastOption(t *testing.T) { - src_arch := historyarchive.MustConnect("mock://test", historyarchive.ConnectOptions{CheckpointFrequency: 64}) + src_arch := historyarchive.MustConnect("mock://test", historyarchive.ArchiveOptions{CheckpointFrequency: 64}) assert.NotEqual(t, nil, src_arch) var src_has historyarchive.HistoryArchiveState @@ -28,8 +28,8 @@ func TestLastOption(t *testing.T) { } func TestRecentOption(t *testing.T) { - src_arch := historyarchive.MustConnect("mock://test1", historyarchive.ConnectOptions{CheckpointFrequency: 64}) - dst_arch := historyarchive.MustConnect("mock://test2", historyarchive.ConnectOptions{CheckpointFrequency: 64}) + src_arch := historyarchive.MustConnect("mock://test1", historyarchive.ArchiveOptions{CheckpointFrequency: 64}) + dst_arch := historyarchive.MustConnect("mock://test2", historyarchive.ArchiveOptions{CheckpointFrequency: 64}) assert.NotEqual(t, nil, src_arch) assert.NotEqual(t, nil, dst_arch) diff --git a/xdr/Stellar-lighthorizon.x b/xdr/Stellar-lighthorizon.x new file mode 100644 index 0000000000..8955871cd1 --- /dev/null +++ b/xdr/Stellar-lighthorizon.x @@ -0,0 +1,39 @@ +// Copyright 2022 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +%#include "xdr/Stellar-ledger.h" +%#include "xdr/Stellar-types.h" + +namespace stellar +{ + +struct BitmapIndex { + uint32 firstBit; + uint32 lastBit; + Value bitmap; +}; + +struct TrieIndex { + uint32 version_; // goxdr gives an error if we simply use "version" as an identifier + TrieNode root; +}; + +struct TrieNodeChild { + opaque key[1]; + TrieNode node; +}; + +struct TrieNode { + Value prefix; + Value value; + TrieNodeChild children<>; +}; + +union SerializedLedgerCloseMeta switch (int v) +{ +case 0: + LedgerCloseMeta v0; +}; + +} diff --git a/xdr/xdr_generated.go b/xdr/xdr_generated.go index e8f49981ff..e8ea1dc525 100644 --- a/xdr/xdr_generated.go +++ b/xdr/xdr_generated.go @@ -12,6 +12,7 @@ // xdr/Stellar-internal.x // xdr/Stellar-ledger-entries.x // xdr/Stellar-ledger.x +// xdr/Stellar-lighthorizon.x // xdr/Stellar-overlay.x // xdr/Stellar-transaction.x // xdr/Stellar-types.x @@ -40,6 +41,7 @@ var XdrFilesSHA256 = map[string]string{ "xdr/Stellar-internal.x": "227835866c1b2122d1eaf28839ba85ea7289d1cb681dda4ca619c2da3d71fe00", "xdr/Stellar-ledger-entries.x": "4f8f2324f567a40065f54f696ea1428740f043ea4154f5986d9f499ad00ac333", "xdr/Stellar-ledger.x": "2c842f3fe6e269498af5467f849cf6818554e90babc845f34c87cda471298d0f", + "xdr/Stellar-lighthorizon.x": "1aac09eaeda224154f653a0c95f02167be0c110fc295bb41b756a080eb8c06df", "xdr/Stellar-overlay.x": "de3957c58b96ae07968b3d3aebea84f83603e95322d1fa336360e13e3aba737a", "xdr/Stellar-transaction.x": "0d2b35a331a540b48643925d0869857236eb2487c02d340ea32e365e784ea2b8", "xdr/Stellar-types.x": "6e3b13f0d3e360b09fa5e2b0e55d43f4d974a769df66afb34e8aecbb329d3f15", @@ -56546,4 +56548,480 @@ func (s ConfigSettingEntry) xdrType() {} var _ xdrType = (*ConfigSettingEntry)(nil) +// BitmapIndex is an XDR Struct defines as: +// +// struct BitmapIndex { +// uint32 firstBit; +// uint32 lastBit; +// Value bitmap; +// }; +type BitmapIndex struct { + FirstBit Uint32 + LastBit Uint32 + Bitmap Value +} + +// EncodeTo encodes this value using the Encoder. +func (s *BitmapIndex) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.FirstBit.EncodeTo(e); err != nil { + return err + } + if err = s.LastBit.EncodeTo(e); err != nil { + return err + } + if err = s.Bitmap.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*BitmapIndex)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *BitmapIndex) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding BitmapIndex: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.FirstBit.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.LastBit.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.Bitmap.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Value: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s BitmapIndex) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *BitmapIndex) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*BitmapIndex)(nil) + _ encoding.BinaryUnmarshaler = (*BitmapIndex)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s BitmapIndex) xdrType() {} + +var _ xdrType = (*BitmapIndex)(nil) + +// TrieIndex is an XDR Struct defines as: +// +// struct TrieIndex { +// uint32 version_; // goxdr gives an error if we simply use "version" as an identifier +// TrieNode root; +// }; +type TrieIndex struct { + Version Uint32 + Root TrieNode +} + +// EncodeTo encodes this value using the Encoder. +func (s *TrieIndex) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.Version.EncodeTo(e); err != nil { + return err + } + if err = s.Root.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*TrieIndex)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *TrieIndex) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding TrieIndex: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.Version.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.Root.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding TrieNode: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s TrieIndex) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *TrieIndex) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*TrieIndex)(nil) + _ encoding.BinaryUnmarshaler = (*TrieIndex)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s TrieIndex) xdrType() {} + +var _ xdrType = (*TrieIndex)(nil) + +// TrieNodeChild is an XDR Struct defines as: +// +// struct TrieNodeChild { +// opaque key[1]; +// TrieNode node; +// }; +type TrieNodeChild struct { + Key [1]byte `xdrmaxsize:"1"` + Node TrieNode +} + +// EncodeTo encodes this value using the Encoder. +func (s *TrieNodeChild) EncodeTo(e *xdr.Encoder) error { + var err error + if _, err = e.EncodeFixedOpaque(s.Key[:]); err != nil { + return err + } + if err = s.Node.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*TrieNodeChild)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *TrieNodeChild) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding TrieNodeChild: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = d.DecodeFixedOpaqueInplace(s.Key[:]) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Key: %w", err) + } + nTmp, err = s.Node.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding TrieNode: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s TrieNodeChild) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *TrieNodeChild) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*TrieNodeChild)(nil) + _ encoding.BinaryUnmarshaler = (*TrieNodeChild)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s TrieNodeChild) xdrType() {} + +var _ xdrType = (*TrieNodeChild)(nil) + +// TrieNode is an XDR Struct defines as: +// +// struct TrieNode { +// Value prefix; +// Value value; +// TrieNodeChild children<>; +// }; +type TrieNode struct { + Prefix Value + Value Value + Children []TrieNodeChild +} + +// EncodeTo encodes this value using the Encoder. +func (s *TrieNode) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.Prefix.EncodeTo(e); err != nil { + return err + } + if err = s.Value.EncodeTo(e); err != nil { + return err + } + if _, err = e.EncodeUint(uint32(len(s.Children))); err != nil { + return err + } + for i := 0; i < len(s.Children); i++ { + if err = s.Children[i].EncodeTo(e); err != nil { + return err + } + } + return nil +} + +var _ decoderFrom = (*TrieNode)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *TrieNode) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding TrieNode: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.Prefix.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Value: %w", err) + } + nTmp, err = s.Value.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Value: %w", err) + } + var l uint32 + l, nTmp, err = d.DecodeUint() + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding TrieNodeChild: %w", err) + } + s.Children = nil + if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding TrieNodeChild: length (%d) exceeds remaining input length (%d)", l, il) + } + s.Children = make([]TrieNodeChild, l) + for i := uint32(0); i < l; i++ { + nTmp, err = s.Children[i].DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding TrieNodeChild: %w", err) + } + } + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s TrieNode) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *TrieNode) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*TrieNode)(nil) + _ encoding.BinaryUnmarshaler = (*TrieNode)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s TrieNode) xdrType() {} + +var _ xdrType = (*TrieNode)(nil) + +// SerializedLedgerCloseMeta is an XDR Union defines as: +// +// union SerializedLedgerCloseMeta switch (int v) +// { +// case 0: +// LedgerCloseMeta v0; +// }; +type SerializedLedgerCloseMeta struct { + V int32 + V0 *LedgerCloseMeta +} + +// SwitchFieldName returns the field name in which this union's +// discriminant is stored +func (u SerializedLedgerCloseMeta) SwitchFieldName() string { + return "V" +} + +// ArmForSwitch returns which field name should be used for storing +// the value for an instance of SerializedLedgerCloseMeta +func (u SerializedLedgerCloseMeta) ArmForSwitch(sw int32) (string, bool) { + switch int32(sw) { + case 0: + return "V0", true + } + return "-", false +} + +// NewSerializedLedgerCloseMeta creates a new SerializedLedgerCloseMeta. +func NewSerializedLedgerCloseMeta(v int32, value interface{}) (result SerializedLedgerCloseMeta, err error) { + result.V = v + switch int32(v) { + case 0: + tv, ok := value.(LedgerCloseMeta) + if !ok { + err = errors.New("invalid value, must be LedgerCloseMeta") + return + } + result.V0 = &tv + } + return +} + +// MustV0 retrieves the V0 value from the union, +// panicing if the value is not set. +func (u SerializedLedgerCloseMeta) MustV0() LedgerCloseMeta { + val, ok := u.GetV0() + + if !ok { + panic("arm V0 is not set") + } + + return val +} + +// GetV0 retrieves the V0 value from the union, +// returning ok if the union's switch indicated the value is valid. +func (u SerializedLedgerCloseMeta) GetV0() (result LedgerCloseMeta, ok bool) { + armName, _ := u.ArmForSwitch(int32(u.V)) + + if armName == "V0" { + result = *u.V0 + ok = true + } + + return +} + +// EncodeTo encodes this value using the Encoder. +func (u SerializedLedgerCloseMeta) EncodeTo(e *xdr.Encoder) error { + var err error + if _, err = e.EncodeInt(int32(u.V)); err != nil { + return err + } + switch int32(u.V) { + case 0: + if err = (*u.V0).EncodeTo(e); err != nil { + return err + } + return nil + } + return fmt.Errorf("V (int32) switch value '%d' is not valid for union SerializedLedgerCloseMeta", u.V) +} + +var _ decoderFrom = (*SerializedLedgerCloseMeta)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (u *SerializedLedgerCloseMeta) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding SerializedLedgerCloseMeta: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + u.V, nTmp, err = d.DecodeInt() + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Int: %w", err) + } + switch int32(u.V) { + case 0: + u.V0 = new(LedgerCloseMeta) + nTmp, err = (*u.V0).DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding LedgerCloseMeta: %w", err) + } + return n, nil + } + return n, fmt.Errorf("union SerializedLedgerCloseMeta has invalid V (int32) switch value '%d'", u.V) +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s SerializedLedgerCloseMeta) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *SerializedLedgerCloseMeta) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*SerializedLedgerCloseMeta)(nil) + _ encoding.BinaryUnmarshaler = (*SerializedLedgerCloseMeta)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s SerializedLedgerCloseMeta) xdrType() {} + +var _ xdrType = (*SerializedLedgerCloseMeta)(nil) + var fmtTest = fmt.Sprint("this is a dummy usage of fmt") From ed7ae81c8546c2c8003a96cbc3a074e67d93de3c Mon Sep 17 00:00:00 2001 From: George Date: Thu, 11 Jan 2024 09:31:00 -0800 Subject: [PATCH 026/234] exp/ledgerexporter: Drop unneeded dependency on `historyarchive`. (#4778) * Also, fix up bad merge in the captive core config --- .../build/ledgerexporter/captive-core-pubnet.cfg | 8 -------- exp/services/ledgerexporter/main.go | 3 +-- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg b/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg index 22b149e3f8..c59b411c5d 100644 --- a/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg +++ b/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg @@ -191,16 +191,8 @@ ADDRESS = "stellar2.franklintempleton.com:11625" HISTORY = "curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" [[VALIDATORS]] -<<<<<<<< HEAD:exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg -NAME="wirexSG" -ADDRESS="sg.stellar.wirexapp.com" -HOME_DOMAIN="wirexapp.com" -PUBLIC_KEY="GAB3GZIE6XAYWXGZUDM4GMFFLJBFMLE2JDPUCWUZXMOMT3NHXDHEWXAS" -HISTORY="curl -sf http://wxhorizonasiastga1.blob.core.windows.net/history/{0} -o {1}" -======== NAME = "FT_SCV_3" HOME_DOMAIN = "www.franklintempleton.com" PUBLIC_KEY = "GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" ADDRESS = "stellar3.franklintempleton.com:11625" HISTORY = "curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" ->>>>>>>> master:services/horizon/internal/configs/captive-core-pubnet.cfg diff --git a/exp/services/ledgerexporter/main.go b/exp/services/ledgerexporter/main.go index 03c4f53b32..42cf1d6ae8 100644 --- a/exp/services/ledgerexporter/main.go +++ b/exp/services/ledgerexporter/main.go @@ -11,7 +11,6 @@ import ( "time" "github.com/aws/aws-sdk-go/service/s3" - "github.com/stellar/go/historyarchive" "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/network" supportlog "github.com/stellar/go/support/log" @@ -60,7 +59,7 @@ func main() { core, err := ledgerbackend.NewCaptive(captiveConfig) logFatalIf(err, "Could not create captive core instance") - target, err := historyarchive.ConnectBackend( + target, err := storage.ConnectBackend( *targetUrl, storage.ConnectOptions{ Context: context.Background(), From 61c90a96506b504ff2033761c9106f616603392a Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Thu, 18 Jan 2024 12:50:36 -0500 Subject: [PATCH 027/234] Pass user agent to archive config in captive core backend --- ingest/ledgerbackend/captive_core_backend.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ingest/ledgerbackend/captive_core_backend.go b/ingest/ledgerbackend/captive_core_backend.go index a8acb19182..ce8da8f8cd 100644 --- a/ingest/ledgerbackend/captive_core_backend.go +++ b/ingest/ledgerbackend/captive_core_backend.go @@ -179,7 +179,8 @@ func NewCaptive(config CaptiveCoreConfig) (*CaptiveStellarCore, error) { NetworkPassphrase: config.NetworkPassphrase, CheckpointFrequency: config.CheckpointFrequency, ConnectOptions: storage.ConnectOptions{ - Context: config.Context, + Context: config.Context, + UserAgent: config.UserAgent, }, }, ) From a234e650898c0b7dc56b9101a8cdb845d84734e0 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Thu, 18 Jan 2024 14:09:05 -0500 Subject: [PATCH 028/234] Add useragent test for archive pool --- historyarchive/archive_pool_test.go | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 historyarchive/archive_pool_test.go diff --git a/historyarchive/archive_pool_test.go b/historyarchive/archive_pool_test.go new file mode 100644 index 0000000000..9f51fd75e3 --- /dev/null +++ b/historyarchive/archive_pool_test.go @@ -0,0 +1,39 @@ +// Copyright 2016 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +package historyarchive + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stellar/go/support/storage" + "github.com/stretchr/testify/assert" +) + +func TestConfiguresHttpUserAgentForArchivePool(t *testing.T) { + var userAgent string + var archiveURLs []string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userAgent = r.Header["User-Agent"][0] + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + archiveURLs = append(archiveURLs, server.URL) + + archiveOptions := ArchiveOptions{ + ConnectOptions: storage.ConnectOptions{ + UserAgent: "uatest", + }, + } + + archivePool, err := NewArchivePool(archiveURLs, archiveOptions) + assert.NoError(t, err) + + ok, err := archivePool.BucketExists(EmptyXdrArrayHash()) + assert.True(t, ok) + assert.NoError(t, err) + assert.Equal(t, userAgent, "uatest") +} From 91076c920d93b0a0470bbb009286b5265684d817 Mon Sep 17 00:00:00 2001 From: tamirms Date: Wed, 20 Dec 2023 09:30:12 +0000 Subject: [PATCH 029/234] Remove captive core info request error logs (#5145) --- ingest/ledgerbackend/captive_core_backend.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/ingest/ledgerbackend/captive_core_backend.go b/ingest/ledgerbackend/captive_core_backend.go index ce8da8f8cd..bc29acb54b 100644 --- a/ingest/ledgerbackend/captive_core_backend.go +++ b/ingest/ledgerbackend/captive_core_backend.go @@ -222,7 +222,6 @@ func (c *CaptiveStellarCore) coreSyncedMetric() float64 { info, err := c.stellarCoreClient.Info(c.config.Context) if err != nil { - c.config.Log.WithError(err).Warn("Cannot connect to Captive Stellar-Core HTTP server") return -1 } @@ -240,7 +239,6 @@ func (c *CaptiveStellarCore) coreVersionMetric() float64 { info, err := c.stellarCoreClient.Info(c.config.Context) if err != nil { - c.config.Log.WithError(err).Warn("Cannot connect to Captive Stellar-Core HTTP server") return -1 } From 729f0721a1638148a527bb0d636aaf3133fb369e Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 5 Jan 2024 19:01:41 +0100 Subject: [PATCH 030/234] Fix captive core toml history entries (#5150) --- ingest/ledgerbackend/toml.go | 2 +- ingest/ledgerbackend/toml_test.go | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/ingest/ledgerbackend/toml.go b/ingest/ledgerbackend/toml.go index 55e36e9b9f..e2234fc1f2 100644 --- a/ingest/ledgerbackend/toml.go +++ b/ingest/ledgerbackend/toml.go @@ -559,7 +559,7 @@ func (c *CaptiveCoreToml) setDefaults(params CaptiveCoreTomlParams) { for i, val := range params.HistoryArchiveURLs { name := fmt.Sprintf("HISTORY.h%d", i) c.HistoryEntries[c.tablePlaceholders.newPlaceholder(name)] = History{ - Get: fmt.Sprintf("curl -sf %s/{0} -o {1}", val), + Get: fmt.Sprintf("curl -sf %s/{0} -o {1}", strings.TrimSuffix(val, "/")), } } } diff --git a/ingest/ledgerbackend/toml_test.go b/ingest/ledgerbackend/toml_test.go index 476a2ea953..c5d40c77e3 100644 --- a/ingest/ledgerbackend/toml_test.go +++ b/ingest/ledgerbackend/toml_test.go @@ -395,6 +395,28 @@ func TestGenerateConfig(t *testing.T) { } } +func TestHistoryArchiveURLTrailingSlash(t *testing.T) { + httpPort := uint(8000) + peerPort := uint(8000) + logPath := "logPath" + + params := CaptiveCoreTomlParams{ + NetworkPassphrase: "Public Global Stellar Network ; September 2015", + HistoryArchiveURLs: []string{"http://localhost:1170/"}, + HTTPPort: &httpPort, + PeerPort: &peerPort, + LogPath: &logPath, + Strict: false, + } + + captiveCoreToml, err := NewCaptiveCoreToml(params) + assert.NoError(t, err) + assert.Len(t, captiveCoreToml.HistoryEntries, 1) + for _, entry := range captiveCoreToml.HistoryEntries { + assert.Equal(t, "curl -sf http://localhost:1170/{0} -o {1}", entry.Get) + } +} + func TestExternalStorageConfigUsesDatabaseToml(t *testing.T) { var err error var captiveCoreToml *CaptiveCoreToml From d5d3218259582adaa87847e0ee72966737a61ba1 Mon Sep 17 00:00:00 2001 From: shawn Date: Mon, 8 Jan 2024 13:56:51 -0800 Subject: [PATCH 031/234] #5152: changed the 'Processed ledger' log output from streamLedger to be different phrase to avoid conflict with existing 'Processed ledger' log output from fsm (#5155) --- services/horizon/internal/ingest/processor_runner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/horizon/internal/ingest/processor_runner.go b/services/horizon/internal/ingest/processor_runner.go index ed066a20d2..34b977c03e 100644 --- a/services/horizon/internal/ingest/processor_runner.go +++ b/services/horizon/internal/ingest/processor_runner.go @@ -353,7 +353,7 @@ func (s *ProcessorRunner) streamLedger(ledger xdr.LedgerCloseMeta, "ledger": true, "commit": false, "duration": time.Since(startTime).Seconds(), - }).Info("Processed ledger") + }).Info("Transaction processors finished for ledger") return nil } From 6a221d707287b886427b2d6fc098008f7eb4cab0 Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 11 Jan 2024 13:06:07 -0800 Subject: [PATCH 032/234] services/horizon/ingest: removed legacy core cursor update against during ledger ingestion (#5158) --- services/horizon/CHANGELOG.md | 5 +- services/horizon/cmd/db.go | 2 - services/horizon/cmd/ingest.go | 3 - services/horizon/internal/config.go | 9 -- services/horizon/internal/flags.go | 28 +++--- services/horizon/internal/flags_test.go | 70 ++++++++++++++ .../internal/ingest/build_state_test.go | 32 ------- .../internal/ingest/db_integration_test.go | 1 - services/horizon/internal/ingest/fsm.go | 19 ---- services/horizon/internal/ingest/main.go | 91 ++++--------------- services/horizon/internal/ingest/main_test.go | 1 - services/horizon/internal/ingest/parallel.go | 3 - .../internal/ingest/resume_state_test.go | 68 -------------- services/horizon/internal/init.go | 4 - .../internal/integration/parameters_test.go | 78 ---------------- services/horizon/internal/test/db/main.go | 6 ++ services/horizon/internal/test/main.go | 11 ++- services/horizon/internal/test/t.go | 14 +-- 18 files changed, 124 insertions(+), 321 deletions(-) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 619c1f40e7..c33cf2c65a 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -8,7 +8,10 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ### Added - Add a deprecation warning for using command-line flags when running Horizon ([5051](https://github.com/stellar/go/pull/5051)) -- Deprecate configuration flags related to legacy non-captive core ingestion ([5100](https://github.com/stellar/go/pull/5100)) + +### Breaking Changes +- Removed configuration flags `--stellar-core-url-db`, `--cursor-name` `--skip-cursor-update` , they were related to legacy non-captive core ingestion and are no longer usable. + ## 2.27.0 ### Fixed diff --git a/services/horizon/cmd/db.go b/services/horizon/cmd/db.go index a83597932e..a0d0e6c518 100644 --- a/services/horizon/cmd/db.go +++ b/services/horizon/cmd/db.go @@ -413,10 +413,8 @@ func runDBReingestRange(ledgerRanges []history.LedgerRange, reingestForce bool, ReingestRetryBackoffSeconds: int(retryBackoffSeconds), CaptiveCoreBinaryPath: config.CaptiveCoreBinaryPath, CaptiveCoreConfigUseDB: config.CaptiveCoreConfigUseDB, - RemoteCaptiveCoreURL: config.RemoteCaptiveCoreURL, CaptiveCoreToml: config.CaptiveCoreToml, CaptiveCoreStoragePath: config.CaptiveCoreStoragePath, - StellarCoreCursor: config.CursorName, StellarCoreURL: config.StellarCoreURL, RoundingSlippageFilter: config.RoundingSlippageFilter, EnableIngestionFiltering: config.EnableIngestionFiltering, diff --git a/services/horizon/cmd/ingest.go b/services/horizon/cmd/ingest.go index e2d38977ab..3833dba7fd 100644 --- a/services/horizon/cmd/ingest.go +++ b/services/horizon/cmd/ingest.go @@ -130,7 +130,6 @@ var ingestVerifyRangeCmd = &cobra.Command{ HistoryArchiveURLs: globalConfig.HistoryArchiveURLs, CaptiveCoreBinaryPath: globalConfig.CaptiveCoreBinaryPath, CaptiveCoreConfigUseDB: globalConfig.CaptiveCoreConfigUseDB, - RemoteCaptiveCoreURL: globalConfig.RemoteCaptiveCoreURL, CheckpointFrequency: globalConfig.CheckpointFrequency, CaptiveCoreToml: globalConfig.CaptiveCoreToml, CaptiveCoreStoragePath: globalConfig.CaptiveCoreStoragePath, @@ -213,7 +212,6 @@ var ingestStressTestCmd = &cobra.Command{ HistoryArchiveURLs: globalConfig.HistoryArchiveURLs, RoundingSlippageFilter: globalConfig.RoundingSlippageFilter, CaptiveCoreBinaryPath: globalConfig.CaptiveCoreBinaryPath, - RemoteCaptiveCoreURL: globalConfig.RemoteCaptiveCoreURL, CaptiveCoreConfigUseDB: globalConfig.CaptiveCoreConfigUseDB, } @@ -353,7 +351,6 @@ var ingestBuildStateCmd = &cobra.Command{ HistoryArchiveURLs: globalConfig.HistoryArchiveURLs, CaptiveCoreBinaryPath: globalConfig.CaptiveCoreBinaryPath, CaptiveCoreConfigUseDB: globalConfig.CaptiveCoreConfigUseDB, - RemoteCaptiveCoreURL: globalConfig.RemoteCaptiveCoreURL, CheckpointFrequency: globalConfig.CheckpointFrequency, CaptiveCoreToml: globalConfig.CaptiveCoreToml, CaptiveCoreStoragePath: globalConfig.CaptiveCoreStoragePath, diff --git a/services/horizon/internal/config.go b/services/horizon/internal/config.go index 1cc14b4900..7454f52bb7 100644 --- a/services/horizon/internal/config.go +++ b/services/horizon/internal/config.go @@ -21,7 +21,6 @@ type Config struct { EnableIngestionFiltering bool CaptiveCoreBinaryPath string - RemoteCaptiveCoreURL string CaptiveCoreConfigPath string CaptiveCoreTomlParams ledgerbackend.CaptiveCoreTomlParams CaptiveCoreToml *ledgerbackend.CaptiveCoreToml @@ -68,11 +67,6 @@ type Config struct { TLSKey string // Ingest toggles whether this horizon instance should run the data ingestion subsystem. Ingest bool - // CursorName is the cursor used for ingesting from stellar-core. - // Setting multiple cursors in different Horizon instances allows multiple - // Horizons to ingest from the same stellar-core instance without cursor - // collisions. - CursorName string // HistoryRetentionCount represents the minimum number of ledgers worth of // history data to retain in the horizon database. For the purposes of // determining a "retention duration", each ledger roughly corresponds to 10 @@ -82,9 +76,6 @@ type Config struct { // out-of-date by before horizon begins to respond with an error to history // requests. StaleThreshold uint - // SkipCursorUpdate causes the ingestor to skip reporting the "last imported - // ledger" state to stellar-core. - SkipCursorUpdate bool // IngestDisableStateVerification disables state verification // `System.verifyState()` when set to `true`. IngestDisableStateVerification bool diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index e2783680fd..40bfc08afe 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -338,11 +338,7 @@ func Flags() (*Config, support.ConfigOptions) { Hidden: true, CustomSetValue: func(opt *support.ConfigOption) error { if val := viper.GetString(opt.Name); val != "" { - stdLog.Printf( - "DEPRECATED - The usage of the flag --stellar-core-db-url has been deprecated. " + - "Horizon now uses Captive-Core ingestion by default and this flag will soon be removed in " + - "the future.", - ) + return fmt.Errorf("flag --stellar-core-db-url and environment variable STELLAR_CORE_DATABASE_URL have been removed and no longer valid, must use captive core configuration for ingestion") } return nil }, @@ -595,11 +591,15 @@ func Flags() (*Config, support.ConfigOptions) { &support.ConfigOption{ Name: "cursor-name", EnvVar: "CURSOR_NAME", - ConfigKey: &config.CursorName, OptType: types.String, - FlagDefault: "HORIZON", - Usage: "ingestor cursor used by horizon to ingest from stellar core. must be uppercase and unique for each horizon instance ingesting from that core instance.", + Hidden: true, UsedInCommands: IngestionCommands, + CustomSetValue: func(opt *support.ConfigOption) error { + if val := viper.GetString(opt.Name); val != "" { + return fmt.Errorf("flag --cursor-name has been removed and no longer valid, must use captive core configuration for ingestion") + } + return nil + }, }, &support.ConfigOption{ Name: "history-retention-count", @@ -619,11 +619,15 @@ func Flags() (*Config, support.ConfigOptions) { }, &support.ConfigOption{ Name: "skip-cursor-update", - ConfigKey: &config.SkipCursorUpdate, - OptType: types.Bool, - FlagDefault: false, - Usage: "causes the ingester to skip reporting the last imported ledger state to stellar-core", + OptType: types.String, + Hidden: true, UsedInCommands: IngestionCommands, + CustomSetValue: func(opt *support.ConfigOption) error { + if val := viper.GetString(opt.Name); val != "" { + return fmt.Errorf("flag --skip-cursor-update has been removed and no longer valid, must use captive core configuration for ingestion") + } + return nil + }, }, &support.ConfigOption{ Name: "ingest-disable-state-verification", diff --git a/services/horizon/internal/flags_test.go b/services/horizon/internal/flags_test.go index b2e617bc00..ef2d5d3a02 100644 --- a/services/horizon/internal/flags_test.go +++ b/services/horizon/internal/flags_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/spf13/cobra" + "github.com/stellar/go/services/horizon/internal/test" "github.com/stretchr/testify/assert" @@ -259,3 +260,72 @@ func TestEnvironmentVariables(t *testing.T) { assert.Equal(t, config.CaptiveCoreConfigPath, "../docker/captive-core-classic-integration-tests.cfg") assert.Equal(t, config.CaptiveCoreConfigUseDB, true) } + +func TestRemovedFlags(t *testing.T) { + tests := []struct { + name string + environmentVars map[string]string + errStr string + cmdArgs []string + }{ + { + name: "STELLAR_CORE_DATABASE_URL removed", + environmentVars: map[string]string{ + "INGEST": "false", + "STELLAR_CORE_DATABASE_URL": "coredb", + "DATABASE_URL": "dburl", + }, + errStr: "flag --stellar-core-db-url and environment variable STELLAR_CORE_DATABASE_URL have been removed and no longer valid, must use captive core configuration for ingestion", + }, + { + name: "--stellar-core-db-url removed", + environmentVars: map[string]string{ + "INGEST": "false", + "DATABASE_URL": "dburl", + }, + errStr: "flag --stellar-core-db-url and environment variable STELLAR_CORE_DATABASE_URL have been removed and no longer valid, must use captive core configuration for ingestion", + cmdArgs: []string{"--stellar-core-db-url=coredb"}, + }, + { + name: "CURSOR_NAME removed", + environmentVars: map[string]string{ + "INGEST": "false", + "CURSOR_NAME": "cursor", + "DATABASE_URL": "dburl", + }, + errStr: "flag --cursor-name has been removed and no longer valid, must use captive core configuration for ingestion", + }, + { + name: "SKIP_CURSOR_UPDATE removed", + environmentVars: map[string]string{ + "INGEST": "false", + "SKIP_CURSOR_UPDATE": "true", + "DATABASE_URL": "dburl", + }, + errStr: "flag --skip-cursor-update has been removed and no longer valid, must use captive core configuration for ingestion", + }, + } + + envManager := test.NewEnvironmentManager() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + envManager.Restore() + }() + err := envManager.InitializeEnvironmentVariables(tt.environmentVars) + require.NoError(t, err) + + config, flags := Flags() + testCmd := &cobra.Command{ + Use: "test", + } + + require.NoError(t, flags.Init(testCmd)) + require.NoError(t, testCmd.ParseFlags(tt.cmdArgs)) + + err = ApplyFlags(config, flags, ApplyOptions{}) + require.Error(t, err) + assert.Equal(t, tt.errStr, err.Error()) + }) + } +} diff --git a/services/horizon/internal/ingest/build_state_test.go b/services/horizon/internal/ingest/build_state_test.go index 7e03818795..d1409182d9 100644 --- a/services/horizon/internal/ingest/build_state_test.go +++ b/services/horizon/internal/ingest/build_state_test.go @@ -10,7 +10,6 @@ import ( "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) @@ -83,12 +82,6 @@ func (s *BuildStateTestSuite) mockCommonHistoryQ() { s.historyQ.On("UpdateLastLedgerIngest", s.ctx, s.lastLedger).Return(nil).Once() s.historyQ.On("UpdateExpStateInvalid", s.ctx, false).Return(nil).Once() s.historyQ.On("TruncateIngestStateTables", s.ctx).Return(nil).Once() - s.stellarCoreClient.On( - "SetCursor", - mock.AnythingOfType("*context.timerCtx"), - defaultCoreCursorName, - int32(62), - ).Return(nil).Once() } func (s *BuildStateTestSuite) TestCheckPointLedgerIsZero() { @@ -175,12 +168,6 @@ func (s *BuildStateTestSuite) TestUpdateLastLedgerIngestReturnsError() { s.historyQ.On("GetLastLedgerIngest", s.ctx).Return(s.lastLedger, nil).Once() s.historyQ.On("GetIngestVersion", s.ctx).Return(CurrentVersion, nil).Once() s.historyQ.On("UpdateLastLedgerIngest", s.ctx, s.lastLedger).Return(errors.New("my error")).Once() - s.stellarCoreClient.On( - "SetCursor", - mock.AnythingOfType("*context.timerCtx"), - defaultCoreCursorName, - int32(62), - ).Return(nil).Once() next, err := buildState{checkpointLedger: s.checkpointLedger}.run(s.system) @@ -194,12 +181,6 @@ func (s *BuildStateTestSuite) TestUpdateExpStateInvalidReturnsError() { s.historyQ.On("GetIngestVersion", s.ctx).Return(CurrentVersion, nil).Once() s.historyQ.On("UpdateLastLedgerIngest", s.ctx, s.lastLedger).Return(nil).Once() s.historyQ.On("UpdateExpStateInvalid", s.ctx, false).Return(errors.New("my error")).Once() - s.stellarCoreClient.On( - "SetCursor", - mock.AnythingOfType("*context.timerCtx"), - defaultCoreCursorName, - int32(62), - ).Return(nil).Once() next, err := buildState{checkpointLedger: s.checkpointLedger}.run(s.system) @@ -215,13 +196,6 @@ func (s *BuildStateTestSuite) TestTruncateIngestStateTablesReturnsError() { s.historyQ.On("UpdateExpStateInvalid", s.ctx, false).Return(nil).Once() s.historyQ.On("TruncateIngestStateTables", s.ctx).Return(errors.New("my error")).Once() - s.stellarCoreClient.On( - "SetCursor", - mock.AnythingOfType("*context.timerCtx"), - defaultCoreCursorName, - int32(62), - ).Return(nil).Once() - next, err := buildState{checkpointLedger: s.checkpointLedger}.run(s.system) s.Assert().Error(err) @@ -251,12 +225,6 @@ func (s *BuildStateTestSuite) TestRunHistoryArchiveIngestionGenesisReturnsError( s.historyQ.On("UpdateLastLedgerIngest", s.ctx, uint32(0)).Return(nil).Once() s.historyQ.On("UpdateExpStateInvalid", s.ctx, false).Return(nil).Once() s.historyQ.On("TruncateIngestStateTables", s.ctx).Return(nil).Once() - s.stellarCoreClient.On( - "SetCursor", - mock.AnythingOfType("*context.timerCtx"), - defaultCoreCursorName, - int32(0), - ).Return(nil).Once() s.runner. On("RunGenesisStateIngestion"). diff --git a/services/horizon/internal/ingest/db_integration_test.go b/services/horizon/internal/ingest/db_integration_test.go index 86576db137..60a45f158e 100644 --- a/services/horizon/internal/ingest/db_integration_test.go +++ b/services/horizon/internal/ingest/db_integration_test.go @@ -81,7 +81,6 @@ func (s *DBTestSuite) SetupTest() { s.historyAdapter = &mockHistoryArchiveAdapter{} var err error sIface, err := NewSystem(Config{ - CoreSession: s.tt.CoreSession(), HistorySession: s.tt.HorizonSession(), HistoryArchiveURLs: []string{"http://ignore.test"}, DisableStateVerification: false, diff --git a/services/horizon/internal/ingest/fsm.go b/services/horizon/internal/ingest/fsm.go index f5b4f94456..3cc6d31c7d 100644 --- a/services/horizon/internal/ingest/fsm.go +++ b/services/horizon/internal/ingest/fsm.go @@ -326,11 +326,6 @@ func (b buildState) run(s *system) (transition, error) { return nextFailState, nil } - if err = s.updateCursor(b.checkpointLedger - 1); err != nil { - // Don't return updateCursor error. - log.WithError(err).Warn("error updating stellar-core cursor") - } - log.Info("Starting ingestion system from empty state...") // Clear last_ingested_ledger in key value store @@ -454,14 +449,6 @@ func (r resumeState) run(s *system) (transition, error) { WithField("lastIngestedLedger", lastIngestedLedger). Info("bumping ingest ledger to next ledger after ingested ledger in db") - // Update cursor if there's more than one ingesting instance: either - // Captive-Core or DB ingestion connected to another Stellar-Core. - // remove now? - if err = s.updateCursor(lastIngestedLedger); err != nil { - // Don't return updateCursor error. - log.WithError(err).Warn("error updating stellar-core cursor") - } - // resume immediately so Captive-Core catchup is not slowed down return resumeImmediately(lastIngestedLedger), nil } @@ -522,12 +509,6 @@ func (r resumeState) run(s *system) (transition, error) { return retryResume(r), err } - //TODO remove now? stellar-core-db-url is removed - if err = s.updateCursor(ingestLedger); err != nil { - // Don't return updateCursor error. - log.WithError(err).Warn("error updating stellar-core cursor") - } - duration = time.Since(startTime).Seconds() s.Metrics().LedgerIngestionDuration.Observe(float64(duration)) diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index f6c9e23f9f..e27dc3aeff 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -80,14 +80,11 @@ const ( var log = logpkg.DefaultLogger.WithField("service", "ingest") type Config struct { - CoreSession db.SessionInterface StellarCoreURL string - StellarCoreCursor string CaptiveCoreBinaryPath string CaptiveCoreStoragePath string CaptiveCoreToml *ledgerbackend.CaptiveCoreToml CaptiveCoreConfigUseDB bool - RemoteCaptiveCoreURL string NetworkPassphrase string HistorySession db.SessionInterface @@ -112,19 +109,6 @@ type Config struct { MaxLedgerPerFlush uint32 } -// LocalCaptiveCoreEnabled returns true if configured to run -// a local captive core instance for ingestion. -func (c Config) LocalCaptiveCoreEnabled() bool { - // c.RemoteCaptiveCoreURL is always empty when running local captive core. - return c.RemoteCaptiveCoreURL == "" -} - -// RemoteCaptiveCoreEnabled returns true if configured to run -// a remote captive core instance for ingestion. -func (c Config) RemoteCaptiveCoreEnabled() bool { - return c.RemoteCaptiveCoreURL != "" -} - const ( getLastIngestedErrMsg string = "Error getting last ingested ledger" getIngestVersionErrMsg string = "Error getting ingestion version" @@ -248,41 +232,26 @@ func NewSystem(config Config) (System, error) { return nil, errors.Wrap(err, "error creating history archive") } - var ledgerBackend ledgerbackend.LedgerBackend - if config.RemoteCaptiveCoreEnabled() { - ledgerBackend, err = ledgerbackend.NewRemoteCaptive(config.RemoteCaptiveCoreURL) - if err != nil { - cancel() - return nil, errors.Wrap(err, "error creating captive core backend") - } - } else if config.LocalCaptiveCoreEnabled() { - logger := log.WithField("subservice", "stellar-core") - ledgerBackend, err = ledgerbackend.NewCaptive( - ledgerbackend.CaptiveCoreConfig{ - BinaryPath: config.CaptiveCoreBinaryPath, - StoragePath: config.CaptiveCoreStoragePath, - UseDB: config.CaptiveCoreConfigUseDB, - Toml: config.CaptiveCoreToml, - NetworkPassphrase: config.NetworkPassphrase, - HistoryArchiveURLs: config.HistoryArchiveURLs, - CheckpointFrequency: config.CheckpointFrequency, - LedgerHashStore: ledgerbackend.NewHorizonDBLedgerHashStore(config.HistorySession), - Log: logger, - Context: ctx, - UserAgent: fmt.Sprintf("captivecore horizon/%s golang/%s", apkg.Version(), runtime.Version()), - }, - ) - if err != nil { - cancel() - return nil, errors.Wrap(err, "error creating captive core backend") - } - } else { - coreSession := config.CoreSession.Clone() - ledgerBackend, err = ledgerbackend.NewDatabaseBackendFromSession(coreSession, config.NetworkPassphrase) - if err != nil { - cancel() - return nil, errors.Wrap(err, "error creating ledger backend") - } + // the only ingest option is local captive core config + logger := log.WithField("subservice", "stellar-core") + ledgerBackend, err := ledgerbackend.NewCaptive( + ledgerbackend.CaptiveCoreConfig{ + BinaryPath: config.CaptiveCoreBinaryPath, + StoragePath: config.CaptiveCoreStoragePath, + UseDB: config.CaptiveCoreConfigUseDB, + Toml: config.CaptiveCoreToml, + NetworkPassphrase: config.NetworkPassphrase, + HistoryArchiveURLs: config.HistoryArchiveURLs, + CheckpointFrequency: config.CheckpointFrequency, + LedgerHashStore: ledgerbackend.NewHorizonDBLedgerHashStore(config.HistorySession), + Log: logger, + Context: ctx, + UserAgent: fmt.Sprintf("captivecore horizon/%s golang/%s", apkg.Version(), runtime.Version()), + }, + ) + if err != nil { + cancel() + return nil, errors.Wrap(err, "error creating captive core backend") } historyQ := &history.Q{config.HistorySession.Clone()} @@ -755,26 +724,6 @@ func (s *system) resetStateVerificationErrors() { s.stateVerificationErrors = 0 } -func (s *system) updateCursor(ledgerSequence uint32) error { - if s.stellarCoreClient == nil { - return nil - } - - cursor := defaultCoreCursorName - if s.config.StellarCoreCursor != "" { - cursor = s.config.StellarCoreCursor - } - - ctx, cancel := context.WithTimeout(s.ctx, time.Second) - defer cancel() - err := s.stellarCoreClient.SetCursor(ctx, cursor, int32(ledgerSequence)) - if err != nil { - return errors.Wrap(err, "Setting stellar-core cursor failed") - } - - return nil -} - func (s *system) Shutdown() { log.Info("Shutting down ingestion system...") s.stateVerificationMutex.Lock() diff --git a/services/horizon/internal/ingest/main_test.go b/services/horizon/internal/ingest/main_test.go index 55860eeaff..460c27e062 100644 --- a/services/horizon/internal/ingest/main_test.go +++ b/services/horizon/internal/ingest/main_test.go @@ -90,7 +90,6 @@ func TestLedgerEligibleForStateVerification(t *testing.T) { func TestNewSystem(t *testing.T) { config := Config{ - CoreSession: &db.Session{DB: &sqlx.DB{}}, HistorySession: &db.Session{DB: &sqlx.DB{}}, DisableStateVerification: true, HistoryArchiveURLs: []string{"https://history.stellar.org/prd/core-live/core_live_001"}, diff --git a/services/horizon/internal/ingest/parallel.go b/services/horizon/internal/ingest/parallel.go index b3c163689d..525f153b81 100644 --- a/services/horizon/internal/ingest/parallel.go +++ b/services/horizon/internal/ingest/parallel.go @@ -52,9 +52,6 @@ func (ps *ParallelSystems) Shutdown() { if ps.config.HistorySession != nil { ps.config.HistorySession.Close() } - if ps.config.CoreSession != nil { - ps.config.CoreSession.Close() - } } func (ps *ParallelSystems) runReingestWorker(s System, stop <-chan struct{}, reingestJobQueue <-chan history.LedgerRange) rangeError { diff --git a/services/horizon/internal/ingest/resume_state_test.go b/services/horizon/internal/ingest/resume_state_test.go index 82a7869d4b..013f176ae8 100644 --- a/services/horizon/internal/ingest/resume_state_test.go +++ b/services/horizon/internal/ingest/resume_state_test.go @@ -273,14 +273,6 @@ func (s *ResumeTestTestSuite) mockSuccessfulIngestion() { s.historyQ.On("UpdateLastLedgerIngest", s.ctx, uint32(101)).Return(nil).Once() s.historyQ.On("Commit").Return(nil).Once() s.historyQ.On("RebuildTradeAggregationBuckets", s.ctx, uint32(101), uint32(101), 0).Return(nil).Once() - - s.stellarCoreClient.On( - "SetCursor", - mock.AnythingOfType("*context.timerCtx"), - defaultCoreCursorName, - int32(101), - ).Return(nil).Once() - s.historyQ.On("GetExpStateInvalid", s.ctx).Return(false, nil).Once() } func (s *ResumeTestTestSuite) TestBumpIngestLedger() { @@ -303,13 +295,6 @@ func (s *ResumeTestTestSuite) TestBumpIngestLedger() { s.historyQ.On("Begin", s.ctx).Return(nil).Once() s.historyQ.On("GetLastLedgerIngest", s.ctx).Return(uint32(101), nil).Once() - s.stellarCoreClient.On( - "SetCursor", - mock.AnythingOfType("*context.timerCtx"), - defaultCoreCursorName, - int32(101), - ).Return(errors.New("my error")).Once() - next, err := resumeState{latestSuccessfullyProcessedLedger: 99}.run(s.system) s.Assert().NoError(err) s.Assert().Equal( @@ -335,45 +320,6 @@ func (s *ResumeTestTestSuite) TestIngestAllMasterNode() { ) } -func (s *ResumeTestTestSuite) TestErrorSettingCursorIgnored() { - s.historyQ.On("Begin", s.ctx).Return(nil).Once() - s.historyQ.On("GetLastLedgerIngest", s.ctx).Return(uint32(100), nil).Once() - s.historyQ.On("GetIngestVersion", s.ctx).Return(CurrentVersion, nil).Once() - s.historyQ.On("GetLatestHistoryLedger", s.ctx).Return(uint32(100), nil) - - s.runner.On("RunAllProcessorsOnLedger", mock.AnythingOfType("xdr.LedgerCloseMeta")). - Run(func(args mock.Arguments) { - meta := args.Get(0).(xdr.LedgerCloseMeta) - s.Assert().Equal(uint32(101), meta.LedgerSequence()) - }). - Return( - ledgerStats{}, - nil, - ).Once() - s.historyQ.On("UpdateLastLedgerIngest", s.ctx, uint32(101)).Return(nil).Once() - s.historyQ.On("Commit").Return(nil).Once() - - s.stellarCoreClient.On( - "SetCursor", - mock.AnythingOfType("*context.timerCtx"), - defaultCoreCursorName, - int32(101), - ).Return(errors.New("my error")).Once() - - s.historyQ.On("GetExpStateInvalid", s.ctx).Return(false, nil).Once() - s.historyQ.On("RebuildTradeAggregationBuckets", s.ctx, uint32(101), uint32(101), 0).Return(nil).Once() - - next, err := resumeState{latestSuccessfullyProcessedLedger: 100}.run(s.system) - s.Assert().NoError(err) - s.Assert().Equal( - transition{ - node: resumeState{latestSuccessfullyProcessedLedger: 101}, - sleepDuration: 0, - }, - next, - ) -} - func (s *ResumeTestTestSuite) TestRebuildTradeAggregationBucketsError() { s.historyQ.On("Begin", s.ctx).Return(nil).Once() s.historyQ.On("GetLastLedgerIngest", s.ctx).Return(uint32(100), nil).Once() @@ -422,13 +368,6 @@ func (s *ResumeTestTestSuite) TestReapingObjectsDisabled() { s.historyQ.On("UpdateLastLedgerIngest", s.ctx, uint32(101)).Return(nil).Once() s.historyQ.On("Commit").Return(nil).Once() - s.stellarCoreClient.On( - "SetCursor", - mock.AnythingOfType("*context.timerCtx"), - defaultCoreCursorName, - int32(101), - ).Return(nil).Once() - s.historyQ.On("GetExpStateInvalid", s.ctx).Return(false, nil).Once() s.historyQ.On("RebuildTradeAggregationBuckets", s.ctx, uint32(101), uint32(101), 0).Return(nil).Once() // Reap lookup tables not executed @@ -466,13 +405,6 @@ func (s *ResumeTestTestSuite) TestErrorReapingObjectsIgnored() { s.historyQ.On("UpdateLastLedgerIngest", s.ctx, uint32(101)).Return(nil).Once() s.historyQ.On("Commit").Return(nil).Once() - s.stellarCoreClient.On( - "SetCursor", - mock.AnythingOfType("*context.timerCtx"), - defaultCoreCursorName, - int32(101), - ).Return(nil).Once() - s.historyQ.On("GetExpStateInvalid", s.ctx).Return(false, nil).Once() s.historyQ.On("RebuildTradeAggregationBuckets", s.ctx, uint32(101), uint32(101), 0).Return(nil).Once() // Reap lookup tables: diff --git a/services/horizon/internal/init.go b/services/horizon/internal/init.go index 5d38c86ccf..1b6664b8ba 100644 --- a/services/horizon/internal/init.go +++ b/services/horizon/internal/init.go @@ -91,9 +91,7 @@ func mustInitHorizonDB(app *App) { func initIngester(app *App) { var err error - var coreSession db.SessionInterface app.ingester, err = ingest.NewSystem(ingest.Config{ - CoreSession: coreSession, HistorySession: mustNewDBSession( db.IngestSubservice, app.config.DatabaseURL, ingest.MaxDBConnections, ingest.MaxDBConnections, app.prometheusRegistry, ), @@ -101,12 +99,10 @@ func initIngester(app *App) { HistoryArchiveURLs: app.config.HistoryArchiveURLs, CheckpointFrequency: app.config.CheckpointFrequency, StellarCoreURL: app.config.StellarCoreURL, - StellarCoreCursor: app.config.CursorName, CaptiveCoreBinaryPath: app.config.CaptiveCoreBinaryPath, CaptiveCoreStoragePath: app.config.CaptiveCoreStoragePath, CaptiveCoreConfigUseDB: app.config.CaptiveCoreConfigUseDB, CaptiveCoreToml: app.config.CaptiveCoreToml, - RemoteCaptiveCoreURL: app.config.RemoteCaptiveCoreURL, DisableStateVerification: app.config.IngestDisableStateVerification, StateVerificationCheckpointFrequency: uint32(app.config.IngestStateVerificationCheckpointFrequency), StateVerificationTimeout: app.config.IngestStateVerificationTimeout, diff --git a/services/horizon/internal/integration/parameters_test.go b/services/horizon/internal/integration/parameters_test.go index 97fab268bc..ebe3c3bfda 100644 --- a/services/horizon/internal/integration/parameters_test.go +++ b/services/horizon/internal/integration/parameters_test.go @@ -541,84 +541,6 @@ func TestDeprecatedOutputs(t *testing.T) { "Configuring section in the developer documentation on how to use them - "+ "https://developers.stellar.org/docs/run-api-server/configuring") }) - t.Run("deprecated output for --stellar-core-db-url and --enable-captive-core-ingestion", func(t *testing.T) { - originalStderr := os.Stderr - r, w, _ := os.Pipe() - os.Stderr = w - stdLog.SetOutput(os.Stderr) - - testConfig := integration.GetTestConfig() - testConfig.HorizonIngestParameters = map[string]string{ - "stellar-core-db-url": "temp-url", - "enable-captive-core-ingestion": "true", - } - test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() - assert.NoError(t, err) - test.WaitForHorizon() - - // Use a wait group to wait for the goroutine to finish before proceeding - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - if err := w.Close(); err != nil { - t.Errorf("Failed to close Stdout") - return - } - }() - - outputBytes, _ := io.ReadAll(r) - wg.Wait() // Wait for the goroutine to finish before proceeding - _ = r.Close() - os.Stderr = originalStderr - - assert.Contains(t, string(outputBytes), "DEPRECATED - The usage of the flag --stellar-core-db-url has been deprecated. "+ - "Horizon now uses Captive-Core ingestion by default and this flag will soon be removed in "+ - "the future.") - assert.Contains(t, string(outputBytes), "DEPRECATED - The usage of the flag --enable-captive-core-ingestion has been deprecated. "+ - "Horizon now uses Captive-Core ingestion by default and this flag will soon be removed in "+ - "the future.") - }) - t.Run("deprecated output for env vars STELLAR_CORE_DATABASE_URL and ENABLE_CAPTIVE_CORE_INGESTION", func(t *testing.T) { - originalStderr := os.Stderr - r, w, _ := os.Pipe() - os.Stderr = w - stdLog.SetOutput(os.Stderr) - - testConfig := integration.GetTestConfig() - testConfig.HorizonEnvironment = map[string]string{ - "STELLAR_CORE_DATABASE_URL": "temp-url", - "ENABLE_CAPTIVE_CORE_INGESTION": "true", - } - test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() - assert.NoError(t, err) - test.WaitForHorizon() - - // Use a wait group to wait for the goroutine to finish before proceeding - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - if err := w.Close(); err != nil { - t.Errorf("Failed to close Stdout") - return - } - }() - - outputBytes, _ := io.ReadAll(r) - wg.Wait() // Wait for the goroutine to finish before proceeding - _ = r.Close() - os.Stderr = originalStderr - - assert.Contains(t, string(outputBytes), "DEPRECATED - The usage of the flag --stellar-core-db-url has been deprecated. "+ - "Horizon now uses Captive-Core ingestion by default and this flag will soon be removed in "+ - "the future.") - assert.Contains(t, string(outputBytes), "DEPRECATED - The usage of the flag --enable-captive-core-ingestion has been deprecated. "+ - "Horizon now uses Captive-Core ingestion by default and this flag will soon be removed in "+ - "the future.") - }) } func TestGlobalFlagsOutput(t *testing.T) { diff --git a/services/horizon/internal/test/db/main.go b/services/horizon/internal/test/db/main.go index 4156ec25fb..6114a677ff 100644 --- a/services/horizon/internal/test/db/main.go +++ b/services/horizon/internal/test/db/main.go @@ -29,6 +29,8 @@ func horizonPostgres(t *testing.T) *db.DB { return horizonDB } +// TODO, remove refs to internal core db, need to remove scenario tests which require this +// to seed core db. func corePostgres(t *testing.T) *db.DB { if coreDB != nil { return coreDB @@ -60,6 +62,8 @@ func HorizonROURL() string { return horizonDB.RO_DSN } +// TODO, remove refs to core db, need to remove scenario tests which require this +// to seed core db. func StellarCore(t *testing.T) *sqlx.DB { if coreDBConn != nil { return coreDBConn @@ -68,6 +72,8 @@ func StellarCore(t *testing.T) *sqlx.DB { return coreDBConn } +// TODO, remove refs to core db, need to remove scenario tests which require this +// to seed core db. func StellarCoreURL() string { if coreDB == nil { log.Panic(fmt.Errorf("StellarCore not initialized")) diff --git a/services/horizon/internal/test/main.go b/services/horizon/internal/test/main.go index fea814b4c3..93ed4a94db 100644 --- a/services/horizon/internal/test/main.go +++ b/services/horizon/internal/test/main.go @@ -25,11 +25,12 @@ type StaticMockServer struct { // T provides a common set of functionality for each test in horizon type T struct { - T *testing.T - Assert *assert.Assertions - Require *require.Assertions - Ctx context.Context - HorizonDB *sqlx.DB + T *testing.T + Assert *assert.Assertions + Require *require.Assertions + Ctx context.Context + HorizonDB *sqlx.DB + //TODO - remove ref to core db once scenario tests are removed. CoreDB *sqlx.DB EndLogTest func() []logrus.Entry } diff --git a/services/horizon/internal/test/t.go b/services/horizon/internal/test/t.go index c2a75da986..2f86f70565 100644 --- a/services/horizon/internal/test/t.go +++ b/services/horizon/internal/test/t.go @@ -18,7 +18,7 @@ import ( "github.com/stellar/go/support/render/hal" ) -// CoreSession returns a db.Session instance pointing at the stellar core test database +// TODO - remove ref to core db once scenario tests are removed. func (t *T) CoreSession() *db.Session { return &db.Session{ DB: t.CoreDB, @@ -143,17 +143,7 @@ func (t *T) UnmarshalExtras(r io.Reader) map[string]string { func (t *T) LoadLedgerStatus() ledger.Status { var next ledger.Status - err := t.CoreSession().GetRaw(t.Ctx, &next, ` - SELECT - COALESCE(MAX(ledgerseq), 0) as core_latest - FROM ledgerheaders - `) - - if err != nil { - panic(err) - } - - err = t.HorizonSession().GetRaw(t.Ctx, &next, ` + err := t.HorizonSession().GetRaw(t.Ctx, &next, ` SELECT COALESCE(MIN(sequence), 0) as history_elder, COALESCE(MAX(sequence), 0) as history_latest From 401f6925cfc423ab85cef4e6669082b6bc134a0f Mon Sep 17 00:00:00 2001 From: shawn Date: Fri, 12 Jan 2024 09:04:31 -0800 Subject: [PATCH 033/234] #5156: do not include range prep time in 'Reingestion done' logged duration (#5159) --- .../internal/ingest/fsm_reingest_history_range_state.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/horizon/internal/ingest/fsm_reingest_history_range_state.go b/services/horizon/internal/ingest/fsm_reingest_history_range_state.go index 4e60f71cd1..e2e7724d68 100644 --- a/services/horizon/internal/ingest/fsm_reingest_history_range_state.go +++ b/services/horizon/internal/ingest/fsm_reingest_history_range_state.go @@ -124,13 +124,14 @@ func (h reingestHistoryRangeState) run(s *system) (transition, error) { h.fromLedger = 2 } - startTime := time.Now() + var startTime time.Time if h.force { if t, err := h.prepareRange(s); err != nil { return t, err } + startTime = time.Now() if err := s.historyQ.Begin(s.ctx); err != nil { return stop(), errors.Wrap(err, "Error starting a transaction") } @@ -167,6 +168,7 @@ func (h reingestHistoryRangeState) run(s *system) (transition, error) { return t, err } + startTime = time.Now() if err := s.historyQ.Begin(s.ctx); err != nil { return stop(), errors.Wrap(err, "Error starting a transaction") } From 53ed21d32263a314ccde8a0d099f5bfe4623c775 Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 18 Jan 2024 12:30:14 -0800 Subject: [PATCH 034/234] http archive requests include user agent and metrics (#5166) --- historyarchive/archive.go | 97 ++++++++++++++++++- historyarchive/archive_pool.go | 12 ++- historyarchive/archive_test.go | 25 +++++ historyarchive/mocks.go | 29 ++++++ .../captive_core_backend_test.go | 15 ++- services/horizon/CHANGELOG.md | 4 +- services/horizon/internal/ingest/fsm.go | 28 ++++++ .../ingest/history_archive_adapter.go | 5 + .../ingest/history_archive_adapter_test.go | 5 + services/horizon/internal/ingest/main.go | 12 +++ .../internal/ingest/resume_state_test.go | 19 ++++ 11 files changed, 245 insertions(+), 6 deletions(-) diff --git a/historyarchive/archive.go b/historyarchive/archive.go index 1679d2210f..f07e8518ce 100644 --- a/historyarchive/archive.go +++ b/historyarchive/archive.go @@ -17,6 +17,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" log "github.com/sirupsen/logrus" @@ -48,6 +49,9 @@ type ArchiveOptions struct { CheckpointFrequency uint32 storage.ConnectOptions + + // CacheConfig controls how/if bucket files are cached on the disk. + CacheConfig CacheOptions } type Ledger struct { @@ -56,6 +60,60 @@ type Ledger struct { TransactionResult xdr.TransactionHistoryResultEntry } +// golang will auto wrap them back to 0 if they overflow after addition. +type archiveStats struct { + requests atomic.Uint32 + fileDownloads atomic.Uint32 + fileUploads atomic.Uint32 + backendName string +} + +type ArchiveStats interface { + GetRequests() uint32 + GetDownloads() uint32 + GetUploads() uint32 + GetBackendName() string +} + +func (as *archiveStats) incrementDownloads() { + as.fileDownloads.Add(1) + as.incrementRequests() +} + +func (as *archiveStats) incrementUploads() { + as.fileUploads.Add(1) + as.incrementRequests() +} + +func (as *archiveStats) incrementRequests() { + as.requests.Add(1) +} + +func (as *archiveStats) GetRequests() uint32 { + return as.requests.Load() +} + +func (as *archiveStats) GetDownloads() uint32 { + return as.fileDownloads.Load() +} + +func (as *archiveStats) GetUploads() uint32 { + return as.fileUploads.Load() +} + +func (as *archiveStats) GetBackendName() string { + return as.backendName +} + +type ArchiveBackend interface { + Exists(path string) (bool, error) + Size(path string) (int64, error) + GetFile(path string) (io.ReadCloser, error) + PutFile(path string, in io.ReadCloser) error + ListFiles(path string) (chan string, chan error) + CanListFiles() bool +} + type ArchiveInterface interface { GetPathHAS(path string) (HistoryArchiveState, error) PutPathHAS(path string, has HistoryArchiveState, opts *CommandOptions) error @@ -75,6 +133,7 @@ type ArchiveInterface interface { GetXdrStreamForHash(hash Hash) (*XdrStream, error) GetXdrStream(pth string) (*XdrStream, error) GetCheckpointManager() CheckpointManager + GetStats() []ArchiveStats } var _ ArchiveInterface = &Archive{} @@ -102,7 +161,13 @@ type Archive struct { checkpointManager CheckpointManager - backend storage.Storage + backend ArchiveBackend + cache *ArchiveBucketCache + stats archiveStats +} + +func (arch *Archive) GetStats() []ArchiveStats { + return []ArchiveStats{&arch.stats} } func (arch *Archive) GetCheckpointManager() CheckpointManager { @@ -112,6 +177,7 @@ func (arch *Archive) GetCheckpointManager() CheckpointManager { func (a *Archive) GetPathHAS(path string) (HistoryArchiveState, error) { var has HistoryArchiveState rdr, err := a.backend.GetFile(path) + a.stats.incrementDownloads() if err != nil { return has, err } @@ -138,6 +204,7 @@ func (a *Archive) GetPathHAS(path string) (HistoryArchiveState, error) { func (a *Archive) PutPathHAS(path string, has HistoryArchiveState, opts *CommandOptions) error { exists, err := a.backend.Exists(path) + a.stats.incrementRequests() if err != nil { return err } @@ -149,19 +216,23 @@ func (a *Archive) PutPathHAS(path string, has HistoryArchiveState, opts *Command if err != nil { return err } + a.stats.incrementUploads() return a.backend.PutFile(path, ioutil.NopCloser(bytes.NewReader(buf))) } func (a *Archive) BucketExists(bucket Hash) (bool, error) { + a.stats.incrementRequests() return a.backend.Exists(BucketPath(bucket)) } func (a *Archive) BucketSize(bucket Hash) (int64, error) { + a.stats.incrementRequests() return a.backend.Size(BucketPath(bucket)) } func (a *Archive) CategoryCheckpointExists(cat string, chk uint32) (bool, error) { + a.stats.incrementRequests() return a.backend.Exists(CategoryCheckpointPath(cat, chk)) } @@ -294,14 +365,17 @@ func (a *Archive) PutRootHAS(has HistoryArchiveState, opts *CommandOptions) erro } func (a *Archive) ListBucket(dp DirPrefix) (chan string, chan error) { + a.stats.incrementRequests() return a.backend.ListFiles(path.Join("bucket", dp.Path())) } func (a *Archive) ListAllBuckets() (chan string, chan error) { + a.stats.incrementRequests() return a.backend.ListFiles("bucket") } func (a *Archive) ListAllBucketHashes() (chan Hash, chan error) { + a.stats.incrementRequests() sch, errs := a.backend.ListFiles("bucket") ch := make(chan Hash) rx := regexp.MustCompile("bucket" + hexPrefixPat + "bucket-([0-9a-f]{64})\\.xdr\\.gz$") @@ -323,6 +397,7 @@ func (a *Archive) ListCategoryCheckpoints(cat string, pth string) (chan uint32, rx := regexp.MustCompile(cat + hexPrefixPat + cat + "-([0-9a-f]{8})\\." + regexp.QuoteMeta(ext) + "$") sch, errs := a.backend.ListFiles(path.Join(cat, pth)) + a.stats.incrementRequests() ch := make(chan uint32) errs = makeErrorPump(errs) @@ -360,6 +435,7 @@ func (a *Archive) GetXdrStream(pth string) (*XdrStream, error) { return nil, errors.New("File has non-.xdr.gz suffix: " + pth) } rdr, err := a.backend.GetFile(pth) + a.stats.incrementDownloads() if err != nil { return nil, err } @@ -390,7 +466,22 @@ func Connect(u string, opts ArchiveOptions) (*Archive, error) { var err error arch.backend, err = ConnectBackend(u, opts.ConnectOptions) - return &arch, err + if (err != nil) { + return &arch, err + } + + if opts.CacheConfig.Cache { + cache, innerErr := MakeArchiveBucketCache(opts.CacheConfig) + if innerErr != nil { + return &arch, innerErr + } + + arch.cache = cache + } + + parsed, err := url.Parse(u) + arch.stats = archiveStats{backendName: parsed.String()} + return &arch, nil } func ConnectBackend(u string, opts storage.ConnectOptions) (storage.Storage, error) { @@ -419,4 +510,4 @@ func MustConnect(u string, opts ArchiveOptions) *Archive { log.Fatal(err) } return arch -} +} \ No newline at end of file diff --git a/historyarchive/archive_pool.go b/historyarchive/archive_pool.go index e4e24e0853..e38437caea 100644 --- a/historyarchive/archive_pool.go +++ b/historyarchive/archive_pool.go @@ -51,8 +51,18 @@ func NewArchivePool(archiveURLs []string, opts ArchiveOptions) (ArchivePool, err return validArchives, nil } +func (pa ArchivePool) GetStats() []ArchiveStats { + stats := []ArchiveStats{} + for _, archive := range pa { + if len(archive.GetStats()) == 1 { + stats = append(stats, archive.GetStats()[0]) + } + } + return stats +} + // Ensure the pool conforms to the ArchiveInterface -var _ ArchiveInterface = ArchivePool{} +var _ ArchiveInterface = &ArchivePool{} // Below are the ArchiveInterface method implementations. diff --git a/historyarchive/archive_test.go b/historyarchive/archive_test.go index 4f90802bf7..91306c3285 100644 --- a/historyarchive/archive_test.go +++ b/historyarchive/archive_test.go @@ -13,6 +13,8 @@ import ( "io" "io/ioutil" "math/big" + "net/http" + "net/http/httptest" "os" "strings" "testing" @@ -183,6 +185,25 @@ func TestScan(t *testing.T) { GetRandomPopulatedArchive().Scan(opts) } +func TestConfiguresHttpUserAgent(t *testing.T) { + var userAgent string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userAgent = r.Header["User-Agent"][0] + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + archive, err := Connect(server.URL, ConnectOptions{ + UserAgent: "uatest", + }) + assert.NoError(t, err) + + ok, err := archive.BucketExists(EmptyXdrArrayHash()) + assert.True(t, ok) + assert.NoError(t, err) + assert.Equal(t, userAgent, "uatest") +} + func TestScanSize(t *testing.T) { defer cleanup() opts := testOptions() @@ -530,6 +551,8 @@ func assertXdrEquals(t *testing.T, a, b xdrEntry) { func TestGetLedgers(t *testing.T) { archive := GetTestMockArchive() _, err := archive.GetLedgers(1000, 1002) + assert.Equal(t, uint32(1), archive.GetStats()[0].GetRequests()) + assert.Equal(t, uint32(0), archive.GetStats()[0].GetDownloads()) assert.EqualError(t, err, "checkpoint 1023 is not published") ledgerHeaders := []xdr.LedgerHeaderHistoryEntry{ @@ -617,6 +640,8 @@ func TestGetLedgers(t *testing.T) { ledgers, err := archive.GetLedgers(1000, 1002) assert.NoError(t, err) assert.Len(t, ledgers, 3) + assert.Equal(t, uint32(7), archive.GetStats()[0].GetRequests()) // it started at 1, incurred 6 requests total, 3 queries, 3 downloads + assert.Equal(t, uint32(3), archive.GetStats()[0].GetDownloads()) // started 0, incurred 3 file downloads for i, seq := range []uint32{1000, 1001, 1002} { ledger := ledgers[seq] assertXdrEquals(t, ledgerHeaders[i], ledger.Header) diff --git a/historyarchive/mocks.go b/historyarchive/mocks.go index 3952211cd3..b256d0d7b9 100644 --- a/historyarchive/mocks.go +++ b/historyarchive/mocks.go @@ -103,3 +103,32 @@ func (m *MockArchive) GetXdrStream(pth string) (*XdrStream, error) { a := m.Called(pth) return a.Get(0).(*XdrStream), a.Error(1) } + +func (m *MockArchive) GetStats() []ArchiveStats { + a := m.Called() + return a.Get(0).([]ArchiveStats) +} + +type MockArchiveStats struct { + mock.Mock +} + +func (m *MockArchiveStats) GetRequests() uint32 { + a := m.Called() + return a.Get(0).(uint32) +} + +func (m *MockArchiveStats) GetDownloads() uint32 { + a := m.Called() + return a.Get(0).(uint32) +} + +func (m *MockArchiveStats) GetUploads() uint32 { + a := m.Called() + return a.Get(0).(uint32) +} + +func (m *MockArchiveStats) GetBackendName() string { + a := m.Called() + return a.Get(0).(string) +} diff --git a/ingest/ledgerbackend/captive_core_backend_test.go b/ingest/ledgerbackend/captive_core_backend_test.go index fb2ea4eff4..5178fd97a1 100644 --- a/ingest/ledgerbackend/captive_core_backend_test.go +++ b/ingest/ledgerbackend/captive_core_backend_test.go @@ -4,6 +4,8 @@ import ( "context" "encoding/hex" "fmt" + "net/http" + "net/http/httptest" "os" "sync" "testing" @@ -138,9 +140,16 @@ func TestCaptiveNew(t *testing.T) { require.NoError(t, err) defer os.RemoveAll(storagePath) + var userAgent string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + userAgent = r.Header["User-Agent"][0] + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + executablePath := "/etc/stellar-core" networkPassphrase := network.PublicNetworkPassphrase - historyURLs := []string{"http://history.stellar.org/prd/core-live/core_live_001"} + historyURLs := []string{server.URL} captiveStellarCore, err := NewCaptive( CaptiveCoreConfig{ @@ -148,12 +157,16 @@ func TestCaptiveNew(t *testing.T) { NetworkPassphrase: networkPassphrase, HistoryArchiveURLs: historyURLs, StoragePath: storagePath, + UserAgent: "uatest", }, ) assert.NoError(t, err) assert.Equal(t, uint32(0), captiveStellarCore.nextLedger) assert.NotNil(t, captiveStellarCore.archive) + _, err = captiveStellarCore.archive.BucketExists(historyarchive.EmptyXdrArrayHash()) + assert.NoError(t, err) + assert.Equal(t, "uatest", userAgent) } func TestCaptivePrepareRange(t *testing.T) { diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index c33cf2c65a..104325337c 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -5,8 +5,10 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased -### Added +### Fixed +- http archive requests include user agent and metrics ([5166](https://github.com/stellar/go/pull/5166)) +### Added - Add a deprecation warning for using command-line flags when running Horizon ([5051](https://github.com/stellar/go/pull/5051)) ### Breaking Changes diff --git a/services/horizon/internal/ingest/fsm.go b/services/horizon/internal/ingest/fsm.go index 3cc6d31c7d..d3831fca5c 100644 --- a/services/horizon/internal/ingest/fsm.go +++ b/services/horizon/internal/ingest/fsm.go @@ -8,6 +8,7 @@ import ( "github.com/prometheus/client_golang/prometheus" + "github.com/stellar/go/historyarchive" "github.com/stellar/go/ingest" "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/support/errors" @@ -523,6 +524,13 @@ func (r resumeState) run(s *system) (transition, error) { r.addLedgerStatsMetricFromMap(s, "trades", tradeStatsMap) r.addProcessorDurationsMetricFromMap(s, stats.transactionDurations) + // since a single system instance is shared throughout all states, + // this will sweep up increments to history archive counters + // done elsewhere such as verifyState invocations since the same system + // instance is passed there and the additional usages of archives will just + // roll up and be reported here as part of resumeState transition + addHistoryArchiveStatsMetrics(s, s.historyAdapter.GetStats()) + localLog := log.WithFields(logpkg.F{ "sequence": ingestLedger, "duration": duration, @@ -565,6 +573,26 @@ func (r resumeState) addProcessorDurationsMetricFromMap(s *system, m map[string] } } +func addHistoryArchiveStatsMetrics(s *system, stats []historyarchive.ArchiveStats) { + for _, historyServerStat := range stats { + s.Metrics().HistoryArchiveStatsCounter. + With(prometheus.Labels{ + "source": historyServerStat.GetBackendName(), + "type": "file_downloads"}). + Add(float64(historyServerStat.GetDownloads())) + s.Metrics().HistoryArchiveStatsCounter. + With(prometheus.Labels{ + "source": historyServerStat.GetBackendName(), + "type": "file_uploads"}). + Add(float64(historyServerStat.GetUploads())) + s.Metrics().HistoryArchiveStatsCounter. + With(prometheus.Labels{ + "source": historyServerStat.GetBackendName(), + "type": "requests"}). + Add(float64(historyServerStat.GetRequests())) + } +} + type waitForCheckpointState struct{} func (waitForCheckpointState) String() string { diff --git a/services/horizon/internal/ingest/history_archive_adapter.go b/services/horizon/internal/ingest/history_archive_adapter.go index d4cde9436f..7e415787e3 100644 --- a/services/horizon/internal/ingest/history_archive_adapter.go +++ b/services/horizon/internal/ingest/history_archive_adapter.go @@ -18,6 +18,7 @@ type historyArchiveAdapterInterface interface { GetLatestLedgerSequence() (uint32, error) BucketListHash(sequence uint32) (xdr.Hash, error) GetState(ctx context.Context, sequence uint32) (ingest.ChangeReader, error) + GetStats() []historyarchive.ArchiveStats } // newHistoryArchiveAdapter is a constructor to make a historyArchiveAdapter @@ -71,3 +72,7 @@ func (haa *historyArchiveAdapter) GetState(ctx context.Context, sequence uint32) return sr, nil } + +func (haa *historyArchiveAdapter) GetStats() []historyarchive.ArchiveStats { + return haa.archive.GetStats() +} diff --git a/services/horizon/internal/ingest/history_archive_adapter_test.go b/services/horizon/internal/ingest/history_archive_adapter_test.go index 7c9207cbe4..20d84149fa 100644 --- a/services/horizon/internal/ingest/history_archive_adapter_test.go +++ b/services/horizon/internal/ingest/history_archive_adapter_test.go @@ -33,6 +33,11 @@ func (m *mockHistoryArchiveAdapter) GetState(ctx context.Context, sequence uint3 return args.Get(0).(ingest.ChangeReader), args.Error(1) } +func (m *mockHistoryArchiveAdapter) GetStats() []historyarchive.ArchiveStats { + a := m.Called() + return a.Get(0).([]historyarchive.ArchiveStats) +} + func TestGetState_Read(t *testing.T) { archive, e := getTestArchive() if !assert.NoError(t, e) { diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index e27dc3aeff..8005593216 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -162,6 +162,9 @@ type Metrics struct { // ProcessorsRunDurationSummary exposes processors run durations. ProcessorsRunDurationSummary *prometheus.SummaryVec + + // ArchiveRequestCounter counts how many http requests are sent to history server + HistoryArchiveStatsCounter *prometheus.CounterVec } type System interface { @@ -393,6 +396,14 @@ func (s *system) initMetrics() { }, []string{"name"}, ) + + s.metrics.HistoryArchiveStatsCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "horizon", Subsystem: "ingest", Name: "history_archive_stats_total", + Help: "counters of different history archive stats", + }, + []string{"source", "type"}, + ) } func (s *system) GetCurrentState() State { @@ -418,6 +429,7 @@ func (s *system) RegisterMetrics(registry *prometheus.Registry) { registry.MustRegister(s.metrics.ProcessorsRunDuration) registry.MustRegister(s.metrics.ProcessorsRunDurationSummary) registry.MustRegister(s.metrics.StateVerifyLedgerEntriesCount) + registry.MustRegister(s.metrics.HistoryArchiveStatsCounter) s.ledgerBackend = ledgerbackend.WithMetrics(s.ledgerBackend, registry, "horizon") } diff --git a/services/horizon/internal/ingest/resume_state_test.go b/services/horizon/internal/ingest/resume_state_test.go index 013f176ae8..d989e7a9e5 100644 --- a/services/horizon/internal/ingest/resume_state_test.go +++ b/services/horizon/internal/ingest/resume_state_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" + "github.com/stellar/go/historyarchive" "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" @@ -260,6 +261,12 @@ func (s *ResumeTestTestSuite) mockSuccessfulIngestion() { s.historyQ.On("GetLastLedgerIngest", s.ctx).Return(uint32(100), nil).Once() s.historyQ.On("GetIngestVersion", s.ctx).Return(CurrentVersion, nil).Once() s.historyQ.On("GetLatestHistoryLedger", s.ctx).Return(uint32(100), nil) + mockStats := &historyarchive.MockArchiveStats{} + mockStats.On("GetBackendName").Return("name") + mockStats.On("GetDownloads").Return(uint32(0)) + mockStats.On("GetRequests").Return(uint32(0)) + mockStats.On("GetUploads").Return(uint32(0)) + s.historyAdapter.On("GetStats").Return([]historyarchive.ArchiveStats{mockStats}).Once() s.runner.On("RunAllProcessorsOnLedger", mock.AnythingOfType("xdr.LedgerCloseMeta")). Run(func(args mock.Arguments) { @@ -370,6 +377,12 @@ func (s *ResumeTestTestSuite) TestReapingObjectsDisabled() { s.historyQ.On("GetExpStateInvalid", s.ctx).Return(false, nil).Once() s.historyQ.On("RebuildTradeAggregationBuckets", s.ctx, uint32(101), uint32(101), 0).Return(nil).Once() + mockStats := &historyarchive.MockArchiveStats{} + mockStats.On("GetBackendName").Return("name") + mockStats.On("GetDownloads").Return(uint32(0)) + mockStats.On("GetRequests").Return(uint32(0)) + mockStats.On("GetUploads").Return(uint32(0)) + s.historyAdapter.On("GetStats").Return([]historyarchive.ArchiveStats{mockStats}).Once() // Reap lookup tables not executed next, err := resumeState{latestSuccessfullyProcessedLedger: 100}.run(s.system) @@ -413,6 +426,12 @@ func (s *ResumeTestTestSuite) TestErrorReapingObjectsIgnored() { s.historyQ.On("GetLastLedgerIngest", s.ctx).Return(uint32(100), nil).Once() s.historyQ.On("ReapLookupTables", mock.AnythingOfType("*context.timerCtx"), mock.Anything).Return(nil, nil, errors.New("error reaping objects")).Once() s.historyQ.On("Rollback").Return(nil).Once() + mockStats := &historyarchive.MockArchiveStats{} + mockStats.On("GetBackendName").Return("name") + mockStats.On("GetDownloads").Return(uint32(0)) + mockStats.On("GetRequests").Return(uint32(0)) + mockStats.On("GetUploads").Return(uint32(0)) + s.historyAdapter.On("GetStats").Return([]historyarchive.ArchiveStats{mockStats}).Once() next, err := resumeState{latestSuccessfullyProcessedLedger: 100}.run(s.system) s.Assert().NoError(err) From 8178b14f0384383e809f0b3edb7ebded8080c4d3 Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 18 Jan 2024 13:23:38 -0800 Subject: [PATCH 035/234] Fix tradeagg rebuild from reingest command with parallel workers (#5168) --- services/horizon/CHANGELOG.md | 4 +- services/horizon/cmd/db.go | 2 +- services/horizon/internal/ingest/fsm.go | 4 +- .../fsm_reingest_history_range_state.go | 5 --- .../ingest/ingest_history_range_state_test.go | 40 +++++++++---------- services/horizon/internal/ingest/main.go | 15 ++++++- services/horizon/internal/ingest/main_test.go | 9 ++++- services/horizon/internal/ingest/parallel.go | 36 +++++++++++++++-- .../horizon/internal/ingest/parallel_test.go | 21 ++++++---- 9 files changed, 91 insertions(+), 45 deletions(-) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 104325337c..0d39a7aca3 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -6,9 +6,11 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased ### Fixed -- http archive requests include user agent and metrics ([5166](https://github.com/stellar/go/pull/5166)) +- Trade agg rebuild errors reported on `db reingest range` with parellel workers ([5168](https://github.com/stellar/go/pull/5168)) +- http archive requests include user agent ([5166](https://github.com/stellar/go/pull/5166)) ### Added +- http archive requests include metrics ([5166](https://github.com/stellar/go/pull/5166)) - Add a deprecation warning for using command-line flags when running Horizon ([5051](https://github.com/stellar/go/pull/5051)) ### Breaking Changes diff --git a/services/horizon/cmd/db.go b/services/horizon/cmd/db.go index a0d0e6c518..725df622b0 100644 --- a/services/horizon/cmd/db.go +++ b/services/horizon/cmd/db.go @@ -443,7 +443,7 @@ func runDBReingestRange(ledgerRanges []history.LedgerRange, reingestForce bool, } defer system.Shutdown() - err = system.ReingestRange(ledgerRanges, reingestForce) + err = system.ReingestRange(ledgerRanges, reingestForce, true) if err != nil { if _, ok := errors.Cause(err).(ingest.ErrReingestRangeConflict); ok { return fmt.Errorf(`The range you have provided overlaps with Horizon's most recently ingested ledger. diff --git a/services/horizon/internal/ingest/fsm.go b/services/horizon/internal/ingest/fsm.go index d3831fca5c..59a1a7c969 100644 --- a/services/horizon/internal/ingest/fsm.go +++ b/services/horizon/internal/ingest/fsm.go @@ -499,7 +499,7 @@ func (r resumeState) run(s *system) (transition, error) { } rebuildStart := time.Now() - err = s.historyQ.RebuildTradeAggregationBuckets(s.ctx, ingestLedger, ingestLedger, s.config.RoundingSlippageFilter) + err = s.RebuildTradeAggregationBuckets(ingestLedger, ingestLedger) if err != nil { return retryResume(r), errors.Wrap(err, "error rebuilding trade aggregations") } @@ -741,7 +741,7 @@ func (v verifyRangeState) run(s *system) (transition, error) { Info("Processed ledger") } - err = s.historyQ.RebuildTradeAggregationBuckets(s.ctx, v.fromLedger, v.toLedger, s.config.RoundingSlippageFilter) + err = s.RebuildTradeAggregationBuckets(v.fromLedger, v.toLedger) if err != nil { return stop(), errors.Wrap(err, "error rebuilding trade aggregations") } diff --git a/services/horizon/internal/ingest/fsm_reingest_history_range_state.go b/services/horizon/internal/ingest/fsm_reingest_history_range_state.go index e2e7724d68..832898d021 100644 --- a/services/horizon/internal/ingest/fsm_reingest_history_range_state.go +++ b/services/horizon/internal/ingest/fsm_reingest_history_range_state.go @@ -183,11 +183,6 @@ func (h reingestHistoryRangeState) run(s *system) (transition, error) { } } - err := s.historyQ.RebuildTradeAggregationBuckets(s.ctx, h.fromLedger, h.toLedger, s.config.RoundingSlippageFilter) - if err != nil { - return stop(), errors.Wrap(err, "Error rebuilding trade aggregations") - } - log.WithFields(logpkg.F{ "from": h.fromLedger, "to": h.toLedger, diff --git a/services/horizon/internal/ingest/ingest_history_range_state_test.go b/services/horizon/internal/ingest/ingest_history_range_state_test.go index 4598008eb8..4f7d2c4944 100644 --- a/services/horizon/internal/ingest/ingest_history_range_state_test.go +++ b/services/horizon/internal/ingest/ingest_history_range_state_test.go @@ -304,16 +304,16 @@ func (s *ReingestHistoryRangeStateTestSuite) TearDownTest() { func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateInvalidRange() { // Recreate mock in this single test to remove Rollback assertion. s.historyQ = &mockDBQ{} - err := s.system.ReingestRange([]history.LedgerRange{{0, 0}}, false) + err := s.system.ReingestRange([]history.LedgerRange{{0, 0}}, false, true) s.Assert().EqualError(err, "Invalid range: {0 0} genesis ledger starts at 1") - err = s.system.ReingestRange([]history.LedgerRange{{0, 100}}, false) + err = s.system.ReingestRange([]history.LedgerRange{{0, 100}}, false, true) s.Assert().EqualError(err, "Invalid range: {0 100} genesis ledger starts at 1") - err = s.system.ReingestRange([]history.LedgerRange{{100, 0}}, false) + err = s.system.ReingestRange([]history.LedgerRange{{100, 0}}, false, true) s.Assert().EqualError(err, "Invalid range: {100 0} from > to") - err = s.system.ReingestRange([]history.LedgerRange{{100, 99}}, false) + err = s.system.ReingestRange([]history.LedgerRange{{100, 99}}, false, true) s.Assert().EqualError(err, "Invalid range: {100 99} from > to") } @@ -323,7 +323,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateInvali s.historyQ.On("Rollback").Return(nil).Once() s.historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() s.system.maxLedgerPerFlush = 0 - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) s.Assert().EqualError(err, "invalid maxLedgerPerFlush, must be greater than 0") } @@ -332,28 +332,28 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateBeginR s.historyQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(0), nil).Once() s.historyQ.On("Begin", s.ctx).Return(errors.New("my error")).Once() - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) s.Assert().EqualError(err, "Error starting a transaction: my error") } func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateGetLastLedgerIngestNonBlockingError() { s.historyQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(0), errors.New("my error")).Once() - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) s.Assert().EqualError(err, "Error getting last ingested ledger: my error") } func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateRangeOverlaps() { s.historyQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(190), nil).Once() - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) s.Assert().Equal(ErrReingestRangeConflict{190}, err) } func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStatRangeOverlapsAtEnd() { s.historyQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(200), nil).Once() - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) s.Assert().Equal(ErrReingestRangeConflict{200}, err) } @@ -369,7 +369,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateClearH "DeleteRangeAll", s.ctx, toidFrom.ToInt64(), toidTo.ToInt64(), ).Return(errors.New("my error")).Once() - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) s.Assert().EqualError(err, "error in DeleteRangeAll: my error") } @@ -397,7 +397,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateRunTra s.ledgerBackend.On("GetLedger", s.ctx, uint32(100)).Return(meta, nil).Once() s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}).Return(errors.New("my error")).Once() - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) s.Assert().EqualError(err, "error processing ledger range 100 - 100: my error") } @@ -428,7 +428,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateCommit s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}).Return(nil).Once() } - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) s.Assert().EqualError(err, "Error committing db transaction: my error") } @@ -460,7 +460,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateSucces } // system.maxLedgerPerFlush has been set by default to 1 in test suite setup - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) s.Assert().NoError(err) } @@ -500,7 +500,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateSucces s.runner.On("RunTransactionProcessorsOnLedgers", firstLedgersBatch).Return(nil).Once() s.runner.On("RunTransactionProcessorsOnLedgers", secondLedgersBatch).Return(nil).Once() s.system.maxLedgerPerFlush = 60 - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) s.Assert().NoError(err) } @@ -534,7 +534,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateSucces s.ledgerBackend.On("GetLedger", s.ctx, uint32(100)).Return(meta, nil).Once() s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}).Return(nil).Once() - err := s.system.ReingestRange([]history.LedgerRange{{100, 100}}, false) + err := s.system.ReingestRange([]history.LedgerRange{{100, 100}}, false, true) s.Assert().NoError(err) } @@ -543,7 +543,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceG s.historyQ.On("Rollback").Return(nil).Once() s.historyQ.On("GetLastLedgerIngest", s.ctx).Return(uint32(0), errors.New("my error")).Once() - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, true) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, true, true) s.Assert().EqualError(err, "Error getting last ingested ledger: my error") } @@ -576,7 +576,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForce( } // system.maxLedgerPerFlush has been set by default to 1 in test suite setup - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, true) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, true, true) s.Assert().NoError(err) } @@ -610,7 +610,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceL s.ledgerBackend.On("GetLedger", s.ctx, uint32(106)).Return(xdr.LedgerCloseMeta{}, errors.New("my error")).Once() // system.maxLedgerPerFlush has been set by default to 1 in test suite setup - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, true) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, true, true) s.Assert().EqualError(err, "error getting ledger: my error") } @@ -644,7 +644,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceL s.ledgerBackend.On("GetLedger", s.ctx, uint32(106)).Return(xdr.LedgerCloseMeta{}, errors.New("my error")).Once() // system.maxLedgerPerFlush has been set by default to 1 in test suite setup - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, true) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, true, true) s.Assert().EqualError(err, "Error committing db transaction: error getting ledger: my error") } @@ -686,6 +686,6 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceW s.runner.On("RunTransactionProcessorsOnLedgers", secondLedgersBatch).Return(nil).Once() s.system.maxLedgerPerFlush = 60 - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, true) + err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, true, true) s.Assert().NoError(err) } diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 8005593216..e19242cd2b 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -174,10 +174,11 @@ type System interface { StressTest(numTransactions, changesPerTransaction int) error VerifyRange(fromLedger, toLedger uint32, verifyState bool) error BuildState(sequence uint32, skipChecks bool) error - ReingestRange(ledgerRanges []history.LedgerRange, force bool) error + ReingestRange(ledgerRanges []history.LedgerRange, force bool, rebuildTradeAgg bool) error BuildGenesisState() error Shutdown() GetCurrentState() State + RebuildTradeAggregationBuckets(fromLedger, toLedger uint32) error } type system struct { @@ -524,7 +525,7 @@ func validateRanges(ledgerRanges []history.LedgerRange) error { // ReingestRange runs the ingestion pipeline on the range of ledgers ingesting // history data only. -func (s *system) ReingestRange(ledgerRanges []history.LedgerRange, force bool) error { +func (s *system) ReingestRange(ledgerRanges []history.LedgerRange, force bool, rebuildTradeAgg bool) error { if err := validateRanges(ledgerRanges); err != nil { return err } @@ -545,10 +546,20 @@ func (s *system) ReingestRange(ledgerRanges []history.LedgerRange, force bool) e if err != nil { return err } + if rebuildTradeAgg { + err = s.RebuildTradeAggregationBuckets(cur.StartSequence, cur.EndSequence) + if err != nil { + return errors.Wrap(err, "Error rebuilding trade aggregations") + } + } } return nil } +func (s *system) RebuildTradeAggregationBuckets(fromLedger, toLedger uint32) error { + return s.historyQ.RebuildTradeAggregationBuckets(s.ctx, fromLedger, toLedger, s.config.RoundingSlippageFilter) +} + // BuildGenesisState runs the ingestion pipeline on genesis ledger. Transitions // to stopState when done. func (s *system) BuildGenesisState() error { diff --git a/services/horizon/internal/ingest/main_test.go b/services/horizon/internal/ingest/main_test.go index 460c27e062..80b5a40ed1 100644 --- a/services/horizon/internal/ingest/main_test.go +++ b/services/horizon/internal/ingest/main_test.go @@ -592,8 +592,8 @@ func (m *mockSystem) BuildState(sequence uint32, skipChecks bool) error { return args.Error(0) } -func (m *mockSystem) ReingestRange(ledgerRanges []history.LedgerRange, force bool) error { - args := m.Called(ledgerRanges, force) +func (m *mockSystem) ReingestRange(ledgerRanges []history.LedgerRange, force bool, rebuildTradeAgg bool) error { + args := m.Called(ledgerRanges, force, rebuildTradeAgg) return args.Error(0) } @@ -607,6 +607,11 @@ func (m *mockSystem) GetCurrentState() State { return args.Get(0).(State) } +func (m *mockSystem) RebuildTradeAggregationBuckets(fromLedger, toLedger uint32) error { + args := m.Called(fromLedger, toLedger) + return args.Error(0) +} + func (m *mockSystem) Shutdown() { m.Called() } diff --git a/services/horizon/internal/ingest/parallel.go b/services/horizon/internal/ingest/parallel.go index 525f153b81..4f07c21cc4 100644 --- a/services/horizon/internal/ingest/parallel.go +++ b/services/horizon/internal/ingest/parallel.go @@ -2,6 +2,7 @@ package ingest import ( "fmt" + "math" "sync" "github.com/stellar/go/services/horizon/internal/db2/history" @@ -61,7 +62,7 @@ func (ps *ParallelSystems) runReingestWorker(s System, stop <-chan struct{}, rei case <-stop: return rangeError{} case reingestRange := <-reingestJobQueue: - err := s.ReingestRange([]history.LedgerRange{reingestRange}, false) + err := s.ReingestRange([]history.LedgerRange{reingestRange}, false, false) if err != nil { return rangeError{ err: err, @@ -73,7 +74,24 @@ func (ps *ParallelSystems) runReingestWorker(s System, stop <-chan struct{}, rei } } -func enqueueReingestTasks(ledgerRanges []history.LedgerRange, batchSize uint32, stop <-chan struct{}, reingestJobQueue chan<- history.LedgerRange) { +func (ps *ParallelSystems) rebuildTradeAggRanges(ledgerRanges []history.LedgerRange) error { + s, err := ps.systemFactory(ps.config) + if err != nil { + return err + } + + for _, cur := range ledgerRanges { + err := s.RebuildTradeAggregationBuckets(cur.StartSequence, cur.EndSequence) + if err != nil { + return errors.Wrapf(err, "Error rebuilding trade aggregations for range start=%v, stop=%v", cur.StartSequence, cur.EndSequence) + } + } + return nil +} + +// returns the lowest ledger to start from of all ledgerRanges +func enqueueReingestTasks(ledgerRanges []history.LedgerRange, batchSize uint32, stop <-chan struct{}, reingestJobQueue chan<- history.LedgerRange) uint32 { + lowestLedger := uint32(math.MaxUint32) for _, cur := range ledgerRanges { for subRangeFrom := cur.StartSequence; subRangeFrom < cur.EndSequence; { // job queuing @@ -83,12 +101,16 @@ func enqueueReingestTasks(ledgerRanges []history.LedgerRange, batchSize uint32, } select { case <-stop: - return + return lowestLedger case reingestJobQueue <- history.LedgerRange{StartSequence: subRangeFrom, EndSequence: subRangeTo}: } + if subRangeFrom < lowestLedger { + lowestLedger = subRangeFrom + } subRangeFrom = subRangeTo + 1 } } + return lowestLedger } func calculateParallelLedgerBatchSize(rangeSize uint32, batchSizeSuggestion uint32, workerCount uint) uint32 { @@ -166,7 +188,7 @@ func (ps *ParallelSystems) ReingestRange(ledgerRanges []history.LedgerRange, bat }() } - enqueueReingestTasks(ledgerRanges, batchSize, stop, reingestJobQueue) + lowestLedger := enqueueReingestTasks(ledgerRanges, batchSize, stop, reingestJobQueue) stopOnce.Do(func() { close(stop) @@ -176,7 +198,13 @@ func (ps *ParallelSystems) ReingestRange(ledgerRanges []history.LedgerRange, bat if lowestRangeErr != nil { lastLedger := ledgerRanges[len(ledgerRanges)-1].EndSequence + if err := ps.rebuildTradeAggRanges([]history.LedgerRange{{StartSequence: lowestLedger, EndSequence: lowestRangeErr.ledgerRange.StartSequence}}); err != nil { + log.WithError(err).Errorf("error when trying to rebuild trade agg for partially completed portion of overall parallel reingestion range, start=%v, stop=%v", lowestLedger, lowestRangeErr.ledgerRange.StartSequence) + } return errors.Wrapf(lowestRangeErr, "job failed, recommended restart range: [%d, %d]", lowestRangeErr.ledgerRange.StartSequence, lastLedger) } + if err := ps.rebuildTradeAggRanges(ledgerRanges); err != nil { + return err + } return nil } diff --git a/services/horizon/internal/ingest/parallel_test.go b/services/horizon/internal/ingest/parallel_test.go index 27ab0c459f..8004a4048c 100644 --- a/services/horizon/internal/ingest/parallel_test.go +++ b/services/horizon/internal/ingest/parallel_test.go @@ -31,7 +31,7 @@ func TestParallelReingestRange(t *testing.T) { m sync.Mutex ) result := &mockSystem{} - result.On("ReingestRange", mock.AnythingOfType("[]history.LedgerRange"), mock.AnythingOfType("bool")).Run( + result.On("ReingestRange", mock.AnythingOfType("[]history.LedgerRange"), false, false).Run( func(args mock.Arguments) { m.Lock() defer m.Unlock() @@ -39,6 +39,7 @@ func TestParallelReingestRange(t *testing.T) { // simulate call time.Sleep(time.Millisecond * time.Duration(10+rand.Int31n(50))) }).Return(error(nil)) + result.On("RebuildTradeAggregationBuckets", uint32(1), uint32(2050)).Return(nil).Once() factory := func(c Config) (System, error) { return result, nil } @@ -59,6 +60,7 @@ func TestParallelReingestRange(t *testing.T) { rangesCalled = nil system, err = newParallelSystems(config, 1, factory) assert.NoError(t, err) + result.On("RebuildTradeAggregationBuckets", uint32(1), uint32(1024)).Return(nil).Once() err = system.ReingestRange([]history.LedgerRange{{1, 1024}}, 64) result.AssertExpectations(t) expected = []history.LedgerRange{ @@ -75,8 +77,10 @@ func TestParallelReingestRangeError(t *testing.T) { config := Config{} result := &mockSystem{} // Fail on the second range - result.On("ReingestRange", []history.LedgerRange{{1537, 1792}}, mock.AnythingOfType("bool")).Return(errors.New("failed because of foo")) - result.On("ReingestRange", mock.AnythingOfType("[]history.LedgerRange"), mock.AnythingOfType("bool")).Return(error(nil)) + result.On("ReingestRange", []history.LedgerRange{{1537, 1792}}, false, false).Return(errors.New("failed because of foo")).Once() + result.On("ReingestRange", mock.AnythingOfType("[]history.LedgerRange"), false, false).Return(nil) + result.On("RebuildTradeAggregationBuckets", uint32(1), uint32(1537)).Return(nil).Once() + factory := func(c Config) (System, error) { return result, nil } @@ -94,17 +98,18 @@ func TestParallelReingestRangeErrorInEarlierJob(t *testing.T) { wg.Add(1) result := &mockSystem{} // Fail on an lower subrange after the first error - result.On("ReingestRange", []history.LedgerRange{{1025, 1280}}, mock.AnythingOfType("bool")).Run(func(mock.Arguments) { + result.On("ReingestRange", []history.LedgerRange{{1025, 1280}}, false, false).Run(func(mock.Arguments) { // Wait for a more recent range to error wg.Wait() // This sleep should help making sure the result of this range is processed later than the one below // (there are no guarantees without instrumenting ReingestRange(), but that's too complicated) time.Sleep(50 * time.Millisecond) - }).Return(errors.New("failed because of foo")) - result.On("ReingestRange", []history.LedgerRange{{1537, 1792}}, mock.AnythingOfType("bool")).Run(func(mock.Arguments) { + }).Return(errors.New("failed because of foo")).Once() + result.On("ReingestRange", []history.LedgerRange{{1537, 1792}}, false, false).Run(func(mock.Arguments) { wg.Done() - }).Return(errors.New("failed because of bar")) - result.On("ReingestRange", mock.AnythingOfType("[]history.LedgerRange"), mock.AnythingOfType("bool")).Return(error(nil)) + }).Return(errors.New("failed because of bar")).Once() + result.On("ReingestRange", mock.AnythingOfType("[]history.LedgerRange"), false, false).Return(error(nil)) + result.On("RebuildTradeAggregationBuckets", uint32(1), uint32(1025)).Return(nil).Once() factory := func(c Config) (System, error) { return result, nil From f1a06c7396212947a77abd9062d27814fbf0fb26 Mon Sep 17 00:00:00 2001 From: Shawn Reuland Date: Fri, 19 Jan 2024 12:00:24 -0800 Subject: [PATCH 036/234] 2.28.0 release prep, update ci tests for latest soroban and changelog notes --- .github/workflows/horizon.yml | 6 +++--- services/horizon/CHANGELOG.md | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index 598da76bca..ed5397d184 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -33,9 +33,9 @@ jobs: env: HORIZON_INTEGRATION_TESTS_ENABLED: true HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL: ${{ matrix.protocol-version }} - PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 20.0.2-1633.669916b56.focal - PROTOCOL_20_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:20.0.2-1633.669916b56.focal - PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:20.0.2-47 + PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 20.1.0-1656.114b833e7.focal + PROTOCOL_20_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:20.1.0-1656.114b833e7.focal + PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:20.2.0 PROTOCOL_19_CORE_DEBIAN_PKG_VERSION: 19.14.0-1500.5664eff4e.focal PROTOCOL_19_CORE_DOCKER_IMG: stellar/stellar-core:19.14.0-1500.5664eff4e.focal PGHOST: localhost diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 0d39a7aca3..0e8338e634 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -5,13 +5,18 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +## 2.28.0 + ### Fixed - Trade agg rebuild errors reported on `db reingest range` with parellel workers ([5168](https://github.com/stellar/go/pull/5168)) - http archive requests include user agent ([5166](https://github.com/stellar/go/pull/5166)) +- Network usage has been significantly reduced with caching. **Warning:** To support the cache, disk requirements may increase by up to 15GB ([5171](https://github.com/stellar/go/pull/5171)). ### Added +- improve ingestion performance timing ([4909](https://github.com/stellar/go/issues/4909)) - http archive requests include metrics ([5166](https://github.com/stellar/go/pull/5166)) - Add a deprecation warning for using command-line flags when running Horizon ([5051](https://github.com/stellar/go/pull/5051)) +- limit global flags displayed on cli help output ([5077](https://github.com/stellar/go/pull/5077)) ### Breaking Changes - Removed configuration flags `--stellar-core-url-db`, `--cursor-name` `--skip-cursor-update` , they were related to legacy non-captive core ingestion and are no longer usable. From 47c2ae268df8344152c444dc04b5c96f558bf327 Mon Sep 17 00:00:00 2001 From: George Date: Fri, 19 Jan 2024 12:17:45 -0800 Subject: [PATCH 037/234] historyarchive: Cache bucket files from history archives on disk. (#5171) * go mod tidy * Add double-close protection * Add request tracking when cache invokes upstream download * Add cache hit tracking * Move stat tracking to a separate file * Modify test to track stats+integrity after caching * Stop double-closing identical XDR stream readers --- historyarchive/archive.go | 91 +++----- historyarchive/archive_cache.go | 202 ++++++++++++++++++ historyarchive/archive_test.go | 34 ++- historyarchive/mocks.go | 5 + historyarchive/stats.go | 57 +++++ historyarchive/xdrstream.go | 6 +- services/horizon/CHANGELOG.md | 14 +- services/horizon/internal/ingest/fsm.go | 5 + services/horizon/internal/ingest/main.go | 9 + .../internal/ingest/resume_state_test.go | 3 + 10 files changed, 354 insertions(+), 72 deletions(-) create mode 100644 historyarchive/archive_cache.go create mode 100644 historyarchive/stats.go diff --git a/historyarchive/archive.go b/historyarchive/archive.go index f07e8518ce..e7c01722c5 100644 --- a/historyarchive/archive.go +++ b/historyarchive/archive.go @@ -10,14 +10,12 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/url" "path" "regexp" "strconv" "strings" "sync" - "sync/atomic" log "github.com/sirupsen/logrus" @@ -47,9 +45,7 @@ type ArchiveOptions struct { // CheckpointFrequency is the number of ledgers between checkpoints // if unset, DefaultCheckpointFrequency will be used CheckpointFrequency uint32 - storage.ConnectOptions - // CacheConfig controls how/if bucket files are cached on the disk. CacheConfig CacheOptions } @@ -60,51 +56,6 @@ type Ledger struct { TransactionResult xdr.TransactionHistoryResultEntry } -// golang will auto wrap them back to 0 if they overflow after addition. -type archiveStats struct { - requests atomic.Uint32 - fileDownloads atomic.Uint32 - fileUploads atomic.Uint32 - backendName string -} - -type ArchiveStats interface { - GetRequests() uint32 - GetDownloads() uint32 - GetUploads() uint32 - GetBackendName() string -} - -func (as *archiveStats) incrementDownloads() { - as.fileDownloads.Add(1) - as.incrementRequests() -} - -func (as *archiveStats) incrementUploads() { - as.fileUploads.Add(1) - as.incrementRequests() -} - -func (as *archiveStats) incrementRequests() { - as.requests.Add(1) -} - -func (as *archiveStats) GetRequests() uint32 { - return as.requests.Load() -} - -func (as *archiveStats) GetDownloads() uint32 { - return as.fileDownloads.Load() -} - -func (as *archiveStats) GetUploads() uint32 { - return as.fileUploads.Load() -} - -func (as *archiveStats) GetBackendName() string { - return as.backendName -} - type ArchiveBackend interface { Exists(path string) (bool, error) Size(path string) (int64, error) @@ -217,13 +168,11 @@ func (a *Archive) PutPathHAS(path string, has HistoryArchiveState, opts *Command return err } a.stats.incrementUploads() - return a.backend.PutFile(path, - ioutil.NopCloser(bytes.NewReader(buf))) + return a.backend.PutFile(path, io.NopCloser(bytes.NewReader(buf))) } func (a *Archive) BucketExists(bucket Hash) (bool, error) { - a.stats.incrementRequests() - return a.backend.Exists(BucketPath(bucket)) + return a.cachedExists(BucketPath(bucket)) } func (a *Archive) BucketSize(bucket Hash) (int64, error) { @@ -396,8 +345,8 @@ func (a *Archive) ListCategoryCheckpoints(cat string, pth string) (chan uint32, ext := categoryExt(cat) rx := regexp.MustCompile(cat + hexPrefixPat + cat + "-([0-9a-f]{8})\\." + regexp.QuoteMeta(ext) + "$") - sch, errs := a.backend.ListFiles(path.Join(cat, pth)) a.stats.incrementRequests() + sch, errs := a.backend.ListFiles(path.Join(cat, pth)) ch := make(chan uint32) errs = makeErrorPump(errs) @@ -434,14 +383,42 @@ func (a *Archive) GetXdrStream(pth string) (*XdrStream, error) { if !strings.HasSuffix(pth, ".xdr.gz") { return nil, errors.New("File has non-.xdr.gz suffix: " + pth) } - rdr, err := a.backend.GetFile(pth) - a.stats.incrementDownloads() + rdr, err := a.cachedGet(pth) if err != nil { return nil, err } return NewXdrGzStream(rdr) } +func (a *Archive) cachedGet(pth string) (io.ReadCloser, error) { + if a.cache != nil { + rdr, foundInCache, err := a.cache.GetFile(pth, a.backend) + if !foundInCache { + a.stats.incrementDownloads() + } else { + a.stats.incrementCacheHits() + } + if err == nil { + return rdr, nil + } + + // If there's an error, retry with the uncached backend. + a.cache.Evict(pth) + } + + a.stats.incrementDownloads() + return a.backend.GetFile(pth) +} + +func (a *Archive) cachedExists(pth string) (bool, error) { + if a.cache != nil && a.cache.Exists(pth) { + return true, nil + } + + a.stats.incrementRequests() + return a.backend.Exists(pth) +} + func Connect(u string, opts ArchiveOptions) (*Archive, error) { arch := Archive{ networkPassphrase: opts.NetworkPassphrase, @@ -501,7 +478,7 @@ func ConnectBackend(u string, opts storage.ConnectOptions) (storage.Storage, err backend, err = storage.ConnectBackend(u, opts) } - return backend, err + return backend, nil } func MustConnect(u string, opts ArchiveOptions) *Archive { diff --git a/historyarchive/archive_cache.go b/historyarchive/archive_cache.go new file mode 100644 index 0000000000..a3029a7ae4 --- /dev/null +++ b/historyarchive/archive_cache.go @@ -0,0 +1,202 @@ +package historyarchive + +import ( + "io" + "os" + "path" + + lru "github.com/hashicorp/golang-lru" + log "github.com/sirupsen/logrus" +) + +type CacheOptions struct { + Cache bool + + Path string + MaxFiles uint +} + +type ArchiveBucketCache struct { + path string + lru *lru.Cache + log *log.Entry +} + +// MakeArchiveBucketCache creates a cache on the disk at the given path that +// acts as an LRU cache, mimicking a particular upstream. +func MakeArchiveBucketCache(opts CacheOptions) (*ArchiveBucketCache, error) { + log_ := log. + WithField("subservice", "fs-cache"). + WithField("path", opts.Path). + WithField("cap", opts.MaxFiles) + + if _, err := os.Stat(opts.Path); err == nil || os.IsExist(err) { + log_.Warnf("Cache directory already exists, removing") + os.RemoveAll(opts.Path) + } + + backend := &ArchiveBucketCache{ + path: opts.Path, + log: log_, + } + + cache, err := lru.NewWithEvict(int(opts.MaxFiles), backend.onEviction) + if err != nil { + return &ArchiveBucketCache{}, err + } + backend.lru = cache + + log_.Info("Bucket cache initialized") + return backend, nil +} + +// GetFile retrieves the file contents from the local cache if present. +// Otherwise, it returns the same result as the upstream, adding that result +// into the local cache if possible. It returns a 3-tuple of a reader (which may +// be nil on an error), an indication of whether or not it was *found* in the +// cache, and any error. +func (abc *ArchiveBucketCache) GetFile( + filepath string, + upstream ArchiveBackend, +) (io.ReadCloser, bool, error) { + L := abc.log.WithField("key", filepath) + localPath := path.Join(abc.path, filepath) + + // If the lockfile exists, we should defer to the remote source but *not* + // update the cache, as it means there's an in-progress sync of the same + // file. + _, statErr := os.Stat(NameLockfile(localPath)) + if statErr == nil || os.IsExist(statErr) { + L.Info("Incomplete file in on-disk cache: deferring") + reader, err := upstream.GetFile(filepath) + return reader, false, err + } else if _, ok := abc.lru.Get(localPath); !ok { + L.Info("File does not exist in the cache: downloading") + + // Since it's not on-disk, pull it from the remote backend, shove it + // into the cache, and write it to disk. + remote, err := upstream.GetFile(filepath) + if err != nil { + return remote, false, err + } + + local, err := abc.createLocal(filepath) + if err != nil { + // If there's some local FS error, we can still continue with the + // remote version, so just log it and continue. + L.WithError(err).Warn("Creating cache file failed") + return remote, false, nil + } + + return teeReadCloser(remote, local, func() error { + L.Debug("Download complete: removing lockfile") + return os.Remove(NameLockfile(localPath)) + }), false, nil + } + + L.Info("Found file in cache") + // The cache claims it exists, so just give it a read and send it. + local, err := os.Open(localPath) + if err != nil { + // Uh-oh, the cache and the disk are not in sync somehow? Let's evict + // this value and try again (recurse) w/ the remote version. + L.WithError(err).Warn("Opening cached file failed") + abc.lru.Remove(localPath) + return abc.GetFile(filepath, upstream) + } + + return local, true, nil +} + +func (abc *ArchiveBucketCache) Exists(filepath string) bool { + return abc.lru.Contains(path.Join(abc.path, filepath)) +} + +// Close purges the cache and cleans up the filesystem. +func (abc *ArchiveBucketCache) Close() error { + abc.lru.Purge() + return os.RemoveAll(abc.path) +} + +// Evict removes a file from the cache and the filesystem. +func (abc *ArchiveBucketCache) Evict(filepath string) { + log.WithField("key", filepath).Info("Evicting file from the disk") + abc.lru.Remove(path.Join(abc.path, filepath)) +} + +func (abc *ArchiveBucketCache) onEviction(key, value interface{}) { + path := key.(string) + os.Remove(NameLockfile(path)) // just in case + if err := os.Remove(path); err != nil { // best effort removal + abc.log.WithError(err). + WithField("key", path). + Warn("Removal failed after cache eviction") + } +} + +func (abc *ArchiveBucketCache) createLocal(filepath string) (*os.File, error) { + localPath := path.Join(abc.path, filepath) + if err := os.MkdirAll(path.Dir(localPath), 0755 /* drwxr-xr-x */); err != nil { + return nil, err + } + + local, err := os.Create(localPath) /* mode -rw-rw-rw- */ + if err != nil { + return nil, err + } + _, err = os.Create(NameLockfile(localPath)) + if err != nil { + return nil, err + } + + abc.lru.Add(localPath, struct{}{}) // just use the cache as an array + return local, nil +} + +func NameLockfile(file string) string { + return file + ".lock" +} + +// The below is a helper interface so that we can use io.TeeReader to write +// data locally immediately as we read it remotely. + +type trc struct { + io.Reader + close func() error + closed bool // prevents a double-close +} + +func (t trc) Close() error { + if t.closed { + return nil + } + + return t.close() +} + +func teeReadCloser(r io.ReadCloser, w io.WriteCloser, onClose func() error) io.ReadCloser { + closer := trc{ + Reader: io.TeeReader(r, w), + closed: false, + } + closer.close = func() error { + if closer.closed { + return nil + } + + // Always run all closers, but return the first error + err1 := r.Close() + err2 := w.Close() + err3 := onClose() + + closer.closed = true + if err1 != nil { + return err1 + } else if err2 != nil { + return err2 + } + return err3 + } + + return closer +} diff --git a/historyarchive/archive_test.go b/historyarchive/archive_test.go index 91306c3285..cb337bb499 100644 --- a/historyarchive/archive_test.go +++ b/historyarchive/archive_test.go @@ -16,6 +16,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "strings" "testing" @@ -48,7 +49,13 @@ func GetTestS3Archive() *Archive { } func GetTestMockArchive() *Archive { - return MustConnect("mock://test", ArchiveOptions{CheckpointFrequency: DefaultCheckpointFrequency}) + return MustConnect("mock://test", + ArchiveOptions{CheckpointFrequency: 64, + CacheConfig: CacheOptions{ + Cache: true, + Path: filepath.Join(os.TempDir(), "history-archive-test-cache"), + MaxFiles: 5, + }}) } var tmpdirs []string @@ -637,11 +644,32 @@ func TestGetLedgers(t *testing.T) { []xdrEntry{results[0], results[1], results[2]}, ) + stats := archive.GetStats()[0] ledgers, err := archive.GetLedgers(1000, 1002) + assert.NoError(t, err) assert.Len(t, ledgers, 3) - assert.Equal(t, uint32(7), archive.GetStats()[0].GetRequests()) // it started at 1, incurred 6 requests total, 3 queries, 3 downloads - assert.Equal(t, uint32(3), archive.GetStats()[0].GetDownloads()) // started 0, incurred 3 file downloads + // it started at 1, incurred 6 requests total, 3 queries, 3 downloads + assert.EqualValues(t, 7, stats.GetRequests()) + // started 0, incurred 3 file downloads + assert.EqualValues(t, 3, stats.GetDownloads()) + for i, seq := range []uint32{1000, 1001, 1002} { + ledger := ledgers[seq] + assertXdrEquals(t, ledgerHeaders[i], ledger.Header) + assertXdrEquals(t, transactions[i], ledger.Transaction) + assertXdrEquals(t, results[i], ledger.TransactionResult) + } + + // Repeat the same check but ensure the cache was used + ledgers, err = archive.GetLedgers(1000, 1002) // all cached + assert.NoError(t, err) + assert.Len(t, ledgers, 3) + + // downloads should not change because of the cache + assert.EqualValues(t, 3, stats.GetDownloads()) + // but requests increase because of 3 fetches to categories + assert.EqualValues(t, 10, stats.GetRequests()) + assert.EqualValues(t, 3, stats.GetCacheHits()) for i, seq := range []uint32{1000, 1001, 1002} { ledger := ledgers[seq] assertXdrEquals(t, ledgerHeaders[i], ledger.Header) diff --git a/historyarchive/mocks.go b/historyarchive/mocks.go index b256d0d7b9..fe497ec36e 100644 --- a/historyarchive/mocks.go +++ b/historyarchive/mocks.go @@ -132,3 +132,8 @@ func (m *MockArchiveStats) GetBackendName() string { a := m.Called() return a.Get(0).(string) } + +func (m *MockArchiveStats) GetCacheHits() uint32 { + a := m.Called() + return a.Get(0).(uint32) +} diff --git a/historyarchive/stats.go b/historyarchive/stats.go new file mode 100644 index 0000000000..c182853d1b --- /dev/null +++ b/historyarchive/stats.go @@ -0,0 +1,57 @@ +package historyarchive + +import "sync/atomic" + +// golang will auto wrap them back to 0 if they overflow after addition. +type archiveStats struct { + requests atomic.Uint32 + fileDownloads atomic.Uint32 + fileUploads atomic.Uint32 + cacheHits atomic.Uint32 + backendName string +} + +type ArchiveStats interface { + GetRequests() uint32 + GetDownloads() uint32 + GetUploads() uint32 + GetCacheHits() uint32 + GetBackendName() string +} + +func (as *archiveStats) incrementDownloads() { + as.fileDownloads.Add(1) + as.incrementRequests() +} + +func (as *archiveStats) incrementUploads() { + as.fileUploads.Add(1) + as.incrementRequests() +} + +func (as *archiveStats) incrementRequests() { + as.requests.Add(1) +} + +func (as *archiveStats) incrementCacheHits() { + as.cacheHits.Add(1) +} + +func (as *archiveStats) GetRequests() uint32 { + return as.requests.Load() +} + +func (as *archiveStats) GetDownloads() uint32 { + return as.fileDownloads.Load() +} + +func (as *archiveStats) GetUploads() uint32 { + return as.fileUploads.Load() +} + +func (as *archiveStats) GetBackendName() string { + return as.backendName +} +func (as *archiveStats) GetCacheHits() uint32 { + return as.cacheHits.Load() +} diff --git a/historyarchive/xdrstream.go b/historyarchive/xdrstream.go index e0d9745585..de8efc3bb6 100644 --- a/historyarchive/xdrstream.go +++ b/historyarchive/xdrstream.go @@ -134,11 +134,7 @@ func (x *XdrStream) closeReaders() error { err = err2 } } - if x.rdr2 != nil { - if err2 := x.rdr2.Close(); err2 != nil { - err = err2 - } - } + if x.gzipReader != nil { if err2 := x.gzipReader.Close(); err2 != nil { err = err2 diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 0e8338e634..2611ade8b2 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -8,18 +8,18 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ## 2.28.0 ### Fixed -- Trade agg rebuild errors reported on `db reingest range` with parellel workers ([5168](https://github.com/stellar/go/pull/5168)) -- http archive requests include user agent ([5166](https://github.com/stellar/go/pull/5166)) +- Ingestion performance timing is improved ([4909](https://github.com/stellar/go/issues/4909)) +- Trade aggregation rebuild errors reported on `db reingest range` with parallel workers ([5168](https://github.com/stellar/go/pull/5168)) +- Limited global flags displayed on cli help output ([5077](https://github.com/stellar/go/pull/5077)) - Network usage has been significantly reduced with caching. **Warning:** To support the cache, disk requirements may increase by up to 15GB ([5171](https://github.com/stellar/go/pull/5171)). ### Added -- improve ingestion performance timing ([4909](https://github.com/stellar/go/issues/4909)) -- http archive requests include metrics ([5166](https://github.com/stellar/go/pull/5166)) -- Add a deprecation warning for using command-line flags when running Horizon ([5051](https://github.com/stellar/go/pull/5051)) -- limit global flags displayed on cli help output ([5077](https://github.com/stellar/go/pull/5077)) +- We now include metrics for history archive requests ([5166](https://github.com/stellar/go/pull/5166)) +- Http history archive requests now include a unique user agent ([5166](https://github.com/stellar/go/pull/5166)) +- Added a deprecation warning for using command-line flags when running Horizon ([5051](https://github.com/stellar/go/pull/5051)) ### Breaking Changes -- Removed configuration flags `--stellar-core-url-db`, `--cursor-name` `--skip-cursor-update` , they were related to legacy non-captive core ingestion and are no longer usable. +- Removed configuration flags `--stellar-core-url-db`, `--cursor-name` `--skip-cursor-update` , they were related to legacy non-captive core ingestion and are no longer usable. ## 2.27.0 diff --git a/services/horizon/internal/ingest/fsm.go b/services/horizon/internal/ingest/fsm.go index 59a1a7c969..e0c667b033 100644 --- a/services/horizon/internal/ingest/fsm.go +++ b/services/horizon/internal/ingest/fsm.go @@ -590,6 +590,11 @@ func addHistoryArchiveStatsMetrics(s *system, stats []historyarchive.ArchiveStat "source": historyServerStat.GetBackendName(), "type": "requests"}). Add(float64(historyServerStat.GetRequests())) + s.Metrics().HistoryArchiveStatsCounter. + With(prometheus.Labels{ + "source": historyServerStat.GetBackendName(), + "type": "cache_hits"}). + Add(float64(historyServerStat.GetCacheHits())) } } diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index e19242cd2b..9acdc8a725 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -6,6 +6,7 @@ package ingest import ( "context" "fmt" + "path" "runtime" "sync" "time" @@ -225,9 +226,17 @@ func NewSystem(config Config) (System, error) { historyarchive.ArchiveOptions{ NetworkPassphrase: config.NetworkPassphrase, CheckpointFrequency: config.CheckpointFrequency, +<<<<<<< HEAD ConnectOptions: storage.ConnectOptions{ Context: ctx, UserAgent: fmt.Sprintf("horizon/%s golang/%s", apkg.Version(), runtime.Version()), +======= + UserAgent: fmt.Sprintf("horizon/%s golang/%s", apkg.Version(), runtime.Version()), + CacheConfig: historyarchive.CacheOptions{ + Cache: true, + Path: path.Join(config.CaptiveCoreStoragePath, "bucket-cache"), + MaxFiles: 50, +>>>>>>> 7e6d25fe (historyarchive: Cache bucket files from history archives on disk. (#5171)) }, }, ) diff --git a/services/horizon/internal/ingest/resume_state_test.go b/services/horizon/internal/ingest/resume_state_test.go index d989e7a9e5..f1f8b2ce2a 100644 --- a/services/horizon/internal/ingest/resume_state_test.go +++ b/services/horizon/internal/ingest/resume_state_test.go @@ -266,6 +266,7 @@ func (s *ResumeTestTestSuite) mockSuccessfulIngestion() { mockStats.On("GetDownloads").Return(uint32(0)) mockStats.On("GetRequests").Return(uint32(0)) mockStats.On("GetUploads").Return(uint32(0)) + mockStats.On("GetCacheHits").Return(uint32(0)) s.historyAdapter.On("GetStats").Return([]historyarchive.ArchiveStats{mockStats}).Once() s.runner.On("RunAllProcessorsOnLedger", mock.AnythingOfType("xdr.LedgerCloseMeta")). @@ -382,6 +383,7 @@ func (s *ResumeTestTestSuite) TestReapingObjectsDisabled() { mockStats.On("GetDownloads").Return(uint32(0)) mockStats.On("GetRequests").Return(uint32(0)) mockStats.On("GetUploads").Return(uint32(0)) + mockStats.On("GetCacheHits").Return(uint32(0)) s.historyAdapter.On("GetStats").Return([]historyarchive.ArchiveStats{mockStats}).Once() // Reap lookup tables not executed @@ -431,6 +433,7 @@ func (s *ResumeTestTestSuite) TestErrorReapingObjectsIgnored() { mockStats.On("GetDownloads").Return(uint32(0)) mockStats.On("GetRequests").Return(uint32(0)) mockStats.On("GetUploads").Return(uint32(0)) + mockStats.On("GetCacheHits").Return(uint32(0)) s.historyAdapter.On("GetStats").Return([]historyarchive.ArchiveStats{mockStats}).Once() next, err := resumeState{latestSuccessfullyProcessedLedger: 100}.run(s.system) From de92d19375a0a30c6d10cddca86ed7d9152abc6e Mon Sep 17 00:00:00 2001 From: George Date: Mon, 22 Jan 2024 16:52:20 -0800 Subject: [PATCH 038/234] services/horizon: Bump the history archive cache size to increase hit rates (#5177) --- services/horizon/internal/ingest/main.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 9acdc8a725..53c4881451 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -226,17 +226,13 @@ func NewSystem(config Config) (System, error) { historyarchive.ArchiveOptions{ NetworkPassphrase: config.NetworkPassphrase, CheckpointFrequency: config.CheckpointFrequency, -<<<<<<< HEAD ConnectOptions: storage.ConnectOptions{ Context: ctx, - UserAgent: fmt.Sprintf("horizon/%s golang/%s", apkg.Version(), runtime.Version()), -======= - UserAgent: fmt.Sprintf("horizon/%s golang/%s", apkg.Version(), runtime.Version()), + UserAgent: fmt.Sprintf("horizon/%s golang/%s", apkg.Version(), runtime.Version()),}, CacheConfig: historyarchive.CacheOptions{ Cache: true, Path: path.Join(config.CaptiveCoreStoragePath, "bucket-cache"), - MaxFiles: 50, ->>>>>>> 7e6d25fe (historyarchive: Cache bucket files from history archives on disk. (#5171)) + MaxFiles: 150, }, }, ) From 431009b9f55dddf251e05c547f244b06a19bcb08 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 22 Jan 2024 18:35:50 -0800 Subject: [PATCH 039/234] historyarchive: Make the library target the same log as Horizon (#5178) * Change logging provider to Horizon's default rather than logrus --- historyarchive/archive_cache.go | 10 +++++++--- services/horizon/internal/ingest/main.go | 1 + support/log/entry.go | 3 +-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/historyarchive/archive_cache.go b/historyarchive/archive_cache.go index a3029a7ae4..a3990428b0 100644 --- a/historyarchive/archive_cache.go +++ b/historyarchive/archive_cache.go @@ -6,7 +6,7 @@ import ( "path" lru "github.com/hashicorp/golang-lru" - log "github.com/sirupsen/logrus" + log "github.com/stellar/go/support/log" ) type CacheOptions struct { @@ -14,6 +14,7 @@ type CacheOptions struct { Path string MaxFiles uint + Log *log.Entry } type ArchiveBucketCache struct { @@ -25,8 +26,11 @@ type ArchiveBucketCache struct { // MakeArchiveBucketCache creates a cache on the disk at the given path that // acts as an LRU cache, mimicking a particular upstream. func MakeArchiveBucketCache(opts CacheOptions) (*ArchiveBucketCache, error) { - log_ := log. - WithField("subservice", "fs-cache"). + log_ := opts.Log + if opts.Log == nil { + log_ = log.WithField("subservice", "fs-cache") + } + log_ = log_. WithField("path", opts.Path). WithField("cap", opts.MaxFiles) diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 53c4881451..dae494efc4 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -232,6 +232,7 @@ func NewSystem(config Config) (System, error) { CacheConfig: historyarchive.CacheOptions{ Cache: true, Path: path.Join(config.CaptiveCoreStoragePath, "bucket-cache"), + Log: log.WithField("subservice", "ha-cache"), MaxFiles: 150, }, }, diff --git a/support/log/entry.go b/support/log/entry.go index a4661cb8c0..9b3b596025 100644 --- a/support/log/entry.go +++ b/support/log/entry.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "io/ioutil" gerr "github.com/go-errors/errors" "github.com/sirupsen/logrus" @@ -198,7 +197,7 @@ func (e *Entry) StartTest(level logrus.Level) func() []logrus.Entry { e.entry.Logger.AddHook(hook) old := e.entry.Logger.Out - e.entry.Logger.Out = ioutil.Discard + e.entry.Logger.Out = io.Discard oldLevel := e.entry.Logger.GetLevel() e.entry.Logger.SetLevel(level) From 67ba6a418563720eeb38a108e6df4ebe166f2940 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Wed, 24 Jan 2024 17:17:23 -0800 Subject: [PATCH 040/234] services/horizon: Add DISABLE_SOROBAN_INGEST flag to skip soroban ingestion processing (#5176) --- .github/workflows/horizon.yml | 2 +- services/horizon/CHANGELOG.md | 5 + services/horizon/cmd/db.go | 1 + services/horizon/internal/config.go | 2 + services/horizon/internal/flags.go | 11 + services/horizon/internal/ingest/main.go | 2 + .../internal/ingest/processor_runner.go | 14 +- .../internal/ingest/processor_runner_test.go | 5 +- .../ingest/processors/effects_processor.go | 22 +- .../processors/effects_processor_test.go | 1 + .../ingest/processors/operations_processor.go | 52 +- .../processors/operations_processor_test.go | 60 ++ .../processors/transactions_processor.go | 29 +- .../processors/transactions_processor_test.go | 2 +- .../horizon/internal/ingest/verify_test.go | 4 +- services/horizon/internal/init.go | 1 + .../integration/invokehostfunction_test.go | 103 +++- .../horizon/internal/integration/sac_test.go | 553 +++++++++++------- 18 files changed, 608 insertions(+), 261 deletions(-) diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index ed5397d184..df314900d2 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -120,7 +120,7 @@ jobs: key: ${{ env.COMBINED_SOURCE_HASH }} - if: ${{ steps.horizon_binary_tests_hash.outputs.cache-hit != 'true' }} - run: go test -race -timeout 45m -v ./services/horizon/internal/integration/... + run: go test -race -timeout 65m -v ./services/horizon/internal/integration/... - name: Save Horizon binary and integration tests source hash to cache if: ${{ success() && steps.horizon_binary_tests_hash.outputs.cache-hit != 'true' }} diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 2611ade8b2..6c9ca1b69f 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -17,6 +17,11 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). - We now include metrics for history archive requests ([5166](https://github.com/stellar/go/pull/5166)) - Http history archive requests now include a unique user agent ([5166](https://github.com/stellar/go/pull/5166)) - Added a deprecation warning for using command-line flags when running Horizon ([5051](https://github.com/stellar/go/pull/5051)) +- New optional config `DISABLE_SOROBAN_INGEST` ([5175](https://github.com/stellar/go/issues/5175)). Defaults to `FALSE`, when `TRUE` and a soroban transaction is ingested, the following will occur: + * no effects will be generated for contract invocations. + * history_transactions.tx_meta column will have serialized xdr that equates to an empty `xdr.TransactionMeta.V3`, `Operations`, `TxChangesAfter`, `TxChangesBefore` will empty arrays and `SorobanMeta` will be nil. + * API transaction model for `result_meta_xdr` will have same empty serialized xdr for `xdr.TransactionMeta.V3`, `Operations`, `TxChangesAfter`, `TxChangesBefore` will empty arrays and `SorobanMeta` will be nil. + * API `Operation` model for `InvokeHostFunctionOp` type, will have empty `asset_balance_changes` ### Breaking Changes - Removed configuration flags `--stellar-core-url-db`, `--cursor-name` `--skip-cursor-update` , they were related to legacy non-captive core ingestion and are no longer usable. diff --git a/services/horizon/cmd/db.go b/services/horizon/cmd/db.go index 725df622b0..07bbf975fa 100644 --- a/services/horizon/cmd/db.go +++ b/services/horizon/cmd/db.go @@ -419,6 +419,7 @@ func runDBReingestRange(ledgerRanges []history.LedgerRange, reingestForce bool, RoundingSlippageFilter: config.RoundingSlippageFilter, EnableIngestionFiltering: config.EnableIngestionFiltering, MaxLedgerPerFlush: maxLedgersPerFlush, + SkipSorobanIngestion: config.SkipSorobanIngestion, } if ingestConfig.HistorySession, err = db.Open("postgres", config.DatabaseURL); err != nil { diff --git a/services/horizon/internal/config.go b/services/horizon/internal/config.go index 7454f52bb7..8fb31075b8 100644 --- a/services/horizon/internal/config.go +++ b/services/horizon/internal/config.go @@ -108,4 +108,6 @@ type Config struct { Network string // DisableTxSub disables transaction submission functionality for Horizon. DisableTxSub bool + // SkipSorobanIngestion skips Soroban related ingestion processing. + SkipSorobanIngestion bool } diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index 40bfc08afe..eb229c65b2 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -57,6 +57,8 @@ const ( EnableIngestionFilteringFlagName = "exp-enable-ingestion-filtering" // DisableTxSubFlagName is the command line flag for disabling transaction submission feature of Horizon DisableTxSubFlagName = "disable-tx-sub" + // SkipSorobanIngestionFlagName is the command line flag for disabling Soroban related ingestion processing + SkipSorobanIngestionFlagName = "disable-soroban-ingest" // StellarPubnet is a constant representing the Stellar public network StellarPubnet = "pubnet" @@ -730,6 +732,15 @@ func Flags() (*Config, support.ConfigOptions) { HistoryArchiveURLsFlagName, CaptiveCoreConfigPathName), UsedInCommands: IngestionCommands, }, + &support.ConfigOption{ + Name: SkipSorobanIngestionFlagName, + ConfigKey: &config.SkipSorobanIngestion, + OptType: types.Bool, + FlagDefault: false, + Required: false, + Usage: "excludes Soroban data during ingestion processing", + UsedInCommands: IngestionCommands, + }, } return config, flags diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index dae494efc4..556724bde1 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -108,6 +108,8 @@ type Config struct { EnableIngestionFiltering bool MaxLedgerPerFlush uint32 + + SkipSorobanIngestion bool } const ( diff --git a/services/horizon/internal/ingest/processor_runner.go b/services/horizon/internal/ingest/processor_runner.go index 34b977c03e..a09442b49d 100644 --- a/services/horizon/internal/ingest/processor_runner.go +++ b/services/horizon/internal/ingest/processor_runner.go @@ -111,6 +111,7 @@ func buildChangeProcessor( source ingestionSource, ledgerSequence uint32, networkPassphrase string, + skipSorobanIngestion bool, ) *groupChangeProcessors { statsChangeProcessor := &statsChangeProcessor{ StatsChangeProcessor: changeStats, @@ -144,13 +145,13 @@ func (s *ProcessorRunner) buildTransactionProcessor(ledgersProcessor *processors processors := []horizonTransactionProcessor{ statsLedgerTransactionProcessor, - processors.NewEffectProcessor(accountLoader, s.historyQ.NewEffectBatchInsertBuilder(), s.config.NetworkPassphrase), + processors.NewEffectProcessor(accountLoader, s.historyQ.NewEffectBatchInsertBuilder(), s.config.NetworkPassphrase, s.config.SkipSorobanIngestion), ledgersProcessor, - processors.NewOperationProcessor(s.historyQ.NewOperationBatchInsertBuilder(), s.config.NetworkPassphrase), + processors.NewOperationProcessor(s.historyQ.NewOperationBatchInsertBuilder(), s.config.NetworkPassphrase, s.config.SkipSorobanIngestion), tradeProcessor, processors.NewParticipantsProcessor(accountLoader, s.historyQ.NewTransactionParticipantsBatchInsertBuilder(), s.historyQ.NewOperationParticipantBatchInsertBuilder()), - processors.NewTransactionProcessor(s.historyQ.NewTransactionBatchInsertBuilder()), + processors.NewTransactionProcessor(s.historyQ.NewTransactionBatchInsertBuilder(), s.config.SkipSorobanIngestion), processors.NewClaimableBalancesTransactionProcessor(cbLoader, s.historyQ.NewTransactionClaimableBalanceBatchInsertBuilder(), s.historyQ.NewOperationClaimableBalanceBatchInsertBuilder()), processors.NewLiquidityPoolsTransactionProcessor(lpLoader, @@ -172,7 +173,10 @@ func (s *ProcessorRunner) buildFilteredOutProcessor() *groupTransactionProcessor // when in online mode, the submission result processor must always run (regardless of filtering) var p []horizonTransactionProcessor if s.config.EnableIngestionFiltering { - txSubProc := processors.NewTransactionFilteredTmpProcessor(s.historyQ.NewTransactionFilteredTmpBatchInsertBuilder()) + txSubProc := processors.NewTransactionFilteredTmpProcessor( + s.historyQ.NewTransactionFilteredTmpBatchInsertBuilder(), + s.config.SkipSorobanIngestion, + ) p = append(p, txSubProc) } @@ -235,6 +239,7 @@ func (s *ProcessorRunner) RunHistoryArchiveIngestion( historyArchiveSource, checkpointLedger, s.config.NetworkPassphrase, + s.config.SkipSorobanIngestion, ) if checkpointLedger == 1 { @@ -493,6 +498,7 @@ func (s *ProcessorRunner) RunAllProcessorsOnLedger(ledger xdr.LedgerCloseMeta) ( ledgerSource, ledger.LedgerSequence(), s.config.NetworkPassphrase, + s.config.SkipSorobanIngestion, ) err = s.runChangeProcessorOnLedger(groupChangeProcessors, ledger) if err != nil { diff --git a/services/horizon/internal/ingest/processor_runner_test.go b/services/horizon/internal/ingest/processor_runner_test.go index eaeca95661..ddac48aa82 100644 --- a/services/horizon/internal/ingest/processor_runner_test.go +++ b/services/horizon/internal/ingest/processor_runner_test.go @@ -180,7 +180,7 @@ func TestProcessorRunnerBuildChangeProcessor(t *testing.T) { } stats := &ingest.StatsChangeProcessor{} - processor := buildChangeProcessor(runner.historyQ, stats, ledgerSource, 123, "") + processor := buildChangeProcessor(runner.historyQ, stats, ledgerSource, 123, "", false) assert.IsType(t, &groupChangeProcessors{}, processor) assert.IsType(t, &statsChangeProcessor{}, processor.processors[0]) @@ -201,7 +201,7 @@ func TestProcessorRunnerBuildChangeProcessor(t *testing.T) { filters: &MockFilters{}, } - processor = buildChangeProcessor(runner.historyQ, stats, historyArchiveSource, 456, "") + processor = buildChangeProcessor(runner.historyQ, stats, historyArchiveSource, 456, "", false) assert.IsType(t, &groupChangeProcessors{}, processor) assert.IsType(t, &statsChangeProcessor{}, processor.processors[0]) @@ -271,6 +271,7 @@ func TestProcessorRunnerWithFilterEnabled(t *testing.T) { config := Config{ NetworkPassphrase: network.PublicNetworkPassphrase, EnableIngestionFiltering: true, + SkipSorobanIngestion: false, } q := &mockDBQ{} diff --git a/services/horizon/internal/ingest/processors/effects_processor.go b/services/horizon/internal/ingest/processors/effects_processor.go index 34e9f9169a..830632f5f5 100644 --- a/services/horizon/internal/ingest/processors/effects_processor.go +++ b/services/horizon/internal/ingest/processors/effects_processor.go @@ -28,17 +28,20 @@ type EffectProcessor struct { accountLoader *history.AccountLoader batch history.EffectBatchInsertBuilder network string + skipSoroban bool } func NewEffectProcessor( accountLoader *history.AccountLoader, batch history.EffectBatchInsertBuilder, network string, + skipSoroban bool, ) *EffectProcessor { return &EffectProcessor{ accountLoader: accountLoader, batch: batch, network: network, + skipSoroban: skipSoroban, } } @@ -50,14 +53,29 @@ func (p *EffectProcessor) ProcessTransaction( return nil } - for opi, op := range transaction.Envelope.Operations() { + elidedTransaction := transaction + + if p.skipSoroban && + elidedTransaction.UnsafeMeta.V == 3 && + elidedTransaction.UnsafeMeta.V3.SorobanMeta != nil { + elidedTransaction.UnsafeMeta.V3 = &xdr.TransactionMetaV3{ + Ext: xdr.ExtensionPoint{}, + TxChangesBefore: xdr.LedgerEntryChanges{}, + Operations: []xdr.OperationMeta{}, + TxChangesAfter: xdr.LedgerEntryChanges{}, + SorobanMeta: nil, + } + } + + for opi, op := range elidedTransaction.Envelope.Operations() { operation := transactionOperationWrapper{ index: uint32(opi), - transaction: transaction, + transaction: elidedTransaction, operation: op, ledgerSequence: uint32(lcm.LedgerSequence()), network: p.network, } + if err := operation.ingestEffects(p.accountLoader, p.batch); err != nil { return errors.Wrapf(err, "reading operation %v effects", operation.ID()) } diff --git a/services/horizon/internal/ingest/processors/effects_processor_test.go b/services/horizon/internal/ingest/processors/effects_processor_test.go index 0243768fde..70af21737a 100644 --- a/services/horizon/internal/ingest/processors/effects_processor_test.go +++ b/services/horizon/internal/ingest/processors/effects_processor_test.go @@ -143,6 +143,7 @@ func (s *EffectsProcessorTestSuiteLedger) SetupTest() { s.accountLoader, s.mockBatchInsertBuilder, networkPassphrase, + false, ) s.txs = []ingest.LedgerTransaction{ diff --git a/services/horizon/internal/ingest/processors/operations_processor.go b/services/horizon/internal/ingest/processors/operations_processor.go index 8ad023145c..92a4b870e9 100644 --- a/services/horizon/internal/ingest/processors/operations_processor.go +++ b/services/horizon/internal/ingest/processors/operations_processor.go @@ -22,14 +22,16 @@ import ( // OperationProcessor operations processor type OperationProcessor struct { - batch history.OperationBatchInsertBuilder - network string + batch history.OperationBatchInsertBuilder + network string + skipSoroban bool } -func NewOperationProcessor(batch history.OperationBatchInsertBuilder, network string) *OperationProcessor { +func NewOperationProcessor(batch history.OperationBatchInsertBuilder, network string, skipSoroban bool) *OperationProcessor { return &OperationProcessor{ - batch: batch, - network: network, + batch: batch, + network: network, + skipSoroban: skipSoroban, } } @@ -37,11 +39,12 @@ func NewOperationProcessor(batch history.OperationBatchInsertBuilder, network st func (p *OperationProcessor) ProcessTransaction(lcm xdr.LedgerCloseMeta, transaction ingest.LedgerTransaction) error { for i, op := range transaction.Envelope.Operations() { operation := transactionOperationWrapper{ - index: uint32(i), - transaction: transaction, - operation: op, - ledgerSequence: lcm.LedgerSequence(), - network: p.network, + index: uint32(i), + transaction: transaction, + operation: op, + ledgerSequence: lcm.LedgerSequence(), + network: p.network, + skipSorobanDetails: p.skipSoroban, } details, err := operation.Details() if err != nil { @@ -82,11 +85,12 @@ func (p *OperationProcessor) Flush(ctx context.Context, session db.SessionInterf // transactionOperationWrapper represents the data for a single operation within a transaction type transactionOperationWrapper struct { - index uint32 - transaction ingest.LedgerTransaction - operation xdr.Operation - ledgerSequence uint32 - network string + index uint32 + transaction ingest.LedgerTransaction + operation xdr.Operation + ledgerSequence uint32 + network string + skipSorobanDetails bool } // ID returns the ID for the operation. @@ -266,6 +270,11 @@ func (operation *transactionOperationWrapper) IsPayment() bool { case xdr.OperationTypeAccountMerge: return true case xdr.OperationTypeInvokeHostFunction: + // #5175, may want to consider skipping this parsing of payment from contracts + // as part of eliding soroban ingestion aspects when DISABLE_SOROBAN_INGEST. + // but, may cause inconsistencies that aren't worth the gain, + // as payments won't be thoroughly accurate, i.e. a payment could have + // happened within a contract invoke. diagnosticEvents, err := operation.transaction.GetDiagnosticEvents() if err != nil { return false @@ -689,11 +698,18 @@ func (operation *transactionOperationWrapper) Details() (map[string]interface{}, } details["parameters"] = params - if balanceChanges, err := operation.parseAssetBalanceChangesFromContractEvents(); err != nil { - return nil, err + var balanceChanges []map[string]interface{} + var parseErr error + if operation.skipSorobanDetails { + // https://github.com/stellar/go/issues/5175 + // intentionally toggle off parsing soroban meta into "asset_balance_changes" + balanceChanges = make([]map[string]interface{}, 0) } else { - details["asset_balance_changes"] = balanceChanges + if balanceChanges, parseErr = operation.parseAssetBalanceChangesFromContractEvents(); parseErr != nil { + return nil, parseErr + } } + details["asset_balance_changes"] = balanceChanges case xdr.HostFunctionTypeHostFunctionTypeCreateContract: args := op.HostFunction.MustCreateContract() diff --git a/services/horizon/internal/ingest/processors/operations_processor_test.go b/services/horizon/internal/ingest/processors/operations_processor_test.go index 4b5fb376cd..275a6056e4 100644 --- a/services/horizon/internal/ingest/processors/operations_processor_test.go +++ b/services/horizon/internal/ingest/processors/operations_processor_test.go @@ -42,6 +42,7 @@ func (s *OperationsProcessorTestSuiteLedger) SetupTest() { s.processor = NewOperationProcessor( s.mockBatchInsertBuilder, "test network", + false, ) } @@ -375,6 +376,65 @@ func (s *OperationsProcessorTestSuiteLedger) TestOperationTypeInvokeHostFunction } s.Assert().Equal(found, 4, "should have one balance changed record for each of mint, burn, clawback, transfer") }) + + s.T().Run("InvokeContractAssetBalancesElidedFromDetails", func(t *testing.T) { + randomIssuer := keypair.MustRandom() + randomAsset := xdr.MustNewCreditAsset("TESTING", randomIssuer.Address()) + passphrase := "passphrase" + randomAccount := keypair.MustRandom().Address() + contractId := [32]byte{} + zeroContractStrKey, err := strkey.Encode(strkey.VersionByteContract, contractId[:]) + s.Assert().NoError(err) + + transferContractEvent := contractevents.GenerateEvent(contractevents.EventTypeTransfer, randomAccount, zeroContractStrKey, "", randomAsset, big.NewInt(10000000), passphrase) + burnContractEvent := contractevents.GenerateEvent(contractevents.EventTypeBurn, zeroContractStrKey, "", "", randomAsset, big.NewInt(10000000), passphrase) + mintContractEvent := contractevents.GenerateEvent(contractevents.EventTypeMint, "", zeroContractStrKey, randomAccount, randomAsset, big.NewInt(10000000), passphrase) + clawbackContractEvent := contractevents.GenerateEvent(contractevents.EventTypeClawback, zeroContractStrKey, "", randomAccount, randomAsset, big.NewInt(10000000), passphrase) + + tx = ingest.LedgerTransaction{ + UnsafeMeta: xdr.TransactionMeta{ + V: 3, + V3: &xdr.TransactionMetaV3{ + SorobanMeta: &xdr.SorobanTransactionMeta{ + Events: []xdr.ContractEvent{ + transferContractEvent, + burnContractEvent, + mintContractEvent, + clawbackContractEvent, + }, + }, + }, + }, + } + wrapper := transactionOperationWrapper{ + skipSorobanDetails: true, + transaction: tx, + operation: xdr.Operation{ + SourceAccount: &source, + Body: xdr.OperationBody{ + Type: xdr.OperationTypeInvokeHostFunction, + InvokeHostFunctionOp: &xdr.InvokeHostFunctionOp{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, + InvokeContract: &xdr.InvokeContractArgs{ + ContractAddress: xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + ContractId: &xdr.Hash{0x1, 0x2}, + }, + FunctionName: "foo", + Args: xdr.ScVec{}, + }, + }, + }, + }, + }, + network: passphrase, + } + + details, err := wrapper.Details() + s.Assert().NoError(err) + s.Assert().Len(details["asset_balance_changes"], 0, "for invokehostfn op, no asset balances should be in details when skip soroban is enabled") + }) } func (s *OperationsProcessorTestSuiteLedger) assertInvokeHostFunctionParameter(parameters []map[string]string, paramPosition int, expectedType string, expectedVal xdr.ScVal) { diff --git a/services/horizon/internal/ingest/processors/transactions_processor.go b/services/horizon/internal/ingest/processors/transactions_processor.go index 871c72624a..b82934d86a 100644 --- a/services/horizon/internal/ingest/processors/transactions_processor.go +++ b/services/horizon/internal/ingest/processors/transactions_processor.go @@ -11,23 +11,40 @@ import ( ) type TransactionProcessor struct { - batch history.TransactionBatchInsertBuilder + batch history.TransactionBatchInsertBuilder + skipSoroban bool } -func NewTransactionFilteredTmpProcessor(batch history.TransactionBatchInsertBuilder) *TransactionProcessor { +func NewTransactionFilteredTmpProcessor(batch history.TransactionBatchInsertBuilder, skipSoroban bool) *TransactionProcessor { return &TransactionProcessor{ - batch: batch, + batch: batch, + skipSoroban: skipSoroban, } } -func NewTransactionProcessor(batch history.TransactionBatchInsertBuilder) *TransactionProcessor { +func NewTransactionProcessor(batch history.TransactionBatchInsertBuilder, skipSoroban bool) *TransactionProcessor { return &TransactionProcessor{ - batch: batch, + batch: batch, + skipSoroban: skipSoroban, } } func (p *TransactionProcessor) ProcessTransaction(lcm xdr.LedgerCloseMeta, transaction ingest.LedgerTransaction) error { - if err := p.batch.Add(transaction, lcm.LedgerSequence()); err != nil { + elidedTransaction := transaction + + if p.skipSoroban && + elidedTransaction.UnsafeMeta.V == 3 && + elidedTransaction.UnsafeMeta.MustV3().SorobanMeta != nil { + elidedTransaction.UnsafeMeta.V3 = &xdr.TransactionMetaV3{ + Ext: xdr.ExtensionPoint{}, + TxChangesBefore: xdr.LedgerEntryChanges{}, + Operations: []xdr.OperationMeta{}, + TxChangesAfter: xdr.LedgerEntryChanges{}, + SorobanMeta: nil, + } + } + + if err := p.batch.Add(elidedTransaction, lcm.LedgerSequence()); err != nil { return errors.Wrap(err, "Error batch inserting transaction rows") } diff --git a/services/horizon/internal/ingest/processors/transactions_processor_test.go b/services/horizon/internal/ingest/processors/transactions_processor_test.go index 987e8ce6f9..873a72af05 100644 --- a/services/horizon/internal/ingest/processors/transactions_processor_test.go +++ b/services/horizon/internal/ingest/processors/transactions_processor_test.go @@ -29,7 +29,7 @@ func TestTransactionsProcessorTestSuiteLedger(t *testing.T) { func (s *TransactionsProcessorTestSuiteLedger) SetupTest() { s.ctx = context.Background() s.mockBatchInsertBuilder = &history.MockTransactionsBatchInsertBuilder{} - s.processor = NewTransactionProcessor(s.mockBatchInsertBuilder) + s.processor = NewTransactionProcessor(s.mockBatchInsertBuilder, false) } func (s *TransactionsProcessorTestSuiteLedger) TearDownTest() { diff --git a/services/horizon/internal/ingest/verify_test.go b/services/horizon/internal/ingest/verify_test.go index 901f21a0ca..e3c0e4ec56 100644 --- a/services/horizon/internal/ingest/verify_test.go +++ b/services/horizon/internal/ingest/verify_test.go @@ -292,7 +292,7 @@ func TestStateVerifierLockBusy(t *testing.T) { tt.Assert.NoError(q.BeginTx(tt.Ctx, &sql.TxOptions{})) checkpointLedger := uint32(63) - changeProcessor := buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, ledgerSource, checkpointLedger, "") + changeProcessor := buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, ledgerSource, checkpointLedger, "", false) gen := randxdr.NewGenerator() var changes []xdr.LedgerEntryChange @@ -350,7 +350,7 @@ func TestStateVerifier(t *testing.T) { ledger := rand.Int31() checkpointLedger := uint32(ledger - (ledger % 64) - 1) - changeProcessor := buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, ledgerSource, checkpointLedger, "") + changeProcessor := buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, ledgerSource, checkpointLedger, "", false) mockChangeReader := &ingest.MockChangeReader{} gen := randxdr.NewGenerator() diff --git a/services/horizon/internal/init.go b/services/horizon/internal/init.go index 1b6664b8ba..4078c7ad00 100644 --- a/services/horizon/internal/init.go +++ b/services/horizon/internal/init.go @@ -110,6 +110,7 @@ func initIngester(app *App) { EnableExtendedLogLedgerStats: app.config.IngestEnableExtendedLogLedgerStats, RoundingSlippageFilter: app.config.RoundingSlippageFilter, EnableIngestionFiltering: app.config.EnableIngestionFiltering, + SkipSorobanIngestion: app.config.SkipSorobanIngestion, }) if err != nil { diff --git a/services/horizon/internal/integration/invokehostfunction_test.go b/services/horizon/internal/integration/invokehostfunction_test.go index 275f0de23b..1b1edc091a 100644 --- a/services/horizon/internal/integration/invokehostfunction_test.go +++ b/services/horizon/internal/integration/invokehostfunction_test.go @@ -3,11 +3,13 @@ package integration import ( "crypto/sha256" "encoding/hex" + "fmt" "os" "path/filepath" "testing" "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/protocols/horizon/operations" "github.com/stellar/go/services/horizon/internal/test/integration" "github.com/stellar/go/txnbuild" @@ -24,13 +26,42 @@ const increment_contract = "soroban_increment_contract.wasm" // Refer to ./services/horizon/internal/integration/contracts/README.md on how to recompile // contract code if needed to new wasm. -func TestContractInvokeHostFunctionInstallContract(t *testing.T) { +func TestInvokeHostFns(t *testing.T) { + // first test contracts when soroban processing is enabled + DisabledSoroban = false + runAllTests(t) + // now test same contracts when soroban processing is disabled + DisabledSoroban = true + runAllTests(t) +} + +func runAllTests(t *testing.T) { + tests := []struct { + name string + fn func(*testing.T) + }{ + {"CaseContractInvokeHostFunctionInstallContract", CaseContractInvokeHostFunctionInstallContract}, + {"CaseContractInvokeHostFunctionCreateContractByAddress", CaseContractInvokeHostFunctionCreateContractByAddress}, + {"CaseContractInvokeHostFunctionInvokeStatelessContractFn", CaseContractInvokeHostFunctionInvokeStatelessContractFn}, + {"CaseContractInvokeHostFunctionInvokeStatefulContractFn", CaseContractInvokeHostFunctionInvokeStatefulContractFn}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("Soroban Processing Disabled = %v. ", DisabledSoroban)+tt.name, func(t *testing.T) { + tt.fn(t) + }) + } +} + +func CaseContractInvokeHostFunctionInstallContract(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{ + "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, EnableSorobanRPC: true, }) @@ -46,6 +77,7 @@ func TestContractInvokeHostFunctionInstallContract(t *testing.T) { clientTx, err := itest.Client().TransactionDetail(tx.Hash) require.NoError(t, err) + verifySorobanMeta(t, clientTx) assert.Equal(t, tx.Hash, clientTx.Hash) var txResult xdr.TransactionResult @@ -71,16 +103,17 @@ func TestContractInvokeHostFunctionInstallContract(t *testing.T) { invokeHostFunctionOpJson, ok := clientInvokeOp.Embedded.Records[0].(operations.InvokeHostFunction) assert.True(t, ok) assert.Equal(t, invokeHostFunctionOpJson.Function, "HostFunctionTypeHostFunctionTypeUploadContractWasm") - } -func TestContractInvokeHostFunctionCreateContractByAddress(t *testing.T) { +func CaseContractInvokeHostFunctionCreateContractByAddress(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{ + "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, EnableSorobanRPC: true, }) @@ -103,6 +136,7 @@ func TestContractInvokeHostFunctionCreateContractByAddress(t *testing.T) { clientTx, err := itest.Client().TransactionDetail(tx.Hash) require.NoError(t, err) + verifySorobanMeta(t, clientTx) assert.Equal(t, tx.Hash, clientTx.Hash) var txResult xdr.TransactionResult @@ -128,13 +162,15 @@ func TestContractInvokeHostFunctionCreateContractByAddress(t *testing.T) { assert.Equal(t, invokeHostFunctionOpJson.Salt, "110986164698320180327942133831752629430491002266485370052238869825166557303060") } -func TestContractInvokeHostFunctionInvokeStatelessContractFn(t *testing.T) { +func CaseContractInvokeHostFunctionInvokeStatelessContractFn(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{ + "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, EnableSorobanRPC: true, }) @@ -196,6 +232,7 @@ func TestContractInvokeHostFunctionInvokeStatelessContractFn(t *testing.T) { clientTx, err := itest.Client().TransactionDetail(tx.Hash) require.NoError(t, err) + verifySorobanMeta(t, clientTx) assert.Equal(t, tx.Hash, clientTx.Hash) var txResult xdr.TransactionResult @@ -209,12 +246,14 @@ func TestContractInvokeHostFunctionInvokeStatelessContractFn(t *testing.T) { assert.True(t, ok) assert.Equal(t, invokeHostFunctionResult.Code, xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess) - // check the function response, should have summed the two input numbers - invokeResult := xdr.Uint64(9) - expectedScVal := xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &invokeResult} - var transactionMeta xdr.TransactionMeta - assert.NoError(t, xdr.SafeUnmarshalBase64(tx.ResultMetaXdr, &transactionMeta)) - assert.True(t, expectedScVal.Equals(transactionMeta.V3.SorobanMeta.ReturnValue)) + if !DisabledSoroban { + // check the function response, should have summed the two input numbers + invokeResult := xdr.Uint64(9) + expectedScVal := xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &invokeResult} + var transactionMeta xdr.TransactionMeta + assert.NoError(t, xdr.SafeUnmarshalBase64(tx.ResultMetaXdr, &transactionMeta)) + assert.True(t, expectedScVal.Equals(transactionMeta.V3.SorobanMeta.ReturnValue)) + } clientInvokeOp, err := itest.Client().Operations(horizonclient.OperationRequest{ ForTransaction: tx.Hash, @@ -237,13 +276,15 @@ func TestContractInvokeHostFunctionInvokeStatelessContractFn(t *testing.T) { assert.Equal(t, invokeHostFunctionOpJson.Parameters[3].Type, "U64") } -func TestContractInvokeHostFunctionInvokeStatefulContractFn(t *testing.T) { +func CaseContractInvokeHostFunctionInvokeStatefulContractFn(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{ + "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, EnableSorobanRPC: true, }) @@ -292,6 +333,7 @@ func TestContractInvokeHostFunctionInvokeStatefulContractFn(t *testing.T) { clientTx, err := itest.Client().TransactionDetail(tx.Hash) require.NoError(t, err) + verifySorobanMeta(t, clientTx) assert.Equal(t, tx.Hash, clientTx.Hash) var txResult xdr.TransactionResult @@ -305,12 +347,14 @@ func TestContractInvokeHostFunctionInvokeStatefulContractFn(t *testing.T) { assert.True(t, ok) assert.Equal(t, invokeHostFunctionResult.Code, xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess) - // check the function response, should have incremented state from 0 to 1 - invokeResult := xdr.Uint32(1) - expectedScVal := xdr.ScVal{Type: xdr.ScValTypeScvU32, U32: &invokeResult} - var transactionMeta xdr.TransactionMeta - assert.NoError(t, xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &transactionMeta)) - assert.True(t, expectedScVal.Equals(transactionMeta.V3.SorobanMeta.ReturnValue)) + if !DisabledSoroban { + // check the function response, should have incremented state from 0 to 1 + invokeResult := xdr.Uint32(1) + expectedScVal := xdr.ScVal{Type: xdr.ScValTypeScvU32, U32: &invokeResult} + var transactionMeta xdr.TransactionMeta + assert.NoError(t, xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &transactionMeta)) + assert.True(t, expectedScVal.Equals(transactionMeta.V3.SorobanMeta.ReturnValue)) + } clientInvokeOp, err := itest.Client().Operations(horizonclient.OperationRequest{ ForTransaction: tx.Hash, @@ -384,3 +428,20 @@ func assembleCreateContractOp(t *testing.T, sourceAccount string, wasmFileName s SourceAccount: sourceAccount, } } + +func verifySorobanMeta(t *testing.T, clientTx horizon.Transaction) { + var txMeta xdr.TransactionMeta + err := xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &txMeta) + require.NoError(t, err) + require.NotNil(t, txMeta.V3) + + if !DisabledSoroban { + require.NotNil(t, txMeta.V3.SorobanMeta) + return + } + + require.Empty(t, txMeta.V3.Operations) + require.Empty(t, txMeta.V3.TxChangesAfter) + require.Empty(t, txMeta.V3.TxChangesBefore) + require.Nil(t, txMeta.V3.SorobanMeta) +} diff --git a/services/horizon/internal/integration/sac_test.go b/services/horizon/internal/integration/sac_test.go index 64c772b44c..c790b5a54c 100644 --- a/services/horizon/internal/integration/sac_test.go +++ b/services/horizon/internal/integration/sac_test.go @@ -2,6 +2,7 @@ package integration import ( "context" + "fmt" "math" "math/big" "strings" @@ -30,19 +31,127 @@ const sac_contract = "soroban_sac_test.wasm" // of the integration tests. const LongTermTTL = 10000 +var ( + DisabledSoroban bool +) + +func TestSAC(t *testing.T) { + // first test contracts when soroban processing is enabled + DisabledSoroban = false + runAllSACTests(t) + // now test same contracts when soroban processing is disabled + DisabledSoroban = true + runAllSACTests(t) +} + +func runAllSACTests(t *testing.T) { + tests := []struct { + name string + fn func(*testing.T) + }{ + {"CaseContractMintToAccount", CaseContractMintToAccount}, + {"CaseContractMintToContract", CaseContractMintToContract}, + {"CaseExpirationAndRestoration", CaseExpirationAndRestoration}, + {"CaseContractTransferBetweenAccounts", CaseContractTransferBetweenAccounts}, + {"CaseContractTransferBetweenAccountAndContract", CaseContractTransferBetweenAccountAndContract}, + {"CaseContractTransferBetweenContracts", CaseContractTransferBetweenContracts}, + {"CaseContractBurnFromAccount", CaseContractBurnFromAccount}, + {"CaseContractBurnFromContract", CaseContractBurnFromContract}, + {"CaseContractClawbackFromAccount", CaseContractClawbackFromAccount}, + {"CaseContractClawbackFromContract", CaseContractClawbackFromContract}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("Soroban Processing Disabled = %v. ", DisabledSoroban)+tt.name, func(t *testing.T) { + tt.fn(t) + }) + } +} + // Tests use precompiled wasm bin files that are added to the testdata directory. // Refer to ./services/horizon/internal/integration/contracts/README.md on how to recompile // contract code if needed to new wasm. -func TestContractMintToAccount(t *testing.T) { +func createSAC(itest *integration.Test, asset xdr.Asset) { + invokeHostFunction := &txnbuild.InvokeHostFunction{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeCreateContract, + CreateContract: &xdr.CreateContractArgs{ + ContractIdPreimage: xdr.ContractIdPreimage{ + Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAsset, + FromAsset: &asset, + }, + Executable: xdr.ContractExecutable{ + Type: xdr.ContractExecutableTypeContractExecutableStellarAsset, + WasmHash: nil, + }, + }, + }, + SourceAccount: itest.Master().Address(), + } + _, _, preFlightOp := assertInvokeHostFnSucceeds(itest, itest.Master(), invokeHostFunction) + sourceAccount, extendTTLOp, minFee := itest.PreflightExtendExpiration( + itest.Master().Address(), + preFlightOp.Ext.SorobanData.Resources.Footprint.ReadWrite, + LongTermTTL, + ) + itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &extendTTLOp) +} + +func invokeStoreSet( + itest *integration.Test, + storeContractID xdr.Hash, + ledgerEntryData xdr.LedgerEntryData, +) *txnbuild.InvokeHostFunction { + key := ledgerEntryData.MustContractData().Key + val := ledgerEntryData.MustContractData().Val + return &txnbuild.InvokeHostFunction{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, + InvokeContract: &xdr.InvokeContractArgs{ + ContractAddress: contractIDParam(storeContractID), + FunctionName: "set", + Args: xdr.ScVec{ + key, + val, + }, + }, + }, + SourceAccount: itest.Master().Address(), + } +} + +func invokeStoreRemove( + itest *integration.Test, + storeContractID xdr.Hash, + ledgerKey xdr.LedgerKey, +) *txnbuild.InvokeHostFunction { + return &txnbuild.InvokeHostFunction{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, + InvokeContract: &xdr.InvokeContractArgs{ + ContractAddress: contractIDParam(storeContractID), + FunctionName: "remove", + Args: xdr.ScVec{ + ledgerKey.MustContractData().Key, + }, + }, + }, + SourceAccount: itest.Master().Address(), + } +} + +func CaseContractMintToAccount(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, - HorizonEnvironment: map[string]string{"INGEST_DISABLE_STATE_VERIFICATION": "true", "CONNECTION_TIMEOUT": "360000"}, - EnableSorobanRPC: true, + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{ + "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban), + }, + EnableSorobanRPC: true, }) issuer := itest.Master().Address() @@ -72,17 +181,22 @@ func TestContractMintToAccount(t *testing.T) { balanceContracts: big.NewInt(0), contractID: stellarAssetContractID(itest, asset), }) - - fx := getTxEffects(itest, mintTx, asset) - require.Len(t, fx, 1) - creditEffect := assertContainsEffect(t, fx, - effects.EffectAccountCredited)[0].(effects.AccountCredited) - assert.Equal(t, recipientKp.Address(), creditEffect.Account) - assert.Equal(t, issuer, creditEffect.Asset.Issuer) - assert.Equal(t, code, creditEffect.Asset.Code) - assert.Equal(t, "20.0000000", creditEffect.Amount) assertEventPayments(itest, mintTx, asset, "", recipient.GetAccountID(), "mint", "20.0000000") + if !DisabledSoroban { + fx := getTxEffects(itest, mintTx, asset) + require.Len(t, fx, 1) + creditEffect := assertContainsEffect(t, fx, + effects.EffectAccountCredited)[0].(effects.AccountCredited) + assert.Equal(t, recipientKp.Address(), creditEffect.Account) + assert.Equal(t, issuer, creditEffect.Asset.Issuer) + assert.Equal(t, code, creditEffect.Asset.Code) + assert.Equal(t, "20.0000000", creditEffect.Amount) + } else { + fx := getTxEffects(itest, mintTx, asset) + require.Len(t, fx, 0) + } + otherRecipientKp, otherRecipient := itest.CreateAccount("100") itest.MustEstablishTrustline(otherRecipientKp, otherRecipient, txnbuild.MustAssetFromXDR(asset)) @@ -94,12 +208,6 @@ func TestContractMintToAccount(t *testing.T) { ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("20")) assertContainsBalance(itest, otherRecipientKp, issuer, code, amount.MustParse("30")) - - fx = getTxEffects(itest, transferTx, asset) - assert.Len(t, fx, 2) - assertContainsEffect(t, fx, - effects.EffectAccountCredited, - effects.EffectAccountDebited) assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -111,41 +219,28 @@ func TestContractMintToAccount(t *testing.T) { balanceContracts: big.NewInt(0), contractID: stellarAssetContractID(itest, asset), }) -} -func createSAC(itest *integration.Test, asset xdr.Asset) { - invokeHostFunction := &txnbuild.InvokeHostFunction{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeCreateContract, - CreateContract: &xdr.CreateContractArgs{ - ContractIdPreimage: xdr.ContractIdPreimage{ - Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAsset, - FromAsset: &asset, - }, - Executable: xdr.ContractExecutable{ - Type: xdr.ContractExecutableTypeContractExecutableStellarAsset, - WasmHash: nil, - }, - }, - }, - SourceAccount: itest.Master().Address(), + if !DisabledSoroban { + fx := getTxEffects(itest, transferTx, asset) + assert.Len(t, fx, 2) + assertContainsEffect(t, fx, + effects.EffectAccountCredited, + effects.EffectAccountDebited) + } else { + fx := getTxEffects(itest, transferTx, asset) + require.Len(t, fx, 0) } - _, _, preFlightOp := assertInvokeHostFnSucceeds(itest, itest.Master(), invokeHostFunction) - sourceAccount, extendTTLOp, minFee := itest.PreflightExtendExpiration( - itest.Master().Address(), - preFlightOp.Ext.SorobanData.Resources.Footprint.ReadWrite, - LongTermTTL, - ) - itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &extendTTLOp) } -func TestContractMintToContract(t *testing.T) { +func CaseContractMintToContract(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{ + "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, EnableSorobanRPC: true, }) @@ -170,19 +265,25 @@ func TestContractMintToContract(t *testing.T) { i128Param(int64(mintAmount.Hi), uint64(mintAmount.Lo)), contractAddressParam(recipientContractID)), ) - assertContainsEffect(t, getTxEffects(itest, mintTx, asset), - effects.EffectContractCredited) - balanceAmount, _, _ := assertInvokeHostFnSucceeds( - itest, - itest.Master(), - contractBalance(itest, issuer, asset, recipientContractID), - ) - assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) - assert.Equal(itest.CurrentTest(), xdr.Uint64(math.MaxUint64-3), (*balanceAmount.I128).Lo) - assert.Equal(itest.CurrentTest(), xdr.Int64(math.MaxInt64), (*balanceAmount.I128).Hi) assertEventPayments(itest, mintTx, asset, "", strkeyRecipientContractID, "mint", amount.String128(mintAmount)) + if !DisabledSoroban { + assertContainsEffect(t, getTxEffects(itest, mintTx, asset), + effects.EffectContractCredited) + + balanceAmount, _, _ := assertInvokeHostFnSucceeds( + itest, + itest.Master(), + contractBalance(itest, issuer, asset, recipientContractID), + ) + assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) + assert.Equal(itest.CurrentTest(), xdr.Uint64(math.MaxUint64-3), (*balanceAmount.I128).Lo) + assert.Equal(itest.CurrentTest(), xdr.Int64(math.MaxInt64), (*balanceAmount.I128).Hi) + } else { + fx := getTxEffects(itest, mintTx, asset) + require.Len(t, fx, 0) + } // calling transfer from the issuer account will also mint the asset _, transferTx, _ := assertInvokeHostFnSucceeds( itest, @@ -190,19 +291,6 @@ func TestContractMintToContract(t *testing.T) { transferWithAmount(itest, issuer, asset, i128Param(0, 3), contractAddressParam(recipientContractID)), ) - assertContainsEffect(t, getTxEffects(itest, transferTx, asset), - effects.EffectAccountDebited, - effects.EffectContractCredited) - - balanceAmount, _, _ = assertInvokeHostFnSucceeds( - itest, - itest.Master(), - contractBalance(itest, issuer, asset, recipientContractID), - ) - assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) - assert.Equal(itest.CurrentTest(), xdr.Uint64(math.MaxUint64), (*balanceAmount.I128).Lo) - assert.Equal(itest.CurrentTest(), xdr.Int64(math.MaxInt64), (*balanceAmount.I128).Hi) - // 2^127 - 1 balanceContracts := new(big.Int).Lsh(big.NewInt(1), 127) balanceContracts.Sub(balanceContracts, big.NewInt(1)) @@ -217,9 +305,27 @@ func TestContractMintToContract(t *testing.T) { balanceContracts: balanceContracts, contractID: stellarAssetContractID(itest, asset), }) + + if !DisabledSoroban { + assertContainsEffect(t, getTxEffects(itest, transferTx, asset), + effects.EffectAccountDebited, + effects.EffectContractCredited) + + balanceAmount, _, _ := assertInvokeHostFnSucceeds( + itest, + itest.Master(), + contractBalance(itest, issuer, asset, recipientContractID), + ) + assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) + assert.Equal(itest.CurrentTest(), xdr.Uint64(math.MaxUint64), (*balanceAmount.I128).Lo) + assert.Equal(itest.CurrentTest(), xdr.Int64(math.MaxInt64), (*balanceAmount.I128).Hi) + } else { + fx := getTxEffects(itest, transferTx, asset) + require.Len(t, fx, 0) + } } -func TestExpirationAndRestoration(t *testing.T) { +func CaseExpirationAndRestoration(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } @@ -232,6 +338,7 @@ func TestExpirationAndRestoration(t *testing.T) { // a fake asset contract in the horizon db and we don't // want state verification to detect this "ingest-disable-state-verification": "true", + "disable-soroban-ingest": fmt.Sprint(DisabledSoroban), }, }) @@ -294,6 +401,7 @@ func TestExpirationAndRestoration(t *testing.T) { LongTermTTL, ) itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &extendTTLOp) + assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -321,6 +429,16 @@ func TestExpirationAndRestoration(t *testing.T) { balanceToExpire, ), ) + + balanceToExpireLedgerKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.LedgerKeyContractData{ + Contract: balanceToExpire.ContractData.Contract, + Key: balanceToExpire.ContractData.Key, + Durability: balanceToExpire.ContractData.Durability, + }, + } + assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -333,14 +451,6 @@ func TestExpirationAndRestoration(t *testing.T) { contractID: storeContractID, }) - balanceToExpireLedgerKey := xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.LedgerKeyContractData{ - Contract: balanceToExpire.ContractData.Contract, - Key: balanceToExpire.ContractData.Key, - Durability: balanceToExpire.ContractData.Durability, - }, - } // The TESTING_MINIMUM_PERSISTENT_ENTRY_LIFETIME=10 configuration in stellar-core // will ensure that the ledger entry expires after 10 ledgers. // Because ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING is set to true, 10 ledgers @@ -372,6 +482,7 @@ func TestExpirationAndRestoration(t *testing.T) { ), ), ) + assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -390,6 +501,7 @@ func TestExpirationAndRestoration(t *testing.T) { balanceToExpireLedgerKey, ) itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &restoreFootprint) + assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -419,6 +531,7 @@ func TestExpirationAndRestoration(t *testing.T) { ), ), ) + assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -444,6 +557,7 @@ func TestExpirationAndRestoration(t *testing.T) { ), ), ) + assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -457,56 +571,15 @@ func TestExpirationAndRestoration(t *testing.T) { }) } -func invokeStoreSet( - itest *integration.Test, - storeContractID xdr.Hash, - ledgerEntryData xdr.LedgerEntryData, -) *txnbuild.InvokeHostFunction { - key := ledgerEntryData.MustContractData().Key - val := ledgerEntryData.MustContractData().Val - return &txnbuild.InvokeHostFunction{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, - InvokeContract: &xdr.InvokeContractArgs{ - ContractAddress: contractIDParam(storeContractID), - FunctionName: "set", - Args: xdr.ScVec{ - key, - val, - }, - }, - }, - SourceAccount: itest.Master().Address(), - } -} - -func invokeStoreRemove( - itest *integration.Test, - storeContractID xdr.Hash, - ledgerKey xdr.LedgerKey, -) *txnbuild.InvokeHostFunction { - return &txnbuild.InvokeHostFunction{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, - InvokeContract: &xdr.InvokeContractArgs{ - ContractAddress: contractIDParam(storeContractID), - FunctionName: "remove", - Args: xdr.ScVec{ - ledgerKey.MustContractData().Key, - }, - }, - }, - SourceAccount: itest.Master().Address(), - } -} - -func TestContractTransferBetweenAccounts(t *testing.T) { +func CaseContractTransferBetweenAccounts(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{ + "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, EnableSorobanRPC: true, }) @@ -534,6 +607,7 @@ func TestContractTransferBetweenAccounts(t *testing.T) { ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1000")) + assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -557,10 +631,6 @@ func TestContractTransferBetweenAccounts(t *testing.T) { assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("970")) assertContainsBalance(itest, otherRecipientKp, issuer, code, amount.MustParse("30")) - - fx := getTxEffects(itest, transferTx, asset) - assert.NotEmpty(t, fx) - assertContainsEffect(t, fx, effects.EffectAccountCredited, effects.EffectAccountDebited) assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -573,15 +643,26 @@ func TestContractTransferBetweenAccounts(t *testing.T) { contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, transferTx, asset, recipientKp.Address(), otherRecipient.GetAccountID(), "transfer", "30.0000000") + + if !DisabledSoroban { + fx := getTxEffects(itest, transferTx, asset) + assert.NotEmpty(t, fx) + assertContainsEffect(t, fx, effects.EffectAccountCredited, effects.EffectAccountDebited) + } else { + fx := getTxEffects(itest, transferTx, asset) + require.Len(t, fx, 0) + } } -func TestContractTransferBetweenAccountAndContract(t *testing.T) { +func CaseContractTransferBetweenAccountAndContract(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{ + "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, EnableSorobanRPC: true, }) @@ -627,9 +708,6 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { mint(itest, issuer, asset, "1000", contractAddressParam(recipientContractID)), ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1000")) - assertContainsEffect(t, getTxEffects(itest, mintTx, asset), - effects.EffectContractCredited) - assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -642,6 +720,14 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { contractID: stellarAssetContractID(itest, asset), }) + if !DisabledSoroban { + assertContainsEffect(t, getTxEffects(itest, mintTx, asset), + effects.EffectContractCredited) + } else { + fx := getTxEffects(itest, mintTx, asset) + require.Len(t, fx, 0) + } + // transfer from account to contract _, transferTx, _ := assertInvokeHostFnSucceeds( itest, @@ -649,8 +735,6 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { transfer(itest, recipientKp.Address(), asset, "30", contractAddressParam(recipientContractID)), ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("970")) - assertContainsEffect(t, getTxEffects(itest, transferTx, asset), - effects.EffectAccountDebited, effects.EffectContractCredited) assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -664,14 +748,19 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { }) assertEventPayments(itest, transferTx, asset, recipientKp.Address(), strkeyRecipientContractID, "transfer", "30.0000000") + if !DisabledSoroban { + assertContainsEffect(t, getTxEffects(itest, transferTx, asset), + effects.EffectAccountDebited, effects.EffectContractCredited) + } else { + fx := getTxEffects(itest, transferTx, asset) + require.Len(t, fx, 0) + } // transfer from contract to account _, transferTx, _ = assertInvokeHostFnSucceeds( itest, recipientKp, transferFromContract(itest, recipientKp.Address(), asset, recipientContractID, recipientContractHash, "500", accountAddressParam(recipient.GetAccountID())), ) - assertContainsEffect(t, getTxEffects(itest, transferTx, asset), - effects.EffectContractDebited, effects.EffectAccountCredited) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1470")) assertAssetStats(itest, assetStats{ code: code, @@ -686,6 +775,13 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { }) assertEventPayments(itest, transferTx, asset, strkeyRecipientContractID, recipientKp.Address(), "transfer", "500.0000000") + if DisabledSoroban { + fx := getTxEffects(itest, transferTx, asset) + require.Len(t, fx, 0) + return + } + assertContainsEffect(t, getTxEffects(itest, transferTx, asset), + effects.EffectContractDebited, effects.EffectAccountCredited) balanceAmount, _, _ := assertInvokeHostFnSucceeds( itest, itest.Master(), @@ -696,13 +792,15 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { assert.Equal(itest.CurrentTest(), xdr.Int64(0), (*balanceAmount.I128).Hi) } -func TestContractTransferBetweenContracts(t *testing.T) { +func CaseContractTransferBetweenContracts(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{ + "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, EnableSorobanRPC: true, }) @@ -742,8 +840,28 @@ func TestContractTransferBetweenContracts(t *testing.T) { itest.Master(), transferFromContract(itest, issuer, asset, emitterContractID, emitterContractHash, "10", contractAddressParam(recipientContractID)), ) - assertContainsEffect(t, getTxEffects(itest, transferTx, asset), - effects.EffectContractCredited, effects.EffectContractDebited) + + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 2, + balanceContracts: big.NewInt(int64(amount.MustParse("1000"))), + contractID: stellarAssetContractID(itest, asset), + }) + assertEventPayments(itest, transferTx, asset, strkeyEmitterContractID, strkeyRecipientContractID, "transfer", "10.0000000") + + if !DisabledSoroban { + assertContainsEffect(t, getTxEffects(itest, transferTx, asset), + effects.EffectContractCredited, effects.EffectContractDebited) + } else { + fx := getTxEffects(itest, transferTx, asset) + require.Len(t, fx, 0) + return + } // Check balances of emitter and recipient emitterBalanceAmount, _, _ := assertInvokeHostFnSucceeds( @@ -763,28 +881,17 @@ func TestContractTransferBetweenContracts(t *testing.T) { assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, recipientBalanceAmount.Type) assert.Equal(itest.CurrentTest(), xdr.Uint64(100000000), (*recipientBalanceAmount.I128).Lo) assert.Equal(itest.CurrentTest(), xdr.Int64(0), (*recipientBalanceAmount.I128).Hi) - - assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 0, - balanceAccounts: 0, - balanceArchivedContracts: big.NewInt(0), - numArchivedContracts: 0, - numContracts: 2, - balanceContracts: big.NewInt(int64(amount.MustParse("1000"))), - contractID: stellarAssetContractID(itest, asset), - }) - assertEventPayments(itest, transferTx, asset, strkeyEmitterContractID, strkeyRecipientContractID, "transfer", "10.0000000") } -func TestContractBurnFromAccount(t *testing.T) { +func CaseContractBurnFromAccount(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{ + "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, EnableSorobanRPC: true, }) @@ -830,16 +937,6 @@ func TestContractBurnFromAccount(t *testing.T) { burn(itest, recipientKp.Address(), asset, "500"), ) - fx := getTxEffects(itest, burnTx, asset) - require.Len(t, fx, 1) - assetEffects := assertContainsEffect(t, fx, effects.EffectAccountDebited) - require.GreaterOrEqual(t, len(assetEffects), 1) - burnEffect := assetEffects[0].(effects.AccountDebited) - - assert.Equal(t, issuer, burnEffect.Asset.Issuer) - assert.Equal(t, code, burnEffect.Asset.Code) - assert.Equal(t, "500.0000000", burnEffect.Amount) - assert.Equal(t, recipientKp.Address(), burnEffect.Account) assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -852,15 +949,33 @@ func TestContractBurnFromAccount(t *testing.T) { contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, burnTx, asset, recipientKp.Address(), "", "burn", "500.0000000") + + if !DisabledSoroban { + fx := getTxEffects(itest, burnTx, asset) + require.Len(t, fx, 1) + assetEffects := assertContainsEffect(t, fx, effects.EffectAccountDebited) + require.GreaterOrEqual(t, len(assetEffects), 1) + burnEffect := assetEffects[0].(effects.AccountDebited) + + assert.Equal(t, issuer, burnEffect.Asset.Issuer) + assert.Equal(t, code, burnEffect.Asset.Code) + assert.Equal(t, "500.0000000", burnEffect.Amount) + assert.Equal(t, recipientKp.Address(), burnEffect.Account) + } else { + fx := getTxEffects(itest, burnTx, asset) + require.Len(t, fx, 0) + } } -func TestContractBurnFromContract(t *testing.T) { +func CaseContractBurnFromContract(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{ + "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, EnableSorobanRPC: true, }) @@ -895,19 +1010,6 @@ func TestContractBurnFromContract(t *testing.T) { burnSelf(itest, issuer, asset, recipientContractID, recipientContractHash, "10"), ) - balanceAmount, _, _ := assertInvokeHostFnSucceeds( - itest, - itest.Master(), - contractBalance(itest, issuer, asset, recipientContractID), - ) - - assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) - assert.Equal(itest.CurrentTest(), xdr.Uint64(9900000000), (*balanceAmount.I128).Lo) - assert.Equal(itest.CurrentTest(), xdr.Int64(0), (*balanceAmount.I128).Hi) - - assertContainsEffect(t, getTxEffects(itest, burnTx, asset), - effects.EffectContractDebited) - assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -920,15 +1022,35 @@ func TestContractBurnFromContract(t *testing.T) { contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, burnTx, asset, strkeyRecipientContractID, "", "burn", "10.0000000") + + if !DisabledSoroban { + balanceAmount, _, _ := assertInvokeHostFnSucceeds( + itest, + itest.Master(), + contractBalance(itest, issuer, asset, recipientContractID), + ) + + assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) + assert.Equal(itest.CurrentTest(), xdr.Uint64(9900000000), (*balanceAmount.I128).Lo) + assert.Equal(itest.CurrentTest(), xdr.Int64(0), (*balanceAmount.I128).Hi) + + assertContainsEffect(t, getTxEffects(itest, burnTx, asset), + effects.EffectContractDebited) + } else { + fx := getTxEffects(itest, burnTx, asset) + require.Len(t, fx, 0) + } } -func TestContractClawbackFromAccount(t *testing.T) { +func CaseContractClawbackFromAccount(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{ + "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, EnableSorobanRPC: true, }) @@ -966,6 +1088,7 @@ func TestContractClawbackFromAccount(t *testing.T) { ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1000")) + assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -983,8 +1106,6 @@ func TestContractClawbackFromAccount(t *testing.T) { itest.Master(), clawback(itest, issuer, asset, "1000", accountAddressParam(recipientKp.Address())), ) - - assertContainsEffect(t, getTxEffects(itest, clawTx, asset), effects.EffectAccountDebited) assertContainsBalance(itest, recipientKp, issuer, code, 0) assertAssetStats(itest, assetStats{ code: code, @@ -998,15 +1119,24 @@ func TestContractClawbackFromAccount(t *testing.T) { contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, clawTx, asset, recipientKp.Address(), "", "clawback", "1000.0000000") + + if !DisabledSoroban { + assertContainsEffect(t, getTxEffects(itest, clawTx, asset), effects.EffectAccountDebited) + } else { + fx := getTxEffects(itest, clawTx, asset) + require.Len(t, fx, 0) + } } -func TestContractClawbackFromContract(t *testing.T) { +func CaseContractClawbackFromContract(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{ + "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, EnableSorobanRPC: true, }) @@ -1044,19 +1174,6 @@ func TestContractClawbackFromContract(t *testing.T) { itest.Master(), clawback(itest, issuer, asset, "10", contractAddressParam(recipientContractID)), ) - - balanceAmount, _, _ := assertInvokeHostFnSucceeds( - itest, - itest.Master(), - contractBalance(itest, issuer, asset, recipientContractID), - ) - assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) - assert.Equal(itest.CurrentTest(), xdr.Uint64(9900000000), (*balanceAmount.I128).Lo) - assert.Equal(itest.CurrentTest(), xdr.Int64(0), (*balanceAmount.I128).Hi) - - assertContainsEffect(t, getTxEffects(itest, clawTx, asset), - effects.EffectContractDebited) - assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -1069,6 +1186,23 @@ func TestContractClawbackFromContract(t *testing.T) { contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, clawTx, asset, strkeyRecipientContractID, "", "clawback", "10.0000000") + + if !DisabledSoroban { + balanceAmount, _, _ := assertInvokeHostFnSucceeds( + itest, + itest.Master(), + contractBalance(itest, issuer, asset, recipientContractID), + ) + assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) + assert.Equal(itest.CurrentTest(), xdr.Uint64(9900000000), (*balanceAmount.I128).Lo) + assert.Equal(itest.CurrentTest(), xdr.Int64(0), (*balanceAmount.I128).Hi) + + assertContainsEffect(t, getTxEffects(itest, clawTx, asset), + effects.EffectContractDebited) + } else { + fx := getTxEffects(itest, clawTx, asset) + require.Len(t, fx, 0) + } } func assertContainsBalance(itest *integration.Test, acct *keypair.Full, issuer, code string, amt xdr.Int64) { @@ -1179,6 +1313,12 @@ func assertEventPayments(itest *integration.Test, txHash string, asset xdr.Asset invokeHostFn := ops.Embedded.Records[0].(operations.InvokeHostFunction) assert.Equal(itest.CurrentTest(), invokeHostFn.Function, "HostFunctionTypeHostFunctionTypeInvokeContract") + + if DisabledSoroban { + require.Equal(itest.CurrentTest(), 0, len(invokeHostFn.AssetBalanceChanges)) + return + } + require.Equal(itest.CurrentTest(), 1, len(invokeHostFn.AssetBalanceChanges)) assetBalanceChange := invokeHostFn.AssetBalanceChanges[0] assert.Equal(itest.CurrentTest(), assetBalanceChange.Amount, amount) @@ -1400,10 +1540,6 @@ func assertInvokeHostFnSucceeds(itest *integration.Test, signer *keypair.Full, o err = xdr.SafeUnmarshalBase64(clientTx.ResultXdr, &txResult) require.NoError(itest.CurrentTest(), err) - var txMetaResult xdr.TransactionMeta - err = xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &txMetaResult) - require.NoError(itest.CurrentTest(), err) - opResults, ok := txResult.OperationResults() assert.True(itest.CurrentTest(), ok) assert.Equal(itest.CurrentTest(), len(opResults), 1) @@ -1411,9 +1547,18 @@ func assertInvokeHostFnSucceeds(itest *integration.Test, signer *keypair.Full, o assert.True(itest.CurrentTest(), ok) assert.Equal(itest.CurrentTest(), invokeHostFunctionResult.Code, xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess) - returnValue := txMetaResult.MustV3().SorobanMeta.ReturnValue + var returnValue *xdr.ScVal + + if !DisabledSoroban { + var txMetaResult xdr.TransactionMeta + err = xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &txMetaResult) + require.NoError(itest.CurrentTest(), err) + returnValue = &txMetaResult.MustV3().SorobanMeta.ReturnValue + } else { + verifySorobanMeta(itest.CurrentTest(), clientTx) + } - return &returnValue, clientTx.Hash, &preFlightOp + return returnValue, clientTx.Hash, &preFlightOp } func stellarAssetContractID(itest *integration.Test, asset xdr.Asset) xdr.Hash { From c003becfb4c1ed8ffbebad13582133c627b063f4 Mon Sep 17 00:00:00 2001 From: George Date: Wed, 24 Jan 2024 22:18:45 -0800 Subject: [PATCH 041/234] historyarchive: Improve existence checks and performance (#5179) --- historyarchive/archive_cache.go | 21 ++++++++++++++++++++- ingest/verify/main.go | 2 +- services/horizon/internal/ingest/verify.go | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/historyarchive/archive_cache.go b/historyarchive/archive_cache.go index a3990428b0..fa279fffd2 100644 --- a/historyarchive/archive_cache.go +++ b/historyarchive/archive_cache.go @@ -113,7 +113,26 @@ func (abc *ArchiveBucketCache) GetFile( } func (abc *ArchiveBucketCache) Exists(filepath string) bool { - return abc.lru.Contains(path.Join(abc.path, filepath)) + localPath := path.Join(abc.path, filepath) + + // First, check if the file exists in the cache. + if abc.lru.Contains(localPath) { + return true + } + + // If it doesn't, it may still exist on the disk which is still a cheaper + // check than going upstream. + // + // Note that this means the cache and disk are out of sync (perhaps due to + // other archives using the same cache location) so we can update it. This + // situation is well-handled by `GetFile`. + _, statErr := os.Stat(localPath) + if statErr == nil || os.IsExist(statErr) { + abc.lru.Add(localPath, struct{}{}) + return true + } + + return false } // Close purges the cache and cleans up the filesystem. diff --git a/ingest/verify/main.go b/ingest/verify/main.go index 4b97ffc2f7..6110448723 100644 --- a/ingest/verify/main.go +++ b/ingest/verify/main.go @@ -66,7 +66,7 @@ func (v *StateVerifier) GetLedgerEntries(count int) ([]xdr.LedgerEntry, error) { } entries := make([]xdr.LedgerEntry, 0, count) - v.currentEntries = make(map[string]xdr.LedgerEntry) + v.currentEntries = make(map[string]xdr.LedgerEntry, count) for count > 0 { entryChange, err := v.stateReader.Read() diff --git a/services/horizon/internal/ingest/verify.go b/services/horizon/internal/ingest/verify.go index bf1ddbe5b5..41b0eb98c5 100644 --- a/services/horizon/internal/ingest/verify.go +++ b/services/horizon/internal/ingest/verify.go @@ -157,8 +157,8 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { } } } - log.WithField("duration", duration).Info("State verification finished") + localLog.WithField("duration", duration).Info("State verification finished") }() localLog.Info("Creating state reader...") From 8dcfaa9b21739f37d8ff1b517c55ea447f1315e7 Mon Sep 17 00:00:00 2001 From: shawn Date: Fri, 26 Jan 2024 13:08:43 -0800 Subject: [PATCH 042/234] update 2.28.0 changelog, captive core cursor removal notes (#5181) --- services/horizon/CHANGELOG.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 6c9ca1b69f..b63160fef7 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -8,7 +8,7 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ## 2.28.0 ### Fixed -- Ingestion performance timing is improved ([4909](https://github.com/stellar/go/issues/4909)) +- Ingestion performance improvements ([4909](https://github.com/stellar/go/issues/4909)) - Trade aggregation rebuild errors reported on `db reingest range` with parallel workers ([5168](https://github.com/stellar/go/pull/5168)) - Limited global flags displayed on cli help output ([5077](https://github.com/stellar/go/pull/5077)) - Network usage has been significantly reduced with caching. **Warning:** To support the cache, disk requirements may increase by up to 15GB ([5171](https://github.com/stellar/go/pull/5171)). @@ -24,7 +24,19 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). * API `Operation` model for `InvokeHostFunctionOp` type, will have empty `asset_balance_changes` ### Breaking Changes -- Removed configuration flags `--stellar-core-url-db`, `--cursor-name` `--skip-cursor-update` , they were related to legacy non-captive core ingestion and are no longer usable. +- Deprecation of legacy, non-captive core ingestion([5158](https://github.com/stellar/go/pull/5158)): + * removed configuration flags `--stellar-core-url-db`, `--cursor-name` `--skip-cursor-update`, they are no longer usable. + * removed automatic updating of core cursor from ingestion background processing. + * Note for upgrading on existing horizon deployments - Since horizon will no longer maintain advancement of this cursor on core, it may require manual removal of the cursor from the core process that your horizon was using for captive core, otherwise that core process may un-necessarily retain older data in buckets on disk up to the last cursor ledger sequence set by prior horizon release. + + The captive core process to check and verify presence of cursor usage is determined by the horizon deployment, if `NETWORK` is present, or `STELLAR_CORE_URL` is present or `CAPTIVE-CORE-HTTP-PORT` is present and set to non-zero value, or `CAPTIVE-CORE_CONFIG_PATH` is used and the toml has `HTTP_PORT` set to non-zero and `PUBLIC_HTTP_PORT` is not set to false, then it is recommended to perform the following preventative measure on the machine hosting horizon after upgraded to 2.28.0 and process restarted: + ``` + $ curl http:///getcursor + 2. # If there are no cursors reported, done, no need for any action + 3. # If any horizon cursors exist they need to be dropped by id. By default horizon sets cursor id to "HORIZON" but if it was customised using the --cursor-name flag the id might be different + $ curl http:///dropcursor?id= + ``` + ## 2.27.0 From b84f1264fa8ca9841044d01ae74ad2148bec2dfe Mon Sep 17 00:00:00 2001 From: Shawn Reuland Date: Fri, 26 Jan 2024 13:26:31 -0800 Subject: [PATCH 043/234] clean up markdown on 2.28.0 release notes --- services/horizon/CHANGELOG.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index b63160fef7..13bcbe92b2 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -26,14 +26,16 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ### Breaking Changes - Deprecation of legacy, non-captive core ingestion([5158](https://github.com/stellar/go/pull/5158)): * removed configuration flags `--stellar-core-url-db`, `--cursor-name` `--skip-cursor-update`, they are no longer usable. - * removed automatic updating of core cursor from ingestion background processing. - * Note for upgrading on existing horizon deployments - Since horizon will no longer maintain advancement of this cursor on core, it may require manual removal of the cursor from the core process that your horizon was using for captive core, otherwise that core process may un-necessarily retain older data in buckets on disk up to the last cursor ledger sequence set by prior horizon release. + * removed automatic updating of core cursor from ingestion background processing.
+ **Note** for upgrading on existing horizon deployments - Since horizon will no longer maintain advancement of this cursor on core, it may require manual removal of the cursor from the core process that your horizon was using for captive core, otherwise that core process may un-necessarily retain older data in buckets on disk up to the last cursor ledger sequence set by prior horizon release. The captive core process to check and verify presence of cursor usage is determined by the horizon deployment, if `NETWORK` is present, or `STELLAR_CORE_URL` is present or `CAPTIVE-CORE-HTTP-PORT` is present and set to non-zero value, or `CAPTIVE-CORE_CONFIG_PATH` is used and the toml has `HTTP_PORT` set to non-zero and `PUBLIC_HTTP_PORT` is not set to false, then it is recommended to perform the following preventative measure on the machine hosting horizon after upgraded to 2.28.0 and process restarted: ``` $ curl http:///getcursor - 2. # If there are no cursors reported, done, no need for any action - 3. # If any horizon cursors exist they need to be dropped by id. By default horizon sets cursor id to "HORIZON" but if it was customised using the --cursor-name flag the id might be different + # If there are no cursors reported, done, no need for any action + # If any horizon cursors exist they need to be dropped by id. + # By default horizon sets cursor id to "HORIZON" but if it was customized + # using the --cursor-name flag the id might be different $ curl http:///dropcursor?id= ``` From 8338a1c01f3604d6cf946467319f2d11cbd59dc2 Mon Sep 17 00:00:00 2001 From: Shawn Reuland Date: Fri, 26 Jan 2024 19:18:37 -0800 Subject: [PATCH 044/234] fixed some lingering merge details from 2.28.0 to master --- historyarchive/archive.go | 26 ++++++++---------------- historyarchive/archive_cache.go | 3 ++- historyarchive/archive_test.go | 20 ++++++++++-------- services/horizon/internal/ingest/main.go | 2 +- 4 files changed, 23 insertions(+), 28 deletions(-) diff --git a/historyarchive/archive.go b/historyarchive/archive.go index e7c01722c5..e6a75b69bd 100644 --- a/historyarchive/archive.go +++ b/historyarchive/archive.go @@ -56,15 +56,6 @@ type Ledger struct { TransactionResult xdr.TransactionHistoryResultEntry } -type ArchiveBackend interface { - Exists(path string) (bool, error) - Size(path string) (int64, error) - GetFile(path string) (io.ReadCloser, error) - PutFile(path string, in io.ReadCloser) error - ListFiles(path string) (chan string, chan error) - CanListFiles() bool -} - type ArchiveInterface interface { GetPathHAS(path string) (HistoryArchiveState, error) PutPathHAS(path string, has HistoryArchiveState, opts *CommandOptions) error @@ -112,8 +103,8 @@ type Archive struct { checkpointManager CheckpointManager - backend ArchiveBackend - cache *ArchiveBucketCache + backend storage.Storage + cache *ArchiveBucketCache stats archiveStats } @@ -443,11 +434,11 @@ func Connect(u string, opts ArchiveOptions) (*Archive, error) { var err error arch.backend, err = ConnectBackend(u, opts.ConnectOptions) - if (err != nil) { + if err != nil { return &arch, err } - if opts.CacheConfig.Cache { + if opts.CacheConfig.Cache { cache, innerErr := MakeArchiveBucketCache(opts.CacheConfig) if innerErr != nil { return &arch, innerErr @@ -456,8 +447,7 @@ func Connect(u string, opts ArchiveOptions) (*Archive, error) { arch.cache = cache } - parsed, err := url.Parse(u) - arch.stats = archiveStats{backendName: parsed.String()} + arch.stats = archiveStats{backendName: u} return &arch, nil } @@ -466,19 +456,21 @@ func ConnectBackend(u string, opts storage.ConnectOptions) (storage.Storage, err return nil, errors.New("URL is empty") } + var err error parsed, err := url.Parse(u) if err != nil { return nil, err } var backend storage.Storage + if parsed.Scheme == "mock" { backend = makeMockBackend() } else { backend, err = storage.ConnectBackend(u, opts) } - return backend, nil + return backend, err } func MustConnect(u string, opts ArchiveOptions) *Archive { @@ -487,4 +479,4 @@ func MustConnect(u string, opts ArchiveOptions) *Archive { log.Fatal(err) } return arch -} \ No newline at end of file +} diff --git a/historyarchive/archive_cache.go b/historyarchive/archive_cache.go index fa279fffd2..50b15b958c 100644 --- a/historyarchive/archive_cache.go +++ b/historyarchive/archive_cache.go @@ -7,6 +7,7 @@ import ( lru "github.com/hashicorp/golang-lru" log "github.com/stellar/go/support/log" + "github.com/stellar/go/support/storage" ) type CacheOptions struct { @@ -61,7 +62,7 @@ func MakeArchiveBucketCache(opts CacheOptions) (*ArchiveBucketCache, error) { // cache, and any error. func (abc *ArchiveBucketCache) GetFile( filepath string, - upstream ArchiveBackend, + upstream storage.Storage, ) (io.ReadCloser, bool, error) { L := abc.log.WithField("key", filepath) localPath := path.Join(abc.path, filepath) diff --git a/historyarchive/archive_test.go b/historyarchive/archive_test.go index cb337bb499..de34c36f68 100644 --- a/historyarchive/archive_test.go +++ b/historyarchive/archive_test.go @@ -49,13 +49,13 @@ func GetTestS3Archive() *Archive { } func GetTestMockArchive() *Archive { - return MustConnect("mock://test", - ArchiveOptions{CheckpointFrequency: 64, - CacheConfig: CacheOptions{ - Cache: true, - Path: filepath.Join(os.TempDir(), "history-archive-test-cache"), - MaxFiles: 5, - }}) + return MustConnect("mock://test", + ArchiveOptions{CheckpointFrequency: 64, + CacheConfig: CacheOptions{ + Cache: true, + Path: filepath.Join(os.TempDir(), "history-archive-test-cache"), + MaxFiles: 5, + }}) } var tmpdirs []string @@ -200,8 +200,10 @@ func TestConfiguresHttpUserAgent(t *testing.T) { })) defer server.Close() - archive, err := Connect(server.URL, ConnectOptions{ - UserAgent: "uatest", + archive, err := Connect(server.URL, ArchiveOptions{ + ConnectOptions: storage.ConnectOptions{ + UserAgent: "uatest", + }, }) assert.NoError(t, err) diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 556724bde1..7d9596db94 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -230,7 +230,7 @@ func NewSystem(config Config) (System, error) { CheckpointFrequency: config.CheckpointFrequency, ConnectOptions: storage.ConnectOptions{ Context: ctx, - UserAgent: fmt.Sprintf("horizon/%s golang/%s", apkg.Version(), runtime.Version()),}, + UserAgent: fmt.Sprintf("horizon/%s golang/%s", apkg.Version(), runtime.Version())}, CacheConfig: historyarchive.CacheOptions{ Cache: true, Path: path.Join(config.CaptiveCoreStoragePath, "bucket-cache"), From 98d54d1623e2bd3f1370061b40fb04aa53dcc03c Mon Sep 17 00:00:00 2001 From: Shawn Reuland Date: Mon, 29 Jan 2024 12:20:26 -0800 Subject: [PATCH 045/234] #5163: cleanup archive pool, when get stats from archive instances, accept all, not just first only --- historyarchive/archive_pool.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/historyarchive/archive_pool.go b/historyarchive/archive_pool.go index e38437caea..4cb5483f63 100644 --- a/historyarchive/archive_pool.go +++ b/historyarchive/archive_pool.go @@ -54,9 +54,7 @@ func NewArchivePool(archiveURLs []string, opts ArchiveOptions) (ArchivePool, err func (pa ArchivePool) GetStats() []ArchiveStats { stats := []ArchiveStats{} for _, archive := range pa { - if len(archive.GetStats()) == 1 { - stats = append(stats, archive.GetStats()[0]) - } + stats = append(stats, archive.GetStats()...) } return stats } From 2df28100a1c5d92ce8f2fb28b5e65afda94dde38 Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 1 Feb 2024 09:28:25 -0800 Subject: [PATCH 046/234] services/horizon/ingest: added more description to the history archive metrics labels (#5185) --- historyarchive/archive.go | 3 ++- services/horizon/internal/ingest/main.go | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/historyarchive/archive.go b/historyarchive/archive.go index e6a75b69bd..ed05a4130d 100644 --- a/historyarchive/archive.go +++ b/historyarchive/archive.go @@ -119,7 +119,8 @@ func (arch *Archive) GetCheckpointManager() CheckpointManager { func (a *Archive) GetPathHAS(path string) (HistoryArchiveState, error) { var has HistoryArchiveState rdr, err := a.backend.GetFile(path) - a.stats.incrementDownloads() + // this is a query on the HA server state, not a data/bucket file download + a.stats.incrementRequests() if err != nil { return has, err } diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 7d9596db94..2726e02484 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -409,7 +409,13 @@ func (s *system) initMetrics() { s.metrics.HistoryArchiveStatsCounter = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: "horizon", Subsystem: "ingest", Name: "history_archive_stats_total", - Help: "counters of different history archive stats", + Help: "Counters of different history archive requests. " + + "'source' label will provide name/address of the physical history archive server from the pool for which a request may be sent. " + + "'type' label will further categorize the potential request into specific requests, " + + "'file_downloads' - the count of files downloaded from an archive server, " + + "'file_uploads' - the count of files uploaded to an archive server, " + + "'requests' - the count of all http requests(includes both queries and file downloads) sent to an archive server, " + + "'cache_hits' - the count of requests for an archive file that were found on local cache instead, no download request sent to archive server.", }, []string{"source", "type"}, ) From 2ea3c3dc43ee9318ffc53ef3440ed99c538c1a31 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Sat, 3 Feb 2024 00:13:37 -0500 Subject: [PATCH 047/234] Fix for transaction submission timeout (#5191) * Add check for ledger state in txsub * Add test for badSeq * Fix failing unittest * Update system_test.go * Small changes * Update main.go --- services/horizon/internal/init.go | 1 + ...eq_txsub_test.go => bad_seq_txsub_test.go} | 42 +++++++++ services/horizon/internal/ledger/main.go | 13 ++- .../horizon/internal/txsub/helpers_test.go | 32 +++++++ services/horizon/internal/txsub/system.go | 11 ++- .../horizon/internal/txsub/system_test.go | 93 +++++++++++++++++++ 6 files changed, 188 insertions(+), 4 deletions(-) rename services/horizon/internal/integration/{negative_seq_txsub_test.go => bad_seq_txsub_test.go} (63%) diff --git a/services/horizon/internal/init.go b/services/horizon/internal/init.go index 4078c7ad00..d4b34f9f4d 100644 --- a/services/horizon/internal/init.go +++ b/services/horizon/internal/init.go @@ -235,5 +235,6 @@ func initSubmissionSystem(app *App) { DB: func(ctx context.Context) txsub.HorizonDB { return &history.Q{SessionInterface: app.HorizonSession()} }, + LedgerState: app.ledgerState, } } diff --git a/services/horizon/internal/integration/negative_seq_txsub_test.go b/services/horizon/internal/integration/bad_seq_txsub_test.go similarity index 63% rename from services/horizon/internal/integration/negative_seq_txsub_test.go rename to services/horizon/internal/integration/bad_seq_txsub_test.go index 787ad0645c..2a5f9d13fe 100644 --- a/services/horizon/internal/integration/negative_seq_txsub_test.go +++ b/services/horizon/internal/integration/bad_seq_txsub_test.go @@ -71,3 +71,45 @@ func TestNegativeSequenceTxSubmission(t *testing.T) { tt.Equal("tx_bad_seq", codes.TransactionCode) } + +func TestBadSeqTxSubmission(t *testing.T) { + tt := assert.New(t) + itest := integration.NewTest(t, integration.Config{}) + master := itest.Master() + + account := itest.MasterAccount() + seqnum, err := account.GetSequenceNumber() + tt.NoError(err) + + op2 := txnbuild.Payment{ + Destination: master.Address(), + Amount: "10", + Asset: txnbuild.NativeAsset{}, + } + + // Submit a simple payment tx, but with a gapped sequence + // that is intentionally set more than one ahead of current account seq + // this should trigger a tx_bad_seq from core + account = &txnbuild.SimpleAccount{ + AccountID: account.GetAccountID(), + Sequence: seqnum + 10, + } + txParams := txnbuild.TransactionParams{ + SourceAccount: account, + Operations: []txnbuild.Operation{&op2}, + BaseFee: txnbuild.MinBaseFee, + Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewInfiniteTimeout()}, + IncrementSequenceNum: false, + } + tx, err := txnbuild.NewTransaction(txParams) + tt.NoError(err) + tx, err = tx.Sign(integration.StandaloneNetworkPassphrase, master) + tt.NoError(err) + _, err = itest.Client().SubmitTransaction(tx) + tt.Error(err) + clientErr, ok := err.(*horizonclient.Error) + tt.True(ok) + codes, err := clientErr.ResultCodes() + tt.NoError(err) + tt.Equal("tx_bad_seq", codes.TransactionCode) +} diff --git a/services/horizon/internal/ledger/main.go b/services/horizon/internal/ledger/main.go index 1d17e09d67..2101048bad 100644 --- a/services/horizon/internal/ledger/main.go +++ b/services/horizon/internal/ledger/main.go @@ -6,10 +6,9 @@ package ledger import ( + "github.com/prometheus/client_golang/prometheus" "sync" "time" - - "github.com/prometheus/client_golang/prometheus" ) // Status represents a snapshot of both horizon's and stellar-core's view of the @@ -31,7 +30,7 @@ type HorizonStatus struct { } // State is an in-memory data structure which holds a snapshot of both -// horizon's and stellar-core's view of the the network +// horizon's and stellar-core's view of the network type State struct { sync.RWMutex current Status @@ -44,6 +43,14 @@ type State struct { } } +type StateInterface interface { + CurrentStatus() Status + SetStatus(next Status) + SetCoreStatus(next CoreStatus) + SetHorizonStatus(next HorizonStatus) + RegisterMetrics(registry *prometheus.Registry) +} + // CurrentStatus returns the cached snapshot of ledger state func (c *State) CurrentStatus() Status { c.RLock() diff --git a/services/horizon/internal/txsub/helpers_test.go b/services/horizon/internal/txsub/helpers_test.go index 0e5a63bca7..3c4cb6cb0b 100644 --- a/services/horizon/internal/txsub/helpers_test.go +++ b/services/horizon/internal/txsub/helpers_test.go @@ -9,6 +9,8 @@ package txsub import ( "context" "database/sql" + "github.com/prometheus/client_golang/prometheus" + "github.com/stellar/go/services/horizon/internal/ledger" "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stretchr/testify/mock" @@ -72,3 +74,33 @@ func (m *mockDBQ) TransactionByHash(ctx context.Context, dest interface{}, hash args := m.Called(ctx, dest, hash) return args.Error(0) } + +type MockLedgerState struct { + mock.Mock +} + +// CurrentStatus mocks the CurrentStatus method. +func (m *MockLedgerState) CurrentStatus() ledger.Status { + args := m.Called() + return args.Get(0).(ledger.Status) +} + +// SetStatus mocks the SetStatus method. +func (m *MockLedgerState) SetStatus(next ledger.Status) { + m.Called(next) +} + +// SetCoreStatus mocks the SetCoreStatus method. +func (m *MockLedgerState) SetCoreStatus(next ledger.CoreStatus) { + m.Called(next) +} + +// SetHorizonStatus mocks the SetHorizonStatus method. +func (m *MockLedgerState) SetHorizonStatus(next ledger.HorizonStatus) { + m.Called(next) +} + +// RegisterMetrics mocks the RegisterMetrics method. +func (m *MockLedgerState) RegisterMetrics(registry *prometheus.Registry) { + m.Called(registry) +} diff --git a/services/horizon/internal/txsub/system.go b/services/horizon/internal/txsub/system.go index 189f1619ff..31038135f3 100644 --- a/services/horizon/internal/txsub/system.go +++ b/services/horizon/internal/txsub/system.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "github.com/stellar/go/services/horizon/internal/ledger" "sync" "time" @@ -40,6 +41,7 @@ type System struct { Submitter Submitter SubmissionTimeout time.Duration Log *log.Entry + LedgerState ledger.StateInterface Metrics struct { // SubmissionDuration exposes timing metrics about the rate and latency of @@ -190,7 +192,7 @@ func (sys *System) waitUntilAccountSequence(ctx context.Context, db HorizonDB, s WithField("sourceAddress", sourceAddress). Warn("missing sequence number for account") } - if num >= seq { + if num >= seq || sys.isSyncedUp() { return nil } } @@ -204,6 +206,13 @@ func (sys *System) waitUntilAccountSequence(ctx context.Context, db HorizonDB, s } } +// isSyncedUp Check if Horizon and Core have synced up: If yes, then no need to wait for account sequence +// and send txBAD_SEQ right away. +func (sys *System) isSyncedUp() bool { + currentStatus := sys.LedgerState.CurrentStatus() + return int(currentStatus.CoreLatest) <= int(currentStatus.HistoryLatest) +} + func (sys *System) deriveTxSubError(ctx context.Context) error { if ctx.Err() == context.Canceled { return ErrCanceled diff --git a/services/horizon/internal/txsub/system_test.go b/services/horizon/internal/txsub/system_test.go index 816cc28e66..b4a36fb522 100644 --- a/services/horizon/internal/txsub/system_test.go +++ b/services/horizon/internal/txsub/system_test.go @@ -6,6 +6,7 @@ import ( "context" "database/sql" "errors" + "github.com/stellar/go/services/horizon/internal/ledger" "testing" "time" @@ -155,6 +156,17 @@ func (suite *SystemTestSuite) TestTimeoutDuringSequenceLoop() { suite.db.On("GetSequenceNumbers", suite.ctx, []string{suite.unmuxedSource.Address()}). Return(map[string]uint64{suite.unmuxedSource.Address(): 0}, nil) + mockLedgerState := &MockLedgerState{} + mockLedgerState.On("CurrentStatus").Return(ledger.Status{ + CoreStatus: ledger.CoreStatus{ + CoreLatest: 3, + }, + HorizonStatus: ledger.HorizonStatus{ + HistoryLatest: 1, + }, + }).Twice() + suite.system.LedgerState = mockLedgerState + r := <-suite.system.Submit( suite.ctx, suite.successTx.Transaction.TxEnvelope, @@ -187,6 +199,17 @@ func (suite *SystemTestSuite) TestClientDisconnectedDuringSequenceLoop() { suite.db.On("GetSequenceNumbers", suite.ctx, []string{suite.unmuxedSource.Address()}). Return(map[string]uint64{suite.unmuxedSource.Address(): 0}, nil) + mockLedgerState := &MockLedgerState{} + mockLedgerState.On("CurrentStatus").Return(ledger.Status{ + CoreStatus: ledger.CoreStatus{ + CoreLatest: 3, + }, + HorizonStatus: ledger.HorizonStatus{ + HistoryLatest: 1, + }, + }).Once() + suite.system.LedgerState = mockLedgerState + r := <-suite.system.Submit( suite.ctx, suite.successTx.Transaction.TxEnvelope, @@ -253,6 +276,17 @@ func (suite *SystemTestSuite) TestSubmit_BadSeq() { }). Return(nil).Once() + mockLedgerState := &MockLedgerState{} + mockLedgerState.On("CurrentStatus").Return(ledger.Status{ + CoreStatus: ledger.CoreStatus{ + CoreLatest: 3, + }, + HorizonStatus: ledger.HorizonStatus{ + HistoryLatest: 1, + }, + }).Twice() + suite.system.LedgerState = mockLedgerState + r := <-suite.system.Submit( suite.ctx, suite.successTx.Transaction.TxEnvelope, @@ -281,6 +315,64 @@ func (suite *SystemTestSuite) TestSubmit_BadSeqNotFound() { Return(map[string]uint64{suite.unmuxedSource.Address(): 1}, nil). Once() + mockLedgerState := &MockLedgerState{} + mockLedgerState.On("CurrentStatus").Return(ledger.Status{ + CoreStatus: ledger.CoreStatus{ + CoreLatest: 3, + }, + HorizonStatus: ledger.HorizonStatus{ + HistoryLatest: 1, + }, + }).Times(3) + suite.system.LedgerState = mockLedgerState + + // set poll interval to 1ms so we don't need to wait 3 seconds for the test to complete + suite.system.Init() + suite.system.accountSeqPollInterval = time.Millisecond + + r := <-suite.system.Submit( + suite.ctx, + suite.successTx.Transaction.TxEnvelope, + suite.successXDR, + suite.successTx.Transaction.TransactionHash, + ) + + assert.NotNil(suite.T(), r.Err) + assert.True(suite.T(), suite.submitter.WasSubmittedTo) +} + +// If error is bad_seq and horizon and core are in sync, then return error +func (suite *SystemTestSuite) TestSubmit_BadSeqErrorWhenInSync() { + suite.submitter.R = suite.badSeq + suite.db.On("PreFilteredTransactionByHash", suite.ctx, mock.Anything, suite.successTx.Transaction.TransactionHash). + Return(sql.ErrNoRows).Twice() + suite.db.On("NoRows", sql.ErrNoRows).Return(true).Twice() + suite.db.On("TransactionByHash", suite.ctx, mock.Anything, suite.successTx.Transaction.TransactionHash). + Return(sql.ErrNoRows).Twice() + suite.db.On("NoRows", sql.ErrNoRows).Return(true).Twice() + suite.db.On("GetSequenceNumbers", suite.ctx, []string{suite.unmuxedSource.Address()}). + Return(map[string]uint64{suite.unmuxedSource.Address(): 0}, nil). + Twice() + + mockLedgerState := &MockLedgerState{} + mockLedgerState.On("CurrentStatus").Return(ledger.Status{ + CoreStatus: ledger.CoreStatus{ + CoreLatest: 3, + }, + HorizonStatus: ledger.HorizonStatus{ + HistoryLatest: 1, + }, + }).Once() + mockLedgerState.On("CurrentStatus").Return(ledger.Status{ + CoreStatus: ledger.CoreStatus{ + CoreLatest: 1, + }, + HorizonStatus: ledger.HorizonStatus{ + HistoryLatest: 1, + }, + }).Once() + suite.system.LedgerState = mockLedgerState + // set poll interval to 1ms so we don't need to wait 3 seconds for the test to complete suite.system.Init() suite.system.accountSeqPollInterval = time.Millisecond @@ -293,6 +385,7 @@ func (suite *SystemTestSuite) TestSubmit_BadSeqNotFound() { ) assert.NotNil(suite.T(), r.Err) + assert.Equal(suite.T(), r.Err.Error(), "tx failed: AAAAAAAAAAD////7AAAAAA==") // decodes to txBadSeq assert.True(suite.T(), suite.submitter.WasSubmittedTo) } From 283f3836fa62452fd9fd77c744ff2ae69ede5b9f Mon Sep 17 00:00:00 2001 From: Shawn Reuland Date: Sun, 4 Feb 2024 15:54:26 -0800 Subject: [PATCH 048/234] updated changelog notes --- services/horizon/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 13bcbe92b2..87dbf74097 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -5,6 +5,12 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +## 2.28.1 + +### Fixed +- Submitting transaction with a future gapped sequence number when horizon ingestion is lagging behind core, may result in delayed 60s timeout response ([5191](https://github.com/stellar/go/pull/5191)) + + ## 2.28.0 ### Fixed From 18a27d62c5859406ebef0e1ed47574d06fc7d03f Mon Sep 17 00:00:00 2001 From: Shawn Reuland Date: Sun, 4 Feb 2024 18:11:01 -0800 Subject: [PATCH 049/234] better description of txsub issue in notes --- services/horizon/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 87dbf74097..fc2b046a57 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -8,7 +8,7 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ## 2.28.1 ### Fixed -- Submitting transaction with a future gapped sequence number when horizon ingestion is lagging behind core, may result in delayed 60s timeout response ([5191](https://github.com/stellar/go/pull/5191)) +- Submitting transaction with a future gapped sequence number greater than 1 past current source account sequence, may result in delayed 60s timeout response, rather than expected HTTP 400 error response with `result_codes: {transaction: "tx_bad_seq"}` ([5191](https://github.com/stellar/go/pull/5191)) ## 2.28.0 From 520edbcc1d0669f1bdf3719bc65b9520999d67e1 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Mon, 5 Feb 2024 10:55:25 -0800 Subject: [PATCH 050/234] Bump dependencies for Soroban pubnet release (#5193) * Bump XDR definitions * Bump Core dependencies * Bump soroban RPC --- .github/workflows/horizon.yml | 6 +++--- Makefile | 17 ++++++++++------- gxdr/xdr_generated.go | 7 +++++-- xdr/Stellar-contract-config-setting.x | 5 ++++- xdr/xdr_commit_generated.txt | 2 +- xdr/xdr_generated.go | 20 ++++++++++++++++---- 6 files changed, 39 insertions(+), 18 deletions(-) diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index df314900d2..677a60b835 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -33,9 +33,9 @@ jobs: env: HORIZON_INTEGRATION_TESTS_ENABLED: true HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL: ${{ matrix.protocol-version }} - PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 20.1.0-1656.114b833e7.focal - PROTOCOL_20_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:20.1.0-1656.114b833e7.focal - PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:20.2.0 + PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 20.2.0-1716.rc3.34d82fc00.focal + PROTOCOL_20_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:20.2.0-1716.rc3.34d82fc00.focal + PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:20.3.0-rc1-52 PROTOCOL_19_CORE_DEBIAN_PKG_VERSION: 19.14.0-1500.5664eff4e.focal PROTOCOL_19_CORE_DOCKER_IMG: stellar/stellar-core:19.14.0-1500.5664eff4e.focal PGHOST: localhost diff --git a/Makefile b/Makefile index e07da4d9b8..abacb05dd8 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Docker build targets use an optional "TAG" environment # variable can be set to use custom tag name. For example: # TAG=my-registry.example.com/keystore:dev make keystore -XDRS = xdr/Stellar-SCP.x \ +DOWNLOADABLE_XDRS = xdr/Stellar-SCP.x \ xdr/Stellar-ledger-entries.x \ xdr/Stellar-ledger.x \ xdr/Stellar-overlay.x \ @@ -12,11 +12,14 @@ xdr/Stellar-contract-meta.x \ xdr/Stellar-contract-spec.x \ xdr/Stellar-contract.x \ xdr/Stellar-internal.x \ -xdr/Stellar-contract-config-setting.x \ -xdr/Stellar-lighthorizon.x +xdr/Stellar-contract-config-setting.x + +XDRS = $(DOWNLOADABLE_XDRS) xdr/Stellar-lighthorizon.x + + XDRGEN_COMMIT=e2cac557162d99b12ae73b846cf3d5bfe16636de -XDR_COMMIT=bb54e505f814386a3f45172e0b7e95b7badbe969 +XDR_COMMIT=b96148cd4acc372cc9af17b909ffe4b12c43ecb6 .PHONY: xdr xdr-clean xdr-update @@ -41,7 +44,7 @@ recoverysigner: regulated-assets-approval-server: $(MAKE) -C services/regulated-assets-approval-server/ docker-build -gxdr/xdr_generated.go: $(XDRS) +gxdr/xdr_generated.go: $(DOWNLOADABLE_XDRS) go run github.com/xdrpp/goxdr/cmd/goxdr -p gxdr -enum-comments -o $@ $(XDRS) gofmt -s -w $@ @@ -49,7 +52,7 @@ xdr/%.x: printf "%s" ${XDR_COMMIT} > xdr/xdr_commit_generated.txt curl -Lsf -o $@ https://raw.githubusercontent.com/stellar/stellar-xdr/$(XDR_COMMIT)/$(@F) -xdr/xdr_generated.go: $(XDRS) +xdr/xdr_generated.go: $(DOWNLOADABLE_XDRS) docker run -it --rm -v $$PWD:/wd -w /wd ruby /bin/bash -c '\ gem install specific_install -v 0.3.8 && \ gem specific_install https://github.com/stellar/xdrgen.git -b $(XDRGEN_COMMIT) && \ @@ -65,6 +68,6 @@ xdr/xdr_generated.go: $(XDRS) xdr: gxdr/xdr_generated.go xdr/xdr_generated.go xdr-clean: - rm xdr/*.x || true + rm $(DOWNLOADABLE_XDRS) || true xdr-update: xdr-clean xdr diff --git a/gxdr/xdr_generated.go b/gxdr/xdr_generated.go index b66e0a2c7c..9d9ab290bb 100644 --- a/gxdr/xdr_generated.go +++ b/gxdr/xdr_generated.go @@ -4341,8 +4341,10 @@ type StateArchivalSettings struct { MaxEntriesToArchive Uint32 // Number of snapshots to use when calculating average BucketList size BucketListSizeWindowSampleSize Uint32 + // How often to sample the BucketList size for the average, in ledgers + BucketListWindowSamplePeriod Uint32 // Maximum number of bytes that we scan for eviction per ledger - EvictionScanSize Uint64 + EvictionScanSize Uint32 // Lowest BucketList level to be scanned to evict entries StartingEvictionScanLevel Uint32 } @@ -28604,7 +28606,8 @@ func (v *StateArchivalSettings) XdrRecurse(x XDR, name string) { x.Marshal(x.Sprintf("%stempRentRateDenominator", name), XDR_Int64(&v.TempRentRateDenominator)) x.Marshal(x.Sprintf("%smaxEntriesToArchive", name), XDR_Uint32(&v.MaxEntriesToArchive)) x.Marshal(x.Sprintf("%sbucketListSizeWindowSampleSize", name), XDR_Uint32(&v.BucketListSizeWindowSampleSize)) - x.Marshal(x.Sprintf("%sevictionScanSize", name), XDR_Uint64(&v.EvictionScanSize)) + x.Marshal(x.Sprintf("%sbucketListWindowSamplePeriod", name), XDR_Uint32(&v.BucketListWindowSamplePeriod)) + x.Marshal(x.Sprintf("%sevictionScanSize", name), XDR_Uint32(&v.EvictionScanSize)) x.Marshal(x.Sprintf("%sstartingEvictionScanLevel", name), XDR_Uint32(&v.StartingEvictionScanLevel)) } func XDR_StateArchivalSettings(v *StateArchivalSettings) *StateArchivalSettings { return v } diff --git a/xdr/Stellar-contract-config-setting.x b/xdr/Stellar-contract-config-setting.x index b187a18c5a..6b5074735d 100644 --- a/xdr/Stellar-contract-config-setting.x +++ b/xdr/Stellar-contract-config-setting.x @@ -165,8 +165,11 @@ struct StateArchivalSettings { // Number of snapshots to use when calculating average BucketList size uint32 bucketListSizeWindowSampleSize; + // How often to sample the BucketList size for the average, in ledgers + uint32 bucketListWindowSamplePeriod; + // Maximum number of bytes that we scan for eviction per ledger - uint64 evictionScanSize; + uint32 evictionScanSize; // Lowest BucketList level to be scanned to evict entries uint32 startingEvictionScanLevel; diff --git a/xdr/xdr_commit_generated.txt b/xdr/xdr_commit_generated.txt index 9746cc5569..b7c3979571 100644 --- a/xdr/xdr_commit_generated.txt +++ b/xdr/xdr_commit_generated.txt @@ -1 +1 @@ -bb54e505f814386a3f45172e0b7e95b7badbe969 \ No newline at end of file +b96148cd4acc372cc9af17b909ffe4b12c43ecb6 \ No newline at end of file diff --git a/xdr/xdr_generated.go b/xdr/xdr_generated.go index e8ea1dc525..ac19618f61 100644 --- a/xdr/xdr_generated.go +++ b/xdr/xdr_generated.go @@ -33,7 +33,7 @@ import ( // XdrFilesSHA256 is the SHA256 hashes of source files. var XdrFilesSHA256 = map[string]string{ "xdr/Stellar-SCP.x": "8f32b04d008f8bc33b8843d075e69837231a673691ee41d8b821ca229a6e802a", - "xdr/Stellar-contract-config-setting.x": "e466c4dfae1d5d181afbd990b91f26c5d8ed84a7fa987875f8d643cf97e34a77", + "xdr/Stellar-contract-config-setting.x": "fc42980e8710514679477f767ecad6f9348c38d24b1e4476fdd7e73e8e672ea8", "xdr/Stellar-contract-env-meta.x": "928a30de814ee589bc1d2aadd8dd81c39f71b7e6f430f56974505ccb1f49654b", "xdr/Stellar-contract-meta.x": "f01532c11ca044e19d9f9f16fe373e9af64835da473be556b9a807ee3319ae0d", "xdr/Stellar-contract-spec.x": "c7ffa21d2e91afb8e666b33524d307955426ff553a486d670c29217ed9888d49", @@ -55300,8 +55300,11 @@ var _ xdrType = (*ContractCostParamEntry)(nil) // // Number of snapshots to use when calculating average BucketList size // uint32 bucketListSizeWindowSampleSize; // +// // How often to sample the BucketList size for the average, in ledgers +// uint32 bucketListWindowSamplePeriod; +// // // Maximum number of bytes that we scan for eviction per ledger -// uint64 evictionScanSize; +// uint32 evictionScanSize; // // // Lowest BucketList level to be scanned to evict entries // uint32 startingEvictionScanLevel; @@ -55314,7 +55317,8 @@ type StateArchivalSettings struct { TempRentRateDenominator Int64 MaxEntriesToArchive Uint32 BucketListSizeWindowSampleSize Uint32 - EvictionScanSize Uint64 + BucketListWindowSamplePeriod Uint32 + EvictionScanSize Uint32 StartingEvictionScanLevel Uint32 } @@ -55342,6 +55346,9 @@ func (s *StateArchivalSettings) EncodeTo(e *xdr.Encoder) error { if err = s.BucketListSizeWindowSampleSize.EncodeTo(e); err != nil { return err } + if err = s.BucketListWindowSamplePeriod.EncodeTo(e); err != nil { + return err + } if err = s.EvictionScanSize.EncodeTo(e); err != nil { return err } @@ -55396,10 +55403,15 @@ func (s *StateArchivalSettings) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, if err != nil { return n, fmt.Errorf("decoding Uint32: %w", err) } + nTmp, err = s.BucketListWindowSamplePeriod.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } nTmp, err = s.EvictionScanSize.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding Uint64: %w", err) + return n, fmt.Errorf("decoding Uint32: %w", err) } nTmp, err = s.StartingEvictionScanLevel.DecodeFrom(d, maxDepth) n += nTmp From 0690766480ac46e9925fc064839a78a27ed7de09 Mon Sep 17 00:00:00 2001 From: pritsheth Date: Mon, 5 Feb 2024 14:33:08 -0800 Subject: [PATCH 051/234] Refactor GetDiagnosticEvents --- ingest/ledger_transaction.go | 39 +------------------------------- xdr/transaction_meta.go | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/ingest/ledger_transaction.go b/ingest/ledger_transaction.go index 56f5af6070..77ca777206 100644 --- a/ingest/ledger_transaction.go +++ b/ingest/ledger_transaction.go @@ -1,8 +1,6 @@ package ingest import ( - "fmt" - "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" ) @@ -155,40 +153,5 @@ func operationChanges(ops []xdr.OperationMeta, index uint32) []Change { // GetDiagnosticEvents returns all contract events emitted by a given operation. func (t *LedgerTransaction) GetDiagnosticEvents() ([]xdr.DiagnosticEvent, error) { - switch t.UnsafeMeta.V { - case 1: - return nil, nil - case 2: - return nil, nil - case 3: - var diagnosticEvents []xdr.DiagnosticEvent - var contractEvents []xdr.ContractEvent - if sorobanMeta := t.UnsafeMeta.MustV3().SorobanMeta; sorobanMeta != nil { - diagnosticEvents = sorobanMeta.DiagnosticEvents - if len(diagnosticEvents) > 0 { - // all contract events and diag events for a single operation(by it's index in the tx) were available - // in tx meta's DiagnosticEvents, no need to look anywhere else for events - return diagnosticEvents, nil - } - - contractEvents = sorobanMeta.Events - if len(contractEvents) == 0 { - // no events were present in this tx meta - return nil, nil - } - } - - // tx meta only provided contract events, no diagnostic events, we convert the contract - // event to a diagnostic event, to fit the response interface. - convertedDiagnosticEvents := make([]xdr.DiagnosticEvent, len(contractEvents)) - for i, event := range contractEvents { - convertedDiagnosticEvents[i] = xdr.DiagnosticEvent{ - InSuccessfulContractCall: true, - Event: event, - } - } - return convertedDiagnosticEvents, nil - default: - return nil, fmt.Errorf("unsupported TransactionMeta version: %v", t.UnsafeMeta.V) - } + return t.UnsafeMeta.GetDiagnosticEvents() } diff --git a/xdr/transaction_meta.go b/xdr/transaction_meta.go index 3fee38ae93..327d19be72 100644 --- a/xdr/transaction_meta.go +++ b/xdr/transaction_meta.go @@ -1,5 +1,9 @@ package xdr +import ( + "fmt" +) + // Operations is a helper on TransactionMeta that returns operations // meta from `TransactionMeta.Operations` or `TransactionMeta.V1.Operations`. func (transactionMeta *TransactionMeta) OperationsMeta() []OperationMeta { @@ -16,3 +20,43 @@ func (transactionMeta *TransactionMeta) OperationsMeta() []OperationMeta { panic("Unsupported TransactionMeta version") } } + +// GetDiagnosticEvents returns all contract events emitted by a given operation. +func (t *TransactionMeta) GetDiagnosticEvents() ([]DiagnosticEvent, error) { + switch t.V { + case 1: + return nil, nil + case 2: + return nil, nil + case 3: + var diagnosticEvents []DiagnosticEvent + var contractEvents []ContractEvent + if sorobanMeta := t.MustV3().SorobanMeta; sorobanMeta != nil { + diagnosticEvents = sorobanMeta.DiagnosticEvents + if len(diagnosticEvents) > 0 { + // all contract events and diag events for a single operation(by it's index in the tx) were available + // in tx meta's DiagnosticEvents, no need to look anywhere else for events + return diagnosticEvents, nil + } + + contractEvents = sorobanMeta.Events + if len(contractEvents) == 0 { + // no events were present in this tx meta + return nil, nil + } + } + + // tx meta only provided contract events, no diagnostic events, we convert the contract + // event to a diagnostic event, to fit the response interface. + convertedDiagnosticEvents := make([]DiagnosticEvent, len(contractEvents)) + for i, event := range contractEvents { + convertedDiagnosticEvents[i] = DiagnosticEvent{ + InSuccessfulContractCall: true, + Event: event, + } + } + return convertedDiagnosticEvents, nil + default: + return nil, fmt.Errorf("unsupported TransactionMeta version: %v", t.V) + } +} From 794f7f2c68b564a20640bc27ba37795a752e728e Mon Sep 17 00:00:00 2001 From: pritsheth Date: Mon, 5 Feb 2024 15:26:29 -0800 Subject: [PATCH 052/234] Fix typo --- xdr/transaction_meta.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xdr/transaction_meta.go b/xdr/transaction_meta.go index 327d19be72..cd8443551a 100644 --- a/xdr/transaction_meta.go +++ b/xdr/transaction_meta.go @@ -34,7 +34,7 @@ func (t *TransactionMeta) GetDiagnosticEvents() ([]DiagnosticEvent, error) { if sorobanMeta := t.MustV3().SorobanMeta; sorobanMeta != nil { diagnosticEvents = sorobanMeta.DiagnosticEvents if len(diagnosticEvents) > 0 { - // all contract events and diag events for a single operation(by it's index in the tx) were available + // all contract events and diag events for a single operation(by its index in the tx) were available // in tx meta's DiagnosticEvents, no need to look anywhere else for events return diagnosticEvents, nil } From 73de95c8eb55afd59a9e3b7ff3b51faa2963cc6b Mon Sep 17 00:00:00 2001 From: mwtzzz <101583293+mwtzzz@users.noreply.github.com> Date: Tue, 6 Feb 2024 16:32:09 -0800 Subject: [PATCH 053/234] do not append UNSTABLE to tag for horizon docker builds (#5196) ### What do not append UNSTABLE to tag for horizon docker builds even if unstable core is used ### Why they've decided they want to be able to bundle unstable captivecore with production horizon, so no point in appending UNSTABLE to the tag ### Testing will be tested ### Issue addressed by this PR https://github.com/stellar/ops/issues/2812 --- services/horizon/docker/Makefile | 3 --- 1 file changed, 3 deletions(-) diff --git a/services/horizon/docker/Makefile b/services/horizon/docker/Makefile index 32074453f0..51ee19f2fe 100644 --- a/services/horizon/docker/Makefile +++ b/services/horizon/docker/Makefile @@ -4,9 +4,6 @@ SUDO := $(shell docker version >/dev/null 2>&1 || echo "sudo") BUILD_DATE := $(shell date -u +%FT%TZ) TAG ?= stellar/stellar-horizon:$(VERSION) -ifeq ($(ALLOW_CORE_UNSTABLE),yes) - TAG := $(TAG)-UNSTABLE -endif docker-build: ifndef VERSION From 6997dec05894806f4041dff9bb699eb9a25bbe82 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Wed, 7 Feb 2024 16:47:23 -0800 Subject: [PATCH 054/234] services/horizon: Fix claimable balance query (#5200) * services/horizon: Fix claimable balance query * Fix Soroban RPC image incompatibility --- .../db2/history/claimable_balances.go | 8 +- .../db2/history/claimable_balances_test.go | 182 ++++++++++++++++++ 2 files changed, 186 insertions(+), 4 deletions(-) diff --git a/services/horizon/internal/db2/history/claimable_balances.go b/services/horizon/internal/db2/history/claimable_balances.go index 5490bef11c..d45780a4c0 100644 --- a/services/horizon/internal/db2/history/claimable_balances.go +++ b/services/horizon/internal/db2/history/claimable_balances.go @@ -67,17 +67,17 @@ func applyClaimableBalancesQueriesCursor(sql sq.SelectBuilder, lCursor int64, rC case db2.OrderAscending: if hasPagedLimit { sql = sql. - Where(sq.Expr("(last_modified_ledger, id) > (?, ?)", lCursor, rCursor)) + Where(sq.Expr("(cb.last_modified_ledger, cb.id) > (?, ?)", lCursor, rCursor)) } - sql = sql.OrderBy("last_modified_ledger asc, id asc") + sql = sql.OrderBy("cb.last_modified_ledger asc, cb.id asc") case db2.OrderDescending: if hasPagedLimit { sql = sql. - Where(sq.Expr("(last_modified_ledger, id) < (?, ?)", lCursor, rCursor)) + Where(sq.Expr("(cb.last_modified_ledger, cb.id) < (?, ?)", lCursor, rCursor)) } - sql = sql.OrderBy("last_modified_ledger desc, id desc") + sql = sql.OrderBy("cb.last_modified_ledger desc, cb.id desc") default: return sql, errors.Errorf("invalid order: %s", order) } diff --git a/services/horizon/internal/db2/history/claimable_balances_test.go b/services/horizon/internal/db2/history/claimable_balances_test.go index ca32975c62..769ab3bc13 100644 --- a/services/horizon/internal/db2/history/claimable_balances_test.go +++ b/services/horizon/internal/db2/history/claimable_balances_test.go @@ -219,6 +219,188 @@ func TestFindClaimableBalancesByDestination(t *testing.T) { tt.Assert.Len(cbs, 1) } +func TestFindClaimableBalancesByCursor(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + tt.Assert.NoError(q.BeginTx(tt.Ctx, &sql.TxOptions{})) + defer func() { + _ = q.Rollback() + }() + + balanceInsertBuilder := q.NewClaimableBalanceBatchInsertBuilder() + claimantsInsertBuilder := q.NewClaimableBalanceClaimantBatchInsertBuilder() + + dest1 := "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML" + dest2 := "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" + + sponsor1 := "GA25GQLHJU3LPEJXEIAXK23AWEA5GWDUGRSHTQHDFT6HXHVMRULSQJUJ" + sponsor2 := "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" + + asset := xdr.MustNewCreditAsset("USD", dest1) + balanceID := xdr.ClaimableBalanceId{ + Type: xdr.ClaimableBalanceIdTypeClaimableBalanceIdTypeV0, + V0: &xdr.Hash{1, 2, 3}, + } + id, err := xdr.MarshalHex(balanceID) + tt.Assert.NoError(err) + cBalance := ClaimableBalance{ + BalanceID: id, + Claimants: []Claimant{ + { + Destination: dest1, + Predicate: xdr.ClaimPredicate{ + Type: xdr.ClaimPredicateTypeClaimPredicateUnconditional, + }, + }, + }, + Asset: asset, + LastModifiedLedger: 123, + Amount: 10, + Sponsor: null.StringFrom(sponsor1), + } + + tt.Assert.NoError(balanceInsertBuilder.Add(cBalance)) + tt.Assert.NoError(insertClaimants(claimantsInsertBuilder, cBalance)) + + balanceID = xdr.ClaimableBalanceId{ + Type: xdr.ClaimableBalanceIdTypeClaimableBalanceIdTypeV0, + V0: &xdr.Hash{3, 2, 1}, + } + id, err = xdr.MarshalHex(balanceID) + tt.Assert.NoError(err) + cBalance = ClaimableBalance{ + BalanceID: id, + Claimants: []Claimant{ + { + Destination: dest1, + Predicate: xdr.ClaimPredicate{ + Type: xdr.ClaimPredicateTypeClaimPredicateUnconditional, + }, + }, + { + Destination: dest2, + Predicate: xdr.ClaimPredicate{ + Type: xdr.ClaimPredicateTypeClaimPredicateUnconditional, + }, + }, + }, + Asset: asset, + LastModifiedLedger: 300, + Amount: 10, + Sponsor: null.StringFrom(sponsor2), + } + + tt.Assert.NoError(balanceInsertBuilder.Add(cBalance)) + tt.Assert.NoError(insertClaimants(claimantsInsertBuilder, cBalance)) + + tt.Assert.NoError(claimantsInsertBuilder.Exec(tt.Ctx)) + tt.Assert.NoError(balanceInsertBuilder.Exec(tt.Ctx)) + + query := ClaimableBalancesQuery{ + PageQuery: db2.MustPageQuery("", false, "", 10), + } + + cbs, err := q.GetClaimableBalances(tt.Ctx, query) + tt.Assert.NoError(err) + tt.Assert.Len(cbs, 2) + + order := "" // default is "asc" + // this validates the cb query with claimant and cb.id/ledger cursor parameters + query.PageQuery = db2.MustPageQuery(fmt.Sprintf("%v-%s", 150, cbs[0].BalanceID), false, order, 10) + query.Claimant = xdr.MustAddressPtr(dest1) + cbs, err = q.GetClaimableBalances(tt.Ctx, query) + tt.Assert.NoError(err) + tt.Assert.Len(cbs, 1) + tt.Assert.Equal(dest2, cbs[0].Claimants[1].Destination) + + // this validates the cb query with claimant, asset, sponsor and cb.id/ledger cursor parameters + query.PageQuery = db2.MustPageQuery(fmt.Sprintf("%v-%s", 150, cbs[0].BalanceID), false, order, 10) + query.Claimant = xdr.MustAddressPtr(dest1) + query.Asset = &asset + query.Sponsor = xdr.MustAddressPtr(sponsor2) + + cbs, err = q.GetClaimableBalances(tt.Ctx, query) + tt.Assert.NoError(err) + tt.Assert.Len(cbs, 1) + tt.Assert.Equal(dest2, cbs[0].Claimants[1].Destination) + + // this validates the cb query with no claimant, asset, sponsor and cb.id/ledger cursor parameters + query.PageQuery = db2.MustPageQuery(fmt.Sprintf("%v-%s", 150, cbs[0].BalanceID), false, order, 10) + query.Claimant = nil + query.Asset = &asset + query.Sponsor = xdr.MustAddressPtr(sponsor2) + + cbs, err = q.GetClaimableBalances(tt.Ctx, query) + tt.Assert.NoError(err) + tt.Assert.Len(cbs, 1) + tt.Assert.Equal(dest2, cbs[0].Claimants[1].Destination) + + order = "desc" + // claimant and cb.id/ledger cursor parameters + query.PageQuery = db2.MustPageQuery(fmt.Sprintf("%v-%s", 301, cbs[0].BalanceID), false, order, 10) + query.Claimant = xdr.MustAddressPtr(dest1) + cbs, err = q.GetClaimableBalances(tt.Ctx, query) + tt.Assert.NoError(err) + tt.Assert.Len(cbs, 1) + tt.Assert.Equal(dest2, cbs[0].Claimants[1].Destination) + + // claimant, asset, sponsor and cb.id/ledger cursor parameters + query.PageQuery = db2.MustPageQuery(fmt.Sprintf("%v-%s", 301, cbs[0].BalanceID), false, order, 10) + query.Claimant = xdr.MustAddressPtr(dest1) + query.Asset = &asset + query.Sponsor = xdr.MustAddressPtr(sponsor2) + + cbs, err = q.GetClaimableBalances(tt.Ctx, query) + tt.Assert.NoError(err) + tt.Assert.Len(cbs, 1) + tt.Assert.Equal(dest2, cbs[0].Claimants[1].Destination) + + // no claimant, asset, sponsor and cb.id/ledger cursor parameters + query.PageQuery = db2.MustPageQuery(fmt.Sprintf("%v-%s", 301, cbs[0].BalanceID), false, order, 10) + query.Claimant = nil + query.Asset = &asset + query.Sponsor = xdr.MustAddressPtr(sponsor2) + + cbs, err = q.GetClaimableBalances(tt.Ctx, query) + tt.Assert.NoError(err) + tt.Assert.Len(cbs, 1) + tt.Assert.Equal(dest2, cbs[0].Claimants[1].Destination) + + order = "asc" + // claimant and cb.id/ledger cursor parameters + query.PageQuery = db2.MustPageQuery(fmt.Sprintf("%v-%s", 150, cbs[0].BalanceID), false, order, 10) + query.Claimant = xdr.MustAddressPtr(dest1) + cbs, err = q.GetClaimableBalances(tt.Ctx, query) + tt.Assert.NoError(err) + tt.Assert.Len(cbs, 1) + tt.Assert.Equal(dest2, cbs[0].Claimants[1].Destination) + + // claimant, asset, sponsor and cb.id/ledger cursor parameters + query.PageQuery = db2.MustPageQuery(fmt.Sprintf("%v-%s", 150, cbs[0].BalanceID), false, order, 10) + query.Claimant = xdr.MustAddressPtr(dest1) + query.Asset = &asset + query.Sponsor = xdr.MustAddressPtr(sponsor2) + + cbs, err = q.GetClaimableBalances(tt.Ctx, query) + tt.Assert.NoError(err) + tt.Assert.Len(cbs, 1) + tt.Assert.Equal(dest2, cbs[0].Claimants[1].Destination) + + // no claimant, asset, sponsor and cb.id/ledger cursor parameters + query.PageQuery = db2.MustPageQuery(fmt.Sprintf("%v-%s", 150, cbs[0].BalanceID), false, order, 10) + query.Claimant = nil + query.Asset = &asset + query.Sponsor = xdr.MustAddressPtr(sponsor2) + + cbs, err = q.GetClaimableBalances(tt.Ctx, query) + tt.Assert.NoError(err) + tt.Assert.Len(cbs, 1) + tt.Assert.Equal(dest2, cbs[0].Claimants[1].Destination) +} + func insertClaimants(claimantsInsertBuilder ClaimableBalanceClaimantBatchInsertBuilder, cBalance ClaimableBalance) error { for _, claimant := range cBalance.Claimants { claimant := ClaimableBalanceClaimant{ From da575f06566737d87dd54d64d7b0113e20fe047e Mon Sep 17 00:00:00 2001 From: George Date: Thu, 8 Feb 2024 12:02:04 -0800 Subject: [PATCH 055/234] services/horizon: Add cache toggle and use libary for on-disk caching (#5197) * Add a `--history-archive-caching` flag (default=true) to toggle behavior * Refactor to use a library: fscache * Hook new metric into prometheus * Add parallel read test to stress cache * Add tests for deadlocking and other misc. scenarios --- go.mod | 3 + go.sum | 6 + historyarchive/archive.go | 128 +++++++++++++--- historyarchive/archive_test.go | 137 +++++++++++++----- historyarchive/failing_mock_archive.go | 76 ++++++++++ historyarchive/mocks.go | 5 + historyarchive/stats.go | 9 ++ historyarchive/xdrstream.go | 2 +- services/horizon/cmd/db.go | 1 + services/horizon/cmd/ingest.go | 3 + services/horizon/internal/config.go | 1 + services/horizon/internal/flags.go | 17 ++- services/horizon/internal/ingest/fsm.go | 5 + services/horizon/internal/ingest/main.go | 18 ++- .../internal/ingest/resume_state_test.go | 3 + services/horizon/internal/init.go | 1 + 16 files changed, 340 insertions(+), 75 deletions(-) create mode 100644 historyarchive/failing_mock_archive.go diff --git a/go.mod b/go.mod index f3d11b5abc..1ca1ea801c 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/aws/aws-sdk-go v1.45.26 github.com/creachadair/jrpc2 v1.1.0 + github.com/djherbis/fscache v0.10.1 github.com/elazarl/go-bindata-assetfs v1.0.1 github.com/getsentry/raven-go v0.2.0 github.com/go-chi/chi v4.1.2+incompatible @@ -91,6 +92,8 @@ require ( golang.org/x/tools v0.14.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect + gopkg.in/djherbis/atime.v1 v1.0.0 // indirect + gopkg.in/djherbis/stream.v1 v1.3.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) diff --git a/go.sum b/go.sum index 9ca895487d..6e4158f9b7 100644 --- a/go.sum +++ b/go.sum @@ -104,6 +104,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/djherbis/fscache v0.10.1 h1:hDv+RGyvD+UDKyRYuLoVNbuRTnf2SrA2K3VyR1br9lk= +github.com/djherbis/fscache v0.10.1/go.mod h1:yyPYtkNnnPXsW+81lAcQS6yab3G2CRfnPLotBvtbf0c= github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= @@ -814,6 +816,10 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/djherbis/atime.v1 v1.0.0 h1:eMRqB/JrLKocla2PBPKgQYg/p5UG4L6AUAs92aP7F60= +gopkg.in/djherbis/atime.v1 v1.0.0/go.mod h1:hQIUStKmJfvf7xdh/wtK84qe+DsTV5LnA9lzxxtPpJ8= +gopkg.in/djherbis/stream.v1 v1.3.1 h1:uGfmsOY1qqMjQQphhRBSGLyA9qumJ56exkRu9ASTjCw= +gopkg.in/djherbis/stream.v1 v1.3.1/go.mod h1:aEV8CBVRmSpLamVJfM903Npic1IKmb2qS30VAZ+sssg= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0 h1:r5ptJ1tBxVAeqw4CrYWhXIMr0SybY3CDHuIbCg5CFVw= diff --git a/historyarchive/archive.go b/historyarchive/archive.go index ed05a4130d..db0e2b5b10 100644 --- a/historyarchive/archive.go +++ b/historyarchive/archive.go @@ -11,12 +11,15 @@ import ( "fmt" "io" "net/url" + "os" "path" "regexp" "strconv" "strings" "sync" + "time" + fscache "github.com/djherbis/fscache" log "github.com/sirupsen/logrus" "github.com/stellar/go/support/errors" @@ -38,6 +41,8 @@ type CommandOptions struct { } type ArchiveOptions struct { + storage.ConnectOptions + // NetworkPassphrase defines the expected network of history archive. It is // checked when getting HAS. If network passphrase does not match, error is // returned. @@ -45,9 +50,8 @@ type ArchiveOptions struct { // CheckpointFrequency is the number of ledgers between checkpoints // if unset, DefaultCheckpointFrequency will be used CheckpointFrequency uint32 - storage.ConnectOptions - // CacheConfig controls how/if bucket files are cached on the disk. - CacheConfig CacheOptions + // CachePath controls where/if bucket files are cached on the disk. + CachePath string } type Ledger struct { @@ -104,8 +108,15 @@ type Archive struct { checkpointManager CheckpointManager backend storage.Storage - cache *ArchiveBucketCache stats archiveStats + cache *archiveBucketCache +} + +type archiveBucketCache struct { + fscache.Cache + + path string + sizes sync.Map } func (arch *Archive) GetStats() []ArchiveStats { @@ -383,23 +394,79 @@ func (a *Archive) GetXdrStream(pth string) (*XdrStream, error) { } func (a *Archive) cachedGet(pth string) (io.ReadCloser, error) { - if a.cache != nil { - rdr, foundInCache, err := a.cache.GetFile(pth, a.backend) - if !foundInCache { - a.stats.incrementDownloads() - } else { - a.stats.incrementCacheHits() - } - if err == nil { - return rdr, nil + if a.cache == nil { + a.stats.incrementDownloads() + return a.backend.GetFile(pth) + } + + L := log.WithField("path", pth).WithField("cache", a.cache.path) + + rdr, wrtr, err := a.cache.Get(pth) + if err != nil { + L.WithError(err). + WithField("remove", a.cache.Remove(pth)). + Warn("On-disk cache retrieval failed") + a.stats.incrementDownloads() + return a.backend.GetFile(pth) + } + + // If a NEW key is being retrieved, it returns a writer to which + // you're expected to write your upstream as well as a reader that + // will read directly from it. + if wrtr != nil { + log.WithField("path", pth).Info("Caching file...") + a.stats.incrementDownloads() + upstreamReader, err := a.backend.GetFile(pth) + if err != nil { + writeErr := wrtr.Close() + readErr := rdr.Close() + removeErr := a.cache.Remove(pth) + // Execution order isn't guaranteed w/in a function call expression + // so we close them with explicit order first. + L.WithError(err).WithFields(log.Fields{ + "write-close": writeErr, + "read-close": readErr, + "cache-rm": removeErr, + }).Warn("Download failed, purging from cache") + return nil, err } - // If there's an error, retry with the uncached backend. - a.cache.Evict(pth) + // Start a goroutine to slurp up the upstream and feed + // it directly to the cache. + go func() { + written, err := io.Copy(wrtr, upstreamReader) + writeErr := wrtr.Close() + readErr := upstreamReader.Close() + fields := log.Fields{ + "wr-close": writeErr, + "rd-close": readErr, + } + + if err != nil { + L.WithFields(fields).WithError(err). + Warn("Failed to download and cache file") + + // Removal must happen *after* handles close. + if removalErr := a.cache.Remove(pth); removalErr != nil { + L.WithError(removalErr).Warn("Removing cached file failed") + } + } else { + L.WithFields(fields).Infof("Cached %dKiB file", written/1024) + + // Track how much bandwidth we've saved from caching by saving + // the size of the file we just downloaded. + a.cache.sizes.Store(pth, written) + } + }() + } else { + // Best-effort check to track bandwidth metrics + if written, found := a.cache.sizes.Load(pth); found { + a.stats.incrementCacheBandwidth(written.(int64)) + } + a.stats.incrementCacheHits() } - a.stats.incrementDownloads() - return a.backend.GetFile(pth) + return rdr, nil } func (a *Archive) cachedExists(pth string) (bool, error) { @@ -439,13 +506,30 @@ func Connect(u string, opts ArchiveOptions) (*Archive, error) { return &arch, err } - if opts.CacheConfig.Cache { - cache, innerErr := MakeArchiveBucketCache(opts.CacheConfig) - if innerErr != nil { - return &arch, innerErr + if opts.CachePath != "" { + // Set up a <= ~10GiB LRU cache for history archives files + haunter := fscache.NewLRUHaunterStrategy( + fscache.NewLRUHaunter(0, 10<<30, time.Minute /* frequency check */), + ) + + // Wipe any existing cache on startup + os.RemoveAll(opts.CachePath) + fs, err := fscache.NewFs(opts.CachePath, 0755 /* drwxr-xr-x */) + + if err != nil { + return &arch, errors.Wrapf(err, + "creating cache at '%s' with mode 0755 failed", + opts.CachePath) + } + + cache, err := fscache.NewCacheWithHaunter(fs, haunter) + if err != nil { + return &arch, errors.Wrapf(err, + "creating cache at '%s' failed", + opts.CachePath) } - arch.cache = cache + arch.cache = &archiveBucketCache{cache, opts.CachePath, sync.Map{}} } arch.stats = archiveStats{backendName: u} diff --git a/historyarchive/archive_test.go b/historyarchive/archive_test.go index de34c36f68..56539d46ce 100644 --- a/historyarchive/archive_test.go +++ b/historyarchive/archive_test.go @@ -18,13 +18,18 @@ import ( "os" "path/filepath" "strings" + "sync" "testing" + "time" "github.com/stellar/go/support/storage" "github.com/stellar/go/xdr" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +var cachePath = filepath.Join(os.TempDir(), "history-archive-test-cache") + func GetTestS3Archive() *Archive { mx := big.NewInt(0xffffffff) r, e := rand.Int(rand.Reader, mx) @@ -49,13 +54,10 @@ func GetTestS3Archive() *Archive { } func GetTestMockArchive() *Archive { - return MustConnect("mock://test", - ArchiveOptions{CheckpointFrequency: 64, - CacheConfig: CacheOptions{ - Cache: true, - Path: filepath.Join(os.TempDir(), "history-archive-test-cache"), - MaxFiles: 5, - }}) + return MustConnect("mock://test", ConnectOptions{ + CheckpointFrequency: 64, + CachePath: cachePath, + }) } var tmpdirs []string @@ -563,7 +565,95 @@ func TestGetLedgers(t *testing.T) { assert.Equal(t, uint32(1), archive.GetStats()[0].GetRequests()) assert.Equal(t, uint32(0), archive.GetStats()[0].GetDownloads()) assert.EqualError(t, err, "checkpoint 1023 is not published") + ledgerHeaders, transactions, results := makeFakeArchive(t, archive) + + stats := archive.GetStats()[0] + ledgers, err := archive.GetLedgers(1000, 1002) + + assert.NoError(t, err) + assert.Len(t, ledgers, 3) + // it started at 1, incurred 6 requests total: 3 queries + 3 downloads + assert.EqualValues(t, 7, stats.GetRequests()) + // started 0, incurred 3 file downloads + assert.EqualValues(t, 3, stats.GetDownloads()) + assert.EqualValues(t, 0, stats.GetCacheHits()) + for i, seq := range []uint32{1000, 1001, 1002} { + ledger := ledgers[seq] + assertXdrEquals(t, ledgerHeaders[i], ledger.Header) + assertXdrEquals(t, transactions[i], ledger.Transaction) + assertXdrEquals(t, results[i], ledger.TransactionResult) + } + + // Repeat the same check but ensure the cache was used + ledgers, err = archive.GetLedgers(1000, 1002) // all cached + assert.NoError(t, err) + assert.Len(t, ledgers, 3) + + // downloads should not change because of the cache + assert.EqualValues(t, 3, stats.GetDownloads()) + // but requests increase because of 3 fetches to categories + assert.EqualValues(t, 10, stats.GetRequests()) + assert.EqualValues(t, 3, stats.GetCacheHits()) + for i, seq := range []uint32{1000, 1001, 1002} { + ledger := ledgers[seq] + assertXdrEquals(t, ledgerHeaders[i], ledger.Header) + assertXdrEquals(t, transactions[i], ledger.Transaction) + assertXdrEquals(t, results[i], ledger.TransactionResult) + } + + // remove the cached files without informing it and ensure it fills up again + require.NoError(t, os.RemoveAll(cachePath)) + ledgers, err = archive.GetLedgers(1000, 1002) // uncached, refetch + assert.NoError(t, err) + assert.Len(t, ledgers, 3) + + // downloads should increase again + assert.EqualValues(t, 6, stats.GetDownloads()) + assert.EqualValues(t, 3, stats.GetCacheHits()) +} + +func TestStressfulGetLedgers(t *testing.T) { + archive := GetTestMockArchive() + ledgerHeaders, transactions, results := makeFakeArchive(t, archive) + + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + + go func() { + time.Sleep(time.Millisecond) // encourage interleaved execution + ledgers, err := archive.GetLedgers(1000, 1002) + assert.NoError(t, err) + assert.Len(t, ledgers, 3) + for i, seq := range []uint32{1000, 1001, 1002} { + ledger := ledgers[seq] + assertXdrEquals(t, ledgerHeaders[i], ledger.Header) + assertXdrEquals(t, transactions[i], ledger.Transaction) + assertXdrEquals(t, results[i], ledger.TransactionResult) + } + + wg.Done() + }() + } + + require.Eventually(t, func() bool { wg.Wait(); return true }, time.Minute, time.Second) +} +func TestCacheDeadlocks(t *testing.T) { + archive := MustConnect("fmock://test", ConnectOptions{ + CheckpointFrequency: 64, + CachePath: cachePath, + }) + makeFakeArchive(t, archive) + _, err := archive.GetLedgers(1000, 1002) + require.Error(t, err) +} + +func makeFakeArchive(t *testing.T, archive *Archive) ( + []xdr.LedgerHeaderHistoryEntry, + []xdr.TransactionHistoryEntry, + []xdr.TransactionHistoryResultEntry, +) { ledgerHeaders := []xdr.LedgerHeaderHistoryEntry{ { Hash: xdr.Hash{1}, @@ -646,36 +736,5 @@ func TestGetLedgers(t *testing.T) { []xdrEntry{results[0], results[1], results[2]}, ) - stats := archive.GetStats()[0] - ledgers, err := archive.GetLedgers(1000, 1002) - - assert.NoError(t, err) - assert.Len(t, ledgers, 3) - // it started at 1, incurred 6 requests total, 3 queries, 3 downloads - assert.EqualValues(t, 7, stats.GetRequests()) - // started 0, incurred 3 file downloads - assert.EqualValues(t, 3, stats.GetDownloads()) - for i, seq := range []uint32{1000, 1001, 1002} { - ledger := ledgers[seq] - assertXdrEquals(t, ledgerHeaders[i], ledger.Header) - assertXdrEquals(t, transactions[i], ledger.Transaction) - assertXdrEquals(t, results[i], ledger.TransactionResult) - } - - // Repeat the same check but ensure the cache was used - ledgers, err = archive.GetLedgers(1000, 1002) // all cached - assert.NoError(t, err) - assert.Len(t, ledgers, 3) - - // downloads should not change because of the cache - assert.EqualValues(t, 3, stats.GetDownloads()) - // but requests increase because of 3 fetches to categories - assert.EqualValues(t, 10, stats.GetRequests()) - assert.EqualValues(t, 3, stats.GetCacheHits()) - for i, seq := range []uint32{1000, 1001, 1002} { - ledger := ledgers[seq] - assertXdrEquals(t, ledgerHeaders[i], ledger.Header) - assertXdrEquals(t, transactions[i], ledger.Transaction) - assertXdrEquals(t, results[i], ledger.TransactionResult) - } + return ledgerHeaders, transactions, results } diff --git a/historyarchive/failing_mock_archive.go b/historyarchive/failing_mock_archive.go new file mode 100644 index 0000000000..5966cb30e7 --- /dev/null +++ b/historyarchive/failing_mock_archive.go @@ -0,0 +1,76 @@ +package historyarchive + +import ( + "io" + + "github.com/stellar/go/support/errors" +) + +// FailingMockArchiveBackend is a mocking backend that will fail only when you +// try to read but otherwise behave like MockArchiveBackend. +type FailingMockArchiveBackend struct { + files map[string][]byte +} + +func (b *FailingMockArchiveBackend) Exists(pth string) (bool, error) { + _, ok := b.files[pth] + return ok, nil +} + +func (b *FailingMockArchiveBackend) Size(pth string) (int64, error) { + f, ok := b.files[pth] + sz := int64(0) + if ok { + sz = int64(len(f)) + } + return sz, nil +} + +func (b *FailingMockArchiveBackend) GetFile(pth string) (io.ReadCloser, error) { + data, ok := b.files[pth] + if !ok { + return nil, errors.New("file does not exist") + } + + fr := FakeReader{} + fr.data = make([]byte, len(data)) + copy(fr.data[:], data[:]) + return &fr, nil +} + +func (b *FailingMockArchiveBackend) PutFile(pth string, in io.ReadCloser) error { + buf, e := io.ReadAll(in) + if e != nil { + return e + } + b.files[pth] = buf + return nil +} + +func (b *FailingMockArchiveBackend) ListFiles(pth string) (chan string, chan error) { + return nil, nil +} + +func (b *FailingMockArchiveBackend) CanListFiles() bool { + return false +} + +func makeFailingMockBackend(opts ConnectOptions) ArchiveBackend { + b := new(FailingMockArchiveBackend) + b.files = make(map[string][]byte) + return b +} + +type FakeReader struct { + data []byte +} + +func (fr *FakeReader) Read(b []byte) (int, error) { + return 0, io.ErrClosedPipe +} + +func (fr *FakeReader) Close() error { + return nil +} + +var _ io.ReadCloser = &FakeReader{} diff --git a/historyarchive/mocks.go b/historyarchive/mocks.go index fe497ec36e..fa5716e5de 100644 --- a/historyarchive/mocks.go +++ b/historyarchive/mocks.go @@ -137,3 +137,8 @@ func (m *MockArchiveStats) GetCacheHits() uint32 { a := m.Called() return a.Get(0).(uint32) } + +func (m *MockArchiveStats) GetCacheBandwidth() uint64 { + a := m.Called() + return a.Get(0).(uint64) +} diff --git a/historyarchive/stats.go b/historyarchive/stats.go index c182853d1b..6dbf8ceed2 100644 --- a/historyarchive/stats.go +++ b/historyarchive/stats.go @@ -8,6 +8,7 @@ type archiveStats struct { fileDownloads atomic.Uint32 fileUploads atomic.Uint32 cacheHits atomic.Uint32 + cacheBw atomic.Uint64 backendName string } @@ -16,6 +17,7 @@ type ArchiveStats interface { GetDownloads() uint32 GetUploads() uint32 GetCacheHits() uint32 + GetCacheBandwidth() uint64 GetBackendName() string } @@ -37,6 +39,10 @@ func (as *archiveStats) incrementCacheHits() { as.cacheHits.Add(1) } +func (as *archiveStats) incrementCacheBandwidth(bytes int64) { + as.cacheBw.Add(uint64(bytes)) +} + func (as *archiveStats) GetRequests() uint32 { return as.requests.Load() } @@ -55,3 +61,6 @@ func (as *archiveStats) GetBackendName() string { func (as *archiveStats) GetCacheHits() uint32 { return as.cacheHits.Load() } +func (as *archiveStats) GetCacheBandwidth() uint64 { + return as.cacheBw.Load() +} diff --git a/historyarchive/xdrstream.go b/historyarchive/xdrstream.go index de8efc3bb6..313c600f8b 100644 --- a/historyarchive/xdrstream.go +++ b/historyarchive/xdrstream.go @@ -107,7 +107,7 @@ func (x *XdrStream) ExpectedHash() ([sha256.Size]byte, bool) { func (x *XdrStream) Close() error { if x.validateHash { // Read all remaining data from rdr - _, err := io.Copy(ioutil.Discard, x.rdr) + _, err := io.Copy(io.Discard, x.rdr) if err != nil { // close the internal readers to avoid memory leaks x.closeReaders() diff --git a/services/horizon/cmd/db.go b/services/horizon/cmd/db.go index 07bbf975fa..7d14ca314e 100644 --- a/services/horizon/cmd/db.go +++ b/services/horizon/cmd/db.go @@ -407,6 +407,7 @@ func runDBReingestRange(ledgerRanges []history.LedgerRange, reingestForce bool, ingestConfig := ingest.Config{ NetworkPassphrase: config.NetworkPassphrase, HistoryArchiveURLs: config.HistoryArchiveURLs, + HistoryArchiveCaching: config.HistoryArchiveCaching, CheckpointFrequency: config.CheckpointFrequency, ReingestEnabled: true, MaxReingestRetries: int(retries), diff --git a/services/horizon/cmd/ingest.go b/services/horizon/cmd/ingest.go index 3833dba7fd..18452dc74a 100644 --- a/services/horizon/cmd/ingest.go +++ b/services/horizon/cmd/ingest.go @@ -128,6 +128,7 @@ var ingestVerifyRangeCmd = &cobra.Command{ NetworkPassphrase: globalConfig.NetworkPassphrase, HistorySession: horizonSession, HistoryArchiveURLs: globalConfig.HistoryArchiveURLs, + HistoryArchiveCaching: globalConfig.HistoryArchiveCaching, CaptiveCoreBinaryPath: globalConfig.CaptiveCoreBinaryPath, CaptiveCoreConfigUseDB: globalConfig.CaptiveCoreConfigUseDB, CheckpointFrequency: globalConfig.CheckpointFrequency, @@ -210,6 +211,7 @@ var ingestStressTestCmd = &cobra.Command{ NetworkPassphrase: globalConfig.NetworkPassphrase, HistorySession: horizonSession, HistoryArchiveURLs: globalConfig.HistoryArchiveURLs, + HistoryArchiveCaching: globalConfig.HistoryArchiveCaching, RoundingSlippageFilter: globalConfig.RoundingSlippageFilter, CaptiveCoreBinaryPath: globalConfig.CaptiveCoreBinaryPath, CaptiveCoreConfigUseDB: globalConfig.CaptiveCoreConfigUseDB, @@ -349,6 +351,7 @@ var ingestBuildStateCmd = &cobra.Command{ NetworkPassphrase: globalConfig.NetworkPassphrase, HistorySession: horizonSession, HistoryArchiveURLs: globalConfig.HistoryArchiveURLs, + HistoryArchiveCaching: globalConfig.HistoryArchiveCaching, CaptiveCoreBinaryPath: globalConfig.CaptiveCoreBinaryPath, CaptiveCoreConfigUseDB: globalConfig.CaptiveCoreConfigUseDB, CheckpointFrequency: globalConfig.CheckpointFrequency, diff --git a/services/horizon/internal/config.go b/services/horizon/internal/config.go index 8fb31075b8..54f843b810 100644 --- a/services/horizon/internal/config.go +++ b/services/horizon/internal/config.go @@ -27,6 +27,7 @@ type Config struct { CaptiveCoreStoragePath string CaptiveCoreReuseStoragePath bool CaptiveCoreConfigUseDB bool + HistoryArchiveCaching bool StellarCoreURL string diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index eb229c65b2..87deb28c48 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -51,6 +51,9 @@ const ( NetworkPassphraseFlagName = "network-passphrase" // HistoryArchiveURLsFlagName is the command line flag for specifying the history archive URLs HistoryArchiveURLsFlagName = "history-archive-urls" + // HistoryArchiveCaching is the flag for controlling whether or not there's + // an on-disk cache for history archive downloads + HistoryArchiveCachingFlagName = "history-archive-caching" // NetworkFlagName is the command line flag for specifying the "network" NetworkFlagName = "network" // EnableIngestionFilteringFlagName is the command line flag for enabling the experimental ingestion filtering feature (now enabled by default) @@ -236,11 +239,7 @@ func Flags() (*Config, support.ConfigOptions) { OptType: types.Bool, FlagDefault: true, Required: false, - Usage: `when enabled, Horizon ingestion will instruct the captive - core invocation to use an external db url for ledger states rather than in memory(RAM).\n - Will result in several GB of space shifting out of RAM and to the external db persistence.\n - The external db url is determined by the presence of DATABASE parameter in the captive-core-config-path or\n - or if absent, the db will default to sqlite and the db file will be stored at location derived from captive-core-storage-path parameter.`, + Usage: `when enabled, Horizon ingestion will instruct the captive core invocation to use an external db url for ledger states rather than in memory(RAM). Will result in several GB of space shifting out of RAM and to the external db persistence. The external db url is determined by the presence of DATABASE parameter in the captive-core-config-path or if absent, the db will default to sqlite and the db file will be stored at location derived from captive-core-storage-path parameter.`, CustomSetValue: func(opt *support.ConfigOption) error { if val := viper.GetBool(opt.Name); val { config.CaptiveCoreConfigUseDB = val @@ -372,6 +371,14 @@ func Flags() (*Config, support.ConfigOptions) { Usage: "comma-separated list of stellar history archives to connect with", UsedInCommands: IngestionCommands, }, + &support.ConfigOption{ + Name: HistoryArchiveCachingFlagName, + ConfigKey: &config.HistoryArchiveCaching, + OptType: types.Bool, + FlagDefault: true, + Usage: "adds caching for history archive downloads (requires an add'l 10GB of disk space on mainnet)", + UsedInCommands: IngestionCommands, + }, &support.ConfigOption{ Name: "port", ConfigKey: &config.Port, diff --git a/services/horizon/internal/ingest/fsm.go b/services/horizon/internal/ingest/fsm.go index e0c667b033..892868e5b9 100644 --- a/services/horizon/internal/ingest/fsm.go +++ b/services/horizon/internal/ingest/fsm.go @@ -595,6 +595,11 @@ func addHistoryArchiveStatsMetrics(s *system, stats []historyarchive.ArchiveStat "source": historyServerStat.GetBackendName(), "type": "cache_hits"}). Add(float64(historyServerStat.GetCacheHits())) + s.Metrics().HistoryArchiveStatsCounter. + With(prometheus.Labels{ + "source": historyServerStat.GetBackendName(), + "type": "cache_bandwidth"}). + Add(float64(historyServerStat.GetCacheBandwidth())) } } diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 2726e02484..7799f2ec76 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -88,8 +88,9 @@ type Config struct { CaptiveCoreConfigUseDB bool NetworkPassphrase string - HistorySession db.SessionInterface - HistoryArchiveURLs []string + HistorySession db.SessionInterface + HistoryArchiveURLs []string + HistoryArchiveCaching bool DisableStateVerification bool EnableReapLookupTables bool @@ -223,6 +224,11 @@ type system struct { func NewSystem(config Config) (System, error) { ctx, cancel := context.WithCancel(context.Background()) + cachingPath := "" + if config.HistoryArchiveCaching { + cachingPath = path.Join(config.CaptiveCoreStoragePath, "bucket-cache") + } + archive, err := historyarchive.NewArchivePool( config.HistoryArchiveURLs, historyarchive.ArchiveOptions{ @@ -230,13 +236,9 @@ func NewSystem(config Config) (System, error) { CheckpointFrequency: config.CheckpointFrequency, ConnectOptions: storage.ConnectOptions{ Context: ctx, - UserAgent: fmt.Sprintf("horizon/%s golang/%s", apkg.Version(), runtime.Version())}, - CacheConfig: historyarchive.CacheOptions{ - Cache: true, - Path: path.Join(config.CaptiveCoreStoragePath, "bucket-cache"), - Log: log.WithField("subservice", "ha-cache"), - MaxFiles: 150, + UserAgent: fmt.Sprintf("horizon/%s golang/%s", apkg.Version(), runtime.Version()) }, + CachePath: cachingPath, }, ) if err != nil { diff --git a/services/horizon/internal/ingest/resume_state_test.go b/services/horizon/internal/ingest/resume_state_test.go index f1f8b2ce2a..985391883f 100644 --- a/services/horizon/internal/ingest/resume_state_test.go +++ b/services/horizon/internal/ingest/resume_state_test.go @@ -267,6 +267,7 @@ func (s *ResumeTestTestSuite) mockSuccessfulIngestion() { mockStats.On("GetRequests").Return(uint32(0)) mockStats.On("GetUploads").Return(uint32(0)) mockStats.On("GetCacheHits").Return(uint32(0)) + mockStats.On("GetCacheBandwidth").Return(uint64(0)) s.historyAdapter.On("GetStats").Return([]historyarchive.ArchiveStats{mockStats}).Once() s.runner.On("RunAllProcessorsOnLedger", mock.AnythingOfType("xdr.LedgerCloseMeta")). @@ -384,6 +385,7 @@ func (s *ResumeTestTestSuite) TestReapingObjectsDisabled() { mockStats.On("GetRequests").Return(uint32(0)) mockStats.On("GetUploads").Return(uint32(0)) mockStats.On("GetCacheHits").Return(uint32(0)) + mockStats.On("GetCacheBandwidth").Return(uint64(0)) s.historyAdapter.On("GetStats").Return([]historyarchive.ArchiveStats{mockStats}).Once() // Reap lookup tables not executed @@ -434,6 +436,7 @@ func (s *ResumeTestTestSuite) TestErrorReapingObjectsIgnored() { mockStats.On("GetRequests").Return(uint32(0)) mockStats.On("GetUploads").Return(uint32(0)) mockStats.On("GetCacheHits").Return(uint32(0)) + mockStats.On("GetCacheBandwidth").Return(uint64(0)) s.historyAdapter.On("GetStats").Return([]historyarchive.ArchiveStats{mockStats}).Once() next, err := resumeState{latestSuccessfullyProcessedLedger: 100}.run(s.system) diff --git a/services/horizon/internal/init.go b/services/horizon/internal/init.go index d4b34f9f4d..60ba7b6c2a 100644 --- a/services/horizon/internal/init.go +++ b/services/horizon/internal/init.go @@ -97,6 +97,7 @@ func initIngester(app *App) { ), NetworkPassphrase: app.config.NetworkPassphrase, HistoryArchiveURLs: app.config.HistoryArchiveURLs, + HistoryArchiveCaching: app.config.HistoryArchiveCaching, CheckpointFrequency: app.config.CheckpointFrequency, StellarCoreURL: app.config.StellarCoreURL, CaptiveCoreBinaryPath: app.config.CaptiveCoreBinaryPath, From a8fe5f092170d444ae4746c8ba76bbcfbbdcf533 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Thu, 8 Feb 2024 13:54:16 -0800 Subject: [PATCH 056/234] Update CHANGELOG.md (#5201) --- services/horizon/CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index fc2b046a57..0e1b116923 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -3,7 +3,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## Unreleased +## 2.28.2 + +### Fixed +- History archive caching would cause file corruption in certain environments [5197](https://github.com/stellar/go/pull/5197) +- Server error in claimable balance API when claimant, asset and cursor query params are supplied [5200](https://github.com/stellar/go/pull/5200) ## 2.28.1 From 531a01fa1bc816e5f3eedf4b1acbc8c2c92f06cd Mon Sep 17 00:00:00 2001 From: George Kudrayvtsev Date: Wed, 14 Feb 2024 12:46:31 -0800 Subject: [PATCH 057/234] Add'l changes after rebase to conform to storage pkg --- historyarchive/archive.go | 2 ++ historyarchive/archive_test.go | 4 ++-- historyarchive/failing_mock_archive.go | 10 ++++++++-- services/horizon/internal/ingest/main.go | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/historyarchive/archive.go b/historyarchive/archive.go index db0e2b5b10..d52c41ec43 100644 --- a/historyarchive/archive.go +++ b/historyarchive/archive.go @@ -551,6 +551,8 @@ func ConnectBackend(u string, opts storage.ConnectOptions) (storage.Storage, err if parsed.Scheme == "mock" { backend = makeMockBackend() + } else if parsed.Scheme == "fmock" { + backend = makeFailingMockBackend() } else { backend, err = storage.ConnectBackend(u, opts) } diff --git a/historyarchive/archive_test.go b/historyarchive/archive_test.go index 56539d46ce..e5be8febbb 100644 --- a/historyarchive/archive_test.go +++ b/historyarchive/archive_test.go @@ -54,7 +54,7 @@ func GetTestS3Archive() *Archive { } func GetTestMockArchive() *Archive { - return MustConnect("mock://test", ConnectOptions{ + return MustConnect("mock://test", ArchiveOptions{ CheckpointFrequency: 64, CachePath: cachePath, }) @@ -640,7 +640,7 @@ func TestStressfulGetLedgers(t *testing.T) { } func TestCacheDeadlocks(t *testing.T) { - archive := MustConnect("fmock://test", ConnectOptions{ + archive := MustConnect("fmock://test", ArchiveOptions{ CheckpointFrequency: 64, CachePath: cachePath, }) diff --git a/historyarchive/failing_mock_archive.go b/historyarchive/failing_mock_archive.go index 5966cb30e7..815b575648 100644 --- a/historyarchive/failing_mock_archive.go +++ b/historyarchive/failing_mock_archive.go @@ -4,6 +4,7 @@ import ( "io" "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/storage" ) // FailingMockArchiveBackend is a mocking backend that will fail only when you @@ -55,9 +56,14 @@ func (b *FailingMockArchiveBackend) CanListFiles() bool { return false } -func makeFailingMockBackend(opts ConnectOptions) ArchiveBackend { - b := new(FailingMockArchiveBackend) +func (b *FailingMockArchiveBackend) Close() error { b.files = make(map[string][]byte) + return nil +} + +func makeFailingMockBackend() storage.Storage { + b := new(FailingMockArchiveBackend) + b.Close() return b } diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 7799f2ec76..98acd68f33 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -236,7 +236,7 @@ func NewSystem(config Config) (System, error) { CheckpointFrequency: config.CheckpointFrequency, ConnectOptions: storage.ConnectOptions{ Context: ctx, - UserAgent: fmt.Sprintf("horizon/%s golang/%s", apkg.Version(), runtime.Version()) + UserAgent: fmt.Sprintf("horizon/%s golang/%s", apkg.Version(), runtime.Version()), }, CachePath: cachingPath, }, From 798da166d174525b801fc241df0b92e6da4e290e Mon Sep 17 00:00:00 2001 From: tamirms Date: Wed, 14 Feb 2024 22:24:13 +0000 Subject: [PATCH 058/234] Fix claimable_balance_claimants subquery in GetClaimableBalances() (#5207) --- .../db2/history/claimable_balances.go | 32 ++++++++++++------- .../db2/history/claimable_balances_test.go | 5 +-- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/services/horizon/internal/db2/history/claimable_balances.go b/services/horizon/internal/db2/history/claimable_balances.go index d45780a4c0..c198ee162d 100644 --- a/services/horizon/internal/db2/history/claimable_balances.go +++ b/services/horizon/internal/db2/history/claimable_balances.go @@ -10,6 +10,7 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/guregu/null" + "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" @@ -57,7 +58,7 @@ func (cbq ClaimableBalancesQuery) Cursor() (int64, string, error) { // ApplyCursor applies cursor to the given sql. For performance reason the limit // is not applied here. This allows us to hint the planner later to use the right // indexes. -func applyClaimableBalancesQueriesCursor(sql sq.SelectBuilder, lCursor int64, rCursor string, order string) (sq.SelectBuilder, error) { +func applyClaimableBalancesQueriesCursor(sql sq.SelectBuilder, tableName string, lCursor int64, rCursor string, order string) (sq.SelectBuilder, error) { hasPagedLimit := false if lCursor > 0 && rCursor != "" { hasPagedLimit = true @@ -67,17 +68,26 @@ func applyClaimableBalancesQueriesCursor(sql sq.SelectBuilder, lCursor int64, rC case db2.OrderAscending: if hasPagedLimit { sql = sql. - Where(sq.Expr("(cb.last_modified_ledger, cb.id) > (?, ?)", lCursor, rCursor)) - + Where( + sq.Expr( + fmt.Sprintf("(%s.last_modified_ledger, %s.id) > (?, ?)", tableName, tableName), + lCursor, rCursor, + ), + ) } - sql = sql.OrderBy("cb.last_modified_ledger asc, cb.id asc") + sql = sql.OrderBy(fmt.Sprintf("%s.last_modified_ledger asc, %s.id asc", tableName, tableName)) case db2.OrderDescending: if hasPagedLimit { sql = sql. - Where(sq.Expr("(cb.last_modified_ledger, cb.id) < (?, ?)", lCursor, rCursor)) + Where( + sq.Expr( + fmt.Sprintf("(%s.last_modified_ledger, %s.id) < (?, ?)", tableName, tableName), + lCursor, + rCursor, + ), + ) } - - sql = sql.OrderBy("cb.last_modified_ledger desc, cb.id desc") + sql = sql.OrderBy(fmt.Sprintf("%s.last_modified_ledger desc, %s.id desc", tableName, tableName)) default: return sql, errors.Errorf("invalid order: %s", order) } @@ -216,7 +226,7 @@ func (q *Q) GetClaimableBalances(ctx context.Context, query ClaimableBalancesQue return nil, errors.Wrap(err, "error getting cursor") } - sql, err := applyClaimableBalancesQueriesCursor(selectClaimableBalances, l, r, query.PageQuery.Order) + sql, err := applyClaimableBalancesQueriesCursor(selectClaimableBalances, "cb", l, r, query.PageQuery.Order) if err != nil { return nil, errors.Wrap(err, "could not apply query to page") } @@ -242,10 +252,10 @@ func (q *Q) GetClaimableBalances(ctx context.Context, query ClaimableBalancesQue // does not perform efficiently. Instead, use a subquery (with LIMIT) to retrieve claimable balances based on // the claimant's address. - var selectClaimableBalanceClaimants = sq.Select("id").From("claimable_balance_claimants"). - Where("destination = ?", query.Claimant.Address()).Limit(query.PageQuery.Limit) + var selectClaimableBalanceClaimants = sq.Select("claimable_balance_claimants.id").From("claimable_balance_claimants"). + Where("claimable_balance_claimants.destination = ?", query.Claimant.Address()).Limit(query.PageQuery.Limit) - subSql, err := applyClaimableBalancesQueriesCursor(selectClaimableBalanceClaimants, l, r, query.PageQuery.Order) + subSql, err := applyClaimableBalancesQueriesCursor(selectClaimableBalanceClaimants, "claimable_balance_claimants", l, r, query.PageQuery.Order) if err != nil { return nil, errors.Wrap(err, "could not apply subquery to page") } diff --git a/services/horizon/internal/db2/history/claimable_balances_test.go b/services/horizon/internal/db2/history/claimable_balances_test.go index 769ab3bc13..2e6d621945 100644 --- a/services/horizon/internal/db2/history/claimable_balances_test.go +++ b/services/horizon/internal/db2/history/claimable_balances_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/guregu/null" + "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/services/horizon/internal/test" "github.com/stellar/go/xdr" @@ -203,7 +204,7 @@ func TestFindClaimableBalancesByDestination(t *testing.T) { tt.Assert.Equal(dest2, cbs[0].Claimants[1].Destination) // this validates the cb query with claimant and cb.id/ledger cursor parameters - query.PageQuery = db2.MustPageQuery(fmt.Sprintf("%v-%s", 150, cbs[0].BalanceID), false, "", 10) + query.PageQuery = db2.MustPageQuery(fmt.Sprintf("%v-%s", 150, cbs[0].BalanceID), false, "asc", 10) query.Claimant = xdr.MustAddressPtr(dest1) cbs, err = q.GetClaimableBalances(tt.Ctx, query) tt.Assert.NoError(err) @@ -212,7 +213,7 @@ func TestFindClaimableBalancesByDestination(t *testing.T) { // this validates the cb query with no claimant parameter, // should still produce working sql, as it triggers different LIMIT position in sql. - query.PageQuery = db2.MustPageQuery("", false, "", 1) + query.PageQuery = db2.MustPageQuery("", false, "desc", 1) query.Claimant = nil cbs, err = q.GetClaimableBalances(tt.Ctx, query) tt.Assert.NoError(err) From cae5b85fa226180e93fbe8a9e4738777acf77b9c Mon Sep 17 00:00:00 2001 From: shawn Date: Wed, 14 Feb 2024 15:21:29 -0800 Subject: [PATCH 059/234] /services/horizon/ingest: add SKIP_TXMETA (#5208) * Revert "services/horizon: Add DISABLE_SOROBAN_INGEST flag to skip soroban ingestion processing (#5176)" This reverts commit bfaf9e18b840d97e4d12afc3e2cd2bd56527642d. * #5189: added optional SKIP_TXMETA parameter to not persist tx meta in transaction model, removed DISABLE_SOROBAN_INGEST, use SKIP_META instead --- services/horizon/CHANGELOG.md | 9 + services/horizon/cmd/db.go | 2 +- services/horizon/internal/config.go | 4 +- services/horizon/internal/flags.go | 10 +- services/horizon/internal/ingest/main.go | 3 +- .../internal/ingest/processor_runner.go | 14 +- .../internal/ingest/processor_runner_test.go | 5 +- ...ble_balances_transaction_processor_test.go | 2 +- .../ingest/processors/effects_processor.go | 22 +- .../processors/effects_processor_test.go | 1 - .../processors/ledgers_processor_test.go | 45 +- ...uidity_pools_transaction_processor_test.go | 2 +- .../ingest/processors/operations_processor.go | 52 +- .../processors/operations_processor_test.go | 70 +-- .../processors/participants_processor_test.go | 8 +- .../processors/trades_processor_test.go | 4 +- .../transaction_operation_wrapper_test.go | 4 +- .../processors/transactions_processor.go | 48 +- .../processors/transactions_processor_test.go | 158 ++++- .../horizon/internal/ingest/verify_test.go | 4 +- services/horizon/internal/init.go | 2 +- .../integration/invokehostfunction_test.go | 103 +--- .../horizon/internal/integration/sac_test.go | 553 +++++++----------- .../internal/integration/transaction_test.go | 134 +++++ 24 files changed, 634 insertions(+), 625 deletions(-) create mode 100644 services/horizon/internal/integration/transaction_test.go diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 0e1b116923..cd125e019b 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## 2.28.3 + +### Added +- New optional config `SKIP_TXMETA` ([5189](https://github.com/stellar/go/issues/5189)). Defaults to `FALSE`, when `TRUE` the following will occur: + * history_transactions.tx_meta column will have serialized xdr that equates to empty for any protocol version, such as for `xdr.TransactionMeta.V3`, `Operations`, `TxChangesAfter`, `TxChangesBefore` will be empty arrays and `SorobanMeta` will be nil. + +### Breaking Changes +- Removed `DISABLE_SOROBAN_INGEST` configuration parameter, use the new `SKIP_TXMETA` parameter instead. + ## 2.28.2 ### Fixed diff --git a/services/horizon/cmd/db.go b/services/horizon/cmd/db.go index 7d14ca314e..965d5d7173 100644 --- a/services/horizon/cmd/db.go +++ b/services/horizon/cmd/db.go @@ -420,7 +420,7 @@ func runDBReingestRange(ledgerRanges []history.LedgerRange, reingestForce bool, RoundingSlippageFilter: config.RoundingSlippageFilter, EnableIngestionFiltering: config.EnableIngestionFiltering, MaxLedgerPerFlush: maxLedgersPerFlush, - SkipSorobanIngestion: config.SkipSorobanIngestion, + SkipTxmeta: config.SkipTxmeta, } if ingestConfig.HistorySession, err = db.Open("postgres", config.DatabaseURL); err != nil { diff --git a/services/horizon/internal/config.go b/services/horizon/internal/config.go index 54f843b810..f1f8ac078d 100644 --- a/services/horizon/internal/config.go +++ b/services/horizon/internal/config.go @@ -109,6 +109,6 @@ type Config struct { Network string // DisableTxSub disables transaction submission functionality for Horizon. DisableTxSub bool - // SkipSorobanIngestion skips Soroban related ingestion processing. - SkipSorobanIngestion bool + // SkipTxmeta, when enabled, will not store meta xdr in history transaction table + SkipTxmeta bool } diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index 87deb28c48..4c8e4dc2f5 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -60,8 +60,8 @@ const ( EnableIngestionFilteringFlagName = "exp-enable-ingestion-filtering" // DisableTxSubFlagName is the command line flag for disabling transaction submission feature of Horizon DisableTxSubFlagName = "disable-tx-sub" - // SkipSorobanIngestionFlagName is the command line flag for disabling Soroban related ingestion processing - SkipSorobanIngestionFlagName = "disable-soroban-ingest" + // SkipTxmeta is the command line flag for disabling persistence of tx meta in history transaction table + SkipTxmeta = "skip-txmeta" // StellarPubnet is a constant representing the Stellar public network StellarPubnet = "pubnet" @@ -740,12 +740,12 @@ func Flags() (*Config, support.ConfigOptions) { UsedInCommands: IngestionCommands, }, &support.ConfigOption{ - Name: SkipSorobanIngestionFlagName, - ConfigKey: &config.SkipSorobanIngestion, + Name: SkipTxmeta, + ConfigKey: &config.SkipTxmeta, OptType: types.Bool, FlagDefault: false, Required: false, - Usage: "excludes Soroban data during ingestion processing", + Usage: "excludes tx meta from persistence on transaction history", UsedInCommands: IngestionCommands, }, } diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 98acd68f33..ae049800fc 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -109,8 +109,7 @@ type Config struct { EnableIngestionFiltering bool MaxLedgerPerFlush uint32 - - SkipSorobanIngestion bool + SkipTxmeta bool } const ( diff --git a/services/horizon/internal/ingest/processor_runner.go b/services/horizon/internal/ingest/processor_runner.go index a09442b49d..0023ca3140 100644 --- a/services/horizon/internal/ingest/processor_runner.go +++ b/services/horizon/internal/ingest/processor_runner.go @@ -111,7 +111,6 @@ func buildChangeProcessor( source ingestionSource, ledgerSequence uint32, networkPassphrase string, - skipSorobanIngestion bool, ) *groupChangeProcessors { statsChangeProcessor := &statsChangeProcessor{ StatsChangeProcessor: changeStats, @@ -145,13 +144,13 @@ func (s *ProcessorRunner) buildTransactionProcessor(ledgersProcessor *processors processors := []horizonTransactionProcessor{ statsLedgerTransactionProcessor, - processors.NewEffectProcessor(accountLoader, s.historyQ.NewEffectBatchInsertBuilder(), s.config.NetworkPassphrase, s.config.SkipSorobanIngestion), + processors.NewEffectProcessor(accountLoader, s.historyQ.NewEffectBatchInsertBuilder(), s.config.NetworkPassphrase), ledgersProcessor, - processors.NewOperationProcessor(s.historyQ.NewOperationBatchInsertBuilder(), s.config.NetworkPassphrase, s.config.SkipSorobanIngestion), + processors.NewOperationProcessor(s.historyQ.NewOperationBatchInsertBuilder(), s.config.NetworkPassphrase), tradeProcessor, processors.NewParticipantsProcessor(accountLoader, s.historyQ.NewTransactionParticipantsBatchInsertBuilder(), s.historyQ.NewOperationParticipantBatchInsertBuilder()), - processors.NewTransactionProcessor(s.historyQ.NewTransactionBatchInsertBuilder(), s.config.SkipSorobanIngestion), + processors.NewTransactionProcessor(s.historyQ.NewTransactionBatchInsertBuilder(), s.config.SkipTxmeta), processors.NewClaimableBalancesTransactionProcessor(cbLoader, s.historyQ.NewTransactionClaimableBalanceBatchInsertBuilder(), s.historyQ.NewOperationClaimableBalanceBatchInsertBuilder()), processors.NewLiquidityPoolsTransactionProcessor(lpLoader, @@ -173,10 +172,7 @@ func (s *ProcessorRunner) buildFilteredOutProcessor() *groupTransactionProcessor // when in online mode, the submission result processor must always run (regardless of filtering) var p []horizonTransactionProcessor if s.config.EnableIngestionFiltering { - txSubProc := processors.NewTransactionFilteredTmpProcessor( - s.historyQ.NewTransactionFilteredTmpBatchInsertBuilder(), - s.config.SkipSorobanIngestion, - ) + txSubProc := processors.NewTransactionFilteredTmpProcessor(s.historyQ.NewTransactionFilteredTmpBatchInsertBuilder(), s.config.SkipTxmeta) p = append(p, txSubProc) } @@ -239,7 +235,6 @@ func (s *ProcessorRunner) RunHistoryArchiveIngestion( historyArchiveSource, checkpointLedger, s.config.NetworkPassphrase, - s.config.SkipSorobanIngestion, ) if checkpointLedger == 1 { @@ -498,7 +493,6 @@ func (s *ProcessorRunner) RunAllProcessorsOnLedger(ledger xdr.LedgerCloseMeta) ( ledgerSource, ledger.LedgerSequence(), s.config.NetworkPassphrase, - s.config.SkipSorobanIngestion, ) err = s.runChangeProcessorOnLedger(groupChangeProcessors, ledger) if err != nil { diff --git a/services/horizon/internal/ingest/processor_runner_test.go b/services/horizon/internal/ingest/processor_runner_test.go index ddac48aa82..eaeca95661 100644 --- a/services/horizon/internal/ingest/processor_runner_test.go +++ b/services/horizon/internal/ingest/processor_runner_test.go @@ -180,7 +180,7 @@ func TestProcessorRunnerBuildChangeProcessor(t *testing.T) { } stats := &ingest.StatsChangeProcessor{} - processor := buildChangeProcessor(runner.historyQ, stats, ledgerSource, 123, "", false) + processor := buildChangeProcessor(runner.historyQ, stats, ledgerSource, 123, "") assert.IsType(t, &groupChangeProcessors{}, processor) assert.IsType(t, &statsChangeProcessor{}, processor.processors[0]) @@ -201,7 +201,7 @@ func TestProcessorRunnerBuildChangeProcessor(t *testing.T) { filters: &MockFilters{}, } - processor = buildChangeProcessor(runner.historyQ, stats, historyArchiveSource, 456, "", false) + processor = buildChangeProcessor(runner.historyQ, stats, historyArchiveSource, 456, "") assert.IsType(t, &groupChangeProcessors{}, processor) assert.IsType(t, &statsChangeProcessor{}, processor.processors[0]) @@ -271,7 +271,6 @@ func TestProcessorRunnerWithFilterEnabled(t *testing.T) { config := Config{ NetworkPassphrase: network.PublicNetworkPassphrase, EnableIngestionFiltering: true, - SkipSorobanIngestion: false, } q := &mockDBQ{} diff --git a/services/horizon/internal/ingest/processors/claimable_balances_transaction_processor_test.go b/services/horizon/internal/ingest/processors/claimable_balances_transaction_processor_test.go index 11ce54505a..ca918e08ea 100644 --- a/services/horizon/internal/ingest/processors/claimable_balances_transaction_processor_test.go +++ b/services/horizon/internal/ingest/processors/claimable_balances_transaction_processor_test.go @@ -67,7 +67,7 @@ func (s *ClaimableBalancesTransactionProcessorTestSuiteLedger) TestEmptyClaimabl func (s *ClaimableBalancesTransactionProcessorTestSuiteLedger) testOperationInserts(balanceID xdr.ClaimableBalanceId, body xdr.OperationBody, change xdr.LedgerEntryChange) { // Setup the transaction - txn := createTransaction(true, 1) + txn := createTransaction(true, 1, 2) txn.Envelope.Operations()[0].Body = body txn.UnsafeMeta.V = 2 txn.UnsafeMeta.V2.Operations = []xdr.OperationMeta{ diff --git a/services/horizon/internal/ingest/processors/effects_processor.go b/services/horizon/internal/ingest/processors/effects_processor.go index 830632f5f5..34e9f9169a 100644 --- a/services/horizon/internal/ingest/processors/effects_processor.go +++ b/services/horizon/internal/ingest/processors/effects_processor.go @@ -28,20 +28,17 @@ type EffectProcessor struct { accountLoader *history.AccountLoader batch history.EffectBatchInsertBuilder network string - skipSoroban bool } func NewEffectProcessor( accountLoader *history.AccountLoader, batch history.EffectBatchInsertBuilder, network string, - skipSoroban bool, ) *EffectProcessor { return &EffectProcessor{ accountLoader: accountLoader, batch: batch, network: network, - skipSoroban: skipSoroban, } } @@ -53,29 +50,14 @@ func (p *EffectProcessor) ProcessTransaction( return nil } - elidedTransaction := transaction - - if p.skipSoroban && - elidedTransaction.UnsafeMeta.V == 3 && - elidedTransaction.UnsafeMeta.V3.SorobanMeta != nil { - elidedTransaction.UnsafeMeta.V3 = &xdr.TransactionMetaV3{ - Ext: xdr.ExtensionPoint{}, - TxChangesBefore: xdr.LedgerEntryChanges{}, - Operations: []xdr.OperationMeta{}, - TxChangesAfter: xdr.LedgerEntryChanges{}, - SorobanMeta: nil, - } - } - - for opi, op := range elidedTransaction.Envelope.Operations() { + for opi, op := range transaction.Envelope.Operations() { operation := transactionOperationWrapper{ index: uint32(opi), - transaction: elidedTransaction, + transaction: transaction, operation: op, ledgerSequence: uint32(lcm.LedgerSequence()), network: p.network, } - if err := operation.ingestEffects(p.accountLoader, p.batch); err != nil { return errors.Wrapf(err, "reading operation %v effects", operation.ID()) } diff --git a/services/horizon/internal/ingest/processors/effects_processor_test.go b/services/horizon/internal/ingest/processors/effects_processor_test.go index 70af21737a..0243768fde 100644 --- a/services/horizon/internal/ingest/processors/effects_processor_test.go +++ b/services/horizon/internal/ingest/processors/effects_processor_test.go @@ -143,7 +143,6 @@ func (s *EffectsProcessorTestSuiteLedger) SetupTest() { s.accountLoader, s.mockBatchInsertBuilder, networkPassphrase, - false, ) s.txs = []ingest.LedgerTransaction{ diff --git a/services/horizon/internal/ingest/processors/ledgers_processor_test.go b/services/horizon/internal/ingest/processors/ledgers_processor_test.go index 1df6640266..7311168721 100644 --- a/services/horizon/internal/ingest/processors/ledgers_processor_test.go +++ b/services/horizon/internal/ingest/processors/ledgers_processor_test.go @@ -33,7 +33,7 @@ func TestLedgersProcessorTestSuiteLedger(t *testing.T) { suite.Run(t, new(LedgersProcessorTestSuiteLedger)) } -func createTransaction(successful bool, numOps int) ingest.LedgerTransaction { +func createTransaction(successful bool, numOps int, metaVer int32) ingest.LedgerTransaction { code := xdr.TransactionResultCodeTxSuccess if !successful { code = xdr.TransactionResultCodeTxFailed @@ -50,6 +50,32 @@ func createTransaction(successful bool, numOps int) ingest.LedgerTransaction { }) } sourceAID := xdr.MustAddress("GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY") + + var txMeta xdr.TransactionMeta + switch metaVer { + case 3: + txMeta = xdr.TransactionMeta{ + V: 3, + V3: &xdr.TransactionMetaV3{ + Operations: make([]xdr.OperationMeta, numOps, numOps), + }, + } + case 2: + txMeta = xdr.TransactionMeta{ + V: 2, + V2: &xdr.TransactionMetaV2{ + Operations: make([]xdr.OperationMeta, numOps, numOps), + }, + } + case 1: + txMeta = xdr.TransactionMeta{ + V: 1, + V1: &xdr.TransactionMetaV1{ + Operations: make([]xdr.OperationMeta, numOps, numOps), + }, + } + } + return ingest.LedgerTransaction{ Result: xdr.TransactionResultPair{ TransactionHash: xdr.Hash{}, @@ -70,12 +96,7 @@ func createTransaction(successful bool, numOps int) ingest.LedgerTransaction { }, }, }, - UnsafeMeta: xdr.TransactionMeta{ - V: 2, - V2: &xdr.TransactionMetaV2{ - Operations: make([]xdr.OperationMeta, numOps, numOps), - }, - }, + UnsafeMeta: txMeta, } } @@ -94,9 +115,9 @@ func (s *LedgersProcessorTestSuiteLedger) SetupTest() { ) s.txs = []ingest.LedgerTransaction{ - createTransaction(true, 1), - createTransaction(false, 3), - createTransaction(true, 4), + createTransaction(true, 1, 1), + createTransaction(false, 3, 2), + createTransaction(true, 4, 3), } s.successCount = 2 @@ -127,8 +148,8 @@ func (s *LedgersProcessorTestSuiteLedger) TestInsertLedgerSucceeds() { }, } nextTransactions := []ingest.LedgerTransaction{ - createTransaction(true, 1), - createTransaction(false, 2), + createTransaction(true, 1, 2), + createTransaction(false, 2, 2), } for _, tx := range nextTransactions { err := s.processor.ProcessTransaction(xdr.LedgerCloseMeta{ diff --git a/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor_test.go b/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor_test.go index 485d890dca..8d08e44d44 100644 --- a/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor_test.go +++ b/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor_test.go @@ -68,7 +68,7 @@ func (s *LiquidityPoolsTransactionProcessorTestSuiteLedger) TestEmptyLiquidityPo func (s *LiquidityPoolsTransactionProcessorTestSuiteLedger) testOperationInserts(poolID xdr.PoolId, body xdr.OperationBody, change xdr.LedgerEntryChange) { // Setup the transaction - txn := createTransaction(true, 1) + txn := createTransaction(true, 1, 2) txn.Envelope.Operations()[0].Body = body txn.UnsafeMeta.V = 2 txn.UnsafeMeta.V2.Operations = []xdr.OperationMeta{ diff --git a/services/horizon/internal/ingest/processors/operations_processor.go b/services/horizon/internal/ingest/processors/operations_processor.go index 92a4b870e9..8ad023145c 100644 --- a/services/horizon/internal/ingest/processors/operations_processor.go +++ b/services/horizon/internal/ingest/processors/operations_processor.go @@ -22,16 +22,14 @@ import ( // OperationProcessor operations processor type OperationProcessor struct { - batch history.OperationBatchInsertBuilder - network string - skipSoroban bool + batch history.OperationBatchInsertBuilder + network string } -func NewOperationProcessor(batch history.OperationBatchInsertBuilder, network string, skipSoroban bool) *OperationProcessor { +func NewOperationProcessor(batch history.OperationBatchInsertBuilder, network string) *OperationProcessor { return &OperationProcessor{ - batch: batch, - network: network, - skipSoroban: skipSoroban, + batch: batch, + network: network, } } @@ -39,12 +37,11 @@ func NewOperationProcessor(batch history.OperationBatchInsertBuilder, network st func (p *OperationProcessor) ProcessTransaction(lcm xdr.LedgerCloseMeta, transaction ingest.LedgerTransaction) error { for i, op := range transaction.Envelope.Operations() { operation := transactionOperationWrapper{ - index: uint32(i), - transaction: transaction, - operation: op, - ledgerSequence: lcm.LedgerSequence(), - network: p.network, - skipSorobanDetails: p.skipSoroban, + index: uint32(i), + transaction: transaction, + operation: op, + ledgerSequence: lcm.LedgerSequence(), + network: p.network, } details, err := operation.Details() if err != nil { @@ -85,12 +82,11 @@ func (p *OperationProcessor) Flush(ctx context.Context, session db.SessionInterf // transactionOperationWrapper represents the data for a single operation within a transaction type transactionOperationWrapper struct { - index uint32 - transaction ingest.LedgerTransaction - operation xdr.Operation - ledgerSequence uint32 - network string - skipSorobanDetails bool + index uint32 + transaction ingest.LedgerTransaction + operation xdr.Operation + ledgerSequence uint32 + network string } // ID returns the ID for the operation. @@ -270,11 +266,6 @@ func (operation *transactionOperationWrapper) IsPayment() bool { case xdr.OperationTypeAccountMerge: return true case xdr.OperationTypeInvokeHostFunction: - // #5175, may want to consider skipping this parsing of payment from contracts - // as part of eliding soroban ingestion aspects when DISABLE_SOROBAN_INGEST. - // but, may cause inconsistencies that aren't worth the gain, - // as payments won't be thoroughly accurate, i.e. a payment could have - // happened within a contract invoke. diagnosticEvents, err := operation.transaction.GetDiagnosticEvents() if err != nil { return false @@ -698,18 +689,11 @@ func (operation *transactionOperationWrapper) Details() (map[string]interface{}, } details["parameters"] = params - var balanceChanges []map[string]interface{} - var parseErr error - if operation.skipSorobanDetails { - // https://github.com/stellar/go/issues/5175 - // intentionally toggle off parsing soroban meta into "asset_balance_changes" - balanceChanges = make([]map[string]interface{}, 0) + if balanceChanges, err := operation.parseAssetBalanceChangesFromContractEvents(); err != nil { + return nil, err } else { - if balanceChanges, parseErr = operation.parseAssetBalanceChangesFromContractEvents(); parseErr != nil { - return nil, parseErr - } + details["asset_balance_changes"] = balanceChanges } - details["asset_balance_changes"] = balanceChanges case xdr.HostFunctionTypeHostFunctionTypeCreateContract: args := op.HostFunction.MustCreateContract() diff --git a/services/horizon/internal/ingest/processors/operations_processor_test.go b/services/horizon/internal/ingest/processors/operations_processor_test.go index 275a6056e4..f13641b69d 100644 --- a/services/horizon/internal/ingest/processors/operations_processor_test.go +++ b/services/horizon/internal/ingest/processors/operations_processor_test.go @@ -42,7 +42,6 @@ func (s *OperationsProcessorTestSuiteLedger) SetupTest() { s.processor = NewOperationProcessor( s.mockBatchInsertBuilder, "test network", - false, ) } @@ -376,65 +375,6 @@ func (s *OperationsProcessorTestSuiteLedger) TestOperationTypeInvokeHostFunction } s.Assert().Equal(found, 4, "should have one balance changed record for each of mint, burn, clawback, transfer") }) - - s.T().Run("InvokeContractAssetBalancesElidedFromDetails", func(t *testing.T) { - randomIssuer := keypair.MustRandom() - randomAsset := xdr.MustNewCreditAsset("TESTING", randomIssuer.Address()) - passphrase := "passphrase" - randomAccount := keypair.MustRandom().Address() - contractId := [32]byte{} - zeroContractStrKey, err := strkey.Encode(strkey.VersionByteContract, contractId[:]) - s.Assert().NoError(err) - - transferContractEvent := contractevents.GenerateEvent(contractevents.EventTypeTransfer, randomAccount, zeroContractStrKey, "", randomAsset, big.NewInt(10000000), passphrase) - burnContractEvent := contractevents.GenerateEvent(contractevents.EventTypeBurn, zeroContractStrKey, "", "", randomAsset, big.NewInt(10000000), passphrase) - mintContractEvent := contractevents.GenerateEvent(contractevents.EventTypeMint, "", zeroContractStrKey, randomAccount, randomAsset, big.NewInt(10000000), passphrase) - clawbackContractEvent := contractevents.GenerateEvent(contractevents.EventTypeClawback, zeroContractStrKey, "", randomAccount, randomAsset, big.NewInt(10000000), passphrase) - - tx = ingest.LedgerTransaction{ - UnsafeMeta: xdr.TransactionMeta{ - V: 3, - V3: &xdr.TransactionMetaV3{ - SorobanMeta: &xdr.SorobanTransactionMeta{ - Events: []xdr.ContractEvent{ - transferContractEvent, - burnContractEvent, - mintContractEvent, - clawbackContractEvent, - }, - }, - }, - }, - } - wrapper := transactionOperationWrapper{ - skipSorobanDetails: true, - transaction: tx, - operation: xdr.Operation{ - SourceAccount: &source, - Body: xdr.OperationBody{ - Type: xdr.OperationTypeInvokeHostFunction, - InvokeHostFunctionOp: &xdr.InvokeHostFunctionOp{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, - InvokeContract: &xdr.InvokeContractArgs{ - ContractAddress: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &xdr.Hash{0x1, 0x2}, - }, - FunctionName: "foo", - Args: xdr.ScVec{}, - }, - }, - }, - }, - }, - network: passphrase, - } - - details, err := wrapper.Details() - s.Assert().NoError(err) - s.Assert().Len(details["asset_balance_changes"], 0, "for invokehostfn op, no asset balances should be in details when skip soroban is enabled") - }) } func (s *OperationsProcessorTestSuiteLedger) assertInvokeHostFunctionParameter(parameters []map[string]string, paramPosition int, expectedType string, expectedVal xdr.ScVal) { @@ -467,7 +407,7 @@ func (s *OperationsProcessorTestSuiteLedger) TestAddOperationSucceeds() { Ed25519: *unmuxed.Ed25519, }, } - firstTx := createTransaction(true, 1) + firstTx := createTransaction(true, 1, 2) firstTx.Index = 1 firstTx.Envelope.Operations()[0].Body = xdr.OperationBody{ Type: xdr.OperationTypePayment, @@ -478,8 +418,8 @@ func (s *OperationsProcessorTestSuiteLedger) TestAddOperationSucceeds() { }, } firstTx.Envelope.V1.Tx.SourceAccount = muxed - secondTx := createTransaction(false, 3) - thirdTx := createTransaction(true, 4) + secondTx := createTransaction(false, 3, 2) + thirdTx := createTransaction(true, 4, 2) txs := []ingest.LedgerTransaction{ firstTx, @@ -511,7 +451,7 @@ func (s *OperationsProcessorTestSuiteLedger) TestAddOperationFails() { }, }, } - tx := createTransaction(true, 1) + tx := createTransaction(true, 1, 2) s.mockBatchInsertBuilder. On( @@ -542,7 +482,7 @@ func (s *OperationsProcessorTestSuiteLedger) TestExecFails() { }, }, } - tx := createTransaction(true, 1) + tx := createTransaction(true, 1, 2) s.mockBatchInsertBuilder. On( diff --git a/services/horizon/internal/ingest/processors/participants_processor_test.go b/services/horizon/internal/ingest/processors/participants_processor_test.go index 2348b79eaf..b81bd22f67 100644 --- a/services/horizon/internal/ingest/processors/participants_processor_test.go +++ b/services/horizon/internal/ingest/processors/participants_processor_test.go @@ -62,13 +62,13 @@ func (s *ParticipantsProcessorTestSuiteLedger) SetupTest() { "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", } - s.firstTx = createTransaction(true, 1) + s.firstTx = createTransaction(true, 1, 2) s.firstTx.Index = 1 aid := xdr.MustAddress(s.addresses[0]) s.firstTx.Envelope.V1.Tx.SourceAccount = aid.ToMuxedAccount() s.firstTxID = toid.New(int32(sequence), 1, 0).ToInt64() - s.secondTx = createTransaction(true, 1) + s.secondTx = createTransaction(true, 1, 2) s.secondTx.Index = 2 s.secondTx.Envelope.Operations()[0].Body = xdr.OperationBody{ Type: xdr.OperationTypeCreateAccount, @@ -80,7 +80,7 @@ func (s *ParticipantsProcessorTestSuiteLedger) SetupTest() { s.secondTx.Envelope.V1.Tx.SourceAccount = aid.ToMuxedAccount() s.secondTxID = toid.New(int32(sequence), 2, 0).ToInt64() - s.thirdTx = createTransaction(true, 1) + s.thirdTx = createTransaction(true, 1, 2) s.thirdTx.Index = 3 aid = xdr.MustAddress(s.addresses[0]) s.thirdTx.Envelope.V1.Tx.SourceAccount = aid.ToMuxedAccount() @@ -150,7 +150,7 @@ func (s *ParticipantsProcessorTestSuiteLedger) TestEmptyParticipants() { } func (s *ParticipantsProcessorTestSuiteLedger) TestFeeBumptransaction() { - feeBumpTx := createTransaction(true, 0) + feeBumpTx := createTransaction(true, 0, 2) feeBumpTx.Index = 1 aid := xdr.MustAddress(s.addresses[0]) feeBumpTx.Envelope.V1.Tx.SourceAccount = aid.ToMuxedAccount() diff --git a/services/horizon/internal/ingest/processors/trades_processor_test.go b/services/horizon/internal/ingest/processors/trades_processor_test.go index 8a7733f4d1..d243758846 100644 --- a/services/horizon/internal/ingest/processors/trades_processor_test.go +++ b/services/horizon/internal/ingest/processors/trades_processor_test.go @@ -229,7 +229,7 @@ func (s *TradeProcessorTestSuiteLedger) TearDownTest() { func (s *TradeProcessorTestSuiteLedger) TestIgnoreFailedTransactions() { ctx := context.Background() - err := s.processor.ProcessTransaction(s.lcm, createTransaction(false, 1)) + err := s.processor.ProcessTransaction(s.lcm, createTransaction(false, 1, 2)) s.Assert().NoError(err) err = s.processor.Flush(ctx, s.mockSession) @@ -806,7 +806,7 @@ func TestTradeProcessor_ProcessTransaction_MuxedAccount(t *testing.T) { Ed25519: *unmuxed.Ed25519, }, } - tx := createTransaction(true, 1) + tx := createTransaction(true, 1, 2) tx.Index = 1 tx.Envelope.Operations()[0].Body = xdr.OperationBody{ Type: xdr.OperationTypePayment, diff --git a/services/horizon/internal/ingest/processors/transaction_operation_wrapper_test.go b/services/horizon/internal/ingest/processors/transaction_operation_wrapper_test.go index 97b83f8f1b..48b1f729b4 100644 --- a/services/horizon/internal/ingest/processors/transaction_operation_wrapper_test.go +++ b/services/horizon/internal/ingest/processors/transaction_operation_wrapper_test.go @@ -798,7 +798,7 @@ func TestTransactionOperationDetails(t *testing.T) { }) } - tx := createTransaction(true, 1) + tx := createTransaction(true, 1, 2) tx.Index = 1 tx.Envelope.Operations()[0].Body = xdr.OperationBody{ @@ -1477,7 +1477,7 @@ var ( func getSponsoredSandwichWrappers() []*transactionOperationWrapper { const ledgerSeq = uint32(12345) - tx := createTransaction(true, 3) + tx := createTransaction(true, 3, 2) tx.Index = 1 tx.UnsafeMeta = xdr.TransactionMeta{ V: 2, diff --git a/services/horizon/internal/ingest/processors/transactions_processor.go b/services/horizon/internal/ingest/processors/transactions_processor.go index b82934d86a..28aae6f327 100644 --- a/services/horizon/internal/ingest/processors/transactions_processor.go +++ b/services/horizon/internal/ingest/processors/transactions_processor.go @@ -11,36 +11,50 @@ import ( ) type TransactionProcessor struct { - batch history.TransactionBatchInsertBuilder - skipSoroban bool + batch history.TransactionBatchInsertBuilder + skipTxmeta bool } -func NewTransactionFilteredTmpProcessor(batch history.TransactionBatchInsertBuilder, skipSoroban bool) *TransactionProcessor { +func NewTransactionFilteredTmpProcessor(batch history.TransactionBatchInsertBuilder, skipTxmeta bool) *TransactionProcessor { return &TransactionProcessor{ - batch: batch, - skipSoroban: skipSoroban, + batch: batch, + skipTxmeta: skipTxmeta, } } -func NewTransactionProcessor(batch history.TransactionBatchInsertBuilder, skipSoroban bool) *TransactionProcessor { +func NewTransactionProcessor(batch history.TransactionBatchInsertBuilder, skipTxmeta bool) *TransactionProcessor { return &TransactionProcessor{ - batch: batch, - skipSoroban: skipSoroban, + batch: batch, + skipTxmeta: skipTxmeta, } } func (p *TransactionProcessor) ProcessTransaction(lcm xdr.LedgerCloseMeta, transaction ingest.LedgerTransaction) error { elidedTransaction := transaction - if p.skipSoroban && - elidedTransaction.UnsafeMeta.V == 3 && - elidedTransaction.UnsafeMeta.MustV3().SorobanMeta != nil { - elidedTransaction.UnsafeMeta.V3 = &xdr.TransactionMetaV3{ - Ext: xdr.ExtensionPoint{}, - TxChangesBefore: xdr.LedgerEntryChanges{}, - Operations: []xdr.OperationMeta{}, - TxChangesAfter: xdr.LedgerEntryChanges{}, - SorobanMeta: nil, + if p.skipTxmeta { + switch elidedTransaction.UnsafeMeta.V { + case 3: + elidedTransaction.UnsafeMeta.V3 = &xdr.TransactionMetaV3{ + Ext: xdr.ExtensionPoint{}, + TxChangesBefore: xdr.LedgerEntryChanges{}, + Operations: []xdr.OperationMeta{}, + TxChangesAfter: xdr.LedgerEntryChanges{}, + SorobanMeta: nil, + } + case 2: + elidedTransaction.UnsafeMeta.V2 = &xdr.TransactionMetaV2{ + TxChangesBefore: xdr.LedgerEntryChanges{}, + Operations: []xdr.OperationMeta{}, + TxChangesAfter: xdr.LedgerEntryChanges{}, + } + case 1: + elidedTransaction.UnsafeMeta.V1 = &xdr.TransactionMetaV1{ + TxChanges: xdr.LedgerEntryChanges{}, + Operations: []xdr.OperationMeta{}, + } + default: + return errors.Errorf("SKIP_TXMETA is enabled, but received an un-supported tx-meta version %v, can't proceed with removal", elidedTransaction.UnsafeMeta.V) } } diff --git a/services/horizon/internal/ingest/processors/transactions_processor_test.go b/services/horizon/internal/ingest/processors/transactions_processor_test.go index 873a72af05..934924adb0 100644 --- a/services/horizon/internal/ingest/processors/transactions_processor_test.go +++ b/services/horizon/internal/ingest/processors/transactions_processor_test.go @@ -6,11 +6,13 @@ import ( "context" "testing" + "github.com/stellar/go/ingest" "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) @@ -36,7 +38,7 @@ func (s *TransactionsProcessorTestSuiteLedger) TearDownTest() { s.mockBatchInsertBuilder.AssertExpectations(s.T()) } -func (s *TransactionsProcessorTestSuiteLedger) TestAddTransactionsSucceeds() { +func (s *TransactionsProcessorTestSuiteLedger) TestAddTransactionsWithMetaSucceeds() { sequence := uint32(20) lcm := xdr.LedgerCloseMeta{ V0: &xdr.LedgerCloseMetaV0{ @@ -47,13 +49,55 @@ func (s *TransactionsProcessorTestSuiteLedger) TestAddTransactionsSucceeds() { }, }, } - firstTx := createTransaction(true, 1) - secondTx := createTransaction(false, 3) - thirdTx := createTransaction(true, 4) + creator := xdr.MustAddress("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") + ledgerEntryChange := xdr.LedgerEntryChange{ + Type: 3, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 0x39, + Data: xdr.LedgerEntryData{ + Type: 0, + Account: &xdr.AccountEntry{ + AccountId: creator, + Balance: 800152377009533292, + SeqNum: 25, + InflationDest: &creator, + Thresholds: xdr.Thresholds{0x1, 0x0, 0x0, 0x0}, + }, + }, + }, + } + + firstTx := createTransaction(true, 1, 1) + firstTx.UnsafeMeta.V1.TxChanges = xdr.LedgerEntryChanges{ledgerEntryChange} + secondTx := createTransaction(false, 3, 2) + secondTx.UnsafeMeta.V2.TxChangesBefore = xdr.LedgerEntryChanges{ledgerEntryChange} + secondTx.UnsafeMeta.V2.TxChangesAfter = xdr.LedgerEntryChanges{ledgerEntryChange} + thirdTx := createTransaction(true, 4, 3) + thirdTx.UnsafeMeta.V3.TxChangesBefore = xdr.LedgerEntryChanges{ledgerEntryChange} + thirdTx.UnsafeMeta.V3.TxChangesAfter = xdr.LedgerEntryChanges{ledgerEntryChange} + thirdTx.UnsafeMeta.V3.SorobanMeta = &xdr.SorobanTransactionMeta{} + + s.mockBatchInsertBuilder.On("Add", firstTx, sequence).Run(func(args mock.Arguments) { + tx := args.Get(0).(ingest.LedgerTransaction) + s.Assert().Len(tx.UnsafeMeta.V1.TxChanges, 1) + s.Assert().Len(tx.UnsafeMeta.V1.Operations, 1) + }).Return(nil).Once() + + s.mockBatchInsertBuilder.On("Add", secondTx, sequence).Run(func(args mock.Arguments) { + tx := args.Get(0).(ingest.LedgerTransaction) + s.Assert().Len(tx.UnsafeMeta.V2.TxChangesAfter, 1) + s.Assert().Len(tx.UnsafeMeta.V2.TxChangesBefore, 1) + s.Assert().Len(tx.UnsafeMeta.V2.Operations, 3) + }).Return(nil).Once() + + s.mockBatchInsertBuilder.On("Add", thirdTx, sequence+1).Run(func(args mock.Arguments) { + tx := args.Get(0).(ingest.LedgerTransaction) + s.Assert().Len(tx.UnsafeMeta.V3.TxChangesAfter, 1) + s.Assert().Len(tx.UnsafeMeta.V3.TxChangesBefore, 1) + s.Assert().NotNil(tx.UnsafeMeta.V3.SorobanMeta) + s.Assert().Len(tx.UnsafeMeta.V3.Operations, 4) + }).Return(nil).Once() - s.mockBatchInsertBuilder.On("Add", firstTx, sequence).Return(nil).Once() - s.mockBatchInsertBuilder.On("Add", secondTx, sequence).Return(nil).Once() - s.mockBatchInsertBuilder.On("Add", thirdTx, sequence+1).Return(nil).Once() s.mockBatchInsertBuilder.On("Exec", s.ctx, s.mockSession).Return(nil).Once() s.Assert().NoError(s.processor.ProcessTransaction(lcm, firstTx)) @@ -64,6 +108,102 @@ func (s *TransactionsProcessorTestSuiteLedger) TestAddTransactionsSucceeds() { s.Assert().NoError(s.processor.Flush(s.ctx, s.mockSession)) } +func (s *TransactionsProcessorTestSuiteLedger) TestAddTransactionsWithSkippedMetaSucceeds() { + elidingTxProcessor := NewTransactionProcessor(s.mockBatchInsertBuilder, true) + + sequence := uint32(20) + lcm := xdr.LedgerCloseMeta{ + V0: &xdr.LedgerCloseMetaV0{ + LedgerHeader: xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + LedgerSeq: xdr.Uint32(sequence), + }, + }, + }, + } + creator := xdr.MustAddress("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") + ledgerEntryChange := xdr.LedgerEntryChange{ + Type: 3, + State: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 0x39, + Data: xdr.LedgerEntryData{ + Type: 0, + Account: &xdr.AccountEntry{ + AccountId: creator, + Balance: 800152377009533292, + SeqNum: 25, + InflationDest: &creator, + Thresholds: xdr.Thresholds{0x1, 0x0, 0x0, 0x0}, + }, + }, + }, + } + + firstTx := createTransaction(true, 1, 1) + firstTx.UnsafeMeta.V1.TxChanges = xdr.LedgerEntryChanges{ledgerEntryChange} + secondTx := createTransaction(false, 3, 2) + secondTx.UnsafeMeta.V2.TxChangesBefore = xdr.LedgerEntryChanges{ledgerEntryChange} + secondTx.UnsafeMeta.V2.TxChangesAfter = xdr.LedgerEntryChanges{ledgerEntryChange} + thirdTx := createTransaction(true, 4, 3) + thirdTx.UnsafeMeta.V3.TxChangesBefore = xdr.LedgerEntryChanges{ledgerEntryChange} + thirdTx.UnsafeMeta.V3.TxChangesAfter = xdr.LedgerEntryChanges{ledgerEntryChange} + thirdTx.UnsafeMeta.V3.SorobanMeta = &xdr.SorobanTransactionMeta{} + + s.mockBatchInsertBuilder.On("Add", mock.AnythingOfType("ingest.LedgerTransaction"), sequence).Run(func(args mock.Arguments) { + tx := args.Get(0).(ingest.LedgerTransaction) + s.Assert().Len(tx.UnsafeMeta.V1.TxChanges, 0) + s.Assert().Len(tx.UnsafeMeta.V1.Operations, 0) + }).Return(nil).Once() + + sequence++ + s.mockBatchInsertBuilder.On("Add", mock.AnythingOfType("ingest.LedgerTransaction"), sequence).Run(func(args mock.Arguments) { + tx := args.Get(0).(ingest.LedgerTransaction) + s.Assert().Len(tx.UnsafeMeta.V2.TxChangesAfter, 0) + s.Assert().Len(tx.UnsafeMeta.V2.TxChangesBefore, 0) + s.Assert().Len(tx.UnsafeMeta.V2.Operations, 0) + }).Return(nil).Once() + + sequence++ + s.mockBatchInsertBuilder.On("Add", mock.AnythingOfType("ingest.LedgerTransaction"), sequence).Run(func(args mock.Arguments) { + tx := args.Get(0).(ingest.LedgerTransaction) + s.Assert().Len(tx.UnsafeMeta.V3.TxChangesAfter, 0) + s.Assert().Len(tx.UnsafeMeta.V3.TxChangesBefore, 0) + s.Assert().Nil(tx.UnsafeMeta.V3.SorobanMeta) + s.Assert().Len(tx.UnsafeMeta.V3.Operations, 0) + }).Return(nil).Once() + + s.mockBatchInsertBuilder.On("Exec", s.ctx, s.mockSession).Return(nil).Once() + + s.Assert().NoError(elidingTxProcessor.ProcessTransaction(lcm, firstTx)) + lcm.V0.LedgerHeader.Header.LedgerSeq++ + s.Assert().NoError(elidingTxProcessor.ProcessTransaction(lcm, secondTx)) + lcm.V0.LedgerHeader.Header.LedgerSeq++ + s.Assert().NoError(elidingTxProcessor.ProcessTransaction(lcm, thirdTx)) + + s.Assert().NoError(elidingTxProcessor.Flush(s.ctx, s.mockSession)) +} + +func (s *TransactionsProcessorTestSuiteLedger) TestAddTransactionsWithSkippedMetaFails() { + elidingTxProcessor := NewTransactionProcessor(s.mockBatchInsertBuilder, true) + + sequence := uint32(20) + lcm := xdr.LedgerCloseMeta{ + V0: &xdr.LedgerCloseMetaV0{ + LedgerHeader: xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + LedgerSeq: xdr.Uint32(sequence), + }, + }, + }, + } + + firstTx := createTransaction(true, 1, 1) + // intentionally mangle the transaction to have an invalid tx meta version + firstTx.UnsafeMeta.V = 8 + + s.Assert().ErrorContains(elidingTxProcessor.ProcessTransaction(lcm, firstTx), "received an un-supported tx-meta version 8") +} + func (s *TransactionsProcessorTestSuiteLedger) TestAddTransactionsFails() { sequence := uint32(20) lcm := xdr.LedgerCloseMeta{ @@ -75,7 +215,7 @@ func (s *TransactionsProcessorTestSuiteLedger) TestAddTransactionsFails() { }, }, } - firstTx := createTransaction(true, 1) + firstTx := createTransaction(true, 1, 2) s.mockBatchInsertBuilder.On("Add", firstTx, sequence). Return(errors.New("transient error")).Once() @@ -95,7 +235,7 @@ func (s *TransactionsProcessorTestSuiteLedger) TestExecFails() { }, }, } - firstTx := createTransaction(true, 1) + firstTx := createTransaction(true, 1, 2) s.mockBatchInsertBuilder.On("Add", firstTx, sequence).Return(nil).Once() s.mockBatchInsertBuilder.On("Exec", s.ctx, s.mockSession).Return(errors.New("transient error")).Once() diff --git a/services/horizon/internal/ingest/verify_test.go b/services/horizon/internal/ingest/verify_test.go index e3c0e4ec56..901f21a0ca 100644 --- a/services/horizon/internal/ingest/verify_test.go +++ b/services/horizon/internal/ingest/verify_test.go @@ -292,7 +292,7 @@ func TestStateVerifierLockBusy(t *testing.T) { tt.Assert.NoError(q.BeginTx(tt.Ctx, &sql.TxOptions{})) checkpointLedger := uint32(63) - changeProcessor := buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, ledgerSource, checkpointLedger, "", false) + changeProcessor := buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, ledgerSource, checkpointLedger, "") gen := randxdr.NewGenerator() var changes []xdr.LedgerEntryChange @@ -350,7 +350,7 @@ func TestStateVerifier(t *testing.T) { ledger := rand.Int31() checkpointLedger := uint32(ledger - (ledger % 64) - 1) - changeProcessor := buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, ledgerSource, checkpointLedger, "", false) + changeProcessor := buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, ledgerSource, checkpointLedger, "") mockChangeReader := &ingest.MockChangeReader{} gen := randxdr.NewGenerator() diff --git a/services/horizon/internal/init.go b/services/horizon/internal/init.go index 60ba7b6c2a..0c0fe2c2cd 100644 --- a/services/horizon/internal/init.go +++ b/services/horizon/internal/init.go @@ -111,7 +111,7 @@ func initIngester(app *App) { EnableExtendedLogLedgerStats: app.config.IngestEnableExtendedLogLedgerStats, RoundingSlippageFilter: app.config.RoundingSlippageFilter, EnableIngestionFiltering: app.config.EnableIngestionFiltering, - SkipSorobanIngestion: app.config.SkipSorobanIngestion, + SkipTxmeta: app.config.SkipTxmeta, }) if err != nil { diff --git a/services/horizon/internal/integration/invokehostfunction_test.go b/services/horizon/internal/integration/invokehostfunction_test.go index 1b1edc091a..275f0de23b 100644 --- a/services/horizon/internal/integration/invokehostfunction_test.go +++ b/services/horizon/internal/integration/invokehostfunction_test.go @@ -3,13 +3,11 @@ package integration import ( "crypto/sha256" "encoding/hex" - "fmt" "os" "path/filepath" "testing" "github.com/stellar/go/clients/horizonclient" - "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/protocols/horizon/operations" "github.com/stellar/go/services/horizon/internal/test/integration" "github.com/stellar/go/txnbuild" @@ -26,42 +24,13 @@ const increment_contract = "soroban_increment_contract.wasm" // Refer to ./services/horizon/internal/integration/contracts/README.md on how to recompile // contract code if needed to new wasm. -func TestInvokeHostFns(t *testing.T) { - // first test contracts when soroban processing is enabled - DisabledSoroban = false - runAllTests(t) - // now test same contracts when soroban processing is disabled - DisabledSoroban = true - runAllTests(t) -} - -func runAllTests(t *testing.T) { - tests := []struct { - name string - fn func(*testing.T) - }{ - {"CaseContractInvokeHostFunctionInstallContract", CaseContractInvokeHostFunctionInstallContract}, - {"CaseContractInvokeHostFunctionCreateContractByAddress", CaseContractInvokeHostFunctionCreateContractByAddress}, - {"CaseContractInvokeHostFunctionInvokeStatelessContractFn", CaseContractInvokeHostFunctionInvokeStatelessContractFn}, - {"CaseContractInvokeHostFunctionInvokeStatefulContractFn", CaseContractInvokeHostFunctionInvokeStatefulContractFn}, - } - - for _, tt := range tests { - t.Run(fmt.Sprintf("Soroban Processing Disabled = %v. ", DisabledSoroban)+tt.name, func(t *testing.T) { - tt.fn(t) - }) - } -} - -func CaseContractInvokeHostFunctionInstallContract(t *testing.T) { +func TestContractInvokeHostFunctionInstallContract(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, - HorizonEnvironment: map[string]string{ - "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, + ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -77,7 +46,6 @@ func CaseContractInvokeHostFunctionInstallContract(t *testing.T) { clientTx, err := itest.Client().TransactionDetail(tx.Hash) require.NoError(t, err) - verifySorobanMeta(t, clientTx) assert.Equal(t, tx.Hash, clientTx.Hash) var txResult xdr.TransactionResult @@ -103,17 +71,16 @@ func CaseContractInvokeHostFunctionInstallContract(t *testing.T) { invokeHostFunctionOpJson, ok := clientInvokeOp.Embedded.Records[0].(operations.InvokeHostFunction) assert.True(t, ok) assert.Equal(t, invokeHostFunctionOpJson.Function, "HostFunctionTypeHostFunctionTypeUploadContractWasm") + } -func CaseContractInvokeHostFunctionCreateContractByAddress(t *testing.T) { +func TestContractInvokeHostFunctionCreateContractByAddress(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, - HorizonEnvironment: map[string]string{ - "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, + ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -136,7 +103,6 @@ func CaseContractInvokeHostFunctionCreateContractByAddress(t *testing.T) { clientTx, err := itest.Client().TransactionDetail(tx.Hash) require.NoError(t, err) - verifySorobanMeta(t, clientTx) assert.Equal(t, tx.Hash, clientTx.Hash) var txResult xdr.TransactionResult @@ -162,15 +128,13 @@ func CaseContractInvokeHostFunctionCreateContractByAddress(t *testing.T) { assert.Equal(t, invokeHostFunctionOpJson.Salt, "110986164698320180327942133831752629430491002266485370052238869825166557303060") } -func CaseContractInvokeHostFunctionInvokeStatelessContractFn(t *testing.T) { +func TestContractInvokeHostFunctionInvokeStatelessContractFn(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, - HorizonEnvironment: map[string]string{ - "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, + ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -232,7 +196,6 @@ func CaseContractInvokeHostFunctionInvokeStatelessContractFn(t *testing.T) { clientTx, err := itest.Client().TransactionDetail(tx.Hash) require.NoError(t, err) - verifySorobanMeta(t, clientTx) assert.Equal(t, tx.Hash, clientTx.Hash) var txResult xdr.TransactionResult @@ -246,14 +209,12 @@ func CaseContractInvokeHostFunctionInvokeStatelessContractFn(t *testing.T) { assert.True(t, ok) assert.Equal(t, invokeHostFunctionResult.Code, xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess) - if !DisabledSoroban { - // check the function response, should have summed the two input numbers - invokeResult := xdr.Uint64(9) - expectedScVal := xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &invokeResult} - var transactionMeta xdr.TransactionMeta - assert.NoError(t, xdr.SafeUnmarshalBase64(tx.ResultMetaXdr, &transactionMeta)) - assert.True(t, expectedScVal.Equals(transactionMeta.V3.SorobanMeta.ReturnValue)) - } + // check the function response, should have summed the two input numbers + invokeResult := xdr.Uint64(9) + expectedScVal := xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &invokeResult} + var transactionMeta xdr.TransactionMeta + assert.NoError(t, xdr.SafeUnmarshalBase64(tx.ResultMetaXdr, &transactionMeta)) + assert.True(t, expectedScVal.Equals(transactionMeta.V3.SorobanMeta.ReturnValue)) clientInvokeOp, err := itest.Client().Operations(horizonclient.OperationRequest{ ForTransaction: tx.Hash, @@ -276,15 +237,13 @@ func CaseContractInvokeHostFunctionInvokeStatelessContractFn(t *testing.T) { assert.Equal(t, invokeHostFunctionOpJson.Parameters[3].Type, "U64") } -func CaseContractInvokeHostFunctionInvokeStatefulContractFn(t *testing.T) { +func TestContractInvokeHostFunctionInvokeStatefulContractFn(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, - HorizonEnvironment: map[string]string{ - "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, + ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -333,7 +292,6 @@ func CaseContractInvokeHostFunctionInvokeStatefulContractFn(t *testing.T) { clientTx, err := itest.Client().TransactionDetail(tx.Hash) require.NoError(t, err) - verifySorobanMeta(t, clientTx) assert.Equal(t, tx.Hash, clientTx.Hash) var txResult xdr.TransactionResult @@ -347,14 +305,12 @@ func CaseContractInvokeHostFunctionInvokeStatefulContractFn(t *testing.T) { assert.True(t, ok) assert.Equal(t, invokeHostFunctionResult.Code, xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess) - if !DisabledSoroban { - // check the function response, should have incremented state from 0 to 1 - invokeResult := xdr.Uint32(1) - expectedScVal := xdr.ScVal{Type: xdr.ScValTypeScvU32, U32: &invokeResult} - var transactionMeta xdr.TransactionMeta - assert.NoError(t, xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &transactionMeta)) - assert.True(t, expectedScVal.Equals(transactionMeta.V3.SorobanMeta.ReturnValue)) - } + // check the function response, should have incremented state from 0 to 1 + invokeResult := xdr.Uint32(1) + expectedScVal := xdr.ScVal{Type: xdr.ScValTypeScvU32, U32: &invokeResult} + var transactionMeta xdr.TransactionMeta + assert.NoError(t, xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &transactionMeta)) + assert.True(t, expectedScVal.Equals(transactionMeta.V3.SorobanMeta.ReturnValue)) clientInvokeOp, err := itest.Client().Operations(horizonclient.OperationRequest{ ForTransaction: tx.Hash, @@ -428,20 +384,3 @@ func assembleCreateContractOp(t *testing.T, sourceAccount string, wasmFileName s SourceAccount: sourceAccount, } } - -func verifySorobanMeta(t *testing.T, clientTx horizon.Transaction) { - var txMeta xdr.TransactionMeta - err := xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &txMeta) - require.NoError(t, err) - require.NotNil(t, txMeta.V3) - - if !DisabledSoroban { - require.NotNil(t, txMeta.V3.SorobanMeta) - return - } - - require.Empty(t, txMeta.V3.Operations) - require.Empty(t, txMeta.V3.TxChangesAfter) - require.Empty(t, txMeta.V3.TxChangesBefore) - require.Nil(t, txMeta.V3.SorobanMeta) -} diff --git a/services/horizon/internal/integration/sac_test.go b/services/horizon/internal/integration/sac_test.go index c790b5a54c..64c772b44c 100644 --- a/services/horizon/internal/integration/sac_test.go +++ b/services/horizon/internal/integration/sac_test.go @@ -2,7 +2,6 @@ package integration import ( "context" - "fmt" "math" "math/big" "strings" @@ -31,127 +30,19 @@ const sac_contract = "soroban_sac_test.wasm" // of the integration tests. const LongTermTTL = 10000 -var ( - DisabledSoroban bool -) - -func TestSAC(t *testing.T) { - // first test contracts when soroban processing is enabled - DisabledSoroban = false - runAllSACTests(t) - // now test same contracts when soroban processing is disabled - DisabledSoroban = true - runAllSACTests(t) -} - -func runAllSACTests(t *testing.T) { - tests := []struct { - name string - fn func(*testing.T) - }{ - {"CaseContractMintToAccount", CaseContractMintToAccount}, - {"CaseContractMintToContract", CaseContractMintToContract}, - {"CaseExpirationAndRestoration", CaseExpirationAndRestoration}, - {"CaseContractTransferBetweenAccounts", CaseContractTransferBetweenAccounts}, - {"CaseContractTransferBetweenAccountAndContract", CaseContractTransferBetweenAccountAndContract}, - {"CaseContractTransferBetweenContracts", CaseContractTransferBetweenContracts}, - {"CaseContractBurnFromAccount", CaseContractBurnFromAccount}, - {"CaseContractBurnFromContract", CaseContractBurnFromContract}, - {"CaseContractClawbackFromAccount", CaseContractClawbackFromAccount}, - {"CaseContractClawbackFromContract", CaseContractClawbackFromContract}, - } - - for _, tt := range tests { - t.Run(fmt.Sprintf("Soroban Processing Disabled = %v. ", DisabledSoroban)+tt.name, func(t *testing.T) { - tt.fn(t) - }) - } -} - // Tests use precompiled wasm bin files that are added to the testdata directory. // Refer to ./services/horizon/internal/integration/contracts/README.md on how to recompile // contract code if needed to new wasm. -func createSAC(itest *integration.Test, asset xdr.Asset) { - invokeHostFunction := &txnbuild.InvokeHostFunction{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeCreateContract, - CreateContract: &xdr.CreateContractArgs{ - ContractIdPreimage: xdr.ContractIdPreimage{ - Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAsset, - FromAsset: &asset, - }, - Executable: xdr.ContractExecutable{ - Type: xdr.ContractExecutableTypeContractExecutableStellarAsset, - WasmHash: nil, - }, - }, - }, - SourceAccount: itest.Master().Address(), - } - _, _, preFlightOp := assertInvokeHostFnSucceeds(itest, itest.Master(), invokeHostFunction) - sourceAccount, extendTTLOp, minFee := itest.PreflightExtendExpiration( - itest.Master().Address(), - preFlightOp.Ext.SorobanData.Resources.Footprint.ReadWrite, - LongTermTTL, - ) - itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &extendTTLOp) -} - -func invokeStoreSet( - itest *integration.Test, - storeContractID xdr.Hash, - ledgerEntryData xdr.LedgerEntryData, -) *txnbuild.InvokeHostFunction { - key := ledgerEntryData.MustContractData().Key - val := ledgerEntryData.MustContractData().Val - return &txnbuild.InvokeHostFunction{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, - InvokeContract: &xdr.InvokeContractArgs{ - ContractAddress: contractIDParam(storeContractID), - FunctionName: "set", - Args: xdr.ScVec{ - key, - val, - }, - }, - }, - SourceAccount: itest.Master().Address(), - } -} - -func invokeStoreRemove( - itest *integration.Test, - storeContractID xdr.Hash, - ledgerKey xdr.LedgerKey, -) *txnbuild.InvokeHostFunction { - return &txnbuild.InvokeHostFunction{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, - InvokeContract: &xdr.InvokeContractArgs{ - ContractAddress: contractIDParam(storeContractID), - FunctionName: "remove", - Args: xdr.ScVec{ - ledgerKey.MustContractData().Key, - }, - }, - }, - SourceAccount: itest.Master().Address(), - } -} - -func CaseContractMintToAccount(t *testing.T) { +func TestContractMintToAccount(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, - HorizonEnvironment: map[string]string{ - "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban), - }, - EnableSorobanRPC: true, + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{"INGEST_DISABLE_STATE_VERIFICATION": "true", "CONNECTION_TIMEOUT": "360000"}, + EnableSorobanRPC: true, }) issuer := itest.Master().Address() @@ -181,21 +72,16 @@ func CaseContractMintToAccount(t *testing.T) { balanceContracts: big.NewInt(0), contractID: stellarAssetContractID(itest, asset), }) - assertEventPayments(itest, mintTx, asset, "", recipient.GetAccountID(), "mint", "20.0000000") - if !DisabledSoroban { - fx := getTxEffects(itest, mintTx, asset) - require.Len(t, fx, 1) - creditEffect := assertContainsEffect(t, fx, - effects.EffectAccountCredited)[0].(effects.AccountCredited) - assert.Equal(t, recipientKp.Address(), creditEffect.Account) - assert.Equal(t, issuer, creditEffect.Asset.Issuer) - assert.Equal(t, code, creditEffect.Asset.Code) - assert.Equal(t, "20.0000000", creditEffect.Amount) - } else { - fx := getTxEffects(itest, mintTx, asset) - require.Len(t, fx, 0) - } + fx := getTxEffects(itest, mintTx, asset) + require.Len(t, fx, 1) + creditEffect := assertContainsEffect(t, fx, + effects.EffectAccountCredited)[0].(effects.AccountCredited) + assert.Equal(t, recipientKp.Address(), creditEffect.Account) + assert.Equal(t, issuer, creditEffect.Asset.Issuer) + assert.Equal(t, code, creditEffect.Asset.Code) + assert.Equal(t, "20.0000000", creditEffect.Amount) + assertEventPayments(itest, mintTx, asset, "", recipient.GetAccountID(), "mint", "20.0000000") otherRecipientKp, otherRecipient := itest.CreateAccount("100") itest.MustEstablishTrustline(otherRecipientKp, otherRecipient, txnbuild.MustAssetFromXDR(asset)) @@ -208,6 +94,12 @@ func CaseContractMintToAccount(t *testing.T) { ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("20")) assertContainsBalance(itest, otherRecipientKp, issuer, code, amount.MustParse("30")) + + fx = getTxEffects(itest, transferTx, asset) + assert.Len(t, fx, 2) + assertContainsEffect(t, fx, + effects.EffectAccountCredited, + effects.EffectAccountDebited) assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -219,28 +111,41 @@ func CaseContractMintToAccount(t *testing.T) { balanceContracts: big.NewInt(0), contractID: stellarAssetContractID(itest, asset), }) +} - if !DisabledSoroban { - fx := getTxEffects(itest, transferTx, asset) - assert.Len(t, fx, 2) - assertContainsEffect(t, fx, - effects.EffectAccountCredited, - effects.EffectAccountDebited) - } else { - fx := getTxEffects(itest, transferTx, asset) - require.Len(t, fx, 0) +func createSAC(itest *integration.Test, asset xdr.Asset) { + invokeHostFunction := &txnbuild.InvokeHostFunction{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeCreateContract, + CreateContract: &xdr.CreateContractArgs{ + ContractIdPreimage: xdr.ContractIdPreimage{ + Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAsset, + FromAsset: &asset, + }, + Executable: xdr.ContractExecutable{ + Type: xdr.ContractExecutableTypeContractExecutableStellarAsset, + WasmHash: nil, + }, + }, + }, + SourceAccount: itest.Master().Address(), } + _, _, preFlightOp := assertInvokeHostFnSucceeds(itest, itest.Master(), invokeHostFunction) + sourceAccount, extendTTLOp, minFee := itest.PreflightExtendExpiration( + itest.Master().Address(), + preFlightOp.Ext.SorobanData.Resources.Footprint.ReadWrite, + LongTermTTL, + ) + itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &extendTTLOp) } -func CaseContractMintToContract(t *testing.T) { +func TestContractMintToContract(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, - HorizonEnvironment: map[string]string{ - "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, + ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -265,25 +170,19 @@ func CaseContractMintToContract(t *testing.T) { i128Param(int64(mintAmount.Hi), uint64(mintAmount.Lo)), contractAddressParam(recipientContractID)), ) + assertContainsEffect(t, getTxEffects(itest, mintTx, asset), + effects.EffectContractCredited) + balanceAmount, _, _ := assertInvokeHostFnSucceeds( + itest, + itest.Master(), + contractBalance(itest, issuer, asset, recipientContractID), + ) + assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) + assert.Equal(itest.CurrentTest(), xdr.Uint64(math.MaxUint64-3), (*balanceAmount.I128).Lo) + assert.Equal(itest.CurrentTest(), xdr.Int64(math.MaxInt64), (*balanceAmount.I128).Hi) assertEventPayments(itest, mintTx, asset, "", strkeyRecipientContractID, "mint", amount.String128(mintAmount)) - if !DisabledSoroban { - assertContainsEffect(t, getTxEffects(itest, mintTx, asset), - effects.EffectContractCredited) - - balanceAmount, _, _ := assertInvokeHostFnSucceeds( - itest, - itest.Master(), - contractBalance(itest, issuer, asset, recipientContractID), - ) - assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) - assert.Equal(itest.CurrentTest(), xdr.Uint64(math.MaxUint64-3), (*balanceAmount.I128).Lo) - assert.Equal(itest.CurrentTest(), xdr.Int64(math.MaxInt64), (*balanceAmount.I128).Hi) - } else { - fx := getTxEffects(itest, mintTx, asset) - require.Len(t, fx, 0) - } // calling transfer from the issuer account will also mint the asset _, transferTx, _ := assertInvokeHostFnSucceeds( itest, @@ -291,6 +190,19 @@ func CaseContractMintToContract(t *testing.T) { transferWithAmount(itest, issuer, asset, i128Param(0, 3), contractAddressParam(recipientContractID)), ) + assertContainsEffect(t, getTxEffects(itest, transferTx, asset), + effects.EffectAccountDebited, + effects.EffectContractCredited) + + balanceAmount, _, _ = assertInvokeHostFnSucceeds( + itest, + itest.Master(), + contractBalance(itest, issuer, asset, recipientContractID), + ) + assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) + assert.Equal(itest.CurrentTest(), xdr.Uint64(math.MaxUint64), (*balanceAmount.I128).Lo) + assert.Equal(itest.CurrentTest(), xdr.Int64(math.MaxInt64), (*balanceAmount.I128).Hi) + // 2^127 - 1 balanceContracts := new(big.Int).Lsh(big.NewInt(1), 127) balanceContracts.Sub(balanceContracts, big.NewInt(1)) @@ -305,27 +217,9 @@ func CaseContractMintToContract(t *testing.T) { balanceContracts: balanceContracts, contractID: stellarAssetContractID(itest, asset), }) - - if !DisabledSoroban { - assertContainsEffect(t, getTxEffects(itest, transferTx, asset), - effects.EffectAccountDebited, - effects.EffectContractCredited) - - balanceAmount, _, _ := assertInvokeHostFnSucceeds( - itest, - itest.Master(), - contractBalance(itest, issuer, asset, recipientContractID), - ) - assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) - assert.Equal(itest.CurrentTest(), xdr.Uint64(math.MaxUint64), (*balanceAmount.I128).Lo) - assert.Equal(itest.CurrentTest(), xdr.Int64(math.MaxInt64), (*balanceAmount.I128).Hi) - } else { - fx := getTxEffects(itest, transferTx, asset) - require.Len(t, fx, 0) - } } -func CaseExpirationAndRestoration(t *testing.T) { +func TestExpirationAndRestoration(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } @@ -338,7 +232,6 @@ func CaseExpirationAndRestoration(t *testing.T) { // a fake asset contract in the horizon db and we don't // want state verification to detect this "ingest-disable-state-verification": "true", - "disable-soroban-ingest": fmt.Sprint(DisabledSoroban), }, }) @@ -401,7 +294,6 @@ func CaseExpirationAndRestoration(t *testing.T) { LongTermTTL, ) itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &extendTTLOp) - assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -429,16 +321,6 @@ func CaseExpirationAndRestoration(t *testing.T) { balanceToExpire, ), ) - - balanceToExpireLedgerKey := xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.LedgerKeyContractData{ - Contract: balanceToExpire.ContractData.Contract, - Key: balanceToExpire.ContractData.Key, - Durability: balanceToExpire.ContractData.Durability, - }, - } - assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -451,6 +333,14 @@ func CaseExpirationAndRestoration(t *testing.T) { contractID: storeContractID, }) + balanceToExpireLedgerKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.LedgerKeyContractData{ + Contract: balanceToExpire.ContractData.Contract, + Key: balanceToExpire.ContractData.Key, + Durability: balanceToExpire.ContractData.Durability, + }, + } // The TESTING_MINIMUM_PERSISTENT_ENTRY_LIFETIME=10 configuration in stellar-core // will ensure that the ledger entry expires after 10 ledgers. // Because ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING is set to true, 10 ledgers @@ -482,7 +372,6 @@ func CaseExpirationAndRestoration(t *testing.T) { ), ), ) - assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -501,7 +390,6 @@ func CaseExpirationAndRestoration(t *testing.T) { balanceToExpireLedgerKey, ) itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &restoreFootprint) - assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -531,7 +419,6 @@ func CaseExpirationAndRestoration(t *testing.T) { ), ), ) - assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -557,7 +444,6 @@ func CaseExpirationAndRestoration(t *testing.T) { ), ), ) - assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -571,15 +457,56 @@ func CaseExpirationAndRestoration(t *testing.T) { }) } -func CaseContractTransferBetweenAccounts(t *testing.T) { +func invokeStoreSet( + itest *integration.Test, + storeContractID xdr.Hash, + ledgerEntryData xdr.LedgerEntryData, +) *txnbuild.InvokeHostFunction { + key := ledgerEntryData.MustContractData().Key + val := ledgerEntryData.MustContractData().Val + return &txnbuild.InvokeHostFunction{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, + InvokeContract: &xdr.InvokeContractArgs{ + ContractAddress: contractIDParam(storeContractID), + FunctionName: "set", + Args: xdr.ScVec{ + key, + val, + }, + }, + }, + SourceAccount: itest.Master().Address(), + } +} + +func invokeStoreRemove( + itest *integration.Test, + storeContractID xdr.Hash, + ledgerKey xdr.LedgerKey, +) *txnbuild.InvokeHostFunction { + return &txnbuild.InvokeHostFunction{ + HostFunction: xdr.HostFunction{ + Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, + InvokeContract: &xdr.InvokeContractArgs{ + ContractAddress: contractIDParam(storeContractID), + FunctionName: "remove", + Args: xdr.ScVec{ + ledgerKey.MustContractData().Key, + }, + }, + }, + SourceAccount: itest.Master().Address(), + } +} + +func TestContractTransferBetweenAccounts(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, - HorizonEnvironment: map[string]string{ - "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, + ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -607,7 +534,6 @@ func CaseContractTransferBetweenAccounts(t *testing.T) { ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1000")) - assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -631,6 +557,10 @@ func CaseContractTransferBetweenAccounts(t *testing.T) { assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("970")) assertContainsBalance(itest, otherRecipientKp, issuer, code, amount.MustParse("30")) + + fx := getTxEffects(itest, transferTx, asset) + assert.NotEmpty(t, fx) + assertContainsEffect(t, fx, effects.EffectAccountCredited, effects.EffectAccountDebited) assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -643,26 +573,15 @@ func CaseContractTransferBetweenAccounts(t *testing.T) { contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, transferTx, asset, recipientKp.Address(), otherRecipient.GetAccountID(), "transfer", "30.0000000") - - if !DisabledSoroban { - fx := getTxEffects(itest, transferTx, asset) - assert.NotEmpty(t, fx) - assertContainsEffect(t, fx, effects.EffectAccountCredited, effects.EffectAccountDebited) - } else { - fx := getTxEffects(itest, transferTx, asset) - require.Len(t, fx, 0) - } } -func CaseContractTransferBetweenAccountAndContract(t *testing.T) { +func TestContractTransferBetweenAccountAndContract(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, - HorizonEnvironment: map[string]string{ - "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, + ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -708,6 +627,9 @@ func CaseContractTransferBetweenAccountAndContract(t *testing.T) { mint(itest, issuer, asset, "1000", contractAddressParam(recipientContractID)), ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1000")) + assertContainsEffect(t, getTxEffects(itest, mintTx, asset), + effects.EffectContractCredited) + assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -720,14 +642,6 @@ func CaseContractTransferBetweenAccountAndContract(t *testing.T) { contractID: stellarAssetContractID(itest, asset), }) - if !DisabledSoroban { - assertContainsEffect(t, getTxEffects(itest, mintTx, asset), - effects.EffectContractCredited) - } else { - fx := getTxEffects(itest, mintTx, asset) - require.Len(t, fx, 0) - } - // transfer from account to contract _, transferTx, _ := assertInvokeHostFnSucceeds( itest, @@ -735,6 +649,8 @@ func CaseContractTransferBetweenAccountAndContract(t *testing.T) { transfer(itest, recipientKp.Address(), asset, "30", contractAddressParam(recipientContractID)), ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("970")) + assertContainsEffect(t, getTxEffects(itest, transferTx, asset), + effects.EffectAccountDebited, effects.EffectContractCredited) assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -748,19 +664,14 @@ func CaseContractTransferBetweenAccountAndContract(t *testing.T) { }) assertEventPayments(itest, transferTx, asset, recipientKp.Address(), strkeyRecipientContractID, "transfer", "30.0000000") - if !DisabledSoroban { - assertContainsEffect(t, getTxEffects(itest, transferTx, asset), - effects.EffectAccountDebited, effects.EffectContractCredited) - } else { - fx := getTxEffects(itest, transferTx, asset) - require.Len(t, fx, 0) - } // transfer from contract to account _, transferTx, _ = assertInvokeHostFnSucceeds( itest, recipientKp, transferFromContract(itest, recipientKp.Address(), asset, recipientContractID, recipientContractHash, "500", accountAddressParam(recipient.GetAccountID())), ) + assertContainsEffect(t, getTxEffects(itest, transferTx, asset), + effects.EffectContractDebited, effects.EffectAccountCredited) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1470")) assertAssetStats(itest, assetStats{ code: code, @@ -775,13 +686,6 @@ func CaseContractTransferBetweenAccountAndContract(t *testing.T) { }) assertEventPayments(itest, transferTx, asset, strkeyRecipientContractID, recipientKp.Address(), "transfer", "500.0000000") - if DisabledSoroban { - fx := getTxEffects(itest, transferTx, asset) - require.Len(t, fx, 0) - return - } - assertContainsEffect(t, getTxEffects(itest, transferTx, asset), - effects.EffectContractDebited, effects.EffectAccountCredited) balanceAmount, _, _ := assertInvokeHostFnSucceeds( itest, itest.Master(), @@ -792,15 +696,13 @@ func CaseContractTransferBetweenAccountAndContract(t *testing.T) { assert.Equal(itest.CurrentTest(), xdr.Int64(0), (*balanceAmount.I128).Hi) } -func CaseContractTransferBetweenContracts(t *testing.T) { +func TestContractTransferBetweenContracts(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, - HorizonEnvironment: map[string]string{ - "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, + ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -840,28 +742,8 @@ func CaseContractTransferBetweenContracts(t *testing.T) { itest.Master(), transferFromContract(itest, issuer, asset, emitterContractID, emitterContractHash, "10", contractAddressParam(recipientContractID)), ) - - assertAssetStats(itest, assetStats{ - code: code, - issuer: issuer, - numAccounts: 0, - balanceAccounts: 0, - balanceArchivedContracts: big.NewInt(0), - numArchivedContracts: 0, - numContracts: 2, - balanceContracts: big.NewInt(int64(amount.MustParse("1000"))), - contractID: stellarAssetContractID(itest, asset), - }) - assertEventPayments(itest, transferTx, asset, strkeyEmitterContractID, strkeyRecipientContractID, "transfer", "10.0000000") - - if !DisabledSoroban { - assertContainsEffect(t, getTxEffects(itest, transferTx, asset), - effects.EffectContractCredited, effects.EffectContractDebited) - } else { - fx := getTxEffects(itest, transferTx, asset) - require.Len(t, fx, 0) - return - } + assertContainsEffect(t, getTxEffects(itest, transferTx, asset), + effects.EffectContractCredited, effects.EffectContractDebited) // Check balances of emitter and recipient emitterBalanceAmount, _, _ := assertInvokeHostFnSucceeds( @@ -881,17 +763,28 @@ func CaseContractTransferBetweenContracts(t *testing.T) { assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, recipientBalanceAmount.Type) assert.Equal(itest.CurrentTest(), xdr.Uint64(100000000), (*recipientBalanceAmount.I128).Lo) assert.Equal(itest.CurrentTest(), xdr.Int64(0), (*recipientBalanceAmount.I128).Hi) + + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 0, + balanceAccounts: 0, + balanceArchivedContracts: big.NewInt(0), + numArchivedContracts: 0, + numContracts: 2, + balanceContracts: big.NewInt(int64(amount.MustParse("1000"))), + contractID: stellarAssetContractID(itest, asset), + }) + assertEventPayments(itest, transferTx, asset, strkeyEmitterContractID, strkeyRecipientContractID, "transfer", "10.0000000") } -func CaseContractBurnFromAccount(t *testing.T) { +func TestContractBurnFromAccount(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, - HorizonEnvironment: map[string]string{ - "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, + ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -937,6 +830,16 @@ func CaseContractBurnFromAccount(t *testing.T) { burn(itest, recipientKp.Address(), asset, "500"), ) + fx := getTxEffects(itest, burnTx, asset) + require.Len(t, fx, 1) + assetEffects := assertContainsEffect(t, fx, effects.EffectAccountDebited) + require.GreaterOrEqual(t, len(assetEffects), 1) + burnEffect := assetEffects[0].(effects.AccountDebited) + + assert.Equal(t, issuer, burnEffect.Asset.Issuer) + assert.Equal(t, code, burnEffect.Asset.Code) + assert.Equal(t, "500.0000000", burnEffect.Amount) + assert.Equal(t, recipientKp.Address(), burnEffect.Account) assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -949,33 +852,15 @@ func CaseContractBurnFromAccount(t *testing.T) { contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, burnTx, asset, recipientKp.Address(), "", "burn", "500.0000000") - - if !DisabledSoroban { - fx := getTxEffects(itest, burnTx, asset) - require.Len(t, fx, 1) - assetEffects := assertContainsEffect(t, fx, effects.EffectAccountDebited) - require.GreaterOrEqual(t, len(assetEffects), 1) - burnEffect := assetEffects[0].(effects.AccountDebited) - - assert.Equal(t, issuer, burnEffect.Asset.Issuer) - assert.Equal(t, code, burnEffect.Asset.Code) - assert.Equal(t, "500.0000000", burnEffect.Amount) - assert.Equal(t, recipientKp.Address(), burnEffect.Account) - } else { - fx := getTxEffects(itest, burnTx, asset) - require.Len(t, fx, 0) - } } -func CaseContractBurnFromContract(t *testing.T) { +func TestContractBurnFromContract(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, - HorizonEnvironment: map[string]string{ - "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, + ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -1010,6 +895,19 @@ func CaseContractBurnFromContract(t *testing.T) { burnSelf(itest, issuer, asset, recipientContractID, recipientContractHash, "10"), ) + balanceAmount, _, _ := assertInvokeHostFnSucceeds( + itest, + itest.Master(), + contractBalance(itest, issuer, asset, recipientContractID), + ) + + assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) + assert.Equal(itest.CurrentTest(), xdr.Uint64(9900000000), (*balanceAmount.I128).Lo) + assert.Equal(itest.CurrentTest(), xdr.Int64(0), (*balanceAmount.I128).Hi) + + assertContainsEffect(t, getTxEffects(itest, burnTx, asset), + effects.EffectContractDebited) + assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -1022,35 +920,15 @@ func CaseContractBurnFromContract(t *testing.T) { contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, burnTx, asset, strkeyRecipientContractID, "", "burn", "10.0000000") - - if !DisabledSoroban { - balanceAmount, _, _ := assertInvokeHostFnSucceeds( - itest, - itest.Master(), - contractBalance(itest, issuer, asset, recipientContractID), - ) - - assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) - assert.Equal(itest.CurrentTest(), xdr.Uint64(9900000000), (*balanceAmount.I128).Lo) - assert.Equal(itest.CurrentTest(), xdr.Int64(0), (*balanceAmount.I128).Hi) - - assertContainsEffect(t, getTxEffects(itest, burnTx, asset), - effects.EffectContractDebited) - } else { - fx := getTxEffects(itest, burnTx, asset) - require.Len(t, fx, 0) - } } -func CaseContractClawbackFromAccount(t *testing.T) { +func TestContractClawbackFromAccount(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, - HorizonEnvironment: map[string]string{ - "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, + ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -1088,7 +966,6 @@ func CaseContractClawbackFromAccount(t *testing.T) { ) assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1000")) - assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -1106,6 +983,8 @@ func CaseContractClawbackFromAccount(t *testing.T) { itest.Master(), clawback(itest, issuer, asset, "1000", accountAddressParam(recipientKp.Address())), ) + + assertContainsEffect(t, getTxEffects(itest, clawTx, asset), effects.EffectAccountDebited) assertContainsBalance(itest, recipientKp, issuer, code, 0) assertAssetStats(itest, assetStats{ code: code, @@ -1119,24 +998,15 @@ func CaseContractClawbackFromAccount(t *testing.T) { contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, clawTx, asset, recipientKp.Address(), "", "clawback", "1000.0000000") - - if !DisabledSoroban { - assertContainsEffect(t, getTxEffects(itest, clawTx, asset), effects.EffectAccountDebited) - } else { - fx := getTxEffects(itest, clawTx, asset) - require.Len(t, fx, 0) - } } -func CaseContractClawbackFromContract(t *testing.T) { +func TestContractClawbackFromContract(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 20 { t.Skip("This test run does not support less than Protocol 20") } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, - HorizonEnvironment: map[string]string{ - "DISABLE_SOROBAN_INGEST": fmt.Sprint(DisabledSoroban)}, + ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -1174,6 +1044,19 @@ func CaseContractClawbackFromContract(t *testing.T) { itest.Master(), clawback(itest, issuer, asset, "10", contractAddressParam(recipientContractID)), ) + + balanceAmount, _, _ := assertInvokeHostFnSucceeds( + itest, + itest.Master(), + contractBalance(itest, issuer, asset, recipientContractID), + ) + assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) + assert.Equal(itest.CurrentTest(), xdr.Uint64(9900000000), (*balanceAmount.I128).Lo) + assert.Equal(itest.CurrentTest(), xdr.Int64(0), (*balanceAmount.I128).Hi) + + assertContainsEffect(t, getTxEffects(itest, clawTx, asset), + effects.EffectContractDebited) + assertAssetStats(itest, assetStats{ code: code, issuer: issuer, @@ -1186,23 +1069,6 @@ func CaseContractClawbackFromContract(t *testing.T) { contractID: stellarAssetContractID(itest, asset), }) assertEventPayments(itest, clawTx, asset, strkeyRecipientContractID, "", "clawback", "10.0000000") - - if !DisabledSoroban { - balanceAmount, _, _ := assertInvokeHostFnSucceeds( - itest, - itest.Master(), - contractBalance(itest, issuer, asset, recipientContractID), - ) - assert.Equal(itest.CurrentTest(), xdr.ScValTypeScvI128, balanceAmount.Type) - assert.Equal(itest.CurrentTest(), xdr.Uint64(9900000000), (*balanceAmount.I128).Lo) - assert.Equal(itest.CurrentTest(), xdr.Int64(0), (*balanceAmount.I128).Hi) - - assertContainsEffect(t, getTxEffects(itest, clawTx, asset), - effects.EffectContractDebited) - } else { - fx := getTxEffects(itest, clawTx, asset) - require.Len(t, fx, 0) - } } func assertContainsBalance(itest *integration.Test, acct *keypair.Full, issuer, code string, amt xdr.Int64) { @@ -1313,12 +1179,6 @@ func assertEventPayments(itest *integration.Test, txHash string, asset xdr.Asset invokeHostFn := ops.Embedded.Records[0].(operations.InvokeHostFunction) assert.Equal(itest.CurrentTest(), invokeHostFn.Function, "HostFunctionTypeHostFunctionTypeInvokeContract") - - if DisabledSoroban { - require.Equal(itest.CurrentTest(), 0, len(invokeHostFn.AssetBalanceChanges)) - return - } - require.Equal(itest.CurrentTest(), 1, len(invokeHostFn.AssetBalanceChanges)) assetBalanceChange := invokeHostFn.AssetBalanceChanges[0] assert.Equal(itest.CurrentTest(), assetBalanceChange.Amount, amount) @@ -1540,6 +1400,10 @@ func assertInvokeHostFnSucceeds(itest *integration.Test, signer *keypair.Full, o err = xdr.SafeUnmarshalBase64(clientTx.ResultXdr, &txResult) require.NoError(itest.CurrentTest(), err) + var txMetaResult xdr.TransactionMeta + err = xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &txMetaResult) + require.NoError(itest.CurrentTest(), err) + opResults, ok := txResult.OperationResults() assert.True(itest.CurrentTest(), ok) assert.Equal(itest.CurrentTest(), len(opResults), 1) @@ -1547,18 +1411,9 @@ func assertInvokeHostFnSucceeds(itest *integration.Test, signer *keypair.Full, o assert.True(itest.CurrentTest(), ok) assert.Equal(itest.CurrentTest(), invokeHostFunctionResult.Code, xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess) - var returnValue *xdr.ScVal - - if !DisabledSoroban { - var txMetaResult xdr.TransactionMeta - err = xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &txMetaResult) - require.NoError(itest.CurrentTest(), err) - returnValue = &txMetaResult.MustV3().SorobanMeta.ReturnValue - } else { - verifySorobanMeta(itest.CurrentTest(), clientTx) - } + returnValue := txMetaResult.MustV3().SorobanMeta.ReturnValue - return returnValue, clientTx.Hash, &preFlightOp + return &returnValue, clientTx.Hash, &preFlightOp } func stellarAssetContractID(itest *integration.Test, asset xdr.Asset) xdr.Hash { diff --git a/services/horizon/internal/integration/transaction_test.go b/services/horizon/internal/integration/transaction_test.go new file mode 100644 index 0000000000..85fbe78522 --- /dev/null +++ b/services/horizon/internal/integration/transaction_test.go @@ -0,0 +1,134 @@ +package integration + +import ( + "testing" + + "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/services/horizon/internal/test/integration" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestP19MetaTransaction(t *testing.T) { + itest := integration.NewTest(t, integration.Config{ + ProtocolVersion: 19, + EnableSorobanRPC: false, + }) + + masterAccount, err := itest.Client().AccountDetail(horizonclient.AccountRequest{ + AccountID: itest.Master().Address(), + }) + require.NoError(t, err) + + op := &txnbuild.Payment{ + SourceAccount: itest.Master().Address(), + Destination: itest.Master().Address(), + Asset: txnbuild.NativeAsset{}, + Amount: "10", + } + + clientTx := itest.MustSubmitOperations(&masterAccount, itest.Master(), op) + + var txMetaResult xdr.TransactionMeta + err = xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &txMetaResult) + require.NoError(t, err) + + assert.Greater(t, len(txMetaResult.MustV2().Operations), 0) + assert.Greater(t, len(txMetaResult.MustV2().TxChangesBefore), 0) + // TODO figure out how to generate TxChangesAfter also + //assert.Greater(t, len(txMetaResult.MustV2().TxChangesAfter), 0) +} + +func TestP19MetaDisabledTransaction(t *testing.T) { + itest := integration.NewTest(t, integration.Config{ + ProtocolVersion: 19, + HorizonEnvironment: map[string]string{"SKIP_TXMETA": "TRUE"}, + EnableSorobanRPC: false, + }) + + masterAccount, err := itest.Client().AccountDetail(horizonclient.AccountRequest{ + AccountID: itest.Master().Address(), + }) + require.NoError(t, err) + + op := &txnbuild.Payment{ + SourceAccount: itest.Master().Address(), + Destination: itest.Master().Address(), + Asset: txnbuild.NativeAsset{}, + Amount: "10", + } + + clientTx := itest.MustSubmitOperations(&masterAccount, itest.Master(), op) + + var txMetaResult xdr.TransactionMeta + err = xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &txMetaResult) + require.NoError(t, err) + + assert.Equal(t, len(txMetaResult.MustV2().Operations), 0) + assert.Equal(t, len(txMetaResult.MustV2().TxChangesAfter), 0) + assert.Equal(t, len(txMetaResult.MustV2().TxChangesBefore), 0) +} + +func TestP20MetaTransaction(t *testing.T) { + if integration.GetCoreMaxSupportedProtocol() < 20 { + t.Skip("This test run does not support less than Protocol 20") + } + + itest := integration.NewTest(t, integration.Config{ + ProtocolVersion: 20, + EnableSorobanRPC: true, + }) + + // establish which account will be contract owner, and load it's current seq + sourceAccount, err := itest.Client().AccountDetail(horizonclient.AccountRequest{ + AccountID: itest.Master().Address(), + }) + require.NoError(t, err) + + installContractOp := assembleInstallContractCodeOp(t, itest.Master().Address(), add_u64_contract) + preFlightOp, minFee := itest.PreflightHostFunctions(&sourceAccount, *installContractOp) + clientTx := itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) + + var txMetaResult xdr.TransactionMeta + err = xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &txMetaResult) + require.NoError(t, err) + + assert.Greater(t, len(txMetaResult.MustV3().Operations), 0) + assert.NotNil(t, txMetaResult.MustV3().SorobanMeta) + assert.Greater(t, len(txMetaResult.MustV3().TxChangesAfter), 0) + assert.Greater(t, len(txMetaResult.MustV3().TxChangesBefore), 0) +} + +func TestP20MetaDisabledTransaction(t *testing.T) { + if integration.GetCoreMaxSupportedProtocol() < 20 { + t.Skip("This test run does not support less than Protocol 20") + } + + itest := integration.NewTest(t, integration.Config{ + ProtocolVersion: 20, + HorizonEnvironment: map[string]string{"SKIP_TXMETA": "TRUE"}, + EnableSorobanRPC: true, + }) + + // establish which account will be contract owner, and load it's current seq + sourceAccount, err := itest.Client().AccountDetail(horizonclient.AccountRequest{ + AccountID: itest.Master().Address(), + }) + require.NoError(t, err) + + installContractOp := assembleInstallContractCodeOp(t, itest.Master().Address(), add_u64_contract) + preFlightOp, minFee := itest.PreflightHostFunctions(&sourceAccount, *installContractOp) + clientTx := itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) + + var txMetaResult xdr.TransactionMeta + err = xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &txMetaResult) + require.NoError(t, err) + + assert.Equal(t, len(txMetaResult.MustV3().Operations), 0) + assert.Nil(t, txMetaResult.MustV3().SorobanMeta) + assert.Equal(t, len(txMetaResult.MustV3().TxChangesAfter), 0) + assert.Equal(t, len(txMetaResult.MustV3().TxChangesBefore), 0) +} From 03e1903247c9e5cb2889560a4530b79c60774114 Mon Sep 17 00:00:00 2001 From: Shawn Reuland Date: Thu, 15 Feb 2024 10:09:24 -0800 Subject: [PATCH 060/234] updated changelog for 2.28.3 --- services/horizon/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index cd125e019b..cd411b7120 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -5,6 +5,9 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ## 2.28.3 +### Fixed +- Fix claimable_balance_claimants subquery in GetClaimableBalances() [5207](https://github.com/stellar/go/pull/5207) + ### Added - New optional config `SKIP_TXMETA` ([5189](https://github.com/stellar/go/issues/5189)). Defaults to `FALSE`, when `TRUE` the following will occur: * history_transactions.tx_meta column will have serialized xdr that equates to empty for any protocol version, such as for `xdr.TransactionMeta.V3`, `Operations`, `TxChangesAfter`, `TxChangesBefore` will be empty arrays and `SorobanMeta` will be nil. From 88206d21db6df3906658029bbccf3d284f7aa3a7 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Tue, 20 Feb 2024 11:10:44 -0500 Subject: [PATCH 061/234] services/horizon: Remove legacy ingest database backend (#5162) * remove legacy ingest backend * Delete hash_order_test.go * Update ledger_backend.go * Update main.go --- ingest/README.md | 16 +- ingest/ledgerbackend/database_backend.go | 271 ------------------ ingest/ledgerbackend/hash_order_test.go | 70 ----- ingest/ledgerbackend/ledger_backend.go | 57 ---- ingest/ledgerbackend/remote_captive_core.go | 262 ----------------- .../ledgerbackend/remote_captive_core_test.go | 83 ------ .../internal/ingest/database_backend_test.go | 35 --- 7 files changed, 3 insertions(+), 791 deletions(-) delete mode 100644 ingest/ledgerbackend/database_backend.go delete mode 100644 ingest/ledgerbackend/hash_order_test.go delete mode 100644 ingest/ledgerbackend/remote_captive_core.go delete mode 100644 ingest/ledgerbackend/remote_captive_core_test.go delete mode 100644 services/horizon/internal/ingest/database_backend_test.go diff --git a/ingest/README.md b/ingest/README.md index e07dd64ec8..cf3d38f8da 100644 --- a/ingest/README.md +++ b/ingest/README.md @@ -23,13 +23,9 @@ From a high level, the ingestion library is broken down into a few modular compo [ Ledger Backend ] | - one of... | - --------|-----+------|----------| - | | | | - Captive Database Remote etc. - Core Captive - Core + Captive + Core ``` This is described in a little more detail in [`doc.go`](./doc.go), its accompanying examples, the documentation within this package, and the rest of this tutorial. @@ -37,13 +33,7 @@ This is described in a little more detail in [`doc.go`](./doc.go), its accompany # Hello, World! -As is tradition, we'll start with a simplistic example that ingests a single ledger from the network. We're immediately faced with a decision, though: _What's the backend?_ We'll use a **Captive Stellar-Core backend** in this example because it requires (little-to-)no setup, but there are couple of alternatives available. You could also use: - - - a **database** (via `NewDatabaseBackend()`), which would ingest ledgers stored in a Stellar-Core database, or - - - a **remote Captive Core** instance (via `NewRemoteCaptive()`), which works much like Captive Core, but points to an instance that isn't (necessarily) running locally. - -With that in mind, here's a minimalist example of the ingestion library: +As is tradition, we'll start with a simplistic example that ingests a single ledger from the network. We'll use the **Captive Stellar-Core backend** to ingest the ledger: ```go package main diff --git a/ingest/ledgerbackend/database_backend.go b/ingest/ledgerbackend/database_backend.go deleted file mode 100644 index 98333c679c..0000000000 --- a/ingest/ledgerbackend/database_backend.go +++ /dev/null @@ -1,271 +0,0 @@ -package ledgerbackend - -import ( - "context" - "database/sql" - "sort" - "time" - - "github.com/stellar/go/network" - "github.com/stellar/go/support/db" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/xdr" -) - -const ( - latestLedgerSeqQuery = "select ledgerseq, closetime from ledgerheaders order by ledgerseq desc limit 1" - txHistoryQuery = "select txbody, txresult, txmeta, txindex from txhistory where ledgerseq = ? " - ledgerHeaderQuery = "select ledgerhash, data from ledgerheaders where ledgerseq = ? " - ledgerSequenceAfterQuery = "select ledgerseq from ledgerheaders where ledgerseq > ? " - txFeeHistoryQuery = "select txchanges, txindex from txfeehistory where ledgerseq = ? " - upgradeHistoryQuery = "select ledgerseq, upgradeindex, upgrade, changes from upgradehistory where ledgerseq = ? order by upgradeindex asc" - orderBy = "order by txindex asc" - dbDriver = "postgres" -) - -// Ensure DatabaseBackend implements LedgerBackend -var _ LedgerBackend = (*DatabaseBackend)(nil) - -// DatabaseBackend implements a database data store. -type DatabaseBackend struct { - networkPassphrase string - session session -} - -func NewDatabaseBackend(dataSourceName, networkPassphrase string) (*DatabaseBackend, error) { - session, err := createSession(dataSourceName) - if err != nil { - return nil, err - } - - return NewDatabaseBackendFromSession(session, networkPassphrase) -} - -func NewDatabaseBackendFromSession(session db.SessionInterface, networkPassphrase string) (*DatabaseBackend, error) { - return &DatabaseBackend{ - session: session, - networkPassphrase: networkPassphrase, - }, nil -} - -func (dbb *DatabaseBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { - _, err := dbb.GetLedger(ctx, ledgerRange.from) - if err != nil { - return errors.Wrap(err, "error getting ledger") - } - - if ledgerRange.bounded { - _, err := dbb.GetLedger(ctx, ledgerRange.to) - if err != nil { - return errors.Wrap(err, "error getting ledger") - } - } - - return nil -} - -// IsPrepared returns true if a given ledgerRange is prepared. -func (*DatabaseBackend) IsPrepared(ctx context.Context, ledgerRange Range) (bool, error) { - return true, nil -} - -// GetLatestLedgerSequence returns the most recent ledger sequence number present in the database. -func (dbb *DatabaseBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - var ledger []ledgerHeader - err := dbb.session.SelectRaw(ctx, &ledger, latestLedgerSeqQuery) - if err != nil { - return 0, errors.Wrap(err, "couldn't select ledger sequence") - } - if len(ledger) == 0 { - return 0, errors.New("no ledgers exist in ledgerheaders table") - } - - return ledger[0].LedgerSeq, nil -} - -func sortByHash(transactions []xdr.TransactionEnvelope, passphrase string) error { - hashes := make([]xdr.Hash, len(transactions)) - txByHash := map[xdr.Hash]xdr.TransactionEnvelope{} - for i, tx := range transactions { - hash, err := network.HashTransactionInEnvelope(tx, passphrase) - if err != nil { - return errors.Wrap(err, "cannot hash transaction") - } - hashes[i] = hash - txByHash[hash] = tx - } - - sort.Slice(hashes, func(i, j int) bool { - a := hashes[i] - b := hashes[j] - for k := range a { - if a[k] < b[k] { - return true - } - if a[k] > b[k] { - return false - } - } - return false - }) - - for i, hash := range hashes { - transactions[i] = txByHash[hash] - } - return nil -} - -// GetLedger will block until the ledger is -// available in the backend (even for UnaboundedRange). -// Please note that requesting a ledger sequence far after current ledger will -// block the execution for a long time. -func (dbb *DatabaseBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { - for { - exists, meta, err := dbb.getLedgerQuery(ctx, sequence) - if err != nil { - return xdr.LedgerCloseMeta{}, err - } - - if exists { - return meta, nil - } else { - // Check if there are ledgers after `sequence`. If so, it's likely - // the requested sequence was removed during maintenance so return - // error. - ledgerAfterExist, oldestSequenceAfter, err := dbb.getLedgerAfterExist(ctx, sequence) - if err != nil { - return xdr.LedgerCloseMeta{}, err - } - - if ledgerAfterExist { - return xdr.LedgerCloseMeta{}, errors.Errorf("requested ledger already removed (oldest sequence after %d is %d)", sequence, oldestSequenceAfter) - } - time.Sleep(time.Second) - } - } -} - -// getLedgerAfterExist returns true (and sequence number) if there's a ledger in -// the Stellar-Core DB with the sequence number higher than sequence. -func (dbb *DatabaseBackend) getLedgerAfterExist(ctx context.Context, sequence uint32) (bool, uint32, error) { - var fetchedSequence uint32 - err := dbb.session.GetRaw(ctx, &fetchedSequence, ledgerSequenceAfterQuery, sequence) - // Return errors... - if err != nil { - switch err { - case sql.ErrNoRows: - // Ledger was not found - return false, fetchedSequence, nil - default: - return false, fetchedSequence, errors.Wrapf(err, "Error getting ledger after %d", sequence) - } - } - - return true, fetchedSequence, nil -} - -// getLedgerQuery returns the LedgerCloseMeta for the given ledger sequence number. -// The first returned value is false when the ledger does not exist in the database. -func (dbb *DatabaseBackend) getLedgerQuery(ctx context.Context, sequence uint32) (bool, xdr.LedgerCloseMeta, error) { - lcm := xdr.LedgerCloseMeta{ - V0: &xdr.LedgerCloseMetaV0{}, - } - - // Query - ledgerheader - var lRow ledgerHeaderHistory - - err := dbb.session.GetRaw(ctx, &lRow, ledgerHeaderQuery, sequence) - // Return errors... - if err != nil { - switch err { - case sql.ErrNoRows: - // Ledger was not found - return false, xdr.LedgerCloseMeta{}, nil - default: - return false, xdr.LedgerCloseMeta{}, errors.Wrap(err, "Error getting ledger header") - } - } - - // ...otherwise store the header - lcm.V0.LedgerHeader = xdr.LedgerHeaderHistoryEntry{ - Hash: lRow.Hash, - Header: lRow.Header, - Ext: xdr.LedgerHeaderHistoryEntryExt{}, - } - - // Query - txhistory - var txhRows []txHistory - err = dbb.session.SelectRaw(ctx, &txhRows, txHistoryQuery+orderBy, sequence) - // Return errors... - if err != nil { - return false, lcm, errors.Wrap(err, "Error getting txHistory") - } - - // ...otherwise store the data - for i, tx := range txhRows { - // Sanity check index. Note that first TXIndex in a ledger is 1 - if i != int(tx.TXIndex)-1 { - return false, xdr.LedgerCloseMeta{}, errors.New("transactions read from DB history table are misordered") - } - - lcm.V0.TxSet.Txs = append(lcm.V0.TxSet.Txs, tx.TXBody) - lcm.V0.TxProcessing = append(lcm.V0.TxProcessing, xdr.TransactionResultMeta{ - Result: tx.TXResult, - TxApplyProcessing: tx.TXMeta, - }) - } - - if err = sortByHash(lcm.V0.TxSet.Txs, dbb.networkPassphrase); err != nil { - return false, xdr.LedgerCloseMeta{}, errors.Wrap(err, "could not sort txset") - } - - // Query - txfeehistory - var txfhRows []txFeeHistory - err = dbb.session.SelectRaw(ctx, &txfhRows, txFeeHistoryQuery+orderBy, sequence) - // Return errors... - if err != nil { - return false, lcm, errors.Wrap(err, "Error getting txFeeHistory") - } - - // ...otherwise store the data - for i, tx := range txfhRows { - // Sanity check index. Note that first TXIndex in a ledger is 1 - if i != int(tx.TXIndex)-1 { - return false, xdr.LedgerCloseMeta{}, errors.New("transactions read from DB fee history table are misordered") - } - lcm.V0.TxProcessing[i].FeeProcessing = tx.TXChanges - } - - // Query - upgradehistory - var upgradeHistoryRows []upgradeHistory - err = dbb.session.SelectRaw(ctx, &upgradeHistoryRows, upgradeHistoryQuery, sequence) - // Return errors... - if err != nil { - return false, lcm, errors.Wrap(err, "Error getting upgradeHistoryRows") - } - - // ...otherwise store the data - lcm.V0.UpgradesProcessing = make([]xdr.UpgradeEntryMeta, len(upgradeHistoryRows)) - for i, upgradeHistoryRow := range upgradeHistoryRows { - lcm.V0.UpgradesProcessing[i] = xdr.UpgradeEntryMeta{ - Upgrade: upgradeHistoryRow.Upgrade, - Changes: upgradeHistoryRow.Changes, - } - } - - return true, lcm, nil -} - -// CreateSession returns a new db.Session that connects to the given DB settings. -func createSession(dataSourceName string) (*db.Session, error) { - if dataSourceName == "" { - return nil, errors.New("missing DatabaseBackend.DataSourceName (e.g. \"postgres://stellar:postgres@localhost:8002/core\")") - } - - return db.Open(dbDriver, dataSourceName) -} - -// Close disconnects an active database session. -func (dbb *DatabaseBackend) Close() error { - return dbb.session.Close() -} diff --git a/ingest/ledgerbackend/hash_order_test.go b/ingest/ledgerbackend/hash_order_test.go deleted file mode 100644 index 537c9d5148..0000000000 --- a/ingest/ledgerbackend/hash_order_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package ledgerbackend - -import ( - "testing" - - "github.com/stellar/go/network" - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestHashOrder(t *testing.T) { - source := xdr.MustAddress("GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU") - account := source.ToMuxedAccount() - original := []xdr.TransactionEnvelope{ - { - Type: xdr.EnvelopeTypeEnvelopeTypeTx, - V1: &xdr.TransactionV1Envelope{ - Tx: xdr.Transaction{ - SourceAccount: account, - SeqNum: 1, - }, - }, - }, - { - Type: xdr.EnvelopeTypeEnvelopeTypeTx, - V1: &xdr.TransactionV1Envelope{ - Tx: xdr.Transaction{ - SourceAccount: account, - SeqNum: 2, - }, - }, - }, - { - Type: xdr.EnvelopeTypeEnvelopeTypeTx, - V1: &xdr.TransactionV1Envelope{ - Tx: xdr.Transaction{ - SourceAccount: account, - SeqNum: 3, - }, - }, - }, - } - - require.NoError(t, sortByHash(original, network.TestNetworkPassphrase)) - hashes := map[int]xdr.Hash{} - - for i, tx := range original { - var err error - hashes[i], err = network.HashTransactionInEnvelope(tx, network.TestNetworkPassphrase) - if err != nil { - assert.NoError(t, err) - } - } - - for i := range original { - if i == 0 { - continue - } - prev := hashes[i-1] - cur := hashes[i] - for j := range prev { - if !assert.True(t, prev[j] < cur[j]) { - break - } else { - break - } - } - } -} diff --git a/ingest/ledgerbackend/ledger_backend.go b/ingest/ledgerbackend/ledger_backend.go index 572de2e183..eddc98fa05 100644 --- a/ingest/ledgerbackend/ledger_backend.go +++ b/ingest/ledgerbackend/ledger_backend.go @@ -21,60 +21,3 @@ type LedgerBackend interface { IsPrepared(ctx context.Context, ledgerRange Range) (bool, error) Close() error } - -// session is the interface needed to access a persistent database session. -// TODO can't use this until we add Close() to the existing db.Session object -type session interface { - GetRaw(ctx context.Context, dest interface{}, query string, args ...interface{}) error - SelectRaw(ctx context.Context, dest interface{}, query string, args ...interface{}) error - Close() error -} - -// ledgerHeaderHistory is a helper struct used to unmarshall header fields from a stellar-core DB. -type ledgerHeaderHistory struct { - Hash xdr.Hash `db:"ledgerhash"` - Header xdr.LedgerHeader `db:"data"` -} - -// ledgerHeader holds a row of data from the stellar-core `ledgerheaders` table. -type ledgerHeader struct { - LedgerHash string `db:"ledgerhash"` - PrevHash string `db:"prevhash"` - BucketListHash string `db:"bucketlisthash"` - CloseTime int64 `db:"closetime"` - LedgerSeq uint32 `db:"ledgerseq"` - Data xdr.LedgerHeader `db:"data"` -} - -// txHistory holds a row of data from the stellar-core `txhistory` table. -type txHistory struct { - TXID string `db:"txid"` - LedgerSeq uint32 `db:"ledgerseq"` - TXIndex uint32 `db:"txindex"` - TXBody xdr.TransactionEnvelope `db:"txbody"` - TXResult xdr.TransactionResultPair `db:"txresult"` - TXMeta xdr.TransactionMeta `db:"txmeta"` -} - -// txFeeHistory holds a row of data from the stellar-core `txfeehistory` table. -type txFeeHistory struct { - TXID string `db:"txid"` - LedgerSeq uint32 `db:"ledgerseq"` - TXIndex uint32 `db:"txindex"` - TXChanges xdr.LedgerEntryChanges `db:"txchanges"` -} - -// scpHistory holds a row of data from the stellar-core `scphistory` table. -// type scpHistory struct { -// NodeID string `db:"nodeid"` -// LedgerSeq uint32 `db:"ledgerseq"` -// Envelope string `db:"envelope"` -// } - -// upgradeHistory holds a row of data from the stellar-core `upgradehistory` table. -type upgradeHistory struct { - LedgerSeq uint32 `db:"ledgerseq"` - UpgradeIndex uint32 `db:"upgradeindex"` - Upgrade xdr.LedgerUpgrade `db:"upgrade"` - Changes xdr.LedgerEntryChanges `db:"changes"` -} diff --git a/ingest/ledgerbackend/remote_captive_core.go b/ingest/ledgerbackend/remote_captive_core.go deleted file mode 100644 index 764114fa3b..0000000000 --- a/ingest/ledgerbackend/remote_captive_core.go +++ /dev/null @@ -1,262 +0,0 @@ -package ledgerbackend - -import ( - "bytes" - "context" - "encoding/json" - "io/ioutil" - "net/http" - "net/url" - "path" - "strconv" - "sync" - "time" - - "github.com/stellar/go/support/errors" - "github.com/stellar/go/xdr" -) - -// PrepareRangeResponse describes the status of the pending PrepareRange operation. -type PrepareRangeResponse struct { - LedgerRange Range `json:"ledgerRange"` - StartTime time.Time `json:"startTime"` - Ready bool `json:"ready"` - ReadyDuration int `json:"readyDuration"` -} - -// LatestLedgerSequenceResponse is the response for the GetLatestLedgerSequence command. -type LatestLedgerSequenceResponse struct { - Sequence uint32 `json:"sequence"` -} - -// LedgerResponse is the response for the GetLedger command. -type LedgerResponse struct { - Ledger Base64Ledger `json:"ledger"` -} - -// Base64Ledger extends xdr.LedgerCloseMeta with JSON encoding and decoding -type Base64Ledger xdr.LedgerCloseMeta - -func (r *Base64Ledger) UnmarshalJSON(b []byte) error { - var base64 string - if err := json.Unmarshal(b, &base64); err != nil { - return err - } - - var parsed xdr.LedgerCloseMeta - if err := xdr.SafeUnmarshalBase64(base64, &parsed); err != nil { - return err - } - *r = Base64Ledger(parsed) - - return nil -} - -func (r Base64Ledger) MarshalJSON() ([]byte, error) { - base64, err := xdr.MarshalBase64(xdr.LedgerCloseMeta(r)) - if err != nil { - return nil, err - } - return json.Marshal(base64) -} - -// RemoteCaptiveStellarCore is an http client for interacting with a remote captive core server. -type RemoteCaptiveStellarCore struct { - url *url.URL - client *http.Client - lock *sync.Mutex - prepareRangePollInterval time.Duration -} - -// RemoteCaptiveOption values can be passed into NewRemoteCaptive to customize a RemoteCaptiveStellarCore instance. -type RemoteCaptiveOption func(c *RemoteCaptiveStellarCore) - -// PrepareRangePollInterval configures how often the captive core server will be polled when blocking -// on the PrepareRange operation. -func PrepareRangePollInterval(d time.Duration) RemoteCaptiveOption { - return func(c *RemoteCaptiveStellarCore) { - c.prepareRangePollInterval = d - } -} - -// NewRemoteCaptive returns a new RemoteCaptiveStellarCore instance. -// -// Only the captiveCoreURL parameter is required. -func NewRemoteCaptive(captiveCoreURL string, options ...RemoteCaptiveOption) (RemoteCaptiveStellarCore, error) { - u, err := url.Parse(captiveCoreURL) - if err != nil { - return RemoteCaptiveStellarCore{}, errors.Wrap(err, "unparseable url") - } - - client := RemoteCaptiveStellarCore{ - prepareRangePollInterval: time.Second, - url: u, - client: &http.Client{Timeout: 10 * time.Second}, - lock: &sync.Mutex{}, - } - for _, option := range options { - option(&client) - } - return client, nil -} - -func decodeResponse(response *http.Response, payload interface{}) error { - defer response.Body.Close() - - if response.StatusCode != http.StatusOK { - body, err := ioutil.ReadAll(response.Body) - if err != nil { - return errors.Wrap(err, "failed to read response body") - } - - return errors.New(string(body)) - } - - if err := json.NewDecoder(response.Body).Decode(payload); err != nil { - return errors.Wrap(err, "failed to decode json payload") - } - return nil -} - -// GetLatestLedgerSequence returns the sequence of the latest ledger available -// in the backend. This method returns an error if not in a session (start with -// PrepareRange). -// -// Note that for UnboundedRange the returned sequence number is not necessarily -// the latest sequence closed by the network. It's always the last value available -// in the backend. -func (c RemoteCaptiveStellarCore) GetLatestLedgerSequence(ctx context.Context) (sequence uint32, err error) { - // TODO: Have a context on this request so we can cancel all outstanding - // requests, not just PrepareRange. - u := *c.url - u.Path = path.Join(u.Path, "latest-sequence") - request, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) - if err != nil { - return 0, errors.Wrap(err, "cannot construct http request") - } - - response, err := c.client.Do(request) - if err != nil { - return 0, errors.Wrap(err, "failed to execute request") - } - - var parsed LatestLedgerSequenceResponse - if err = decodeResponse(response, &parsed); err != nil { - return 0, err - } - - return parsed.Sequence, nil -} - -// Close cancels any pending PrepareRange requests. -func (c RemoteCaptiveStellarCore) Close() error { - return nil -} - -// PrepareRange prepares the given range (including from and to) to be loaded. -// Captive stellar-core backend needs to initalize Stellar-Core state to be -// able to stream ledgers. -// Stellar-Core mode depends on the provided ledgerRange: -// - For BoundedRange it will start Stellar-Core in catchup mode. -// - For UnboundedRange it will first catchup to starting ledger and then run -// it normally (including connecting to the Stellar network). -// -// Please note that using a BoundedRange, currently, requires a full-trust on -// history archive. This issue is being fixed in Stellar-Core. -func (c RemoteCaptiveStellarCore) PrepareRange(ctx context.Context, ledgerRange Range) error { - // TODO: removing createContext call here means we could technically have - // multiple prepareRange requests happening at the same time. Do we still - // need to enforce that? - - timer := time.NewTimer(c.prepareRangePollInterval) - defer timer.Stop() - - for { - ready, err := c.IsPrepared(ctx, ledgerRange) - if err != nil { - return err - } - if ready { - return nil - } - - select { - case <-ctx.Done(): - return errors.Wrap(ctx.Err(), "shutting down") - case <-timer.C: - timer.Reset(c.prepareRangePollInterval) - } - } -} - -// IsPrepared returns true if a given ledgerRange is prepared. -func (c RemoteCaptiveStellarCore) IsPrepared(ctx context.Context, ledgerRange Range) (bool, error) { - // TODO: Have some way to cancel all outstanding requests, not just - // PrepareRange. - u := *c.url - u.Path = path.Join(u.Path, "prepare-range") - rangeBytes, err := json.Marshal(ledgerRange) - if err != nil { - return false, errors.Wrap(err, "cannot serialize range") - } - body := bytes.NewReader(rangeBytes) - request, err := http.NewRequestWithContext(ctx, "POST", u.String(), body) - if err != nil { - return false, errors.Wrap(err, "cannot construct http request") - } - request.Header.Add("Content-Type", "application/json; charset=utf-8") - - var response *http.Response - response, err = c.client.Do(request) - if err != nil { - return false, errors.Wrap(err, "failed to execute request") - } - - var parsed PrepareRangeResponse - if err = decodeResponse(response, &parsed); err != nil { - return false, err - } - - return parsed.Ready, nil -} - -// GetLedger long-polls a remote stellar core backend, until the requested -// ledger is ready. - -// Call PrepareRange first to instruct the backend which ledgers to fetch. -// -// Requesting a ledger on non-prepared backend will return an error. -// -// Because data is streamed from Stellar-Core ledger after ledger user should -// request sequences in a non-decreasing order. If the requested sequence number -// is less than the last requested sequence number, an error will be returned. -func (c RemoteCaptiveStellarCore) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { - for { - // TODO: Have some way to cancel all outstanding requests, not just - // PrepareRange. - u := *c.url - u.Path = path.Join(u.Path, "ledger", strconv.FormatUint(uint64(sequence), 10)) - request, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) - if err != nil { - return xdr.LedgerCloseMeta{}, errors.Wrap(err, "cannot construct http request") - } - - response, err := c.client.Do(request) - if err != nil { - return xdr.LedgerCloseMeta{}, errors.Wrap(err, "failed to execute request") - } - - if response.StatusCode == http.StatusRequestTimeout { - response.Body.Close() - // This request timed out. Retry. - continue - } - - var parsed LedgerResponse - if err = decodeResponse(response, &parsed); err != nil { - return xdr.LedgerCloseMeta{}, err - } - - return xdr.LedgerCloseMeta(parsed.Ledger), nil - } -} diff --git a/ingest/ledgerbackend/remote_captive_core_test.go b/ingest/ledgerbackend/remote_captive_core_test.go deleted file mode 100644 index 393b981589..0000000000 --- a/ingest/ledgerbackend/remote_captive_core_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package ledgerbackend - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "sync/atomic" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/stellar/go/xdr" -) - -func TestGetLedgerSucceeds(t *testing.T) { - expectedLedger := xdr.LedgerCloseMeta{ - V0: &xdr.LedgerCloseMetaV0{ - LedgerHeader: xdr.LedgerHeaderHistoryEntry{ - Header: xdr.LedgerHeader{ - LedgerSeq: 64, - }, - }, - }, - } - called := 0 - var encodeFailed int64 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - called++ - if nil != json.NewEncoder(w).Encode(LedgerResponse{ - Ledger: Base64Ledger(expectedLedger), - }) { - atomic.AddInt64(&encodeFailed, 1) - } - })) - defer server.Close() - - client, err := NewRemoteCaptive(server.URL) - require.NoError(t, err) - - ledger, err := client.GetLedger(context.Background(), 64) - require.NoError(t, err) - require.Equal(t, 1, called) - require.Equal(t, expectedLedger, ledger) - require.Equal(t, int64(0), atomic.LoadInt64(&encodeFailed)) -} - -func TestGetLedgerTakesAWhile(t *testing.T) { - expectedLedger := xdr.LedgerCloseMeta{ - V0: &xdr.LedgerCloseMetaV0{ - LedgerHeader: xdr.LedgerHeaderHistoryEntry{ - Header: xdr.LedgerHeader{ - LedgerSeq: 64, - }, - }, - }, - } - called := 0 - var encodeFailed int64 - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - called++ - if called == 1 { - // TODO: Check this is what the server really does. - w.WriteHeader(http.StatusRequestTimeout) - return - } - if nil != json.NewEncoder(w).Encode(LedgerResponse{ - Ledger: Base64Ledger(expectedLedger), - }) { - atomic.AddInt64(&encodeFailed, 1) - } - })) - defer server.Close() - - client, err := NewRemoteCaptive(server.URL) - require.NoError(t, err) - - ledger, err := client.GetLedger(context.Background(), 64) - require.NoError(t, err) - require.Equal(t, 2, called) - require.Equal(t, expectedLedger, ledger) - require.Equal(t, int64(0), atomic.LoadInt64(&encodeFailed)) -} diff --git a/services/horizon/internal/ingest/database_backend_test.go b/services/horizon/internal/ingest/database_backend_test.go deleted file mode 100644 index 69de728116..0000000000 --- a/services/horizon/internal/ingest/database_backend_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package ingest - -import ( - "testing" - - "github.com/stellar/go/ingest/ledgerbackend" - "github.com/stellar/go/network" - "github.com/stellar/go/services/horizon/internal/test" -) - -func TestGetLatestLedger(t *testing.T) { - tt := test.Start(t) - tt.ScenarioWithoutHorizon("base") - defer tt.Finish() - - backend, err := ledgerbackend.NewDatabaseBackendFromSession(tt.CoreSession(), network.TestNetworkPassphrase) - tt.Assert.NoError(err) - seq, err := backend.GetLatestLedgerSequence(tt.Ctx) - tt.Assert.NoError(err) - tt.Assert.Equal(uint32(3), seq) -} - -func TestGetLatestLedgerNotFound(t *testing.T) { - tt := test.Start(t) - tt.ScenarioWithoutHorizon("base") - defer tt.Finish() - - _, err := tt.CoreDB.Exec(`DELETE FROM ledgerheaders`) - tt.Assert.NoError(err, "failed to remove ledgerheaders") - - backend, err := ledgerbackend.NewDatabaseBackendFromSession(tt.CoreSession(), network.TestNetworkPassphrase) - tt.Assert.NoError(err) - _, err = backend.GetLatestLedgerSequence(tt.Ctx) - tt.Assert.EqualError(err, "no ledgers exist in ledgerheaders table") -} From 8e18724472b5b9e4ea2e6131e08e82aba4742e32 Mon Sep 17 00:00:00 2001 From: Yurii Momotenko Date: Wed, 21 Feb 2024 06:53:32 +0200 Subject: [PATCH 062/234] clients/horizonclient: allow sending user-defined headers on requests (#5214) * Add Headers field to horizonclient Client for custom headers * Move custom header logic above SDK's headers to avoid overriding. * Add unit tests for custom headers logic * Rename unit tests for custom headers logic --- clients/horizonclient/client.go | 4 ++ clients/horizonclient/client_test.go | 60 ++++++++++++++++++++++++++++ clients/horizonclient/main.go | 3 ++ 3 files changed, 67 insertions(+) create mode 100644 clients/horizonclient/client_test.go diff --git a/clients/horizonclient/client.go b/clients/horizonclient/client.go index b27b400364..0b51237bdd 100644 --- a/clients/horizonclient/client.go +++ b/clients/horizonclient/client.go @@ -254,6 +254,10 @@ func (c *Client) stream( } func (c *Client) setClientAppHeaders(req *http.Request) { + for key, value := range c.Headers { + req.Header.Set(key, value) + } + req.Header.Set("X-Client-Name", "go-stellar-sdk") req.Header.Set("X-Client-Version", c.Version()) req.Header.Set("X-App-Name", c.AppName) diff --git a/clients/horizonclient/client_test.go b/clients/horizonclient/client_test.go new file mode 100644 index 0000000000..49403b26b3 --- /dev/null +++ b/clients/horizonclient/client_test.go @@ -0,0 +1,60 @@ +package horizonclient + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func setupClient() *Client { + return &Client{ + HorizonURL: "https://localhost/", + AppName: "client_test", + AppVersion: "4.5.7", + } +} + +func TestSetClientAppHeaders_DefaultLogic(t *testing.T) { + client := setupClient() + + request := LedgerRequest{ + Order: OrderAsc, + Limit: 5, + } + + req, err := request.HTTPRequest(client.HorizonURL) + assert.NoError(t, err) + assert.Equal(t, 0, len(req.Header)) + + client.setClientAppHeaders(req) + assert.Equal(t, 4, len(req.Header)) + + assert.Equal(t, "go-stellar-sdk", req.Header.Get("X-Client-Name")) + assert.Equal(t, version, req.Header.Get("X-Client-Version")) + assert.Equal(t, "client_test", req.Header.Get("X-App-Name")) + assert.Equal(t, "4.5.7", req.Header.Get("X-App-Version")) +} + +func TestSetClientAppHeaders_CustomHeadersLogic(t *testing.T) { + client := setupClient() + + client.Headers = make(map[string]string) + client.Headers["X-Api-Key"] = "abcde" + + request := LedgerRequest{ + Order: OrderAsc, + Limit: 5, + } + + req, err := request.HTTPRequest(client.HorizonURL) + assert.NoError(t, err) + assert.Equal(t, 0, len(req.Header)) + + client.setClientAppHeaders(req) + assert.Equal(t, 5, len(req.Header)) + + assert.Equal(t, "go-stellar-sdk", req.Header.Get("X-Client-Name")) + assert.Equal(t, version, req.Header.Get("X-Client-Version")) + assert.Equal(t, "client_test", req.Header.Get("X-App-Name")) + assert.Equal(t, "4.5.7", req.Header.Get("X-App-Version")) + assert.Equal(t, "abcde", req.Header.Get("X-Api-Key")) +} diff --git a/clients/horizonclient/main.go b/clients/horizonclient/main.go index 8eb7bdc606..c7fa40f03c 100644 --- a/clients/horizonclient/main.go +++ b/clients/horizonclient/main.go @@ -145,6 +145,9 @@ type Client struct { AppVersion string horizonTimeout time.Duration + // Headers allows specifying additional HTTP headers for requests made by the client. + Headers map[string]string + // clock is a Clock returning the current time. clock *clock.Clock } From b0d394e7944382dddee5e168c839f40a6c3b6f26 Mon Sep 17 00:00:00 2001 From: tamirms Date: Wed, 21 Feb 2024 21:41:51 +0000 Subject: [PATCH 063/234] services/horizon/internal/ingest: Add metrics for ingestion loaders (#5209) --- .../internal/db2/history/account_loader.go | 22 ++++++++++ .../db2/history/account_loader_test.go | 12 +++-- .../internal/db2/history/asset_loader.go | 14 ++++++ .../internal/db2/history/asset_loader_test.go | 15 +++++-- .../db2/history/claimable_balance_loader.go | 14 ++++++ .../history/claimable_balance_loader_test.go | 16 +++++-- .../db2/history/liquidity_pool_loader.go | 14 ++++++ .../db2/history/liquidity_pool_loader_test.go | 15 +++++-- services/horizon/internal/ingest/fsm.go | 27 ++++++++++++ .../internal/ingest/group_processors.go | 44 ++++++++++++------- services/horizon/internal/ingest/main.go | 26 +++++++++++ services/horizon/internal/ingest/main_test.go | 8 +++- .../internal/ingest/processor_runner.go | 24 +++++++--- 13 files changed, 213 insertions(+), 38 deletions(-) diff --git a/services/horizon/internal/db2/history/account_loader.go b/services/horizon/internal/db2/history/account_loader.go index f3946b0448..e7e7e90854 100644 --- a/services/horizon/internal/db2/history/account_loader.go +++ b/services/horizon/internal/db2/history/account_loader.go @@ -39,6 +39,7 @@ type AccountLoader struct { sealed bool set set.Set[string] ids map[string]int64 + stats LoaderStats } var errSealed = errors.New("cannot register more entries to loader after calling Exec()") @@ -49,6 +50,7 @@ func NewAccountLoader() *AccountLoader { sealed: false, set: set.Set[string]{}, ids: map[string]int64{}, + stats: LoaderStats{}, } } @@ -99,6 +101,14 @@ func (a *AccountLoader) lookupKeys(ctx context.Context, q *Q, addresses []string return nil } +// LoaderStats describes the result of executing a history lookup id loader +type LoaderStats struct { + // Total is the number of elements registered to the loader + Total int + // Inserted is the number of elements inserted into the lookup table + Inserted int +} + // Exec will look up all the history account ids for the addresses registered in the loader. // If there are no history account ids for a given set of addresses, Exec will insert rows // into the history_accounts table to establish a mapping between address and history account id. @@ -116,6 +126,7 @@ func (a *AccountLoader) Exec(ctx context.Context, session db.SessionInterface) e if err := a.lookupKeys(ctx, q, addresses); err != nil { return err } + a.stats.Total += len(addresses) insert := 0 for _, address := range addresses { @@ -149,10 +160,21 @@ func (a *AccountLoader) Exec(ctx context.Context, session db.SessionInterface) e if err != nil { return err } + a.stats.Inserted += insert return a.lookupKeys(ctx, q, addresses) } +// Stats returns the number of addresses registered in the loader and the number of addresses +// inserted into the history_accounts table. +func (a *AccountLoader) Stats() LoaderStats { + return a.stats +} + +func (a *AccountLoader) Name() string { + return "AccountLoader" +} + type bulkInsertField struct { name string dbType string diff --git a/services/horizon/internal/db2/history/account_loader_test.go b/services/horizon/internal/db2/history/account_loader_test.go index 54d2c7a143..ed30b43bd9 100644 --- a/services/horizon/internal/db2/history/account_loader_test.go +++ b/services/horizon/internal/db2/history/account_loader_test.go @@ -31,14 +31,20 @@ func TestAccountLoader(t *testing.T) { assert.Equal(t, future, duplicateFuture) } - assert.NoError(t, loader.Exec(context.Background(), session)) + err := loader.Exec(context.Background(), session) + assert.NoError(t, err) + assert.Equal(t, LoaderStats{ + Total: 100, + Inserted: 100, + }, loader.Stats()) assert.Panics(t, func() { loader.GetFuture(keypair.MustRandom().Address()) }) q := &Q{session} for _, address := range addresses { - internalId, err := loader.GetNow(address) + var internalId int64 + internalId, err = loader.GetNow(address) assert.NoError(t, err) var account Account assert.NoError(t, q.AccountByAddress(context.Background(), &account, address)) @@ -46,7 +52,7 @@ func TestAccountLoader(t *testing.T) { assert.Equal(t, account.Address, address) } - _, err := loader.GetNow("not present") + _, err = loader.GetNow("not present") assert.Error(t, err) assert.Contains(t, err.Error(), `was not found`) } diff --git a/services/horizon/internal/db2/history/asset_loader.go b/services/horizon/internal/db2/history/asset_loader.go index b5ee9a8326..bdf40fb843 100644 --- a/services/horizon/internal/db2/history/asset_loader.go +++ b/services/horizon/internal/db2/history/asset_loader.go @@ -60,6 +60,7 @@ type AssetLoader struct { sealed bool set set.Set[AssetKey] ids map[AssetKey]int64 + stats LoaderStats } // NewAssetLoader will construct a new AssetLoader instance. @@ -68,6 +69,7 @@ func NewAssetLoader() *AssetLoader { sealed: false, set: set.Set[AssetKey]{}, ids: map[AssetKey]int64{}, + stats: LoaderStats{}, } } @@ -145,6 +147,7 @@ func (a *AssetLoader) Exec(ctx context.Context, session db.SessionInterface) err if err := a.lookupKeys(ctx, q, keys); err != nil { return err } + a.stats.Total += len(keys) assetTypes := make([]string, 0, len(a.set)-len(a.ids)) assetCodes := make([]string, 0, len(a.set)-len(a.ids)) @@ -196,10 +199,21 @@ func (a *AssetLoader) Exec(ctx context.Context, session db.SessionInterface) err if err != nil { return err } + a.stats.Inserted += insert return a.lookupKeys(ctx, q, keys) } +// Stats returns the number of assets registered in the loader and the number of assets +// inserted into the history_assets table. +func (a *AssetLoader) Stats() LoaderStats { + return a.stats +} + +func (a *AssetLoader) Name() string { + return "AssetLoader" +} + // AssetLoaderStub is a stub wrapper around AssetLoader which allows // you to manually configure the mapping of assets to history asset ids type AssetLoaderStub struct { diff --git a/services/horizon/internal/db2/history/asset_loader_test.go b/services/horizon/internal/db2/history/asset_loader_test.go index d67163d764..f097561e4a 100644 --- a/services/horizon/internal/db2/history/asset_loader_test.go +++ b/services/horizon/internal/db2/history/asset_loader_test.go @@ -76,14 +76,20 @@ func TestAssetLoader(t *testing.T) { assert.Equal(t, future, duplicateFuture) } - assert.NoError(t, loader.Exec(context.Background(), session)) + err := loader.Exec(context.Background(), session) + assert.NoError(t, err) + assert.Equal(t, LoaderStats{ + Total: 100, + Inserted: 100, + }, loader.Stats()) assert.Panics(t, func() { loader.GetFuture(AssetKey{Type: "invalid"}) }) q := &Q{session} for _, key := range keys { - internalID, err := loader.GetNow(key) + var internalID int64 + internalID, err = loader.GetNow(key) assert.NoError(t, err) var assetXDR xdr.Asset if key.Type == "native" { @@ -91,12 +97,13 @@ func TestAssetLoader(t *testing.T) { } else { assetXDR = xdr.MustNewCreditAsset(key.Code, key.Issuer) } - assetID, err := q.GetAssetID(context.Background(), assetXDR) + var assetID int64 + assetID, err = q.GetAssetID(context.Background(), assetXDR) assert.NoError(t, err) assert.Equal(t, assetID, internalID) } - _, err := loader.GetNow(AssetKey{}) + _, err = loader.GetNow(AssetKey{}) assert.Error(t, err) assert.Contains(t, err.Error(), `was not found`) } diff --git a/services/horizon/internal/db2/history/claimable_balance_loader.go b/services/horizon/internal/db2/history/claimable_balance_loader.go index dd7dee4ea5..ef18683cb6 100644 --- a/services/horizon/internal/db2/history/claimable_balance_loader.go +++ b/services/horizon/internal/db2/history/claimable_balance_loader.go @@ -34,6 +34,7 @@ type ClaimableBalanceLoader struct { sealed bool set set.Set[string] ids map[string]int64 + stats LoaderStats } // NewClaimableBalanceLoader will construct a new ClaimableBalanceLoader instance. @@ -42,6 +43,7 @@ func NewClaimableBalanceLoader() *ClaimableBalanceLoader { sealed: false, set: set.Set[string]{}, ids: map[string]int64{}, + stats: LoaderStats{}, } } @@ -109,6 +111,7 @@ func (a *ClaimableBalanceLoader) Exec(ctx context.Context, session db.SessionInt if err := a.lookupKeys(ctx, q, ids); err != nil { return err } + a.stats.Total += len(ids) insert := 0 for _, id := range ids { @@ -142,6 +145,17 @@ func (a *ClaimableBalanceLoader) Exec(ctx context.Context, session db.SessionInt if err != nil { return err } + a.stats.Inserted += insert return a.lookupKeys(ctx, q, ids) } + +// Stats returns the number of claimable balances registered in the loader and the number of claimable balances +// inserted into the history_claimable_balances table. +func (a *ClaimableBalanceLoader) Stats() LoaderStats { + return a.stats +} + +func (a *ClaimableBalanceLoader) Name() string { + return "ClaimableBalanceLoader" +} diff --git a/services/horizon/internal/db2/history/claimable_balance_loader_test.go b/services/horizon/internal/db2/history/claimable_balance_loader_test.go index 4dd7324521..aaf91ccdcc 100644 --- a/services/horizon/internal/db2/history/claimable_balance_loader_test.go +++ b/services/horizon/internal/db2/history/claimable_balance_loader_test.go @@ -2,6 +2,7 @@ package history import ( "context" + "database/sql/driver" "testing" "github.com/stretchr/testify/assert" @@ -39,7 +40,12 @@ func TestClaimableBalanceLoader(t *testing.T) { assert.Equal(t, future, duplicateFuture) } - assert.NoError(t, loader.Exec(context.Background(), session)) + err := loader.Exec(context.Background(), session) + assert.NoError(t, err) + assert.Equal(t, LoaderStats{ + Total: 100, + Inserted: 100, + }, loader.Stats()) assert.Panics(t, func() { loader.GetFuture("not-present") }) @@ -47,16 +53,18 @@ func TestClaimableBalanceLoader(t *testing.T) { q := &Q{session} for i, id := range ids { future := futures[i] - internalID, err := future.Value() + var internalID driver.Value + internalID, err = future.Value() assert.NoError(t, err) - cb, err := q.ClaimableBalanceByID(context.Background(), id) + var cb HistoryClaimableBalance + cb, err = q.ClaimableBalanceByID(context.Background(), id) assert.NoError(t, err) assert.Equal(t, cb.BalanceID, id) assert.Equal(t, cb.InternalID, internalID) } futureCb := &FutureClaimableBalanceID{id: "not-present", loader: loader} - _, err := futureCb.Value() + _, err = futureCb.Value() assert.Error(t, err) assert.Contains(t, err.Error(), `was not found`) } diff --git a/services/horizon/internal/db2/history/liquidity_pool_loader.go b/services/horizon/internal/db2/history/liquidity_pool_loader.go index cf89ae67b4..d619fa3bb4 100644 --- a/services/horizon/internal/db2/history/liquidity_pool_loader.go +++ b/services/horizon/internal/db2/history/liquidity_pool_loader.go @@ -34,6 +34,7 @@ type LiquidityPoolLoader struct { sealed bool set set.Set[string] ids map[string]int64 + stats LoaderStats } // NewLiquidityPoolLoader will construct a new LiquidityPoolLoader instance. @@ -42,6 +43,7 @@ func NewLiquidityPoolLoader() *LiquidityPoolLoader { sealed: false, set: set.Set[string]{}, ids: map[string]int64{}, + stats: LoaderStats{}, } } @@ -109,6 +111,7 @@ func (a *LiquidityPoolLoader) Exec(ctx context.Context, session db.SessionInterf if err := a.lookupKeys(ctx, q, ids); err != nil { return err } + a.stats.Total += len(ids) insert := 0 for _, id := range ids { @@ -142,10 +145,21 @@ func (a *LiquidityPoolLoader) Exec(ctx context.Context, session db.SessionInterf if err != nil { return err } + a.stats.Inserted += insert return a.lookupKeys(ctx, q, ids) } +// Stats returns the number of liquidity pools registered in the loader and the number of liquidity pools +// inserted into the history_liquidity_pools table. +func (a *LiquidityPoolLoader) Stats() LoaderStats { + return a.stats +} + +func (a *LiquidityPoolLoader) Name() string { + return "LiquidityPoolLoader" +} + // LiquidityPoolLoaderStub is a stub wrapper around LiquidityPoolLoader which allows // you to manually configure the mapping of liquidity pools to history liquidity ppol ids type LiquidityPoolLoaderStub struct { diff --git a/services/horizon/internal/db2/history/liquidity_pool_loader_test.go b/services/horizon/internal/db2/history/liquidity_pool_loader_test.go index 6e5b4addf7..25ca80826c 100644 --- a/services/horizon/internal/db2/history/liquidity_pool_loader_test.go +++ b/services/horizon/internal/db2/history/liquidity_pool_loader_test.go @@ -34,22 +34,29 @@ func TestLiquidityPoolLoader(t *testing.T) { assert.Equal(t, future, duplicateFuture) } - assert.NoError(t, loader.Exec(context.Background(), session)) + err := loader.Exec(context.Background(), session) + assert.NoError(t, err) + assert.Equal(t, LoaderStats{ + Total: 100, + Inserted: 100, + }, loader.Stats()) assert.Panics(t, func() { loader.GetFuture("not-present") }) q := &Q{session} for _, id := range ids { - internalID, err := loader.GetNow(id) + var internalID int64 + internalID, err = loader.GetNow(id) assert.NoError(t, err) - lp, err := q.LiquidityPoolByID(context.Background(), id) + var lp HistoryLiquidityPool + lp, err = q.LiquidityPoolByID(context.Background(), id) assert.NoError(t, err) assert.Equal(t, lp.PoolID, id) assert.Equal(t, lp.InternalID, internalID) } - _, err := loader.GetNow("not present") + _, err = loader.GetNow("not present") assert.Error(t, err) assert.Contains(t, err.Error(), `was not found`) } diff --git a/services/horizon/internal/ingest/fsm.go b/services/horizon/internal/ingest/fsm.go index 892868e5b9..c79e11e28a 100644 --- a/services/horizon/internal/ingest/fsm.go +++ b/services/horizon/internal/ingest/fsm.go @@ -11,6 +11,7 @@ import ( "github.com/stellar/go/historyarchive" "github.com/stellar/go/ingest" "github.com/stellar/go/ingest/ledgerbackend" + "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/support/errors" logpkg "github.com/stellar/go/support/log" "github.com/stellar/go/xdr" @@ -523,6 +524,8 @@ func (r resumeState) run(s *system) (transition, error) { tradeStatsMap := stats.tradeStats.Map() r.addLedgerStatsMetricFromMap(s, "trades", tradeStatsMap) r.addProcessorDurationsMetricFromMap(s, stats.transactionDurations) + r.addLoaderDurationsMetricFromMap(s, stats.transactionDurations) + r.addLoaderStatsMetric(s, stats.loaderStats) // since a single system instance is shared throughout all states, // this will sweep up increments to history archive counters @@ -573,6 +576,30 @@ func (r resumeState) addProcessorDurationsMetricFromMap(s *system, m map[string] } } +func (r resumeState) addLoaderDurationsMetricFromMap(s *system, m map[string]time.Duration) { + for loaderName, value := range m { + s.Metrics().LoadersRunDurationSummary. + With(prometheus.Labels{"name": loaderName}).Observe(value.Seconds()) + } +} + +func (r resumeState) addLoaderStatsMetric(s *system, loaderSTats map[string]history.LoaderStats) { + for loaderName, stats := range loaderSTats { + s.Metrics().LoadersStatsSummary. + With(prometheus.Labels{ + "name": loaderName, + "stat": "total_queried", + }). + Observe(float64(stats.Total)) + s.Metrics().LoadersStatsSummary. + With(prometheus.Labels{ + "name": loaderName, + "stat": "total_inserted", + }). + Observe(float64(stats.Inserted)) + } +} + func addHistoryArchiveStatsMetrics(s *system, stats []historyarchive.ArchiveStats) { for _, historyServerStat := range stats { s.Metrics().HistoryArchiveStatsCounter. diff --git a/services/horizon/internal/ingest/group_processors.go b/services/horizon/internal/ingest/group_processors.go index 5af3d024ef..1ea46b4f21 100644 --- a/services/horizon/internal/ingest/group_processors.go +++ b/services/horizon/internal/ingest/group_processors.go @@ -6,21 +6,22 @@ import ( "time" "github.com/stellar/go/ingest" + "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/services/horizon/internal/ingest/processors" "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" ) -type processorsRunDurations map[string]time.Duration +type runDurations map[string]time.Duration -func (d processorsRunDurations) AddRunDuration(name string, startTime time.Time) { +func (d runDurations) AddRunDuration(name string, startTime time.Time) { d[name] += time.Since(startTime) } type groupChangeProcessors struct { - processors []horizonChangeProcessor - processorsRunDurations + processors []horizonChangeProcessor + processorsRunDurations runDurations } func newGroupChangeProcessors(processors []horizonChangeProcessor) *groupChangeProcessors { @@ -36,7 +37,7 @@ func (g groupChangeProcessors) ProcessChange(ctx context.Context, change ingest. if err := p.ProcessChange(ctx, change); err != nil { return errors.Wrapf(err, "error in %T.ProcessChange", p) } - g.AddRunDuration(fmt.Sprintf("%T", p), startTime) + g.processorsRunDurations.AddRunDuration(fmt.Sprintf("%T", p), startTime) } return nil } @@ -47,15 +48,17 @@ func (g groupChangeProcessors) Commit(ctx context.Context) error { if err := p.Commit(ctx); err != nil { return errors.Wrapf(err, "error in %T.Commit", p) } - g.AddRunDuration(fmt.Sprintf("%T", p), startTime) + g.processorsRunDurations.AddRunDuration(fmt.Sprintf("%T", p), startTime) } return nil } type groupTransactionProcessors struct { - processors []horizonTransactionProcessor - lazyLoaders []horizonLazyLoader - processorsRunDurations + processors []horizonTransactionProcessor + lazyLoaders []horizonLazyLoader + processorsRunDurations runDurations + loaderRunDurations runDurations + loaderStats map[string]history.LoaderStats transactionStatsProcessor *processors.StatsLedgerTransactionProcessor tradeProcessor *processors.TradeProcessor } @@ -78,6 +81,8 @@ func newGroupTransactionProcessors(processors []horizonTransactionProcessor, return &groupTransactionProcessors{ processors: processors, processorsRunDurations: make(map[string]time.Duration), + loaderRunDurations: make(map[string]time.Duration), + loaderStats: make(map[string]history.LoaderStats), lazyLoaders: lazyLoaders, transactionStatsProcessor: transactionStatsProcessor, tradeProcessor: tradeProcessor, @@ -90,7 +95,7 @@ func (g groupTransactionProcessors) ProcessTransaction(lcm xdr.LedgerCloseMeta, if err := p.ProcessTransaction(lcm, tx); err != nil { return errors.Wrapf(err, "error in %T.ProcessTransaction", p) } - g.AddRunDuration(fmt.Sprintf("%T", p), startTime) + g.processorsRunDurations.AddRunDuration(fmt.Sprintf("%T", p), startTime) } return nil } @@ -99,9 +104,16 @@ func (g groupTransactionProcessors) Flush(ctx context.Context, session db.Sessio // need to trigger all lazy loaders to now resolve their future placeholders // with real db values first for _, loader := range g.lazyLoaders { + startTime := time.Now() if err := loader.Exec(ctx, session); err != nil { return errors.Wrapf(err, "error during lazy loader resolution, %T.Exec", loader) } + name := loader.Name() + g.loaderRunDurations.AddRunDuration(name, startTime) + if _, ok := g.loaderStats[name]; ok { + return fmt.Errorf("%s is present multiple times", name) + } + g.loaderStats[name] = loader.Stats() } // now flush each processor which may call loader.GetNow(), which @@ -111,13 +123,15 @@ func (g groupTransactionProcessors) Flush(ctx context.Context, session db.Sessio if err := p.Flush(ctx, session); err != nil { return errors.Wrapf(err, "error in %T.Flush", p) } - g.AddRunDuration(fmt.Sprintf("%T", p), startTime) + g.processorsRunDurations.AddRunDuration(fmt.Sprintf("%T", p), startTime) } return nil } func (g *groupTransactionProcessors) ResetStats() { g.processorsRunDurations = make(map[string]time.Duration) + g.loaderRunDurations = make(map[string]time.Duration) + g.loaderStats = make(map[string]history.LoaderStats) if g.tradeProcessor != nil { g.tradeProcessor.ResetStats() } @@ -128,14 +142,14 @@ func (g *groupTransactionProcessors) ResetStats() { type groupTransactionFilterers struct { filterers []processors.LedgerTransactionFilterer - processorsRunDurations + runDurations droppedTransactions int64 } func newGroupTransactionFilterers(filterers []processors.LedgerTransactionFilterer) *groupTransactionFilterers { return &groupTransactionFilterers{ - filterers: filterers, - processorsRunDurations: make(map[string]time.Duration), + filterers: filterers, + runDurations: make(map[string]time.Duration), } } @@ -158,5 +172,5 @@ func (g *groupTransactionFilterers) FilterTransaction(ctx context.Context, tx in func (g *groupTransactionFilterers) ResetStats() { g.droppedTransactions = 0 - g.processorsRunDurations = make(map[string]time.Duration) + g.runDurations = make(map[string]time.Duration) } diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index ae049800fc..127d8f0293 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -166,6 +166,12 @@ type Metrics struct { // ProcessorsRunDurationSummary exposes processors run durations. ProcessorsRunDurationSummary *prometheus.SummaryVec + // LoadersRunDurationSummary exposes run durations for the ingestion loaders. + LoadersRunDurationSummary *prometheus.SummaryVec + + // LoadersRunDurationSummary exposes stats for the ingestion loaders. + LoadersStatsSummary *prometheus.SummaryVec + // ArchiveRequestCounter counts how many http requests are sent to history server HistoryArchiveStatsCounter *prometheus.CounterVec } @@ -407,6 +413,24 @@ func (s *system) initMetrics() { []string{"name"}, ) + s.metrics.LoadersRunDurationSummary = prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Namespace: "horizon", Subsystem: "ingest", Name: "loader_run_duration_seconds", + Help: "run durations of ingestion loaders, sliding window = 10m", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"name"}, + ) + + s.metrics.LoadersStatsSummary = prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Namespace: "horizon", Subsystem: "ingest", Name: "loader_stats", + Help: "stats from ingestion loaders, sliding window = 10m", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"name", "stat"}, + ) + s.metrics.HistoryArchiveStatsCounter = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: "horizon", Subsystem: "ingest", Name: "history_archive_stats_total", @@ -444,6 +468,8 @@ func (s *system) RegisterMetrics(registry *prometheus.Registry) { registry.MustRegister(s.metrics.LedgerStatsCounter) registry.MustRegister(s.metrics.ProcessorsRunDuration) registry.MustRegister(s.metrics.ProcessorsRunDurationSummary) + registry.MustRegister(s.metrics.LoadersRunDurationSummary) + registry.MustRegister(s.metrics.LoadersStatsSummary) registry.MustRegister(s.metrics.StateVerifyLedgerEntriesCount) registry.MustRegister(s.metrics.HistoryArchiveStatsCounter) s.ledgerBackend = ledgerbackend.WithMetrics(s.ledgerBackend, registry, "horizon") diff --git a/services/horizon/internal/ingest/main_test.go b/services/horizon/internal/ingest/main_test.go index 80b5a40ed1..71c8119273 100644 --- a/services/horizon/internal/ingest/main_test.go +++ b/services/horizon/internal/ingest/main_test.go @@ -531,14 +531,18 @@ func (m *mockProcessorsRunner) RunAllProcessorsOnLedger(ledger xdr.LedgerCloseMe func (m *mockProcessorsRunner) RunTransactionProcessorsOnLedger(ledger xdr.LedgerCloseMeta) ( processors.StatsLedgerTransactionProcessorResults, - processorsRunDurations, + runDurations, processors.TradeStats, + runDurations, + map[string]history.LoaderStats, error, ) { args := m.Called(ledger) return args.Get(0).(processors.StatsLedgerTransactionProcessorResults), - args.Get(1).(processorsRunDurations), + args.Get(1).(runDurations), args.Get(2).(processors.TradeStats), + args.Get(3).(runDurations), + args.Get(4).(map[string]history.LoaderStats), args.Error(3) } diff --git a/services/horizon/internal/ingest/processor_runner.go b/services/horizon/internal/ingest/processor_runner.go index 0023ca3140..c9730276c1 100644 --- a/services/horizon/internal/ingest/processor_runner.go +++ b/services/horizon/internal/ingest/processor_runner.go @@ -37,6 +37,8 @@ type horizonTransactionProcessor interface { type horizonLazyLoader interface { Exec(ctx context.Context, session db.SessionInterface) error + Name() string + Stats() history.LoaderStats } type statsChangeProcessor struct { @@ -49,9 +51,11 @@ func (statsChangeProcessor) Commit(ctx context.Context) error { type ledgerStats struct { changeStats ingest.StatsChangeProcessorResults - changeDurations processorsRunDurations + changeDurations runDurations transactionStats processors.StatsLedgerTransactionProcessorResults - transactionDurations processorsRunDurations + transactionDurations runDurations + loaderDurations runDurations + loaderStats map[string]history.LoaderStats tradeStats processors.TradeStats } @@ -68,8 +72,10 @@ type ProcessorRunnerInterface interface { ) (ingest.StatsChangeProcessorResults, error) RunTransactionProcessorsOnLedger(ledger xdr.LedgerCloseMeta) ( transactionStats processors.StatsLedgerTransactionProcessorResults, - transactionDurations processorsRunDurations, + transactionDurations runDurations, tradeStats processors.TradeStats, + loaderDurations runDurations, + loaderStats map[string]history.LoaderStats, err error, ) RunTransactionProcessorsOnLedgers(ledgers []xdr.LedgerCloseMeta) error @@ -360,8 +366,10 @@ func (s *ProcessorRunner) streamLedger(ledger xdr.LedgerCloseMeta, func (s *ProcessorRunner) RunTransactionProcessorsOnLedger(ledger xdr.LedgerCloseMeta) ( transactionStats processors.StatsLedgerTransactionProcessorResults, - transactionDurations processorsRunDurations, + transactionDurations runDurations, tradeStats processors.TradeStats, + loaderDurations runDurations, + loaderStats map[string]history.LoaderStats, err error, ) { // ensure capture of the ledger to history regardless of whether it has transactions. @@ -394,7 +402,9 @@ func (s *ProcessorRunner) RunTransactionProcessorsOnLedger(ledger xdr.LedgerClos for key, duration := range groupFilteredOutProcessors.processorsRunDurations { transactionDurations[key] = duration } - for key, duration := range groupTransactionFilterers.processorsRunDurations { + loaderStats = groupTransactionProcessors.loaderStats + loaderDurations = groupTransactionProcessors.loaderRunDurations + for key, duration := range groupTransactionFilterers.runDurations { transactionDurations[key] = duration } @@ -499,13 +509,15 @@ func (s *ProcessorRunner) RunAllProcessorsOnLedger(ledger xdr.LedgerCloseMeta) ( return } - transactionStats, transactionDurations, tradeStats, err := s.RunTransactionProcessorsOnLedger(ledger) + transactionStats, transactionDurations, tradeStats, loaderDurations, loaderStats, err := s.RunTransactionProcessorsOnLedger(ledger) stats.changeStats = changeStatsProcessor.GetResults() stats.changeDurations = groupChangeProcessors.processorsRunDurations stats.transactionStats = transactionStats stats.transactionDurations = transactionDurations stats.tradeStats = tradeStats + stats.loaderDurations = loaderDurations + stats.loaderStats = loaderStats return } From 29c49f9c099f6b8d2fd6b998f6bbbad265907f16 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Tue, 27 Feb 2024 08:57:56 -0800 Subject: [PATCH 064/234] exp/services/ledgerexporter: Initial implementation (#5160) --- .github/workflows/horizon.yml | 1 + Makefile | 4 +- exp/services/ledgerexporter/README.md | 83 ++++++++ exp/services/ledgerexporter/config.toml | 7 + exp/services/ledgerexporter/internal/app.go | 131 ++++++++++++ .../ledgerexporter/internal/config.go | 192 +++++++++++++++++ .../ledgerexporter/internal/config_test.go | 170 +++++++++++++++ .../ledgerexporter/internal/datastore.go | 57 +++++ .../ledgerexporter/internal/exportmanager.go | 114 ++++++++++ .../internal/exportmanager_test.go | 156 ++++++++++++++ .../ledgerexporter/internal/gcs_datastore.go | 105 ++++++++++ .../internal/ledger_meta_archive.go | 65 ++++++ .../internal/ledger_meta_archive_test.go | 84 ++++++++ .../ledgerexporter/internal/mock_datastore.go | 43 ++++ .../ledgerexporter/internal/uploader.go | 74 +++++++ .../ledgerexporter/internal/uploader_test.go | 127 ++++++++++++ exp/services/ledgerexporter/internal/utils.go | 104 ++++++++++ .../ledgerexporter/internal/utils_test.go | 87 ++++++++ exp/services/ledgerexporter/main.go | 178 +--------------- go.mod | 37 ++-- go.sum | 80 +++---- gxdr/xdr_generated.go | 85 +++++++- .../configs/captive-core-pubnet.cfg | 195 ++++++++++++++++++ .../configs/captive-core-testnet.cfg | 28 +++ ingest/ledgerbackend/toml.go | 9 + xdr/Stellar-exporter.x | 23 +++ xdr/xdr_generated.go | 112 ++++++++++ 27 files changed, 2120 insertions(+), 231 deletions(-) create mode 100644 exp/services/ledgerexporter/README.md create mode 100644 exp/services/ledgerexporter/config.toml create mode 100644 exp/services/ledgerexporter/internal/app.go create mode 100644 exp/services/ledgerexporter/internal/config.go create mode 100644 exp/services/ledgerexporter/internal/config_test.go create mode 100644 exp/services/ledgerexporter/internal/datastore.go create mode 100644 exp/services/ledgerexporter/internal/exportmanager.go create mode 100644 exp/services/ledgerexporter/internal/exportmanager_test.go create mode 100644 exp/services/ledgerexporter/internal/gcs_datastore.go create mode 100644 exp/services/ledgerexporter/internal/ledger_meta_archive.go create mode 100644 exp/services/ledgerexporter/internal/ledger_meta_archive_test.go create mode 100644 exp/services/ledgerexporter/internal/mock_datastore.go create mode 100644 exp/services/ledgerexporter/internal/uploader.go create mode 100644 exp/services/ledgerexporter/internal/uploader_test.go create mode 100644 exp/services/ledgerexporter/internal/utils.go create mode 100644 exp/services/ledgerexporter/internal/utils_test.go create mode 100644 ingest/ledgerbackend/configs/captive-core-pubnet.cfg create mode 100644 ingest/ledgerbackend/configs/captive-core-testnet.cfg create mode 100644 xdr/Stellar-exporter.x diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index 677a60b835..bf9cae7246 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -162,6 +162,7 @@ jobs: ledger-exporter: name: Test and push the Ledger Exporter images runs-on: ubuntu-latest + if: false # Disable the job steps: - uses: actions/checkout@v3 with: diff --git a/Makefile b/Makefile index abacb05dd8..13486ea7d7 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,8 @@ xdr/Stellar-contract.x \ xdr/Stellar-internal.x \ xdr/Stellar-contract-config-setting.x -XDRS = $(DOWNLOADABLE_XDRS) xdr/Stellar-lighthorizon.x - +XDRS = $(DOWNLOADABLE_XDRS) xdr/Stellar-lighthorizon.x \ + xdr/Stellar-exporter.x XDRGEN_COMMIT=e2cac557162d99b12ae73b846cf3d5bfe16636de diff --git a/exp/services/ledgerexporter/README.md b/exp/services/ledgerexporter/README.md new file mode 100644 index 0000000000..ad8a3ddeae --- /dev/null +++ b/exp/services/ledgerexporter/README.md @@ -0,0 +1,83 @@ +# Ledger Exporter (Work in Progress) + +The Ledger Exporter is a tool designed to export ledger data from a Stellar network and upload it to a specified destination. It supports both bounded and unbounded modes, allowing users to export a specific range of ledgers or continuously export new ledgers as they arrive on the network. + +Ledger Exporter currently uses captive-core as the ledger backend and GCS as the destination data store. + +# Exported Data Format +The tool allows for the export of multiple ledgers in a single exported file. The exported data is in XDR format and is compressed using gzip before being uploaded. + +```go +type LedgerCloseMetaBatch struct { + StartSequence uint32 + EndSequence uint32 + LedgerCloseMetas []LedgerCloseMeta +} +``` + +## Getting Started + +### Installation (coming soon) + +### Command Line Options + +#### Bounded Mode: +Exports a specific range of ledgers, defined by --start and --end. +```bash +ledgerexporter --start --end --config-file +``` + +#### Unbounded Mode: +Exports ledgers continuously starting from --start. In this mode, the end ledger is either not provided or set to 0. +```bash +ledgerexporter --start --config-file +``` + + +Starts exporting from a specified number of ledgers before the latest ledger sequence number on the network. +```bash +ledgerexporter --from-last --config-file +``` + +### Configuration (toml): + +```toml +network = "testnet" # Options: `testnet` or `pubnet` +destination_url = "gcs://your-bucket-name" + +[exporter_config] +ledgers_per_file = 64 +files_per_partition = 10 +``` + +#### Stellar-core configuration: +- The exporter automatically configures stellar-core based on the network specified in the config. +- Ensure you have stellar-core installed and accessible in your system's $PATH. + +### Exported Files + +#### File Organization: +- Ledgers are grouped into files, with the number of ledgers per file set by `ledgers_per_file`. +- Files are further organized into partitions, with the number of files per partition set by `files_per_partition`. + +### Filename Structure: +- Filenames indicate the ledger range they contain, e.g., `0-63.xdr.gz` holds ledgers 0 to 63. +- Partition directories group files, e.g., `/0-639/` holds files for ledgers 0 to 639. + +#### Example: +with `ledgers_per_file = 64` and `files_per_partition = 10`: +- Partition names: `/0-639`, `/640-1279`, ... +- Filenames: `/0-639/0-63.xdr.gz`, `/0-639/64-127.xdr.gz`, ... + +#### Special Cases: + +- If `ledgers_per_file` is set to 1, filenames will only contain the ledger number. +- If `files_per_partition` is set to 1, filenames will not contain the partition. + +#### Note: +- Avoid changing `ledgers_per_file` and `files_per_partition` after configuration for consistency. + +#### Retrieving Data: +- To locate a specific ledger sequence, calculate the partition name and ledger file name using `files_per_partition` and `ledgers_per_file`. +- The `GetObjectKeyFromSequenceNumber` function automates this calculation. + diff --git a/exp/services/ledgerexporter/config.toml b/exp/services/ledgerexporter/config.toml new file mode 100644 index 0000000000..30c4a4fafe --- /dev/null +++ b/exp/services/ledgerexporter/config.toml @@ -0,0 +1,7 @@ +network = "testnet" +destination_url = "gcs://exporter-test/ledgers" + +[exporter_config] + ledgers_per_file = 1 + files_per_partition = 64000 + diff --git a/exp/services/ledgerexporter/internal/app.go b/exp/services/ledgerexporter/internal/app.go new file mode 100644 index 0000000000..fb4a5f788b --- /dev/null +++ b/exp/services/ledgerexporter/internal/app.go @@ -0,0 +1,131 @@ +package ledgerexporter + +import ( + "context" + _ "embed" + "fmt" + "os" + "os/signal" + "sync" + "syscall" + + "github.com/pkg/errors" + "github.com/stellar/go/ingest/ledgerbackend" + _ "github.com/stellar/go/network" + "github.com/stellar/go/support/log" +) + +var ( + logger = log.New().WithField("service", "ledger-exporter") +) + +type App struct { + config Config + ledgerBackend ledgerbackend.LedgerBackend + dataStore DataStore + exportManager ExportManager + uploader Uploader +} + +func NewApp() *App { + logger.SetLevel(log.DebugLevel) + + config := Config{} + err := config.LoadConfig() + logFatalIf(err, "Could not load configuration") + + app := &App{config: config} + return app +} + +func (a *App) init(ctx context.Context) { + a.dataStore = mustNewDataStore(ctx, &a.config) + a.ledgerBackend = mustNewLedgerBackend(ctx, a.config) + a.exportManager = NewExportManager(a.config.ExporterConfig, a.ledgerBackend) + a.uploader = NewUploader(a.dataStore, a.exportManager.GetMetaArchiveChannel()) +} + +func (a *App) close() { + if err := a.dataStore.Close(); err != nil { + logger.WithError(err).Error("Error closing datastore") + } + if err := a.ledgerBackend.Close(); err != nil { + logger.WithError(err).Error("Error closing ledgerBackend") + } +} + +func (a *App) Run() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + a.init(ctx) + defer a.close() + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + + err := a.uploader.Run(ctx) + if err != nil && !errors.Is(err, context.Canceled) { + logger.WithError(err).Error("Error executing Uploader") + cancel() + } + }() + + go func() { + defer wg.Done() + + err := a.exportManager.Run(ctx, a.config.StartLedger, a.config.EndLedger) + if err != nil && !errors.Is(err, context.Canceled) { + logger.WithError(err).Error("Error executing ExportManager") + cancel() + } + }() + + // Handle OS signals to gracefully terminate the service + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + go func() { + sig := <-sigCh + logger.Infof("Received termination signal: %v", sig) + cancel() + }() + + wg.Wait() + logger.Info("Shutting down ledger-exporter") +} + +func mustNewDataStore(ctx context.Context, config *Config) DataStore { + dataStore, err := NewDataStore(ctx, fmt.Sprintf("%s/%s", config.DestinationURL, config.Network)) + logFatalIf(err, "Could not connect to destination data store") + return dataStore +} + +// mustNewLedgerBackend Creates and initializes captive core ledger backend +// Currently, only supports captive-core as ledger backend +func mustNewLedgerBackend(ctx context.Context, config Config) ledgerbackend.LedgerBackend { + captiveConfig := config.GenerateCaptiveCoreConfig() + + // Create a new captive core backend + backend, err := ledgerbackend.NewCaptive(captiveConfig) + logFatalIf(err, "Failed to create captive-core instance") + + var ledgerRange ledgerbackend.Range + if config.EndLedger == 0 { + ledgerRange = ledgerbackend.UnboundedRange(config.StartLedger) + } else { + ledgerRange = ledgerbackend.BoundedRange(config.StartLedger, config.EndLedger) + } + + err = backend.PrepareRange(ctx, ledgerRange) + logFatalIf(err, "Could not prepare captive core ledger backend") + return backend +} + +func logFatalIf(err error, message string, args ...interface{}) { + if err != nil { + logger.WithError(err).Fatalf(message, args...) + } +} diff --git a/exp/services/ledgerexporter/internal/config.go b/exp/services/ledgerexporter/internal/config.go new file mode 100644 index 0000000000..640841b1d9 --- /dev/null +++ b/exp/services/ledgerexporter/internal/config.go @@ -0,0 +1,192 @@ +package ledgerexporter + +import ( + _ "embed" + "flag" + "os/exec" + + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/ingest/ledgerbackend" + "github.com/stellar/go/network" + + "github.com/pelletier/go-toml" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/ordered" +) + +const Pubnet = "pubnet" +const Testnet = "testnet" + +type StellarCoreConfig struct { + NetworkPassphrase string `toml:"network_passphrase"` + HistoryArchiveUrls []string `toml:"history_archive_urls"` + StellarCoreBinaryPath string `toml:"stellar_core_binary_path"` + CaptiveCoreTomlPath string `toml:"captive_core_toml_path"` +} + +type Config struct { + Network string `toml:"network"` + DestinationURL string `toml:"destination_url"` + ExporterConfig ExporterConfig `toml:"exporter_config"` + StellarCoreConfig StellarCoreConfig `toml:"stellar_core_config"` + + //From command-line + StartLedger uint32 `toml:"start"` + EndLedger uint32 `toml:"end"` + StartFromLastLedgers uint32 `toml:"from-last"` +} + +func (config *Config) LoadConfig() error { + // Parse command-line options + startLedger := flag.Uint("start", 0, "Starting ledger") + endLedger := flag.Uint("end", 0, "Ending ledger (inclusive)") + startFromLastNLedger := flag.Uint("from-last", 0, "Start streaming from last N ledgers") + + configFilePath := flag.String("config-file", "config.toml", "Path to the TOML config file") + flag.Parse() + + config.StartLedger = uint32(*startLedger) + config.EndLedger = uint32(*endLedger) + config.StartFromLastLedgers = uint32(*startFromLastNLedger) + + // Load config TOML file + cfg, err := toml.LoadFile(*configFilePath) + if err != nil { + return err + } + + // Unmarshal TOML data into the Config struct + err = cfg.Unmarshal(config) + logFatalIf(err, "Error unmarshalling TOML config.") + logger.Infof("Config: %v", *config) + + var historyArchiveUrls []string + switch config.Network { + case Pubnet: + historyArchiveUrls = network.PublicNetworkhistoryArchiveURLs + case Testnet: + historyArchiveUrls = network.TestNetworkhistoryArchiveURLs + default: + logger.Fatalf("Invalid network %s", config.Network) + } + + // Retrieve the latest ledger sequence from history archives + latestNetworkLedger, err := getLatestLedgerSequenceFromHistoryArchives(historyArchiveUrls) + logFatalIf(err, "Failed to retrieve the latest ledger sequence from history archives.") + + // Validate config params + err = config.validateAndSetLedgerRange(latestNetworkLedger) + logFatalIf(err, "Error validating config params.") + + // Validate and build the appropriate range + // TODO: Make it configurable + config.adjustLedgerRange() + + return nil +} + +func (config *Config) validateAndSetLedgerRange(latestNetworkLedger uint32) error { + if config.StartFromLastLedgers > 0 && (config.StartLedger > 0 || config.EndLedger > 0) { + return errors.New("--from-last cannot be used with --start or --end") + } + + if config.StartFromLastLedgers > 0 { + if config.StartFromLastLedgers > latestNetworkLedger { + return errors.Errorf("--from-last %d exceeds latest network ledger %d", + config.StartLedger, latestNetworkLedger) + } + config.StartLedger = latestNetworkLedger - config.StartFromLastLedgers + logger.Infof("Setting start ledger to %d, calculated as latest ledger (%d) minus --from-last value (%d)", + config.StartLedger, latestNetworkLedger, config.StartFromLastLedgers) + } + + if config.StartLedger > latestNetworkLedger { + return errors.Errorf("--start %d exceeds latest network ledger %d", + config.StartLedger, latestNetworkLedger) + } + + // Ensure that the start ledger is at least 2. + config.StartLedger = ordered.Max(2, config.StartLedger) + + if config.EndLedger != 0 { // Bounded mode + if config.EndLedger < config.StartLedger { + return errors.New("invalid --end value, must be >= --start") + } + if config.EndLedger > latestNetworkLedger { + return errors.Errorf("--end %d exceeds latest network ledger %d", + config.EndLedger, latestNetworkLedger) + } + } + + return nil +} + +func (config *Config) adjustLedgerRange() { + logger.Infof("Requested ledger range start=%d, end=%d", config.StartLedger, config.EndLedger) + + // Check if either the start or end ledger does not fall on the "LedgersPerFile" boundary + // and adjust the start and end ledger accordingly. + // Align the start ledger to the nearest "LedgersPerFile" boundary. + config.StartLedger = config.StartLedger / config.ExporterConfig.LedgersPerFile * config.ExporterConfig.LedgersPerFile + + // Ensure that the adjusted start ledger is at least 2. + config.StartLedger = ordered.Max(2, config.StartLedger) + + // Align the end ledger (for bounded cases) to the nearest "LedgersPerFile" boundary. + if config.EndLedger != 0 { + // Add an extra batch only if "LedgersPerFile" is greater than 1 and the end ledger doesn't fall on the boundary. + if config.ExporterConfig.LedgersPerFile > 1 && config.EndLedger%config.ExporterConfig.LedgersPerFile != 0 { + config.EndLedger = (config.EndLedger/config.ExporterConfig.LedgersPerFile + 1) * config.ExporterConfig.LedgersPerFile + } + } + + logger.Infof("Adjusted ledger range: start=%d, end=%d", config.StartLedger, config.EndLedger) +} + +func (config *Config) GenerateCaptiveCoreConfig() ledgerbackend.CaptiveCoreConfig { + coreConfig := &config.StellarCoreConfig + + // Look for stellar-core binary in $PATH, if not supplied + if coreConfig.StellarCoreBinaryPath == "" { + var err error + coreConfig.StellarCoreBinaryPath, err = exec.LookPath("stellar-core") + logFatalIf(err, "Failed to find stellar-core binary") + } + + var captiveCoreConfig []byte + // Default network config + switch config.Network { + case Pubnet: + coreConfig.NetworkPassphrase = network.PublicNetworkPassphrase + coreConfig.HistoryArchiveUrls = network.PublicNetworkhistoryArchiveURLs + captiveCoreConfig = ledgerbackend.PubnetDefaultConfig + + case Testnet: + coreConfig.NetworkPassphrase = network.TestNetworkPassphrase + coreConfig.HistoryArchiveUrls = network.TestNetworkhistoryArchiveURLs + captiveCoreConfig = ledgerbackend.TestnetDefaultConfig + + default: + logger.Fatalf("Invalid network %s", config.Network) + } + + params := ledgerbackend.CaptiveCoreTomlParams{ + NetworkPassphrase: coreConfig.NetworkPassphrase, + HistoryArchiveURLs: coreConfig.HistoryArchiveUrls, + UseDB: true, + } + + captiveCoreToml, err := ledgerbackend.NewCaptiveCoreTomlFromData(captiveCoreConfig, params) + logFatalIf(err, "Failed to create captive-core toml") + + return ledgerbackend.CaptiveCoreConfig{ + BinaryPath: coreConfig.StellarCoreBinaryPath, + NetworkPassphrase: params.NetworkPassphrase, + HistoryArchiveURLs: params.HistoryArchiveURLs, + CheckpointFrequency: historyarchive.DefaultCheckpointFrequency, + Log: logger.WithField("subservice", "stellar-core"), + Toml: captiveCoreToml, + UserAgent: "ledger-exporter", + UseDB: true, + } +} diff --git a/exp/services/ledgerexporter/internal/config_test.go b/exp/services/ledgerexporter/internal/config_test.go new file mode 100644 index 0000000000..6a320c7424 --- /dev/null +++ b/exp/services/ledgerexporter/internal/config_test.go @@ -0,0 +1,170 @@ +package ledgerexporter + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidateStartAndEndLedger(t *testing.T) { + const latestNetworkLedger = 20000 + + config := &Config{ + ExporterConfig: ExporterConfig{ + LedgersPerFile: 1, + }, + } + tests := []struct { + name string + startLedger uint32 + endLedger uint32 + errMsg string + }{ + { + name: "End ledger same as latest ledger", + startLedger: 512, + endLedger: 512, + errMsg: "", + }, + { + name: "End ledger greater than start ledger", + startLedger: 512, + endLedger: 600, + errMsg: "", + }, + { + name: "No end ledger provided, unbounded mode", + startLedger: 512, + endLedger: 0, + errMsg: "", + }, + { + name: "End ledger before start ledger", + startLedger: 512, + endLedger: 2, + errMsg: "invalid --end value, must be >= --start", + }, + { + name: "End ledger exceeds latest ledger", + startLedger: 512, + endLedger: latestNetworkLedger + 1, + errMsg: fmt.Sprintf("--end %d exceeds latest network ledger %d", + latestNetworkLedger+1, latestNetworkLedger), + }, + { + name: "Start ledger 0", + startLedger: 0, + endLedger: 2, + errMsg: "", + }, + { + name: "Start ledger exceeds latest ledger", + startLedger: latestNetworkLedger + 1, + endLedger: 0, + errMsg: fmt.Sprintf("--start %d exceeds latest network ledger %d", + latestNetworkLedger+1, latestNetworkLedger), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config.StartLedger = tt.startLedger + config.EndLedger = tt.endLedger + if tt.errMsg != "" { + require.Equal(t, tt.errMsg, config.validateAndSetLedgerRange(latestNetworkLedger).Error()) + } else { + require.NoError(t, config.validateAndSetLedgerRange(latestNetworkLedger)) + } + }) + } +} + +func TestAdjustLedgerRangeBoundedMode(t *testing.T) { + tests := []struct { + name string + config *Config + expected *Config + }{ + { + name: "Min start ledger 2", + config: &Config{StartLedger: 0, EndLedger: 10, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, + expected: &Config{StartLedger: 2, EndLedger: 10, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, + }, + { + name: "No change, 1 ledger per file", + config: &Config{StartLedger: 2, EndLedger: 2, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, + expected: &Config{StartLedger: 2, EndLedger: 2, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, + }, + { + name: "Min start ledger2, round up end ledger, 10 ledgers per file", + config: &Config{StartLedger: 0, EndLedger: 1, ExporterConfig: ExporterConfig{LedgersPerFile: 10}}, + expected: &Config{StartLedger: 2, EndLedger: 10, ExporterConfig: ExporterConfig{LedgersPerFile: 10}}, + }, + { + name: "Round down start ledger and round up end ledger, 15 ledgers per file ", + config: &Config{StartLedger: 4, EndLedger: 10, ExporterConfig: ExporterConfig{LedgersPerFile: 15}}, + expected: &Config{StartLedger: 2, EndLedger: 15, ExporterConfig: ExporterConfig{LedgersPerFile: 15}}, + }, + { + name: "Round down start ledger and round up end ledger, 64 ledgers per file ", + config: &Config{StartLedger: 400, EndLedger: 500, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, + expected: &Config{StartLedger: 384, EndLedger: 512, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, + }, + { + name: "No change, 64 ledger per file", + config: &Config{StartLedger: 64, EndLedger: 128, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, + expected: &Config{StartLedger: 64, EndLedger: 128, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.config.adjustLedgerRange() + require.EqualValues(t, tt.expected.StartLedger, tt.config.StartLedger) + require.EqualValues(t, tt.expected.EndLedger, tt.config.EndLedger) + }) + } +} + +func TestAdjustLedgerRangeUnBoundedMode(t *testing.T) { + tests := []struct { + name string + config *Config + expected *Config + }{ + { + name: "Min start ledger 2", + config: &Config{StartLedger: 0, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, + expected: &Config{StartLedger: 2, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, + }, + { + name: "No change, 1 ledger per file", + config: &Config{StartLedger: 2, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, + expected: &Config{StartLedger: 2, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, + }, + { + name: "Round down start ledger, 15 ledgers per file ", + config: &Config{StartLedger: 4, ExporterConfig: ExporterConfig{LedgersPerFile: 15}}, + expected: &Config{StartLedger: 2, ExporterConfig: ExporterConfig{LedgersPerFile: 15}}, + }, + { + name: "Round down start ledger, 64 ledgers per file ", + config: &Config{StartLedger: 400, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, + expected: &Config{StartLedger: 384, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, + }, + { + name: "No change, 64 ledger per file", + config: &Config{StartLedger: 64, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, + expected: &Config{StartLedger: 64, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.config.adjustLedgerRange() + require.EqualValues(t, int(tt.expected.StartLedger), int(tt.config.StartLedger)) + require.EqualValues(t, int(tt.expected.EndLedger), int(tt.config.EndLedger)) + }) + } +} diff --git a/exp/services/ledgerexporter/internal/datastore.go b/exp/services/ledgerexporter/internal/datastore.go new file mode 100644 index 0000000000..0367e9008e --- /dev/null +++ b/exp/services/ledgerexporter/internal/datastore.go @@ -0,0 +1,57 @@ +package ledgerexporter + +import ( + "context" + "io" + "strings" + + "cloud.google.com/go/storage" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/url" + "google.golang.org/api/option" +) + +// DataStore defines an interface for interacting with data storage +type DataStore interface { + GetFile(ctx context.Context, path string) (io.ReadCloser, error) + PutFile(ctx context.Context, path string, in io.WriterTo) error + PutFileIfNotExists(ctx context.Context, path string, in io.WriterTo) error + Exists(ctx context.Context, path string) (bool, error) + Size(ctx context.Context, path string) (int64, error) + Close() error +} + +// NewDataStore creates a new DataStore based on the destination URL. +// Currently, only accepts GCS URLs. +func NewDataStore(ctx context.Context, destinationURL string) (DataStore, error) { + parsed, err := url.Parse(destinationURL) + if err != nil { + return nil, err + } + + pth := parsed.Path + if parsed.Scheme != "gcs" { + return nil, errors.Errorf("Invalid destination URL %s. Expected GCS URL ", destinationURL) + } + + // Inside gcs, all paths start _without_ the leading / + pth = strings.TrimPrefix(pth, "/") + bucketName := parsed.Host + prefix := pth + + logger.Infof("creating GCS client for bucket: %s, prefix: %s", bucketName, prefix) + + var options []option.ClientOption + client, err := storage.NewClient(ctx, options...) + if err != nil { + return nil, err + } + + // Check the bucket exists + bucket := client.Bucket(bucketName) + if _, err := bucket.Attrs(ctx); err != nil { + return nil, errors.Wrap(err, "failed to retrieve bucket attributes") + } + + return &GCSDataStore{client: client, bucket: bucket, prefix: prefix}, nil +} diff --git a/exp/services/ledgerexporter/internal/exportmanager.go b/exp/services/ledgerexporter/internal/exportmanager.go new file mode 100644 index 0000000000..de322aa30c --- /dev/null +++ b/exp/services/ledgerexporter/internal/exportmanager.go @@ -0,0 +1,114 @@ +package ledgerexporter + +import ( + "context" + + "github.com/pkg/errors" + "github.com/stellar/go/ingest/ledgerbackend" + "github.com/stellar/go/xdr" +) + +type ExporterConfig struct { + LedgersPerFile uint32 `toml:"ledgers_per_file"` + FilesPerPartition uint32 `toml:"files_per_partition"` +} + +// ExportManager manages the creation and handling of export objects. +type ExportManager interface { + GetMetaArchiveChannel() chan *LedgerMetaArchive + Run(ctx context.Context, startLedger uint32, endLedger uint32) error + AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta xdr.LedgerCloseMeta) error +} + +type exportManager struct { + config ExporterConfig + ledgerBackend ledgerbackend.LedgerBackend + currentMetaArchive *LedgerMetaArchive + metaArchiveCh chan *LedgerMetaArchive +} + +// NewExportManager creates a new ExportManager with the provided configuration. +func NewExportManager(config ExporterConfig, backend ledgerbackend.LedgerBackend) ExportManager { + return &exportManager{ + config: config, + ledgerBackend: backend, + metaArchiveCh: make(chan *LedgerMetaArchive, 1), + } +} + +// GetMetaArchiveChannel returns a channel that receives LedgerMetaArchive objects. +func (e *exportManager) GetMetaArchiveChannel() chan *LedgerMetaArchive { + return e.metaArchiveCh +} + +// AddLedgerCloseMeta adds ledger metadata to the current export object +func (e *exportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta xdr.LedgerCloseMeta) error { + ledgerSeq := ledgerCloseMeta.LedgerSequence() + + // Determine the object key for the given ledger sequence + objectKey, err := GetObjectKeyFromSequenceNumber(e.config, ledgerSeq) + if err != nil { + return errors.Wrapf(err, "failed to get object key for ledger %d", ledgerSeq) + } + if e.currentMetaArchive != nil && e.currentMetaArchive.GetObjectKey() != objectKey { + return errors.New("Current meta archive object key mismatch") + } + if e.currentMetaArchive == nil { + endSeq := ledgerSeq + e.config.LedgersPerFile - 1 + if ledgerSeq < e.config.LedgersPerFile { + // Special case: Adjust the end ledger sequence for the first batch. + // Since the start ledger is 2 instead of 0, we want to ensure that the end ledger sequence + // does not exceed LedgersPerFile. + // For example, if LedgersPerFile is 64, the file name for the first batch should be 0-63, not 2-66. + endSeq = e.config.LedgersPerFile - 1 + } + + // Create a new LedgerMetaArchive and add it to the map. + e.currentMetaArchive = NewLedgerMetaArchive(objectKey, ledgerSeq, endSeq) + } + + err = e.currentMetaArchive.AddLedger(ledgerCloseMeta) + if err != nil { + return errors.Wrapf(err, "failed to add ledger %d", ledgerSeq) + } + + if ledgerSeq >= e.currentMetaArchive.GetEndLedgerSequence() { + // Current archive is full, send it for upload + select { + case e.metaArchiveCh <- e.currentMetaArchive: + e.currentMetaArchive = nil + case <-ctx.Done(): + return ctx.Err() + } + } + return nil +} + +// Run iterates over the specified range of ledgers, retrieves ledger data +// from the backend, and processes the corresponding ledger close metadata. +// The process continues until the ending ledger number is reached or a cancellation +// signal is received. +func (e *exportManager) Run(ctx context.Context, startLedger, endLedger uint32) error { + + // Close the object channel + defer close(e.metaArchiveCh) + + for nextLedger := startLedger; endLedger < 1 || nextLedger <= endLedger; nextLedger++ { + select { + case <-ctx.Done(): + logger.Info("Stopping ExportManager due to context cancellation") + return ctx.Err() + default: + ledgerCloseMeta, err := e.ledgerBackend.GetLedger(ctx, nextLedger) + if err != nil { + return errors.Wrapf(err, "failed to retrieve ledger %d from the ledger backend", nextLedger) + } + err = e.AddLedgerCloseMeta(ctx, ledgerCloseMeta) + if err != nil { + return errors.Wrapf(err, "failed to add ledgerCloseMeta for ledger %d", nextLedger) + } + } + } + logger.Infof("ExportManager successfully exported ledgers from %d to %d", startLedger, endLedger) + return nil +} diff --git a/exp/services/ledgerexporter/internal/exportmanager_test.go b/exp/services/ledgerexporter/internal/exportmanager_test.go new file mode 100644 index 0000000000..f6f330ec08 --- /dev/null +++ b/exp/services/ledgerexporter/internal/exportmanager_test.go @@ -0,0 +1,156 @@ +package ledgerexporter + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/mock" + + "github.com/stellar/go/ingest/ledgerbackend" + "github.com/stellar/go/support/collections/set" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +func TestExporterSuite(t *testing.T) { + suite.Run(t, new(ExportManagerSuite)) +} + +// ExportManagerSuite is a test suite for the ExportManager. +type ExportManagerSuite struct { + suite.Suite + ctx context.Context + mockBackend ledgerbackend.MockDatabaseBackend +} + +func (s *ExportManagerSuite) SetupTest() { + s.ctx = context.Background() + s.mockBackend = ledgerbackend.MockDatabaseBackend{} +} + +func (s *ExportManagerSuite) TearDownTest() { + s.mockBackend.AssertExpectations(s.T()) +} + +func (s *ExportManagerSuite) TestRun() { + config := ExporterConfig{LedgersPerFile: 64, FilesPerPartition: 10} + exporter := NewExportManager(config, &s.mockBackend) + + start := uint32(0) + end := uint32(255) + expectedKeys := set.NewSet[string](10) + for i := start; i <= end; i++ { + s.mockBackend.On("GetLedger", s.ctx, i). + Return(createLedgerCloseMeta(i), nil) + key, _ := GetObjectKeyFromSequenceNumber(config, i) + expectedKeys.Add(key) + } + + actualKeys := set.NewSet[string](10) + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + for v := range exporter.GetMetaArchiveChannel() { + actualKeys.Add(v.objectKey) + } + }() + + err := exporter.Run(s.ctx, start, end) + require.NoError(s.T(), err) + + wg.Wait() + + require.Equal(s.T(), expectedKeys, actualKeys) +} + +func (s *ExportManagerSuite) TestRunContextCancel() { + config := ExporterConfig{LedgersPerFile: 1, FilesPerPartition: 1} + exporter := NewExportManager(config, &s.mockBackend) + ctx, cancel := context.WithCancel(context.Background()) + + s.mockBackend.On("GetLedger", mock.Anything, mock.Anything). + Return(createLedgerCloseMeta(1), nil) + + go func() { + <-time.After(time.Second * 1) + cancel() + }() + + go func() { + ch := exporter.GetMetaArchiveChannel() + for i := 0; i < 127; i++ { + <-ch + } + }() + + err := exporter.Run(ctx, 0, 255) + require.EqualError(s.T(), err, "failed to add ledgerCloseMeta for ledger 128: context canceled") + +} + +func (s *ExportManagerSuite) TestRunWithCanceledContext() { + config := ExporterConfig{LedgersPerFile: 1, FilesPerPartition: 10} + exporter := NewExportManager(config, &s.mockBackend) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := exporter.Run(ctx, 1, 10) + require.EqualError(s.T(), err, "context canceled") +} + +func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { + config := ExporterConfig{LedgersPerFile: 1, FilesPerPartition: 10} + exporter := NewExportManager(config, &s.mockBackend) + objectCh := exporter.GetMetaArchiveChannel() + expectedkeys := set.NewSet[string](10) + actualKeys := set.NewSet[string](10) + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + for v := range objectCh { + actualKeys.Add(v.objectKey) + } + }() + + start := uint32(0) + end := uint32(255) + for i := start; i <= end; i++ { + require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(i))) + + key, err := GetObjectKeyFromSequenceNumber(config, i) + require.NoError(s.T(), err) + expectedkeys.Add(key) + } + + close(objectCh) + wg.Wait() + require.Equal(s.T(), expectedkeys, actualKeys) +} + +func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { + config := ExporterConfig{LedgersPerFile: 1, FilesPerPartition: 10} + exporter := NewExportManager(config, &s.mockBackend) + + ctx, cancel := context.WithCancel(context.Background()) + go func() { + <-time.After(time.Second * 1) + cancel() + }() + + require.NoError(s.T(), exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(1))) + err := exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(2)) + require.EqualError(s.T(), err, "context canceled") +} + +func (s *ExportManagerSuite) TestAddLedgerCloseMetaKeyMismatch() { + config := ExporterConfig{LedgersPerFile: 10, FilesPerPartition: 1} + exporter := NewExportManager(config, &s.mockBackend) + + require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(16))) + require.EqualError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(21)), + "Current meta archive object key mismatch") +} diff --git a/exp/services/ledgerexporter/internal/gcs_datastore.go b/exp/services/ledgerexporter/internal/gcs_datastore.go new file mode 100644 index 0000000000..4fa1287e94 --- /dev/null +++ b/exp/services/ledgerexporter/internal/gcs_datastore.go @@ -0,0 +1,105 @@ +package ledgerexporter + +import ( + "context" + "io" + "net/http" + "os" + "path" + + "google.golang.org/api/googleapi" + + "cloud.google.com/go/storage" + "github.com/stellar/go/support/errors" +) + +// GCSDataStore implements DataStore for GCS +type GCSDataStore struct { + client *storage.Client + bucket *storage.BucketHandle + prefix string +} + +// GetFile retrieves a file from the GCS bucket. +func (b *GCSDataStore) GetFile(ctx context.Context, filePath string) (io.ReadCloser, error) { + filePath = path.Join(b.prefix, filePath) + r, err := b.bucket.Object(filePath).NewReader(ctx) + if err != nil { + if gcsError, ok := err.(*googleapi.Error); ok { + logger.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) + } + return nil, errors.Wrapf(err, "error retrieving file: %s", filePath) + } + logger.Infof("File retrieved successfully: %s", filePath) + return r, nil +} + +// PutFileIfNotExists uploads a file to GCS only if it doesn't already exist. +func (b *GCSDataStore) PutFileIfNotExists(ctx context.Context, filePath string, in io.WriterTo) error { + err := b.putFile(ctx, filePath, in, &storage.Conditions{DoesNotExist: true}) + if err != nil { + if gcsError, ok := err.(*googleapi.Error); ok { + switch gcsError.Code { + case http.StatusPreconditionFailed: + logger.Infof("Precondition failed: %s already exists in the bucket", filePath) + return nil // Treat as success + default: + logger.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) + } + } + return errors.Wrapf(err, "error uploading file: %s", filePath) + } + logger.Infof("File uploaded successfully: %s", filePath) + return nil +} + +// PutFile uploads a file to GCS +func (b *GCSDataStore) PutFile(ctx context.Context, filePath string, in io.WriterTo) error { + err := b.putFile(ctx, filePath, in, nil) // No conditions for regular PutFile + + if err != nil { + if gcsError, ok := err.(*googleapi.Error); ok { + logger.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) + } + return errors.Wrapf(err, "error uploading file: %v", filePath) + } + logger.Infof("File uploaded successfully: %s", filePath) + return nil +} + +// Size retrieves the size of a file in the GCS bucket. +func (b *GCSDataStore) Size(ctx context.Context, pth string) (int64, error) { + pth = path.Join(b.prefix, pth) + attrs, err := b.bucket.Object(pth).Attrs(ctx) + if err == storage.ErrObjectNotExist { + err = os.ErrNotExist + } + if err != nil { + return 0, err + } + return attrs.Size, nil +} + +// Exists checks if a file exists in the GCS bucket. +func (b *GCSDataStore) Exists(ctx context.Context, pth string) (bool, error) { + _, err := b.Size(ctx, pth) + return err == nil, err +} + +// Close closes the GCS client connection. +func (b *GCSDataStore) Close() error { + return b.client.Close() +} + +func (b *GCSDataStore) putFile(ctx context.Context, filePath string, in io.WriterTo, conditions *storage.Conditions) error { + filePath = path.Join(b.prefix, filePath) + o := b.bucket.Object(filePath) + if conditions != nil { + o = o.If(*conditions) + } + w := o.NewWriter(ctx) + if _, err := in.WriteTo(w); err != nil { + return errors.Wrapf(err, "failed to put file: %s", filePath) + } + return w.Close() +} diff --git a/exp/services/ledgerexporter/internal/ledger_meta_archive.go b/exp/services/ledgerexporter/internal/ledger_meta_archive.go new file mode 100644 index 0000000000..2a193f812c --- /dev/null +++ b/exp/services/ledgerexporter/internal/ledger_meta_archive.go @@ -0,0 +1,65 @@ +package ledgerexporter + +import ( + "fmt" + + "github.com/stellar/go/xdr" +) + +// LedgerMetaArchive represents a file with metadata and binary data. +type LedgerMetaArchive struct { + // file name + objectKey string + // Actual binary data + data xdr.LedgerCloseMetaBatch +} + +// NewLedgerMetaArchive creates a new LedgerMetaArchive instance. +func NewLedgerMetaArchive(key string, startSeq uint32, endSeq uint32) *LedgerMetaArchive { + return &LedgerMetaArchive{ + objectKey: key, + data: xdr.LedgerCloseMetaBatch{ + StartSequence: xdr.Uint32(startSeq), + EndSequence: xdr.Uint32(endSeq), + }, + } +} + +// AddLedger adds a LedgerCloseMeta to the archive. +func (f *LedgerMetaArchive) AddLedger(ledgerCloseMeta xdr.LedgerCloseMeta) error { + if ledgerCloseMeta.LedgerSequence() < uint32(f.data.StartSequence) || + ledgerCloseMeta.LedgerSequence() > uint32(f.data.EndSequence) { + return fmt.Errorf("ledger sequence %d is outside valid range [%d, %d]", + ledgerCloseMeta.LedgerSequence(), f.data.StartSequence, f.data.EndSequence) + } + + if len(f.data.LedgerCloseMetas) > 0 { + lastSequence := f.data.LedgerCloseMetas[len(f.data.LedgerCloseMetas)-1].LedgerSequence() + if ledgerCloseMeta.LedgerSequence() != lastSequence+1 { + return fmt.Errorf("ledgers must be added sequentially: expected sequence %d, got %d", + lastSequence+1, ledgerCloseMeta.LedgerSequence()) + } + } + f.data.LedgerCloseMetas = append(f.data.LedgerCloseMetas, ledgerCloseMeta) + return nil +} + +// GetLedgerCount returns the number of ledgers currently in the archive. +func (f *LedgerMetaArchive) GetLedgerCount() uint32 { + return uint32(len(f.data.LedgerCloseMetas)) +} + +// GetStartLedgerSequence returns the starting ledger sequence of the archive. +func (f *LedgerMetaArchive) GetStartLedgerSequence() uint32 { + return uint32(f.data.StartSequence) +} + +// GetEndLedgerSequence returns the ending ledger sequence of the archive. +func (f *LedgerMetaArchive) GetEndLedgerSequence() uint32 { + return uint32(f.data.EndSequence) +} + +// GetObjectKey returns the object key of the archive. +func (f *LedgerMetaArchive) GetObjectKey() string { + return f.objectKey +} diff --git a/exp/services/ledgerexporter/internal/ledger_meta_archive_test.go b/exp/services/ledgerexporter/internal/ledger_meta_archive_test.go new file mode 100644 index 0000000000..3403cbaafa --- /dev/null +++ b/exp/services/ledgerexporter/internal/ledger_meta_archive_test.go @@ -0,0 +1,84 @@ +package ledgerexporter + +import ( + "fmt" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/require" +) + +func createLedgerCloseMeta(ledgerSeq uint32) xdr.LedgerCloseMeta { + return xdr.LedgerCloseMeta{ + V0: &xdr.LedgerCloseMetaV0{ + LedgerHeader: xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + LedgerSeq: xdr.Uint32(ledgerSeq), + }, + }, + }, + } +} + +func TestLedgerMetaArchive_AddLedgerValidRange(t *testing.T) { + + tests := []struct { + name string + startSeq uint32 + endSeq uint32 + seqNum uint32 + errMsg string + }{ + {startSeq: 10, endSeq: 100, seqNum: 10, errMsg: ""}, + {startSeq: 10, endSeq: 100, seqNum: 11, errMsg: ""}, + {startSeq: 10, endSeq: 100, seqNum: 99, errMsg: ""}, + {startSeq: 10, endSeq: 100, seqNum: 100, errMsg: ""}, + {startSeq: 10, endSeq: 100, seqNum: 9, errMsg: "ledger sequence 9 is outside valid range [10, 100]"}, + {startSeq: 10, endSeq: 100, seqNum: 101, errMsg: "ledger sequence 101 is outside valid range [10, 100]"}, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("range [%d, %d]: Add seq %d", tt.startSeq, tt.endSeq, tt.seqNum), + func(t *testing.T) { + f := NewLedgerMetaArchive("", tt.startSeq, tt.endSeq) + err := f.AddLedger(createLedgerCloseMeta(tt.seqNum)) + if tt.errMsg != "" { + require.EqualError(t, err, tt.errMsg) + } else { + require.NoError(t, err) + } + }) + } +} +func TestLedgerMetaArchive_AddLedgerSequential(t *testing.T) { + var start, end uint32 = 1, 100 + f := NewLedgerMetaArchive("", start, end+100) + + // Add ledgers sequentially + for i := start; i <= end; i++ { + require.NoError(t, f.AddLedger(createLedgerCloseMeta(i))) + } + + // Test out of sequence + testCases := []struct { + ledgerSeq uint32 + expectedErrMsg string + }{ + { + end + 2, + fmt.Sprintf("ledgers must be added sequentially: expected sequence %d, got %d", end+1, end+2), + }, + { + end, + fmt.Sprintf("ledgers must be added sequentially: expected sequence %d, got %d", end+1, end), + }, + { + end - 1, + fmt.Sprintf("ledgers must be added sequentially: expected sequence %d, got %d", end+1, end-1), + }, + } + + for _, tc := range testCases { + err := f.AddLedger(createLedgerCloseMeta(tc.ledgerSeq)) + require.EqualError(t, err, tc.expectedErrMsg) + } +} diff --git a/exp/services/ledgerexporter/internal/mock_datastore.go b/exp/services/ledgerexporter/internal/mock_datastore.go new file mode 100644 index 0000000000..7675a87461 --- /dev/null +++ b/exp/services/ledgerexporter/internal/mock_datastore.go @@ -0,0 +1,43 @@ +package ledgerexporter + +import ( + "context" + "io" + + "github.com/stretchr/testify/mock" +) + +// MockDataStore is a mock implementation for the Storage interface. +type MockDataStore struct { + mock.Mock +} + +func (m *MockDataStore) Exists(ctx context.Context, path string) (bool, error) { + args := m.Called(ctx, path) + return args.Bool(0), args.Error(1) +} + +func (m *MockDataStore) Size(ctx context.Context, path string) (int64, error) { + args := m.Called(ctx, path) + return args.Get(0).(int64), args.Error(1) +} + +func (m *MockDataStore) GetFile(ctx context.Context, path string) (io.ReadCloser, error) { + args := m.Called(ctx, path) + return args.Get(0).(io.ReadCloser), args.Error(1) +} + +func (m *MockDataStore) PutFile(ctx context.Context, path string, in io.WriterTo) error { + args := m.Called(ctx, path, in) + return args.Error(0) +} + +func (m *MockDataStore) PutFileIfNotExists(ctx context.Context, path string, in io.WriterTo) error { + args := m.Called(ctx, path, in) + return args.Error(0) +} + +func (m *MockDataStore) Close() error { + args := m.Called() + return args.Error(0) +} diff --git a/exp/services/ledgerexporter/internal/uploader.go b/exp/services/ledgerexporter/internal/uploader.go new file mode 100644 index 0000000000..633db4ead7 --- /dev/null +++ b/exp/services/ledgerexporter/internal/uploader.go @@ -0,0 +1,74 @@ +package ledgerexporter + +import ( + "context" + "time" + + "github.com/pkg/errors" +) + +// Uploader is responsible for uploading data to a storage destination. +type Uploader interface { + Run(ctx context.Context) error + Upload(ctx context.Context, metaArchive *LedgerMetaArchive) error +} + +type uploader struct { + dataStore DataStore + metaArchiveCh chan *LedgerMetaArchive +} + +func NewUploader(destination DataStore, metaArchiveCh chan *LedgerMetaArchive) Uploader { + return &uploader{ + dataStore: destination, + metaArchiveCh: metaArchiveCh, + } +} + +// Upload uploads the serialized binary data of ledger TxMeta to the specified destination. +// TODO: Add retry logic. +func (u *uploader) Upload(ctx context.Context, metaArchive *LedgerMetaArchive) error { + logger.Infof("Uploading: %s", metaArchive.GetObjectKey()) + + err := u.dataStore.PutFileIfNotExists(ctx, metaArchive.GetObjectKey(), + &XDRGzipEncoder{XdrPayload: &metaArchive.data}) + if err != nil { + return errors.Wrapf(err, "error uploading %s", metaArchive.GetObjectKey()) + } + return nil +} + +// TODO: make it configurable +var uploaderShutdownWaitTime = 10 * time.Second + +// Run starts the uploader, continuously listening for LedgerMetaArchive objects to upload. +func (u *uploader) Run(ctx context.Context) error { + uploadCtx, cancel := context.WithCancel(context.Background()) + go func() { + <-ctx.Done() + logger.Info("Context done, waiting for remaining uploads to complete...") + // wait for a few seconds to upload remaining objects from metaArchiveCh + <-time.After(uploaderShutdownWaitTime) + logger.Info("Timeout reached, canceling remaining uploads...") + cancel() + }() + + for { + select { + case <-uploadCtx.Done(): + return uploadCtx.Err() + + case metaObject, ok := <-u.metaArchiveCh: + if !ok { + logger.Info("Meta archive channel closed, stopping uploader") + return nil + } + //Upload the received LedgerMetaArchive. + err := u.Upload(uploadCtx, metaObject) + if err != nil { + return err + } + logger.Infof("Uploaded %s successfully", metaObject.objectKey) + } + } +} diff --git a/exp/services/ledgerexporter/internal/uploader_test.go b/exp/services/ledgerexporter/internal/uploader_test.go new file mode 100644 index 0000000000..c2a0fb96ab --- /dev/null +++ b/exp/services/ledgerexporter/internal/uploader_test.go @@ -0,0 +1,127 @@ +package ledgerexporter + +import ( + "bytes" + "context" + "fmt" + "io" + "testing" + "time" + + "github.com/stellar/go/support/errors" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +func TestUploaderSuite(t *testing.T) { + suite.Run(t, new(UploaderSuite)) +} + +// UploaderSuite is a test suite for the Uploader. +type UploaderSuite struct { + suite.Suite + ctx context.Context + mockDataStore MockDataStore +} + +func (s *UploaderSuite) SetupTest() { + s.ctx = context.Background() + s.mockDataStore = MockDataStore{} +} + +func (s *UploaderSuite) TestUpload() { + key, start, end := "test-1-100", uint32(1), uint32(100) + archive := NewLedgerMetaArchive(key, start, end) + for i := start; i <= end; i++ { + _ = archive.AddLedger(createLedgerCloseMeta(i)) + } + + var capturedWriterTo io.WriterTo + var capturedKey string + s.mockDataStore.On("PutFileIfNotExists", mock.Anything, key, mock.Anything). + Run(func(args mock.Arguments) { + capturedKey = args.Get(1).(string) + capturedWriterTo = args.Get(2).(io.WriterTo) + }).Return(nil).Once() + + dataUploader := uploader{dataStore: &s.mockDataStore} + require.NoError(s.T(), dataUploader.Upload(context.Background(), archive)) + + var capturedBuf bytes.Buffer + _, err := capturedWriterTo.WriteTo(&capturedBuf) + require.NoError(s.T(), err) + + var decodedArchive LedgerMetaArchive + decoder := &XDRGzipDecoder{XdrPayload: &decodedArchive.data} + _, err = decoder.ReadFrom(&capturedBuf) + require.NoError(s.T(), err) + + // require that the decoded data matches the original test data + require.Equal(s.T(), key, capturedKey) + require.Equal(s.T(), archive.data, decodedArchive.data) +} + +func (s *UploaderSuite) TestUploadPutError() { + key, start, end := "test-1-100", uint32(1), uint32(100) + archive := NewLedgerMetaArchive(key, start, end) + + s.mockDataStore.On("PutFileIfNotExists", context.Background(), key, + mock.Anything).Return(errors.New("error in PutFileIfNotExists")) + + dataUploader := uploader{dataStore: &s.mockDataStore} + err := dataUploader.Upload(context.Background(), archive) + require.Equal(s.T(), fmt.Sprintf("error uploading %s: error in PutFileIfNotExists", key), err.Error()) +} + +func (s *UploaderSuite) TestRunChannelClose() { + s.mockDataStore.On("PutFileIfNotExists", mock.Anything, + mock.Anything, mock.Anything).Return(nil) + + objectCh := make(chan *LedgerMetaArchive, 1) + go func() { + key, start, end := "test", uint32(1), uint32(100) + for i := start; i <= end; i++ { + objectCh <- NewLedgerMetaArchive(key, i, i) + } + <-time.After(time.Second * 2) + close(objectCh) + }() + + dataUploader := uploader{dataStore: &s.mockDataStore, metaArchiveCh: objectCh} + require.NoError(s.T(), dataUploader.Run(context.Background())) +} + +func (s *UploaderSuite) TestRunContextCancel() { + objectCh := make(chan *LedgerMetaArchive, 1) + s.mockDataStore.On("PutFileIfNotExists", mock.Anything, mock.Anything, mock.Anything).Return(nil) + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + for { + objectCh <- NewLedgerMetaArchive("test", 1, 1) + } + }() + + go func() { + <-time.After(time.Second * 2) + cancel() + }() + + dataUploader := uploader{dataStore: &s.mockDataStore, metaArchiveCh: objectCh} + err := dataUploader.Run(ctx) + + require.EqualError(s.T(), err, "context canceled") +} + +func (s *UploaderSuite) TestRunUploadError() { + objectCh := make(chan *LedgerMetaArchive, 10) + objectCh <- NewLedgerMetaArchive("test", 1, 1) + + s.mockDataStore.On("PutFileIfNotExists", mock.Anything, "test", + mock.Anything).Return(errors.New("Put error")) + + dataUploader := uploader{dataStore: &s.mockDataStore, metaArchiveCh: objectCh} + err := dataUploader.Run(context.Background()) + require.Equal(s.T(), "error uploading test: Put error", err.Error()) +} diff --git a/exp/services/ledgerexporter/internal/utils.go b/exp/services/ledgerexporter/internal/utils.go new file mode 100644 index 0000000000..d1bc8e20d1 --- /dev/null +++ b/exp/services/ledgerexporter/internal/utils.go @@ -0,0 +1,104 @@ +package ledgerexporter + +import ( + "compress/gzip" + "fmt" + "io" + + xdr3 "github.com/stellar/go-xdr/xdr3" + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/storage" +) + +const ( + fileSuffix = ".xdr.gz" +) + +// GetObjectKeyFromSequenceNumber generates the file name from the ledger sequence number based on configuration. +func GetObjectKeyFromSequenceNumber(config ExporterConfig, ledgerSeq uint32) (string, error) { + var objectKey string + + if config.LedgersPerFile < 1 { + return "", errors.Errorf("Invalid ledgers per file (%d): must be at least 1", config.LedgersPerFile) + } + + if config.FilesPerPartition > 1 { + partitionSize := config.LedgersPerFile * config.FilesPerPartition + partitionStart := (ledgerSeq / partitionSize) * partitionSize + partitionEnd := partitionStart + partitionSize - 1 + objectKey = fmt.Sprintf("%d-%d/", partitionStart, partitionEnd) + } + + fileStart := (ledgerSeq / config.LedgersPerFile) * config.LedgersPerFile + fileEnd := fileStart + config.LedgersPerFile - 1 + objectKey += fmt.Sprintf("%d", fileStart) + + // Multiple ledgers per file + if fileStart != fileEnd { + objectKey += fmt.Sprintf("-%d", fileEnd) + } + objectKey += fileSuffix + + return objectKey, nil +} + +// getLatestLedgerSequenceFromHistoryArchives returns the most recent ledger sequence (checkpoint ledger) +// number present in the history archives. +func getLatestLedgerSequenceFromHistoryArchives(historyArchivesURLs []string) (uint32, error) { + for _, historyArchiveURL := range historyArchivesURLs { + ha, err := historyarchive.Connect( + historyArchiveURL, + historyarchive.ArchiveOptions{ + ConnectOptions: storage.ConnectOptions{ + UserAgent: "ledger-exporter", + }, + }, + ) + if err != nil { + logger.WithError(err).Warnf("Error connecting to history archive %s", historyArchiveURL) + continue // Skip to next archive + } + + has, err := ha.GetRootHAS() + if err != nil { + logger.WithError(err).Warnf("Error getting RootHAS for %s", historyArchiveURL) + continue // Skip to next archive + } + + return has.CurrentLedger, nil + } + + return 0, errors.New("failed to retrieve the latest ledger sequence from any history archive") +} + +type XDRGzipEncoder struct { + XdrPayload interface{} +} + +func (g *XDRGzipEncoder) WriteTo(w io.Writer) (int64, error) { + gw := gzip.NewWriter(w) + n, err := xdr3.Marshal(gw, g.XdrPayload) + if err != nil { + return int64(n), err + } + return int64(n), gw.Close() +} + +type XDRGzipDecoder struct { + XdrPayload interface{} +} + +func (d *XDRGzipDecoder) ReadFrom(r io.Reader) (int64, error) { + gr, err := gzip.NewReader(r) + if err != nil { + return 0, err + } + defer gr.Close() + + n, err := xdr3.Unmarshal(gr, d.XdrPayload) + if err != nil { + return int64(n), err + } + return int64(n), nil +} diff --git a/exp/services/ledgerexporter/internal/utils_test.go b/exp/services/ledgerexporter/internal/utils_test.go new file mode 100644 index 0000000000..c11b500c21 --- /dev/null +++ b/exp/services/ledgerexporter/internal/utils_test.go @@ -0,0 +1,87 @@ +package ledgerexporter + +import ( + "bytes" + "fmt" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/require" +) + +func TestGetObjectKeyFromSequenceNumber(t *testing.T) { + testCases := []struct { + filesPerPartition uint32 + ledgerSeq uint32 + ledgersPerFile uint32 + expectedKey string + expectedError bool + }{ + {0, 5, 1, "5.xdr.gz", false}, + {0, 5, 10, "0-9.xdr.gz", false}, + {2, 5, 0, "", true}, + {2, 10, 100, "0-199/0-99.xdr.gz", false}, + {2, 150, 50, "100-199/150-199.xdr.gz", false}, + {2, 300, 200, "0-399/200-399.xdr.gz", false}, + {2, 1, 1, "0-1/1.xdr.gz", false}, + {4, 10, 100, "0-399/0-99.xdr.gz", false}, + {4, 250, 50, "200-399/250-299.xdr.gz", false}, + {1, 300, 200, "200-399.xdr.gz", false}, + {1, 1, 1, "1.xdr.gz", false}, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("LedgerSeq-%d-LedgersPerFile-%d", tc.ledgerSeq, tc.ledgersPerFile), func(t *testing.T) { + config := ExporterConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile} + key, err := GetObjectKeyFromSequenceNumber(config, tc.ledgerSeq) + + if tc.expectedError { + require.Error(t, err) + require.Empty(t, key) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedKey, key) + } + }) + } +} + +func createTestLedgerCloseMetaBatch(startSeq, endSeq uint32, count int) xdr.LedgerCloseMetaBatch { + var ledgerCloseMetas []xdr.LedgerCloseMeta + for i := 0; i < count; i++ { + ledgerCloseMetas = append(ledgerCloseMetas, createLedgerCloseMeta(startSeq+uint32(i))) + } + return xdr.LedgerCloseMetaBatch{ + StartSequence: xdr.Uint32(startSeq), + EndSequence: xdr.Uint32(endSeq), + LedgerCloseMetas: ledgerCloseMetas, + } +} + +func TestEncodeDecodeLedgerCloseMetaBatch(t *testing.T) { + testData := createTestLedgerCloseMetaBatch(1000, 1005, 6) + + // Encode the test data + var encoder XDRGzipEncoder + encoder.XdrPayload = testData + + var buf bytes.Buffer + _, err := encoder.WriteTo(&buf) + require.NoError(t, err) + + // Decode the encoded data + var decoder XDRGzipDecoder + decoder.XdrPayload = &xdr.LedgerCloseMetaBatch{} + + _, err = decoder.ReadFrom(&buf) + require.NoError(t, err) + + // Check if the decoded data matches the original test data + decodedData := decoder.XdrPayload.(*xdr.LedgerCloseMetaBatch) + require.Equal(t, testData.StartSequence, decodedData.StartSequence) + require.Equal(t, testData.EndSequence, decodedData.EndSequence) + require.Equal(t, len(testData.LedgerCloseMetas), len(decodedData.LedgerCloseMetas)) + for i := range testData.LedgerCloseMetas { + require.Equal(t, testData.LedgerCloseMetas[i], decodedData.LedgerCloseMetas[i]) + } +} diff --git a/exp/services/ledgerexporter/main.go b/exp/services/ledgerexporter/main.go index 42cf1d6ae8..f1a81e95ba 100644 --- a/exp/services/ledgerexporter/main.go +++ b/exp/services/ledgerexporter/main.go @@ -1,180 +1,8 @@ package main -import ( - "bytes" - "context" - "flag" - "io" - "os" - "strconv" - "strings" - "time" - - "github.com/aws/aws-sdk-go/service/s3" - "github.com/stellar/go/ingest/ledgerbackend" - "github.com/stellar/go/network" - supportlog "github.com/stellar/go/support/log" - "github.com/stellar/go/support/storage" - "github.com/stellar/go/xdr" -) - -var logger = supportlog.New() +import exporter "github.com/stellar/go/exp/services/ledgerexporter/internal" func main() { - targetUrl := flag.String("target", "gcs://horizon-archive-poc", "history archive url to write txmeta files") - stellarCoreBinaryPath := flag.String("stellar-core-binary-path", os.Getenv("STELLAR_CORE_BINARY_PATH"), "path to the stellar core binary") - networkPassphrase := flag.String("network-passphrase", network.TestNetworkPassphrase, "network passphrase") - historyArchiveUrls := flag.String("history-archive-urls", "https://history.stellar.org/prd/core-testnet/core_testnet_001", "comma-separated list of history archive urls to read from") - captiveCoreTomlPath := flag.String("captive-core-toml-path", os.Getenv("CAPTIVE_CORE_TOML_PATH"), "path to load captive core toml file from") - startingLedger := flag.Uint("start-ledger", 2, "ledger to start export from") - continueFromLatestLedger := flag.Bool("continue", false, "start export from the last exported ledger (as indicated in the target's /latest path)") - endingLedger := flag.Uint("end-ledger", 0, "ledger at which to stop the export (must be a closed ledger), 0 means no ending") - writeLatestPath := flag.Bool("write-latest-path", true, "update the value of the /latest path on the target") - captiveCoreUseDb := flag.Bool("captive-core-use-db", true, "configure captive core to store database on disk in working directory rather than in memory") - flag.Parse() - - logger.SetLevel(supportlog.InfoLevel) - - params := ledgerbackend.CaptiveCoreTomlParams{ - NetworkPassphrase: *networkPassphrase, - HistoryArchiveURLs: strings.Split(*historyArchiveUrls, ","), - UseDB: *captiveCoreUseDb, - } - if *captiveCoreTomlPath == "" { - logger.Fatal("Missing -captive-core-toml-path flag") - } - - captiveCoreToml, err := ledgerbackend.NewCaptiveCoreTomlFromFile(*captiveCoreTomlPath, params) - logFatalIf(err, "Invalid captive core toml") - - captiveConfig := ledgerbackend.CaptiveCoreConfig{ - BinaryPath: *stellarCoreBinaryPath, - NetworkPassphrase: params.NetworkPassphrase, - HistoryArchiveURLs: params.HistoryArchiveURLs, - CheckpointFrequency: 64, - Log: logger.WithField("subservice", "stellar-core"), - Toml: captiveCoreToml, - UseDB: *captiveCoreUseDb, - } - core, err := ledgerbackend.NewCaptive(captiveConfig) - logFatalIf(err, "Could not create captive core instance") - - target, err := storage.ConnectBackend( - *targetUrl, - storage.ConnectOptions{ - Context: context.Background(), - S3WriteACL: s3.ObjectCannedACLBucketOwnerFullControl, - }, - ) - logFatalIf(err, "Could not connect to target") - defer target.Close() - - // Build the appropriate range for the given backend state. - startLedger := uint32(*startingLedger) - endLedger := uint32(*endingLedger) - - logger.Infof("processing requested range of -start-ledger=%v, -end-ledger=%v", startLedger, endLedger) - if *continueFromLatestLedger { - if startLedger != 0 { - logger.Fatalf("-start-ledger and -continue cannot both be set") - } - startLedger = readLatestLedger(target) - logger.Infof("continue flag was enabled, next ledger found was %v", startLedger) - } - - if startLedger < 2 { - logger.Fatalf("-start-ledger must be >= 2") - } - if endLedger != 0 && endLedger < startLedger { - logger.Fatalf("-end-ledger must be >= -start-ledger") - } - - var ledgerRange ledgerbackend.Range - if endLedger == 0 { - ledgerRange = ledgerbackend.UnboundedRange(startLedger) - } else { - ledgerRange = ledgerbackend.BoundedRange(startLedger, endLedger) - } - - logger.Infof("preparing to export %s", ledgerRange) - err = core.PrepareRange(context.Background(), ledgerRange) - logFatalIf(err, "could not prepare range") - - for nextLedger := startLedger; endLedger < 1 || nextLedger <= endLedger; { - ledger, err := core.GetLedger(context.Background(), nextLedger) - if err != nil { - logger.WithError(err).Warnf("could not fetch ledger %v, retrying", nextLedger) - time.Sleep(time.Second) - continue - } - - if err = writeLedger(target, ledger); err != nil { - logger.WithError(err).Warnf( - "could not write ledger object %v, retrying", - uint64(ledger.LedgerSequence())) - continue - } - - if *writeLatestPath { - if err = writeLatestLedger(target, nextLedger); err != nil { - logger.WithError(err).Warnf("could not write latest ledger %v", nextLedger) - } - } - - nextLedger++ - } - -} - -// readLatestLedger determines the latest ledger in the given backend (at the -// /latest path), defaulting to Ledger #2 if one doesn't exist -func readLatestLedger(backend storage.Storage) uint32 { - r, err := backend.GetFile("latest") - if os.IsNotExist(err) { - return 2 - } - - logFatalIf(err, "could not open latest ledger bucket") - defer r.Close() - - var buf bytes.Buffer - _, err = io.Copy(&buf, r) - logFatalIf(err, "could not read latest ledger") - - parsed, err := strconv.ParseUint(buf.String(), 10, 32) - logFatalIf(err, "could not parse latest ledger: %s", buf.String()) - return uint32(parsed) -} - -// writeLedger stores the given LedgerCloseMeta instance as a raw binary at the -// /ledgers/ path. If an error is returned, it may be transient so you -// should attempt to retry. -func writeLedger(backend storage.Storage, ledger xdr.LedgerCloseMeta) error { - toSerialize := xdr.SerializedLedgerCloseMeta{ - V: 0, - V0: &ledger, - } - blob, err := toSerialize.MarshalBinary() - logFatalIf(err, "could not serialize ledger %v", ledger.LedgerSequence()) - return backend.PutFile( - "ledgers/"+strconv.FormatUint(uint64(ledger.LedgerSequence()), 10), - io.NopCloser(bytes.NewReader(blob)), - ) -} - -func writeLatestLedger(backend storage.Storage, ledger uint32) error { - return backend.PutFile( - "latest", - io.NopCloser( - bytes.NewBufferString( - strconv.FormatUint(uint64(ledger), 10), - ), - ), - ) -} - -func logFatalIf(err error, message string, args ...interface{}) { - if err != nil { - logger.WithError(err).Fatalf(message, args...) - } + app := exporter.NewApp() + app.Run() } diff --git a/go.mod b/go.mod index 1ca1ea801c..0e55add07c 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( cloud.google.com/go/firestore v1.14.0 // indirect - cloud.google.com/go/storage v1.30.1 + cloud.google.com/go/storage v1.37.0 firebase.google.com/go v3.12.0+incompatible github.com/2opremio/pretty v0.2.2-0.20230601220618-e1d5758b2a95 github.com/BurntSushi/toml v1.3.2 @@ -20,7 +20,7 @@ require ( github.com/go-chi/chi v4.1.2+incompatible github.com/go-errors/errors v1.5.1 github.com/golang-jwt/jwt v3.2.2+incompatible - github.com/google/uuid v1.4.0 + github.com/google/uuid v1.5.0 github.com/gorilla/schema v1.2.0 github.com/graph-gophers/graphql-go v1.3.0 github.com/guregu/null v4.0.0+incompatible @@ -50,13 +50,13 @@ require ( github.com/stretchr/testify v1.8.4 github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8 github.com/xdrpp/goxdr v0.1.1 - google.golang.org/api v0.149.0 + google.golang.org/api v0.157.0 gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0 gopkg.in/square/go-jose.v2 v2.4.1 gopkg.in/tylerb/graceful.v1 v1.2.15 ) -require golang.org/x/sync v0.4.0 +require golang.org/x/sync v0.6.0 require ( cloud.google.com/go/compute v1.23.3 // indirect @@ -67,9 +67,10 @@ require ( github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/creachadair/mds v0.0.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gobuffalo/packd v1.0.2 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect @@ -84,21 +85,23 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.opentelemetry.io/otel v1.19.0 // indirect - go.opentelemetry.io/otel/metric v1.19.0 // indirect - go.opentelemetry.io/otel/trace v1.19.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect + go.opentelemetry.io/otel v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.13.0 // indirect golang.org/x/tools v0.14.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240122161410-6c6643bf1457 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect gopkg.in/djherbis/atime.v1 v1.0.0 // indirect gopkg.in/djherbis/stream.v1 v1.3.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) require ( - cloud.google.com/go v0.111.0 // indirect + cloud.google.com/go v0.112.0 // indirect github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/goreplay v1.3.2 @@ -139,16 +142,16 @@ require ( github.com/yudai/golcs v0.0.0-20150405163532-d1c525dea8ce // indirect github.com/yudai/pp v2.0.1+incompatible // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.16.0 // indirect + golang.org/x/crypto v0.18.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d - golang.org/x/net v0.19.0 // indirect - golang.org/x/oauth2 v0.13.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/term v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.3.0 + golang.org/x/time v0.5.0 google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 // indirect + google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect google.golang.org/grpc v1.60.1 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect diff --git a/go.sum b/go.sum index 6e4158f9b7..1e31cfdb08 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,8 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM= -cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU= +cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= +cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -47,8 +47,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= -cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +cloud.google.com/go/storage v1.37.0 h1:WI8CsaFO8Q9KjPVtsZ5Cmi0dXV25zMoX0FklT7c3Jm4= +cloud.google.com/go/storage v1.37.0/go.mod h1:i34TiT2IhiNDmcj65PqwCjcoUX7Z5pLzS8DEmoiFq1k= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= firebase.google.com/go v3.12.0+incompatible h1:q70KCp/J0oOL8kJ8oV2j3646kV4TB8Y5IvxXC0WT1bo= firebase.google.com/go v3.12.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs= @@ -94,6 +94,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/jrpc2 v1.1.0 h1:SgpJf0v1rVCZx68+4APv6dgsTFsIHlpgFD1NlQAWA0A= github.com/creachadair/jrpc2 v1.1.0/go.mod h1:5jN7MKwsm8qvgfTsTzLX3JIfidsAkZ1c8DZSQmp+g38= @@ -117,8 +118,11 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU= github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= @@ -141,8 +145,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -227,8 +231,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -455,13 +459,17 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= -go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= -go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= -go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= -go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= -go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= -go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -474,8 +482,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -552,8 +560,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -563,8 +571,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -576,8 +584,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -632,8 +640,8 @@ golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -649,8 +657,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -707,7 +715,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -727,8 +735,8 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY= -google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= +google.golang.org/api v0.157.0 h1:ORAeqmbrrozeyw5NjnMxh7peHO0UzV4wWYSwZeCUb20= +google.golang.org/api v0.157.0/go.mod h1:+z4v4ufbZ1WEpld6yMGHyggs+PmAHiaLNj5ytP3N01g= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -774,12 +782,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos= -google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY= -google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 h1:EWIeHfGuUf00zrVZGEgYFxok7plSAXBGcH7NNdMAWvA= -google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg= +google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= +google.golang.org/genproto/googleapis/api v0.0.0-20240122161410-6c6643bf1457 h1:KHBtwE+eQc3+NxpjmRFlQ3pJQ2FNnhhgB9xOV8kyBuU= +google.golang.org/genproto/googleapis/api v0.0.0-20240122161410-6c6643bf1457/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/gxdr/xdr_generated.go b/gxdr/xdr_generated.go index 9d9ab290bb..d4cd83cdf0 100644 --- a/gxdr/xdr_generated.go +++ b/gxdr/xdr_generated.go @@ -1,4 +1,4 @@ -// Code generated by goxdr -p gxdr -enum-comments -o gxdr/xdr_generated.go xdr/Stellar-SCP.x xdr/Stellar-ledger-entries.x xdr/Stellar-ledger.x xdr/Stellar-overlay.x xdr/Stellar-transaction.x xdr/Stellar-types.x xdr/Stellar-contract-env-meta.x xdr/Stellar-contract-meta.x xdr/Stellar-contract-spec.x xdr/Stellar-contract.x xdr/Stellar-internal.x xdr/Stellar-contract-config-setting.x xdr/Stellar-lighthorizon.x; DO NOT EDIT. +// Code generated by goxdr -p gxdr -enum-comments -o gxdr/xdr_generated.go xdr/Stellar-SCP.x xdr/Stellar-ledger-entries.x xdr/Stellar-ledger.x xdr/Stellar-overlay.x xdr/Stellar-transaction.x xdr/Stellar-types.x xdr/Stellar-contract-env-meta.x xdr/Stellar-contract-meta.x xdr/Stellar-contract-spec.x xdr/Stellar-contract.x xdr/Stellar-internal.x xdr/Stellar-contract-config-setting.x xdr/Stellar-lighthorizon.x xdr/Stellar-exporter.x; DO NOT EDIT. package gxdr @@ -4445,6 +4445,16 @@ type SerializedLedgerCloseMeta struct { _u interface{} } +// Batch of ledgers along with their transaction metadata +type LedgerCloseMetaBatch struct { + // starting ledger sequence number in the batch + StartSequence Uint32 + // ending ledger sequence number in the batch + EndSequence Uint32 + // Ledger close meta for each ledger within the batch + LedgerCloseMetas []LedgerCloseMeta +} + // // Helper types and generated marshaling functions // @@ -29398,3 +29408,76 @@ func (u *SerializedLedgerCloseMeta) XdrRecurse(x XDR, name string) { XdrPanic("invalid V (%v) in SerializedLedgerCloseMeta", u.V) } func XDR_SerializedLedgerCloseMeta(v *SerializedLedgerCloseMeta) *SerializedLedgerCloseMeta { return v } + +type _XdrVec_unbounded_LedgerCloseMeta []LedgerCloseMeta + +func (_XdrVec_unbounded_LedgerCloseMeta) XdrBound() uint32 { + const bound uint32 = 4294967295 // Force error if not const or doesn't fit + return bound +} +func (_XdrVec_unbounded_LedgerCloseMeta) XdrCheckLen(length uint32) { + if length > uint32(4294967295) { + XdrPanic("_XdrVec_unbounded_LedgerCloseMeta length %d exceeds bound 4294967295", length) + } else if int(length) < 0 { + XdrPanic("_XdrVec_unbounded_LedgerCloseMeta length %d exceeds max int", length) + } +} +func (v _XdrVec_unbounded_LedgerCloseMeta) GetVecLen() uint32 { return uint32(len(v)) } +func (v *_XdrVec_unbounded_LedgerCloseMeta) SetVecLen(length uint32) { + v.XdrCheckLen(length) + if int(length) <= cap(*v) { + if int(length) != len(*v) { + *v = (*v)[:int(length)] + } + return + } + newcap := 2 * cap(*v) + if newcap < int(length) { // also catches overflow where 2*cap < 0 + newcap = int(length) + } else if bound := uint(4294967295); uint(newcap) > bound { + if int(bound) < 0 { + bound = ^uint(0) >> 1 + } + newcap = int(bound) + } + nv := make([]LedgerCloseMeta, int(length), newcap) + copy(nv, *v) + *v = nv +} +func (v *_XdrVec_unbounded_LedgerCloseMeta) XdrMarshalN(x XDR, name string, n uint32) { + v.XdrCheckLen(n) + for i := 0; i < int(n); i++ { + if i >= len(*v) { + v.SetVecLen(uint32(i + 1)) + } + XDR_LedgerCloseMeta(&(*v)[i]).XdrMarshal(x, x.Sprintf("%s[%d]", name, i)) + } + if int(n) < len(*v) { + *v = (*v)[:int(n)] + } +} +func (v *_XdrVec_unbounded_LedgerCloseMeta) XdrRecurse(x XDR, name string) { + size := XdrSize{Size: uint32(len(*v)), Bound: 4294967295} + x.Marshal(name, &size) + v.XdrMarshalN(x, name, size.Size) +} +func (_XdrVec_unbounded_LedgerCloseMeta) XdrTypeName() string { return "LedgerCloseMeta<>" } +func (v *_XdrVec_unbounded_LedgerCloseMeta) XdrPointer() interface{} { return (*[]LedgerCloseMeta)(v) } +func (v _XdrVec_unbounded_LedgerCloseMeta) XdrValue() interface{} { return ([]LedgerCloseMeta)(v) } +func (v *_XdrVec_unbounded_LedgerCloseMeta) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } + +type XdrType_LedgerCloseMetaBatch = *LedgerCloseMetaBatch + +func (v *LedgerCloseMetaBatch) XdrPointer() interface{} { return v } +func (LedgerCloseMetaBatch) XdrTypeName() string { return "LedgerCloseMetaBatch" } +func (v LedgerCloseMetaBatch) XdrValue() interface{} { return v } +func (v *LedgerCloseMetaBatch) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *LedgerCloseMetaBatch) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%sstartSequence", name), XDR_Uint32(&v.StartSequence)) + x.Marshal(x.Sprintf("%sendSequence", name), XDR_Uint32(&v.EndSequence)) + x.Marshal(x.Sprintf("%sledgerCloseMetas", name), (*_XdrVec_unbounded_LedgerCloseMeta)(&v.LedgerCloseMetas)) +} +func XDR_LedgerCloseMetaBatch(v *LedgerCloseMetaBatch) *LedgerCloseMetaBatch { return v } diff --git a/ingest/ledgerbackend/configs/captive-core-pubnet.cfg b/ingest/ledgerbackend/configs/captive-core-pubnet.cfg new file mode 100644 index 0000000000..f8b9a33985 --- /dev/null +++ b/ingest/ledgerbackend/configs/captive-core-pubnet.cfg @@ -0,0 +1,195 @@ +# WARNING! Do not use this config in production. Quorum sets should +# be carefully selected manually. +NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" +FAILURE_SAFETY=1 +HTTP_PORT=11626 +PEER_PORT=11725 + +[[HOME_DOMAINS]] +HOME_DOMAIN="stellar.org" +QUALITY="HIGH" + +[[HOME_DOMAINS]] +HOME_DOMAIN="satoshipay.io" +QUALITY="HIGH" + +[[HOME_DOMAINS]] +HOME_DOMAIN="lobstr.co" +QUALITY="HIGH" + +[[HOME_DOMAINS]] +HOME_DOMAIN="www.coinqvest.com" +QUALITY="HIGH" + +[[HOME_DOMAINS]] +HOME_DOMAIN="publicnode.org" +QUALITY="HIGH" + +[[HOME_DOMAINS]] +HOME_DOMAIN="stellar.blockdaemon.com" +QUALITY="HIGH" + +[[HOME_DOMAINS]] +HOME_DOMAIN = "www.franklintempleton.com" +QUALITY = "HIGH" + +[[VALIDATORS]] +NAME="sdf_1" +HOME_DOMAIN="stellar.org" +PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" +ADDRESS="core-live-a.stellar.org:11625" +HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" + +[[VALIDATORS]] +NAME="sdf_2" +HOME_DOMAIN="stellar.org" +PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" +ADDRESS="core-live-b.stellar.org:11625" +HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" + +[[VALIDATORS]] +NAME="sdf_3" +HOME_DOMAIN="stellar.org" +PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" +ADDRESS="core-live-c.stellar.org:11625" +HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" + +[[VALIDATORS]] +NAME="satoshipay_singapore" +HOME_DOMAIN="satoshipay.io" +PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" +ADDRESS="stellar-sg-sin.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" + +[[VALIDATORS]] +NAME="satoshipay_iowa" +HOME_DOMAIN="satoshipay.io" +PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" +ADDRESS="stellar-us-iowa.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" + +[[VALIDATORS]] +NAME="satoshipay_frankfurt" +HOME_DOMAIN="satoshipay.io" +PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" +ADDRESS="stellar-de-fra.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" + +[[VALIDATORS]] +NAME="lobstr_1_europe" +HOME_DOMAIN="lobstr.co" +PUBLIC_KEY="GCFONE23AB7Y6C5YZOMKUKGETPIAJA4QOYLS5VNS4JHBGKRZCPYHDLW7" +ADDRESS="v1.stellar.lobstr.co:11625" +HISTORY="curl -sf https://stellar-archive-1-lobstr.s3.amazonaws.com/{0} -o {1}" + +[[VALIDATORS]] +NAME="lobstr_2_europe" +HOME_DOMAIN="lobstr.co" +PUBLIC_KEY="GDXQB3OMMQ6MGG43PWFBZWBFKBBDUZIVSUDAZZTRAWQZKES2CDSE5HKJ" +ADDRESS="v2.stellar.lobstr.co:11625" +HISTORY="curl -sf https://stellar-archive-2-lobstr.s3.amazonaws.com/{0} -o {1}" + +[[VALIDATORS]] +NAME="lobstr_3_north_america" +HOME_DOMAIN="lobstr.co" +PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" +ADDRESS="v3.stellar.lobstr.co:11625" +HISTORY="curl -sf https://stellar-archive-3-lobstr.s3.amazonaws.com/{0} -o {1}" + +[[VALIDATORS]] +NAME="lobstr_4_asia" +HOME_DOMAIN="lobstr.co" +PUBLIC_KEY="GA7TEPCBDQKI7JQLQ34ZURRMK44DVYCIGVXQQWNSWAEQR6KB4FMCBT7J" +ADDRESS="v4.stellar.lobstr.co:11625" +HISTORY="curl -sf https://stellar-archive-4-lobstr.s3.amazonaws.com/{0} -o {1}" + +[[VALIDATORS]] +NAME="lobstr_5_australia" +HOME_DOMAIN="lobstr.co" +PUBLIC_KEY="GA5STBMV6QDXFDGD62MEHLLHZTPDI77U3PFOD2SELU5RJDHQWBR5NNK7" +ADDRESS="v5.stellar.lobstr.co:11625" +HISTORY="curl -sf https://stellar-archive-5-lobstr.s3.amazonaws.com/{0} -o {1}" + +[[VALIDATORS]] +NAME="coinqvest_hong_kong" +HOME_DOMAIN="www.coinqvest.com" +PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" +ADDRESS="hongkong.stellar.coinqvest.com:11625" +HISTORY="curl -sf https://hongkong.stellar.coinqvest.com/history/{0} -o {1}" + +[[VALIDATORS]] +NAME="coinqvest_germany" +HOME_DOMAIN="www.coinqvest.com" +PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" +ADDRESS="germany.stellar.coinqvest.com:11625" +HISTORY="curl -sf https://germany.stellar.coinqvest.com/history/{0} -o {1}" + +[[VALIDATORS]] +NAME="coinqvest_finland" +HOME_DOMAIN="www.coinqvest.com" +PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" +ADDRESS="finland.stellar.coinqvest.com:11625" +HISTORY="curl -sf https://finland.stellar.coinqvest.com/history/{0} -o {1}" + +[[VALIDATORS]] +NAME="bootes" +HOME_DOMAIN="publicnode.org" +PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" +ADDRESS="bootes.publicnode.org" +HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" + +[[VALIDATORS]] +NAME="hercules" +HOME_DOMAIN="publicnode.org" +PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" +ADDRESS="hercules.publicnode.org" +HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" + +[[VALIDATORS]] +NAME="lyra" +HOME_DOMAIN="publicnode.org" +PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" +ADDRESS="lyra.publicnode.org" +HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" + +[[VALIDATORS]] +NAME="Blockdaemon_Validator_1" +HOME_DOMAIN="stellar.blockdaemon.com" +PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" +ADDRESS="stellar-full-validator1.bdnodes.net" +HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" + +[[VALIDATORS]] +NAME="Blockdaemon_Validator_2" +HOME_DOMAIN="stellar.blockdaemon.com" +PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" +ADDRESS="stellar-full-validator2.bdnodes.net" +HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" + +[[VALIDATORS]] +NAME="Blockdaemon_Validator_3" +HOME_DOMAIN="stellar.blockdaemon.com" +PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" +ADDRESS="stellar-full-validator3.bdnodes.net" +HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" + +[[VALIDATORS]] +NAME = "FT_SCV_1" +HOME_DOMAIN = "www.franklintempleton.com" +PUBLIC_KEY = "GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" +ADDRESS = "stellar1.franklintempleton.com:11625" +HISTORY = "curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" + +[[VALIDATORS]] +NAME = "FT_SCV_2" +HOME_DOMAIN = "www.franklintempleton.com" +PUBLIC_KEY = "GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" +ADDRESS = "stellar2.franklintempleton.com:11625" +HISTORY = "curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" + +[[VALIDATORS]] +NAME = "FT_SCV_3" +HOME_DOMAIN = "www.franklintempleton.com" +PUBLIC_KEY = "GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" +ADDRESS = "stellar3.franklintempleton.com:11625" +HISTORY = "curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" diff --git a/ingest/ledgerbackend/configs/captive-core-testnet.cfg b/ingest/ledgerbackend/configs/captive-core-testnet.cfg new file mode 100644 index 0000000000..9abeecc8f5 --- /dev/null +++ b/ingest/ledgerbackend/configs/captive-core-testnet.cfg @@ -0,0 +1,28 @@ +NETWORK_PASSPHRASE="Test SDF Network ; September 2015" +UNSAFE_QUORUM=true +FAILURE_SAFETY=1 + +[[HOME_DOMAINS]] +HOME_DOMAIN="testnet.stellar.org" +QUALITY="HIGH" + +[[VALIDATORS]] +NAME="sdf_testnet_1" +HOME_DOMAIN="testnet.stellar.org" +PUBLIC_KEY="GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" +ADDRESS="core-testnet1.stellar.org" +HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_001/{0} -o {1}" + +[[VALIDATORS]] +NAME="sdf_testnet_2" +HOME_DOMAIN="testnet.stellar.org" +PUBLIC_KEY="GCUCJTIYXSOXKBSNFGNFWW5MUQ54HKRPGJUTQFJ5RQXZXNOLNXYDHRAP" +ADDRESS="core-testnet2.stellar.org" +HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_002/{0} -o {1}" + +[[VALIDATORS]] +NAME="sdf_testnet_3" +HOME_DOMAIN="testnet.stellar.org" +PUBLIC_KEY="GC2V2EFSXN6SQTWVYA5EPJPBWWIMSD2XQNKUOHGEKB535AQE2I6IXV2Z" +ADDRESS="core-testnet3.stellar.org" +HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_003/{0} -o {1}" \ No newline at end of file diff --git a/ingest/ledgerbackend/toml.go b/ingest/ledgerbackend/toml.go index e2234fc1f2..bc3ab2247a 100644 --- a/ingest/ledgerbackend/toml.go +++ b/ingest/ledgerbackend/toml.go @@ -2,6 +2,7 @@ package ledgerbackend import ( "bytes" + _ "embed" "fmt" "os" "os/exec" @@ -16,6 +17,14 @@ import ( "github.com/pelletier/go-toml" ) +var ( + //go:embed configs/captive-core-pubnet.cfg + PubnetDefaultConfig []byte + + //go:embed configs/captive-core-testnet.cfg + TestnetDefaultConfig []byte +) + const ( defaultHTTPPort = 11626 defaultFailureSafety = -1 diff --git a/xdr/Stellar-exporter.x b/xdr/Stellar-exporter.x new file mode 100644 index 0000000000..4ac92654b1 --- /dev/null +++ b/xdr/Stellar-exporter.x @@ -0,0 +1,23 @@ +// Copyright 2024 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +%#include "xdr/Stellar-ledger.h" + +namespace stellar +{ + +// Batch of ledgers along with their transaction metadata +struct LedgerCloseMetaBatch +{ + // starting ledger sequence number in the batch + uint32 startSequence; + + // ending ledger sequence number in the batch + uint32 endSequence; + + // Ledger close meta for each ledger within the batch + LedgerCloseMeta ledgerCloseMetas<>; +}; + +} diff --git a/xdr/xdr_generated.go b/xdr/xdr_generated.go index ac19618f61..ad832d79b4 100644 --- a/xdr/xdr_generated.go +++ b/xdr/xdr_generated.go @@ -9,6 +9,7 @@ // xdr/Stellar-contract-meta.x // xdr/Stellar-contract-spec.x // xdr/Stellar-contract.x +// xdr/Stellar-exporter.x // xdr/Stellar-internal.x // xdr/Stellar-ledger-entries.x // xdr/Stellar-ledger.x @@ -38,6 +39,7 @@ var XdrFilesSHA256 = map[string]string{ "xdr/Stellar-contract-meta.x": "f01532c11ca044e19d9f9f16fe373e9af64835da473be556b9a807ee3319ae0d", "xdr/Stellar-contract-spec.x": "c7ffa21d2e91afb8e666b33524d307955426ff553a486d670c29217ed9888d49", "xdr/Stellar-contract.x": "7f665e4103e146a88fcdabce879aaaacd3bf9283feb194cc47ff986264c1e315", + "xdr/Stellar-exporter.x": "a00c83d02e8c8382e06f79a191f1fb5abd097a4bbcab8481c67467e3270e0529", "xdr/Stellar-internal.x": "227835866c1b2122d1eaf28839ba85ea7289d1cb681dda4ca619c2da3d71fe00", "xdr/Stellar-ledger-entries.x": "4f8f2324f567a40065f54f696ea1428740f043ea4154f5986d9f499ad00ac333", "xdr/Stellar-ledger.x": "2c842f3fe6e269498af5467f849cf6818554e90babc845f34c87cda471298d0f", @@ -57036,4 +57038,114 @@ func (s SerializedLedgerCloseMeta) xdrType() {} var _ xdrType = (*SerializedLedgerCloseMeta)(nil) +// LedgerCloseMetaBatch is an XDR Struct defines as: +// +// struct LedgerCloseMetaBatch +// { +// // starting ledger sequence number in the batch +// uint32 startSequence; +// +// // ending ledger sequence number in the batch +// uint32 endSequence; +// +// // Ledger close meta for each ledger within the batch +// LedgerCloseMeta ledgerCloseMetas<>; +// }; +type LedgerCloseMetaBatch struct { + StartSequence Uint32 + EndSequence Uint32 + LedgerCloseMetas []LedgerCloseMeta +} + +// EncodeTo encodes this value using the Encoder. +func (s *LedgerCloseMetaBatch) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.StartSequence.EncodeTo(e); err != nil { + return err + } + if err = s.EndSequence.EncodeTo(e); err != nil { + return err + } + if _, err = e.EncodeUint(uint32(len(s.LedgerCloseMetas))); err != nil { + return err + } + for i := 0; i < len(s.LedgerCloseMetas); i++ { + if err = s.LedgerCloseMetas[i].EncodeTo(e); err != nil { + return err + } + } + return nil +} + +var _ decoderFrom = (*LedgerCloseMetaBatch)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *LedgerCloseMetaBatch) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding LedgerCloseMetaBatch: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.StartSequence.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.EndSequence.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + var l uint32 + l, nTmp, err = d.DecodeUint() + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding LedgerCloseMeta: %w", err) + } + s.LedgerCloseMetas = nil + if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding LedgerCloseMeta: length (%d) exceeds remaining input length (%d)", l, il) + } + s.LedgerCloseMetas = make([]LedgerCloseMeta, l) + for i := uint32(0); i < l; i++ { + nTmp, err = s.LedgerCloseMetas[i].DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding LedgerCloseMeta: %w", err) + } + } + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s LedgerCloseMetaBatch) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *LedgerCloseMetaBatch) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*LedgerCloseMetaBatch)(nil) + _ encoding.BinaryUnmarshaler = (*LedgerCloseMetaBatch)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s LedgerCloseMetaBatch) xdrType() {} + +var _ xdrType = (*LedgerCloseMetaBatch)(nil) + var fmtTest = fmt.Sprint("this is a dummy usage of fmt") From c3f65f49276a0537449361630f6856996c2087bf Mon Sep 17 00:00:00 2001 From: tamirms Date: Wed, 28 Feb 2024 18:58:28 +0000 Subject: [PATCH 065/234] services/horizon/internal/ingest: Fix transaction processor metrics (#5216) --- .../internal/ingest/filters/account.go | 4 + .../horizon/internal/ingest/filters/asset.go | 4 + services/horizon/internal/ingest/fsm.go | 2 - .../internal/ingest/group_processors.go | 22 ++-- .../internal/ingest/group_processors_test.go | 8 ++ services/horizon/internal/ingest/main_test.go | 18 ---- .../internal/ingest/processor_runner.go | 101 ++++++++++++++++-- .../processors/account_data_processor.go | 4 + .../ingest/processors/accounts_processor.go | 5 + .../processors/asset_stats_processor.go | 4 + .../claimable_balances_change_processor.go | 4 + ...laimable_balances_transaction_processor.go | 4 + .../ingest/processors/effects_processor.go | 4 + .../ingest/processors/ledgers_processor.go | 4 + .../liquidity_pools_change_processor.go | 4 + .../liquidity_pools_transaction_processor.go | 4 + .../internal/ingest/processors/main.go | 2 + .../ingest/processors/offers_processor.go | 4 + .../ingest/processors/operations_processor.go | 4 + .../processors/participants_processor.go | 4 + .../ingest/processors/signers_processor.go | 4 + .../stats_ledger_transaction_processor.go | 4 + .../ingest/processors/trades_processor.go | 4 + .../processors/transactions_processor.go | 7 ++ .../processors/trust_lines_processor.go | 4 + 25 files changed, 194 insertions(+), 39 deletions(-) diff --git a/services/horizon/internal/ingest/filters/account.go b/services/horizon/internal/ingest/filters/account.go index b2372ae42d..08def6a2b7 100644 --- a/services/horizon/internal/ingest/filters/account.go +++ b/services/horizon/internal/ingest/filters/account.go @@ -26,6 +26,10 @@ func NewAccountFilter() AccountFilter { } } +func (filter *accountFilter) Name() string { + return "filters.accountFilter" +} + func (filter *accountFilter) RefreshAccountFilter(filterConfig *history.AccountFilterConfig) error { // only need to re-initialize the filter config state(rules) if its cached version(in memory) // is older than the incoming config version based on lastModified epoch timestamp diff --git a/services/horizon/internal/ingest/filters/asset.go b/services/horizon/internal/ingest/filters/asset.go index ee7a44deb9..0a3a4ca6c7 100644 --- a/services/horizon/internal/ingest/filters/asset.go +++ b/services/horizon/internal/ingest/filters/asset.go @@ -34,6 +34,10 @@ func NewAssetFilter() AssetFilter { } } +func (filter *assetFilter) Name() string { + return "filters.assetFilter" +} + func (filter *assetFilter) RefreshAssetFilter(filterConfig *history.AssetFilterConfig) error { // only need to re-initialize the filter config state(rules) if it's cached version(in memory) // is older than the incoming config version based on lastModified epoch timestamp diff --git a/services/horizon/internal/ingest/fsm.go b/services/horizon/internal/ingest/fsm.go index c79e11e28a..9c66f30f43 100644 --- a/services/horizon/internal/ingest/fsm.go +++ b/services/horizon/internal/ingest/fsm.go @@ -567,8 +567,6 @@ func (r resumeState) addLedgerStatsMetricFromMap(s *system, prefix string, m map func (r resumeState) addProcessorDurationsMetricFromMap(s *system, m map[string]time.Duration) { for processorName, value := range m { - // * is not accepted in Prometheus labels - processorName = strings.Replace(processorName, "*", "", -1) s.Metrics().ProcessorsRunDuration. With(prometheus.Labels{"name": processorName}).Add(value.Seconds()) s.Metrics().ProcessorsRunDurationSummary. diff --git a/services/horizon/internal/ingest/group_processors.go b/services/horizon/internal/ingest/group_processors.go index 1ea46b4f21..fd0843cc28 100644 --- a/services/horizon/internal/ingest/group_processors.go +++ b/services/horizon/internal/ingest/group_processors.go @@ -2,7 +2,6 @@ package ingest import ( "context" - "fmt" "time" "github.com/stellar/go/ingest" @@ -31,13 +30,17 @@ func newGroupChangeProcessors(processors []horizonChangeProcessor) *groupChangeP } } +func (g groupChangeProcessors) Name() string { + return "groupChangeProcessors" +} + func (g groupChangeProcessors) ProcessChange(ctx context.Context, change ingest.Change) error { for _, p := range g.processors { startTime := time.Now() if err := p.ProcessChange(ctx, change); err != nil { return errors.Wrapf(err, "error in %T.ProcessChange", p) } - g.processorsRunDurations.AddRunDuration(fmt.Sprintf("%T", p), startTime) + g.processorsRunDurations.AddRunDuration(p.Name(), startTime) } return nil } @@ -48,7 +51,7 @@ func (g groupChangeProcessors) Commit(ctx context.Context) error { if err := p.Commit(ctx); err != nil { return errors.Wrapf(err, "error in %T.Commit", p) } - g.processorsRunDurations.AddRunDuration(fmt.Sprintf("%T", p), startTime) + g.processorsRunDurations.AddRunDuration(p.Name(), startTime) } return nil } @@ -95,7 +98,7 @@ func (g groupTransactionProcessors) ProcessTransaction(lcm xdr.LedgerCloseMeta, if err := p.ProcessTransaction(lcm, tx); err != nil { return errors.Wrapf(err, "error in %T.ProcessTransaction", p) } - g.processorsRunDurations.AddRunDuration(fmt.Sprintf("%T", p), startTime) + g.processorsRunDurations.AddRunDuration(p.Name(), startTime) } return nil } @@ -110,9 +113,6 @@ func (g groupTransactionProcessors) Flush(ctx context.Context, session db.Sessio } name := loader.Name() g.loaderRunDurations.AddRunDuration(name, startTime) - if _, ok := g.loaderStats[name]; ok { - return fmt.Errorf("%s is present multiple times", name) - } g.loaderStats[name] = loader.Stats() } @@ -123,7 +123,7 @@ func (g groupTransactionProcessors) Flush(ctx context.Context, session db.Sessio if err := p.Flush(ctx, session); err != nil { return errors.Wrapf(err, "error in %T.Flush", p) } - g.processorsRunDurations.AddRunDuration(fmt.Sprintf("%T", p), startTime) + g.processorsRunDurations.AddRunDuration(p.Name(), startTime) } return nil } @@ -153,6 +153,10 @@ func newGroupTransactionFilterers(filterers []processors.LedgerTransactionFilter } } +func (g *groupTransactionFilterers) Name() string { + return "groupTransactionFilterers" +} + func (g *groupTransactionFilterers) FilterTransaction(ctx context.Context, tx ingest.LedgerTransaction) (bool, error) { for _, f := range g.filterers { startTime := time.Now() @@ -160,7 +164,7 @@ func (g *groupTransactionFilterers) FilterTransaction(ctx context.Context, tx in if err != nil { return false, errors.Wrapf(err, "error in %T.FilterTransaction", f) } - g.AddRunDuration(fmt.Sprintf("%T", f), startTime) + g.AddRunDuration(f.Name(), startTime) if !include { // filter out, we can return early g.droppedTransactions++ diff --git a/services/horizon/internal/ingest/group_processors_test.go b/services/horizon/internal/ingest/group_processors_test.go index 71e50d3911..9fc5a3acf1 100644 --- a/services/horizon/internal/ingest/group_processors_test.go +++ b/services/horizon/internal/ingest/group_processors_test.go @@ -23,6 +23,10 @@ type mockHorizonChangeProcessor struct { mock.Mock } +func (m *mockHorizonChangeProcessor) Name() string { + return "mockHorizonChangeProcessor" +} + func (m *mockHorizonChangeProcessor) ProcessChange(ctx context.Context, change ingest.Change) error { args := m.Called(ctx, change) return args.Error(0) @@ -39,6 +43,10 @@ type mockHorizonTransactionProcessor struct { mock.Mock } +func (m *mockHorizonTransactionProcessor) Name() string { + return "mockHorizonTransactionProcessor" +} + func (m *mockHorizonTransactionProcessor) ProcessTransaction(lcm xdr.LedgerCloseMeta, transaction ingest.LedgerTransaction) error { args := m.Called(lcm, transaction) return args.Error(0) diff --git a/services/horizon/internal/ingest/main_test.go b/services/horizon/internal/ingest/main_test.go index 71c8119273..3ba66ef4ba 100644 --- a/services/horizon/internal/ingest/main_test.go +++ b/services/horizon/internal/ingest/main_test.go @@ -15,7 +15,6 @@ import ( "github.com/stellar/go/ingest" "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/services/horizon/internal/db2/history" - "github.com/stellar/go/services/horizon/internal/ingest/processors" "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" logpkg "github.com/stellar/go/support/log" @@ -529,23 +528,6 @@ func (m *mockProcessorsRunner) RunAllProcessorsOnLedger(ledger xdr.LedgerCloseMe args.Error(1) } -func (m *mockProcessorsRunner) RunTransactionProcessorsOnLedger(ledger xdr.LedgerCloseMeta) ( - processors.StatsLedgerTransactionProcessorResults, - runDurations, - processors.TradeStats, - runDurations, - map[string]history.LoaderStats, - error, -) { - args := m.Called(ledger) - return args.Get(0).(processors.StatsLedgerTransactionProcessorResults), - args.Get(1).(runDurations), - args.Get(2).(processors.TradeStats), - args.Get(3).(runDurations), - args.Get(4).(map[string]history.LoaderStats), - args.Error(3) -} - func (m *mockProcessorsRunner) RunTransactionProcessorsOnLedgers(ledgers []xdr.LedgerCloseMeta) error { args := m.Called(ledgers) return args.Error(0) diff --git a/services/horizon/internal/ingest/processor_runner.go b/services/horizon/internal/ingest/processor_runner.go index c9730276c1..649b345b0c 100644 --- a/services/horizon/internal/ingest/processor_runner.go +++ b/services/horizon/internal/ingest/processor_runner.go @@ -28,10 +28,12 @@ const ( type horizonChangeProcessor interface { processors.ChangeProcessor + Name() string Commit(context.Context) error } type horizonTransactionProcessor interface { + Name() string processors.LedgerTransactionProcessor } @@ -45,6 +47,10 @@ type statsChangeProcessor struct { *ingest.StatsChangeProcessor } +func (statsChangeProcessor) Name() string { + return "ingest.statsChangeProcessor" +} + func (statsChangeProcessor) Commit(ctx context.Context) error { return nil } @@ -70,14 +76,6 @@ type ProcessorRunnerInterface interface { ledgerProtocolVersion uint32, bucketListHash xdr.Hash, ) (ingest.StatsChangeProcessorResults, error) - RunTransactionProcessorsOnLedger(ledger xdr.LedgerCloseMeta) ( - transactionStats processors.StatsLedgerTransactionProcessorResults, - transactionDurations runDurations, - tradeStats processors.TradeStats, - loaderDurations runDurations, - loaderStats map[string]history.LoaderStats, - err error, - ) RunTransactionProcessorsOnLedgers(ledgers []xdr.LedgerCloseMeta) error RunAllProcessorsOnLedger(ledger xdr.LedgerCloseMeta) ( stats ledgerStats, @@ -243,6 +241,13 @@ func (s *ProcessorRunner) RunHistoryArchiveIngestion( s.config.NetworkPassphrase, ) + if err := registerChangeProcessors( + nameRegistry{}, + changeProcessor, + ); err != nil { + return ingest.StatsChangeProcessorResults{}, err + } + if checkpointLedger == 1 { if err := changeProcessor.ProcessChange(s.ctx, ingest.GenesisChange(s.config.NetworkPassphrase)); err != nil { return changeStats.GetResults(), errors.Wrap(err, "Error ingesting genesis ledger") @@ -364,7 +369,7 @@ func (s *ProcessorRunner) streamLedger(ledger xdr.LedgerCloseMeta, return nil } -func (s *ProcessorRunner) RunTransactionProcessorsOnLedger(ledger xdr.LedgerCloseMeta) ( +func (s *ProcessorRunner) runTransactionProcessorsOnLedger(registry nameRegistry, ledger xdr.LedgerCloseMeta) ( transactionStats processors.StatsLedgerTransactionProcessorResults, transactionDurations runDurations, tradeStats processors.TradeStats, @@ -380,6 +385,15 @@ func (s *ProcessorRunner) RunTransactionProcessorsOnLedger(ledger xdr.LedgerClos groupFilteredOutProcessors := s.buildFilteredOutProcessor() groupTransactionProcessors := s.buildTransactionProcessor(ledgersProcessor) + if err = registerTransactionProcessors( + registry, + groupTransactionFilterers, + groupFilteredOutProcessors, + groupTransactionProcessors, + ); err != nil { + return + } + err = s.streamLedger(ledger, groupTransactionFilterers, groupFilteredOutProcessors, @@ -413,6 +427,64 @@ func (s *ProcessorRunner) RunTransactionProcessorsOnLedger(ledger xdr.LedgerClos return } +// nameRegistry ensures all ingestion components have a unique name +// for metrics reporting +type nameRegistry map[string]struct{} + +func (n nameRegistry) add(name string) error { + if _, ok := n[name]; ok { + return fmt.Errorf("%s is duplicated", name) + } + n[name] = struct{}{} + return nil +} + +func registerChangeProcessors( + registry nameRegistry, + group *groupChangeProcessors, +) error { + for _, p := range group.processors { + if err := registry.add(p.Name()); err != nil { + return err + } + } + return nil +} + +func registerTransactionProcessors( + registry nameRegistry, + groupTransactionFilterers *groupTransactionFilterers, + groupFilteredOutProcessors *groupTransactionProcessors, + groupTransactionProcessors *groupTransactionProcessors, +) error { + for _, f := range groupTransactionFilterers.filterers { + if err := registry.add(f.Name()); err != nil { + return err + } + } + for _, p := range groupTransactionProcessors.processors { + if err := registry.add(p.Name()); err != nil { + return err + } + } + for _, l := range groupTransactionProcessors.lazyLoaders { + if err := registry.add(l.Name()); err != nil { + return err + } + } + for _, p := range groupFilteredOutProcessors.processors { + if err := registry.add(p.Name()); err != nil { + return err + } + } + for _, l := range groupFilteredOutProcessors.lazyLoaders { + if err := registry.add(l.Name()); err != nil { + return err + } + } + return nil +} + func (s *ProcessorRunner) RunTransactionProcessorsOnLedgers(ledgers []xdr.LedgerCloseMeta) (err error) { ledgersProcessor := processors.NewLedgerProcessor(s.historyQ.NewLedgerBatchInsertBuilder(), CurrentVersion) @@ -504,12 +576,21 @@ func (s *ProcessorRunner) RunAllProcessorsOnLedger(ledger xdr.LedgerCloseMeta) ( ledger.LedgerSequence(), s.config.NetworkPassphrase, ) + + registry := nameRegistry{} + if err = registerChangeProcessors( + registry, + groupChangeProcessors, + ); err != nil { + return + } + err = s.runChangeProcessorOnLedger(groupChangeProcessors, ledger) if err != nil { return } - transactionStats, transactionDurations, tradeStats, loaderDurations, loaderStats, err := s.RunTransactionProcessorsOnLedger(ledger) + transactionStats, transactionDurations, tradeStats, loaderDurations, loaderStats, err := s.runTransactionProcessorsOnLedger(registry, ledger) stats.changeStats = changeStatsProcessor.GetResults() stats.changeDurations = groupChangeProcessors.processorsRunDurations diff --git a/services/horizon/internal/ingest/processors/account_data_processor.go b/services/horizon/internal/ingest/processors/account_data_processor.go index f774700fbd..5067935648 100644 --- a/services/horizon/internal/ingest/processors/account_data_processor.go +++ b/services/horizon/internal/ingest/processors/account_data_processor.go @@ -27,6 +27,10 @@ func (p *AccountDataProcessor) reset() { p.batchInsertBuilder = p.dataQ.NewAccountDataBatchInsertBuilder() } +func (p *AccountDataProcessor) Name() string { + return "processors.AccountDataProcessor" +} + func (p *AccountDataProcessor) ProcessChange(ctx context.Context, change ingest.Change) error { // We're interested in data only if change.Type != xdr.LedgerEntryTypeData { diff --git a/services/horizon/internal/ingest/processors/accounts_processor.go b/services/horizon/internal/ingest/processors/accounts_processor.go index 681b7d3847..17128f80a5 100644 --- a/services/horizon/internal/ingest/processors/accounts_processor.go +++ b/services/horizon/internal/ingest/processors/accounts_processor.go @@ -4,6 +4,7 @@ import ( "context" "github.com/guregu/null/zero" + "github.com/stellar/go/ingest" "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/support/errors" @@ -28,6 +29,10 @@ func (p *AccountsProcessor) reset() { p.batchInsertBuilder = p.accountsQ.NewAccountsBatchInsertBuilder() } +func (p *AccountsProcessor) Name() string { + return "processors.AccountsProcessor" +} + func (p *AccountsProcessor) ProcessChange(ctx context.Context, change ingest.Change) error { if change.Type != xdr.LedgerEntryTypeAccount { return nil diff --git a/services/horizon/internal/ingest/processors/asset_stats_processor.go b/services/horizon/internal/ingest/processors/asset_stats_processor.go index c3da42b730..9423f245e9 100644 --- a/services/horizon/internal/ingest/processors/asset_stats_processor.go +++ b/services/horizon/internal/ingest/processors/asset_stats_processor.go @@ -44,6 +44,10 @@ func NewAssetStatsProcessor( return p } +func (p *AssetStatsProcessor) Name() string { + return "processors.AssetStatsProcessor" +} + func (p *AssetStatsProcessor) ProcessChange(ctx context.Context, change ingest.Change) error { if change.Type != xdr.LedgerEntryTypeLiquidityPool && change.Type != xdr.LedgerEntryTypeClaimableBalance && diff --git a/services/horizon/internal/ingest/processors/claimable_balances_change_processor.go b/services/horizon/internal/ingest/processors/claimable_balances_change_processor.go index 5d985e691e..de729f9605 100644 --- a/services/horizon/internal/ingest/processors/claimable_balances_change_processor.go +++ b/services/horizon/internal/ingest/processors/claimable_balances_change_processor.go @@ -27,6 +27,10 @@ func NewClaimableBalancesChangeProcessor(Q history.QClaimableBalances) *Claimabl return p } +func (p *ClaimableBalancesChangeProcessor) Name() string { + return "processors.ClaimableBalancesChangeProcessor" +} + func (p *ClaimableBalancesChangeProcessor) reset() { p.cache = ingest.NewChangeCompactor() p.claimantsInsertBuilder = p.qClaimableBalances.NewClaimableBalanceClaimantBatchInsertBuilder() diff --git a/services/horizon/internal/ingest/processors/claimable_balances_transaction_processor.go b/services/horizon/internal/ingest/processors/claimable_balances_transaction_processor.go index 394d2e0f9b..80261f9327 100644 --- a/services/horizon/internal/ingest/processors/claimable_balances_transaction_processor.go +++ b/services/horizon/internal/ingest/processors/claimable_balances_transaction_processor.go @@ -29,6 +29,10 @@ func NewClaimableBalancesTransactionProcessor( } } +func (p *ClaimableBalancesTransactionProcessor) Name() string { + return "processors.ClaimableBalancesTransactionProcessor" +} + func (p *ClaimableBalancesTransactionProcessor) ProcessTransaction( lcm xdr.LedgerCloseMeta, transaction ingest.LedgerTransaction, ) error { diff --git a/services/horizon/internal/ingest/processors/effects_processor.go b/services/horizon/internal/ingest/processors/effects_processor.go index 34e9f9169a..e6e0e4e4a5 100644 --- a/services/horizon/internal/ingest/processors/effects_processor.go +++ b/services/horizon/internal/ingest/processors/effects_processor.go @@ -42,6 +42,10 @@ func NewEffectProcessor( } } +func (p *EffectProcessor) Name() string { + return "processors.EffectProcessor" +} + func (p *EffectProcessor) ProcessTransaction( lcm xdr.LedgerCloseMeta, transaction ingest.LedgerTransaction, ) error { diff --git a/services/horizon/internal/ingest/processors/ledgers_processor.go b/services/horizon/internal/ingest/processors/ledgers_processor.go index 5ecfb2ad7f..0ed2d78b5f 100644 --- a/services/horizon/internal/ingest/processors/ledgers_processor.go +++ b/services/horizon/internal/ingest/processors/ledgers_processor.go @@ -32,6 +32,10 @@ func NewLedgerProcessor(batch history.LedgerBatchInsertBuilder, ingestVersion in } } +func (p *LedgersProcessor) Name() string { + return "processors.LedgersProcessor" +} + func (p *LedgersProcessor) ProcessLedger(lcm xdr.LedgerCloseMeta) *ledgerInfo { sequence := lcm.LedgerSequence() entry, ok := p.ledgers[sequence] diff --git a/services/horizon/internal/ingest/processors/liquidity_pools_change_processor.go b/services/horizon/internal/ingest/processors/liquidity_pools_change_processor.go index c5e5252280..3dce45aa8e 100644 --- a/services/horizon/internal/ingest/processors/liquidity_pools_change_processor.go +++ b/services/horizon/internal/ingest/processors/liquidity_pools_change_processor.go @@ -24,6 +24,10 @@ func NewLiquidityPoolsChangeProcessor(Q history.QLiquidityPools, sequence uint32 return p } +func (p *LiquidityPoolsChangeProcessor) Name() string { + return "processors.LiquidityPoolsChangeProcessor" +} + func (p *LiquidityPoolsChangeProcessor) reset() { p.cache = ingest.NewChangeCompactor() } diff --git a/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor.go b/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor.go index c721f9e4ba..f902904f3d 100644 --- a/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor.go +++ b/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor.go @@ -30,6 +30,10 @@ func NewLiquidityPoolsTransactionProcessor( } } +func (p *LiquidityPoolsTransactionProcessor) Name() string { + return "processors.LiquidityPoolsTransactionProcessor" +} + func (p *LiquidityPoolsTransactionProcessor) ProcessTransaction(lcm xdr.LedgerCloseMeta, transaction ingest.LedgerTransaction) error { err := p.addTransactionLiquidityPools(lcm.LedgerSequence(), transaction) if err != nil { diff --git a/services/horizon/internal/ingest/processors/main.go b/services/horizon/internal/ingest/processors/main.go index 94f83f3fa9..2b6c1cc8fb 100644 --- a/services/horizon/internal/ingest/processors/main.go +++ b/services/horizon/internal/ingest/processors/main.go @@ -5,6 +5,7 @@ import ( "io" "github.com/guregu/null" + "github.com/stellar/go/ingest" "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" @@ -26,6 +27,7 @@ type LedgerTransactionProcessor interface { } type LedgerTransactionFilterer interface { + Name() string FilterTransaction(ctx context.Context, transaction ingest.LedgerTransaction) (bool, error) } diff --git a/services/horizon/internal/ingest/processors/offers_processor.go b/services/horizon/internal/ingest/processors/offers_processor.go index 13ee130d9e..2a3e529be5 100644 --- a/services/horizon/internal/ingest/processors/offers_processor.go +++ b/services/horizon/internal/ingest/processors/offers_processor.go @@ -27,6 +27,10 @@ func NewOffersProcessor(offersQ history.QOffers, sequence uint32) *OffersProcess return p } +func (p *OffersProcessor) Name() string { + return "processors.OffersProcessor" +} + func (p *OffersProcessor) reset() { p.cache = ingest.NewChangeCompactor() p.insertBatchBuilder = p.offersQ.NewOffersBatchInsertBuilder() diff --git a/services/horizon/internal/ingest/processors/operations_processor.go b/services/horizon/internal/ingest/processors/operations_processor.go index 8ad023145c..252d5a6ba0 100644 --- a/services/horizon/internal/ingest/processors/operations_processor.go +++ b/services/horizon/internal/ingest/processors/operations_processor.go @@ -33,6 +33,10 @@ func NewOperationProcessor(batch history.OperationBatchInsertBuilder, network st } } +func (p *OperationProcessor) Name() string { + return "processors.OperationProcessor" +} + // ProcessTransaction process the given transaction func (p *OperationProcessor) ProcessTransaction(lcm xdr.LedgerCloseMeta, transaction ingest.LedgerTransaction) error { for i, op := range transaction.Envelope.Operations() { diff --git a/services/horizon/internal/ingest/processors/participants_processor.go b/services/horizon/internal/ingest/processors/participants_processor.go index 7d4ae7fe39..e889e747c0 100644 --- a/services/horizon/internal/ingest/processors/participants_processor.go +++ b/services/horizon/internal/ingest/processors/participants_processor.go @@ -99,6 +99,10 @@ func participantsForMeta( return participants, nil } +func (p *ParticipantsProcessor) Name() string { + return "processors.ParticipantsProcessor" +} + func (p *ParticipantsProcessor) addTransactionParticipants( sequence uint32, transaction ingest.LedgerTransaction, diff --git a/services/horizon/internal/ingest/processors/signers_processor.go b/services/horizon/internal/ingest/processors/signers_processor.go index b72867522d..b21183820d 100644 --- a/services/horizon/internal/ingest/processors/signers_processor.go +++ b/services/horizon/internal/ingest/processors/signers_processor.go @@ -30,6 +30,10 @@ func NewSignersProcessor( return p } +func (p *SignersProcessor) Name() string { + return "processors.SignersProcessor" +} + func (p *SignersProcessor) reset() { p.batchInsertBuilder = p.signersQ.NewAccountSignersBatchInsertBuilder() p.cache = ingest.NewChangeCompactor() diff --git a/services/horizon/internal/ingest/processors/stats_ledger_transaction_processor.go b/services/horizon/internal/ingest/processors/stats_ledger_transaction_processor.go index 83188e02f8..9efe51dc37 100644 --- a/services/horizon/internal/ingest/processors/stats_ledger_transaction_processor.go +++ b/services/horizon/internal/ingest/processors/stats_ledger_transaction_processor.go @@ -141,6 +141,10 @@ func (p *StatsLedgerTransactionProcessor) ProcessTransaction(lcm xdr.LedgerClose return nil } +func (p *StatsLedgerTransactionProcessor) Name() string { + return "processors.StatsLedgerTransactionProcessor" +} + func (p *StatsLedgerTransactionProcessor) GetResults() StatsLedgerTransactionProcessorResults { return p.results } diff --git a/services/horizon/internal/ingest/processors/trades_processor.go b/services/horizon/internal/ingest/processors/trades_processor.go index 66c6aeb8d3..131531fc97 100644 --- a/services/horizon/internal/ingest/processors/trades_processor.go +++ b/services/horizon/internal/ingest/processors/trades_processor.go @@ -55,6 +55,10 @@ func (stats *TradeStats) Map() map[string]interface{} { } } +func (p *TradeProcessor) Name() string { + return "processors.TradeProcessor" +} + // ProcessTransaction process the given transaction func (p *TradeProcessor) ProcessTransaction(lcm xdr.LedgerCloseMeta, transaction ingest.LedgerTransaction) (err error) { if !transaction.Result.Successful() { diff --git a/services/horizon/internal/ingest/processors/transactions_processor.go b/services/horizon/internal/ingest/processors/transactions_processor.go index 28aae6f327..da615674f8 100644 --- a/services/horizon/internal/ingest/processors/transactions_processor.go +++ b/services/horizon/internal/ingest/processors/transactions_processor.go @@ -13,12 +13,14 @@ import ( type TransactionProcessor struct { batch history.TransactionBatchInsertBuilder skipTxmeta bool + name string } func NewTransactionFilteredTmpProcessor(batch history.TransactionBatchInsertBuilder, skipTxmeta bool) *TransactionProcessor { return &TransactionProcessor{ batch: batch, skipTxmeta: skipTxmeta, + name: "processors.TransactionFilteredTmpProcessor", } } @@ -26,9 +28,14 @@ func NewTransactionProcessor(batch history.TransactionBatchInsertBuilder, skipTx return &TransactionProcessor{ batch: batch, skipTxmeta: skipTxmeta, + name: "processors.TransactionProcessor", } } +func (p *TransactionProcessor) Name() string { + return p.name +} + func (p *TransactionProcessor) ProcessTransaction(lcm xdr.LedgerCloseMeta, transaction ingest.LedgerTransaction) error { elidedTransaction := transaction diff --git a/services/horizon/internal/ingest/processors/trust_lines_processor.go b/services/horizon/internal/ingest/processors/trust_lines_processor.go index 3152ef0db7..a7cae533bd 100644 --- a/services/horizon/internal/ingest/processors/trust_lines_processor.go +++ b/services/horizon/internal/ingest/processors/trust_lines_processor.go @@ -22,6 +22,10 @@ func NewTrustLinesProcessor(trustLinesQ history.QTrustLines) *TrustLinesProcesso return p } +func (p *TrustLinesProcessor) Name() string { + return "processors.TrustLinesProcessor" +} + func (p *TrustLinesProcessor) reset() { p.cache = ingest.NewChangeCompactor() p.batchInsertBuilder = p.trustLinesQ.NewTrustLinesBatchInsertBuilder() From 1379989965f40001ae6d62dfa7249642292f7f91 Mon Sep 17 00:00:00 2001 From: tamirms Date: Tue, 5 Mar 2024 07:36:51 +0000 Subject: [PATCH 066/234] Fix account transactions query (#5229) --- services/horizon/internal/db2/history/main.go | 1 + .../internal/db2/history/transaction.go | 6 +- .../internal/db2/history/transaction_test.go | 118 ++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) diff --git a/services/horizon/internal/db2/history/main.go b/services/horizon/internal/db2/history/main.go index e3a8212731..41a4cb0068 100644 --- a/services/horizon/internal/db2/history/main.go +++ b/services/horizon/internal/db2/history/main.go @@ -883,6 +883,7 @@ type TransactionsQ struct { parent *Q sql sq.SelectBuilder includeFailed bool + txIdCol string } // TrustLine is row of data from the `trust_lines` table from horizon DB diff --git a/services/horizon/internal/db2/history/transaction.go b/services/horizon/internal/db2/history/transaction.go index a308ab9ddc..8e349f5f9d 100644 --- a/services/horizon/internal/db2/history/transaction.go +++ b/services/horizon/internal/db2/history/transaction.go @@ -93,6 +93,7 @@ func (q *Q) Transactions() *TransactionsQ { parent: q, sql: selectTransactionHistory, includeFailed: false, + txIdCol: "ht.id", } } @@ -107,6 +108,7 @@ func (q *TransactionsQ) ForAccount(ctx context.Context, aid string) *Transaction q.sql = q.sql. Join("history_transaction_participants htp ON htp.history_transaction_id = ht.id"). Where("htp.history_account_id = ?", account.ID) + q.txIdCol = "htp.history_transaction_id" return q } @@ -123,6 +125,7 @@ func (q *TransactionsQ) ForClaimableBalance(ctx context.Context, cbID string) *T q.sql = q.sql. Join("history_transaction_claimable_balances htcb ON htcb.history_transaction_id = ht.id"). Where("htcb.history_claimable_balance_id = ?", hCB.InternalID) + q.txIdCol = "htcb.history_transaction_id" return q } @@ -139,6 +142,7 @@ func (q *TransactionsQ) ForLiquidityPool(ctx context.Context, poolID string) *Tr q.sql = q.sql. Join("history_transaction_liquidity_pools htlp ON htlp.history_transaction_id = ht.id"). Where("htlp.history_liquidity_pool_id = ?", hLP.InternalID) + q.txIdCol = "htlp.history_transaction_id" return q } @@ -175,7 +179,7 @@ func (q *TransactionsQ) Page(page db2.PageQuery) *TransactionsQ { return q } - q.sql, q.Err = page.ApplyTo(q.sql, "ht.id") + q.sql, q.Err = page.ApplyTo(q.sql, q.txIdCol) return q } diff --git a/services/horizon/internal/db2/history/transaction_test.go b/services/horizon/internal/db2/history/transaction_test.go index 30c22c7660..4e85624701 100644 --- a/services/horizon/internal/db2/history/transaction_test.go +++ b/services/horizon/internal/db2/history/transaction_test.go @@ -9,6 +9,7 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/guregu/null" + "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/xdr" "github.com/stellar/go/ingest" @@ -921,3 +922,120 @@ func TestHistoryTransactionSchemasMatch(t *testing.T) { tt.Assert.ElementsMatch(txColumns, txTmpFilteredTmpColumns) } + +func TestTransactionQueryBuilder(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + tt.Assert.NoError(q.Begin(tt.Ctx)) + + address := "GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON" + accountLoader := NewAccountLoader() + accountLoader.GetFuture(address) + + cbID := "00000000178826fbfe339e1f5c53417c6fedfe2c05e8bec14303143ec46b38981b09c3f9" + cbLoader := NewClaimableBalanceLoader() + cbLoader.GetFuture(cbID) + + lpID := "0000a8198b5e25994c1ca5b0556faeb27325ac746296944144e0a7406d501e8a" + lpLoader := NewLiquidityPoolLoader() + lpLoader.GetFuture(lpID) + + tt.Assert.NoError(accountLoader.Exec(tt.Ctx, q)) + tt.Assert.NoError(cbLoader.Exec(tt.Ctx, q)) + tt.Assert.NoError(lpLoader.Exec(tt.Ctx, q)) + + tt.Assert.NoError(q.Commit()) + + txQ := q.Transactions().ForAccount(tt.Ctx, address).Page(db2.PageQuery{Cursor: "8589938689", Order: "asc", Limit: 10}) + tt.Assert.NoError(txQ.Err) + got, _, err := txQ.sql.ToSql() + tt.Assert.NoError(err) + // Transactions for account queries will use + // history_transaction_participants.history_transaction_id in their predicates. + want := "SELECT " + + "ht.id, ht.transaction_hash, ht.ledger_sequence, ht.application_order, " + + "ht.account, ht.account_muxed, ht.account_sequence, ht.max_fee, " + + "COALESCE(ht.fee_charged, ht.max_fee) as fee_charged, ht.operation_count, " + + "ht.tx_envelope, ht.tx_result, ht.tx_meta, ht.tx_fee_meta, ht.created_at, " + + "ht.updated_at, COALESCE(ht.successful, true) as successful, ht.signatures, " + + "ht.memo_type, ht.memo, ht.time_bounds, ht.ledger_bounds, ht.min_account_sequence, " + + "ht.min_account_sequence_age, ht.min_account_sequence_ledger_gap, ht.extra_signers, " + + "hl.closed_at AS ledger_close_time, ht.inner_transaction_hash, ht.fee_account, " + + "ht.fee_account_muxed, ht.new_max_fee, ht.inner_signatures " + + "FROM history_transactions ht " + + "LEFT JOIN history_ledgers hl ON ht.ledger_sequence = hl.sequence " + + "JOIN history_transaction_participants htp ON htp.history_transaction_id = ht.id " + + "WHERE htp.history_account_id = ? AND htp.history_transaction_id > ? " + + "ORDER BY htp.history_transaction_id asc LIMIT 10" + tt.Assert.EqualValues(want, got) + + txQ = q.Transactions().ForClaimableBalance(tt.Ctx, cbID).Page(db2.PageQuery{Cursor: "8589938689", Order: "asc", Limit: 10}) + tt.Assert.NoError(txQ.Err) + got, _, err = txQ.sql.ToSql() + tt.Assert.NoError(err) + // Transactions for claimable balance queries will use + // history_transaction_claimable_balances.history_transaction_id in their predicates. + want = "SELECT " + + "ht.id, ht.transaction_hash, ht.ledger_sequence, ht.application_order, " + + "ht.account, ht.account_muxed, ht.account_sequence, ht.max_fee, " + + "COALESCE(ht.fee_charged, ht.max_fee) as fee_charged, ht.operation_count, " + + "ht.tx_envelope, ht.tx_result, ht.tx_meta, ht.tx_fee_meta, ht.created_at, " + + "ht.updated_at, COALESCE(ht.successful, true) as successful, ht.signatures, " + + "ht.memo_type, ht.memo, ht.time_bounds, ht.ledger_bounds, ht.min_account_sequence, " + + "ht.min_account_sequence_age, ht.min_account_sequence_ledger_gap, ht.extra_signers, " + + "hl.closed_at AS ledger_close_time, ht.inner_transaction_hash, ht.fee_account, " + + "ht.fee_account_muxed, ht.new_max_fee, ht.inner_signatures " + + "FROM history_transactions ht " + + "LEFT JOIN history_ledgers hl ON ht.ledger_sequence = hl.sequence " + + "JOIN history_transaction_claimable_balances htcb ON htcb.history_transaction_id = ht.id " + + "WHERE htcb.history_claimable_balance_id = ? AND htcb.history_transaction_id > ? " + + "ORDER BY htcb.history_transaction_id asc LIMIT 10" + tt.Assert.EqualValues(want, got) + + txQ = q.Transactions().ForLiquidityPool(tt.Ctx, lpID).Page(db2.PageQuery{Cursor: "8589938689", Order: "asc", Limit: 10}) + tt.Assert.NoError(txQ.Err) + got, _, err = txQ.sql.ToSql() + tt.Assert.NoError(err) + // Transactions for liquidity pool queries will use + // history_transaction_liquidity_pools.history_transaction_id in their predicates. + want = "SELECT " + + "ht.id, ht.transaction_hash, ht.ledger_sequence, ht.application_order, " + + "ht.account, ht.account_muxed, ht.account_sequence, ht.max_fee, " + + "COALESCE(ht.fee_charged, ht.max_fee) as fee_charged, ht.operation_count, " + + "ht.tx_envelope, ht.tx_result, ht.tx_meta, ht.tx_fee_meta, ht.created_at, " + + "ht.updated_at, COALESCE(ht.successful, true) as successful, ht.signatures, " + + "ht.memo_type, ht.memo, ht.time_bounds, ht.ledger_bounds, ht.min_account_sequence, " + + "ht.min_account_sequence_age, ht.min_account_sequence_ledger_gap, ht.extra_signers, " + + "hl.closed_at AS ledger_close_time, ht.inner_transaction_hash, ht.fee_account, " + + "ht.fee_account_muxed, ht.new_max_fee, ht.inner_signatures " + + "FROM history_transactions ht " + + "LEFT JOIN history_ledgers hl ON ht.ledger_sequence = hl.sequence " + + "JOIN history_transaction_liquidity_pools htlp ON htlp.history_transaction_id = ht.id " + + "WHERE htlp.history_liquidity_pool_id = ? AND htlp.history_transaction_id > ? " + + "ORDER BY htlp.history_transaction_id asc LIMIT 10" + tt.Assert.EqualValues(want, got) + + txQ = q.Transactions().Page(db2.PageQuery{Cursor: "8589938689", Order: "asc", Limit: 10}) + tt.Assert.NoError(txQ.Err) + got, _, err = txQ.sql.ToSql() + tt.Assert.NoError(err) + // Other Transaction queries will use history_transactions.id in their predicates. + want = "SELECT " + + "ht.id, ht.transaction_hash, ht.ledger_sequence, ht.application_order, " + + "ht.account, ht.account_muxed, ht.account_sequence, ht.max_fee, " + + "COALESCE(ht.fee_charged, ht.max_fee) as fee_charged, ht.operation_count, " + + "ht.tx_envelope, ht.tx_result, ht.tx_meta, ht.tx_fee_meta, ht.created_at, " + + "ht.updated_at, COALESCE(ht.successful, true) as successful, ht.signatures, " + + "ht.memo_type, ht.memo, ht.time_bounds, ht.ledger_bounds, ht.min_account_sequence, " + + "ht.min_account_sequence_age, ht.min_account_sequence_ledger_gap, ht.extra_signers, " + + "hl.closed_at AS ledger_close_time, ht.inner_transaction_hash, ht.fee_account, " + + "ht.fee_account_muxed, ht.new_max_fee, ht.inner_signatures " + + "FROM history_transactions ht " + + "LEFT JOIN history_ledgers hl ON ht.ledger_sequence = hl.sequence " + + "WHERE ht.id > ? " + + "ORDER BY ht.id asc LIMIT 10" + tt.Assert.EqualValues(want, got) +} From e21bc4307ce7b36ed30cc9289126df3eba8448ed Mon Sep 17 00:00:00 2001 From: shawn Date: Tue, 5 Mar 2024 12:59:54 -0800 Subject: [PATCH 067/234] services/horizon: Add new metrics counters for db connection close events (#5225) --- services/horizon/CHANGELOG.md | 5 + support/db/main.go | 8 +- support/db/metrics.go | 73 +++++++++- support/db/session.go | 81 ++++++++--- support/db/session_test.go | 267 ++++++++++++++++++++++++++++++---- 5 files changed, 369 insertions(+), 65 deletions(-) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index cd411b7120..6bb186b393 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## unreleased + +### Added +- New `db_error_total` metrics key with labels `ctx_error`, `db_error`, and `db_error_extra` ([5225](https://github.com/stellar/go/pull/5225)). + ## 2.28.3 ### Fixed diff --git a/support/db/main.go b/support/db/main.go index 2fb1f18a10..dca23526ee 100644 --- a/support/db/main.go +++ b/support/db/main.go @@ -118,10 +118,14 @@ type Session struct { // DB is the database connection that queries should be executed against. DB *sqlx.DB - tx *sqlx.Tx - txOptions *sql.TxOptions + tx *sqlx.Tx + txOptions *sql.TxOptions + errorHandlers []ErrorHandlerFunc } +// dbErr - the Postgres error +// ctx - the caller's context +type ErrorHandlerFunc func(dbErr error, ctx context.Context) type SessionInterface interface { BeginTx(ctx context.Context, opts *sql.TxOptions) error Begin(ctx context.Context) error diff --git a/support/db/metrics.go b/support/db/metrics.go index 0726a85f91..5abfe3013a 100644 --- a/support/db/metrics.go +++ b/support/db/metrics.go @@ -3,11 +3,13 @@ package db import ( "context" "database/sql" + "errors" "fmt" "strings" "time" "github.com/Masterminds/squirrel" + "github.com/lib/pq" "github.com/prometheus/client_golang/prometheus" ) @@ -58,6 +60,7 @@ type SessionWithMetrics struct { maxLifetimeClosedCounter prometheus.CounterFunc roundTripProbe *roundTripProbe roundTripTimeSummary prometheus.Summary + errorCounter *prometheus.CounterVec } func RegisterMetrics(base *Session, namespace string, sub Subservice, registry *prometheus.Registry) SessionInterface { @@ -66,6 +69,8 @@ func RegisterMetrics(base *Session, namespace string, sub Subservice, registry * registry: registry, } + base.AddErrorHandler(s.handleErrorEvent) + s.queryCounter = prometheus.NewCounterVec( prometheus.CounterOpts{ Namespace: namespace, @@ -226,6 +231,18 @@ func RegisterMetrics(base *Session, namespace string, sub Subservice, registry * ) registry.MustRegister(s.maxLifetimeClosedCounter) + s.errorCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: "db", + Name: "error_total", + Help: "total number of db related errors, details are captured in labels", + ConstLabels: prometheus.Labels{"subservice": string(sub)}, + }, + []string{"ctx_error", "db_error", "db_error_extra"}, + ) + registry.MustRegister(s.errorCounter) + s.roundTripTimeSummary = prometheus.NewSummary( prometheus.SummaryOpts{ Namespace: namespace, @@ -262,15 +279,10 @@ func (s *SessionWithMetrics) Close() error { s.registry.Unregister(s.maxIdleClosedCounter) s.registry.Unregister(s.maxIdleTimeClosedCounter) s.registry.Unregister(s.maxLifetimeClosedCounter) + s.registry.Unregister(s.errorCounter) return s.SessionInterface.Close() } -// TODO: Implement these -// func (s *SessionWithMetrics) BeginTx(ctx context.Context, opts *sql.TxOptions) error { -// func (s *SessionWithMetrics) Begin(ctx context.Context) error { -// func (s *SessionWithMetrics) Commit(ctx context.Context) error -// func (s *SessionWithMetrics) Rollback(ctx context.Context) error - func (s *SessionWithMetrics) TruncateTables(ctx context.Context, tables []string) (err error) { timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { s.queryDurationSummary.With(prometheus.Labels{ @@ -314,6 +326,7 @@ func (s *SessionWithMetrics) Clone() SessionInterface { maxIdleClosedCounter: s.maxIdleClosedCounter, maxIdleTimeClosedCounter: s.maxIdleTimeClosedCounter, maxLifetimeClosedCounter: s.maxLifetimeClosedCounter, + errorCounter: s.errorCounter, } } @@ -356,6 +369,53 @@ func getQueryType(ctx context.Context, query squirrel.Sqlizer) QueryType { return UndefinedQueryType } +// derive the db 'error_total' metric from the err returned by libpq +// +// dbErr - the error returned by any libpq method call +// ctx - the caller's context used on libpb method call +func (s *SessionWithMetrics) handleErrorEvent(dbErr error, ctx context.Context) { + if dbErr == nil || s.NoRows(dbErr) { + return + } + + ctxError := "n/a" + dbError := "other" + errorExtra := "n/a" + var pqErr *pq.Error + + switch { + case errors.As(dbErr, &pqErr): + dbError = string(pqErr.Code) + switch pqErr.Message { + case "canceling statement due to user request": + errorExtra = "user_request" + case "canceling statement due to statement timeout": + errorExtra = "statement_timeout" + } + case strings.Contains(dbErr.Error(), "driver: bad connection"): + dbError = "driver_bad_connection" + case strings.Contains(dbErr.Error(), "sql: transaction has already been committed or rolled back"): + dbError = "tx_already_rollback" + case errors.Is(dbErr, context.Canceled): + dbError = "canceled" + case errors.Is(dbErr, context.DeadlineExceeded): + dbError = "deadline_exceeded" + } + + switch { + case errors.Is(ctx.Err(), context.Canceled): + ctxError = "canceled" + case errors.Is(ctx.Err(), context.DeadlineExceeded): + ctxError = "deadline_exceeded" + } + + s.errorCounter.With(prometheus.Labels{ + "ctx_error": ctxError, + "db_error": dbError, + "db_error_extra": errorExtra, + }).Inc() +} + func (s *SessionWithMetrics) Get(ctx context.Context, dest interface{}, query squirrel.Sqlizer) (err error) { queryType := string(getQueryType(ctx, query)) timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { @@ -373,7 +433,6 @@ func (s *SessionWithMetrics) Get(ctx context.Context, dest interface{}, query sq "route": contextRoute(ctx), }).Inc() }() - err = s.SessionInterface.Get(ctx, dest, query) return err } diff --git a/support/db/session.go b/support/db/session.go index 4ad0bc86b5..472fc40a37 100644 --- a/support/db/session.go +++ b/support/db/session.go @@ -3,6 +3,7 @@ package db import ( "context" "database/sql" + go_errors "errors" "fmt" "reflect" "strings" @@ -10,6 +11,7 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" + "github.com/lib/pq" "github.com/stellar/go/support/db/sqlutils" "github.com/stellar/go/support/errors" "github.com/stellar/go/support/log" @@ -23,7 +25,7 @@ func (s *Session) Begin(ctx context.Context) error { tx, err := s.DB.BeginTxx(ctx, nil) if err != nil { - if knownErr := s.replaceWithKnownError(err, ctx); knownErr != nil { + if knownErr := s.handleError(err, ctx); knownErr != nil { return knownErr } @@ -44,7 +46,7 @@ func (s *Session) BeginTx(ctx context.Context, opts *sql.TxOptions) error { tx, err := s.DB.BeginTxx(ctx, opts) if err != nil { - if knownErr := s.replaceWithKnownError(err, ctx); knownErr != nil { + if knownErr := s.handleError(err, ctx); knownErr != nil { return knownErr } @@ -92,7 +94,7 @@ func (s *Session) Commit() error { s.tx = nil s.txOptions = nil - if knownErr := s.replaceWithKnownError(err, context.Background()); knownErr != nil { + if knownErr := s.handleError(err, context.Background()); knownErr != nil { return knownErr } return err @@ -146,7 +148,7 @@ func (s *Session) GetRaw(ctx context.Context, dest interface{}, query string, ar return nil } - if knownErr := s.replaceWithKnownError(err, ctx); knownErr != nil { + if knownErr := s.handleError(err, ctx); knownErr != nil { return knownErr } @@ -215,7 +217,7 @@ func (s *Session) ExecRaw(ctx context.Context, query string, args ...interface{} return result, nil } - if knownErr := s.replaceWithKnownError(err, ctx); knownErr != nil { + if knownErr := s.handleError(err, ctx); knownErr != nil { return nil, knownErr } @@ -232,29 +234,60 @@ func (s *Session) NoRows(err error) bool { return err == sql.ErrNoRows } -// replaceWithKnownError tries to replace Postgres error with package error. -// Returns a new error if the err is known. -func (s *Session) replaceWithKnownError(err error, ctx context.Context) error { - if err == nil { +func (s *Session) AddErrorHandler(handler ErrorHandlerFunc) { + s.errorHandlers = append(s.errorHandlers, handler) +} + +// handleError does housekeeping on errors from db. +// dbErr - the libpq client error +// ctx - the calling context +// +// tries to replace dbErr with horizon package error, returns a new error if the err is known. +// invokes any additional error handlers that may have been +// added to the session, passing the caller's context +func (s *Session) handleError(dbErr error, ctx context.Context) error { + if dbErr == nil { return nil } + for _, handler := range s.errorHandlers { + handler(dbErr, ctx) + } + + var dbErrorCode pq.ErrorCode + var pqErr *pq.Error + + // if libpql sends to server, and then any server side error is reported, + // libpq passes back only an pq.ErrorCode from method call + // even if the caller context generates a cancel/deadline error during the server trip, + // libpq will only return an instance of pq.ErrorCode as a non-wrapped error + if go_errors.As(dbErr, &pqErr) { + dbErrorCode = pqErr.Code + } + switch { - case ctx.Err() == context.Canceled: - return ErrCancelled - case ctx.Err() == context.DeadlineExceeded: - // if libpq waits too long to obtain conn from pool, can get ctx timeout before server trip - return ErrTimeout - case strings.Contains(err.Error(), "pq: canceling statement due to user request"): - return ErrTimeout - case strings.Contains(err.Error(), "pq: canceling statement due to conflict with recovery"): + case strings.Contains(dbErr.Error(), "pq: canceling statement due to conflict with recovery"): return ErrConflictWithRecovery - case strings.Contains(err.Error(), "driver: bad connection"): + case strings.Contains(dbErr.Error(), "driver: bad connection"): return ErrBadConnection - case strings.Contains(err.Error(), "pq: canceling statement due to statement timeout"): - return ErrStatementTimeout - case strings.Contains(err.Error(), "transaction has already been committed or rolled back"): + case strings.Contains(dbErr.Error(), "transaction has already been committed or rolled back"): return ErrAlreadyRolledback + case go_errors.Is(ctx.Err(), context.Canceled): + // when horizon's context is cancelled by it's upstream api client, + // it will propagate to here and libpq will emit a wrapped err that has the cancel err + return ErrCancelled + case go_errors.Is(ctx.Err(), context.DeadlineExceeded): + // when horizon's context times out(it's set to app connection-timeout), + // it will trigger libpq to emit a wrapped err that has the deadline err + return ErrTimeout + case dbErrorCode == "57014": + // https://www.postgresql.org/docs/12/errcodes-appendix.html, query_canceled + // this code can be generated for multiple cases, + // by libpq sending a signal to server when it experiences a context cancel/deadline + // or it could happen based on just server statement_timeout setting + // since we check the context cancel/deadline err state first, getting here means + // this can only be from a statement timeout + return ErrStatementTimeout default: return nil } @@ -284,7 +317,7 @@ func (s *Session) QueryRaw(ctx context.Context, query string, args ...interface{ return result, nil } - if knownErr := s.replaceWithKnownError(err, ctx); knownErr != nil { + if knownErr := s.handleError(err, ctx); knownErr != nil { return nil, knownErr } @@ -318,7 +351,7 @@ func (s *Session) Rollback() error { s.tx = nil s.txOptions = nil - if knownErr := s.replaceWithKnownError(err, context.Background()); knownErr != nil { + if knownErr := s.handleError(err, context.Background()); knownErr != nil { return knownErr } return err @@ -362,7 +395,7 @@ func (s *Session) SelectRaw( return nil } - if knownErr := s.replaceWithKnownError(err, ctx); knownErr != nil { + if knownErr := s.handleError(err, ctx); knownErr != nil { return knownErr } diff --git a/support/db/session_test.go b/support/db/session_test.go index 8629b2ca7e..1fd2a3902b 100644 --- a/support/db/session_test.go +++ b/support/db/session_test.go @@ -2,33 +2,72 @@ package db import ( "context" + "sync" "testing" "time" + //"github.com/lib/pq" + "github.com/lib/pq" + "github.com/prometheus/client_golang/prometheus" "github.com/stellar/go/support/db/dbtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestServerTimeout(t *testing.T) { +func TestContextTimeoutDuringSql(t *testing.T) { db := dbtest.Postgres(t).Load(testSchema) defer db.Close() var cancel context.CancelFunc ctx := context.Background() - ctx, cancel = context.WithTimeout(ctx, time.Duration(1)) + ctx, cancel = context.WithTimeout(ctx, 2*time.Second) assert := assert.New(t) - sess := &Session{DB: db.Open()} - defer sess.DB.Close() + sessRaw := &Session{DB: db.Open()} + reg := prometheus.NewRegistry() + + sess := RegisterMetrics(sessRaw, "test", "subtest", reg) + defer sess.Close() defer cancel() var count int - err := sess.GetRaw(ctx, &count, "SELECT pg_sleep(2), COUNT(*) FROM people") - assert.ErrorIs(err, ErrTimeout, "long running db server operation past context timeout, should return timeout") + var wg sync.WaitGroup + wg.Add(1) + + go func() { + err := sess.GetRaw(ctx, &count, "SELECT pg_sleep(5) FROM people") + assert.ErrorIs(err, ErrTimeout, "long running db server operation past context timeout, should return timeout") + wg.Done() + }() + + require.Eventually(t, func() bool { wg.Wait(); return true }, 5*time.Second, time.Second) + assertDbErrorMetrics(reg, "deadline_exceeded", "57014", "user_request", assert) +} + +func TestContextTimeoutBeforeSql(t *testing.T) { + db := dbtest.Postgres(t).Load(testSchema) + defer db.Close() + + var cancel context.CancelFunc + ctx := context.Background() + ctx, cancel = context.WithTimeout(ctx, time.Millisecond) + assert := assert.New(t) + + sessRaw := &Session{DB: db.Open()} + reg := prometheus.NewRegistry() + + sess := RegisterMetrics(sessRaw, "test", "subtest", reg) + defer sess.Close() + defer cancel() + + var count int + time.Sleep(500 * time.Millisecond) + err := sess.GetRaw(ctx, &count, "SELECT pg_sleep(5) FROM people") + assert.ErrorIs(err, ErrTimeout, "any db server operation should return error immediately if context already timed out") + assertDbErrorMetrics(reg, "deadline_exceeded", "deadline_exceeded", "n/a", assert) } -func TestUserCancel(t *testing.T) { +func TestContextCancelledBeforeSql(t *testing.T) { db := dbtest.Postgres(t).Load(testSchema) defer db.Close() @@ -37,14 +76,68 @@ func TestUserCancel(t *testing.T) { ctx, cancel = context.WithCancel(ctx) assert := assert.New(t) - sess := &Session{DB: db.Open()} - defer sess.DB.Close() + sessRaw := &Session{DB: db.Open()} + reg := prometheus.NewRegistry() + + sess := RegisterMetrics(sessRaw, "test", "subtest", reg) + defer sess.Close() defer cancel() var count int cancel() err := sess.GetRaw(ctx, &count, "SELECT pg_sleep(2), COUNT(*) FROM people") - assert.ErrorIs(err, ErrCancelled, "any ongoing db server operation should return error immediately after user cancel") + assert.ErrorIs(err, ErrCancelled, "any db server operation should return error immediately if user already cancel") + assertDbErrorMetrics(reg, "canceled", "canceled", "n/a", assert) +} + +func TestContextCancelDuringSql(t *testing.T) { + db := dbtest.Postgres(t).Load(testSchema) + defer db.Close() + + var cancel context.CancelFunc + ctx := context.Background() + ctx, cancel = context.WithCancel(ctx) + assert := assert.New(t) + + sessRaw := &Session{DB: db.Open()} + reg := prometheus.NewRegistry() + + sess := RegisterMetrics(sessRaw, "test", "subtest", reg) + defer sess.Close() + defer cancel() + + var count int + var wg sync.WaitGroup + wg.Add(1) + + go func() { + err := sess.GetRaw(ctx, &count, "SELECT pg_sleep(5) FROM people") + assert.ErrorIs(err, ErrCancelled, "any ongoing db server operation should return error immediately after user cancel") + wg.Done() + }() + time.Sleep(time.Second) + cancel() + + require.Eventually(t, func() bool { wg.Wait(); return true }, 5*time.Second, time.Second) + assertDbErrorMetrics(reg, "canceled", "57014", "user_request", assert) +} + +func TestStatementTimeout(t *testing.T) { + assert := assert.New(t) + db := dbtest.Postgres(t).Load(testSchema) + defer db.Close() + + sessRaw, err := Open(db.Dialect, db.DSN, StatementTimeout(50*time.Millisecond)) + reg := prometheus.NewRegistry() + + sess := RegisterMetrics(sessRaw, "test", "subtest", reg) + assert.NoError(err) + defer sess.Close() + + var count int + err = sess.GetRaw(context.Background(), &count, "SELECT pg_sleep(2) FROM people") + assert.ErrorIs(err, ErrStatementTimeout) + assertDbErrorMetrics(reg, "n/a", "57014", "statement_timeout", assert) } func TestSession(t *testing.T) { @@ -54,10 +147,13 @@ func TestSession(t *testing.T) { ctx := context.Background() assert := assert.New(t) require := require.New(t) - sess := &Session{DB: db.Open()} - defer sess.DB.Close() + sessRaw := &Session{DB: db.Open()} + reg := prometheus.NewRegistry() - assert.Equal("postgres", sess.Dialect()) + sess := RegisterMetrics(sessRaw, "test", "subtest", reg) + defer sess.Close() + + assert.Equal("postgres", sessRaw.Dialect()) var count int err := sess.GetRaw(ctx, &count, "SELECT COUNT(*) FROM people") @@ -124,61 +220,168 @@ func TestSession(t *testing.T) { assert.Len(names, 2) // Test ReplacePlaceholders - out, err := sess.ReplacePlaceholders("? = ? = ? = ??") + out, err := sessRaw.ReplacePlaceholders("? = ? = ? = ??") if assert.NoError(err) { assert.Equal("$1 = $2 = $3 = ?", out) } + + assertZeroErrorMetrics(reg, assert) } -func TestStatementTimeout(t *testing.T) { +func TestIdleTransactionTimeout(t *testing.T) { assert := assert.New(t) db := dbtest.Postgres(t).Load(testSchema) defer db.Close() - sess, err := Open(db.Dialect, db.DSN, StatementTimeout(50*time.Millisecond)) + sessRaw, err := Open(db.Dialect, db.DSN, IdleTransactionTimeout(50*time.Millisecond)) assert.NoError(err) + reg := prometheus.NewRegistry() + sess := RegisterMetrics(sessRaw, "test", "subtest", reg) + defer sess.Close() + assert.NoError(sess.Begin(context.Background())) + <-time.After(150 * time.Millisecond) + var count int - err = sess.GetRaw(context.Background(), &count, "SELECT pg_sleep(2), COUNT(*) FROM people") - assert.ErrorIs(err, ErrStatementTimeout) + err = sess.GetRaw(context.Background(), &count, "SELECT COUNT(*) FROM people") + assert.ErrorIs(err, ErrBadConnection) + assertDbErrorMetrics(reg, "n/a", "driver_bad_connection", "n/a", assert) } -func TestIdleTransactionTimeout(t *testing.T) { +func TestIdleTransactionTimeoutAndContextTimeout(t *testing.T) { assert := assert.New(t) db := dbtest.Postgres(t).Load(testSchema) defer db.Close() - sess, err := Open(db.Dialect, db.DSN, IdleTransactionTimeout(50*time.Millisecond)) + var cancel context.CancelFunc + ctx := context.Background() + ctx, cancel = context.WithTimeout(ctx, 150*time.Millisecond) + + sessRaw, err := Open(db.Dialect, db.DSN, IdleTransactionTimeout(100*time.Millisecond)) assert.NoError(err) + reg := prometheus.NewRegistry() + sess := RegisterMetrics(sessRaw, "test", "subtest", reg) + defer sess.Close() + defer cancel() + var wg sync.WaitGroup + wg.Add(1) assert.NoError(sess.Begin(context.Background())) - <-time.After(150 * time.Millisecond) - var count int - err = sess.GetRaw(context.Background(), &count, "SELECT COUNT(*) FROM people") - assert.ErrorIs(err, ErrBadConnection) + <-time.After(200 * time.Millisecond) + + go func() { + _, err := sess.ExecRaw(ctx, "SELECT pg_sleep(5) FROM people") + assert.ErrorIs(err, ErrTimeout, "long running db server operation past context timeout, should return timeout") + wg.Done() + }() + + require.Eventually(t, func() bool { wg.Wait(); return true }, 5*time.Second, time.Second) + // this demonstrates subtley of libpq error handling: + // first a server session was created + // 100ms elapsed and idle server session was triggered on server side, server sent signal back to libpq, libpq marks the session locally as bad + // 150ms caller ctx deadlined + // now caller invokes libpq and tries to submit a sql statement on the now-closed session with deadlined ctx also + // libpq only reports an error of deadline exceeded it will not emit the driver_bad_connection due to closed server session + assertDbErrorMetrics(reg, "deadline_exceeded", "deadline_exceeded", "n/a", assert) +} + +func TestDbServerErrorCodeInMetrics(t *testing.T) { + assert := assert.New(t) + db := dbtest.Postgres(t).Load(testSchema) + defer db.Close() + + sessRaw := &Session{DB: db.Open()} + reg := prometheus.NewRegistry() + sess := RegisterMetrics(sessRaw, "test", "subtest", reg) + + defer sess.Close() + var pqErr *pq.Error + + _, err := sess.ExecRaw(context.Background(), "oops, invalid sql") + assert.ErrorAs(err, &pqErr) + assertDbErrorMetrics(reg, "n/a", "42601", "n/a", assert) } -func TestSessionRollbackAfterContextCanceled(t *testing.T) { +func TestDbOtherErrorInMetrics(t *testing.T) { + assert := assert.New(t) db := dbtest.Postgres(t).Load(testSchema) defer db.Close() - sess := setupRolledbackTx(t, db) - defer sess.DB.Close() + conn := db.Open() + conn.Close() + sessRaw := &Session{DB: conn} + reg := prometheus.NewRegistry() + sess := RegisterMetrics(sessRaw, "test", "subtest", reg) + + defer sess.Close() - assert.ErrorIs(t, sess.Rollback(), ErrAlreadyRolledback) + var count int + err := sess.GetRaw(context.Background(), &count, "SELECT COUNT(*) FROM people") + assert.ErrorContains(err, "sql: database is closed") + assertDbErrorMetrics(reg, "n/a", "other", "n/a", assert) } -func TestSessionCommitAfterContextCanceled(t *testing.T) { +func TestSessionAfterRollback(t *testing.T) { + assert := assert.New(t) db := dbtest.Postgres(t).Load(testSchema) defer db.Close() - sess := setupRolledbackTx(t, db) - defer sess.DB.Close() + sessRaw := setupRolledbackTx(t, db) + reg := prometheus.NewRegistry() + sess := RegisterMetrics(sessRaw, "test", "subtest", reg) + defer sess.Close() + + var count int + err := sess.GetRaw(context.Background(), &count, "SELECT COUNT(*) FROM people") + assert.ErrorIs(err, ErrAlreadyRolledback) + assertDbErrorMetrics(reg, "n/a", "tx_already_rollback", "n/a", assert) +} - assert.ErrorIs(t, sess.Commit(), ErrAlreadyRolledback) +func assertZeroErrorMetrics(reg *prometheus.Registry, assert *assert.Assertions) { + metrics, err := reg.Gather() + assert.NoError(err) + + for _, metricFamily := range metrics { + if metricFamily.GetName() == "test_db_error_total" { + assert.Fail("error_total metrics should not be present, never incremented") + } + } + +} + +func assertDbErrorMetrics(reg *prometheus.Registry, assertCtxError, assertDbError, assertDbErrorExtra string, assert *assert.Assertions) { + metrics, err := reg.Gather() + assert.NoError(err) + + for _, metricFamily := range metrics { + if metricFamily.GetName() == "test_db_error_total" { + assert.Len(metricFamily.GetMetric(), 1) + assert.Equal(metricFamily.GetMetric()[0].GetCounter().GetValue(), float64(1)) + var ctxError = "" + var dbError = "" + var dbErrorExtra = "" + for _, label := range metricFamily.GetMetric()[0].GetLabel() { + if label.GetName() == "ctx_error" { + ctxError = label.GetValue() + } + if label.GetName() == "db_error" { + dbError = label.GetValue() + } + if label.GetName() == "db_error_extra" { + dbErrorExtra = label.GetValue() + } + } + + assert.Equal(ctxError, assertCtxError) + assert.Equal(dbError, assertDbError) + assert.Equal(dbErrorExtra, assertDbErrorExtra) + return + } + } + assert.Fail("error_total metrics were not correct") } func setupRolledbackTx(t *testing.T, db *dbtest.DB) *Session { From 742b367e15723cecfe59902e70592cd7df1c5e18 Mon Sep 17 00:00:00 2001 From: tamirms Date: Wed, 6 Mar 2024 17:09:52 +0000 Subject: [PATCH 068/234] Fix ingestion loader duration metric (#5234) --- services/horizon/internal/ingest/fsm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/horizon/internal/ingest/fsm.go b/services/horizon/internal/ingest/fsm.go index 9c66f30f43..3ce02864c6 100644 --- a/services/horizon/internal/ingest/fsm.go +++ b/services/horizon/internal/ingest/fsm.go @@ -524,7 +524,7 @@ func (r resumeState) run(s *system) (transition, error) { tradeStatsMap := stats.tradeStats.Map() r.addLedgerStatsMetricFromMap(s, "trades", tradeStatsMap) r.addProcessorDurationsMetricFromMap(s, stats.transactionDurations) - r.addLoaderDurationsMetricFromMap(s, stats.transactionDurations) + r.addLoaderDurationsMetricFromMap(s, stats.loaderDurations) r.addLoaderStatsMetric(s, stats.loaderStats) // since a single system instance is shared throughout all states, From ab3a9265a31d7a86a794d447061652ab53cc3237 Mon Sep 17 00:00:00 2001 From: George Date: Thu, 7 Mar 2024 15:28:33 -0800 Subject: [PATCH 069/234] historyarchive: Add round-robin, error-resilience, and back-off to the `ArchivePool` (#5224) * Perform round robin w/ individual backoff * Remove transient retry test from checkpoints * go.mod fixups with a tidy run --- go.mod | 5 +- go.sum | 2 + historyarchive/archive_pool.go | 225 ++++++++++++++----- historyarchive/archive_pool_test.go | 79 ++++++- ingest/checkpoint_change_reader.go | 18 +- ingest/checkpoint_change_reader_test.go | 34 +-- ingest/ledgerbackend/captive_core_backend.go | 2 +- services/horizon/CHANGELOG.md | 31 +-- 8 files changed, 271 insertions(+), 125 deletions(-) diff --git a/go.mod b/go.mod index 0e55add07c..0e8d89dac2 100644 --- a/go.mod +++ b/go.mod @@ -56,7 +56,10 @@ require ( gopkg.in/tylerb/graceful.v1 v1.2.15 ) -require golang.org/x/sync v0.6.0 +require ( + github.com/cenkalti/backoff/v4 v4.2.1 + golang.org/x/sync v0.6.0 +) require ( cloud.google.com/go/compute v1.23.3 // indirect diff --git a/go.sum b/go.sum index 1e31cfdb08..ab7ad33230 100644 --- a/go.sum +++ b/go.sum @@ -82,6 +82,8 @@ github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENU github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/buger/goreplay v1.3.2 h1:MFAStZZCsHMPeN5xJ11rhUtV4ZctFRgzSHTfWSWOJsg= github.com/buger/goreplay v1.3.2/go.mod h1:EyAKHxJR6K6phd0NaoPETSDbJRB/ogIw3Y15UlSbVBM= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s= github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= diff --git a/historyarchive/archive_pool.go b/historyarchive/archive_pool.go index 4cb5483f63..259f8ff48a 100644 --- a/historyarchive/archive_pool.go +++ b/historyarchive/archive_pool.go @@ -5,15 +5,24 @@ package historyarchive import ( + "context" "math/rand" + "time" - "github.com/stellar/go/support/errors" + "github.com/pkg/errors" + log "github.com/stellar/go/support/log" "github.com/stellar/go/xdr" + + backoff "github.com/cenkalti/backoff/v4" ) -// A ArchivePool is just a collection of `ArchiveInterface`s so that we can +// An ArchivePool is just a collection of `ArchiveInterface`s so that we can // distribute requests fairly throughout the pool. -type ArchivePool []ArchiveInterface +type ArchivePool struct { + backoff backoff.BackOff + pool []ArchiveInterface + curr int +} // NewArchivePool tries connecting to each of the provided history archive URLs, // returning a pool of valid archives. @@ -21,39 +30,47 @@ type ArchivePool []ArchiveInterface // If none of the archives work, this returns the error message of the last // failed archive. Note that the errors for each individual archive are hard to // track if there's success overall. -func NewArchivePool(archiveURLs []string, opts ArchiveOptions) (ArchivePool, error) { +func NewArchivePool(archiveURLs []string, opts ArchiveOptions) (ArchiveInterface, error) { + return NewArchivePoolWithBackoff( + archiveURLs, + opts, + backoff.WithMaxRetries(backoff.NewConstantBackOff(250*time.Millisecond), 3), + ) +} + +func NewArchivePoolWithBackoff(archiveURLs []string, opts ArchiveOptions, strategy backoff.BackOff) (ArchiveInterface, error) { if len(archiveURLs) <= 0 { return nil, errors.New("No history archives provided") } - var lastErr error = nil + ap := ArchivePool{ + pool: make([]ArchiveInterface, 0, len(archiveURLs)), + backoff: strategy, + } + var lastErr error // Try connecting to all of the listed archives, but only store valid ones. - var validArchives ArchivePool for _, url := range archiveURLs { - archive, err := Connect( - url, - opts, - ) - + archive, err := Connect(url, opts) if err != nil { lastErr = errors.Wrapf(err, "Error connecting to history archive (%s)", url) continue } - validArchives = append(validArchives, archive) + ap.pool = append(ap.pool, archive) } - if len(validArchives) == 0 { + if len(ap.pool) == 0 { return nil, lastErr } - return validArchives, nil + ap.curr = rand.Intn(len(ap.pool)) // don't necessarily start at zero + return &ap, nil } -func (pa ArchivePool) GetStats() []ArchiveStats { +func (pa *ArchivePool) GetStats() []ArchiveStats { stats := []ArchiveStats{} - for _, archive := range pa { + for _, archive := range pa.pool { stats = append(stats, archive.GetStats()...) } return stats @@ -62,80 +79,178 @@ func (pa ArchivePool) GetStats() []ArchiveStats { // Ensure the pool conforms to the ArchiveInterface var _ ArchiveInterface = &ArchivePool{} -// Below are the ArchiveInterface method implementations. +// +// These are helpers to round-robin calls through archives. +// -func (pa ArchivePool) GetAnyArchive() ArchiveInterface { - return pa[rand.Intn(len(pa))] +// getNextArchive statefully round-robins through the pool +func (pa *ArchivePool) getNextArchive() ArchiveInterface { + // Round-robin through the archives + pa.curr = (pa.curr + 1) % len(pa.pool) + return pa.pool[pa.curr] } -func (pa ArchivePool) GetPathHAS(path string) (HistoryArchiveState, error) { - return pa.GetAnyArchive().GetPathHAS(path) +// runRoundRobin is a helper method that will run a particular action on every +// archive in the pool until it succeeds or the pool is exhausted (whichever +// comes first), repeating with a constant 500ms backoff. +func (pa *ArchivePool) runRoundRobin(runner func(ai ArchiveInterface) error) error { + return backoff.Retry(func() error { + var err error + ai := pa.getNextArchive() + if err = runner(ai); err == nil { + return nil + } + + if errors.Is(err, context.Canceled) || + errors.Is(err, context.DeadlineExceeded) { + return backoff.Permanent(err) + } + + // Intentionally avoid logging context errors + if stats := ai.GetStats(); len(stats) > 0 { + log.WithField("error", err).Warnf( + "Encountered an error with archive '%s'", + stats[0].GetBackendName()) + } + + return err + }, pa.backoff) +} + +// +// Below are the ArchiveInterface method implementations. +// + +func (pa *ArchivePool) GetPathHAS(path string) (HistoryArchiveState, error) { + has := HistoryArchiveState{} + err := pa.runRoundRobin(func(ai ArchiveInterface) error { + var innerErr error + has, innerErr = ai.GetPathHAS(path) + return innerErr + }) + return has, err } -func (pa ArchivePool) PutPathHAS(path string, has HistoryArchiveState, opts *CommandOptions) error { - return pa.GetAnyArchive().PutPathHAS(path, has, opts) +func (pa *ArchivePool) PutPathHAS(path string, has HistoryArchiveState, opts *CommandOptions) error { + return pa.runRoundRobin(func(ai ArchiveInterface) error { + return ai.PutPathHAS(path, has, opts) + }) } -func (pa ArchivePool) BucketExists(bucket Hash) (bool, error) { - return pa.GetAnyArchive().BucketExists(bucket) +func (pa *ArchivePool) BucketExists(bucket Hash) (bool, error) { + status := false + return status, pa.runRoundRobin(func(ai ArchiveInterface) error { + var err error + status, err = ai.BucketExists(bucket) + return err + }) } -func (pa ArchivePool) BucketSize(bucket Hash) (int64, error) { - return pa.GetAnyArchive().BucketSize(bucket) +func (pa *ArchivePool) BucketSize(bucket Hash) (int64, error) { + var bsize int64 + return bsize, pa.runRoundRobin(func(ai ArchiveInterface) error { + var err error + bsize, err = ai.BucketSize(bucket) + return err + }) } -func (pa ArchivePool) CategoryCheckpointExists(cat string, chk uint32) (bool, error) { - return pa.GetAnyArchive().CategoryCheckpointExists(cat, chk) +func (pa *ArchivePool) CategoryCheckpointExists(cat string, chk uint32) (bool, error) { + var ok bool + return ok, pa.runRoundRobin(func(ai ArchiveInterface) error { + var err error + ok, err = ai.CategoryCheckpointExists(cat, chk) + return err + }) } -func (pa ArchivePool) GetLedgerHeader(chk uint32) (xdr.LedgerHeaderHistoryEntry, error) { - return pa.GetAnyArchive().GetLedgerHeader(chk) +func (pa *ArchivePool) GetLedgerHeader(chk uint32) (xdr.LedgerHeaderHistoryEntry, error) { + var entry xdr.LedgerHeaderHistoryEntry + return entry, pa.runRoundRobin(func(ai ArchiveInterface) error { + var err error + entry, err = ai.GetLedgerHeader(chk) + return err + }) } -func (pa ArchivePool) GetRootHAS() (HistoryArchiveState, error) { - return pa.GetAnyArchive().GetRootHAS() +func (pa *ArchivePool) GetRootHAS() (HistoryArchiveState, error) { + var state HistoryArchiveState + return state, pa.runRoundRobin(func(ai ArchiveInterface) error { + var err error + state, err = ai.GetRootHAS() + return err + }) } -func (pa ArchivePool) GetLedgers(start, end uint32) (map[uint32]*Ledger, error) { - return pa.GetAnyArchive().GetLedgers(start, end) +func (pa *ArchivePool) GetLedgers(start, end uint32) (map[uint32]*Ledger, error) { + var dict map[uint32]*Ledger + + return dict, pa.runRoundRobin(func(ai ArchiveInterface) error { + var err error + dict, err = ai.GetLedgers(start, end) + return err + }) } -func (pa ArchivePool) GetCheckpointHAS(chk uint32) (HistoryArchiveState, error) { - return pa.GetAnyArchive().GetCheckpointHAS(chk) +func (pa *ArchivePool) GetCheckpointHAS(chk uint32) (HistoryArchiveState, error) { + var state HistoryArchiveState + return state, pa.runRoundRobin(func(ai ArchiveInterface) error { + var err error + state, err = ai.GetCheckpointHAS(chk) + return err + }) } -func (pa ArchivePool) PutCheckpointHAS(chk uint32, has HistoryArchiveState, opts *CommandOptions) error { - return pa.GetAnyArchive().PutCheckpointHAS(chk, has, opts) +func (pa *ArchivePool) PutCheckpointHAS(chk uint32, has HistoryArchiveState, opts *CommandOptions) error { + return pa.runRoundRobin(func(ai ArchiveInterface) error { + return ai.PutCheckpointHAS(chk, has, opts) + }) } -func (pa ArchivePool) PutRootHAS(has HistoryArchiveState, opts *CommandOptions) error { - return pa.GetAnyArchive().PutRootHAS(has, opts) +func (pa *ArchivePool) PutRootHAS(has HistoryArchiveState, opts *CommandOptions) error { + return pa.runRoundRobin(func(ai ArchiveInterface) error { + return ai.PutRootHAS(has, opts) + }) } -func (pa ArchivePool) ListBucket(dp DirPrefix) (chan string, chan error) { - return pa.GetAnyArchive().ListBucket(dp) +func (pa *ArchivePool) GetXdrStreamForHash(hash Hash) (*XdrStream, error) { + var stream *XdrStream + return stream, pa.runRoundRobin(func(ai ArchiveInterface) error { + var err error + stream, err = ai.GetXdrStreamForHash(hash) + return err + }) } -func (pa ArchivePool) ListAllBuckets() (chan string, chan error) { - return pa.GetAnyArchive().ListAllBuckets() +func (pa *ArchivePool) GetXdrStream(pth string) (*XdrStream, error) { + var stream *XdrStream + return stream, pa.runRoundRobin(func(ai ArchiveInterface) error { + var err error + stream, err = ai.GetXdrStream(pth) + return err + }) } -func (pa ArchivePool) ListAllBucketHashes() (chan Hash, chan error) { - return pa.GetAnyArchive().ListAllBucketHashes() +func (pa *ArchivePool) GetCheckpointManager() CheckpointManager { + return pa.getNextArchive().GetCheckpointManager() } -func (pa ArchivePool) ListCategoryCheckpoints(cat string, pth string) (chan uint32, chan error) { - return pa.GetAnyArchive().ListCategoryCheckpoints(cat, pth) +// +// The channel-based methods do not have automatic retries. +// + +func (pa *ArchivePool) ListBucket(dp DirPrefix) (chan string, chan error) { + return pa.getNextArchive().ListBucket(dp) } -func (pa ArchivePool) GetXdrStreamForHash(hash Hash) (*XdrStream, error) { - return pa.GetAnyArchive().GetXdrStreamForHash(hash) +func (pa *ArchivePool) ListAllBuckets() (chan string, chan error) { + return pa.getNextArchive().ListAllBuckets() } -func (pa ArchivePool) GetXdrStream(pth string) (*XdrStream, error) { - return pa.GetAnyArchive().GetXdrStream(pth) +func (pa *ArchivePool) ListAllBucketHashes() (chan Hash, chan error) { + return pa.getNextArchive().ListAllBucketHashes() } -func (pa ArchivePool) GetCheckpointManager() CheckpointManager { - return pa.GetAnyArchive().GetCheckpointManager() +func (pa *ArchivePool) ListCategoryCheckpoints(cat string, pth string) (chan uint32, chan error) { + return pa.getNextArchive().ListCategoryCheckpoints(cat, pth) } diff --git a/historyarchive/archive_pool_test.go b/historyarchive/archive_pool_test.go index 9f51fd75e3..2562b3ae13 100644 --- a/historyarchive/archive_pool_test.go +++ b/historyarchive/archive_pool_test.go @@ -5,12 +5,16 @@ package historyarchive import ( + "fmt" "net/http" "net/http/httptest" + "strings" "testing" + "time" "github.com/stellar/go/support/storage" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestConfiguresHttpUserAgentForArchivePool(t *testing.T) { @@ -30,10 +34,77 @@ func TestConfiguresHttpUserAgentForArchivePool(t *testing.T) { } archivePool, err := NewArchivePool(archiveURLs, archiveOptions) - assert.NoError(t, err) + require.NoError(t, err) ok, err := archivePool.BucketExists(EmptyXdrArrayHash()) - assert.True(t, ok) - assert.NoError(t, err) - assert.Equal(t, userAgent, "uatest") + require.True(t, ok) + require.NoError(t, err) + require.Equal(t, userAgent, "uatest") +} + +func TestArchivePoolRoundRobin(t *testing.T) { + accesses := []string{} + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + parts := strings.Split(r.URL.Path, "/") + accesses = append(accesses, parts[2]) + w.Write([]byte("boo")) + })) + + pool, err := NewArchivePool([]string{ + fmt.Sprintf("%s/%s/%s", server.URL, "fake-archive", "1"), + fmt.Sprintf("%s/%s/%s", server.URL, "fake-archive", "2"), + fmt.Sprintf("%s/%s/%s", server.URL, "fake-archive", "3"), + }, ArchiveOptions{}) + require.NoError(t, err) + + _, err = pool.BucketExists(EmptyXdrArrayHash()) + require.NoError(t, err) + _, err = pool.BucketExists(EmptyXdrArrayHash()) + require.NoError(t, err) + _, err = pool.BucketExists(EmptyXdrArrayHash()) + require.NoError(t, err) + _, err = pool.BucketExists(EmptyXdrArrayHash()) + require.NoError(t, err) + + assert.Contains(t, accesses, "1") + assert.Contains(t, accesses, "2") + assert.Contains(t, accesses, "3") + assert.Len(t, accesses, 4) +} + +func TestArchivePoolCycles(t *testing.T) { + accesses := []string{} + requestTimes := []time.Time{} + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + parts := strings.Split(r.URL.Path, "/") + accesses = append(accesses, parts[2]) + requestTimes = append(requestTimes, time.Now()) + w.Write([]byte("failure")) + })) + + pool, err := NewArchivePool([]string{ + fmt.Sprintf("%s/%s/%s", server.URL, "fake-archive", "1"), + fmt.Sprintf("%s/%s/%s", server.URL, "fake-archive", "2"), + fmt.Sprintf("%s/%s/%s", server.URL, "fake-archive", "3"), + }, ArchiveOptions{}) + require.NoError(t, err) + + // + // A single access should retry thrice with constant back-off, so we check + // the distinct accesses and an appropriate delay. + // + _, err = pool.GetPathHAS("path") + require.Error(t, err) + + require.Len(t, accesses, 4) + assert.Contains(t, accesses, "1") + assert.Contains(t, accesses, "2") + assert.Contains(t, accesses, "3") + + assert.GreaterOrEqualf(t, + requestTimes[len(requestTimes)-1].Sub(requestTimes[0]), + 740*time.Millisecond, // some leeway + "") } diff --git a/ingest/checkpoint_change_reader.go b/ingest/checkpoint_change_reader.go index 37e5e994e1..d1365740c6 100644 --- a/ingest/checkpoint_change_reader.go +++ b/ingest/checkpoint_change_reader.go @@ -71,8 +71,6 @@ const ( // bucket in a single run. This is done to allow preloading keys from // temp set. preloadedEntries = 20000 - - sleepDuration = time.Second ) // NewCheckpointChangeReader constructs a new CheckpointChangeReader instance. @@ -126,21 +124,7 @@ func NewCheckpointChangeReader( } func (r *CheckpointChangeReader) bucketExists(hash historyarchive.Hash) (bool, error) { - duration := sleepDuration - var exists bool - var err error - for attempts := 0; ; attempts++ { - exists, err = r.archive.BucketExists(hash) - if err == nil { - return exists, nil - } - if attempts >= maxStreamRetries { - break - } - r.sleep(duration) - duration *= 2 - } - return exists, err + return r.archive.BucketExists(hash) } // streamBuckets is internal method that streams buckets from the given HAS. diff --git a/ingest/checkpoint_change_reader_test.go b/ingest/checkpoint_change_reader_test.go index 08730ddd0f..958f3199ac 100644 --- a/ingest/checkpoint_change_reader_test.go +++ b/ingest/checkpoint_change_reader_test.go @@ -311,47 +311,15 @@ func (s *BucketExistsTestSuite) TearDownTest() { s.mockArchive.AssertExpectations(s.T()) } -func (s *BucketExistsTestSuite) testBucketExists( - numErrors int, expectedSleeps []time.Duration, -) { +func (s *BucketExistsTestSuite) TestBucketExists() { for _, expected := range []bool{true, false} { hash := historyarchive.Hash{1, 2, 3} - if numErrors > 0 { - s.mockArchive.On("BucketExists", hash). - Return(true, errors.New("transient error")).Times(numErrors) - } s.mockArchive.On("BucketExists", hash). Return(expected, nil).Once() - s.expectedSleeps = expectedSleeps exists, err := s.reader.bucketExists(hash) s.Assert().Equal(expected, exists) s.Assert().NoError(err) - s.Assert().Empty(s.expectedSleeps) - } -} - -func (s *BucketExistsTestSuite) TestSucceedsFirstTime() { - s.testBucketExists(0, []time.Duration{}) -} - -func (s *BucketExistsTestSuite) TestSucceedsSecondTime() { - s.testBucketExists(1, []time.Duration{time.Second}) -} - -func (s *BucketExistsTestSuite) TestSucceedsThirdime() { - s.testBucketExists(2, []time.Duration{time.Second, 2 * time.Second}) -} - -func (s *BucketExistsTestSuite) TestFailsAfterThirdTime() { - hash := historyarchive.Hash{1, 2, 3} - s.mockArchive.On("BucketExists", hash). - Return(true, errors.New("transient error")).Times(4) - s.expectedSleeps = []time.Duration{ - time.Second, 2 * time.Second, 4 * time.Second, } - _, err := s.reader.bucketExists(hash) - s.Assert().EqualError(err, "transient error") - s.Assert().Empty(s.expectedSleeps) } func TestReadBucketEntryTestSuite(t *testing.T) { diff --git a/ingest/ledgerbackend/captive_core_backend.go b/ingest/ledgerbackend/captive_core_backend.go index bc29acb54b..50e933bb6a 100644 --- a/ingest/ledgerbackend/captive_core_backend.go +++ b/ingest/ledgerbackend/captive_core_backend.go @@ -191,7 +191,7 @@ func NewCaptive(config CaptiveCoreConfig) (*CaptiveStellarCore, error) { } c := &CaptiveStellarCore{ - archive: &archivePool, + archive: archivePool, ledgerHashStore: config.LedgerHashStore, useDB: config.UseDB, cancel: cancel, diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 6bb186b393..7bf15774f1 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -3,19 +3,23 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## unreleased +## Unreleased ### Added -- New `db_error_total` metrics key with labels `ctx_error`, `db_error`, and `db_error_extra` ([5225](https://github.com/stellar/go/pull/5225)). +- New `db_error_total` metrics key with labels `ctx_error`, `db_error`, and `db_error_extra` ([5225](https://github.com/stellar/go/pull/5225)). + +### Fixed +- History archive access is more effective when you pass list of URLs to Horizon: they will now be accessed in a round-robin fashion, use alternative archives on errors, and intelligently back off ([5224](https://github.com/stellar/go/pull/5224)) + ## 2.28.3 ### Fixed -- Fix claimable_balance_claimants subquery in GetClaimableBalances() [5207](https://github.com/stellar/go/pull/5207) +- Fix claimable_balance_claimants subquery in GetClaimableBalances() ([5207](https://github.com/stellar/go/pull/5207)) ### Added - New optional config `SKIP_TXMETA` ([5189](https://github.com/stellar/go/issues/5189)). Defaults to `FALSE`, when `TRUE` the following will occur: - * history_transactions.tx_meta column will have serialized xdr that equates to empty for any protocol version, such as for `xdr.TransactionMeta.V3`, `Operations`, `TxChangesAfter`, `TxChangesBefore` will be empty arrays and `SorobanMeta` will be nil. + * history_transactions.tx_meta column will have serialized xdr that equates to empty for any protocol version, such as for `xdr.TransactionMeta.V3`, `Operations`, `TxChangesAfter`, `TxChangesBefore` will be empty arrays and `SorobanMeta` will be nil. ### Breaking Changes - Removed `DISABLE_SOROBAN_INGEST` configuration parameter, use the new `SKIP_TXMETA` parameter instead. @@ -23,15 +27,14 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ## 2.28.2 ### Fixed -- History archive caching would cause file corruption in certain environments [5197](https://github.com/stellar/go/pull/5197) -- Server error in claimable balance API when claimant, asset and cursor query params are supplied [5200](https://github.com/stellar/go/pull/5200) +- History archive caching would cause file corruption in certain environments ([5197](https://github.com/stellar/go/pull/5197)) +- Server error in claimable balance API when claimant, asset and cursor query params are supplied ([5200](https://github.com/stellar/go/pull/5200)) ## 2.28.1 ### Fixed - Submitting transaction with a future gapped sequence number greater than 1 past current source account sequence, may result in delayed 60s timeout response, rather than expected HTTP 400 error response with `result_codes: {transaction: "tx_bad_seq"}` ([5191](https://github.com/stellar/go/pull/5191)) - ## 2.28.0 ### Fixed @@ -51,21 +54,21 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). * API `Operation` model for `InvokeHostFunctionOp` type, will have empty `asset_balance_changes` ### Breaking Changes -- Deprecation of legacy, non-captive core ingestion([5158](https://github.com/stellar/go/pull/5158)): +- Deprecation of legacy, non-captive core ingestion([5158](https://github.com/stellar/go/pull/5158)): * removed configuration flags `--stellar-core-url-db`, `--cursor-name` `--skip-cursor-update`, they are no longer usable. * removed automatic updating of core cursor from ingestion background processing.
- **Note** for upgrading on existing horizon deployments - Since horizon will no longer maintain advancement of this cursor on core, it may require manual removal of the cursor from the core process that your horizon was using for captive core, otherwise that core process may un-necessarily retain older data in buckets on disk up to the last cursor ledger sequence set by prior horizon release. - + **Note** for upgrading on existing horizon deployments - Since horizon will no longer maintain advancement of this cursor on core, it may require manual removal of the cursor from the core process that your horizon was using for captive core, otherwise that core process may un-necessarily retain older data in buckets on disk up to the last cursor ledger sequence set by prior horizon release. + The captive core process to check and verify presence of cursor usage is determined by the horizon deployment, if `NETWORK` is present, or `STELLAR_CORE_URL` is present or `CAPTIVE-CORE-HTTP-PORT` is present and set to non-zero value, or `CAPTIVE-CORE_CONFIG_PATH` is used and the toml has `HTTP_PORT` set to non-zero and `PUBLIC_HTTP_PORT` is not set to false, then it is recommended to perform the following preventative measure on the machine hosting horizon after upgraded to 2.28.0 and process restarted: ``` $ curl http:///getcursor # If there are no cursors reported, done, no need for any action - # If any horizon cursors exist they need to be dropped by id. - # By default horizon sets cursor id to "HORIZON" but if it was customized + # If any horizon cursors exist they need to be dropped by id. + # By default horizon sets cursor id to "HORIZON" but if it was customized # using the --cursor-name flag the id might be different $ curl http:///dropcursor?id= - ``` - + ``` + ## 2.27.0 From 6fe5270b6f0b1582f6353df93c2dc2a12157cfec Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 11 Mar 2024 09:34:40 +0000 Subject: [PATCH 070/234] services/horizon/internal/httpx: Add prometheus metrics for requests in flight and requests received (#5240) --- services/horizon/internal/httpx/middleware.go | 15 ++++++++++++--- services/horizon/internal/httpx/server.go | 17 +++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/services/horizon/internal/httpx/middleware.go b/services/horizon/internal/httpx/middleware.go index cdcd7f4e3c..eaa49634a5 100644 --- a/services/horizon/internal/httpx/middleware.go +++ b/services/horizon/internal/httpx/middleware.go @@ -78,11 +78,21 @@ func loggerMiddleware(serverMetrics *ServerMetrics) func(next http.Handler) http // is reset before sending the first event no Content-Type header is sent in a response. acceptHeader := r.Header.Get("Accept") streaming := strings.Contains(acceptHeader, render.MimeEventStream) + route := supportHttp.GetChiRoutePattern(r) + + requestLabels := prometheus.Labels{ + "route": route, + "streaming": strconv.FormatBool(streaming), + "method": r.Method, + } + serverMetrics.RequestsInFlightGauge.With(requestLabels).Inc() + defer serverMetrics.RequestsInFlightGauge.With(requestLabels).Dec() + serverMetrics.RequestsReceivedCounter.With(requestLabels).Inc() then := time.Now() next.ServeHTTP(mw, r.WithContext(ctx)) duration := time.Since(then) - logEndOfRequest(ctx, r, serverMetrics.RequestDurationSummary, duration, mw, streaming) + logEndOfRequest(ctx, r, route, serverMetrics.RequestDurationSummary, duration, mw, streaming) }) } } @@ -129,8 +139,7 @@ func getClientData(r *http.Request, headerName string) string { return value } -func logEndOfRequest(ctx context.Context, r *http.Request, requestDurationSummary *prometheus.SummaryVec, duration time.Duration, mw middleware.WrapResponseWriter, streaming bool) { - route := supportHttp.GetChiRoutePattern(r) +func logEndOfRequest(ctx context.Context, r *http.Request, route string, requestDurationSummary *prometheus.SummaryVec, duration time.Duration, mw middleware.WrapResponseWriter, streaming bool) { referer := r.Referer() if referer == "" { diff --git a/services/horizon/internal/httpx/server.go b/services/horizon/internal/httpx/server.go index 65058c0ea5..262d51578d 100644 --- a/services/horizon/internal/httpx/server.go +++ b/services/horizon/internal/httpx/server.go @@ -23,6 +23,8 @@ import ( type ServerMetrics struct { RequestDurationSummary *prometheus.SummaryVec ReplicaLagErrorsCounter prometheus.Counter + RequestsInFlightGauge *prometheus.GaugeVec + RequestsReceivedCounter *prometheus.CounterVec } type TLSConfig struct { @@ -71,6 +73,19 @@ func NewServer(serverConfig ServerConfig, routerConfig RouterConfig, ledgerState }, []string{"status", "route", "streaming", "method"}, ), + RequestsInFlightGauge: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Namespace: "horizon", Subsystem: "http", Name: "requests_in_flight", + Help: "HTTP requests in flight", + }, + []string{"route", "streaming", "method"}, + ), + RequestsReceivedCounter: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "horizon", Subsystem: "http", Name: "requests_received", + }, + []string{"route", "streaming", "method"}, + ), ReplicaLagErrorsCounter: prometheus.NewCounter( prometheus.CounterOpts{ Namespace: "horizon", Subsystem: "http", Name: "replica_lag_errors_count", @@ -109,6 +124,8 @@ func NewServer(serverConfig ServerConfig, routerConfig RouterConfig, ledgerState func (s *Server) RegisterMetrics(registry *prometheus.Registry) { registry.MustRegister(s.Metrics.RequestDurationSummary) registry.MustRegister(s.Metrics.ReplicaLagErrorsCounter) + registry.MustRegister(s.Metrics.RequestsInFlightGauge) + registry.MustRegister(s.Metrics.RequestsReceivedCounter) } func (s *Server) Serve() error { From cabbe8419ee02b599f0894d7e67f0fcd8b9e2bd8 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Wed, 6 Mar 2024 14:43:17 -0800 Subject: [PATCH 071/234] Bump go version to 1.22 (#5232) Co-authored-by: tamirms --- .github/workflows/go.yml | 6 +++--- .github/workflows/horizon-release.yml | 2 +- .github/workflows/horizon.yml | 4 ++-- exp/services/recoverysigner/docker/Dockerfile | 2 +- exp/services/webauth/docker/Dockerfile | 2 +- go.mod | 4 +++- go.sum | 21 +++++++++++++++++++ services/friendbot/docker/Dockerfile | 2 +- services/horizon/Makefile | 2 +- services/horizon/docker/Dockerfile.dev | 2 +- .../horizon/docker/verify-range/Dockerfile | 1 + .../horizon/docker/verify-range/dependencies | 1 - .../scripts/check_release_hash/Dockerfile | 2 +- 13 files changed, 37 insertions(+), 14 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f1fd33028d..31bd95b8ff 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: os: [ubuntu-22.04] - go: ["1.21"] + go: ["1.22.1"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 @@ -38,7 +38,7 @@ jobs: strategy: matrix: os: [ubuntu-22.04] - go: ["1.20", "1.21"] + go: ["1.21", "1.22"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 @@ -56,7 +56,7 @@ jobs: strategy: matrix: os: [ubuntu-22.04] - go: ["1.19", "1.20"] + go: ["1.21", "1.22"] pg: [12] runs-on: ${{ matrix.os }} services: diff --git a/.github/workflows/horizon-release.yml b/.github/workflows/horizon-release.yml index f8dda0ceac..3977ab85d3 100644 --- a/.github/workflows/horizon-release.yml +++ b/.github/workflows/horizon-release.yml @@ -22,7 +22,7 @@ jobs: - uses: ./.github/actions/setup-go with: - go-version: "1.20" + go-version: "1.22" - name: Check dependencies run: ./gomod.sh diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index bf9cae7246..3f95b9e24a 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [ubuntu-20.04, ubuntu-22.04] - go: ["1.20", "1.21"] + go: ["1.21", "1.22"] pg: [12] ingestion-backend: [captive-core, captive-core-remote-storage] protocol-version: [19, 20] @@ -143,7 +143,7 @@ jobs: - name: Build and test the Verify Range Docker image run: | - docker build -f services/horizon/docker/verify-range/Dockerfile -t stellar/horizon-verify-range services/horizon/docker/verify-range/ + docker build --build-arg="GO_VERSION=$(sed -En 's/^toolchain[[:space:]]+go([[:digit:].]+)$/\1/p' go.mod)" -f services/horizon/docker/verify-range/Dockerfile -t stellar/horizon-verify-range services/horizon/docker/verify-range/ # Any range should do for basic testing, this range was chosen pretty early in history so that it only takes a few mins to run docker run -e BRANCH=$(git rev-parse HEAD) -e FROM=10000063 -e TO=10000127 stellar/horizon-verify-range diff --git a/exp/services/recoverysigner/docker/Dockerfile b/exp/services/recoverysigner/docker/Dockerfile index 8cd9a72ae6..ff5c14e731 100644 --- a/exp/services/recoverysigner/docker/Dockerfile +++ b/exp/services/recoverysigner/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.20-bullseye as build +FROM golang:1.22-bullseye as build ADD . /src/recoverysigner WORKDIR /src/recoverysigner diff --git a/exp/services/webauth/docker/Dockerfile b/exp/services/webauth/docker/Dockerfile index c6bc287d5b..64cff400aa 100644 --- a/exp/services/webauth/docker/Dockerfile +++ b/exp/services/webauth/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.20-bullseye as build +FROM golang:1.22-bullseye as build ADD . /src/webauth WORKDIR /src/webauth diff --git a/go.mod b/go.mod index 0e8d89dac2..65d73fa328 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/stellar/go -go 1.20 +go 1.22 + +toolchain go1.22.1 require ( cloud.google.com/go/firestore v1.14.0 // indirect diff --git a/go.sum b/go.sum index ab7ad33230..0fe77e5201 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,7 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/jrpc2 v1.1.0 h1:SgpJf0v1rVCZx68+4APv6dgsTFsIHlpgFD1NlQAWA0A= github.com/creachadair/jrpc2 v1.1.0/go.mod h1:5jN7MKwsm8qvgfTsTzLX3JIfidsAkZ1c8DZSQmp+g38= @@ -121,6 +122,7 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU= github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -129,6 +131,7 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= @@ -156,10 +159,13 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= +github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= github.com/gobuffalo/packd v1.0.2 h1:Yg523YqnOxGIWCp69W12yYBKsoChwI7mtu6ceM9Bwfw= github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8= github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= +github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -217,6 +223,7 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -283,6 +290,7 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= +github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= @@ -293,6 +301,7 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -309,13 +318,19 @@ github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3v github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE= github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM= github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= +github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= +github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/mattbaird/elastigo v0.0.0-20170123220020-2fe47fd29e4b/go.mod h1:5MWrJXKRQyhQdUCF+vu6U5c4nQpg70vW3eHaU0/AYbU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -332,6 +347,7 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= @@ -351,6 +367,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -470,6 +487,7 @@ go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znn go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -718,6 +736,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -826,6 +845,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/djherbis/atime.v1 v1.0.0 h1:eMRqB/JrLKocla2PBPKgQYg/p5UG4L6AUAs92aP7F60= gopkg.in/djherbis/atime.v1 v1.0.0/go.mod h1:hQIUStKmJfvf7xdh/wtK84qe+DsTV5LnA9lzxxtPpJ8= gopkg.in/djherbis/stream.v1 v1.3.1 h1:uGfmsOY1qqMjQQphhRBSGLyA9qumJ56exkRu9ASTjCw= @@ -852,6 +872,7 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/services/friendbot/docker/Dockerfile b/services/friendbot/docker/Dockerfile index dc1c74b93f..764fa5e276 100644 --- a/services/friendbot/docker/Dockerfile +++ b/services/friendbot/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.20-bullseye as build +FROM golang:1.22-bullseye as build ADD . /src/friendbot WORKDIR /src/friendbot diff --git a/services/horizon/Makefile b/services/horizon/Makefile index 9c5a3a8ddf..0789453373 100644 --- a/services/horizon/Makefile +++ b/services/horizon/Makefile @@ -11,7 +11,7 @@ binary-build: --pull always \ --env CGO_ENABLED=0 \ --env GOFLAGS="-ldflags=-X=github.com/stellar/go/support/app.version=$(VERSION_STRING)" \ - golang:1.20-bullseye \ + golang:1.22-bullseye \ /bin/bash -c '\ git config --global --add safe.directory /go/src/github.com/stellar/go && \ cd /go/src/github.com/stellar/go && \ diff --git a/services/horizon/docker/Dockerfile.dev b/services/horizon/docker/Dockerfile.dev index 1d1be8d688..5cef8d89d1 100644 --- a/services/horizon/docker/Dockerfile.dev +++ b/services/horizon/docker/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM golang:1.20-bullseye AS builder +FROM golang:1.22-bullseye AS builder ARG VERSION="devel" WORKDIR /go/src/github.com/stellar/go diff --git a/services/horizon/docker/verify-range/Dockerfile b/services/horizon/docker/verify-range/Dockerfile index 0143dd2cfa..6323870f38 100644 --- a/services/horizon/docker/verify-range/Dockerfile +++ b/services/horizon/docker/verify-range/Dockerfile @@ -1,5 +1,6 @@ FROM ubuntu:22.04 +ARG GO_VERSION ARG STELLAR_CORE_VERSION ENV STELLAR_CORE_VERSION=${STELLAR_CORE_VERSION:-*} # to remove tzdata interactive flow diff --git a/services/horizon/docker/verify-range/dependencies b/services/horizon/docker/verify-range/dependencies index 910ee9cf21..3eacede44b 100644 --- a/services/horizon/docker/verify-range/dependencies +++ b/services/horizon/docker/verify-range/dependencies @@ -19,7 +19,6 @@ cd stellar-go git config --add remote.origin.fetch "+refs/pull/*/head:refs/remotes/origin/pull/*" git fetch --force --quiet origin -GO_VERSION=$(sed -En 's/^go[[:space:]]+([[:digit:].]+)$/\1/p' go.mod) wget -q https://dl.google.com/go/go${GO_VERSION}.linux-amd64.tar.gz tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz rm -f go${GO_VERSION}.linux-amd64.tar.gz diff --git a/services/horizon/internal/scripts/check_release_hash/Dockerfile b/services/horizon/internal/scripts/check_release_hash/Dockerfile index 6a054aab34..08f818dad3 100644 --- a/services/horizon/internal/scripts/check_release_hash/Dockerfile +++ b/services/horizon/internal/scripts/check_release_hash/Dockerfile @@ -1,5 +1,5 @@ # Change to Go version used in CI or rebuild with --build-arg. -ARG GO_IMAGE=golang:1.20-bullseye +ARG GO_IMAGE=golang:1.22-bullseye FROM $GO_IMAGE WORKDIR /go/src/github.com/stellar/go From 3fd44ca553f153840150e812fc41318abb3c2d9f Mon Sep 17 00:00:00 2001 From: shawn Date: Wed, 6 Mar 2024 15:26:13 -0800 Subject: [PATCH 072/234] services/horizon: return null txmeta in api model when SKIP_TXMETA enabled (#5228) --- clients/horizonclient/main_test.go | 35 ++++++++++- protocols/horizon/README.md | 2 + protocols/horizon/main.go | 2 +- services/horizon/CHANGELOG.md | 2 + .../horizon/internal/actions/operation.go | 8 ++- .../internal/actions/submit_transaction.go | 2 + .../horizon/internal/actions/transaction.go | 6 +- services/horizon/internal/app.go | 1 + services/horizon/internal/httpx/router.go | 20 +++++-- .../internal/integration/transaction_test.go | 17 +----- .../internal/resourceadapter/operations.go | 7 ++- .../resourceadapter/operations_test.go | 21 +++---- .../internal/resourceadapter/transaction.go | 7 ++- .../resourceadapter/transaction_test.go | 58 +++++++++++++++---- 14 files changed, 136 insertions(+), 52 deletions(-) diff --git a/clients/horizonclient/main_test.go b/clients/horizonclient/main_test.go index 2149bbbf29..f7b4ba0788 100644 --- a/clients/horizonclient/main_test.go +++ b/clients/horizonclient/main_test.go @@ -3,6 +3,7 @@ package horizonclient import ( "fmt" "net/http" + "strings" "testing" "time" @@ -905,6 +906,25 @@ func TestSubmitTransactionRequest(t *testing.T) { _, err = client.SubmitTransaction(tx) assert.NoError(t, err) + // verify submit parses correctly when result_meta_xdr absent when skip_meta=true + hmock.On( + "POST", + "https://localhost/transactions", + ).Return(func(request *http.Request) (*http.Response, error) { + val := request.FormValue("tx") + assert.Equal(t, val, txXdr) + return httpmock.NewStringResponse(http.StatusOK, strings.Replace(txDetailResponse, "", "", 1)), nil + }) + + hmock.On( + "GET", + "https://localhost/accounts/GACTJ4ZFCDZMD2UFR4R7MZOWYBCF6HBP65YKCUT37MUQFPJLDLJ3N5D2/data/config.memo_required", + ).ReturnString(404, notFoundResponse) + + theTx, err := client.SubmitTransaction(tx) + assert.NoError(t, err) + assert.Empty(t, theTx.ResultMetaXdr) + // memo required - does not submit transaction hmock.On( "GET", @@ -1388,7 +1408,7 @@ func TestTransactionsRequest(t *testing.T) { hmock.On( "GET", "https://localhost/transactions/5131aed266a639a6eb4802a92fba310454e711ded830ed899745b9e777d7110c", - ).ReturnString(200, txDetailResponse) + ).ReturnString(200, strings.Replace(txDetailResponse, "", "result_meta_xdr: AAAAAQAAAAIAAAADAAavdgAAAAAAAAAAtoYrQZHbnPLAFsF4YB88J5VSg0/piQNHm0SL9l0HW1EAAAAXSHbnnAAGr3UAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAavdgAAAAAAAAAAtoYrQZHbnPLAFsF4YB88J5VSg0/piQNHm0SL9l0HW1EAAAAXSHbnnAAGr3UAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAABAAAAAMABq9zAAAAAAAAAADMSEvcRKXsaUNna++Hy7gWm/CfqTjEA7xoGypfrFGUHAAAAAUQ/z+cAABeBgAASuQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEABq92AAAAAAAAAADMSEvcRKXsaUNna++Hy7gWm/CfqTjEA7xoGypfrFGUHAAAAAcXjracAABeBgAASuQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAMABq92AAAAAAAAAAC2hitBkduc8sAWwXhgHzwnlVKDT+mJA0ebRIv2XQdbUQAAABdIduecAAavdQAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEABq92AAAAAAAAAAC2hitBkduc8sAWwXhgHzwnlVKDT+mJA0ebRIv2XQdbUQAAABVB53CcAAavdQAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", 1)) record, err := client.TransactionDetail(txHash) if assert.NoError(t, err) { @@ -1397,6 +1417,17 @@ func TestTransactionsRequest(t *testing.T) { assert.Equal(t, record.Hash, "5131aed266a639a6eb4802a92fba310454e711ded830ed899745b9e777d7110c") assert.Equal(t, record.Memo, "2A1V6J5703G47XHY") } + + // transaction detail when skip meta enabled and result_meta_xdr is absent + hmock.On( + "GET", + "https://localhost/transactions/5131aed266a639a6eb4802a92fba310454e711ded830ed899745b9e777d7110c", + ).ReturnString(200, strings.Replace(txDetailResponse, "", "", 1)) + + record, err = client.TransactionDetail(txHash) + if assert.NoError(t, err) { + assert.Empty(t, record.ResultMetaXdr) + } } func TestOrderBookRequest(t *testing.T) { @@ -2492,7 +2523,7 @@ var txDetailResponse = `{ "operation_count": 1, "envelope_xdr": "AAAAALaGK0GR25zywBbBeGAfPCeVUoNP6YkDR5tEi/ZdB1tRAAAAZAAGr3UAAAABAAAAAAAAAAEAAAAQMkExVjZKNTcwM0c0N1hIWQAAAAEAAAABAAAAALaGK0GR25zywBbBeGAfPCeVUoNP6YkDR5tEi/ZdB1tRAAAAAQAAAADMSEvcRKXsaUNna++Hy7gWm/CfqTjEA7xoGypfrFGUHAAAAAAAAAACBo93AAAAAAAAAAABXQdbUQAAAECQ5m6ZHsv8/Gd/aRJ2EMLurJMxFynT7KbD51T7gD91Gqp/fzsRHilSGoVSw5ztmtJb2LP7o3bQbiZynQiJPl8C", "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=", - "result_meta_xdr": "AAAAAQAAAAIAAAADAAavdgAAAAAAAAAAtoYrQZHbnPLAFsF4YB88J5VSg0/piQNHm0SL9l0HW1EAAAAXSHbnnAAGr3UAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAavdgAAAAAAAAAAtoYrQZHbnPLAFsF4YB88J5VSg0/piQNHm0SL9l0HW1EAAAAXSHbnnAAGr3UAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAABAAAAAMABq9zAAAAAAAAAADMSEvcRKXsaUNna++Hy7gWm/CfqTjEA7xoGypfrFGUHAAAAAUQ/z+cAABeBgAASuQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEABq92AAAAAAAAAADMSEvcRKXsaUNna++Hy7gWm/CfqTjEA7xoGypfrFGUHAAAAAcXjracAABeBgAASuQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAMABq92AAAAAAAAAAC2hitBkduc8sAWwXhgHzwnlVKDT+mJA0ebRIv2XQdbUQAAABdIduecAAavdQAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEABq92AAAAAAAAAAC2hitBkduc8sAWwXhgHzwnlVKDT+mJA0ebRIv2XQdbUQAAABVB53CcAAavdQAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", + "result_meta_xdr": "", "fee_meta_xdr": "AAAAAgAAAAMABq91AAAAAAAAAAC2hitBkduc8sAWwXhgHzwnlVKDT+mJA0ebRIv2XQdbUQAAABdIdugAAAavdQAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEABq92AAAAAAAAAAC2hitBkduc8sAWwXhgHzwnlVKDT+mJA0ebRIv2XQdbUQAAABdIduecAAavdQAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", "memo_type": "text", "signatures": [ diff --git a/protocols/horizon/README.md b/protocols/horizon/README.md index c6f7ba2654..54b9ed1449 100644 --- a/protocols/horizon/README.md +++ b/protocols/horizon/README.md @@ -17,6 +17,8 @@ For each new version we will only track changes from the previous version. #### Changes +* In ["Transaction"](https://developers.stellar.org/api/horizon/resources/transactions/object), +`result_meta_xdr` field is [now nullable](https://github.com/stellar/go/pull/5228), and will be `null` when Horizon has `SKIP_TXMETA=true` set, otherwise if Horizon is configured with `SKIP_TXMETA=false` which is default, then `result_meta_xdr` will be the same value of base64 encoded xdr. * Operations responses may include a `transaction` field which represents the transaction that created the operation. ### 0.15.0 diff --git a/protocols/horizon/main.go b/protocols/horizon/main.go index 08da47b7b4..bdec98ba0b 100644 --- a/protocols/horizon/main.go +++ b/protocols/horizon/main.go @@ -518,7 +518,7 @@ type Transaction struct { OperationCount int32 `json:"operation_count"` EnvelopeXdr string `json:"envelope_xdr"` ResultXdr string `json:"result_xdr"` - ResultMetaXdr string `json:"result_meta_xdr"` + ResultMetaXdr string `json:"result_meta_xdr,omitempty"` FeeMetaXdr string `json:"fee_meta_xdr"` MemoType string `json:"memo_type"` MemoBytes string `json:"memo_bytes,omitempty"` diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 7bf15774f1..b036851710 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -11,6 +11,8 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - History archive access is more effective when you pass list of URLs to Horizon: they will now be accessed in a round-robin fashion, use alternative archives on errors, and intelligently back off ([5224](https://github.com/stellar/go/pull/5224)) +### Breaking Changes +- The Horizon API Transaction resource field in json `result_meta_xdr` is now optional and Horizon API will not emit the field when Horizon has been configured with `SKIP_TXMETA=true`, effectively null, otherwise if Horizon is configured with `SKIP_TXMETA=false` which is default, then the API Transaction field `result_meta_xdr` will remain present and populated with base64 encoded xdr [5228](https://github.com/stellar/go/pull/5228). ## 2.28.3 diff --git a/services/horizon/internal/actions/operation.go b/services/horizon/internal/actions/operation.go index 670f592b57..f59191aee0 100644 --- a/services/horizon/internal/actions/operation.go +++ b/services/horizon/internal/actions/operation.go @@ -65,6 +65,7 @@ func (qp OperationsQuery) Validate() error { type GetOperationsHandler struct { LedgerState *ledger.State OnlyPayments bool + SkipTxMeta bool } // GetResourcePage returns a page of operations. @@ -126,12 +127,13 @@ func (handler GetOperationsHandler) GetResourcePage(w HeaderWriter, r *http.Requ return nil, err } - return buildOperationsPage(ctx, historyQ, ops, txs, qp.IncludeTransactions()) + return buildOperationsPage(ctx, historyQ, ops, txs, qp.IncludeTransactions(), handler.SkipTxMeta) } // GetOperationByIDHandler is the action handler for all end-points returning a list of operations. type GetOperationByIDHandler struct { LedgerState *ledger.State + SkipTxMeta bool } // OperationQuery query struct for operation/id end-point @@ -182,10 +184,11 @@ func (handler GetOperationByIDHandler) GetResource(w HeaderWriter, r *http.Reque op.TransactionHash, tx, ledger, + handler.SkipTxMeta, ) } -func buildOperationsPage(ctx context.Context, historyQ *history.Q, operations []history.Operation, transactions []history.Transaction, includeTransactions bool) ([]hal.Pageable, error) { +func buildOperationsPage(ctx context.Context, historyQ *history.Q, operations []history.Operation, transactions []history.Transaction, includeTransactions bool, skipTxMeta bool) ([]hal.Pageable, error) { ledgerCache := history.LedgerCache{} for _, record := range operations { ledgerCache.Queue(record.LedgerSequence()) @@ -216,6 +219,7 @@ func buildOperationsPage(ctx context.Context, historyQ *history.Q, operations [] operationRecord.TransactionHash, transactionRecord, ledger, + skipTxMeta, ) if err != nil { return nil, err diff --git a/services/horizon/internal/actions/submit_transaction.go b/services/horizon/internal/actions/submit_transaction.go index b877f75a7b..314caf32a5 100644 --- a/services/horizon/internal/actions/submit_transaction.go +++ b/services/horizon/internal/actions/submit_transaction.go @@ -27,6 +27,7 @@ type SubmitTransactionHandler struct { NetworkPassphrase string DisableTxSub bool CoreStateGetter + SkipTxMeta bool } type envelopeInfo struct { @@ -84,6 +85,7 @@ func (handler SubmitTransactionHandler) response(r *http.Request, info envelopeI info.hash, &resource, result.Transaction, + handler.SkipTxMeta, ) return resource, err } diff --git a/services/horizon/internal/actions/transaction.go b/services/horizon/internal/actions/transaction.go index f823ad9acb..6903d5db2f 100644 --- a/services/horizon/internal/actions/transaction.go +++ b/services/horizon/internal/actions/transaction.go @@ -23,6 +23,7 @@ type TransactionQuery struct { // GetTransactionByHashHandler is the action handler for the end-point returning a transaction. type GetTransactionByHashHandler struct { + SkipTxMeta bool } // GetResource returns a transaction page. @@ -49,7 +50,7 @@ func (handler GetTransactionByHashHandler) GetResource(w HeaderWriter, r *http.R return resource, errors.Wrap(err, "loading transaction record") } - if err = resourceadapter.PopulateTransaction(ctx, qp.TransactionHash, &resource, record); err != nil { + if err = resourceadapter.PopulateTransaction(ctx, qp.TransactionHash, &resource, record, handler.SkipTxMeta); err != nil { return resource, errors.Wrap(err, "could not populate transaction") } return resource, nil @@ -90,6 +91,7 @@ func (qp TransactionsQuery) Validate() error { // GetTransactionsHandler is the action handler for all end-points returning a list of transactions. type GetTransactionsHandler struct { LedgerState *ledger.State + SkipTxMeta bool } // GetResourcePage returns a page of transactions. @@ -126,7 +128,7 @@ func (handler GetTransactionsHandler) GetResourcePage(w HeaderWriter, r *http.Re for _, record := range records { var res horizon.Transaction - err = resourceadapter.PopulateTransaction(ctx, record.TransactionHash, &res, record) + err = resourceadapter.PopulateTransaction(ctx, record.TransactionHash, &res, record, handler.SkipTxMeta) if err != nil { return nil, errors.Wrap(err, "could not populate transaction") } diff --git a/services/horizon/internal/app.go b/services/horizon/internal/app.go index b1cd7a1c85..59a8fb4432 100644 --- a/services/horizon/internal/app.go +++ b/services/horizon/internal/app.go @@ -552,6 +552,7 @@ func (a *App) init() error { }, cache: newHealthCache(healthCacheTTL), }, + SkipTxMeta: a.config.SkipTxmeta, } if a.primaryHistoryQ != nil { diff --git a/services/horizon/internal/httpx/router.go b/services/horizon/internal/httpx/router.go index 8fa57d0379..2755b9e062 100644 --- a/services/horizon/internal/httpx/router.go +++ b/services/horizon/internal/httpx/router.go @@ -50,6 +50,7 @@ type RouterConfig struct { HealthCheck http.Handler EnableIngestionFiltering bool DisableTxSub bool + SkipTxMeta bool } type Router struct { @@ -191,8 +192,9 @@ func (r *Router) addRoutes(config *RouterConfig, rateLimiter *throttled.HTTPRate r.With(historyMiddleware).Method(http.MethodGet, "/operations", streamableHistoryPageHandler(ledgerState, actions.GetOperationsHandler{ LedgerState: ledgerState, OnlyPayments: false, + SkipTxMeta: config.SkipTxMeta, }, streamHandler)) - r.With(historyMiddleware).Method(http.MethodGet, "/transactions", streamableHistoryPageHandler(ledgerState, actions.GetTransactionsHandler{LedgerState: ledgerState}, streamHandler)) + r.With(historyMiddleware).Method(http.MethodGet, "/transactions", streamableHistoryPageHandler(ledgerState, actions.GetTransactionsHandler{LedgerState: ledgerState, SkipTxMeta: config.SkipTxMeta}, streamHandler)) r.With(historyMiddleware).Method(http.MethodGet, "/effects", streamableHistoryPageHandler(ledgerState, actions.GetEffectsHandler{LedgerState: ledgerState}, streamHandler)) r.With(historyMiddleware).Method(http.MethodGet, "/trades", streamableHistoryPageHandler(ledgerState, actions.GetTradesHandler{LedgerState: ledgerState, CoreStateGetter: config.CoreGetter}, streamHandler)) }) @@ -241,29 +243,32 @@ func (r *Router) addRoutes(config *RouterConfig, rateLimiter *throttled.HTTPRate r.With(historyMiddleware).Method(http.MethodGet, "/accounts/{account_id:\\w+}/operations", streamableHistoryPageHandler(ledgerState, actions.GetOperationsHandler{ LedgerState: ledgerState, OnlyPayments: false, + SkipTxMeta: config.SkipTxMeta, }, streamHandler)) r.With(historyMiddleware).Method(http.MethodGet, "/accounts/{account_id:\\w+}/payments", streamableHistoryPageHandler(ledgerState, actions.GetOperationsHandler{ LedgerState: ledgerState, OnlyPayments: true, }, streamHandler)) r.With(historyMiddleware).Method(http.MethodGet, "/accounts/{account_id:\\w+}/trades", streamableHistoryPageHandler(ledgerState, actions.GetTradesHandler{LedgerState: ledgerState, CoreStateGetter: config.CoreGetter}, streamHandler)) - r.With(historyMiddleware).Method(http.MethodGet, "/accounts/{account_id:\\w+}/transactions", streamableHistoryPageHandler(ledgerState, actions.GetTransactionsHandler{LedgerState: ledgerState}, streamHandler)) + r.With(historyMiddleware).Method(http.MethodGet, "/accounts/{account_id:\\w+}/transactions", streamableHistoryPageHandler(ledgerState, actions.GetTransactionsHandler{LedgerState: ledgerState, SkipTxMeta: config.SkipTxMeta}, streamHandler)) }) // ledger actions r.Route("/ledgers", func(r chi.Router) { r.With(historyMiddleware).Method(http.MethodGet, "/", streamableHistoryPageHandler(ledgerState, actions.GetLedgersHandler{LedgerState: ledgerState}, streamHandler)) r.Route("/{ledger_id}", func(r chi.Router) { r.With(historyMiddleware).Method(http.MethodGet, "/", ObjectActionHandler{actions.GetLedgerByIDHandler{LedgerState: ledgerState}}) - r.With(historyMiddleware).Method(http.MethodGet, "/transactions", streamableHistoryPageHandler(ledgerState, actions.GetTransactionsHandler{LedgerState: ledgerState}, streamHandler)) + r.With(historyMiddleware).Method(http.MethodGet, "/transactions", streamableHistoryPageHandler(ledgerState, actions.GetTransactionsHandler{LedgerState: ledgerState, SkipTxMeta: config.SkipTxMeta}, streamHandler)) r.Group(func(r chi.Router) { r.With(historyMiddleware).Method(http.MethodGet, "/effects", streamableHistoryPageHandler(ledgerState, actions.GetEffectsHandler{LedgerState: ledgerState}, streamHandler)) r.With(historyMiddleware).Method(http.MethodGet, "/operations", streamableHistoryPageHandler(ledgerState, actions.GetOperationsHandler{ LedgerState: ledgerState, OnlyPayments: false, + SkipTxMeta: config.SkipTxMeta, }, streamHandler)) r.With(historyMiddleware).Method(http.MethodGet, "/payments", streamableHistoryPageHandler(ledgerState, actions.GetOperationsHandler{ LedgerState: ledgerState, OnlyPayments: true, + SkipTxMeta: config.SkipTxMeta, }, streamHandler)) }) }) @@ -275,18 +280,19 @@ func (r *Router) addRoutes(config *RouterConfig, rateLimiter *throttled.HTTPRate LedgerState: ledgerState, OnlyPayments: false, }, streamHandler)) - r.With(historyMiddleware).Method(http.MethodGet, "/claimable_balances/{claimable_balance_id:\\w+}/transactions", streamableHistoryPageHandler(ledgerState, actions.GetTransactionsHandler{LedgerState: ledgerState}, streamHandler)) + r.With(historyMiddleware).Method(http.MethodGet, "/claimable_balances/{claimable_balance_id:\\w+}/transactions", streamableHistoryPageHandler(ledgerState, actions.GetTransactionsHandler{LedgerState: ledgerState, SkipTxMeta: config.SkipTxMeta}, streamHandler)) }) // transaction history actions r.Route("/transactions", func(r chi.Router) { - r.With(historyMiddleware).Method(http.MethodGet, "/", streamableHistoryPageHandler(ledgerState, actions.GetTransactionsHandler{LedgerState: ledgerState}, streamHandler)) + r.With(historyMiddleware).Method(http.MethodGet, "/", streamableHistoryPageHandler(ledgerState, actions.GetTransactionsHandler{LedgerState: ledgerState, SkipTxMeta: config.SkipTxMeta}, streamHandler)) r.Route("/{tx_id}", func(r chi.Router) { r.With(historyMiddleware).Method(http.MethodGet, "/", ObjectActionHandler{actions.GetTransactionByHashHandler{}}) r.With(historyMiddleware).Method(http.MethodGet, "/effects", streamableHistoryPageHandler(ledgerState, actions.GetEffectsHandler{LedgerState: ledgerState}, streamHandler)) r.With(historyMiddleware).Method(http.MethodGet, "/operations", streamableHistoryPageHandler(ledgerState, actions.GetOperationsHandler{ LedgerState: ledgerState, OnlyPayments: false, + SkipTxMeta: config.SkipTxMeta, }, streamHandler)) r.With(historyMiddleware).Method(http.MethodGet, "/payments", streamableHistoryPageHandler(ledgerState, actions.GetOperationsHandler{ LedgerState: ledgerState, @@ -300,8 +306,9 @@ func (r *Router) addRoutes(config *RouterConfig, rateLimiter *throttled.HTTPRate r.With(historyMiddleware).Method(http.MethodGet, "/", streamableHistoryPageHandler(ledgerState, actions.GetOperationsHandler{ LedgerState: ledgerState, OnlyPayments: false, + SkipTxMeta: config.SkipTxMeta, }, streamHandler)) - r.With(historyMiddleware).Method(http.MethodGet, "/{id}", ObjectActionHandler{actions.GetOperationByIDHandler{LedgerState: ledgerState}}) + r.With(historyMiddleware).Method(http.MethodGet, "/{id}", ObjectActionHandler{actions.GetOperationByIDHandler{LedgerState: ledgerState, SkipTxMeta: config.SkipTxMeta}}) r.With(historyMiddleware).Method(http.MethodGet, "/{op_id}/effects", streamableHistoryPageHandler(ledgerState, actions.GetEffectsHandler{LedgerState: ledgerState}, streamHandler)) }) @@ -329,6 +336,7 @@ func (r *Router) addRoutes(config *RouterConfig, rateLimiter *throttled.HTTPRate NetworkPassphrase: config.NetworkPassphrase, DisableTxSub: config.DisableTxSub, CoreStateGetter: config.CoreGetter, + SkipTxMeta: config.SkipTxMeta, }}) // Network state related endpoints diff --git a/services/horizon/internal/integration/transaction_test.go b/services/horizon/internal/integration/transaction_test.go index 85fbe78522..a0db5816dd 100644 --- a/services/horizon/internal/integration/transaction_test.go +++ b/services/horizon/internal/integration/transaction_test.go @@ -63,13 +63,7 @@ func TestP19MetaDisabledTransaction(t *testing.T) { clientTx := itest.MustSubmitOperations(&masterAccount, itest.Master(), op) - var txMetaResult xdr.TransactionMeta - err = xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &txMetaResult) - require.NoError(t, err) - - assert.Equal(t, len(txMetaResult.MustV2().Operations), 0) - assert.Equal(t, len(txMetaResult.MustV2().TxChangesAfter), 0) - assert.Equal(t, len(txMetaResult.MustV2().TxChangesBefore), 0) + assert.Empty(t, clientTx.ResultMetaXdr) } func TestP20MetaTransaction(t *testing.T) { @@ -123,12 +117,5 @@ func TestP20MetaDisabledTransaction(t *testing.T) { preFlightOp, minFee := itest.PreflightHostFunctions(&sourceAccount, *installContractOp) clientTx := itest.MustSubmitOperationsWithFee(&sourceAccount, itest.Master(), minFee+txnbuild.MinBaseFee, &preFlightOp) - var txMetaResult xdr.TransactionMeta - err = xdr.SafeUnmarshalBase64(clientTx.ResultMetaXdr, &txMetaResult) - require.NoError(t, err) - - assert.Equal(t, len(txMetaResult.MustV3().Operations), 0) - assert.Nil(t, txMetaResult.MustV3().SorobanMeta) - assert.Equal(t, len(txMetaResult.MustV3().TxChangesAfter), 0) - assert.Equal(t, len(txMetaResult.MustV3().TxChangesBefore), 0) + assert.Empty(t, clientTx.ResultMetaXdr) } diff --git a/services/horizon/internal/resourceadapter/operations.go b/services/horizon/internal/resourceadapter/operations.go index 2f995fb395..fc301aec64 100644 --- a/services/horizon/internal/resourceadapter/operations.go +++ b/services/horizon/internal/resourceadapter/operations.go @@ -20,10 +20,11 @@ func NewOperation( transactionHash string, transactionRow *history.Transaction, ledger history.Ledger, + skipTxMeta bool, ) (result hal.Pageable, err error) { base := operations.Base{} - err = PopulateBaseOperation(ctx, &base, operationRow, transactionHash, transactionRow, ledger) + err = PopulateBaseOperation(ctx, &base, operationRow, transactionHash, transactionRow, ledger, skipTxMeta) if err != nil { return } @@ -166,7 +167,7 @@ func NewOperation( } // Populate fills out this resource using `row` as the source. -func PopulateBaseOperation(ctx context.Context, dest *operations.Base, operationRow history.Operation, transactionHash string, transactionRow *history.Transaction, ledger history.Ledger) error { +func PopulateBaseOperation(ctx context.Context, dest *operations.Base, operationRow history.Operation, transactionHash string, transactionRow *history.Transaction, ledger history.Ledger, skipTxMeta bool) error { dest.ID = fmt.Sprintf("%d", operationRow.ID) dest.PT = operationRow.PagingToken() dest.TransactionSuccessful = operationRow.TransactionSuccessful @@ -190,7 +191,7 @@ func PopulateBaseOperation(ctx context.Context, dest *operations.Base, operation if transactionRow != nil { dest.Transaction = new(horizon.Transaction) - return PopulateTransaction(ctx, transactionHash, dest.Transaction, *transactionRow) + return PopulateTransaction(ctx, transactionHash, dest.Transaction, *transactionRow, skipTxMeta) } return nil } diff --git a/services/horizon/internal/resourceadapter/operations_test.go b/services/horizon/internal/resourceadapter/operations_test.go index 39660a3678..f6acfa664b 100644 --- a/services/horizon/internal/resourceadapter/operations_test.go +++ b/services/horizon/internal/resourceadapter/operations_test.go @@ -19,7 +19,7 @@ func TestNewOperationAllTypesCovered(t *testing.T) { row := history.Operation{ Type: xdr.OperationType(typ), } - op, err := NewOperation(context.Background(), row, "foo", tx, history.Ledger{}) + op, err := NewOperation(context.Background(), row, "foo", tx, history.Ledger{}, false) assert.NoError(t, err, s) // if we got a base type, the operation is not covered if _, ok := op.(operations.Base); ok { @@ -31,7 +31,7 @@ func TestNewOperationAllTypesCovered(t *testing.T) { row := history.Operation{ Type: xdr.OperationType(200000), } - op, err := NewOperation(context.Background(), row, "foo", tx, history.Ledger{}) + op, err := NewOperation(context.Background(), row, "foo", tx, history.Ledger{}, false) assert.NoError(t, err) assert.IsType(t, op, operations.Base{}) @@ -52,7 +52,7 @@ func TestPopulateOperation_Successful(t *testing.T) { assert.NoError( t, - PopulateBaseOperation(ctx, &dest, row, "", nil, ledger), + PopulateBaseOperation(ctx, &dest, row, "", nil, ledger, false), ) assert.True(t, dest.TransactionSuccessful) assert.Nil(t, dest.Transaction) @@ -62,7 +62,7 @@ func TestPopulateOperation_Successful(t *testing.T) { assert.NoError( t, - PopulateBaseOperation(ctx, &dest, row, "", nil, ledger), + PopulateBaseOperation(ctx, &dest, row, "", nil, ledger, false), ) assert.False(t, dest.TransactionSuccessful) assert.Nil(t, dest.Transaction) @@ -92,12 +92,13 @@ func TestPopulateOperation_WithTransaction(t *testing.T) { assert.NoError( t, - PopulateBaseOperation(ctx, &dest, operationsRow, transactionRow.TransactionHash, &transactionRow, ledger), + PopulateBaseOperation(ctx, &dest, operationsRow, transactionRow.TransactionHash, &transactionRow, ledger, true), ) assert.True(t, dest.TransactionSuccessful) assert.True(t, dest.Transaction.Successful) assert.Equal(t, int64(100), dest.Transaction.FeeCharged) assert.Equal(t, int64(10000), dest.Transaction.MaxFee) + assert.Empty(t, dest.Transaction.ResultMetaXdr) } func TestPopulateOperation_AllowTrust(t *testing.T) { @@ -308,7 +309,7 @@ func getJSONResponse(typ xdr.OperationType, details string) (rsp map[string]inte Type: typ, DetailsString: null.StringFrom(details), } - resource, err := NewOperation(ctx, operationsRow, "", &transactionRow, history.Ledger{}) + resource, err := NewOperation(ctx, operationsRow, "", &transactionRow, history.Ledger{}, false) if err != nil { return } @@ -343,19 +344,19 @@ func TestFeeBumpOperation(t *testing.T) { assert.NoError( t, - PopulateBaseOperation(ctx, &dest, operationsRow, transactionRow.TransactionHash, nil, history.Ledger{}), + PopulateBaseOperation(ctx, &dest, operationsRow, transactionRow.TransactionHash, nil, history.Ledger{}, false), ) assert.Equal(t, transactionRow.TransactionHash, dest.TransactionHash) assert.NoError( t, - PopulateBaseOperation(ctx, &dest, operationsRow, transactionRow.InnerTransactionHash.String, nil, history.Ledger{}), + PopulateBaseOperation(ctx, &dest, operationsRow, transactionRow.InnerTransactionHash.String, nil, history.Ledger{}, false), ) assert.Equal(t, transactionRow.InnerTransactionHash.String, dest.TransactionHash) assert.NoError( t, - PopulateBaseOperation(ctx, &dest, operationsRow, transactionRow.TransactionHash, &transactionRow, history.Ledger{}), + PopulateBaseOperation(ctx, &dest, operationsRow, transactionRow.TransactionHash, &transactionRow, history.Ledger{}, false), ) assert.Equal(t, transactionRow.TransactionHash, dest.TransactionHash) @@ -374,7 +375,7 @@ func TestFeeBumpOperation(t *testing.T) { assert.NoError( t, - PopulateBaseOperation(ctx, &dest, operationsRow, transactionRow.InnerTransactionHash.String, &transactionRow, history.Ledger{}), + PopulateBaseOperation(ctx, &dest, operationsRow, transactionRow.InnerTransactionHash.String, &transactionRow, history.Ledger{}, false), ) assert.Equal(t, transactionRow.InnerTransactionHash.String, dest.TransactionHash) assert.Equal(t, transactionRow.InnerTransactionHash.String, dest.Transaction.Hash) diff --git a/services/horizon/internal/resourceadapter/transaction.go b/services/horizon/internal/resourceadapter/transaction.go index 547f8f4b40..f458830c2d 100644 --- a/services/horizon/internal/resourceadapter/transaction.go +++ b/services/horizon/internal/resourceadapter/transaction.go @@ -23,6 +23,7 @@ func PopulateTransaction( transactionHash string, dest *protocol.Transaction, row history.Transaction, + skipTxMeta bool, ) error { dest.ID = transactionHash dest.PT = row.PagingToken() @@ -43,7 +44,11 @@ func PopulateTransaction( dest.OperationCount = row.OperationCount dest.EnvelopeXdr = row.TxEnvelope dest.ResultXdr = row.TxResult - dest.ResultMetaXdr = row.TxMeta + if skipTxMeta { + dest.ResultMetaXdr = "" + } else { + dest.ResultMetaXdr = row.TxMeta + } dest.FeeMetaXdr = row.TxFeeMeta dest.MemoType = row.MemoType dest.Memo = row.Memo.String diff --git a/services/horizon/internal/resourceadapter/transaction_test.go b/services/horizon/internal/resourceadapter/transaction_test.go index 29c8040ce6..694fc885fb 100644 --- a/services/horizon/internal/resourceadapter/transaction_test.go +++ b/services/horizon/internal/resourceadapter/transaction_test.go @@ -34,18 +34,40 @@ func TestPopulateTransaction_Successful(t *testing.T) { }, } - assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row)) + assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row, false)) assert.True(t, dest.Successful) dest = Transaction{} row = history.Transaction{ TransactionWithoutLedger: history.TransactionWithoutLedger{ Successful: false, + TxMeta: "xyz", }, } - assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row)) + assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row, false)) assert.False(t, dest.Successful) + assert.NotEmpty(t, dest.ResultMetaXdr) +} + +func TestPopulateTransactionWhenSkipMeta(t *testing.T) { + ctx, _ := test.ContextWithLogBuffer() + + var ( + dest Transaction + row history.Transaction + ) + + dest = Transaction{} + row = history.Transaction{ + TransactionWithoutLedger: history.TransactionWithoutLedger{ + Successful: true, + }, + } + + assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row, true)) + assert.True(t, dest.Successful) + assert.Empty(t, dest.ResultMetaXdr) } func TestPopulateTransaction_HashMemo(t *testing.T) { @@ -55,12 +77,14 @@ func TestPopulateTransaction_HashMemo(t *testing.T) { TransactionWithoutLedger: history.TransactionWithoutLedger{ MemoType: "hash", Memo: null.StringFrom("abcdef"), + TxMeta: "xyz", }, } - assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row)) + assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row, false)) assert.Equal(t, "hash", dest.MemoType) assert.Equal(t, "abcdef", dest.Memo) assert.Equal(t, "", dest.MemoBytes) + assert.NotEmpty(t, dest.ResultMetaXdr) } func TestPopulateTransaction_TextMemo(t *testing.T) { @@ -122,15 +146,17 @@ func TestPopulateTransaction_TextMemo(t *testing.T) { MemoType: "text", TxEnvelope: envelopeXDR, Memo: null.StringFrom("sample"), + TxMeta: "xyz", }, } var dest Transaction - assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row)) + assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row, false)) assert.Equal(t, "text", dest.MemoType) assert.Equal(t, "sample", dest.Memo) assert.Equal(t, base64.StdEncoding.EncodeToString(rawMemo), dest.MemoBytes) + assert.NotEmpty(t, dest.ResultMetaXdr) } } @@ -148,12 +174,14 @@ func TestPopulateTransaction_Fee(t *testing.T) { TransactionWithoutLedger: history.TransactionWithoutLedger{ MaxFee: 10000, FeeCharged: 100, + TxMeta: "xyz", }, } - assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row)) + assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row, false)) assert.Equal(t, int64(100), dest.FeeCharged) assert.Equal(t, int64(10000), dest.MaxFee) + assert.NotEmpty(t, dest.ResultMetaXdr) } // TestPopulateTransaction_Preconditions tests transaction object population. @@ -188,10 +216,12 @@ func TestPopulateTransaction_Preconditions(t *testing.T) { MinAccountSequenceAge: null.StringFrom(fmt.Sprint(minSequenceAge)), MinAccountSequenceLedgerGap: null.IntFrom(int64(minSequenceLedgerGap)), ExtraSigners: pq.StringArray{"D34DB33F", "8BADF00D"}, + TxMeta: "xyz", }, } - assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row)) + assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row, false)) + assert.NotEmpty(t, dest.ResultMetaXdr) p := dest.Preconditions assert.Equal(t, validAfter.Format(time.RFC3339), dest.ValidAfter) assert.Equal(t, validBefore.Format(time.RFC3339), dest.ValidBefore) @@ -282,11 +312,13 @@ func TestPopulateTransaction_PreconditionsV2(t *testing.T) { Upper: null.IntFrom(int64(envelopeTimebounds.MaxTime)), }, TxEnvelope: envelopeXDR, + TxMeta: "xyz", }, } var dest Transaction - assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row)) + assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row, false)) + assert.NotEmpty(t, dest.ResultMetaXdr) gotTimebounds := dest.Preconditions.TimeBounds assert.Equal(t, "5", gotTimebounds.MinTime) @@ -307,7 +339,7 @@ func TestPopulateTransaction_PreconditionsV2_Omissions(t *testing.T) { generic := map[string]interface{}{} row := history.Transaction{TransactionWithoutLedger: tx} - tt.NoError(PopulateTransaction(ctx, row.TransactionHash, &dest, row)) + tt.NoError(PopulateTransaction(ctx, row.TransactionHash, &dest, row, false)) bytes, err := dest.MarshalJSON() tt.NoError(err) @@ -325,6 +357,7 @@ func TestPopulateTransaction_PreconditionsV2_Omissions(t *testing.T) { MinAccountSequence: null.IntFromPtr(nil), MinAccountSequenceAge: null.StringFrom("0"), ExtraSigners: pq.StringArray{}, + TxMeta: "xyz", }, { AccountSequence: 1, MinAccountSequenceLedgerGap: null.IntFrom(0), @@ -333,6 +366,7 @@ func TestPopulateTransaction_PreconditionsV2_Omissions(t *testing.T) { MinAccountSequence: null.IntFromPtr(nil), MinAccountSequenceAge: null.StringFromPtr(nil), ExtraSigners: nil, + TxMeta: "xyz", }, } { dest, js := jsonifyTx(tx) @@ -354,6 +388,7 @@ func TestPopulateTransaction_PreconditionsV2_Omissions(t *testing.T) { // exist entirely. tx.MinAccountSequenceLedgerGap = null.IntFromPtr(nil) dest, js = jsonifyTx(tx) + assert.NotEmpty(t, dest.ResultMetaXdr) tt.NotContains(js, "preconditions") } } @@ -375,10 +410,12 @@ func TestFeeBumpTransaction(t *testing.T) { InnerTransactionHash: null.StringFrom("2374e99349b9ef7dba9a5db3339b78fda8f34777b1af33ba468ad5c0df946d4d"), Signatures: []string{"a", "b", "c"}, InnerSignatures: []string{"d", "e", "f"}, + TxMeta: "xyz", }, } - assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row)) + assert.NoError(t, PopulateTransaction(ctx, row.TransactionHash, &dest, row, false)) + assert.NotEmpty(t, dest.ResultMetaXdr) assert.Equal(t, row.TransactionHash, dest.Hash) assert.Equal(t, row.TransactionHash, dest.ID) assert.Equal(t, row.FeeAccount.String, dest.FeeAccount) @@ -397,7 +434,8 @@ func TestFeeBumpTransaction(t *testing.T) { assert.Equal(t, []string{"a", "b", "c"}, dest.FeeBumpTransaction.Signatures) assert.Equal(t, "/transactions/"+row.TransactionHash, dest.Links.Transaction.Href) - assert.NoError(t, PopulateTransaction(ctx, row.InnerTransactionHash.String, &dest, row)) + assert.NoError(t, PopulateTransaction(ctx, row.InnerTransactionHash.String, &dest, row, false)) + assert.NotEmpty(t, dest.ResultMetaXdr) assert.Equal(t, row.InnerTransactionHash.String, dest.Hash) assert.Equal(t, row.InnerTransactionHash.String, dest.ID) assert.Equal(t, row.FeeAccount.String, dest.FeeAccount) From 73c6f37751eec0a78a765e665c32b55540fb5492 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Thu, 7 Mar 2024 11:53:57 -0800 Subject: [PATCH 073/234] services/horizon: Update CHANGELOG (#5238) --- services/horizon/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index b036851710..4003073503 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -7,6 +7,7 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ### Added - New `db_error_total` metrics key with labels `ctx_error`, `db_error`, and `db_error_extra` ([5225](https://github.com/stellar/go/pull/5225)). +- Bumped go version to the latest (1.22.1) ### Fixed - History archive access is more effective when you pass list of URLs to Horizon: they will now be accessed in a round-robin fashion, use alternative archives on errors, and intelligently back off ([5224](https://github.com/stellar/go/pull/5224)) From 324ea1a238ab34cb87dc427e59a0c2619a5d4b8c Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 11 Mar 2024 15:59:51 +0000 Subject: [PATCH 074/234] simplify effects query paging where clause (#5233) --- services/horizon/internal/actions/effects.go | 19 +- .../horizon/internal/db2/history/effect.go | 165 +++++++++--------- .../effect_batch_insert_builder_test.go | 9 +- .../internal/db2/history/effect_test.go | 12 +- services/horizon/internal/db2/history/main.go | 8 - .../internal/db2/history/transaction_test.go | 14 +- 6 files changed, 119 insertions(+), 108 deletions(-) diff --git a/services/horizon/internal/actions/effects.go b/services/horizon/internal/actions/effects.go index a141067d25..cc7406d7bc 100644 --- a/services/horizon/internal/actions/effects.go +++ b/services/horizon/internal/actions/effects.go @@ -95,25 +95,20 @@ func (handler GetEffectsHandler) GetResourcePage(w HeaderWriter, r *http.Request } func loadEffectRecords(ctx context.Context, hq *history.Q, qp EffectsQuery, pq db2.PageQuery) ([]history.Effect, error) { - effects := hq.Effects() - switch { case qp.AccountID != "": - effects.ForAccount(ctx, qp.AccountID) + return hq.EffectsForAccount(ctx, qp.AccountID, pq) case qp.LiquidityPoolID != "": - effects.ForLiquidityPool(ctx, pq, qp.LiquidityPoolID) + return hq.EffectsForLiquidityPool(ctx, qp.LiquidityPoolID, pq) case qp.OperationID > 0: - effects.ForOperation(int64(qp.OperationID)) + return hq.EffectsForOperation(ctx, int64(qp.OperationID), pq) case qp.LedgerID > 0: - effects.ForLedger(ctx, int32(qp.LedgerID)) + return hq.EffectsForLedger(ctx, int32(qp.LedgerID), pq) case qp.TxHash != "": - effects.ForTransaction(ctx, qp.TxHash) + return hq.EffectsForTransaction(ctx, qp.TxHash, pq) + default: + return hq.Effects(ctx, pq) } - - var result []history.Effect - err := effects.Page(pq).Select(ctx, &result) - - return result, err } func loadEffectLedgers(ctx context.Context, hq *history.Q, effects []history.Effect) (map[int32]history.Ledger, error) { diff --git a/services/horizon/internal/db2/history/effect.go b/services/horizon/internal/db2/history/effect.go index 13a9c52519..bdf1e2dfb0 100644 --- a/services/horizon/internal/db2/history/effect.go +++ b/services/horizon/internal/db2/history/effect.go @@ -69,75 +69,83 @@ func (r *Effect) PagingToken() string { return fmt.Sprintf("%d-%d", r.HistoryOperationID, r.Order) } -// Effects provides a helper to filter rows from the `history_effects` -// table with pre-defined filters. See `TransactionsQ` methods for the -// available filters. -func (q *Q) Effects() *EffectsQ { - return &EffectsQ{ - parent: q, - sql: selectEffect, +// Effects returns a page of effects without any filters besides the cursor +func (q *Q) Effects(ctx context.Context, page db2.PageQuery) ([]Effect, error) { + op, idx, err := parseEffectsCursor(page) + if err != nil { + return nil, err + } + + var rows []Effect + query := selectEffect + // we do not use selectEffectsPage() because we have found the + // query below to be more efficient when there are no other constraints + // such as filtering by account / ledger / transaction / etc + switch page.Order { + case "asc": + query = query. + Where("(heff.history_operation_id, heff.order) > (?, ?)", op, idx). + OrderBy("heff.history_operation_id asc, heff.order asc") + case "desc": + query = query. + Where("(heff.history_operation_id, heff.order) < (?, ?)", op, idx). + OrderBy("heff.history_operation_id desc, heff.order desc") } + + query = query.Limit(page.Limit) + + if err = q.Select(ctx, &rows, query); err != nil { + return nil, err + } + return rows, nil } -// ForAccount filters the operations collection to a specific account -func (q *EffectsQ) ForAccount(ctx context.Context, aid string) *EffectsQ { +// EffectsForAccount returns a page of effects for a given account +func (q *Q) EffectsForAccount(ctx context.Context, aid string, page db2.PageQuery) ([]Effect, error) { var account Account - q.Err = q.parent.AccountByAddress(ctx, &account, aid) - if q.Err != nil { - return q + if err := q.AccountByAddress(ctx, &account, aid); err != nil { + return nil, err } - q.sql = q.sql.Where("heff.history_account_id = ?", account.ID) - - return q + query := selectEffect.Where("heff.history_account_id = ?", account.ID) + return q.selectEffectsPage(ctx, query, page) } -// ForLedger filters the query to only effects in a specific ledger, -// specified by its sequence. -func (q *EffectsQ) ForLedger(ctx context.Context, seq int32) *EffectsQ { +// EffectsForLedger returns a page of effects for a given ledger sequence +func (q *Q) EffectsForLedger(ctx context.Context, seq int32, page db2.PageQuery) ([]Effect, error) { var ledger Ledger - q.Err = q.parent.LedgerBySequence(ctx, &ledger, seq) - if q.Err != nil { - return q + if err := q.LedgerBySequence(ctx, &ledger, seq); err != nil { + return nil, err } start := toid.ID{LedgerSequence: seq} end := toid.ID{LedgerSequence: seq + 1} - q.sql = q.sql.Where( + query := selectEffect.Where( "heff.history_operation_id >= ? AND heff.history_operation_id < ?", start.ToInt64(), end.ToInt64(), ) - - return q + return q.selectEffectsPage(ctx, query, page) } -// ForOperation filters the query to only effects in a specific operation, -// specified by its id. -func (q *EffectsQ) ForOperation(id int64) *EffectsQ { +// EffectsForOperation returns a page of effects for a given operation id. +func (q *Q) EffectsForOperation(ctx context.Context, id int64, page db2.PageQuery) ([]Effect, error) { start := toid.Parse(id) end := start end.IncOperationOrder() - q.sql = q.sql.Where( + query := selectEffect.Where( "heff.history_operation_id >= ? AND heff.history_operation_id < ?", start.ToInt64(), end.ToInt64(), ) - - return q + return q.selectEffectsPage(ctx, query, page) } -// ForLiquidityPool filters the query to only effects in a specific liquidity pool, -// specified by its id. -func (q *EffectsQ) ForLiquidityPool(ctx context.Context, page db2.PageQuery, id string) *EffectsQ { - if q.Err != nil { - return q - } - +// EffectsForLiquidityPool returns a page of effects for a given liquidity pool. +func (q *Q) EffectsForLiquidityPool(ctx context.Context, id string, page db2.PageQuery) ([]Effect, error) { op, _, err := page.CursorInt64Pair(db2.DefaultPairSep) if err != nil { - q.Err = err - return q + return nil, err } query := `SELECT holp.history_operation_id @@ -150,59 +158,62 @@ func (q *EffectsQ) ForLiquidityPool(ctx context.Context, page db2.PageQuery, id case "desc": query += "AND holp.history_operation_id <= ? ORDER BY holp.history_operation_id desc LIMIT ?" default: - q.Err = errors.Errorf("invalid paging order: %s", page.Order) - return q + return nil, errors.Errorf("invalid paging order: %s", page.Order) } var liquidityPoolOperationIDs []int64 - err = q.parent.SelectRaw(ctx, &liquidityPoolOperationIDs, query, id, op, page.Limit) + err = q.SelectRaw(ctx, &liquidityPoolOperationIDs, query, id, op, page.Limit) if err != nil { - q.Err = err - return q + return nil, err } - q.sql = q.sql.Where(map[string]interface{}{ - "heff.history_operation_id": liquidityPoolOperationIDs, - }) - return q + return q.selectEffectsPage( + ctx, + selectEffect.Where(map[string]interface{}{ + "heff.history_operation_id": liquidityPoolOperationIDs, + }), + page, + ) } -// ForTransaction filters the query to only effects in a specific -// transaction, specified by the transactions's hex-encoded hash. -func (q *EffectsQ) ForTransaction(ctx context.Context, hash string) *EffectsQ { +// EffectsForTransaction returns a page of effects for a given transaction +func (q *Q) EffectsForTransaction(ctx context.Context, hash string, page db2.PageQuery) ([]Effect, error) { var tx Transaction - q.Err = q.parent.TransactionByHash(ctx, &tx, hash) - if q.Err != nil { - return q + if err := q.TransactionByHash(ctx, &tx, hash); err != nil { + return nil, err } start := toid.Parse(tx.ID) end := start end.TransactionOrder++ - q.sql = q.sql.Where( - "heff.history_operation_id >= ? AND heff.history_operation_id < ?", - start.ToInt64(), - end.ToInt64(), - ) - return q + return q.selectEffectsPage( + ctx, + selectEffect.Where("heff.history_operation_id >= ? AND heff.history_operation_id < ?", + start.ToInt64(), + end.ToInt64(), + ), + page, + ) } -// Page specifies the paging constraints for the query being built by `q`. -func (q *EffectsQ) Page(page db2.PageQuery) *EffectsQ { - if q.Err != nil { - return q - } - +func parseEffectsCursor(page db2.PageQuery) (int64, int64, error) { op, idx, err := page.CursorInt64Pair(db2.DefaultPairSep) if err != nil { - q.Err = err - return q + return 0, 0, err } if idx > math.MaxInt32 { idx = math.MaxInt32 } + return op, idx, nil +} + +func (q *Q) selectEffectsPage(ctx context.Context, query sq.SelectBuilder, page db2.PageQuery) ([]Effect, error) { + op, idx, err := parseEffectsCursor(page) + if err != nil { + return nil, err + } // NOTE: Remember to test the queries below with EXPLAIN / EXPLAIN ANALYZE // before changing them. @@ -210,7 +221,7 @@ func (q *EffectsQ) Page(page db2.PageQuery) *EffectsQ { // DB will perform a full table scan. switch page.Order { case "asc": - q.sql = q.sql. + query = query. Where(`( heff.history_operation_id >= ? AND ( @@ -219,7 +230,7 @@ func (q *EffectsQ) Page(page db2.PageQuery) *EffectsQ { ))`, op, op, op, idx). OrderBy("heff.history_operation_id asc, heff.order asc") case "desc": - q.sql = q.sql. + query = query. Where(`( heff.history_operation_id <= ? AND ( @@ -229,18 +240,14 @@ func (q *EffectsQ) Page(page db2.PageQuery) *EffectsQ { OrderBy("heff.history_operation_id desc, heff.order desc") } - q.sql = q.sql.Limit(page.Limit) - return q -} + query = query.Limit(page.Limit) -// Select loads the results of the query specified by `q` into `dest`. -func (q *EffectsQ) Select(ctx context.Context, dest interface{}) error { - if q.Err != nil { - return q.Err + var rows []Effect + if err = q.Select(ctx, &rows, query); err != nil { + return nil, err } - q.Err = q.parent.Select(ctx, dest, q.sql) - return q.Err + return rows, nil } // QEffects defines history_effects related queries. diff --git a/services/horizon/internal/db2/history/effect_batch_insert_builder_test.go b/services/horizon/internal/db2/history/effect_batch_insert_builder_test.go index dc02148a7d..e1ac998953 100644 --- a/services/horizon/internal/db2/history/effect_batch_insert_builder_test.go +++ b/services/horizon/internal/db2/history/effect_batch_insert_builder_test.go @@ -6,6 +6,7 @@ import ( "github.com/guregu/null" + "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/services/horizon/internal/test" "github.com/stellar/go/toid" ) @@ -42,8 +43,12 @@ func TestAddEffect(t *testing.T) { tt.Assert.NoError(builder.Exec(tt.Ctx, q)) tt.Assert.NoError(q.Commit()) - effects := []Effect{} - tt.Assert.NoError(q.Effects().Select(tt.Ctx, &effects)) + effects, err := q.Effects(tt.Ctx, db2.PageQuery{ + Cursor: "0-0", + Order: "asc", + Limit: 200, + }) + tt.Require.NoError(err) tt.Assert.Len(effects, 1) effect := effects[0] diff --git a/services/horizon/internal/db2/history/effect_test.go b/services/horizon/internal/db2/history/effect_test.go index 498d5e92df..19af0ceff8 100644 --- a/services/horizon/internal/db2/history/effect_test.go +++ b/services/horizon/internal/db2/history/effect_test.go @@ -57,11 +57,11 @@ func TestEffectsForLiquidityPool(t *testing.T) { tt.Assert.NoError(q.Commit()) var result []Effect - err = q.Effects().ForLiquidityPool(tt.Ctx, db2.PageQuery{ + result, err = q.EffectsForLiquidityPool(tt.Ctx, liquidityPoolID, db2.PageQuery{ Cursor: "0-0", Order: "asc", Limit: 10, - }, liquidityPoolID).Select(tt.Ctx, &result) + }) tt.Assert.NoError(err) tt.Assert.Len(result, 1) @@ -156,8 +156,12 @@ func TestEffectsForTrustlinesSponsorshipEmptyAssetType(t *testing.T) { tt.Require.NoError(builder.Exec(tt.Ctx, q)) tt.Assert.NoError(q.Commit()) - var results []Effect - tt.Require.NoError(q.Effects().Select(tt.Ctx, &results)) + results, err := q.Effects(tt.Ctx, db2.PageQuery{ + Cursor: "0-0", + Order: "asc", + Limit: 200, + }) + tt.Require.NoError(err) tt.Require.Len(results, len(tests)) for i, test := range tests { diff --git a/services/horizon/internal/db2/history/main.go b/services/horizon/internal/db2/history/main.go index 41a4cb0068..d9c5ea7557 100644 --- a/services/horizon/internal/db2/history/main.go +++ b/services/horizon/internal/db2/history/main.go @@ -632,14 +632,6 @@ type SequenceBumped struct { NewSeq int64 `json:"new_seq"` } -// EffectsQ is a helper struct to aid in configuring queries that loads -// slices of Ledger structs. -type EffectsQ struct { - Err error - parent *Q - sql sq.SelectBuilder -} - // EffectType is the numeric type for an effect, used as the `type` field in the // `history_effects` table. type EffectType int diff --git a/services/horizon/internal/db2/history/transaction_test.go b/services/horizon/internal/db2/history/transaction_test.go index 4e85624701..65c6734644 100644 --- a/services/horizon/internal/db2/history/transaction_test.go +++ b/services/horizon/internal/db2/history/transaction_test.go @@ -892,12 +892,20 @@ func TestFetchFeeBumpTransaction(t *testing.T) { tt.Assert.Equal(byOuterhash, byInnerHash) } - var outerEffects, innerEffects []Effect - err = q.Effects().ForTransaction(tt.Ctx, fixture.OuterHash).Select(tt.Ctx, &outerEffects) + var innerEffects []Effect + outerEffects, err := q.EffectsForTransaction(tt.Ctx, fixture.OuterHash, db2.PageQuery{ + Cursor: "0-0", + Order: "asc", + Limit: 200, + }) tt.Assert.NoError(err) tt.Assert.Len(outerEffects, 1) - err = q.Effects().ForTransaction(tt.Ctx, fixture.InnerHash).Select(tt.Ctx, &innerEffects) + innerEffects, err = q.EffectsForTransaction(tt.Ctx, fixture.InnerHash, db2.PageQuery{ + Cursor: "0-0", + Order: "asc", + Limit: 200, + }) tt.Assert.NoError(err) tt.Assert.Equal(outerEffects, innerEffects) } From d3086517567cf6b8f64a6116ba56d881c943f87c Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 11 Mar 2024 17:05:15 +0000 Subject: [PATCH 075/234] speed up asset loader query (#5237) --- .../horizon/internal/db2/history/asset_loader.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/services/horizon/internal/db2/history/asset_loader.go b/services/horizon/internal/db2/history/asset_loader.go index bdf40fb843..fe17dc17be 100644 --- a/services/horizon/internal/db2/history/asset_loader.go +++ b/services/horizon/internal/db2/history/asset_loader.go @@ -7,8 +7,6 @@ import ( "sort" "strings" - sq "github.com/Masterminds/squirrel" - "github.com/stellar/go/support/collections/set" "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" @@ -108,13 +106,17 @@ func (a *AssetLoader) lookupKeys(ctx context.Context, q *Q, keys []AssetKey) err for i := 0; i < len(keys); i += loaderLookupBatchSize { end := ordered.Min(len(keys), i+loaderLookupBatchSize) subset := keys[i:end] - keyStrings := make([]string, 0, len(subset)) + args := make([]interface{}, 0, 3*len(subset)) + placeHolders := make([]string, 0, len(subset)) for _, key := range subset { - keyStrings = append(keyStrings, key.Type+"/"+key.Code+"/"+key.Issuer) + args = append(args, key.Code, key.Type, key.Issuer) + placeHolders = append(placeHolders, "(?, ?, ?)") } - err := q.Select(ctx, &rows, sq.Select("*").From("history_assets").Where(sq.Eq{ - "concat(asset_type, '/', asset_code, '/', asset_issuer)": keyStrings, - })) + rawSQL := fmt.Sprintf( + "SELECT * FROM history_assets WHERE (asset_code, asset_type, asset_issuer) in (%s)", + strings.Join(placeHolders, ", "), + ) + err := q.SelectRaw(ctx, &rows, rawSQL, args...) if err != nil { return errors.Wrap(err, "could not select assets") } From 4b0b0787e00851a0da6769c20f5e66db0d848400 Mon Sep 17 00:00:00 2001 From: Shawn Reuland Date: Mon, 11 Mar 2024 13:14:32 -0700 Subject: [PATCH 076/234] updated CHANGELOG for 2.29.0 --- services/horizon/CHANGELOG.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 4003073503..50c1407d26 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -5,12 +5,21 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +## 2.29.0 + ### Added - New `db_error_total` metrics key with labels `ctx_error`, `db_error`, and `db_error_extra` ([5225](https://github.com/stellar/go/pull/5225)). -- Bumped go version to the latest (1.22.1) +- Bumped go version to the latest (1.22.1) ([5232](https://github.com/stellar/go/pull/5232)) +- Add metrics for ingestion loaders ([5209](https://github.com/stellar/go/pull/5209)). +- Add metrics for http api requests in flight and requests received ([5240](https://github.com/stellar/go/pull/5240)). ### Fixed - History archive access is more effective when you pass list of URLs to Horizon: they will now be accessed in a round-robin fashion, use alternative archives on errors, and intelligently back off ([5224](https://github.com/stellar/go/pull/5224)) +- Remove captive core info request error logs ([5145](https://github.com/stellar/go/pull/5145)) +- Removed duplicate "Processed Ledger" log statement during resume state ([5152](https://github.com/stellar/go/pull/5152)) +- Fixed incorrect duration for ingestion processor metric ([5216](https://github.com/stellar/go/pull/5216)) +- Fixed sql performance on account transactions query ([5229](https://github.com/stellar/go/pull/5229)) + ### Breaking Changes - The Horizon API Transaction resource field in json `result_meta_xdr` is now optional and Horizon API will not emit the field when Horizon has been configured with `SKIP_TXMETA=true`, effectively null, otherwise if Horizon is configured with `SKIP_TXMETA=false` which is default, then the API Transaction field `result_meta_xdr` will remain present and populated with base64 encoded xdr [5228](https://github.com/stellar/go/pull/5228). From 71fef3cca734362a8e1c2658a169d944a4503157 Mon Sep 17 00:00:00 2001 From: tamirms Date: Thu, 14 Mar 2024 09:52:05 +0000 Subject: [PATCH 077/234] services/horizon/internal/ingest/processors: Fix bug in claimable balance change processor (#5246) Co-authored-by: Urvi --- .../db2/history/claimable_balances.go | 61 ++++++++++++++ .../db2/history/claimable_balances_test.go | 79 +++++++++++++++++++ .../db2/history/mock_q_claimable_balances.go | 5 ++ .../claimable_balances_change_processor.go | 19 ++++- ...laimable_balances_change_processor_test.go | 71 ++++++++++++++++- 5 files changed, 230 insertions(+), 5 deletions(-) diff --git a/services/horizon/internal/db2/history/claimable_balances.go b/services/horizon/internal/db2/history/claimable_balances.go index c198ee162d..abdf4ed758 100644 --- a/services/horizon/internal/db2/history/claimable_balances.go +++ b/services/horizon/internal/db2/history/claimable_balances.go @@ -140,6 +140,7 @@ type Claimant struct { // QClaimableBalances defines claimable-balance-related related queries. type QClaimableBalances interface { + UpsertClaimableBalances(ctx context.Context, cb []ClaimableBalance) error RemoveClaimableBalances(ctx context.Context, ids []string) (int64, error) RemoveClaimableBalanceClaimants(ctx context.Context, ids []string) (int64, error) GetClaimableBalancesByID(ctx context.Context, ids []string) ([]ClaimableBalance, error) @@ -185,6 +186,66 @@ func (q *Q) GetClaimantsByClaimableBalances(ctx context.Context, ids []string) ( return claimantsMap, err } +// UpsertClaimableBalances upserts a batch of claimable balances in the claimable_balances table. +// It also upserts the corresponding claimants in the claimable_balance_claimants table. +func (q *Q) UpsertClaimableBalances(ctx context.Context, cbs []ClaimableBalance) error { + if err := q.upsertCBs(ctx, cbs); err != nil { + return errors.Wrap(err, "could not upsert claimable balances") + } + + if err := q.upsertCBClaimants(ctx, cbs); err != nil { + return errors.Wrap(err, "could not upsert claimable balance claimants") + } + + return nil +} + +func (q *Q) upsertCBClaimants(ctx context.Context, cbs []ClaimableBalance) error { + var id, lastModifiedLedger, destination []interface{} + + for _, cb := range cbs { + for _, claimant := range cb.Claimants { + id = append(id, cb.BalanceID) + lastModifiedLedger = append(lastModifiedLedger, cb.LastModifiedLedger) + destination = append(destination, claimant.Destination) + } + } + + upsertFields := []upsertField{ + {"id", "text", id}, + {"destination", "text", destination}, + {"last_modified_ledger", "integer", lastModifiedLedger}, + } + + return q.upsertRows(ctx, "claimable_balance_claimants", "id, destination", upsertFields) +} + +func (q *Q) upsertCBs(ctx context.Context, cbs []ClaimableBalance) error { + var id, claimants, asset, amount, sponsor, lastModifiedLedger, flags []interface{} + + for _, cb := range cbs { + id = append(id, cb.BalanceID) + claimants = append(claimants, cb.Claimants) + asset = append(asset, cb.Asset) + amount = append(amount, cb.Amount) + sponsor = append(sponsor, cb.Sponsor) + lastModifiedLedger = append(lastModifiedLedger, cb.LastModifiedLedger) + flags = append(flags, cb.Flags) + } + + upsertFields := []upsertField{ + {"id", "text", id}, + {"claimants", "jsonb", claimants}, + {"asset", "text", asset}, + {"amount", "bigint", amount}, + {"sponsor", "text", sponsor}, + {"last_modified_ledger", "integer", lastModifiedLedger}, + {"flags", "int", flags}, + } + + return q.upsertRows(ctx, "claimable_balances", "id", upsertFields) +} + // RemoveClaimableBalances deletes claimable balances table. // Returns number of rows affected and error. func (q *Q) RemoveClaimableBalances(ctx context.Context, ids []string) (int64, error) { diff --git a/services/horizon/internal/db2/history/claimable_balances_test.go b/services/horizon/internal/db2/history/claimable_balances_test.go index 2e6d621945..1ffe442244 100644 --- a/services/horizon/internal/db2/history/claimable_balances_test.go +++ b/services/horizon/internal/db2/history/claimable_balances_test.go @@ -588,6 +588,85 @@ func TestFindClaimableBalancesByDestinationWithLimit(t *testing.T) { }) } +func TestUpdateClaimableBalance(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &Q{tt.HorizonSession()} + + accountID := "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML" + lastModifiedLedgerSeq := xdr.Uint32(123) + asset := xdr.MustNewCreditAsset("USD", accountID) + balanceID := xdr.ClaimableBalanceId{ + Type: xdr.ClaimableBalanceIdTypeClaimableBalanceIdTypeV0, + V0: &xdr.Hash{1, 2, 3}, + } + id, err := xdr.MarshalHex(balanceID) + tt.Assert.NoError(err) + cBalance := ClaimableBalance{ + BalanceID: id, + Claimants: []Claimant{ + { + Destination: accountID, + Predicate: xdr.ClaimPredicate{ + Type: xdr.ClaimPredicateTypeClaimPredicateUnconditional, + }, + }, + }, + Asset: asset, + LastModifiedLedger: 123, + Amount: 10, + } + + err = q.UpsertClaimableBalances(tt.Ctx, []ClaimableBalance{cBalance}) + tt.Assert.NoError(err) + + cBalancesClaimants, err := q.GetClaimantsByClaimableBalances(tt.Ctx, []string{cBalance.BalanceID}) + tt.Assert.NoError(err) + tt.Assert.Len(cBalancesClaimants[cBalance.BalanceID], 1) + tt.Assert.Equal(ClaimableBalanceClaimant{ + BalanceID: cBalance.BalanceID, + Destination: accountID, + LastModifiedLedger: cBalance.LastModifiedLedger, + }, cBalancesClaimants[cBalance.BalanceID][0]) + + // add sponsor + cBalance2 := ClaimableBalance{ + BalanceID: id, + Claimants: []Claimant{ + { + Destination: accountID, + Predicate: xdr.ClaimPredicate{ + Type: xdr.ClaimPredicateTypeClaimPredicateUnconditional, + }, + }, + }, + Asset: asset, + LastModifiedLedger: 123 + 1, + Amount: 10, + Sponsor: null.StringFrom("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + } + + err = q.UpsertClaimableBalances(tt.Ctx, []ClaimableBalance{cBalance2}) + tt.Assert.NoError(err) + + cbs := []ClaimableBalance{} + err = q.Select(tt.Ctx, &cbs, selectClaimableBalances) + tt.Assert.NoError(err) + tt.Assert.Len(cbs, 1) + tt.Assert.Equal("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", cbs[0].Sponsor.String) + tt.Assert.Equal(uint32(lastModifiedLedgerSeq+1), cbs[0].LastModifiedLedger) + + cBalancesClaimants, err = q.GetClaimantsByClaimableBalances(tt.Ctx, []string{cBalance2.BalanceID}) + tt.Assert.NoError(err) + tt.Assert.Len(cBalancesClaimants[cBalance2.BalanceID], 1) + tt.Assert.Equal(ClaimableBalanceClaimant{ + BalanceID: cBalance2.BalanceID, + Destination: accountID, + LastModifiedLedger: cBalance2.LastModifiedLedger, + }, cBalancesClaimants[cBalance2.BalanceID][0]) +} + func TestFindClaimableBalance(t *testing.T) { tt := test.Start(t) defer tt.Finish() diff --git a/services/horizon/internal/db2/history/mock_q_claimable_balances.go b/services/horizon/internal/db2/history/mock_q_claimable_balances.go index 64b65cf1a3..6a3adffac1 100644 --- a/services/horizon/internal/db2/history/mock_q_claimable_balances.go +++ b/services/horizon/internal/db2/history/mock_q_claimable_balances.go @@ -21,6 +21,11 @@ func (m *MockQClaimableBalances) GetClaimableBalancesByID(ctx context.Context, i return a.Get(0).([]ClaimableBalance), a.Error(1) } +func (m *MockQClaimableBalances) UpsertClaimableBalances(ctx context.Context, cbs []ClaimableBalance) error { + a := m.Called(ctx, cbs) + return a.Error(0) +} + func (m *MockQClaimableBalances) RemoveClaimableBalances(ctx context.Context, ids []string) (int64, error) { a := m.Called(ctx, ids) return a.Get(0).(int64), a.Error(1) diff --git a/services/horizon/internal/ingest/processors/claimable_balances_change_processor.go b/services/horizon/internal/ingest/processors/claimable_balances_change_processor.go index de729f9605..fce002881c 100644 --- a/services/horizon/internal/ingest/processors/claimable_balances_change_processor.go +++ b/services/horizon/internal/ingest/processors/claimable_balances_change_processor.go @@ -2,7 +2,6 @@ package processors import ( "context" - "fmt" "github.com/stellar/go/ingest" "github.com/stellar/go/services/horizon/internal/db2/history" @@ -60,7 +59,8 @@ func (p *ClaimableBalancesChangeProcessor) ProcessChange(ctx context.Context, ch func (p *ClaimableBalancesChangeProcessor) Commit(ctx context.Context) error { defer p.reset() var ( - cbIDsToDelete []string + cbIDsToDelete []string + updatedBalances []history.ClaimableBalance ) changes := p.cache.GetChanges() for _, change := range changes { @@ -97,8 +97,13 @@ func (p *ClaimableBalancesChangeProcessor) Commit(ctx context.Context) error { } cbIDsToDelete = append(cbIDsToDelete, id) default: - // claimable balance can only be created or removed - return fmt.Errorf("invalid change entry for a claimable balance was detected") + // this case should only occur if the sponsor has changed in the claimable balance + // the other fields of a claimable balance are immutable + postCB, err := p.ledgerEntryToRow(change.Post) + if err != nil { + return err + } + updatedBalances = append(updatedBalances, postCB) } } @@ -112,6 +117,12 @@ func (p *ClaimableBalancesChangeProcessor) Commit(ctx context.Context) error { return errors.Wrap(err, "error executing ClaimableBalanceBatchInsertBuilder") } + if len(updatedBalances) > 0 { + if err = p.qClaimableBalances.UpsertClaimableBalances(ctx, updatedBalances); err != nil { + return errors.Wrap(err, "error updating claimable balances") + } + } + if len(cbIDsToDelete) > 0 { count, err := p.qClaimableBalances.RemoveClaimableBalances(ctx, cbIDsToDelete) if err != nil { diff --git a/services/horizon/internal/ingest/processors/claimable_balances_change_processor_test.go b/services/horizon/internal/ingest/processors/claimable_balances_change_processor_test.go index 524de095f7..a10cc9db7d 100644 --- a/services/horizon/internal/ingest/processors/claimable_balances_change_processor_test.go +++ b/services/horizon/internal/ingest/processors/claimable_balances_change_processor_test.go @@ -8,10 +8,11 @@ import ( "github.com/guregu/null" + "github.com/stretchr/testify/suite" + "github.com/stellar/go/ingest" "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/xdr" - "github.com/stretchr/testify/suite" ) func TestClaimableBalancesChangeProcessorTestSuiteState(t *testing.T) { @@ -249,3 +250,71 @@ func (s *ClaimableBalancesChangeProcessorTestSuiteLedger) TestRemoveClaimableBal []string{id}, ).Return(int64(1), nil).Once() } + +func (s *ClaimableBalancesChangeProcessorTestSuiteLedger) TestUpdateClaimableBalanceAddSponsor() { + balanceID := xdr.ClaimableBalanceId{ + Type: xdr.ClaimableBalanceIdTypeClaimableBalanceIdTypeV0, + V0: &xdr.Hash{1, 2, 3}, + } + cBalance := xdr.ClaimableBalanceEntry{ + BalanceId: balanceID, + Claimants: []xdr.Claimant{}, + Asset: xdr.MustNewCreditAsset("USD", "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Amount: 10, + } + lastModifiedLedgerSeq := xdr.Uint32(123) + + pre := xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeClaimableBalance, + ClaimableBalance: &cBalance, + }, + LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, + Ext: xdr.LedgerEntryExt{ + V: 1, + V1: &xdr.LedgerEntryExtensionV1{ + SponsoringId: nil, + }, + }, + } + + // add sponsor + updated := xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeClaimableBalance, + ClaimableBalance: &cBalance, + }, + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + Ext: xdr.LedgerEntryExt{ + V: 1, + V1: &xdr.LedgerEntryExtensionV1{ + SponsoringId: xdr.MustAddressPtr("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + }, + }, + } + s.mockClaimableBalanceBatchInsertBuilder.On("Exec", s.ctx).Return(nil).Once() + + err := s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeClaimableBalance, + Pre: &pre, + Post: &updated, + }) + s.Assert().NoError(err) + + id, err := xdr.MarshalHex(balanceID) + s.Assert().NoError(err) + s.mockQ.On( + "UpsertClaimableBalances", + s.ctx, + []history.ClaimableBalance{ + { + BalanceID: id, + Claimants: []history.Claimant{}, + Asset: cBalance.Asset, + Amount: cBalance.Amount, + LastModifiedLedger: uint32(lastModifiedLedgerSeq), + Sponsor: null.StringFrom("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + }, + }, + ).Return(nil).Once() +} From 50677eeedca9d3e968178546c874a11cc2c302a1 Mon Sep 17 00:00:00 2001 From: tamirms Date: Thu, 14 Mar 2024 10:21:04 +0000 Subject: [PATCH 078/234] support/db: Delay canceling queries from client side when there's a statement / transaction timeout configured in postgres (#5223) --- services/horizon/internal/app.go | 1 + services/horizon/internal/config.go | 1 + .../internal/db2/history/liquidity_pools.go | 5 +- .../db2/history/liquidity_pools_test.go | 1 + .../horizon/internal/db2/history/offers.go | 4 +- services/horizon/internal/flags.go | 31 +++++ services/horizon/internal/flags_test.go | 96 +++++++++++++--- services/horizon/internal/httpx/middleware.go | 13 ++- services/horizon/internal/httpx/router.go | 6 +- services/horizon/internal/init.go | 22 ++-- services/horizon/internal/middleware_test.go | 2 +- staticcheck.sh | 2 +- support/config/config_option.go | 4 +- support/config/config_option_test.go | 18 +-- support/db/main.go | 6 +- support/db/mock_session.go | 8 +- support/db/session.go | 106 ++++++++++++++++-- support/db/session_test.go | 43 ++++++- 18 files changed, 297 insertions(+), 72 deletions(-) diff --git a/services/horizon/internal/app.go b/services/horizon/internal/app.go index 59a8fb4432..ed338f2ac3 100644 --- a/services/horizon/internal/app.go +++ b/services/horizon/internal/app.go @@ -532,6 +532,7 @@ func (a *App) init() error { SSEUpdateFrequency: a.config.SSEUpdateFrequency, StaleThreshold: a.config.StaleThreshold, ConnectionTimeout: a.config.ConnectionTimeout, + ClientQueryTimeout: a.config.ClientQueryTimeout, MaxHTTPRequestSize: a.config.MaxHTTPRequestSize, NetworkPassphrase: a.config.NetworkPassphrase, MaxPathLength: a.config.MaxPathLength, diff --git a/services/horizon/internal/config.go b/services/horizon/internal/config.go index f1f8ac078d..eb89d72efd 100644 --- a/services/horizon/internal/config.go +++ b/services/horizon/internal/config.go @@ -38,6 +38,7 @@ type Config struct { SSEUpdateFrequency time.Duration ConnectionTimeout time.Duration + ClientQueryTimeout time.Duration // MaxHTTPRequestSize is the maximum allowed request payload size MaxHTTPRequestSize uint RateQuota *throttled.RateQuota diff --git a/services/horizon/internal/db2/history/liquidity_pools.go b/services/horizon/internal/db2/history/liquidity_pools.go index 46e6ba59d3..3259163a89 100644 --- a/services/horizon/internal/db2/history/liquidity_pools.go +++ b/services/horizon/internal/db2/history/liquidity_pools.go @@ -9,8 +9,9 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/guregu/null" - "github.com/jmoiron/sqlx" + "github.com/stellar/go/services/horizon/internal/db2" + "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" ) @@ -188,7 +189,7 @@ func (q *Q) GetLiquidityPools(ctx context.Context, query LiquidityPoolsQuery) ([ } func (q *Q) StreamAllLiquidityPools(ctx context.Context, callback func(LiquidityPool) error) error { - var rows *sqlx.Rows + var rows *db.Rows var err error if rows, err = q.Query(ctx, selectLiquidityPools.Where("deleted = ?", false)); err != nil { diff --git a/services/horizon/internal/db2/history/liquidity_pools_test.go b/services/horizon/internal/db2/history/liquidity_pools_test.go index fd268d2518..2488945168 100644 --- a/services/horizon/internal/db2/history/liquidity_pools_test.go +++ b/services/horizon/internal/db2/history/liquidity_pools_test.go @@ -112,6 +112,7 @@ func TestStreamAllLiquidity(t *testing.T) { pools = append(pools, pool) return nil }) + tt.Assert.NoError(err) sort.Slice(pools, func(i, j int) bool { return pools[i].PoolID < pools[j].PoolID }) diff --git a/services/horizon/internal/db2/history/offers.go b/services/horizon/internal/db2/history/offers.go index 98a08fef87..1dcfe25321 100644 --- a/services/horizon/internal/db2/history/offers.go +++ b/services/horizon/internal/db2/history/offers.go @@ -5,8 +5,8 @@ import ( "database/sql" sq "github.com/Masterminds/squirrel" - "github.com/jmoiron/sqlx" + "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" ) @@ -108,7 +108,7 @@ func (q *Q) StreamAllOffers(ctx context.Context, callback func(Offer) error) err } func (q *Q) streamAllOffersBatch(ctx context.Context, lastId int64, limit uint64, callback func(Offer) error) (int64, error) { - var rows *sqlx.Rows + var rows *db.Rows var err error rows, err = q.Query(ctx, selectOffers. diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index 4c8e4dc2f5..48a83057f4 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "strings" + "time" "github.com/sirupsen/logrus" "github.com/spf13/viper" @@ -69,6 +70,7 @@ const ( StellarTestnet = "testnet" defaultMaxHTTPRequestSize = uint(200 * 1024) + clientQueryTimeoutNotSet = -1 ) var ( @@ -437,6 +439,30 @@ func Flags() (*Config, support.ConfigOptions) { Usage: "defines the timeout of connection after which 504 response will be sent or stream will be closed, if Horizon is behind a load balancer with idle connection timeout, this should be set to a few seconds less that idle timeout, does not apply to POST /transactions", UsedInCommands: ApiServerCommands, }, + &support.ConfigOption{ + Name: "client-query-timeout", + ConfigKey: &config.ClientQueryTimeout, + OptType: types.Int, + FlagDefault: clientQueryTimeoutNotSet, + CustomSetValue: func(co *support.ConfigOption) error { + if !support.IsExplicitlySet(co) { + *(co.ConfigKey.(*time.Duration)) = time.Duration(co.FlagDefault.(int)) + return nil + } + duration := viper.GetInt(co.Name) + if duration < 0 { + return fmt.Errorf("%s cannot be negative", co.Name) + } + *(co.ConfigKey.(*time.Duration)) = time.Duration(duration) * time.Second + return nil + }, + Usage: "defines the timeout for when horizon will cancel all postgres queries connected to an HTTP request. The timeout is measured in seconds since the start of the HTTP request. Note, this timeout does not apply to POST /transactions. " + + "The difference between client-query-timeout and connection-timeout is that connection-timeout applies a postgres statement timeout whereas client-query-timeout will send an additional request to postgres to cancel the ongoing query. " + + "Generally, client-query-timeout should be configured to be higher than connection-timeout to allow the postgres statement timeout to kill long running queries without having to send the additional cancel request to postgres. " + + "By default, client-query-timeout will be set to twice the connection-timeout. Setting client-query-timeout to 0 will disable the timeout which means that Horizon will never kill long running queries using the cancel request, however, " + + "long running queries can still be killed through the postgres statement timeout which is configured via the connection-timeout flag.", + UsedInCommands: ApiServerCommands, + }, &support.ConfigOption{ Name: "max-http-request-size", ConfigKey: &config.MaxHTTPRequestSize, @@ -983,5 +1009,10 @@ func ApplyFlags(config *Config, flags support.ConfigOptions, options ApplyOption " If Horizon is behind both, use --behind-cloudflare only") } + if config.ClientQueryTimeout == clientQueryTimeoutNotSet { + // the default value for cancel-db-query-timeout is twice the connection-timeout + config.ClientQueryTimeout = config.ConnectionTimeout * 2 + } + return nil } diff --git a/services/horizon/internal/flags_test.go b/services/horizon/internal/flags_test.go index ef2d5d3a02..3da39bc7a5 100644 --- a/services/horizon/internal/flags_test.go +++ b/services/horizon/internal/flags_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "testing" + "time" "github.com/spf13/cobra" @@ -207,22 +208,69 @@ func Test_createCaptiveCoreConfig(t *testing.T) { } } -func TestEnvironmentVariables(t *testing.T) { - environmentVars := map[string]string{ - "INGEST": "false", - "HISTORY_ARCHIVE_URLS": "http://localhost:1570", - "DATABASE_URL": "postgres://postgres@localhost/test_332cb65e6b00?sslmode=disable&timezone=UTC", - "STELLAR_CORE_URL": "http://localhost:11626", - "NETWORK_PASSPHRASE": "Standalone Network ; February 2017", - "APPLY_MIGRATIONS": "true", - "CHECKPOINT_FREQUENCY": "8", - "MAX_DB_CONNECTIONS": "50", - "ADMIN_PORT": "6060", - "PORT": "8001", - "CAPTIVE_CORE_BINARY_PATH": os.Getenv("HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_BIN"), - "CAPTIVE_CORE_CONFIG_PATH": "../docker/captive-core-classic-integration-tests.cfg", - "CAPTIVE_CORE_USE_DB": "true", +func TestClientQueryTimeoutFlag(t *testing.T) { + for _, testCase := range []struct { + name string + flag string + parsed time.Duration + err string + }{ + { + "negative value", + "-1", + 0, + "client-query-timeout cannot be negative", + }, + { + "default value", + "", + time.Second * 110, + "", + }, + { + "custom value", + "20", + time.Second * 20, + "", + }, + } { + t.Run(testCase.name, func(t *testing.T) { + environmentVars := horizonEnvVars() + if testCase.flag != "" { + environmentVars["CLIENT_QUERY_TIMEOUT"] = testCase.flag + } + + envManager := test.NewEnvironmentManager() + defer func() { + envManager.Restore() + }() + if err := envManager.InitializeEnvironmentVariables(environmentVars); err != nil { + require.NoError(t, err) + } + + config, flags := Flags() + horizonCmd := &cobra.Command{ + Use: "horizon", + Short: "Client-facing api server for the Stellar network", + SilenceErrors: true, + SilenceUsage: true, + Long: "Client-facing API server for the Stellar network.", + } + if err := flags.Init(horizonCmd); err != nil { + require.NoError(t, err) + } + if err := ApplyFlags(config, flags, ApplyOptions{RequireCaptiveCoreFullConfig: true, AlwaysIngest: false}); err != nil { + require.EqualError(t, err, testCase.err) + } else { + require.Empty(t, testCase.err) + } + require.Equal(t, testCase.parsed, config.ClientQueryTimeout) + }) } +} + +func TestEnvironmentVariables(t *testing.T) { + environmentVars := horizonEnvVars() envManager := test.NewEnvironmentManager() defer func() { @@ -261,6 +309,24 @@ func TestEnvironmentVariables(t *testing.T) { assert.Equal(t, config.CaptiveCoreConfigUseDB, true) } +func horizonEnvVars() map[string]string { + return map[string]string{ + "INGEST": "false", + "HISTORY_ARCHIVE_URLS": "http://localhost:1570", + "DATABASE_URL": "postgres://postgres@localhost/test_332cb65e6b00?sslmode=disable&timezone=UTC", + "STELLAR_CORE_URL": "http://localhost:11626", + "NETWORK_PASSPHRASE": "Standalone Network ; February 2017", + "APPLY_MIGRATIONS": "true", + "CHECKPOINT_FREQUENCY": "8", + "MAX_DB_CONNECTIONS": "50", + "ADMIN_PORT": "6060", + "PORT": "8001", + "CAPTIVE_CORE_BINARY_PATH": os.Getenv("HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_BIN"), + "CAPTIVE_CORE_CONFIG_PATH": "../docker/captive-core-classic-integration-tests.cfg", + "CAPTIVE_CORE_USE_DB": "true", + } +} + func TestRemovedFlags(t *testing.T) { tests := []struct { name string diff --git a/services/horizon/internal/httpx/middleware.go b/services/horizon/internal/httpx/middleware.go index eaa49634a5..9240ed0833 100644 --- a/services/horizon/internal/httpx/middleware.go +++ b/services/horizon/internal/httpx/middleware.go @@ -197,7 +197,7 @@ func recoverMiddleware(h http.Handler) http.Handler { // NewHistoryMiddleware adds session to the request context and ensures Horizon // is not in a stale state, which is when the difference between latest core // ledger and latest history ledger is higher than the given threshold -func NewHistoryMiddleware(ledgerState *ledger.State, staleThreshold int32, session db.SessionInterface) func(http.Handler) http.Handler { +func NewHistoryMiddleware(ledgerState *ledger.State, staleThreshold int32, session db.SessionInterface, contextDBTimeout time.Duration) func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -205,6 +205,7 @@ func NewHistoryMiddleware(ledgerState *ledger.State, staleThreshold int32, sessi if routePattern := supportHttp.GetChiRoutePattern(r); routePattern != "" { ctx = context.WithValue(ctx, &db.RouteContextKey, routePattern) } + ctx = setContextDBTimeout(contextDBTimeout, ctx) if staleThreshold > 0 { ls := ledgerState.CurrentStatus() isStale := (ls.CoreLatest - ls.HistoryLatest) > int32(staleThreshold) @@ -238,6 +239,7 @@ func NewHistoryMiddleware(ledgerState *ledger.State, staleThreshold int32, sessi // returning invalid data to the user) type StateMiddleware struct { HorizonSession db.SessionInterface + ClientQueryTimeout time.Duration NoStateVerification bool } @@ -276,6 +278,7 @@ func (m *StateMiddleware) WrapFunc(h http.HandlerFunc) http.HandlerFunc { if routePattern := supportHttp.GetChiRoutePattern(r); routePattern != "" { ctx = context.WithValue(ctx, &db.RouteContextKey, routePattern) } + ctx = setContextDBTimeout(m.ClientQueryTimeout, ctx) session := m.HorizonSession.Clone() q := &history.Q{session} sseRequest := render.Negotiate(r) == render.MimeEventStream @@ -344,6 +347,14 @@ func (m *StateMiddleware) WrapFunc(h http.HandlerFunc) http.HandlerFunc { } } +func setContextDBTimeout(timeout time.Duration, ctx context.Context) context.Context { + var deadline time.Time + if timeout > 0 { + deadline = time.Now().Add(timeout) + } + return context.WithValue(ctx, &db.DeadlineCtxKey, deadline) +} + // WrapFunc executes the middleware on a given HTTP handler function func (m *StateMiddleware) Wrap(h http.Handler) http.Handler { return m.WrapFunc(h.ServeHTTP) diff --git a/services/horizon/internal/httpx/router.go b/services/horizon/internal/httpx/router.go index 2755b9e062..60cf617468 100644 --- a/services/horizon/internal/httpx/router.go +++ b/services/horizon/internal/httpx/router.go @@ -38,6 +38,7 @@ type RouterConfig struct { SSEUpdateFrequency time.Duration StaleThreshold uint ConnectionTimeout time.Duration + ClientQueryTimeout time.Duration MaxHTTPRequestSize uint NetworkPassphrase string MaxPathLength uint @@ -139,7 +140,8 @@ func (r *Router) addMiddleware(config *RouterConfig, func (r *Router) addRoutes(config *RouterConfig, rateLimiter *throttled.HTTPRateLimiter, ledgerState *ledger.State) { stateMiddleware := StateMiddleware{ - HorizonSession: config.DBSession, + HorizonSession: config.DBSession, + ClientQueryTimeout: config.ClientQueryTimeout, } r.Method(http.MethodGet, "/health", config.HealthCheck) @@ -157,7 +159,7 @@ func (r *Router) addRoutes(config *RouterConfig, rateLimiter *throttled.HTTPRate LedgerSourceFactory: historyLedgerSourceFactory{ledgerState: ledgerState, updateFrequency: config.SSEUpdateFrequency}, } - historyMiddleware := NewHistoryMiddleware(ledgerState, int32(config.StaleThreshold), config.DBSession) + historyMiddleware := NewHistoryMiddleware(ledgerState, int32(config.StaleThreshold), config.DBSession, config.ClientQueryTimeout) // State endpoints behind stateMiddleware r.Group(func(r chi.Router) { r.Route("/accounts", func(r chi.Router) { diff --git a/services/horizon/internal/init.go b/services/horizon/internal/init.go index 0c0fe2c2cd..2311833508 100644 --- a/services/horizon/internal/init.go +++ b/services/horizon/internal/init.go @@ -45,38 +45,29 @@ func mustInitHorizonDB(app *App) { log.Fatalf("max open connections to horizon db must be greater than %d", ingest.MaxDBConnections) } } + serverSidePGTimeoutConfigs := []db.ClientConfig{ + db.StatementTimeout(app.config.ConnectionTimeout), + db.IdleTransactionTimeout(app.config.ConnectionTimeout), + } if app.config.RoDatabaseURL == "" { - var clientConfigs []db.ClientConfig - if !app.config.Ingest { - // if we are not ingesting then we don't expect to have long db queries / transactions - clientConfigs = append( - clientConfigs, - db.StatementTimeout(app.config.ConnectionTimeout), - db.IdleTransactionTimeout(app.config.ConnectionTimeout), - ) - } app.historyQ = &history.Q{mustNewDBSession( db.HistorySubservice, app.config.DatabaseURL, maxIdle, maxOpen, app.prometheusRegistry, - clientConfigs..., + serverSidePGTimeoutConfigs..., )} } else { // If RO set, use it for all DB queries - roClientConfigs := []db.ClientConfig{ - db.StatementTimeout(app.config.ConnectionTimeout), - db.IdleTransactionTimeout(app.config.ConnectionTimeout), - } app.historyQ = &history.Q{mustNewDBSession( db.HistorySubservice, app.config.RoDatabaseURL, maxIdle, maxOpen, app.prometheusRegistry, - roClientConfigs..., + serverSidePGTimeoutConfigs..., )} app.primaryHistoryQ = &history.Q{mustNewDBSession( @@ -85,6 +76,7 @@ func mustInitHorizonDB(app *App) { maxIdle, maxOpen, app.prometheusRegistry, + serverSidePGTimeoutConfigs..., )} } } diff --git a/services/horizon/internal/middleware_test.go b/services/horizon/internal/middleware_test.go index 40a74ea69e..269269bf8e 100644 --- a/services/horizon/internal/middleware_test.go +++ b/services/horizon/internal/middleware_test.go @@ -402,7 +402,7 @@ func TestCheckHistoryStaleMiddleware(t *testing.T) { } ledgerState := &ledger.State{} ledgerState.SetStatus(state) - historyMiddleware := httpx.NewHistoryMiddleware(ledgerState, testCase.staleThreshold, tt.HorizonSession()) + historyMiddleware := httpx.NewHistoryMiddleware(ledgerState, testCase.staleThreshold, tt.HorizonSession(), 0) handler := chi.NewRouter() handler.With(historyMiddleware).MethodFunc("GET", "/", endpoint) w := httptest.NewRecorder() diff --git a/staticcheck.sh b/staticcheck.sh index 539641a4b3..5e07d0d026 100755 --- a/staticcheck.sh +++ b/staticcheck.sh @@ -1,7 +1,7 @@ #! /bin/bash set -e -version='2023.1.1' +version='2023.1.7' staticcheck='go run honnef.co/go/tools/cmd/staticcheck@'"$version" diff --git a/support/config/config_option.go b/support/config/config_option.go index 97df9ef956..1fab1910d7 100644 --- a/support/config/config_option.go +++ b/support/config/config_option.go @@ -12,6 +12,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" + "github.com/stellar/go/support/errors" "github.com/stellar/go/support/strutils" ) @@ -248,13 +249,12 @@ func parseEnvVars(entries []string) map[string]bool { return set } -var envVars = parseEnvVars(os.Environ()) - // IsExplicitlySet returns true if and only if the given config option was set explicitly either // via a command line argument or via an environment variable func IsExplicitlySet(co *ConfigOption) bool { // co.flag.Changed is only set to true when the configuration is set via command line parameter. // In the case where a variable is configured via environment variable we need to check envVars. + envVars := parseEnvVars(os.Environ()) return co.flag.Changed || envVars[co.EnvVar] } diff --git a/support/config/config_option_test.go b/support/config/config_option_test.go index 30be01424b..418a50267e 100644 --- a/support/config/config_option_test.go +++ b/support/config/config_option_test.go @@ -88,13 +88,8 @@ func TestConfigOption_optionalFlags_env_set_empty(t *testing.T) { } configOpts.Init(cmd) - prev := envVars - envVars = map[string]bool{ - "STRING": true, - } - defer func() { - envVars = prev - }() + defer os.Setenv("STRING", os.Getenv("STRING")) + os.Setenv("STRING", "") cmd.Execute() assert.Equal(t, "", *optString) @@ -118,15 +113,6 @@ func TestConfigOption_optionalFlags_env_set(t *testing.T) { } configOpts.Init(cmd) - prev := envVars - envVars = map[string]bool{ - "STRING": true, - "UINT": true, - } - defer func() { - envVars = prev - }() - defer os.Setenv("STRING", os.Getenv("STRING")) defer os.Setenv("UINT", os.Getenv("UINT")) os.Setenv("STRING", "str") diff --git a/support/db/main.go b/support/db/main.go index dca23526ee..4b0b4c8b84 100644 --- a/support/db/main.go +++ b/support/db/main.go @@ -21,6 +21,7 @@ import ( "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" + "github.com/stellar/go/support/errors" // Enable postgres @@ -119,6 +120,7 @@ type Session struct { DB *sqlx.DB tx *sqlx.Tx + txCancel context.CancelFunc txOptions *sql.TxOptions errorHandlers []ErrorHandlerFunc } @@ -140,8 +142,8 @@ type SessionInterface interface { GetRaw(ctx context.Context, dest interface{}, query string, args ...interface{}) error Select(ctx context.Context, dest interface{}, query squirrel.Sqlizer) error SelectRaw(ctx context.Context, dest interface{}, query string, args ...interface{}) error - Query(ctx context.Context, query squirrel.Sqlizer) (*sqlx.Rows, error) - QueryRaw(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) + Query(ctx context.Context, query squirrel.Sqlizer) (*Rows, error) + QueryRaw(ctx context.Context, query string, args ...interface{}) (*Rows, error) GetTable(name string) *Table Exec(ctx context.Context, query squirrel.Sqlizer) (sql.Result, error) ExecRaw(ctx context.Context, query string, args ...interface{}) (sql.Result, error) diff --git a/support/db/mock_session.go b/support/db/mock_session.go index ce932cdbb3..e6cb58c987 100644 --- a/support/db/mock_session.go +++ b/support/db/mock_session.go @@ -72,14 +72,14 @@ func (m *MockSession) GetRaw(ctx context.Context, dest interface{}, query string return argss.Error(0) } -func (m *MockSession) Query(ctx context.Context, query squirrel.Sqlizer) (*sqlx.Rows, error) { +func (m *MockSession) Query(ctx context.Context, query squirrel.Sqlizer) (*Rows, error) { args := m.Called(ctx, query) - return args.Get(0).(*sqlx.Rows), args.Error(1) + return args.Get(0).(*Rows), args.Error(1) } -func (m *MockSession) QueryRaw(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) { +func (m *MockSession) QueryRaw(ctx context.Context, query string, args ...interface{}) (*Rows, error) { argss := m.Called(ctx, query, args) - return argss.Get(0).(*sqlx.Rows), argss.Error(1) + return argss.Get(0).(*Rows), argss.Error(1) } func (m *MockSession) Select(ctx context.Context, dest interface{}, query squirrel.Sqlizer) error { diff --git a/support/db/session.go b/support/db/session.go index 472fc40a37..6b5c2b18c0 100644 --- a/support/db/session.go +++ b/support/db/session.go @@ -12,28 +12,71 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/jmoiron/sqlx" "github.com/lib/pq" + "github.com/stellar/go/support/db/sqlutils" "github.com/stellar/go/support/errors" "github.com/stellar/go/support/log" ) +var DeadlineCtxKey = CtxKey("deadline") + +func noop() {} + +// context() checks if there is a override on the context timeout which is configured using DeadlineCtxKey. +// If the override exists, we return a new context with the desired deadline. Otherwise, we return the +// original context. +// Note that the override will not be applied if requestCtx has already been terminated. +// The timeout can be disabled by setting the DeadlineCtxKey value to a zero time.Time value, +// in that case the query will never be canceled. +func (s *Session) context(requestCtx context.Context) (context.Context, context.CancelFunc, error) { + var ctx context.Context + var cancel context.CancelFunc + + // if there is no DeadlineCtxKey value in the context we default to using the request context. + // this case is expected during ingestion where we don't want any queries to be canceled unless + // horizon is shutting down. + deadline, ok := requestCtx.Value(&DeadlineCtxKey).(time.Time) + if !ok { + return requestCtx, noop, nil + } + + // if requestCtx is already terminated don't proceed with the db statement + if requestCtx.Err() != nil { + return requestCtx, nil, requestCtx.Err() + } + + if deadline.IsZero() { + ctx, cancel = context.Background(), noop + } else { + ctx, cancel = context.WithDeadline(context.Background(), deadline) + } + return ctx, cancel, nil +} + // Begin binds this session to a new transaction. func (s *Session) Begin(ctx context.Context) error { if s.tx != nil { return errors.New("already in transaction") } + ctx, cancel, err := s.context(ctx) + if err != nil { + return err + } tx, err := s.DB.BeginTxx(ctx, nil) if err != nil { if knownErr := s.handleError(err, ctx); knownErr != nil { + cancel() return knownErr } + cancel() return errors.Wrap(err, "beginx failed") } log.Debug("sql: begin") s.tx = tx s.txOptions = nil + s.txCancel = cancel return nil } @@ -43,19 +86,26 @@ func (s *Session) BeginTx(ctx context.Context, opts *sql.TxOptions) error { if s.tx != nil { return errors.New("already in transaction") } + ctx, cancel, err := s.context(ctx) + if err != nil { + return err + } tx, err := s.DB.BeginTxx(ctx, opts) if err != nil { if knownErr := s.handleError(err, ctx); knownErr != nil { + cancel() return knownErr } + cancel() return errors.Wrap(err, "beginTx failed") } log.Debug("sql: begin") s.tx = tx s.txOptions = opts + s.txCancel = cancel return nil } @@ -93,6 +143,8 @@ func (s *Session) Commit() error { log.Debug("sql: commit") s.tx = nil s.txOptions = nil + s.txCancel() + s.txCancel = nil if knownErr := s.handleError(err, context.Background()); knownErr != nil { return knownErr @@ -135,7 +187,13 @@ func (s *Session) Get(ctx context.Context, dest interface{}, query sq.Sqlizer) e // GetRaw runs `query` with `args`, setting the first result found on // `dest`, if any. func (s *Session) GetRaw(ctx context.Context, dest interface{}, query string, args ...interface{}) error { - query, err := s.ReplacePlaceholders(query) + ctx, cancel, err := s.context(ctx) + if err != nil { + return err + } + defer cancel() + + query, err = s.ReplacePlaceholders(query) if err != nil { return errors.Wrap(err, "replace placeholders failed") } @@ -204,7 +262,13 @@ func (s *Session) ExecAll(ctx context.Context, script string) error { // ExecRaw runs `query` with `args` func (s *Session) ExecRaw(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { - query, err := s.ReplacePlaceholders(query) + ctx, cancel, err := s.context(ctx) + if err != nil { + return nil, err + } + defer cancel() + + query, err = s.ReplacePlaceholders(query) if err != nil { return nil, errors.Wrap(err, "replace placeholders failed") } @@ -294,7 +358,7 @@ func (s *Session) handleError(dbErr error, ctx context.Context) error { } // Query runs `query`, returns a *sqlx.Rows instance -func (s *Session) Query(ctx context.Context, query sq.Sqlizer) (*sqlx.Rows, error) { +func (s *Session) Query(ctx context.Context, query sq.Sqlizer) (*Rows, error) { sql, args, err := s.build(query) if err != nil { return nil, err @@ -302,10 +366,26 @@ func (s *Session) Query(ctx context.Context, query sq.Sqlizer) (*sqlx.Rows, erro return s.QueryRaw(ctx, sql, args...) } +type Rows struct { + sqlx.Rows + cancel context.CancelFunc +} + +func (r *Rows) Close() error { + defer r.cancel() + return r.Rows.Close() +} + // QueryRaw runs `query` with `args` -func (s *Session) QueryRaw(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) { - query, err := s.ReplacePlaceholders(query) +func (s *Session) QueryRaw(ctx context.Context, query string, args ...interface{}) (*Rows, error) { + ctx, cancel, err := s.context(ctx) if err != nil { + return nil, err + } + + query, err = s.ReplacePlaceholders(query) + if err != nil { + cancel() return nil, errors.Wrap(err, "replace placeholders failed") } @@ -314,8 +394,12 @@ func (s *Session) QueryRaw(ctx context.Context, query string, args ...interface{ s.log(ctx, "query", start, query, args) if err == nil { - return result, nil + return &Rows{ + Rows: *result, + cancel: cancel, + }, nil } + defer cancel() if knownErr := s.handleError(err, ctx); knownErr != nil { return nil, knownErr @@ -350,6 +434,8 @@ func (s *Session) Rollback() error { log.Debug("sql: rollback") s.tx = nil s.txOptions = nil + s.txCancel() + s.txCancel = nil if knownErr := s.handleError(err, context.Background()); knownErr != nil { return knownErr @@ -381,8 +467,14 @@ func (s *Session) SelectRaw( query string, args ...interface{}, ) error { + ctx, cancel, err := s.context(ctx) + if err != nil { + return err + } + defer cancel() + s.clearSliceIfPossible(dest) - query, err := s.ReplacePlaceholders(query) + query, err = s.ReplacePlaceholders(query) if err != nil { return errors.Wrap(err, "replace placeholders failed") } diff --git a/support/db/session_test.go b/support/db/session_test.go index 1fd2a3902b..3ae06d2b4c 100644 --- a/support/db/session_test.go +++ b/support/db/session_test.go @@ -6,12 +6,12 @@ import ( "testing" "time" - //"github.com/lib/pq" "github.com/lib/pq" "github.com/prometheus/client_golang/prometheus" - "github.com/stellar/go/support/db/dbtest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/stellar/go/support/db/dbtest" ) func TestContextTimeoutDuringSql(t *testing.T) { @@ -140,6 +140,45 @@ func TestStatementTimeout(t *testing.T) { assertDbErrorMetrics(reg, "n/a", "57014", "statement_timeout", assert) } +func TestDeadlineOverride(t *testing.T) { + db := dbtest.Postgres(t).Load(testSchema) + defer db.Close() + + sess := &Session{DB: db.Open()} + defer sess.DB.Close() + + resultCtx, _, err := sess.context(context.Background()) + assert.NoError(t, err) + _, ok := resultCtx.Deadline() + assert.False(t, ok) + + deadline := time.Now().Add(time.Hour) + requestCtx := context.WithValue(context.Background(), &DeadlineCtxKey, deadline) + resultCtx, _, err = sess.context(requestCtx) + assert.NoError(t, err) + d, ok := resultCtx.Deadline() + assert.True(t, ok) + assert.Equal(t, deadline, d) + + requestCtx, cancel := context.WithDeadline(requestCtx, time.Now().Add(time.Minute*30)) + resultCtx, _, err = sess.context(requestCtx) + assert.NoError(t, err) + d, ok = resultCtx.Deadline() + assert.True(t, ok) + assert.Equal(t, deadline, d) + + cancel() + assert.NoError(t, resultCtx.Err()) + _, _, err = sess.context(requestCtx) + assert.EqualError(t, err, "context canceled") + + var emptyTime time.Time + resultCtx, _, err = sess.context(context.WithValue(context.Background(), &DeadlineCtxKey, emptyTime)) + assert.NoError(t, err) + _, ok = resultCtx.Deadline() + assert.False(t, ok) +} + func TestSession(t *testing.T) { db := dbtest.Postgres(t).Load(testSchema) defer db.Close() From 98afa8d126cbba95971bbaa8c391b78333badf49 Mon Sep 17 00:00:00 2001 From: tamirms Date: Thu, 14 Mar 2024 11:16:00 +0000 Subject: [PATCH 079/234] services/horizon/internal: Add limit on number of horizon requests in flight (#5244) --- services/horizon/internal/app.go | 1 + services/horizon/internal/config.go | 11 ++++++----- services/horizon/internal/flags.go | 14 ++++++++++++-- services/horizon/internal/httpx/router.go | 12 ++++++++---- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/services/horizon/internal/app.go b/services/horizon/internal/app.go index ed338f2ac3..8fb86b5f54 100644 --- a/services/horizon/internal/app.go +++ b/services/horizon/internal/app.go @@ -533,6 +533,7 @@ func (a *App) init() error { StaleThreshold: a.config.StaleThreshold, ConnectionTimeout: a.config.ConnectionTimeout, ClientQueryTimeout: a.config.ClientQueryTimeout, + MaxConcurrentRequests: a.config.MaxConcurrentRequests, MaxHTTPRequestSize: a.config.MaxHTTPRequestSize, NetworkPassphrase: a.config.NetworkPassphrase, MaxPathLength: a.config.MaxPathLength, diff --git a/services/horizon/internal/config.go b/services/horizon/internal/config.go index eb89d72efd..94b6f5514b 100644 --- a/services/horizon/internal/config.go +++ b/services/horizon/internal/config.go @@ -40,11 +40,12 @@ type Config struct { ConnectionTimeout time.Duration ClientQueryTimeout time.Duration // MaxHTTPRequestSize is the maximum allowed request payload size - MaxHTTPRequestSize uint - RateQuota *throttled.RateQuota - FriendbotURL *url.URL - LogLevel logrus.Level - LogFile string + MaxHTTPRequestSize uint + RateQuota *throttled.RateQuota + MaxConcurrentRequests uint + FriendbotURL *url.URL + LogLevel logrus.Level + LogFile string // MaxPathLength is the maximum length of the path returned by `/paths` endpoint. MaxPathLength uint diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index 48a83057f4..9df44af44d 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -69,8 +69,9 @@ const ( // StellarTestnet is a constant representing the Stellar test network StellarTestnet = "testnet" - defaultMaxHTTPRequestSize = uint(200 * 1024) - clientQueryTimeoutNotSet = -1 + defaultMaxConcurrentRequests = uint(1000) + defaultMaxHTTPRequestSize = uint(200 * 1024) + clientQueryTimeoutNotSet = -1 ) var ( @@ -471,6 +472,15 @@ func Flags() (*Config, support.ConfigOptions) { Usage: "sets the limit on the maximum allowed http request payload size, default is 200kb, to disable the limit check, set to 0, only do so if you acknowledge the implications of accepting unbounded http request payload sizes.", UsedInCommands: ApiServerCommands, }, + &support.ConfigOption{ + Name: "max-concurrent-requests", + ConfigKey: &config.MaxConcurrentRequests, + OptType: types.Uint, + FlagDefault: defaultMaxConcurrentRequests, + Usage: "sets the limit on the maximum number of concurrent http requests, default is 1000, to disable the limit set to 0. " + + "If Horizon receives a request which would exceed the limit of concurrent http requests, Horizon will respond with a 429 status code.", + UsedInCommands: ApiServerCommands, + }, &support.ConfigOption{ Name: "per-hour-rate-limit", ConfigKey: &config.RateQuota, diff --git a/services/horizon/internal/httpx/router.go b/services/horizon/internal/httpx/router.go index 60cf617468..4ba978a96a 100644 --- a/services/horizon/internal/httpx/router.go +++ b/services/horizon/internal/httpx/router.go @@ -28,10 +28,11 @@ import ( ) type RouterConfig struct { - DBSession db.SessionInterface - PrimaryDBSession db.SessionInterface - TxSubmitter *txsub.System - RateQuota *throttled.RateQuota + DBSession db.SessionInterface + PrimaryDBSession db.SessionInterface + TxSubmitter *txsub.System + RateQuota *throttled.RateQuota + MaxConcurrentRequests uint BehindCloudflare bool BehindAWSLoadBalancer bool @@ -91,6 +92,9 @@ func (r *Router) addMiddleware(config *RouterConfig, BehindAWSLoadBalancer: config.BehindAWSLoadBalancer, })) r.Use(loggerMiddleware(serverMetrics)) + if config.MaxConcurrentRequests > 0 { + r.Use(chimiddleware.Throttle(int(config.MaxConcurrentRequests))) + } r.Use(timeoutMiddleware(config.ConnectionTimeout)) if config.MaxHTTPRequestSize > 0 { r.Use(func(handler http.Handler) http.Handler { From 55a279ff012e4796062f73636ff100df3074f36d Mon Sep 17 00:00:00 2001 From: Shawn Reuland Date: Thu, 14 Mar 2024 11:05:07 -0700 Subject: [PATCH 080/234] update changelog notes for latest on 2.29.0 --- services/horizon/CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 50c1407d26..003bab1d5a 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -12,6 +12,7 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). - Bumped go version to the latest (1.22.1) ([5232](https://github.com/stellar/go/pull/5232)) - Add metrics for ingestion loaders ([5209](https://github.com/stellar/go/pull/5209)). - Add metrics for http api requests in flight and requests received ([5240](https://github.com/stellar/go/pull/5240)). +- Add `MAX_CONCURRENT_REQUESTS`, defaults to 1000, limits the number of horizon api requests in flight ([5244](https://github.com/stellar/go/pull/5244)) ### Fixed - History archive access is more effective when you pass list of URLs to Horizon: they will now be accessed in a round-robin fashion, use alternative archives on errors, and intelligently back off ([5224](https://github.com/stellar/go/pull/5224)) @@ -19,7 +20,8 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). - Removed duplicate "Processed Ledger" log statement during resume state ([5152](https://github.com/stellar/go/pull/5152)) - Fixed incorrect duration for ingestion processor metric ([5216](https://github.com/stellar/go/pull/5216)) - Fixed sql performance on account transactions query ([5229](https://github.com/stellar/go/pull/5229)) - +- Fix bug in claimable balance change processor ([5246](https://github.com/stellar/go/pull/5246)) +- Delay canceling queries from client side when there's a statement / transaction timeout configured in postgres ([5223](https://github.com/stellar/go/pull/5223)) ### Breaking Changes - The Horizon API Transaction resource field in json `result_meta_xdr` is now optional and Horizon API will not emit the field when Horizon has been configured with `SKIP_TXMETA=true`, effectively null, otherwise if Horizon is configured with `SKIP_TXMETA=false` which is default, then the API Transaction field `result_meta_xdr` will remain present and populated with base64 encoded xdr [5228](https://github.com/stellar/go/pull/5228). From 4a00bc7e1afbb895dc669853c98af8f6f6663223 Mon Sep 17 00:00:00 2001 From: tamirms Date: Thu, 14 Mar 2024 21:45:17 +0000 Subject: [PATCH 081/234] fix flag usage for max-concurrent-requests (#5247) --- services/horizon/internal/flags.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index 9df44af44d..5f11389a5f 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -478,7 +478,7 @@ func Flags() (*Config, support.ConfigOptions) { OptType: types.Uint, FlagDefault: defaultMaxConcurrentRequests, Usage: "sets the limit on the maximum number of concurrent http requests, default is 1000, to disable the limit set to 0. " + - "If Horizon receives a request which would exceed the limit of concurrent http requests, Horizon will respond with a 429 status code.", + "If Horizon receives a request which would exceed the limit of concurrent http requests, Horizon will respond with a 503 status code.", UsedInCommands: ApiServerCommands, }, &support.ConfigOption{ From b12f3e8d642f73a71d25e471c60f5869ef477a61 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Sat, 16 Mar 2024 23:25:32 -0400 Subject: [PATCH 082/234] Create cloud_storage_backend to process ledgerexporter ledgerclosemeta --- exp/services/ledgerexporter/internal/app.go | 7 +- .../ledgerexporter/internal/uploader.go | 5 +- .../ledgerexporter/internal/uploader_test.go | 5 +- ingest/ledgerbackend/cloud_storage_backend.go | 133 ++++++++++++++++++ .../datastore}/datastore.go | 6 +- .../datastore}/gcs_datastore.go | 36 +++-- .../datastore}/mock_datastore.go | 7 +- 7 files changed, 181 insertions(+), 18 deletions(-) create mode 100644 ingest/ledgerbackend/cloud_storage_backend.go rename {exp/services/ledgerexporter/internal => support/datastore}/datastore.go (88%) rename {exp/services/ledgerexporter/internal => support/datastore}/gcs_datastore.go (75%) rename {exp/services/ledgerexporter/internal => support/datastore}/mock_datastore.go (84%) diff --git a/exp/services/ledgerexporter/internal/app.go b/exp/services/ledgerexporter/internal/app.go index fb4a5f788b..bb895b0738 100644 --- a/exp/services/ledgerexporter/internal/app.go +++ b/exp/services/ledgerexporter/internal/app.go @@ -12,6 +12,7 @@ import ( "github.com/pkg/errors" "github.com/stellar/go/ingest/ledgerbackend" _ "github.com/stellar/go/network" + "github.com/stellar/go/support/datastore" "github.com/stellar/go/support/log" ) @@ -22,7 +23,7 @@ var ( type App struct { config Config ledgerBackend ledgerbackend.LedgerBackend - dataStore DataStore + dataStore datastore.DataStore exportManager ExportManager uploader Uploader } @@ -97,8 +98,8 @@ func (a *App) Run() { logger.Info("Shutting down ledger-exporter") } -func mustNewDataStore(ctx context.Context, config *Config) DataStore { - dataStore, err := NewDataStore(ctx, fmt.Sprintf("%s/%s", config.DestinationURL, config.Network)) +func mustNewDataStore(ctx context.Context, config *Config) datastore.DataStore { + dataStore, err := datastore.NewDataStore(ctx, fmt.Sprintf("%s/%s", config.DestinationURL, config.Network)) logFatalIf(err, "Could not connect to destination data store") return dataStore } diff --git a/exp/services/ledgerexporter/internal/uploader.go b/exp/services/ledgerexporter/internal/uploader.go index 633db4ead7..530f7f76d6 100644 --- a/exp/services/ledgerexporter/internal/uploader.go +++ b/exp/services/ledgerexporter/internal/uploader.go @@ -5,6 +5,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/stellar/go/support/datastore" ) // Uploader is responsible for uploading data to a storage destination. @@ -14,11 +15,11 @@ type Uploader interface { } type uploader struct { - dataStore DataStore + dataStore datastore.DataStore metaArchiveCh chan *LedgerMetaArchive } -func NewUploader(destination DataStore, metaArchiveCh chan *LedgerMetaArchive) Uploader { +func NewUploader(destination datastore.DataStore, metaArchiveCh chan *LedgerMetaArchive) Uploader { return &uploader{ dataStore: destination, metaArchiveCh: metaArchiveCh, diff --git a/exp/services/ledgerexporter/internal/uploader_test.go b/exp/services/ledgerexporter/internal/uploader_test.go index c2a0fb96ab..264b6f50c1 100644 --- a/exp/services/ledgerexporter/internal/uploader_test.go +++ b/exp/services/ledgerexporter/internal/uploader_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/stellar/go/support/datastore" "github.com/stellar/go/support/errors" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -22,12 +23,12 @@ func TestUploaderSuite(t *testing.T) { type UploaderSuite struct { suite.Suite ctx context.Context - mockDataStore MockDataStore + mockDataStore datastore.MockDataStore } func (s *UploaderSuite) SetupTest() { s.ctx = context.Background() - s.mockDataStore = MockDataStore{} + s.mockDataStore = datastore.MockDataStore{} } func (s *UploaderSuite) TestUpload() { diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go new file mode 100644 index 0000000000..0d33e28d63 --- /dev/null +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -0,0 +1,133 @@ +package ledgerbackend + +import ( + "context" + "fmt" + "io" + + "github.com/pkg/errors" + "github.com/stellar/go/support/datastore" + "github.com/stellar/go/xdr" +) + +// Suffix for TxMeta files +const ( + fileSuffix = ".xdr.gz" + ledgersPerFile = 1 + filesPerPartition = 64000 +) + +// Ensure CloudStorageBackend implements LedgerBackend +var _ LedgerBackend = (*CloudStorageBackend)(nil) + +// CloudStorageBackend is a ledger backend that reads from a cloud storage service. +// The cloud storage service contains files generated from the ledgerExporter. +type CloudStorageBackend struct { + lcmDataStore datastore.DataStore + storageURL string +} + +// Return a new CloudStorageBackend instance. +func NewCloudStorageBackend(ctx context.Context, storageURL string) (*CloudStorageBackend, error) { + lcmDataStore, err := datastore.NewDataStore(ctx, storageURL) + if err != nil { + return nil, err + } + + return &CloudStorageBackend{lcmDataStore: lcmDataStore, storageURL: storageURL}, nil +} + +// GetLatestLedgerSequence returns the most recent ledger sequence number in the cloud storage bucket. +func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { + // TODO: Can probably copy the code that the ledger exporter will use for resumability + // Otherwise can use the ListObject function in datastore and find the largest valued filename + return uint32(0), nil +} + +// GetLedger returns the LedgerCloseMeta for the specified ledger sequence number +func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { + var ledgerCloseMetaBatch xdr.LedgerCloseMetaBatch + + objectKey, err := GetObjectKeyFromSequenceNumber(sequence, ledgersPerFile, filesPerPartition) + if err != nil { + return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed to get object key for ledger %d", sequence) + } + + reader, err := csb.lcmDataStore.GetFile(ctx, objectKey) + if err != nil { + return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed getting file: %s", objectKey) + } + + defer reader.Close() + + objectBytes, err := io.ReadAll(reader) + if err != nil { + return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed reading file: %s", objectKey) + } + + err = ledgerCloseMetaBatch.UnmarshalBinary(objectBytes) + if err != nil { + return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed unmarshalling file: %s", objectKey) + } + + ledgerCloseMetasIndex := sequence - uint32(ledgerCloseMetaBatch.StartSequence) + ledgerCloseMeta := ledgerCloseMetaBatch.LedgerCloseMetas[ledgerCloseMetasIndex] + + return ledgerCloseMeta, nil +} + +// PrepareRange checks if the starting and ending (if bounded) ledgers exist. +func (csb *CloudStorageBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { + _, err := csb.GetLedger(ctx, ledgerRange.from) + if err != nil { + return errors.Wrapf(err, "error getting ledger %d", ledgerRange.from) + } + + if ledgerRange.bounded { + _, err := csb.GetLedger(ctx, ledgerRange.to) + if err != nil { + return errors.Wrapf(err, "error getting ending ledger %d", ledgerRange.to) + } + } + + return nil +} + +// IsPrepared is a no-op for CloudStorageBackend. +func (csb *CloudStorageBackend) IsPrepared(ctx context.Context, ledgerRange Range) (bool, error) { + return true, nil +} + +// Close is a no-op for CloudStorageBackend. +func (csb *CloudStorageBackend) Close() error { + return nil +} + +// TODO: Should this function also be modified and added to support/datastore? +// This function should be shared between ledger exporter and this legerbackend reader +func GetObjectKeyFromSequenceNumber(ledgerSeq uint32, ledgersPerFile uint32, filesPerPartition uint32) (string, error) { + var objectKey string + + if ledgersPerFile < 1 { + return "", errors.Errorf("Invalid ledgers per file (%d): must be at least 1", ledgersPerFile) + } + + if filesPerPartition > 1 { + partitionSize := ledgersPerFile * filesPerPartition + partitionStart := (ledgerSeq / partitionSize) * partitionSize + partitionEnd := partitionStart + partitionSize - 1 + objectKey = fmt.Sprintf("%d-%d/", partitionStart, partitionEnd) + } + + fileStart := (ledgerSeq / ledgersPerFile) * ledgersPerFile + fileEnd := fileStart + ledgersPerFile - 1 + objectKey += fmt.Sprintf("%d", fileStart) + + // Multiple ledgers per file + if fileStart != fileEnd { + objectKey += fmt.Sprintf("-%d", fileEnd) + } + objectKey += fileSuffix + + return objectKey, nil +} diff --git a/exp/services/ledgerexporter/internal/datastore.go b/support/datastore/datastore.go similarity index 88% rename from exp/services/ledgerexporter/internal/datastore.go rename to support/datastore/datastore.go index 0367e9008e..4481394223 100644 --- a/exp/services/ledgerexporter/internal/datastore.go +++ b/support/datastore/datastore.go @@ -1,4 +1,4 @@ -package ledgerexporter +package datastore import ( "context" @@ -7,6 +7,7 @@ import ( "cloud.google.com/go/storage" "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/log" "github.com/stellar/go/support/url" "google.golang.org/api/option" ) @@ -19,6 +20,7 @@ type DataStore interface { Exists(ctx context.Context, path string) (bool, error) Size(ctx context.Context, path string) (int64, error) Close() error + ListObjects(ctx context.Context, path string) ([]string, error) } // NewDataStore creates a new DataStore based on the destination URL. @@ -39,7 +41,7 @@ func NewDataStore(ctx context.Context, destinationURL string) (DataStore, error) bucketName := parsed.Host prefix := pth - logger.Infof("creating GCS client for bucket: %s, prefix: %s", bucketName, prefix) + log.Infof("creating GCS client for bucket: %s, prefix: %s", bucketName, prefix) var options []option.ClientOption client, err := storage.NewClient(ctx, options...) diff --git a/exp/services/ledgerexporter/internal/gcs_datastore.go b/support/datastore/gcs_datastore.go similarity index 75% rename from exp/services/ledgerexporter/internal/gcs_datastore.go rename to support/datastore/gcs_datastore.go index 4fa1287e94..240efaa59a 100644 --- a/exp/services/ledgerexporter/internal/gcs_datastore.go +++ b/support/datastore/gcs_datastore.go @@ -1,4 +1,4 @@ -package ledgerexporter +package datastore import ( "context" @@ -8,9 +8,11 @@ import ( "path" "google.golang.org/api/googleapi" + "google.golang.org/api/iterator" "cloud.google.com/go/storage" "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/log" ) // GCSDataStore implements DataStore for GCS @@ -26,11 +28,11 @@ func (b *GCSDataStore) GetFile(ctx context.Context, filePath string) (io.ReadClo r, err := b.bucket.Object(filePath).NewReader(ctx) if err != nil { if gcsError, ok := err.(*googleapi.Error); ok { - logger.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) + log.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) } return nil, errors.Wrapf(err, "error retrieving file: %s", filePath) } - logger.Infof("File retrieved successfully: %s", filePath) + log.Infof("File retrieved successfully: %s", filePath) return r, nil } @@ -41,15 +43,15 @@ func (b *GCSDataStore) PutFileIfNotExists(ctx context.Context, filePath string, if gcsError, ok := err.(*googleapi.Error); ok { switch gcsError.Code { case http.StatusPreconditionFailed: - logger.Infof("Precondition failed: %s already exists in the bucket", filePath) + log.Infof("Precondition failed: %s already exists in the bucket", filePath) return nil // Treat as success default: - logger.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) + log.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) } } return errors.Wrapf(err, "error uploading file: %s", filePath) } - logger.Infof("File uploaded successfully: %s", filePath) + log.Infof("File uploaded successfully: %s", filePath) return nil } @@ -59,11 +61,11 @@ func (b *GCSDataStore) PutFile(ctx context.Context, filePath string, in io.Write if err != nil { if gcsError, ok := err.(*googleapi.Error); ok { - logger.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) + log.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) } return errors.Wrapf(err, "error uploading file: %v", filePath) } - logger.Infof("File uploaded successfully: %s", filePath) + log.Infof("File uploaded successfully: %s", filePath) return nil } @@ -103,3 +105,21 @@ func (b *GCSDataStore) putFile(ctx context.Context, filePath string, in io.Write } return w.Close() } + +func (b *GCSDataStore) ListObjects(ctx context.Context, path string) ([]string, error) { + var objectNames []string + + o := b.bucket.Objects(ctx, nil) + for { + attrs, err := o.Next() + if err == iterator.Done { + break + } + if err != nil { + log.Fatal(err) + } + objectNames = append(objectNames, attrs.Name) + } + + return nil, nil +} diff --git a/exp/services/ledgerexporter/internal/mock_datastore.go b/support/datastore/mock_datastore.go similarity index 84% rename from exp/services/ledgerexporter/internal/mock_datastore.go rename to support/datastore/mock_datastore.go index 7675a87461..8e503bfa05 100644 --- a/exp/services/ledgerexporter/internal/mock_datastore.go +++ b/support/datastore/mock_datastore.go @@ -1,4 +1,4 @@ -package ledgerexporter +package datastore import ( "context" @@ -41,3 +41,8 @@ func (m *MockDataStore) Close() error { args := m.Called() return args.Error(0) } + +func (m *MockDataStore) ListObjects(ctx context.Context, path string) ([]string, error) { + args := m.Called(ctx, path) + return args.Get(0).([]string), args.Error(0) +} From d8c263446e5b8e0335b3662ba98985e60eacb71b Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Sun, 17 Mar 2024 00:41:23 -0400 Subject: [PATCH 083/234] Fully form file path --- ingest/ledgerbackend/cloud_storage_backend.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index 0d33e28d63..f8ac84ea02 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -53,6 +53,8 @@ func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed to get object key for ledger %d", sequence) } + objectKey = csb.storageURL + objectKey + reader, err := csb.lcmDataStore.GetFile(ctx, objectKey) if err != nil { return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed getting file: %s", objectKey) From 1f6ee7dafc3cbe94435d4e74d94f558c74ce75be Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Sun, 17 Mar 2024 01:00:26 -0400 Subject: [PATCH 084/234] unzip --- ingest/ledgerbackend/cloud_storage_backend.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index f8ac84ea02..9db590ac7c 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -1,6 +1,7 @@ package ledgerbackend import ( + "compress/gzip" "context" "fmt" "io" @@ -62,7 +63,14 @@ func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) defer reader.Close() - objectBytes, err := io.ReadAll(reader) + gzipReader, err := gzip.NewReader(reader) + if err != nil { + return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed getting file: %s", objectKey) + } + + defer gzipReader.Close() + + objectBytes, err := io.ReadAll(gzipReader) if err != nil { return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed reading file: %s", objectKey) } From c50b067c7e22699ab1228925fa58b01415f4e933 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Sun, 17 Mar 2024 01:08:25 -0400 Subject: [PATCH 085/234] Remove storage url concat --- ingest/ledgerbackend/cloud_storage_backend.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index 9db590ac7c..1827113642 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -54,8 +54,6 @@ func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed to get object key for ledger %d", sequence) } - objectKey = csb.storageURL + objectKey - reader, err := csb.lcmDataStore.GetFile(ctx, objectKey) if err != nil { return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed getting file: %s", objectKey) From f34a84277137f19eb5dbd7ad94282b5eda17f70f Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Sun, 17 Mar 2024 01:29:42 -0400 Subject: [PATCH 086/234] Test get latest change --- ingest/ledgerbackend/cloud_storage_backend.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index 1827113642..dadd9a10f9 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -55,6 +55,7 @@ func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) } reader, err := csb.lcmDataStore.GetFile(ctx, objectKey) + if err != nil { return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed getting file: %s", objectKey) } From 75262f68a0a1a9d3dfaee2e3ca8f53958050f191 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Tue, 19 Mar 2024 03:30:10 -0400 Subject: [PATCH 087/234] Remove validators --- .../configs/captive-core-pubnet.cfg | 166 +----------------- 1 file changed, 1 insertion(+), 165 deletions(-) diff --git a/ingest/ledgerbackend/configs/captive-core-pubnet.cfg b/ingest/ledgerbackend/configs/captive-core-pubnet.cfg index f8b9a33985..c6227bcc66 100644 --- a/ingest/ledgerbackend/configs/captive-core-pubnet.cfg +++ b/ingest/ledgerbackend/configs/captive-core-pubnet.cfg @@ -9,30 +9,6 @@ PEER_PORT=11725 HOME_DOMAIN="stellar.org" QUALITY="HIGH" -[[HOME_DOMAINS]] -HOME_DOMAIN="satoshipay.io" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="lobstr.co" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="www.coinqvest.com" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="publicnode.org" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="stellar.blockdaemon.com" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN = "www.franklintempleton.com" -QUALITY = "HIGH" - [[VALIDATORS]] NAME="sdf_1" HOME_DOMAIN="stellar.org" @@ -52,144 +28,4 @@ NAME="sdf_3" HOME_DOMAIN="stellar.org" PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" ADDRESS="core-live-c.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" - -[[VALIDATORS]] -NAME="satoshipay_singapore" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" -ADDRESS="stellar-sg-sin.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" - -[[VALIDATORS]] -NAME="satoshipay_iowa" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" -ADDRESS="stellar-us-iowa.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" - -[[VALIDATORS]] -NAME="satoshipay_frankfurt" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" -ADDRESS="stellar-de-fra.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" - -[[VALIDATORS]] -NAME="lobstr_1_europe" -HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GCFONE23AB7Y6C5YZOMKUKGETPIAJA4QOYLS5VNS4JHBGKRZCPYHDLW7" -ADDRESS="v1.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-1-lobstr.s3.amazonaws.com/{0} -o {1}" - -[[VALIDATORS]] -NAME="lobstr_2_europe" -HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GDXQB3OMMQ6MGG43PWFBZWBFKBBDUZIVSUDAZZTRAWQZKES2CDSE5HKJ" -ADDRESS="v2.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-2-lobstr.s3.amazonaws.com/{0} -o {1}" - -[[VALIDATORS]] -NAME="lobstr_3_north_america" -HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" -ADDRESS="v3.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-3-lobstr.s3.amazonaws.com/{0} -o {1}" - -[[VALIDATORS]] -NAME="lobstr_4_asia" -HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GA7TEPCBDQKI7JQLQ34ZURRMK44DVYCIGVXQQWNSWAEQR6KB4FMCBT7J" -ADDRESS="v4.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-4-lobstr.s3.amazonaws.com/{0} -o {1}" - -[[VALIDATORS]] -NAME="lobstr_5_australia" -HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GA5STBMV6QDXFDGD62MEHLLHZTPDI77U3PFOD2SELU5RJDHQWBR5NNK7" -ADDRESS="v5.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-5-lobstr.s3.amazonaws.com/{0} -o {1}" - -[[VALIDATORS]] -NAME="coinqvest_hong_kong" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" -ADDRESS="hongkong.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://hongkong.stellar.coinqvest.com/history/{0} -o {1}" - -[[VALIDATORS]] -NAME="coinqvest_germany" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" -ADDRESS="germany.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://germany.stellar.coinqvest.com/history/{0} -o {1}" - -[[VALIDATORS]] -NAME="coinqvest_finland" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" -ADDRESS="finland.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://finland.stellar.coinqvest.com/history/{0} -o {1}" - -[[VALIDATORS]] -NAME="bootes" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" -ADDRESS="bootes.publicnode.org" -HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" - -[[VALIDATORS]] -NAME="hercules" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" -ADDRESS="hercules.publicnode.org" -HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" - -[[VALIDATORS]] -NAME="lyra" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" -ADDRESS="lyra.publicnode.org" -HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" - -[[VALIDATORS]] -NAME="Blockdaemon_Validator_1" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" -ADDRESS="stellar-full-validator1.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" - -[[VALIDATORS]] -NAME="Blockdaemon_Validator_2" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" -ADDRESS="stellar-full-validator2.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" - -[[VALIDATORS]] -NAME="Blockdaemon_Validator_3" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" -ADDRESS="stellar-full-validator3.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" - -[[VALIDATORS]] -NAME = "FT_SCV_1" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" -ADDRESS = "stellar1.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" - -[[VALIDATORS]] -NAME = "FT_SCV_2" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" -ADDRESS = "stellar2.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" - -[[VALIDATORS]] -NAME = "FT_SCV_3" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" -ADDRESS = "stellar3.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" +HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" \ No newline at end of file From dcdb98948e48b6547e9d296e190e5c1bcc1cd2f0 Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 22 Mar 2024 15:29:25 +0000 Subject: [PATCH 088/234] services/horizon/internal/ingest: Add missing tables to TruncateIngestStateTables() (#5253) --- .../horizon/internal/db2/history/ingestion.go | 2 + .../horizon/internal/ingest/verify_test.go | 101 +++++++++++------- 2 files changed, 67 insertions(+), 36 deletions(-) diff --git a/services/horizon/internal/db2/history/ingestion.go b/services/horizon/internal/db2/history/ingestion.go index d82188d83e..b35b05ece2 100644 --- a/services/horizon/internal/db2/history/ingestion.go +++ b/services/horizon/internal/db2/history/ingestion.go @@ -17,6 +17,8 @@ func (q *Q) TruncateIngestStateTables(ctx context.Context) error { "claimable_balances", "claimable_balance_claimants", "exp_asset_stats", + "contract_asset_balances", + "contract_asset_stats", "liquidity_pools", "offers", "trust_lines", diff --git a/services/horizon/internal/ingest/verify_test.go b/services/horizon/internal/ingest/verify_test.go index 901f21a0ca..f32bccdc8f 100644 --- a/services/horizon/internal/ingest/verify_test.go +++ b/services/horizon/internal/ingest/verify_test.go @@ -283,6 +283,39 @@ func ttlForContractData(tt *test.T, gen randxdr.Generator, contractData xdr.Ledg return ttl } +func TestTruncateIngestStateTables(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &history.Q{&db.Session{DB: tt.HorizonDB}} + + ledgerEntries := generateRandomLedgerEntries(tt) + // insert ledger entries of all types into the DB + tt.Assert.NoError(q.BeginTx(tt.Ctx, &sql.TxOptions{})) + checkpointLedger := uint32(63) + changeProcessor := buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, historyArchiveSource, checkpointLedger, "") + for _, change := range ingest.GetChangesFromLedgerEntryChanges(ledgerEntries) { + tt.Assert.NoError(changeProcessor.ProcessChange(tt.Ctx, change)) + } + tt.Assert.NoError(changeProcessor.Commit(tt.Ctx)) + tt.Assert.NoError(q.Commit()) + + // clear out the state tables + q.TruncateIngestStateTables(tt.Ctx) + + // reinsert the same ledger entries from before + tt.Assert.NoError(q.BeginTx(tt.Ctx, &sql.TxOptions{})) + changeProcessor = buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, historyArchiveSource, checkpointLedger, "") + for _, change := range ingest.GetChangesFromLedgerEntryChanges(ledgerEntries) { + tt.Assert.NoError(changeProcessor.ProcessChange(tt.Ctx, change)) + } + // this should succeed if we cleared out the state tables properly + // otherwise, there will be a duplicate key error when we attempt to + // insert a row that is already present + tt.Assert.NoError(changeProcessor.Commit(tt.Ctx)) + tt.Assert.NoError(q.Commit()) +} + func TestStateVerifierLockBusy(t *testing.T) { tt := test.Start(t) defer tt.Finish() @@ -292,21 +325,9 @@ func TestStateVerifierLockBusy(t *testing.T) { tt.Assert.NoError(q.BeginTx(tt.Ctx, &sql.TxOptions{})) checkpointLedger := uint32(63) - changeProcessor := buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, ledgerSource, checkpointLedger, "") + changeProcessor := buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, historyArchiveSource, checkpointLedger, "") - gen := randxdr.NewGenerator() - var changes []xdr.LedgerEntryChange - for i := 0; i < 10; i++ { - changes = append(changes, - genLiquidityPool(tt, gen), - genClaimableBalance(tt, gen), - genOffer(tt, gen), - genTrustLine(tt, gen), - genAccount(tt, gen), - genAccountData(tt, gen), - ) - } - for _, change := range ingest.GetChangesFromLedgerEntryChanges(changes) { + for _, change := range ingest.GetChangesFromLedgerEntryChanges(generateRandomLedgerEntries(tt)) { tt.Assert.NoError(changeProcessor.ProcessChange(tt.Ctx, change)) } tt.Assert.NoError(changeProcessor.Commit(tt.Ctx)) @@ -350,34 +371,14 @@ func TestStateVerifier(t *testing.T) { ledger := rand.Int31() checkpointLedger := uint32(ledger - (ledger % 64) - 1) - changeProcessor := buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, ledgerSource, checkpointLedger, "") + changeProcessor := buildChangeProcessor(q, &ingest.StatsChangeProcessor{}, historyArchiveSource, checkpointLedger, "") mockChangeReader := &ingest.MockChangeReader{} - gen := randxdr.NewGenerator() - var changes []xdr.LedgerEntryChange - for i := 0; i < 100; i++ { - changes = append(changes, - genLiquidityPool(tt, gen), - genClaimableBalance(tt, gen), - genOffer(tt, gen), - genTrustLine(tt, gen), - genAccount(tt, gen), - genAccountData(tt, gen), - genContractCode(tt, gen), - genConfigSetting(tt, gen), - genTTL(tt, gen), - ) - changes = append(changes, genAssetContractMetadata(tt, gen)...) - } - - coverage := map[xdr.LedgerEntryType]int{} - for _, change := range ingest.GetChangesFromLedgerEntryChanges(changes) { + for _, change := range ingest.GetChangesFromLedgerEntryChanges(generateRandomLedgerEntries(tt)) { mockChangeReader.On("Read").Return(change, nil).Once() tt.Assert.NoError(changeProcessor.ProcessChange(tt.Ctx, change)) - coverage[change.Type]++ } tt.Assert.NoError(changeProcessor.Commit(tt.Ctx)) - tt.Assert.Equal(len(xdr.LedgerEntryTypeMap), len(coverage)) tt.Assert.NoError(q.Commit()) @@ -402,3 +403,31 @@ func TestStateVerifier(t *testing.T) { mockChangeReader.AssertExpectations(t) mockHistoryAdapter.AssertExpectations(t) } + +func generateRandomLedgerEntries(tt *test.T) []xdr.LedgerEntryChange { + gen := randxdr.NewGenerator() + + var changes []xdr.LedgerEntryChange + for i := 0; i < 100; i++ { + changes = append(changes, + genLiquidityPool(tt, gen), + genClaimableBalance(tt, gen), + genOffer(tt, gen), + genTrustLine(tt, gen), + genAccount(tt, gen), + genAccountData(tt, gen), + genContractCode(tt, gen), + genConfigSetting(tt, gen), + genTTL(tt, gen), + ) + changes = append(changes, genAssetContractMetadata(tt, gen)...) + } + + coverage := map[xdr.LedgerEntryType]int{} + for _, change := range changes { + coverage[change.Created.Data.Type]++ + } + tt.Assert.Equal(len(xdr.LedgerEntryTypeMap), len(coverage)) + + return changes +} From 4413131c83660208e531cdfa225389367af12ecc Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 25 Mar 2024 15:23:41 +0000 Subject: [PATCH 089/234] services/horizon/internal/ingest: Remove unnecessary use of ChangeCompactor to reduce memory bloat during ingestion (#5252) --- ingest/change.go | 71 --- ingest/change_compactor.go | 7 + ingest/ledger_change_reader.go | 51 ++ ingest/ledger_transaction_test.go | 406 +--------------- .../account_data_batch_insert_builder.go | 6 + .../internal/db2/history/account_signers.go | 6 +- .../account_signers_batch_insert_builder.go | 4 + .../db2/history/account_signers_test.go | 5 +- .../history/accounts_batch_insert_builder.go | 6 + .../claimable_balance_batch_insert_builder.go | 6 + ...e_balance_claimant_batch_insert_builder.go | 6 + services/horizon/internal/db2/history/main.go | 3 +- .../mock_account_data_batch_insert_builder.go | 5 + ...ck_account_signers_batch_insert_builder.go | 6 + .../mock_accounts_batch_insert_builder.go | 5 + ..._claimable_balance_batch_insert_builder.go | 6 + ...e_balance_claimant_batch_insert_builder.go | 7 + .../mock_offers_batch_insert_builder.go | 5 + .../internal/db2/history/mock_q_signers.go | 4 +- .../mock_trust_lines_batch_insert_builder.go | 5 + .../history/offers_batch_insert_builder.go | 6 + .../trust_lines_batch_insert_builder.go | 6 + .../internal/ingest/processor_runner.go | 12 +- .../internal/ingest/processor_runner_test.go | 6 +- .../processors/account_data_processor.go | 71 ++- .../accounts_data_processor_test.go | 49 +- .../ingest/processors/accounts_processor.go | 87 ++-- .../processors/accounts_processor_test.go | 126 +++-- .../processors/asset_stats_processor.go | 65 ++- .../processors/asset_stats_processor_test.go | 18 +- .../claimable_balances_change_processor.go | 116 +++-- ...laimable_balances_change_processor_test.go | 4 + .../liquidity_pools_change_processor.go | 51 +- .../liquidity_pools_change_processor_test.go | 24 +- .../ingest/processors/offers_processor.go | 56 +-- .../processors/offers_processor_test.go | 184 ++------ .../processors/signer_processor_test.go | 164 +++---- .../ingest/processors/signers_diff_test.go | 438 ++++++++++++++++++ .../ingest/processors/signers_processor.go | 146 +++--- .../processors/trust_lines_processor.go | 97 ++-- .../processors/trust_lines_processor_test.go | 250 +++------- 41 files changed, 1210 insertions(+), 1386 deletions(-) create mode 100644 services/horizon/internal/ingest/processors/signers_diff_test.go diff --git a/ingest/change.go b/ingest/change.go index 027b37b861..8d435f4229 100644 --- a/ingest/change.go +++ b/ingest/change.go @@ -229,74 +229,3 @@ func (c *Change) AccountChangedExceptSigners() (bool, error) { return !bytes.Equal(preBinary, postBinary), nil } - -// AccountSignersChanged returns true if account signers have changed. -// Notice: this will return true on master key changes too! -func (c *Change) AccountSignersChanged() bool { - if c.Type != xdr.LedgerEntryTypeAccount { - panic("This should not be called on changes other than Account changes") - } - - // New account so new master key (which is also a signer) - if c.Pre == nil { - return true - } - - // Account merged. Account being merge can still have signers. - // c.Pre != nil at this point. - if c.Post == nil { - return true - } - - // c.Pre != nil && c.Post != nil at this point. - preAccountEntry := c.Pre.Data.MustAccount() - postAccountEntry := c.Post.Data.MustAccount() - - preSigners := preAccountEntry.SignerSummary() - postSigners := postAccountEntry.SignerSummary() - - if len(preSigners) != len(postSigners) { - return true - } - - for postSigner, postWeight := range postSigners { - preWeight, exist := preSigners[postSigner] - if !exist { - return true - } - - if preWeight != postWeight { - return true - } - } - - preSignerSponsors := preAccountEntry.SignerSponsoringIDs() - postSignerSponsors := postAccountEntry.SignerSponsoringIDs() - - if len(preSignerSponsors) != len(postSignerSponsors) { - return true - } - - for i := 0; i < len(preSignerSponsors); i++ { - preSponsor := preSignerSponsors[i] - postSponsor := postSignerSponsors[i] - - if preSponsor == nil && postSponsor != nil { - return true - } else if preSponsor != nil && postSponsor == nil { - return true - } else if preSponsor != nil && postSponsor != nil { - preSponsorAccountID := xdr.AccountId(*preSponsor) - preSponsorAddress := preSponsorAccountID.Address() - - postSponsorAccountID := xdr.AccountId(*postSponsor) - postSponsorAddress := postSponsorAccountID.Address() - - if preSponsorAddress != postSponsorAddress { - return true - } - } - } - - return false -} diff --git a/ingest/change_compactor.go b/ingest/change_compactor.go index b93082761e..a03126268a 100644 --- a/ingest/change_compactor.go +++ b/ingest/change_compactor.go @@ -14,6 +14,13 @@ import ( // previously removed returns an error. In such case verify.StateError is // returned. // +// The ChangeCompactor should not be used when ingesting from history archives +// because the history archive snapshots only contain CREATED changes. +// The ChangeCompactor is suited for compacting ledger entry changes derived +// from LedgerCloseMeta payloads because they typically contain a mix of +// CREATED, UPDATED, and REMOVED ledger entry changes and therefore may benefit +// from compaction. +// // It applies changes to the cache using the following algorithm: // // 1. If the change is CREATED it checks if any change connected to given entry diff --git a/ingest/ledger_change_reader.go b/ingest/ledger_change_reader.go index d09c579dbd..a282a04d87 100644 --- a/ingest/ledger_change_reader.go +++ b/ingest/ledger_change_reader.go @@ -76,6 +76,57 @@ func NewLedgerChangeReaderFromLedgerCloseMeta(networkPassphrase string, ledger x }, nil } +type compactingChangeReader struct { + input ChangeReader + changes []Change + compacted bool +} + +func (c *compactingChangeReader) compact() error { + compactor := NewChangeCompactor() + for { + change, err := c.input.Read() + if err == io.EOF { + break + } + if err != nil { + return err + } + if err = compactor.AddChange(change); err != nil { + return err + } + } + c.changes = compactor.GetChanges() + c.compacted = true + return nil +} + +func (c *compactingChangeReader) Read() (Change, error) { + if !c.compacted { + if err := c.compact(); err != nil { + return Change{}, err + } + } + if len(c.changes) == 0 { + return Change{}, io.EOF + } + change := c.changes[0] + c.changes = c.changes[1:] + return change, nil +} + +func (c *compactingChangeReader) Close() error { + return c.input.Close() +} + +// NewCompactingChangeReader wraps a given ChangeReader and returns a ChangeReader +// which compacts all the the Changes extracted from the input. +func NewCompactingChangeReader(input ChangeReader) ChangeReader { + return &compactingChangeReader{ + input: input, + } +} + // Read returns the next change in the stream. // If there are no changes remaining io.EOF is returned as an error. func (r *LedgerChangeReader) Read() (Change, error) { diff --git a/ingest/ledger_transaction_test.go b/ingest/ledger_transaction_test.go index 9533d21585..ced92e6918 100644 --- a/ingest/ledger_transaction_test.go +++ b/ingest/ledger_transaction_test.go @@ -3,9 +3,10 @@ package ingest import ( "testing" - "github.com/stellar/go/xdr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/stellar/go/xdr" ) func TestChangeAccountChangedExceptSignersInvalidType(t *testing.T) { @@ -810,406 +811,3 @@ func TestChangeAccountChangedExceptSignersNoChanges(t *testing.T) { assert.NotNil(t, change.Post.Data.Account.Signers) assert.Len(t, change.Post.Data.Account.Signers, 1) } - -func TestChangeAccountSignersChangedInvalidType(t *testing.T) { - change := Change{ - Type: xdr.LedgerEntryTypeOffer, - } - - assert.Panics(t, func() { - change.AccountSignersChanged() - }) -} - -func TestChangeAccountSignersChangedNoPre(t *testing.T) { - change := Change{ - Type: xdr.LedgerEntryTypeAccount, - Pre: nil, - Post: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - }, - }, - }, - } - - assert.True(t, change.AccountSignersChanged()) -} - -func TestChangeAccountSignersChangedNoPostMasterKey(t *testing.T) { - change := Change{ - Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - // Master weight = 1 - Thresholds: [4]byte{1, 1, 1, 1}, - }, - }, - }, - Post: nil, - } - - assert.True(t, change.AccountSignersChanged()) -} - -func TestChangeAccountSignersChangedNoPostNoMasterKey(t *testing.T) { - change := Change{ - Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - // Master weight = 0 - Thresholds: [4]byte{0, 1, 1, 1}, - }, - }, - }, - Post: nil, - } - - // Account being merge can still have signers so they will be removed. - assert.True(t, change.AccountSignersChanged()) -} - -func TestChangeAccountSignersChangedMasterKeyRemoved(t *testing.T) { - change := Change{ - Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - // Master weight = 1 - Thresholds: [4]byte{1, 1, 1, 1}, - }, - }, - }, - Post: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - // Master weight = 0 - Thresholds: [4]byte{0, 1, 1, 1}, - }, - }, - }, - } - - assert.True(t, change.AccountSignersChanged()) -} - -func TestChangeAccountSignersChangedMasterKeyAdded(t *testing.T) { - change := Change{ - Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - // Master weight = 0 - Thresholds: [4]byte{0, 1, 1, 1}, - }, - }, - }, - Post: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - // Master weight = 1 - Thresholds: [4]byte{1, 1, 1, 1}, - }, - }, - }, - } - - assert.True(t, change.AccountSignersChanged()) -} - -func TestChangeAccountSignersChangedSignerAdded(t *testing.T) { - change := Change{ - Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{}, - }, - }, - }, - Post: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{ - { - Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), - Weight: 1, - }, - }, - }, - }, - }, - } - - assert.True(t, change.AccountSignersChanged()) -} - -func TestChangeAccountSignersChangedSignerRemoved(t *testing.T) { - change := Change{ - Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{ - { - Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), - Weight: 1, - }, - }, - }, - }, - }, - Post: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{}, - }, - }, - }, - } - - assert.True(t, change.AccountSignersChanged()) -} - -func TestChangeAccountSignersChangedSignerWeightChanged(t *testing.T) { - change := Change{ - Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{ - { - Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), - Weight: 1, - }, - }, - }, - }, - }, - Post: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{ - { - Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), - Weight: 2, - }, - }, - }, - }, - }, - } - - assert.True(t, change.AccountSignersChanged()) -} - -func TestChangeAccountSignersChangedSponsorAdded(t *testing.T) { - sponsor, err := xdr.AddressToAccountId("GBADGWKHSUFOC4C7E3KXKINZSRX5KPHUWHH67UGJU77LEORGVLQ3BN3B") - assert.NoError(t, err) - - change := Change{ - Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{ - { - Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), - Weight: 1, - }, - }, - }, - }, - }, - Post: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{ - { - Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), - Weight: 1, - }, - }, - Ext: xdr.AccountEntryExt{ - V1: &xdr.AccountEntryExtensionV1{ - Ext: xdr.AccountEntryExtensionV1Ext{ - V2: &xdr.AccountEntryExtensionV2{ - SignerSponsoringIDs: []xdr.SponsorshipDescriptor{ - &sponsor, - }, - }, - }, - }, - }, - }, - }, - }, - } - - assert.True(t, change.AccountSignersChanged()) -} - -func TestChangeAccountSignersChangedSponsorRemoved(t *testing.T) { - sponsor, err := xdr.AddressToAccountId("GBADGWKHSUFOC4C7E3KXKINZSRX5KPHUWHH67UGJU77LEORGVLQ3BN3B") - assert.NoError(t, err) - - change := Change{ - Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{ - { - Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), - Weight: 1, - }, - }, - Ext: xdr.AccountEntryExt{ - V1: &xdr.AccountEntryExtensionV1{ - Ext: xdr.AccountEntryExtensionV1Ext{ - V2: &xdr.AccountEntryExtensionV2{ - SignerSponsoringIDs: []xdr.SponsorshipDescriptor{ - &sponsor, - }, - }, - }, - }, - }, - }, - }, - }, - Post: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{ - { - Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), - Weight: 1, - }, - }, - }, - }, - }, - } - - assert.True(t, change.AccountSignersChanged()) -} - -func TestChangeAccountSignersChangedSponsorChanged(t *testing.T) { - sponsor, err := xdr.AddressToAccountId("GBADGWKHSUFOC4C7E3KXKINZSRX5KPHUWHH67UGJU77LEORGVLQ3BN3B") - assert.NoError(t, err) - - newSponsor, err := xdr.AddressToAccountId("GB2Y6D5QFDJSCR6GSBO5D2LOLGZI4RVPRGZSSPLIFWNJZ7SL73TOMXAQ") - assert.NoError(t, err) - - change := Change{ - Type: xdr.LedgerEntryTypeAccount, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{ - { - Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), - Weight: 1, - }, - }, - Ext: xdr.AccountEntryExt{ - V1: &xdr.AccountEntryExtensionV1{ - Ext: xdr.AccountEntryExtensionV1Ext{ - V2: &xdr.AccountEntryExtensionV2{ - SignerSponsoringIDs: []xdr.SponsorshipDescriptor{ - &sponsor, - }, - }, - }, - }, - }, - }, - }, - }, - Post: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 10, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeAccount, - Account: &xdr.AccountEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Signers: []xdr.Signer{ - { - Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), - Weight: 1, - }, - }, - Ext: xdr.AccountEntryExt{ - V1: &xdr.AccountEntryExtensionV1{ - Ext: xdr.AccountEntryExtensionV1Ext{ - V2: &xdr.AccountEntryExtensionV2{ - SignerSponsoringIDs: []xdr.SponsorshipDescriptor{ - &newSponsor, - }, - }, - }, - }, - }, - }, - }, - }, - } - - assert.True(t, change.AccountSignersChanged()) -} diff --git a/services/horizon/internal/db2/history/account_data_batch_insert_builder.go b/services/horizon/internal/db2/history/account_data_batch_insert_builder.go index 75c8ff6124..acb1ba2b37 100644 --- a/services/horizon/internal/db2/history/account_data_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/account_data_batch_insert_builder.go @@ -9,6 +9,7 @@ import ( type AccountDataBatchInsertBuilder interface { Add(data Data) error Exec(ctx context.Context) error + Len() int } type accountDataBatchInsertBuilder struct { @@ -48,3 +49,8 @@ func (i *accountDataBatchInsertBuilder) Add(data Data) error { func (i *accountDataBatchInsertBuilder) Exec(ctx context.Context) error { return i.builder.Exec(ctx, i.session, i.table) } + +// Len returns the number of elements in the batch +func (i *accountDataBatchInsertBuilder) Len() int { + return i.builder.Len() +} diff --git a/services/horizon/internal/db2/history/account_signers.go b/services/horizon/internal/db2/history/account_signers.go index d523c96ff2..78afcdf06c 100644 --- a/services/horizon/internal/db2/history/account_signers.go +++ b/services/horizon/internal/db2/history/account_signers.go @@ -68,12 +68,12 @@ func (q *Q) CreateAccountSigner(ctx context.Context, account, signer string, wei return result.RowsAffected() } -// RemoveAccountSigner deletes a row in the accounts_signers table. +// RemoveAccountSigners deletes rows in the accounts_signers table. // Returns number of rows affected and error. -func (q *Q) RemoveAccountSigner(ctx context.Context, account, signer string) (int64, error) { +func (q *Q) RemoveAccountSigners(ctx context.Context, account string, signers []string) (int64, error) { sql := sq.Delete("accounts_signers").Where(sq.Eq{ "account_id": account, - "signer": signer, + "signer": signers, }) result, err := q.Exec(ctx, sql) diff --git a/services/horizon/internal/db2/history/account_signers_batch_insert_builder.go b/services/horizon/internal/db2/history/account_signers_batch_insert_builder.go index d347fb3e17..6fbace0211 100644 --- a/services/horizon/internal/db2/history/account_signers_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/account_signers_batch_insert_builder.go @@ -16,3 +16,7 @@ func (i *accountSignersBatchInsertBuilder) Add(signer AccountSigner) error { func (i *accountSignersBatchInsertBuilder) Exec(ctx context.Context) error { return i.builder.Exec(ctx, i.session, i.table) } + +func (i *accountSignersBatchInsertBuilder) Len() int { + return i.builder.Len() +} diff --git a/services/horizon/internal/db2/history/account_signers_test.go b/services/horizon/internal/db2/history/account_signers_test.go index 158df82409..0e22d7cdfb 100644 --- a/services/horizon/internal/db2/history/account_signers_test.go +++ b/services/horizon/internal/db2/history/account_signers_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/guregu/null" + "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/services/horizon/internal/test" ) @@ -120,7 +121,7 @@ func TestRemoveNonExistentAccountSigner(t *testing.T) { account := "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH3" signer := "GC23QF2HUE52AMXUFUH3AYJAXXGXXV2VHXYYR6EYXETPKDXZSAW67XO5" - rowsAffected, err := q.RemoveAccountSigner(tt.Ctx, account, signer) + rowsAffected, err := q.RemoveAccountSigners(tt.Ctx, account, []string{signer}) tt.Assert.NoError(err) tt.Assert.Equal(int64(0), rowsAffected) } @@ -147,7 +148,7 @@ func TestRemoveAccountSigner(t *testing.T) { tt.Assert.Len(results, 1) tt.Assert.Equal(expected, results[0]) - rowsAffected, err := q.RemoveAccountSigner(tt.Ctx, account, signer) + rowsAffected, err := q.RemoveAccountSigners(tt.Ctx, account, []string{signer}) tt.Assert.NoError(err) tt.Assert.Equal(int64(1), rowsAffected) diff --git a/services/horizon/internal/db2/history/accounts_batch_insert_builder.go b/services/horizon/internal/db2/history/accounts_batch_insert_builder.go index 5e68468094..6e5491df3e 100644 --- a/services/horizon/internal/db2/history/accounts_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/accounts_batch_insert_builder.go @@ -10,6 +10,7 @@ import ( type AccountsBatchInsertBuilder interface { Add(account AccountEntry) error Exec(ctx context.Context) error + Len() int } // AccountsBatchInsertBuilder is a simple wrapper around db.FastBatchInsertBuilder @@ -37,3 +38,8 @@ func (i *accountsBatchInsertBuilder) Add(account AccountEntry) error { func (i *accountsBatchInsertBuilder) Exec(ctx context.Context) error { return i.builder.Exec(ctx, i.session, i.table) } + +// Len returns the number of elements in the batch +func (i *accountsBatchInsertBuilder) Len() int { + return i.builder.Len() +} diff --git a/services/horizon/internal/db2/history/claimable_balance_batch_insert_builder.go b/services/horizon/internal/db2/history/claimable_balance_batch_insert_builder.go index 515cc1643c..8af9cf06f7 100644 --- a/services/horizon/internal/db2/history/claimable_balance_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/claimable_balance_batch_insert_builder.go @@ -11,6 +11,7 @@ import ( type ClaimableBalanceBatchInsertBuilder interface { Add(claimableBalance ClaimableBalance) error Exec(ctx context.Context) error + Len() int } // ClaimableBalanceBatchInsertBuilder is a simple wrapper around db.FastBatchInsertBuilder @@ -38,3 +39,8 @@ func (i *claimableBalanceBatchInsertBuilder) Add(claimableBalance ClaimableBalan func (i *claimableBalanceBatchInsertBuilder) Exec(ctx context.Context) error { return i.builder.Exec(ctx, i.session, i.table) } + +// Len returns the number of items in the batch. +func (i *claimableBalanceBatchInsertBuilder) Len() int { + return i.builder.Len() +} diff --git a/services/horizon/internal/db2/history/claimable_balance_claimant_batch_insert_builder.go b/services/horizon/internal/db2/history/claimable_balance_claimant_batch_insert_builder.go index 2f4dc6733c..f7e02c4c26 100644 --- a/services/horizon/internal/db2/history/claimable_balance_claimant_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/claimable_balance_claimant_batch_insert_builder.go @@ -11,6 +11,7 @@ import ( type ClaimableBalanceClaimantBatchInsertBuilder interface { Add(claimableBalanceClaimant ClaimableBalanceClaimant) error Exec(ctx context.Context) error + Len() int } // ClaimableBalanceClaimantBatchInsertBuilder is a simple wrapper around db.FastBatchInsertBuilder @@ -38,3 +39,8 @@ func (i *claimableBalanceClaimantBatchInsertBuilder) Add(claimableBalanceClaiman func (i *claimableBalanceClaimantBatchInsertBuilder) Exec(ctx context.Context) error { return i.builder.Exec(ctx, i.session, i.table) } + +// Len returns the number of items in the batch. +func (i *claimableBalanceClaimantBatchInsertBuilder) Len() int { + return i.builder.Len() +} diff --git a/services/horizon/internal/db2/history/main.go b/services/horizon/internal/db2/history/main.go index d9c5ea7557..08c80a0385 100644 --- a/services/horizon/internal/db2/history/main.go +++ b/services/horizon/internal/db2/history/main.go @@ -326,6 +326,7 @@ type AccountSigner struct { type AccountSignersBatchInsertBuilder interface { Add(signer AccountSigner) error Exec(ctx context.Context) error + Len() int } // accountSignersBatchInsertBuilder is a simple wrapper around db.BatchInsertBuilder @@ -799,7 +800,7 @@ type QSigners interface { AccountsForSigner(ctx context.Context, signer string, page db2.PageQuery) ([]AccountSigner, error) NewAccountSignersBatchInsertBuilder() AccountSignersBatchInsertBuilder CreateAccountSigner(ctx context.Context, account, signer string, weight int32, sponsor *string) (int64, error) - RemoveAccountSigner(ctx context.Context, account, signer string) (int64, error) + RemoveAccountSigners(ctx context.Context, account string, signers []string) (int64, error) SignersForAccounts(ctx context.Context, accounts []string) ([]AccountSigner, error) CountAccounts(ctx context.Context) (int, error) } diff --git a/services/horizon/internal/db2/history/mock_account_data_batch_insert_builder.go b/services/horizon/internal/db2/history/mock_account_data_batch_insert_builder.go index aa5af10730..ef719b7caa 100644 --- a/services/horizon/internal/db2/history/mock_account_data_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/mock_account_data_batch_insert_builder.go @@ -19,3 +19,8 @@ func (m *MockAccountDataBatchInsertBuilder) Exec(ctx context.Context) error { a := m.Called(ctx) return a.Error(0) } + +func (m *MockAccountDataBatchInsertBuilder) Len() int { + a := m.Called() + return a.Int(0) +} diff --git a/services/horizon/internal/db2/history/mock_account_signers_batch_insert_builder.go b/services/horizon/internal/db2/history/mock_account_signers_batch_insert_builder.go index 9ce4fd7d4c..9bbe7c77cb 100644 --- a/services/horizon/internal/db2/history/mock_account_signers_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/mock_account_signers_batch_insert_builder.go @@ -2,6 +2,7 @@ package history import ( "context" + "github.com/stretchr/testify/mock" ) @@ -18,3 +19,8 @@ func (m *MockAccountSignersBatchInsertBuilder) Exec(ctx context.Context) error { a := m.Called(ctx) return a.Error(0) } + +func (m *MockAccountSignersBatchInsertBuilder) Len() int { + a := m.Called() + return a.Get(0).(int) +} diff --git a/services/horizon/internal/db2/history/mock_accounts_batch_insert_builder.go b/services/horizon/internal/db2/history/mock_accounts_batch_insert_builder.go index a200a15e15..e0199d4867 100644 --- a/services/horizon/internal/db2/history/mock_accounts_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/mock_accounts_batch_insert_builder.go @@ -19,3 +19,8 @@ func (m *MockAccountsBatchInsertBuilder) Exec(ctx context.Context) error { a := m.Called(ctx) return a.Error(0) } + +func (m *MockAccountsBatchInsertBuilder) Len() int { + a := m.Called() + return a.Int(0) +} diff --git a/services/horizon/internal/db2/history/mock_claimable_balance_batch_insert_builder.go b/services/horizon/internal/db2/history/mock_claimable_balance_batch_insert_builder.go index 0c1a1e7049..0f18ec4ae5 100644 --- a/services/horizon/internal/db2/history/mock_claimable_balance_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/mock_claimable_balance_batch_insert_builder.go @@ -19,3 +19,9 @@ func (m *MockClaimableBalanceBatchInsertBuilder) Exec(ctx context.Context) error a := m.Called(ctx) return a.Error(0) } + +// Len returns the number of items in the batch. +func (m *MockClaimableBalanceBatchInsertBuilder) Len() int { + a := m.Called() + return a.Int(0) +} diff --git a/services/horizon/internal/db2/history/mock_claimable_balance_claimant_batch_insert_builder.go b/services/horizon/internal/db2/history/mock_claimable_balance_claimant_batch_insert_builder.go index 3c8fb24d3c..66f7ac021e 100644 --- a/services/horizon/internal/db2/history/mock_claimable_balance_claimant_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/mock_claimable_balance_claimant_batch_insert_builder.go @@ -2,6 +2,7 @@ package history import ( "context" + "github.com/stretchr/testify/mock" ) @@ -18,3 +19,9 @@ func (m *MockClaimableBalanceClaimantBatchInsertBuilder) Exec(ctx context.Contex a := m.Called(ctx) return a.Error(0) } + +// Len returns the number of items in the batch. +func (m *MockClaimableBalanceClaimantBatchInsertBuilder) Len() int { + a := m.Called() + return a.Int(0) +} diff --git a/services/horizon/internal/db2/history/mock_offers_batch_insert_builder.go b/services/horizon/internal/db2/history/mock_offers_batch_insert_builder.go index 5caa81f008..4c205200ba 100644 --- a/services/horizon/internal/db2/history/mock_offers_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/mock_offers_batch_insert_builder.go @@ -19,3 +19,8 @@ func (m *MockOffersBatchInsertBuilder) Exec(ctx context.Context) error { a := m.Called(ctx) return a.Error(0) } + +func (m *MockOffersBatchInsertBuilder) Len() int { + a := m.Called() + return a.Int(0) +} diff --git a/services/horizon/internal/db2/history/mock_q_signers.go b/services/horizon/internal/db2/history/mock_q_signers.go index 1b2a4864bc..51f526fdae 100644 --- a/services/horizon/internal/db2/history/mock_q_signers.go +++ b/services/horizon/internal/db2/history/mock_q_signers.go @@ -42,8 +42,8 @@ func (m *MockQSigners) CreateAccountSigner(ctx context.Context, account, signer return a.Get(0).(int64), a.Error(1) } -func (m *MockQSigners) RemoveAccountSigner(ctx context.Context, account, signer string) (int64, error) { - a := m.Called(ctx, account, signer) +func (m *MockQSigners) RemoveAccountSigners(ctx context.Context, account string, signers []string) (int64, error) { + a := m.Called(ctx, account, signers) return a.Get(0).(int64), a.Error(1) } diff --git a/services/horizon/internal/db2/history/mock_trust_lines_batch_insert_builder.go b/services/horizon/internal/db2/history/mock_trust_lines_batch_insert_builder.go index 38e1b41db0..c244a340e9 100644 --- a/services/horizon/internal/db2/history/mock_trust_lines_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/mock_trust_lines_batch_insert_builder.go @@ -19,3 +19,8 @@ func (m *MockTrustLinesBatchInsertBuilder) Exec(ctx context.Context) error { a := m.Called(ctx) return a.Error(0) } + +func (m *MockTrustLinesBatchInsertBuilder) Len() int { + a := m.Called() + return a.Int(0) +} diff --git a/services/horizon/internal/db2/history/offers_batch_insert_builder.go b/services/horizon/internal/db2/history/offers_batch_insert_builder.go index 8a345d08a5..c2bfc95432 100644 --- a/services/horizon/internal/db2/history/offers_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/offers_batch_insert_builder.go @@ -10,6 +10,7 @@ import ( type OffersBatchInsertBuilder interface { Add(offer Offer) error Exec(ctx context.Context) error + Len() int } // OffersBatchInsertBuilder is a simple wrapper around db.FastBatchInsertBuilder @@ -37,3 +38,8 @@ func (i *offersBatchInsertBuilder) Add(offer Offer) error { func (i *offersBatchInsertBuilder) Exec(ctx context.Context) error { return i.builder.Exec(ctx, i.session, i.table) } + +// Len returns the number of items in the batch. +func (i *offersBatchInsertBuilder) Len() int { + return i.builder.Len() +} diff --git a/services/horizon/internal/db2/history/trust_lines_batch_insert_builder.go b/services/horizon/internal/db2/history/trust_lines_batch_insert_builder.go index 2c77469775..802084c23a 100644 --- a/services/horizon/internal/db2/history/trust_lines_batch_insert_builder.go +++ b/services/horizon/internal/db2/history/trust_lines_batch_insert_builder.go @@ -10,6 +10,7 @@ import ( type TrustLinesBatchInsertBuilder interface { Add(line TrustLine) error Exec(ctx context.Context) error + Len() int } // trustLinesBatchInsertBuilder is a simple wrapper around db.FastBatchInsertBuilder @@ -37,3 +38,8 @@ func (i *trustLinesBatchInsertBuilder) Add(line TrustLine) error { func (i *trustLinesBatchInsertBuilder) Exec(ctx context.Context) error { return i.builder.Exec(ctx, i.session, i.table) } + +// Len returns the number of items in the batch. +func (i *trustLinesBatchInsertBuilder) Len() int { + return i.builder.Len() +} diff --git a/services/horizon/internal/ingest/processor_runner.go b/services/horizon/internal/ingest/processor_runner.go index 649b345b0c..e26db5fe31 100644 --- a/services/horizon/internal/ingest/processor_runner.go +++ b/services/horizon/internal/ingest/processor_runner.go @@ -120,14 +120,13 @@ func buildChangeProcessor( StatsChangeProcessor: changeStats, } - useLedgerCache := source == ledgerSource return newGroupChangeProcessors([]horizonChangeProcessor{ statsChangeProcessor, processors.NewAccountDataProcessor(historyQ), processors.NewAccountsProcessor(historyQ), processors.NewOffersProcessor(historyQ, ledgerSequence), processors.NewAssetStatsProcessor(historyQ, networkPassphrase, source == historyArchiveSource, ledgerSequence), - processors.NewSignersProcessor(historyQ, useLedgerCache), + processors.NewSignersProcessor(historyQ), processors.NewTrustLinesProcessor(historyQ), processors.NewClaimableBalancesChangeProcessor(historyQ), processors.NewLiquidityPoolsChangeProcessor(historyQ, ledgerSequence), @@ -301,15 +300,10 @@ func (s *ProcessorRunner) runChangeProcessorOnLedger( if err != nil { return errors.Wrap(err, "Error creating ledger change reader") } - changeReader = newloggingChangeReader( - changeReader, - "ledger", - ledger.LedgerSequence(), - logFrequency, - s.logMemoryStats, - ) + changeReader = ingest.NewCompactingChangeReader(changeReader) if err = processors.StreamChanges(s.ctx, changeProcessor, changeReader); err != nil { return errors.Wrap(err, "Error streaming changes from ledger") + } err = changeProcessor.Commit(s.ctx) diff --git a/services/horizon/internal/ingest/processor_runner_test.go b/services/horizon/internal/ingest/processor_runner_test.go index eaeca95661..cad841b602 100644 --- a/services/horizon/internal/ingest/processor_runner_test.go +++ b/services/horizon/internal/ingest/processor_runner_test.go @@ -191,8 +191,6 @@ func TestProcessorRunnerBuildChangeProcessor(t *testing.T) { assert.False(t, reflect.ValueOf(processor.processors[4]). Elem().FieldByName("ingestFromHistoryArchive").Bool()) assert.IsType(t, &processors.SignersProcessor{}, processor.processors[5]) - assert.True(t, reflect.ValueOf(processor.processors[5]). - Elem().FieldByName("useLedgerEntryCache").Bool()) assert.IsType(t, &processors.TrustLinesProcessor{}, processor.processors[6]) runner = ProcessorRunner{ @@ -212,8 +210,6 @@ func TestProcessorRunnerBuildChangeProcessor(t *testing.T) { assert.True(t, reflect.ValueOf(processor.processors[4]). Elem().FieldByName("ingestFromHistoryArchive").Bool()) assert.IsType(t, &processors.SignersProcessor{}, processor.processors[5]) - assert.False(t, reflect.ValueOf(processor.processors[5]). - Elem().FieldByName("useLedgerEntryCache").Bool()) assert.IsType(t, &processors.TrustLinesProcessor{}, processor.processors[6]) } @@ -605,6 +601,7 @@ func mockTxProcessorBatchBuilders(q *mockDBQ, mockSession *db.MockSession, ctx c func mockChangeProcessorBatchBuilders(q *mockDBQ, ctx context.Context, mockExec bool) []interface{} { mockAccountSignersBatchInsertBuilder := &history.MockAccountSignersBatchInsertBuilder{} + mockAccountSignersBatchInsertBuilder.On("Len").Return(1).Maybe() if mockExec { mockAccountSignersBatchInsertBuilder.On("Exec", ctx).Return(nil).Once() } @@ -612,6 +609,7 @@ func mockChangeProcessorBatchBuilders(q *mockDBQ, ctx context.Context, mockExec Return(mockAccountSignersBatchInsertBuilder).Twice() mockAccountsBatchInsertBuilder := &history.MockAccountsBatchInsertBuilder{} + mockAccountsBatchInsertBuilder.On("Len").Return(1).Maybe() if mockExec { mockAccountsBatchInsertBuilder.On("Exec", ctx).Return(nil).Once() } diff --git a/services/horizon/internal/ingest/processors/account_data_processor.go b/services/horizon/internal/ingest/processors/account_data_processor.go index 5067935648..b738bb71ec 100644 --- a/services/horizon/internal/ingest/processors/account_data_processor.go +++ b/services/horizon/internal/ingest/processors/account_data_processor.go @@ -12,8 +12,9 @@ import ( type AccountDataProcessor struct { dataQ history.QData - cache *ingest.ChangeCompactor batchInsertBuilder history.AccountDataBatchInsertBuilder + dataToUpdate []history.Data + dataToDelete []history.AccountDataKey } func NewAccountDataProcessor(dataQ history.QData) *AccountDataProcessor { @@ -23,8 +24,9 @@ func NewAccountDataProcessor(dataQ history.QData) *AccountDataProcessor { } func (p *AccountDataProcessor) reset() { - p.cache = ingest.NewChangeCompactor() p.batchInsertBuilder = p.dataQ.NewAccountDataBatchInsertBuilder() + p.dataToUpdate = []history.Data{} + p.dataToDelete = []history.AccountDataKey{} } func (p *AccountDataProcessor) Name() string { @@ -37,14 +39,29 @@ func (p *AccountDataProcessor) ProcessChange(ctx context.Context, change ingest. return nil } - err := p.cache.AddChange(change) - if err != nil { - return errors.Wrap(err, "error adding to ledgerCache") + switch { + case change.Pre == nil && change.Post != nil: + // Created + err := p.batchInsertBuilder.Add(p.ledgerEntryToRow(change.Post)) + if err != nil { + return errors.Wrap(err, "Error adding to AccountDataBatchInsertBuilder") + } + case change.Pre != nil && change.Post == nil: + // Removed + data := change.Pre.Data.MustData() + key := history.AccountDataKey{ + AccountID: data.AccountId.Address(), + DataName: string(data.DataName), + } + p.dataToDelete = append(p.dataToDelete, key) + default: + // Updated + p.dataToUpdate = append(p.dataToUpdate, p.ledgerEntryToRow(change.Post)) } - if p.cache.Size() > maxBatchSize { - err = p.Commit(ctx) - if err != nil { + if p.batchInsertBuilder.Len()+len(p.dataToUpdate)+len(p.dataToDelete) > maxBatchSize { + + if err := p.Commit(ctx); err != nil { return errors.Wrap(err, "error in Commit") } } @@ -54,54 +71,28 @@ func (p *AccountDataProcessor) ProcessChange(ctx context.Context, change ingest. func (p *AccountDataProcessor) Commit(ctx context.Context) error { defer p.reset() - var ( - datasToUpsert []history.Data - datasToDelete []history.AccountDataKey - ) - changes := p.cache.GetChanges() - for _, change := range changes { - switch { - case change.Pre == nil && change.Post != nil: - // Created - err := p.batchInsertBuilder.Add(p.ledgerEntryToRow(change.Post)) - if err != nil { - return errors.Wrap(err, "Error adding to AccountDataBatchInsertBuilder") - } - case change.Pre != nil && change.Post == nil: - // Removed - data := change.Pre.Data.MustData() - key := history.AccountDataKey{ - AccountID: data.AccountId.Address(), - DataName: string(data.DataName), - } - datasToDelete = append(datasToDelete, key) - default: - // Updated - datasToUpsert = append(datasToUpsert, p.ledgerEntryToRow(change.Post)) - } - } err := p.batchInsertBuilder.Exec(ctx) if err != nil { return errors.Wrap(err, "Error executing AccountDataBatchInsertBuilder") } - if len(datasToUpsert) > 0 { - if err := p.dataQ.UpsertAccountData(ctx, datasToUpsert); err != nil { + if len(p.dataToUpdate) > 0 { + if err := p.dataQ.UpsertAccountData(ctx, p.dataToUpdate); err != nil { return errors.Wrap(err, "error executing upsert") } } - if len(datasToDelete) > 0 { - count, err := p.dataQ.RemoveAccountData(ctx, datasToDelete) + if len(p.dataToDelete) > 0 { + count, err := p.dataQ.RemoveAccountData(ctx, p.dataToDelete) if err != nil { return errors.Wrap(err, "error executing removal") } - if count != int64(len(datasToDelete)) { + if count != int64(len(p.dataToDelete)) { return ingest.NewStateError(errors.Errorf( "%d rows affected when deleting %d account data", count, - len(datasToDelete), + len(p.dataToDelete), )) } } diff --git a/services/horizon/internal/ingest/processors/accounts_data_processor_test.go b/services/horizon/internal/ingest/processors/accounts_data_processor_test.go index 273cfcca4b..a3efef186e 100644 --- a/services/horizon/internal/ingest/processors/accounts_data_processor_test.go +++ b/services/horizon/internal/ingest/processors/accounts_data_processor_test.go @@ -6,10 +6,11 @@ import ( "context" "testing" + "github.com/stretchr/testify/suite" + "github.com/stellar/go/ingest" "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/xdr" - "github.com/stretchr/testify/suite" ) func TestAccountsDataProcessorTestSuiteState(t *testing.T) { @@ -32,6 +33,7 @@ func (s *AccountsDataProcessorTestSuiteState) SetupTest() { s.mockQ.On("NewAccountDataBatchInsertBuilder"). Return(s.mockAccountDataBatchInsertBuilder) s.mockAccountDataBatchInsertBuilder.On("Exec", s.ctx).Return(nil) + s.mockAccountDataBatchInsertBuilder.On("Len").Return(1).Maybe() s.processor = NewAccountDataProcessor(s.mockQ) } @@ -96,6 +98,7 @@ func (s *AccountsDataProcessorTestSuiteLedger) SetupTest() { s.mockQ.On("NewAccountDataBatchInsertBuilder"). Return(s.mockAccountDataBatchInsertBuilder) s.mockAccountDataBatchInsertBuilder.On("Exec", s.ctx).Return(nil) + s.mockAccountDataBatchInsertBuilder.On("Len").Return(1).Maybe() s.processor = NewAccountDataProcessor(s.mockQ) } @@ -117,6 +120,14 @@ func (s *AccountsDataProcessorTestSuiteLedger) TestNewAccountData() { } lastModifiedLedgerSeq := xdr.Uint32(123) + historyData := history.Data{ + AccountID: data.AccountId.Address(), + Name: string(data.DataName), + Value: history.AccountDataValue(data.DataValue), + LastModifiedLedger: uint32(lastModifiedLedgerSeq), + } + s.mockAccountDataBatchInsertBuilder.On("Add", historyData).Return(nil).Once() + err := s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeData, Pre: nil, @@ -129,42 +140,6 @@ func (s *AccountsDataProcessorTestSuiteLedger) TestNewAccountData() { }, }) s.Assert().NoError(err) - - updatedData := xdr.DataEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - DataName: "test", - DataValue: []byte{2, 2, 2, 2}, - } - - updatedEntry := xdr.LedgerEntry{ - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeData, - Data: &updatedData, - }, - LastModifiedLedgerSeq: lastModifiedLedgerSeq, - } - - err = s.processor.ProcessChange(s.ctx, ingest.Change{ - Type: xdr.LedgerEntryTypeData, - Pre: &xdr.LedgerEntry{ - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeData, - Data: &data, - }, - LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, - }, - Post: &updatedEntry, - }) - s.Assert().NoError(err) - - // We use LedgerEntryChangesCache so all changes are squashed - historyData := history.Data{ - AccountID: updatedData.AccountId.Address(), - Name: string(updatedData.DataName), - Value: history.AccountDataValue(updatedData.DataValue), - LastModifiedLedger: uint32(updatedEntry.LastModifiedLedgerSeq), - } - s.mockAccountDataBatchInsertBuilder.On("Add", historyData).Return(nil).Once() } func (s *AccountsDataProcessorTestSuiteLedger) TestUpdateAccountData() { diff --git a/services/horizon/internal/ingest/processors/accounts_processor.go b/services/horizon/internal/ingest/processors/accounts_processor.go index 17128f80a5..3c621c8bda 100644 --- a/services/horizon/internal/ingest/processors/accounts_processor.go +++ b/services/horizon/internal/ingest/processors/accounts_processor.go @@ -14,8 +14,9 @@ import ( type AccountsProcessor struct { accountsQ history.QAccounts - cache *ingest.ChangeCompactor - batchInsertBuilder history.AccountsBatchInsertBuilder + batchUpdateAccounts []history.AccountEntry + removeBatch []string + batchInsertBuilder history.AccountsBatchInsertBuilder } func NewAccountsProcessor(accountsQ history.QAccounts) *AccountsProcessor { @@ -25,8 +26,9 @@ func NewAccountsProcessor(accountsQ history.QAccounts) *AccountsProcessor { } func (p *AccountsProcessor) reset() { - p.cache = ingest.NewChangeCompactor() p.batchInsertBuilder = p.accountsQ.NewAccountsBatchInsertBuilder() + p.batchUpdateAccounts = []history.AccountEntry{} + p.removeBatch = []string{} } func (p *AccountsProcessor) Name() string { @@ -38,12 +40,37 @@ func (p *AccountsProcessor) ProcessChange(ctx context.Context, change ingest.Cha return nil } - err := p.cache.AddChange(change) + changed, err := change.AccountChangedExceptSigners() if err != nil { - return errors.Wrap(err, "error adding to ledgerCache") + return errors.Wrap(err, "Error running change.AccountChangedExceptSigners") } - if p.cache.Size() > maxBatchSize { + if !changed { + return nil + } + + switch { + case change.Pre == nil && change.Post != nil: + // Created + row := p.ledgerEntryToRow(*change.Post) + err = p.batchInsertBuilder.Add(row) + if err != nil { + return errors.Wrap(err, "Error adding to AccountsBatchInsertBuilder") + } + case change.Pre != nil && change.Post != nil: + // Updated + row := p.ledgerEntryToRow(*change.Post) + p.batchUpdateAccounts = append(p.batchUpdateAccounts, row) + case change.Pre != nil && change.Post == nil: + // Removed + account := change.Pre.Data.MustAccount() + accountID := account.AccountId.Address() + p.removeBatch = append(p.removeBatch, accountID) + default: + return errors.New("Invalid io.Change: change.Pre == nil && change.Post == nil") + } + + if p.batchInsertBuilder.Len()+len(p.batchUpdateAccounts)+len(p.removeBatch) > maxBatchSize { err = p.Commit(ctx) if err != nil { return errors.Wrap(err, "error in Commit") @@ -56,66 +83,30 @@ func (p *AccountsProcessor) ProcessChange(ctx context.Context, change ingest.Cha func (p *AccountsProcessor) Commit(ctx context.Context) error { defer p.reset() - batchUpsertAccounts := []history.AccountEntry{} - removeBatch := []string{} - - changes := p.cache.GetChanges() - for _, change := range changes { - changed, err := change.AccountChangedExceptSigners() - if err != nil { - return errors.Wrap(err, "Error running change.AccountChangedExceptSigners") - } - - if !changed { - continue - } - - switch { - case change.Pre == nil && change.Post != nil: - // Created - row := p.ledgerEntryToRow(*change.Post) - err := p.batchInsertBuilder.Add(row) - if err != nil { - return errors.Wrap(err, "Error adding to AccountsBatchInsertBuilder") - } - case change.Pre != nil && change.Post != nil: - // Updated - row := p.ledgerEntryToRow(*change.Post) - batchUpsertAccounts = append(batchUpsertAccounts, row) - case change.Pre != nil && change.Post == nil: - // Removed - account := change.Pre.Data.MustAccount() - accountID := account.AccountId.Address() - removeBatch = append(removeBatch, accountID) - default: - return errors.New("Invalid io.Change: change.Pre == nil && change.Post == nil") - } - } - err := p.batchInsertBuilder.Exec(ctx) if err != nil { return errors.Wrap(err, "Error executing AccountsBatchInsertBuilder") } // Upsert accounts - if len(batchUpsertAccounts) > 0 { - err := p.accountsQ.UpsertAccounts(ctx, batchUpsertAccounts) + if len(p.batchUpdateAccounts) > 0 { + err := p.accountsQ.UpsertAccounts(ctx, p.batchUpdateAccounts) if err != nil { return errors.Wrap(err, "errors in UpsertAccounts") } } - if len(removeBatch) > 0 { - rowsAffected, err := p.accountsQ.RemoveAccounts(ctx, removeBatch) + if len(p.removeBatch) > 0 { + rowsAffected, err := p.accountsQ.RemoveAccounts(ctx, p.removeBatch) if err != nil { return errors.Wrap(err, "error in RemoveAccounts") } - if rowsAffected != int64(len(removeBatch)) { + if rowsAffected != int64(len(p.removeBatch)) { return ingest.NewStateError(errors.Errorf( "%d rows affected when removing %d accounts", rowsAffected, - len(removeBatch), + len(p.removeBatch), )) } } diff --git a/services/horizon/internal/ingest/processors/accounts_processor_test.go b/services/horizon/internal/ingest/processors/accounts_processor_test.go index 26f6af3487..d99ee4400b 100644 --- a/services/horizon/internal/ingest/processors/accounts_processor_test.go +++ b/services/horizon/internal/ingest/processors/accounts_processor_test.go @@ -7,10 +7,11 @@ import ( "testing" "github.com/guregu/null/zero" + "github.com/stretchr/testify/suite" + "github.com/stellar/go/ingest" "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/xdr" - "github.com/stretchr/testify/suite" ) func TestAccountsProcessorTestSuiteState(t *testing.T) { @@ -32,6 +33,7 @@ func (s *AccountsProcessorTestSuiteState) SetupTest() { s.mockAccountsBatchInsertBuilder = &history.MockAccountsBatchInsertBuilder{} s.mockQ.On("NewAccountsBatchInsertBuilder").Return(s.mockAccountsBatchInsertBuilder).Twice() s.mockAccountsBatchInsertBuilder.On("Exec", s.ctx).Return(nil).Once() + s.mockAccountsBatchInsertBuilder.On("Len").Return(1).Maybe() s.processor = NewAccountsProcessor(s.mockQ) } @@ -92,6 +94,7 @@ func (s *AccountsProcessorTestSuiteLedger) SetupTest() { s.mockAccountsBatchInsertBuilder = &history.MockAccountsBatchInsertBuilder{} s.mockQ.On("NewAccountsBatchInsertBuilder").Return(s.mockAccountsBatchInsertBuilder).Twice() s.mockAccountsBatchInsertBuilder.On("Exec", s.ctx).Return(nil).Once() + s.mockAccountsBatchInsertBuilder.On("Len").Return(1).Maybe() s.processor = NewAccountsProcessor(s.mockQ) } @@ -111,6 +114,16 @@ func (s *AccountsProcessorTestSuiteLedger) TestNewAccount() { Thresholds: [4]byte{1, 1, 1, 1}, } lastModifiedLedgerSeq := xdr.Uint32(123) + // We use LedgerEntryChangesCache so all changes are squashed + s.mockAccountsBatchInsertBuilder.On("Add", history.AccountEntry{ + AccountID: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + MasterWeight: 1, + ThresholdLow: 1, + ThresholdMedium: 1, + ThresholdHigh: 1, + HomeDomain: "", + LastModifiedLedger: uint32(123), + }).Return(nil).Once() err := s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeAccount, @@ -131,6 +144,22 @@ func (s *AccountsProcessorTestSuiteLedger) TestNewAccount() { HomeDomain: "stellar.org", } + s.mockQ.On( + "UpsertAccounts", + s.ctx, + []history.AccountEntry{ + { + AccountID: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + MasterWeight: 0, + ThresholdLow: 1, + ThresholdMedium: 2, + ThresholdHigh: 3, + HomeDomain: "stellar.org", + LastModifiedLedger: uint32(123), + }, + }, + ).Return(nil).Once() + err = s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeAccount, Pre: &xdr.LedgerEntry{ @@ -149,17 +178,6 @@ func (s *AccountsProcessorTestSuiteLedger) TestNewAccount() { }, }) s.Assert().NoError(err) - - // We use LedgerEntryChangesCache so all changes are squashed - s.mockAccountsBatchInsertBuilder.On("Add", history.AccountEntry{ - AccountID: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - MasterWeight: 0, - ThresholdLow: 1, - ThresholdMedium: 2, - ThresholdHigh: 3, - HomeDomain: "stellar.org", - LastModifiedLedger: uint32(123), - }).Return(nil).Once() } func (s *AccountsProcessorTestSuiteLedger) TestNewAccountUpgrade() { @@ -178,6 +196,16 @@ func (s *AccountsProcessorTestSuiteLedger) TestNewAccountUpgrade() { } lastModifiedLedgerSeq := xdr.Uint32(123) + s.mockAccountsBatchInsertBuilder.On("Add", history.AccountEntry{ + AccountID: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + MasterWeight: 1, + ThresholdLow: 1, + ThresholdMedium: 1, + ThresholdHigh: 1, + HomeDomain: "", + LastModifiedLedger: uint32(123), + }).Return(nil).Once() + err := s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeAccount, Pre: nil, @@ -214,6 +242,24 @@ func (s *AccountsProcessorTestSuiteLedger) TestNewAccountUpgrade() { }, } + s.mockQ.On( + "UpsertAccounts", + s.ctx, + []history.AccountEntry{ + { + AccountID: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + SequenceLedger: zero.IntFrom(2346), + SequenceTime: zero.IntFrom(1647265534), + MasterWeight: 0, + ThresholdLow: 1, + ThresholdMedium: 2, + ThresholdHigh: 3, + HomeDomain: "stellar.org", + LastModifiedLedger: uint32(123), + }, + }, + ).Return(nil).Once() + err = s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeAccount, Pre: &xdr.LedgerEntry{ @@ -233,18 +279,6 @@ func (s *AccountsProcessorTestSuiteLedger) TestNewAccountUpgrade() { }) s.Assert().NoError(err) - // We use LedgerEntryChangesCache so all changes are squashed - s.mockAccountsBatchInsertBuilder.On("Add", history.AccountEntry{ - AccountID: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - SequenceLedger: zero.IntFrom(2346), - SequenceTime: zero.IntFrom(1647265534), - MasterWeight: 0, - ThresholdLow: 1, - ThresholdMedium: 2, - ThresholdHigh: 3, - HomeDomain: "stellar.org", - LastModifiedLedger: uint32(123), - }).Return(nil).Once() } func (s *AccountsProcessorTestSuiteLedger) TestRemoveAccount() { @@ -277,6 +311,15 @@ func (s *AccountsProcessorTestSuiteLedger) TestProcessUpgradeChange() { } lastModifiedLedgerSeq := xdr.Uint32(123) + s.mockAccountsBatchInsertBuilder.On("Add", history.AccountEntry{ + LastModifiedLedger: uint32(lastModifiedLedgerSeq), + AccountID: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + MasterWeight: 1, + ThresholdLow: 1, + ThresholdMedium: 1, + ThresholdHigh: 1, + }).Return(nil).Once() + err := s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeAccount, Pre: nil, @@ -296,6 +339,24 @@ func (s *AccountsProcessorTestSuiteLedger) TestProcessUpgradeChange() { HomeDomain: "stellar.org", } + s.mockQ.On( + "UpsertAccounts", + s.ctx, + []history.AccountEntry{ + { + LastModifiedLedger: uint32(lastModifiedLedgerSeq) + 1, + AccountID: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + SequenceTime: zero.IntFrom(0), + SequenceLedger: zero.IntFrom(0), + MasterWeight: 0, + ThresholdLow: 1, + ThresholdMedium: 2, + ThresholdHigh: 3, + HomeDomain: "stellar.org", + }, + }, + ).Return(nil).Once() + err = s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeAccount, Pre: &xdr.LedgerEntry{ @@ -314,18 +375,6 @@ func (s *AccountsProcessorTestSuiteLedger) TestProcessUpgradeChange() { }, }) s.Assert().NoError(err) - - s.mockAccountsBatchInsertBuilder.On("Add", history.AccountEntry{ - LastModifiedLedger: uint32(lastModifiedLedgerSeq) + 1, - AccountID: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - SequenceTime: zero.IntFrom(0), - SequenceLedger: zero.IntFrom(0), - MasterWeight: 0, - ThresholdLow: 1, - ThresholdMedium: 2, - ThresholdHigh: 3, - HomeDomain: "stellar.org", - }).Return(nil).Once() } func (s *AccountsProcessorTestSuiteLedger) TestFeeProcessedBeforeEverythingElse() { @@ -379,6 +428,11 @@ func (s *AccountsProcessorTestSuiteLedger) TestFeeProcessedBeforeEverythingElse( "UpsertAccounts", s.ctx, []history.AccountEntry{ + { + LastModifiedLedger: 0, + AccountID: "GAHK7EEG2WWHVKDNT4CEQFZGKF2LGDSW2IVM4S5DP42RBW3K6BTODB4A", + Balance: 100, + }, { LastModifiedLedger: 0, AccountID: "GAHK7EEG2WWHVKDNT4CEQFZGKF2LGDSW2IVM4S5DP42RBW3K6BTODB4A", diff --git a/services/horizon/internal/ingest/processors/asset_stats_processor.go b/services/horizon/internal/ingest/processors/asset_stats_processor.go index 9423f245e9..6ae83c3a3c 100644 --- a/services/horizon/internal/ingest/processors/asset_stats_processor.go +++ b/services/horizon/internal/ingest/processors/asset_stats_processor.go @@ -15,8 +15,9 @@ import ( type AssetStatsProcessor struct { assetStatsQ history.QAssetStats - cache *ingest.ChangeCompactor currentLedger uint32 + assetStatSet AssetStatSet + contractDataChanges []ingest.Change removedExpirationEntries map[xdr.Hash]uint32 createdExpirationEntries map[xdr.Hash]uint32 updatedExpirationEntries map[xdr.Hash][2]uint32 @@ -36,7 +37,8 @@ func NewAssetStatsProcessor( assetStatsQ: assetStatsQ, ingestFromHistoryArchive: ingestFromHistoryArchive, networkPassphrase: networkPassphrase, - cache: ingest.NewChangeCompactor(), + assetStatSet: NewAssetStatSet(), + contractDataChanges: []ingest.Change{}, removedExpirationEntries: map[xdr.Hash]uint32{}, createdExpirationEntries: map[xdr.Hash]uint32{}, updatedExpirationEntries: map[xdr.Hash][2]uint32{}, @@ -56,9 +58,18 @@ func (p *AssetStatsProcessor) ProcessChange(ctx context.Context, change ingest.C change.Type != xdr.LedgerEntryTypeTtl { return nil } - // only ingest contract data entries which could be relevant to - // asset stats - if change.Type == xdr.LedgerEntryTypeContractData { + + var err error + switch change.Type { + case xdr.LedgerEntryTypeLiquidityPool: + err = p.assetStatSet.AddLiquidityPool(change) + case xdr.LedgerEntryTypeClaimableBalance: + err = p.assetStatSet.AddClaimableBalance(change) + case xdr.LedgerEntryTypeTrustline: + err = p.assetStatSet.AddTrustline(change) + case xdr.LedgerEntryTypeContractData: + // only ingest contract data entries which could be relevant to + // asset stats ledgerEntry := change.Post if ledgerEntry == nil { ledgerEntry = change.Pre @@ -68,11 +79,14 @@ func (p *AssetStatsProcessor) ProcessChange(ctx context.Context, change ingest.C if asset == nil && !balanceFound { return nil } + p.contractDataChanges = append(p.contractDataChanges, change) + case xdr.LedgerEntryTypeTtl: + err = p.addExpirationChange(change) + default: + return errors.Errorf("Change type %v is unexpected", change.Type) } - if err := p.cache.AddChange(change); err != nil { - return errors.Wrap(err, "error adding to ledgerCache") - } - return nil + + return err } func (p *AssetStatsProcessor) addExpirationChange(change ingest.Change) error { @@ -121,30 +135,6 @@ func (p *AssetStatsProcessor) addExpirationChange(change ingest.Change) error { } func (p *AssetStatsProcessor) Commit(ctx context.Context) error { - assetStatSet := NewAssetStatSet() - - changes := p.cache.GetChanges() - var contractDataChanges []ingest.Change - for _, change := range changes { - var err error - switch change.Type { - case xdr.LedgerEntryTypeLiquidityPool: - err = assetStatSet.AddLiquidityPool(change) - case xdr.LedgerEntryTypeClaimableBalance: - err = assetStatSet.AddClaimableBalance(change) - case xdr.LedgerEntryTypeTrustline: - err = assetStatSet.AddTrustline(change) - case xdr.LedgerEntryTypeContractData: - contractDataChanges = append(contractDataChanges, change) - case xdr.LedgerEntryTypeTtl: - err = p.addExpirationChange(change) - default: - return errors.Errorf("Change type %v is unexpected", change.Type) - } - if err != nil { - return errors.Wrap(err, "Error adjusting asset stat") - } - } contractAssetStatSet := NewContractAssetStatSet( p.assetStatsQ, @@ -154,18 +144,17 @@ func (p *AssetStatsProcessor) Commit(ctx context.Context) error { p.updatedExpirationEntries, p.currentLedger, ) - for _, change := range contractDataChanges { + for _, change := range p.contractDataChanges { if err := contractAssetStatSet.AddContractData(ctx, change); err != nil { return errors.Wrap(err, "Error ingesting contract data") } } - return p.updateDB(ctx, assetStatSet, contractAssetStatSet) + return p.updateDB(ctx, contractAssetStatSet) } func (p *AssetStatsProcessor) updateDB( ctx context.Context, - assetStatSet AssetStatSet, contractAssetStatSet *ContractAssetStatSet, ) error { if p.ingestFromHistoryArchive { @@ -173,7 +162,7 @@ func (p *AssetStatsProcessor) updateDB( // that there are only created ledger entries. We don't need to execute any // updates or removals on the asset stats tables. And we can also skip // ingesting restored contract balances and expired contract balances. - assetStatsDeltas := assetStatSet.All() + assetStatsDeltas := p.assetStatSet.All() if len(assetStatsDeltas) > 0 { var err error assetStatsDeltas, err = IncludeContractIDsInAssetStats( @@ -203,7 +192,7 @@ func (p *AssetStatsProcessor) updateDB( return nil } - assetStatsDeltas := assetStatSet.All() + assetStatsDeltas := p.assetStatSet.All() if err := p.updateAssetStats(ctx, assetStatsDeltas, contractAssetStatSet.contractToAsset); err != nil { return err diff --git a/services/horizon/internal/ingest/processors/asset_stats_processor_test.go b/services/horizon/internal/ingest/processors/asset_stats_processor_test.go index a1fb68237c..e8080773e8 100644 --- a/services/horizon/internal/ingest/processors/asset_stats_processor_test.go +++ b/services/horizon/internal/ingest/processors/asset_stats_processor_test.go @@ -1193,7 +1193,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestExpirationLedgerCannotDecrease( s.Assert().NoError(err) keyHash := getKeyHashForBalance(s.T(), eurID, [32]byte{1}) - s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + s.Assert().EqualError(s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeTtl, Pre: &xdr.LedgerEntry{ LastModifiedLedgerSeq: lastModifiedLedgerSeq, @@ -1215,11 +1215,8 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestExpirationLedgerCannotDecrease( }, }, }, - })) - - s.Assert().EqualError( - s.processor.Commit(s.ctx), - "Error adjusting asset stat: unexpected change in expiration ledger Pre: 2235 Post: 2234", + }), + "unexpected change in expiration ledger Pre: 2235 Post: 2234", ) } @@ -1230,7 +1227,7 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestExpirationLedgerCannotBeLessTha s.Assert().NoError(err) keyHash := getKeyHashForBalance(s.T(), eurID, [32]byte{1}) - s.Assert().NoError(s.processor.ProcessChange(s.ctx, ingest.Change{ + s.Assert().EqualError(s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeTtl, Pre: &xdr.LedgerEntry{ LastModifiedLedgerSeq: lastModifiedLedgerSeq, @@ -1252,11 +1249,8 @@ func (s *AssetStatsProcessorTestSuiteLedger) TestExpirationLedgerCannotBeLessTha }, }, }, - })) - - s.Assert().EqualError( - s.processor.Commit(s.ctx), - "Error adjusting asset stat: post expiration ledger is less than current ledger. Pre: 1230 Post: 1234 current ledger: 1235", + }), + "post expiration ledger is less than current ledger. Pre: 1230 Post: 1234 current ledger: 1235", ) } diff --git a/services/horizon/internal/ingest/processors/claimable_balances_change_processor.go b/services/horizon/internal/ingest/processors/claimable_balances_change_processor.go index fce002881c..d6da955194 100644 --- a/services/horizon/internal/ingest/processors/claimable_balances_change_processor.go +++ b/services/horizon/internal/ingest/processors/claimable_balances_change_processor.go @@ -12,7 +12,8 @@ import ( type ClaimableBalancesChangeProcessor struct { encodingBuffer *xdr.EncodingBuffer qClaimableBalances history.QClaimableBalances - cache *ingest.ChangeCompactor + cbIDsToDelete []string + updatedBalances []history.ClaimableBalance claimantsInsertBuilder history.ClaimableBalanceClaimantBatchInsertBuilder claimableBalanceInsertBuilder history.ClaimableBalanceBatchInsertBuilder } @@ -31,7 +32,8 @@ func (p *ClaimableBalancesChangeProcessor) Name() string { } func (p *ClaimableBalancesChangeProcessor) reset() { - p.cache = ingest.NewChangeCompactor() + p.cbIDsToDelete = []string{} + p.updatedBalances = []history.ClaimableBalance{} p.claimantsInsertBuilder = p.qClaimableBalances.NewClaimableBalanceClaimantBatchInsertBuilder() p.claimableBalanceInsertBuilder = p.qClaimableBalances.NewClaimableBalanceBatchInsertBuilder() } @@ -41,14 +43,50 @@ func (p *ClaimableBalancesChangeProcessor) ProcessChange(ctx context.Context, ch return nil } - err := p.cache.AddChange(change) - if err != nil { - return errors.Wrap(err, "error adding to ledgerCache") - } + switch { + case change.Pre == nil && change.Post != nil: + // Created + cb, err := p.ledgerEntryToRow(change.Post) + if err != nil { + return err + } + // Add claimable balance + if err := p.claimableBalanceInsertBuilder.Add(cb); err != nil { + return errors.Wrap(err, "error adding to ClaimableBalanceBatchInsertBuilder") + } - if p.cache.Size() > maxBatchSize { - err = p.Commit(ctx) + // Add claimants + for _, claimant := range cb.Claimants { + claimant := history.ClaimableBalanceClaimant{ + BalanceID: cb.BalanceID, + Destination: claimant.Destination, + LastModifiedLedger: cb.LastModifiedLedger, + } + + if err := p.claimantsInsertBuilder.Add(claimant); err != nil { + return errors.Wrap(err, "error adding to ClaimableBalanceClaimantBatchInsertBuilder") + } + } + case change.Pre != nil && change.Post == nil: + // Removed + cBalance := change.Pre.Data.MustClaimableBalance() + id, err := p.encodingBuffer.MarshalHex(cBalance.BalanceId) + if err != nil { + return err + } + p.cbIDsToDelete = append(p.cbIDsToDelete, id) + default: + // this case should only occur if the sponsor has changed in the claimable balance + // the other fields of a claimable balance are immutable + postCB, err := p.ledgerEntryToRow(change.Post) if err != nil { + return err + } + p.updatedBalances = append(p.updatedBalances, postCB) + } + if p.claimableBalanceInsertBuilder.Len()+p.claimantsInsertBuilder.Len()+len(p.updatedBalances)+len(p.cbIDsToDelete) > maxBatchSize { + + if err := p.Commit(ctx); err != nil { return errors.Wrap(err, "error in Commit") } } @@ -58,54 +96,6 @@ func (p *ClaimableBalancesChangeProcessor) ProcessChange(ctx context.Context, ch func (p *ClaimableBalancesChangeProcessor) Commit(ctx context.Context) error { defer p.reset() - var ( - cbIDsToDelete []string - updatedBalances []history.ClaimableBalance - ) - changes := p.cache.GetChanges() - for _, change := range changes { - switch { - case change.Pre == nil && change.Post != nil: - // Created - cb, err := p.ledgerEntryToRow(change.Post) - if err != nil { - return err - } - // Add claimable balance - if err := p.claimableBalanceInsertBuilder.Add(cb); err != nil { - return errors.Wrap(err, "error adding to ClaimableBalanceBatchInsertBuilder") - } - - // Add claimants - for _, claimant := range cb.Claimants { - claimant := history.ClaimableBalanceClaimant{ - BalanceID: cb.BalanceID, - Destination: claimant.Destination, - LastModifiedLedger: cb.LastModifiedLedger, - } - - if err := p.claimantsInsertBuilder.Add(claimant); err != nil { - return errors.Wrap(err, "error adding to ClaimableBalanceClaimantBatchInsertBuilder") - } - } - case change.Pre != nil && change.Post == nil: - // Removed - cBalance := change.Pre.Data.MustClaimableBalance() - id, err := p.encodingBuffer.MarshalHex(cBalance.BalanceId) - if err != nil { - return err - } - cbIDsToDelete = append(cbIDsToDelete, id) - default: - // this case should only occur if the sponsor has changed in the claimable balance - // the other fields of a claimable balance are immutable - postCB, err := p.ledgerEntryToRow(change.Post) - if err != nil { - return err - } - updatedBalances = append(updatedBalances, postCB) - } - } err := p.claimantsInsertBuilder.Exec(ctx) if err != nil { @@ -117,27 +107,27 @@ func (p *ClaimableBalancesChangeProcessor) Commit(ctx context.Context) error { return errors.Wrap(err, "error executing ClaimableBalanceBatchInsertBuilder") } - if len(updatedBalances) > 0 { - if err = p.qClaimableBalances.UpsertClaimableBalances(ctx, updatedBalances); err != nil { + if len(p.updatedBalances) > 0 { + if err = p.qClaimableBalances.UpsertClaimableBalances(ctx, p.updatedBalances); err != nil { return errors.Wrap(err, "error updating claimable balances") } } - if len(cbIDsToDelete) > 0 { - count, err := p.qClaimableBalances.RemoveClaimableBalances(ctx, cbIDsToDelete) + if len(p.cbIDsToDelete) > 0 { + count, err := p.qClaimableBalances.RemoveClaimableBalances(ctx, p.cbIDsToDelete) if err != nil { return errors.Wrap(err, "error executing removal") } - if count != int64(len(cbIDsToDelete)) { + if count != int64(len(p.cbIDsToDelete)) { return ingest.NewStateError(errors.Errorf( "%d rows affected when deleting %d claimable balances", count, - len(cbIDsToDelete), + len(p.cbIDsToDelete), )) } // Remove ClaimableBalanceClaimants - _, err = p.qClaimableBalances.RemoveClaimableBalanceClaimants(ctx, cbIDsToDelete) + _, err = p.qClaimableBalances.RemoveClaimableBalanceClaimants(ctx, p.cbIDsToDelete) if err != nil { return errors.Wrap(err, "error executing removal of claimants") } diff --git a/services/horizon/internal/ingest/processors/claimable_balances_change_processor_test.go b/services/horizon/internal/ingest/processors/claimable_balances_change_processor_test.go index a10cc9db7d..518a230bc1 100644 --- a/services/horizon/internal/ingest/processors/claimable_balances_change_processor_test.go +++ b/services/horizon/internal/ingest/processors/claimable_balances_change_processor_test.go @@ -43,6 +43,8 @@ func (s *ClaimableBalancesChangeProcessorTestSuiteState) SetupTest() { s.mockClaimantsBatchInsertBuilder.On("Exec", s.ctx).Return(nil) s.mockClaimableBalanceBatchInsertBuilder.On("Exec", s.ctx).Return(nil) + s.mockClaimantsBatchInsertBuilder.On("Len").Return(1).Maybe() + s.mockClaimableBalanceBatchInsertBuilder.On("Len").Return(1).Maybe() s.processor = NewClaimableBalancesChangeProcessor(s.mockQ) } @@ -140,6 +142,8 @@ func (s *ClaimableBalancesChangeProcessorTestSuiteLedger) SetupTest() { s.mockClaimantsBatchInsertBuilder.On("Exec", s.ctx).Return(nil) s.mockClaimableBalanceBatchInsertBuilder.On("Exec", s.ctx).Return(nil) + s.mockClaimantsBatchInsertBuilder.On("Len").Return(1).Maybe() + s.mockClaimableBalanceBatchInsertBuilder.On("Len").Return(1).Maybe() s.processor = NewClaimableBalancesChangeProcessor(s.mockQ) } diff --git a/services/horizon/internal/ingest/processors/liquidity_pools_change_processor.go b/services/horizon/internal/ingest/processors/liquidity_pools_change_processor.go index 3dce45aa8e..ec2f315a63 100644 --- a/services/horizon/internal/ingest/processors/liquidity_pools_change_processor.go +++ b/services/horizon/internal/ingest/processors/liquidity_pools_change_processor.go @@ -11,7 +11,7 @@ import ( type LiquidityPoolsChangeProcessor struct { qLiquidityPools history.QLiquidityPools - cache *ingest.ChangeCompactor + lps []history.LiquidityPool sequence uint32 } @@ -29,7 +29,7 @@ func (p *LiquidityPoolsChangeProcessor) Name() string { } func (p *LiquidityPoolsChangeProcessor) reset() { - p.cache = ingest.NewChangeCompactor() + p.lps = []history.LiquidityPool{} } func (p *LiquidityPoolsChangeProcessor) ProcessChange(ctx context.Context, change ingest.Change) error { @@ -37,45 +37,34 @@ func (p *LiquidityPoolsChangeProcessor) ProcessChange(ctx context.Context, chang return nil } - err := p.cache.AddChange(change) - if err != nil { - return errors.Wrap(err, "error adding to ledgerCache") + switch { + case change.Pre == nil && change.Post != nil: + // Created + p.lps = append(p.lps, p.ledgerEntryToRow(change.Post)) + case change.Pre != nil && change.Post == nil: + // Removed + lp := p.ledgerEntryToRow(change.Pre) + lp.Deleted = true + lp.LastModifiedLedger = p.sequence + p.lps = append(p.lps, lp) + default: + // Updated + p.lps = append(p.lps, p.ledgerEntryToRow(change.Post)) } - if p.cache.Size() > maxBatchSize { - err = p.Commit(ctx) - if err != nil { + if len(p.lps) > maxBatchSize { + if err := p.Commit(ctx); err != nil { return errors.Wrap(err, "error in Commit") } - p.reset() } return nil } func (p *LiquidityPoolsChangeProcessor) Commit(ctx context.Context) error { - - changes := p.cache.GetChanges() - var lps []history.LiquidityPool - for _, change := range changes { - switch { - case change.Pre == nil && change.Post != nil: - // Created - lps = append(lps, p.ledgerEntryToRow(change.Post)) - case change.Pre != nil && change.Post == nil: - // Removed - lp := p.ledgerEntryToRow(change.Pre) - lp.Deleted = true - lp.LastModifiedLedger = p.sequence - lps = append(lps, lp) - default: - // Updated - lps = append(lps, p.ledgerEntryToRow(change.Post)) - } - } - - if len(lps) > 0 { - if err := p.qLiquidityPools.UpsertLiquidityPools(ctx, lps); err != nil { + defer p.reset() + if len(p.lps) > 0 { + if err := p.qLiquidityPools.UpsertLiquidityPools(ctx, p.lps); err != nil { return errors.Wrap(err, "error upserting liquidity pools") } } diff --git a/services/horizon/internal/ingest/processors/liquidity_pools_change_processor_test.go b/services/horizon/internal/ingest/processors/liquidity_pools_change_processor_test.go index 4e7383b1fe..26fa6c6b41 100644 --- a/services/horizon/internal/ingest/processors/liquidity_pools_change_processor_test.go +++ b/services/horizon/internal/ingest/processors/liquidity_pools_change_processor_test.go @@ -183,7 +183,7 @@ func (s *LiquidityPoolsChangeProcessorTestSuiteLedger) TestNewLiquidityPool() { Type: xdr.LedgerEntryTypeLiquidityPool, LiquidityPool: &lpEntry, }, - LastModifiedLedgerSeq: lastModifiedLedgerSeq, + LastModifiedLedgerSeq: lastModifiedLedgerSeq + 1, Ext: xdr.LedgerEntryExt{ V: 1, V1: &xdr.LedgerEntryExtensionV1{ @@ -200,7 +200,7 @@ func (s *LiquidityPoolsChangeProcessorTestSuiteLedger) TestNewLiquidityPool() { }) s.Assert().NoError(err) - postLP := history.LiquidityPool{ + preLP := history.LiquidityPool{ PoolID: "cafebabedeadbeef000000000000000000000000000000000000000000000000", Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, Fee: 34, @@ -218,7 +218,25 @@ func (s *LiquidityPoolsChangeProcessorTestSuiteLedger) TestNewLiquidityPool() { }, LastModifiedLedger: 123, } - s.mockQ.On("UpsertLiquidityPools", s.ctx, []history.LiquidityPool{postLP}).Return(nil).Once() + postLP := history.LiquidityPool{ + PoolID: "cafebabedeadbeef000000000000000000000000000000000000000000000000", + Type: xdr.LiquidityPoolTypeLiquidityPoolConstantProduct, + Fee: 34, + TrustlineCount: 52115, + ShareCount: 412241, + AssetReserves: []history.LiquidityPoolAssetReserve{ + { + xdr.MustNewCreditAsset("USD", "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + 450, + }, + { + xdr.MustNewNativeAsset(), + 500, + }, + }, + LastModifiedLedger: 124, + } + s.mockQ.On("UpsertLiquidityPools", s.ctx, []history.LiquidityPool{preLP, postLP}).Return(nil).Once() s.mockQ.On("CompactLiquidityPools", s.ctx, s.sequence-100).Return(int64(0), nil).Once() } diff --git a/services/horizon/internal/ingest/processors/offers_processor.go b/services/horizon/internal/ingest/processors/offers_processor.go index 2a3e529be5..bf1e2648c0 100644 --- a/services/horizon/internal/ingest/processors/offers_processor.go +++ b/services/horizon/internal/ingest/processors/offers_processor.go @@ -17,7 +17,7 @@ type OffersProcessor struct { offersQ history.QOffers sequence uint32 - cache *ingest.ChangeCompactor + batchUpdateOffers []history.Offer insertBatchBuilder history.OffersBatchInsertBuilder } @@ -32,7 +32,7 @@ func (p *OffersProcessor) Name() string { } func (p *OffersProcessor) reset() { - p.cache = ingest.NewChangeCompactor() + p.batchUpdateOffers = []history.Offer{} p.insertBatchBuilder = p.offersQ.NewOffersBatchInsertBuilder() } @@ -41,11 +41,28 @@ func (p *OffersProcessor) ProcessChange(ctx context.Context, change ingest.Chang return nil } - if err := p.cache.AddChange(change); err != nil { - return errors.Wrap(err, "error adding to ledgerCache") + switch { + case change.Pre == nil && change.Post != nil: + // Created + err := p.insertBatchBuilder.Add(p.ledgerEntryToRow(change.Post)) + if err != nil { + return errors.New("Error adding to OffersBatchInsertBuilder") + } + case change.Pre != nil && change.Post != nil: + // Updated + row := p.ledgerEntryToRow(change.Post) + p.batchUpdateOffers = append(p.batchUpdateOffers, row) + case change.Pre != nil && change.Post == nil: + // Removed + row := p.ledgerEntryToRow(change.Pre) + row.Deleted = true + row.LastModifiedLedger = p.sequence + p.batchUpdateOffers = append(p.batchUpdateOffers, row) + default: + return errors.New("Invalid io.Change: change.Pre == nil && change.Post == nil") } - if p.cache.Size() > maxBatchSize { + if p.insertBatchBuilder.Len()+len(p.batchUpdateOffers) > maxBatchSize { if err := p.flushCache(ctx); err != nil { return errors.Wrap(err, "error in Commit") } @@ -74,38 +91,13 @@ func (p *OffersProcessor) ledgerEntryToRow(entry *xdr.LedgerEntry) history.Offer func (p *OffersProcessor) flushCache(ctx context.Context) error { defer p.reset() - var batchUpsertOffers []history.Offer - changes := p.cache.GetChanges() - for _, change := range changes { - switch { - case change.Pre == nil && change.Post != nil: - // Created - err := p.insertBatchBuilder.Add(p.ledgerEntryToRow(change.Post)) - if err != nil { - return errors.New("Error adding to OffersBatchInsertBuilder") - } - case change.Pre != nil && change.Post != nil: - // Updated - row := p.ledgerEntryToRow(change.Post) - batchUpsertOffers = append(batchUpsertOffers, row) - case change.Pre != nil && change.Post == nil: - // Removed - row := p.ledgerEntryToRow(change.Pre) - row.Deleted = true - row.LastModifiedLedger = p.sequence - batchUpsertOffers = append(batchUpsertOffers, row) - default: - return errors.New("Invalid io.Change: change.Pre == nil && change.Post == nil") - } - } - err := p.insertBatchBuilder.Exec(ctx) if err != nil { return errors.New("Error executing OffersBatchInsertBuilder") } - if len(batchUpsertOffers) > 0 { - err := p.offersQ.UpsertOffers(ctx, batchUpsertOffers) + if len(p.batchUpdateOffers) > 0 { + err := p.offersQ.UpsertOffers(ctx, p.batchUpdateOffers) if err != nil { return errors.Wrap(err, "errors in UpsertOffers") } diff --git a/services/horizon/internal/ingest/processors/offers_processor_test.go b/services/horizon/internal/ingest/processors/offers_processor_test.go index 77128a1b0f..8e39cd0f29 100644 --- a/services/horizon/internal/ingest/processors/offers_processor_test.go +++ b/services/horizon/internal/ingest/processors/offers_processor_test.go @@ -35,6 +35,7 @@ func (s *OffersProcessorTestSuiteState) SetupTest() { s.mockOffersBatchInsertBuilder = &history.MockOffersBatchInsertBuilder{} s.mockQ.On("NewOffersBatchInsertBuilder").Return(s.mockOffersBatchInsertBuilder).Twice() s.mockOffersBatchInsertBuilder.On("Exec", s.ctx).Return(nil).Once() + s.mockOffersBatchInsertBuilder.On("Len").Return(1).Maybe() s.sequence = 456 s.processor = NewOffersProcessor(s.mockQ, s.sequence) @@ -99,6 +100,7 @@ func (s *OffersProcessorTestSuiteLedger) SetupTest() { s.mockOffersBatchInsertBuilder = &history.MockOffersBatchInsertBuilder{} s.mockQ.On("NewOffersBatchInsertBuilder").Return(s.mockOffersBatchInsertBuilder).Twice() s.mockOffersBatchInsertBuilder.On("Exec", s.ctx).Return(nil).Once() + s.mockOffersBatchInsertBuilder.On("Len").Return(1).Maybe() s.sequence = 456 s.processor = NewOffersProcessor(s.mockQ, s.sequence) @@ -133,6 +135,15 @@ func (s *OffersProcessorTestSuiteLedger) setupInsertOffer() { } lastModifiedLedgerSeq := xdr.Uint32(1234) + s.mockOffersBatchInsertBuilder.On("Add", history.Offer{ + SellerID: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + OfferID: 2, + Pricen: int32(1), + Priced: int32(2), + Price: float64(1) / float64(2), + LastModifiedLedger: uint32(lastModifiedLedgerSeq), + }).Return(nil).Once() + err = s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeOffer, Pre: nil, @@ -145,43 +156,6 @@ func (s *OffersProcessorTestSuiteLedger) setupInsertOffer() { }, }) s.Assert().NoError(err) - - updatedOffer := xdr.OfferEntry{ - SellerId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - OfferId: xdr.Int64(2), - Price: xdr.Price{1, 6}, - } - - updatedEntry := xdr.LedgerEntry{ - LastModifiedLedgerSeq: lastModifiedLedgerSeq, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeOffer, - Offer: &updatedOffer, - }, - } - - err = s.processor.ProcessChange(s.ctx, ingest.Change{ - Type: xdr.LedgerEntryTypeOffer, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: lastModifiedLedgerSeq - 1, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeOffer, - Offer: &offer, - }, - }, - Post: &updatedEntry, - }) - s.Assert().NoError(err) - - // We use LedgerEntryChangesCache so all changes are squashed - s.mockOffersBatchInsertBuilder.On("Add", history.Offer{ - SellerID: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - OfferID: 2, - Pricen: int32(1), - Priced: int32(6), - Price: float64(1) / float64(6), - LastModifiedLedger: uint32(lastModifiedLedgerSeq), - }).Return(nil).Once() } func (s *OffersProcessorTestSuiteLedger) TestInsertOffer() { @@ -243,6 +217,43 @@ func (s *OffersProcessorTestSuiteLedger) TestUpsertManyOffers() { }, } + s.mockOffersBatchInsertBuilder.On("Add", history.Offer{ + SellerID: "GDMUVYVYPYZYBDXNJWKFT3X2GCZCICTL3GSVP6AWBGB4ZZG7ZRDA746P", + OfferID: 3, + Pricen: int32(2), + Priced: int32(3), + Price: float64(2) / float64(3), + LastModifiedLedger: uint32(lastModifiedLedgerSeq), + }).Return(nil).Once() + + s.mockOffersBatchInsertBuilder.On("Add", history.Offer{ + SellerID: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", + OfferID: 4, + Pricen: int32(2), + Priced: int32(6), + Price: float64(2) / float64(6), + LastModifiedLedger: uint32(lastModifiedLedgerSeq), + }).Return(nil).Once() + + s.mockQ.On("UpsertOffers", s.ctx, mock.Anything).Run(func(args mock.Arguments) { + // To fix order issue due to using ChangeCompactor + offers := args.Get(1).([]history.Offer) + s.Assert().ElementsMatch( + offers, + []history.Offer{ + { + SellerID: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + OfferID: 2, + Pricen: int32(1), + Priced: int32(6), + Price: float64(1) / float64(6), + LastModifiedLedger: uint32(lastModifiedLedgerSeq), + }, + }, + ) + }).Return(nil).Once() + s.mockQ.On("CompactOffers", s.ctx, s.sequence-100).Return(int64(0), nil).Once() + err := s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeOffer, Pre: &xdr.LedgerEntry{ @@ -281,43 +292,6 @@ func (s *OffersProcessorTestSuiteLedger) TestUpsertManyOffers() { }, }) s.Assert().NoError(err) - - s.mockOffersBatchInsertBuilder.On("Add", history.Offer{ - SellerID: "GDMUVYVYPYZYBDXNJWKFT3X2GCZCICTL3GSVP6AWBGB4ZZG7ZRDA746P", - OfferID: 3, - Pricen: int32(2), - Priced: int32(3), - Price: float64(2) / float64(3), - LastModifiedLedger: uint32(lastModifiedLedgerSeq), - }).Return(nil).Once() - - s.mockOffersBatchInsertBuilder.On("Add", history.Offer{ - SellerID: "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", - OfferID: 4, - Pricen: int32(2), - Priced: int32(6), - Price: float64(2) / float64(6), - LastModifiedLedger: uint32(lastModifiedLedgerSeq), - }).Return(nil).Once() - - s.mockQ.On("UpsertOffers", s.ctx, mock.Anything).Run(func(args mock.Arguments) { - // To fix order issue due to using ChangeCompactor - offers := args.Get(1).([]history.Offer) - s.Assert().ElementsMatch( - offers, - []history.Offer{ - { - SellerID: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - OfferID: 2, - Pricen: int32(1), - Priced: int32(6), - Price: float64(1) / float64(6), - LastModifiedLedger: uint32(lastModifiedLedgerSeq), - }, - }, - ) - }).Return(nil).Once() - s.mockQ.On("CompactOffers", s.ctx, s.sequence-100).Return(int64(0), nil).Once() s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -353,68 +327,6 @@ func (s *OffersProcessorTestSuiteLedger) TestRemoveOffer() { s.Assert().NoError(s.processor.Commit(s.ctx)) } -func (s *OffersProcessorTestSuiteLedger) TestProcessUpgradeChange() { - // add offer - offer := xdr.OfferEntry{ - SellerId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - OfferId: xdr.Int64(2), - Price: xdr.Price{1, 2}, - } - lastModifiedLedgerSeq := xdr.Uint32(1234) - - err := s.processor.ProcessChange(s.ctx, ingest.Change{ - Type: xdr.LedgerEntryTypeOffer, - Post: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: lastModifiedLedgerSeq, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeOffer, - Offer: &offer, - }, - }, - }) - s.Assert().NoError(err) - - updatedOffer := xdr.OfferEntry{ - SellerId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - OfferId: xdr.Int64(2), - Price: xdr.Price{1, 6}, - } - - updatedEntry := xdr.LedgerEntry{ - LastModifiedLedgerSeq: lastModifiedLedgerSeq, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeOffer, - Offer: &updatedOffer, - }, - } - - err = s.processor.ProcessChange(s.ctx, ingest.Change{ - Type: xdr.LedgerEntryTypeOffer, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: lastModifiedLedgerSeq, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeOffer, - Offer: &offer, - }, - }, - Post: &updatedEntry, - }) - s.Assert().NoError(err) - - // We use LedgerEntryChangesCache so all changes are squashed - s.mockOffersBatchInsertBuilder.On("Add", history.Offer{ - SellerID: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - OfferID: 2, - Pricen: 1, - Priced: 6, - Price: float64(1) / float64(6), - LastModifiedLedger: uint32(lastModifiedLedgerSeq), - }).Return(nil).Once() - - s.mockQ.On("CompactOffers", s.ctx, s.sequence-100).Return(int64(0), nil).Once() - s.Assert().NoError(s.processor.Commit(s.ctx)) -} - func (s *OffersProcessorTestSuiteLedger) TestRemoveMultipleOffers() { err := s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeOffer, diff --git a/services/horizon/internal/ingest/processors/signer_processor_test.go b/services/horizon/internal/ingest/processors/signer_processor_test.go index 5b4d147f15..f7dc16fee5 100644 --- a/services/horizon/internal/ingest/processors/signer_processor_test.go +++ b/services/horizon/internal/ingest/processors/signer_processor_test.go @@ -7,12 +7,14 @@ import ( "testing" "github.com/guregu/null" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" "github.com/stellar/go/ingest" "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" - "github.com/stretchr/testify/suite" ) func TestAccountsSignerProcessorTestSuiteState(t *testing.T) { @@ -36,8 +38,8 @@ func (s *AccountsSignerProcessorTestSuiteState) SetupTest() { On("NewAccountSignersBatchInsertBuilder"). Return(s.mockBatchInsertBuilder).Twice() s.mockBatchInsertBuilder.On("Exec", s.ctx).Return(nil) - - s.processor = NewSignersProcessor(s.mockQ, false) + s.mockBatchInsertBuilder.On("Len").Return(1).Maybe() + s.processor = NewSignersProcessor(s.mockQ) } func (s *AccountsSignerProcessorTestSuiteState) TearDownTest() { @@ -168,10 +170,11 @@ func (s *AccountsSignerProcessorTestSuiteLedger) SetupTest() { s.mockAccountSignersBatchInsertBuilder = &history.MockAccountSignersBatchInsertBuilder{} s.mockQ. On("NewAccountSignersBatchInsertBuilder"). - Return(s.mockAccountSignersBatchInsertBuilder).Twice() + Return(s.mockAccountSignersBatchInsertBuilder) s.mockAccountSignersBatchInsertBuilder.On("Exec", s.ctx).Return(nil) + s.mockAccountSignersBatchInsertBuilder.On("Len").Return(1).Maybe() - s.processor = NewSignersProcessor(s.mockQ, true) + s.processor = NewSignersProcessor(s.mockQ) } func (s *AccountsSignerProcessorTestSuiteLedger) TearDownTest() { @@ -240,29 +243,7 @@ func (s *AccountsSignerProcessorTestSuiteLedger) TestNoUpdatesWhenNoSignerChange } func (s *AccountsSignerProcessorTestSuiteLedger) TestNewSigner() { - // Remove old signer - s.mockQ. - On( - "RemoveAccountSigner", - s.ctx, - "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", - ). - Return(int64(1), nil).Once() - - // Create new and old signer - s.mockAccountSignersBatchInsertBuilder. - On( - "Add", - history.AccountSigner{ - "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", - int32(10), - null.String{}, - }, - ). - Return(nil).Once() - + // Create new signer s.mockAccountSignersBatchInsertBuilder. On( "Add", @@ -317,35 +298,13 @@ func (s *AccountsSignerProcessorTestSuiteLedger) TestSignerRemoved() { // Remove old signers s.mockQ. On( - "RemoveAccountSigner", - s.ctx, - "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", - ). - Return(int64(1), nil).Once() - - s.mockQ. - On( - "RemoveAccountSigner", + "RemoveAccountSigners", s.ctx, "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - "GCAHY6JSXQFKWKP6R7U5JPXDVNV4DJWOWRFLY3Y6YPBF64QRL4BPFDNS", + []string{"GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV"}, ). Return(int64(1), nil).Once() - // Create new signer - s.mockAccountSignersBatchInsertBuilder. - On( - "Add", - history.AccountSigner{ - "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - "GCAHY6JSXQFKWKP6R7U5JPXDVNV4DJWOWRFLY3Y6YPBF64QRL4BPFDNS", - int32(15), - null.String{}, - }, - ). - Return(nil).Once() - err := s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeAccount, Pre: &xdr.LedgerEntry{ @@ -391,35 +350,13 @@ func (s *AccountsSignerProcessorTestSuiteLedger) TestSignerPreAuthTxRemovedTxFai // Remove old signers s.mockQ. On( - "RemoveAccountSigner", + "RemoveAccountSigners", s.ctx, "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + []string{"TBU2RRGLXH3E5CQHTD3ODLDF2BWDCYUSSBLLZ5GNW7JXHDIYKXZWHXL7"}, ). Return(int64(1), nil).Once() - s.mockQ. - On( - "RemoveAccountSigner", - s.ctx, - "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - "TBU2RRGLXH3E5CQHTD3ODLDF2BWDCYUSSBLLZ5GNW7JXHDIYKXZWHXL7", - ). - Return(int64(1), nil).Once() - - // Create new signer - s.mockAccountSignersBatchInsertBuilder. - On( - "Add", - history.AccountSigner{ - "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", - int32(10), - null.String{}, - }, - ). - Return(nil).Once() - err := s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeAccount, Pre: &xdr.LedgerEntry{ @@ -462,10 +399,10 @@ func (s *AccountsSignerProcessorTestSuiteLedger) TestSignerPreAuthTxRemovedTxFai func (s *AccountsSignerProcessorTestSuiteLedger) TestRemoveAccount() { s.mockQ. On( - "RemoveAccountSigner", + "RemoveAccountSigners", s.ctx, "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + []string{"GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"}, ). Return(int64(1), nil).Once() @@ -489,10 +426,10 @@ func (s *AccountsSignerProcessorTestSuiteLedger) TestRemoveAccount() { func (s *AccountsSignerProcessorTestSuiteLedger) TestRemoveAccountNoRowsAffected() { s.mockQ. On( - "RemoveAccountSigner", + "RemoveAccountSigners", s.ctx, "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + []string{"GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"}, ). Return(int64(0), nil).Once() @@ -509,44 +446,18 @@ func (s *AccountsSignerProcessorTestSuiteLedger) TestRemoveAccountNoRowsAffected }, Post: nil, }) - s.Assert().NoError(err) - - err = s.processor.Commit(s.ctx) s.Assert().Error(err) s.Assert().IsType(ingest.StateError{}, errors.Cause(err)) s.Assert().EqualError( err, "Expected "+ "account=GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML "+ - "signer=GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML in database but not found when removing "+ + "signers=[GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML] in database but not found when removing "+ "(rows affected = 0)", ) } func (s *AccountsSignerProcessorTestSuiteLedger) TestProcessUpgradeChange() { - // Remove old signer - s.mockQ. - On( - "RemoveAccountSigner", - s.ctx, - "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", - ). - Return(int64(1), nil).Once() - - // Create new and old (updated) signer - s.mockAccountSignersBatchInsertBuilder. - On( - "Add", - history.AccountSigner{ - "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", - "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", - int32(12), - null.String{}, - }, - ). - Return(nil).Once() - s.mockAccountSignersBatchInsertBuilder. On( "Add", @@ -595,6 +506,37 @@ func (s *AccountsSignerProcessorTestSuiteLedger) TestProcessUpgradeChange() { }) s.Assert().NoError(err) + // Remove old signer + s.mockQ. + On( + "RemoveAccountSigners", + s.ctx, + "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + mock.MatchedBy(func(signers []string) bool { + return assert.ElementsMatch(s.T(), + []string{ + "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + "GCAHY6JSXQFKWKP6R7U5JPXDVNV4DJWOWRFLY3Y6YPBF64QRL4BPFDNS", + }, + signers, + ) + }), + ). + Return(int64(2), nil).Once() + + // Create new and old (updated) signer + s.mockAccountSignersBatchInsertBuilder. + On( + "Add", + history.AccountSigner{ + "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + "GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV", + int32(12), + null.String{}, + }, + ). + Return(nil).Once() + err = s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeAccount, Pre: &xdr.LedgerEntry{ @@ -608,6 +550,10 @@ func (s *AccountsSignerProcessorTestSuiteLedger) TestProcessUpgradeChange() { Key: xdr.MustSigner("GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV"), Weight: 10, }, + { + Key: xdr.MustSigner("GCAHY6JSXQFKWKP6R7U5JPXDVNV4DJWOWRFLY3Y6YPBF64QRL4BPFDNS"), + Weight: 15, + }, }, }, }, @@ -623,10 +569,6 @@ func (s *AccountsSignerProcessorTestSuiteLedger) TestProcessUpgradeChange() { Key: xdr.MustSigner("GCBBDQLCTNASZJ3MTKAOYEOWRGSHDFAJVI7VPZUOP7KXNHYR3HP2BUKV"), Weight: 12, }, - { - Key: xdr.MustSigner("GCAHY6JSXQFKWKP6R7U5JPXDVNV4DJWOWRFLY3Y6YPBF64QRL4BPFDNS"), - Weight: 15, - }, }, }, }, diff --git a/services/horizon/internal/ingest/processors/signers_diff_test.go b/services/horizon/internal/ingest/processors/signers_diff_test.go new file mode 100644 index 0000000000..28b5d4c8e1 --- /dev/null +++ b/services/horizon/internal/ingest/processors/signers_diff_test.go @@ -0,0 +1,438 @@ +package processors + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/xdr" +) + +func TestAccountSignersDiff(t *testing.T) { + sponsor, err := xdr.AddressToAccountId("GBADGWKHSUFOC4C7E3KXKINZSRX5KPHUWHH67UGJU77LEORGVLQ3BN3B") + assert.NoError(t, err) + newSponsor, err := xdr.AddressToAccountId("GB2Y6D5QFDJSCR6GSBO5D2LOLGZI4RVPRGZSSPLIFWNJZ7SL73TOMXAQ") + assert.NoError(t, err) + + for _, testCase := range []struct { + name string + input ingest.Change + removed []string + signersAdded map[string]int32 + sponsorsPerSigner map[string]string + }{ + { + "account added without master weight", + ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: nil, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + }, + }, + }, + }, + []string{}, + map[string]int32{}, + map[string]string{}, + }, + { + "account removed with master weight", + ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + // Master weight = 1 + Thresholds: [4]byte{1, 1, 1, 1}, + }, + }, + }, + Post: nil, + }, + []string{"GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"}, + nil, + nil, + }, + { + "account removed without master key", + ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + // Master weight = 0 + Thresholds: [4]byte{0, 1, 1, 1}, + }, + }, + }, + Post: nil, + }, + []string{}, + nil, + nil, + }, + { + "master key removed", + ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + // Master weight = 1 + Thresholds: [4]byte{1, 1, 1, 1}, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + // Master weight = 0 + Thresholds: [4]byte{0, 1, 1, 1}, + }, + }, + }, + }, + []string{"GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"}, + map[string]int32{}, + map[string]string{}, + }, + { + "master key added", + ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + // Master weight = 0 + Thresholds: [4]byte{0, 1, 1, 1}, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + // Master weight = 1 + Thresholds: [4]byte{1, 1, 1, 1}, + }, + }, + }, + }, + []string{}, + map[string]int32{ + "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML": 1, + }, + map[string]string{}, + }, + { + "signer added", + ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{}, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, + }, + }, + }, + }, + }, + []string{}, + map[string]int32{ + "GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX": 1, + }, + map[string]string{}, + }, + { + "signer removed", + ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, + }, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{}, + }, + }, + }, + }, + []string{"GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"}, + map[string]int32{}, + map[string]string{}, + }, + { + "signer weight changed", + ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, + }, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 2, + }, + }, + }, + }, + }, + }, + []string{"GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"}, + map[string]int32{ + "GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX": 2, + }, + map[string]string{}, + }, + { + "sponsor added", + ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, + }, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, + }, + Ext: xdr.AccountEntryExt{ + V1: &xdr.AccountEntryExtensionV1{ + Ext: xdr.AccountEntryExtensionV1Ext{ + V2: &xdr.AccountEntryExtensionV2{ + SignerSponsoringIDs: []xdr.SponsorshipDescriptor{ + &sponsor, + }, + }, + }, + }, + }, + }, + }, + }, + }, + []string{"GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"}, + map[string]int32{ + "GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX": 1, + }, + map[string]string{ + "GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX": sponsor.Address(), + }, + }, + { + "sponsor removed", + ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, + }, + Ext: xdr.AccountEntryExt{ + V1: &xdr.AccountEntryExtensionV1{ + Ext: xdr.AccountEntryExtensionV1Ext{ + V2: &xdr.AccountEntryExtensionV2{ + SignerSponsoringIDs: []xdr.SponsorshipDescriptor{ + &sponsor, + }, + }, + }, + }, + }, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, + }, + }, + }, + }, + }, + []string{"GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"}, + map[string]int32{"GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX": 1}, + map[string]string{}, + }, + { + "sponsor updated", + ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, + }, + Ext: xdr.AccountEntryExt{ + V1: &xdr.AccountEntryExtensionV1{ + Ext: xdr.AccountEntryExtensionV1Ext{ + V2: &xdr.AccountEntryExtensionV2{ + SignerSponsoringIDs: []xdr.SponsorshipDescriptor{ + &sponsor, + }, + }, + }, + }, + }, + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Signers: []xdr.Signer{ + { + Key: xdr.MustSigner("GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"), + Weight: 1, + }, + }, + Ext: xdr.AccountEntryExt{ + V1: &xdr.AccountEntryExtensionV1{ + Ext: xdr.AccountEntryExtensionV1Ext{ + V2: &xdr.AccountEntryExtensionV2{ + SignerSponsoringIDs: []xdr.SponsorshipDescriptor{ + &newSponsor, + }, + }, + }, + }, + }, + }, + }, + }, + }, + []string{"GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX"}, + map[string]int32{ + "GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX": 1, + }, + map[string]string{ + "GCCCU34WDY2RATQTOOQKY6SZWU6J5DONY42SWGW2CIXGW4LICAGNRZKX": newSponsor.Address(), + }, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + removed, signersAdded, sponsorsPerSigner := accountSignersDiff(testCase.input) + assert.ElementsMatch(t, testCase.removed, removed) + assert.Equal(t, testCase.signersAdded, signersAdded) + assert.Equal(t, testCase.sponsorsPerSigner, sponsorsPerSigner) + }) + } +} diff --git a/services/horizon/internal/ingest/processors/signers_processor.go b/services/horizon/internal/ingest/processors/signers_processor.go index b21183820d..2b18ac6674 100644 --- a/services/horizon/internal/ingest/processors/signers_processor.go +++ b/services/horizon/internal/ingest/processors/signers_processor.go @@ -14,18 +14,13 @@ import ( type SignersProcessor struct { signersQ history.QSigners - cache *ingest.ChangeCompactor batchInsertBuilder history.AccountSignersBatchInsertBuilder - // insertOnlyMode is a mode in which we don't use ledger cache and we just - // add signers to a batch, then we Exec all signers in one insert query. - // This is done to make history buckets processing faster (batch inserting). - useLedgerEntryCache bool } func NewSignersProcessor( - signersQ history.QSigners, useLedgerEntryCache bool, + signersQ history.QSigners, ) *SignersProcessor { - p := &SignersProcessor{signersQ: signersQ, useLedgerEntryCache: useLedgerEntryCache} + p := &SignersProcessor{signersQ: signersQ} p.reset() return p } @@ -36,37 +31,73 @@ func (p *SignersProcessor) Name() string { func (p *SignersProcessor) reset() { p.batchInsertBuilder = p.signersQ.NewAccountSignersBatchInsertBuilder() - p.cache = ingest.NewChangeCompactor() } -func (p *SignersProcessor) ProcessChange(ctx context.Context, change ingest.Change) error { - if change.Type != xdr.LedgerEntryTypeAccount { - return nil +func accountSignersDiff(change ingest.Change) ([]string, map[string]int32, map[string]string) { + var preSignerSummary map[string]int32 + var preSponsorsPerSigner map[string]string + var postSignerSummary map[string]int32 + var postSponsorsPerSigner map[string]string + var removedSigners []string + + if change.Pre != nil { + accountEntry := change.Pre.Data.MustAccount() + preSignerSummary = accountEntry.SignerSummary() + preSponsorsPerSigner = map[string]string{} + for signer, sponsor := range accountEntry.SponsorPerSigner() { + preSponsorsPerSigner[signer] = sponsor.Address() + } } - if p.useLedgerEntryCache { - err := p.cache.AddChange(change) - if err != nil { - return errors.Wrap(err, "error adding to ledgerCache") + if change.Post != nil { + accountEntry := change.Post.Data.MustAccount() + postSignerSummary = accountEntry.SignerSummary() + postSponsorsPerSigner = map[string]string{} + for signer, sponsor := range accountEntry.SponsorPerSigner() { + postSponsorsPerSigner[signer] = sponsor.Address() } + } - if p.cache.Size() > maxBatchSize { - err = p.Commit(ctx) - if err != nil { - return errors.Wrap(err, "error in Commit") - } + for signer, preWeight := range preSignerSummary { + postWeight, ok := postSignerSummary[signer] + if ok && preWeight == postWeight && preSponsorsPerSigner[signer] == postSponsorsPerSigner[signer] { + delete(postSignerSummary, signer) + delete(postSponsorsPerSigner, signer) + continue } + removedSigners = append(removedSigners, signer) + } + return removedSigners, postSignerSummary, postSponsorsPerSigner +} +func (p *SignersProcessor) ProcessChange(ctx context.Context, change ingest.Change) error { + if change.Type != xdr.LedgerEntryTypeAccount { + return nil + } + + removed, signerSummary, sponsersPerSigner := accountSignersDiff(change) + if len(removed) == 0 && len(signerSummary) == 0 { return nil } - if change.Pre == nil && change.Post != nil { - postAccountEntry := change.Post.Data.MustAccount() - if err := p.addAccountSigners(postAccountEntry); err != nil { + if len(removed) > 0 { + accountAddress := change.Pre.Data.MustAccount().AccountId.Address() + if err := p.removeAccountSigners(ctx, accountAddress, removed); err != nil { + return err + } + } + + if len(signerSummary) > 0 { + accountAddress := change.Post.Data.MustAccount().AccountId.Address() + if err := p.addAccountSigners(accountAddress, signerSummary, sponsersPerSigner); err != nil { return err } - } else { - return errors.New("SignersProcessor is in insert only mode") + } + + if p.batchInsertBuilder.Len() > maxBatchSize { + if err := p.Commit(ctx); err != nil { + return errors.Wrap(err, "error in Commit") + } } return nil @@ -75,29 +106,6 @@ func (p *SignersProcessor) ProcessChange(ctx context.Context, change ingest.Chan func (p *SignersProcessor) Commit(ctx context.Context) error { defer p.reset() - if p.useLedgerEntryCache { - changes := p.cache.GetChanges() - for _, change := range changes { - if !change.AccountSignersChanged() { - continue - } - - // The code below removes all Pre signers adds Post signers but - // can be improved by finding a diff (check performance first). - if change.Pre != nil { - if err := p.removeAccountSigners(ctx, change.Pre.Data.MustAccount()); err != nil { - return err - } - } - - if change.Post != nil { - if err := p.addAccountSigners(change.Post.Data.MustAccount()); err != nil { - return err - } - } - } - } - err := p.batchInsertBuilder.Exec(ctx) if err != nil { return errors.Wrap(err, "error executing AccountSignersBatchInsertBuilder") @@ -106,38 +114,40 @@ func (p *SignersProcessor) Commit(ctx context.Context) error { return nil } -func (p *SignersProcessor) removeAccountSigners(ctx context.Context, accountEntry xdr.AccountEntry) error { - for signer := range accountEntry.SignerSummary() { - rowsAffected, err := p.signersQ.RemoveAccountSigner(ctx, accountEntry.AccountId.Address(), signer) - if err != nil { - return errors.Wrap(err, "Error removing a signer") - } +func (p *SignersProcessor) removeAccountSigners(ctx context.Context, accountAddress string, signers []string) error { + rowsAffected, err := p.signersQ.RemoveAccountSigners(ctx, accountAddress, signers) + if err != nil { + return errors.Wrap(err, "Error removing a signer") + } - if rowsAffected != 1 { - return ingest.NewStateError(errors.Errorf( - "Expected account=%s signer=%s in database but not found when removing (rows affected = %d)", - accountEntry.AccountId.Address(), - signer, - rowsAffected, - )) - } + if rowsAffected != int64(len(signers)) { + return ingest.NewStateError(errors.Errorf( + "Expected account=%s signers=%s in database but not found when removing (rows affected = %d)", + accountAddress, + signers, + rowsAffected, + )) } + return nil } -func (p *SignersProcessor) addAccountSigners(accountEntry xdr.AccountEntry) error { - sponsorsPerSigner := accountEntry.SponsorPerSigner() - for signer, weight := range accountEntry.SignerSummary() { +func (p *SignersProcessor) addAccountSigners( + accountAddress string, + signerSummary map[string]int32, + sponsorsPerSigner map[string]string, +) error { + for signer, weight := range signerSummary { // Ignore master key var sponsor null.String - if signer != accountEntry.AccountId.Address() { + if signer != accountAddress { if sponsorDesc, isSponsored := sponsorsPerSigner[signer]; isSponsored { - sponsor = null.StringFrom(sponsorDesc.Address()) + sponsor = null.StringFrom(sponsorDesc) } } if err := p.batchInsertBuilder.Add(history.AccountSigner{ - Account: accountEntry.AccountId.Address(), + Account: accountAddress, Signer: signer, Weight: weight, Sponsor: sponsor, diff --git a/services/horizon/internal/ingest/processors/trust_lines_processor.go b/services/horizon/internal/ingest/processors/trust_lines_processor.go index a7cae533bd..318be167b3 100644 --- a/services/horizon/internal/ingest/processors/trust_lines_processor.go +++ b/services/horizon/internal/ingest/processors/trust_lines_processor.go @@ -12,8 +12,9 @@ import ( type TrustLinesProcessor struct { trustLinesQ history.QTrustLines - cache *ingest.ChangeCompactor - batchInsertBuilder history.TrustLinesBatchInsertBuilder + batchUpdateTrustlines []history.TrustLine + batchRemoveTrustLineKeys []string + batchInsertBuilder history.TrustLinesBatchInsertBuilder } func NewTrustLinesProcessor(trustLinesQ history.QTrustLines) *TrustLinesProcessor { @@ -27,7 +28,8 @@ func (p *TrustLinesProcessor) Name() string { } func (p *TrustLinesProcessor) reset() { - p.cache = ingest.NewChangeCompactor() + p.batchUpdateTrustlines = []history.TrustLine{} + p.batchRemoveTrustLineKeys = []string{} p.batchInsertBuilder = p.trustLinesQ.NewTrustLinesBatchInsertBuilder() } @@ -36,14 +38,41 @@ func (p *TrustLinesProcessor) ProcessChange(ctx context.Context, change ingest.C return nil } - err := p.cache.AddChange(change) - if err != nil { - return errors.Wrap(err, "error adding to ledgerCache") - } + switch { + case change.Pre == nil && change.Post != nil: + // Created + line, err := xdrToTrustline(*change.Post) + if err != nil { + return errors.Wrap(err, "Error extracting trustline") + } - if p.cache.Size() > maxBatchSize { - err = p.Commit(ctx) + err = p.batchInsertBuilder.Add(line) + if err != nil { + return errors.Wrap(err, "Error adding to TrustLinesBatchInsertBuilder") + } + case change.Pre != nil && change.Post != nil: + // Updated + tl, err := xdrToTrustline(*change.Post) + if err != nil { + return errors.Wrap(err, "Error extracting trustline") + } + p.batchUpdateTrustlines = append(p.batchUpdateTrustlines, tl) + case change.Pre != nil && change.Post == nil: + // Removed + trustLineEntry := change.Pre.Data.MustTrustLine() + ledgerKeyString, err := trustLineLedgerKey(trustLineEntry) if err != nil { + return errors.Wrap(err, "Error extracting ledger key") + } + p.batchRemoveTrustLineKeys = append(p.batchRemoveTrustLineKeys, ledgerKeyString) + + default: + return errors.New("Invalid io.Change: change.Pre == nil && change.Post == nil") + } + + if p.batchInsertBuilder.Len()+len(p.batchUpdateTrustlines)+len(p.batchRemoveTrustLineKeys) > maxBatchSize { + + if err := p.Commit(ctx); err != nil { return errors.Wrap(err, "error in Commit") } } @@ -104,67 +133,29 @@ func xdrToTrustline(ledgerEntry xdr.LedgerEntry) (history.TrustLine, error) { func (p *TrustLinesProcessor) Commit(ctx context.Context) error { defer p.reset() - var batchUpsertTrustLines []history.TrustLine - var batchRemoveTrustLineKeys []string - - changes := p.cache.GetChanges() - for _, change := range changes { - switch { - case change.Pre == nil && change.Post != nil: - // Created - line, err := xdrToTrustline(*change.Post) - if err != nil { - return errors.Wrap(err, "Error extracting trustline") - } - - err = p.batchInsertBuilder.Add(line) - if err != nil { - return errors.Wrap(err, "Error adding to TrustLinesBatchInsertBuilder") - } - case change.Pre != nil && change.Post != nil: - // Updated - tl, err := xdrToTrustline(*change.Post) - if err != nil { - return errors.Wrap(err, "Error extracting trustline") - } - batchUpsertTrustLines = append(batchUpsertTrustLines, tl) - case change.Pre != nil && change.Post == nil: - // Removed - trustLineEntry := change.Pre.Data.MustTrustLine() - ledgerKeyString, err := trustLineLedgerKey(trustLineEntry) - if err != nil { - return errors.Wrap(err, "Error extracting ledger key") - } - batchRemoveTrustLineKeys = append(batchRemoveTrustLineKeys, ledgerKeyString) - - default: - return errors.New("Invalid io.Change: change.Pre == nil && change.Post == nil") - } - } - err := p.batchInsertBuilder.Exec(ctx) if err != nil { return errors.Wrap(err, "Error executing TrustLinesBatchInsertBuilder") } - if len(batchUpsertTrustLines) > 0 { - err := p.trustLinesQ.UpsertTrustLines(ctx, batchUpsertTrustLines) + if len(p.batchUpdateTrustlines) > 0 { + err := p.trustLinesQ.UpsertTrustLines(ctx, p.batchUpdateTrustlines) if err != nil { return errors.Wrap(err, "errors in UpsertTrustLines") } } - if len(batchRemoveTrustLineKeys) > 0 { - rowsAffected, err := p.trustLinesQ.RemoveTrustLines(ctx, batchRemoveTrustLineKeys) + if len(p.batchRemoveTrustLineKeys) > 0 { + rowsAffected, err := p.trustLinesQ.RemoveTrustLines(ctx, p.batchRemoveTrustLineKeys) if err != nil { return err } - if rowsAffected != int64(len(batchRemoveTrustLineKeys)) { + if rowsAffected != int64(len(p.batchRemoveTrustLineKeys)) { return ingest.NewStateError(errors.Errorf( "%d rows affected when removing %d trust lines", rowsAffected, - len(batchRemoveTrustLineKeys), + len(p.batchRemoveTrustLineKeys), )) } } diff --git a/services/horizon/internal/ingest/processors/trust_lines_processor_test.go b/services/horizon/internal/ingest/processors/trust_lines_processor_test.go index 07990a0658..4c31f7ba25 100644 --- a/services/horizon/internal/ingest/processors/trust_lines_processor_test.go +++ b/services/horizon/internal/ingest/processors/trust_lines_processor_test.go @@ -37,6 +37,7 @@ func (s *TrustLinesProcessorTestSuiteState) SetupTest() { s.mockTrustLinesBatchInsertBuilder = &history.MockTrustLinesBatchInsertBuilder{} s.mockQ.On("NewTrustLinesBatchInsertBuilder").Return(s.mockTrustLinesBatchInsertBuilder).Twice() s.mockTrustLinesBatchInsertBuilder.On("Exec", s.ctx).Return(nil).Once() + s.mockTrustLinesBatchInsertBuilder.On("Len").Return(1).Maybe() s.processor = NewTrustLinesProcessor(s.mockQ) } @@ -65,32 +66,6 @@ func (s *TrustLinesProcessorTestSuiteState) TestCreateTrustLine() { Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), } - err := s.processor.ProcessChange(s.ctx, ingest.Change{ - Type: xdr.LedgerEntryTypeTrustline, - Pre: nil, - Post: &xdr.LedgerEntry{ - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeTrustline, - TrustLine: &trustLine, - }, - LastModifiedLedgerSeq: lastModifiedLedgerSeq, - }, - }) - s.Assert().NoError(err) - - err = s.processor.ProcessChange(s.ctx, ingest.Change{ - Type: xdr.LedgerEntryTypeTrustline, - Pre: nil, - Post: &xdr.LedgerEntry{ - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeTrustline, - TrustLine: &poolShareTrustLine, - }, - LastModifiedLedgerSeq: lastModifiedLedgerSeq, - }, - }) - s.Assert().NoError(err) - s.mockTrustLinesBatchInsertBuilder.On("Add", history.TrustLine{ AccountID: trustLine.AccountId.Address(), AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, @@ -118,14 +93,6 @@ func (s *TrustLinesProcessorTestSuiteState) TestCreateTrustLine() { LastModifiedLedger: uint32(lastModifiedLedgerSeq), }).Return(nil).Once() -} - -func (s *TrustLinesProcessorTestSuiteState) TestCreateTrustLineUnauthorized() { - trustLine := xdr.TrustLineEntry{ - AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), - Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()).ToTrustLineAsset(), - } - lastModifiedLedgerSeq := xdr.Uint32(123) err := s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeTrustline, Pre: nil, @@ -139,6 +106,27 @@ func (s *TrustLinesProcessorTestSuiteState) TestCreateTrustLineUnauthorized() { }) s.Assert().NoError(err) + err = s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeTrustline, + Pre: nil, + Post: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &poolShareTrustLine, + }, + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + }, + }) + s.Assert().NoError(err) +} + +func (s *TrustLinesProcessorTestSuiteState) TestCreateTrustLineUnauthorized() { + trustLine := xdr.TrustLineEntry{ + AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), + Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()).ToTrustLineAsset(), + } + lastModifiedLedgerSeq := xdr.Uint32(123) + s.mockTrustLinesBatchInsertBuilder.On("Add", history.TrustLine{ AccountID: trustLine.AccountId.Address(), AssetType: xdr.AssetTypeAssetTypeCreditAlphanum4, @@ -154,6 +142,19 @@ func (s *TrustLinesProcessorTestSuiteState) TestCreateTrustLineUnauthorized() { LastModifiedLedger: uint32(lastModifiedLedgerSeq), Sponsor: null.String{}, }).Return(nil).Once() + + err := s.processor.ProcessChange(s.ctx, ingest.Change{ + Type: xdr.LedgerEntryTypeTrustline, + Pre: nil, + Post: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeTrustline, + TrustLine: &trustLine, + }, + LastModifiedLedgerSeq: lastModifiedLedgerSeq, + }, + }) + s.Assert().NoError(err) } func TestTrustLinesProcessorTestSuiteLedger(t *testing.T) { @@ -175,6 +176,7 @@ func (s *TrustLinesProcessorTestSuiteLedger) SetupTest() { s.mockTrustLinesBatchInsertBuilder = &history.MockTrustLinesBatchInsertBuilder{} s.mockQ.On("NewTrustLinesBatchInsertBuilder").Return(s.mockTrustLinesBatchInsertBuilder).Twice() s.mockTrustLinesBatchInsertBuilder.On("Exec", s.ctx).Return(nil).Once() + s.mockTrustLinesBatchInsertBuilder.On("Len").Return(1).Maybe() s.processor = NewTrustLinesProcessor(s.mockQ) } @@ -219,6 +221,38 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestInsertTrustLine() { } lastModifiedLedgerSeq := xdr.Uint32(1234) + s.mockTrustLinesBatchInsertBuilder.On("Add", history.TrustLine{ + AccountID: trustLine.AccountId.Address(), + AssetType: trustLine.Asset.Type, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "EUR", + Balance: int64(trustLine.Balance), + LedgerKey: "AAAAAQAAAAAdBJqAD9qPq+j2nRDdjdp5KVoUh8riPkNO9ato7BNs8wAAAAFFVVIAAAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3", + Limit: int64(trustLine.Limit), + LiquidityPoolID: "", + BuyingLiabilities: int64(trustLine.Liabilities().Buying), + SellingLiabilities: int64(trustLine.Liabilities().Selling), + Flags: uint32(trustLine.Flags), + LastModifiedLedger: uint32(lastModifiedLedgerSeq), + Sponsor: null.String{}, + }).Return(nil).Once() + + s.mockTrustLinesBatchInsertBuilder.On("Add", history.TrustLine{ + AccountID: unauthorizedTrustline.AccountId.Address(), + AssetType: unauthorizedTrustline.Asset.Type, + AssetIssuer: trustLineIssuer.Address(), + AssetCode: "USD", + Balance: int64(unauthorizedTrustline.Balance), + LedgerKey: "AAAAAQAAAAC2LgFRDBZ3J52nLm30kq2iMgrO7dYzYAN3hvjtf1IHWgAAAAFVU0QAAAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3", + Limit: int64(unauthorizedTrustline.Limit), + LiquidityPoolID: "", + BuyingLiabilities: int64(unauthorizedTrustline.Liabilities().Buying), + SellingLiabilities: int64(unauthorizedTrustline.Liabilities().Selling), + Flags: uint32(unauthorizedTrustline.Flags), + LastModifiedLedger: uint32(lastModifiedLedgerSeq), + Sponsor: null.String{}, + }).Return(nil).Once() + err = s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeTrustline, Pre: nil, @@ -245,87 +279,6 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestInsertTrustLine() { }) s.Assert().NoError(err) - updatedTrustLine := xdr.TrustLineEntry{ - AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), - Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()).ToTrustLineAsset(), - Balance: 10, - Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), - } - updatedUnauthorizedTrustline := xdr.TrustLineEntry{ - AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), - Asset: xdr.MustNewCreditAsset("USD", trustLineIssuer.Address()).ToTrustLineAsset(), - Balance: 10, - } - - err = s.processor.ProcessChange(s.ctx, ingest.Change{ - Type: xdr.LedgerEntryTypeTrustline, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: lastModifiedLedgerSeq, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeTrustline, - TrustLine: &trustLine, - }, - }, - Post: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: lastModifiedLedgerSeq, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeTrustline, - TrustLine: &updatedTrustLine, - }, - }, - }) - s.Assert().NoError(err) - - err = s.processor.ProcessChange(s.ctx, ingest.Change{ - Type: xdr.LedgerEntryTypeTrustline, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: lastModifiedLedgerSeq, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeTrustline, - TrustLine: &unauthorizedTrustline, - }, - }, - Post: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: lastModifiedLedgerSeq, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeTrustline, - TrustLine: &updatedUnauthorizedTrustline, - }, - }, - }) - s.Assert().NoError(err) - - s.mockTrustLinesBatchInsertBuilder.On("Add", history.TrustLine{ - AccountID: updatedTrustLine.AccountId.Address(), - AssetType: updatedTrustLine.Asset.Type, - AssetIssuer: trustLineIssuer.Address(), - AssetCode: "EUR", - Balance: int64(updatedTrustLine.Balance), - LedgerKey: "AAAAAQAAAAAdBJqAD9qPq+j2nRDdjdp5KVoUh8riPkNO9ato7BNs8wAAAAFFVVIAAAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3", - Limit: int64(updatedTrustLine.Limit), - LiquidityPoolID: "", - BuyingLiabilities: int64(updatedTrustLine.Liabilities().Buying), - SellingLiabilities: int64(updatedTrustLine.Liabilities().Selling), - Flags: uint32(updatedTrustLine.Flags), - LastModifiedLedger: uint32(lastModifiedLedgerSeq), - Sponsor: null.String{}, - }).Return(nil).Once() - - s.mockTrustLinesBatchInsertBuilder.On("Add", history.TrustLine{ - AccountID: updatedUnauthorizedTrustline.AccountId.Address(), - AssetType: updatedUnauthorizedTrustline.Asset.Type, - AssetIssuer: trustLineIssuer.Address(), - AssetCode: "USD", - Balance: int64(updatedUnauthorizedTrustline.Balance), - LedgerKey: "AAAAAQAAAAC2LgFRDBZ3J52nLm30kq2iMgrO7dYzYAN3hvjtf1IHWgAAAAFVU0QAAAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3", - Limit: int64(updatedUnauthorizedTrustline.Limit), - LiquidityPoolID: "", - BuyingLiabilities: int64(updatedUnauthorizedTrustline.Liabilities().Buying), - SellingLiabilities: int64(updatedUnauthorizedTrustline.Liabilities().Selling), - Flags: uint32(updatedUnauthorizedTrustline.Flags), - LastModifiedLedger: uint32(lastModifiedLedgerSeq), - Sponsor: null.String{}, - }).Return(nil).Once() s.Assert().NoError(s.processor.Commit(s.ctx)) } @@ -564,73 +517,6 @@ func (s *TrustLinesProcessorTestSuiteLedger) TestRemoveTrustLine() { s.Assert().NoError(s.processor.Commit(s.ctx)) } -func (s *TrustLinesProcessorTestSuiteLedger) TestProcessUpgradeChange() { - // add trust line - lastModifiedLedgerSeq := xdr.Uint32(1234) - trustLine := xdr.TrustLineEntry{ - AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), - Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()).ToTrustLineAsset(), - Balance: 0, - Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), - } - - err := s.processor.ProcessChange(s.ctx, ingest.Change{ - Type: xdr.LedgerEntryTypeTrustline, - Post: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: lastModifiedLedgerSeq, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeTrustline, - TrustLine: &trustLine, - }, - }, - }) - s.Assert().NoError(err) - - updatedTrustLine := xdr.TrustLineEntry{ - AccountId: xdr.MustAddress("GAOQJGUAB7NI7K7I62ORBXMN3J4SSWQUQ7FOEPSDJ322W2HMCNWPHXFB"), - Asset: xdr.MustNewCreditAsset("EUR", trustLineIssuer.Address()).ToTrustLineAsset(), - Balance: 10, - Flags: xdr.Uint32(xdr.TrustLineFlagsAuthorizedFlag), - } - - err = s.processor.ProcessChange(s.ctx, ingest.Change{ - Type: xdr.LedgerEntryTypeTrustline, - Pre: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: lastModifiedLedgerSeq, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeTrustline, - TrustLine: &trustLine, - }, - }, - Post: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: lastModifiedLedgerSeq, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeTrustline, - TrustLine: &updatedTrustLine, - }, - }, - }) - s.Assert().NoError(err) - - s.mockTrustLinesBatchInsertBuilder.On("Add", history.TrustLine{ - AccountID: updatedTrustLine.AccountId.Address(), - AssetType: updatedTrustLine.Asset.Type, - AssetIssuer: trustLineIssuer.Address(), - AssetCode: "EUR", - Balance: int64(updatedTrustLine.Balance), - LedgerKey: "AAAAAQAAAAAdBJqAD9qPq+j2nRDdjdp5KVoUh8riPkNO9ato7BNs8wAAAAFFVVIAAAAAAGL8HQvQkbK2HA3WVjRrKmjX00fG8sLI7m0ERwJW/AX3", - Limit: int64(updatedTrustLine.Limit), - LiquidityPoolID: "", - BuyingLiabilities: int64(updatedTrustLine.Liabilities().Buying), - SellingLiabilities: int64(updatedTrustLine.Liabilities().Selling), - Flags: uint32(updatedTrustLine.Flags), - LastModifiedLedger: uint32(lastModifiedLedgerSeq), - Sponsor: null.String{}, - }).Return(nil).Once() - - s.Assert().NoError(s.processor.Commit(s.ctx)) -} - func (s *TrustLinesProcessorTestSuiteLedger) TestRemoveTrustlineNoRowsAffected() { err := s.processor.ProcessChange(s.ctx, ingest.Change{ Type: xdr.LedgerEntryTypeTrustline, From 8a7c4f3bb8210c6ca8ae3e138ee4e6fcf9f911a1 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Mon, 25 Mar 2024 13:13:00 -0400 Subject: [PATCH 090/234] services/horizon: Add deprecation warning for --captive-core-use-db (#5231) * Add deprecation warning and test * Update services/horizon/internal/flags.go Co-authored-by: shawn * Update parameters_test.go * Change the printed message --------- Co-authored-by: shawn --- services/horizon/internal/flags.go | 3 ++ .../internal/integration/parameters_test.go | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index 5f11389a5f..54e76fbc56 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -245,6 +245,9 @@ func Flags() (*Config, support.ConfigOptions) { Usage: `when enabled, Horizon ingestion will instruct the captive core invocation to use an external db url for ledger states rather than in memory(RAM). Will result in several GB of space shifting out of RAM and to the external db persistence. The external db url is determined by the presence of DATABASE parameter in the captive-core-config-path or if absent, the db will default to sqlite and the db file will be stored at location derived from captive-core-storage-path parameter.`, CustomSetValue: func(opt *support.ConfigOption) error { if val := viper.GetBool(opt.Name); val { + stdLog.Printf("The usage of the flag --captive-core-use-db has been deprecated. " + + "Setting it to false to achieve in-memory functionality on captive core will be removed in " + + "future releases. We recommend removing usage of this flag now in preparation.") config.CaptiveCoreConfigUseDB = val config.CaptiveCoreTomlParams.UseDB = val } diff --git a/services/horizon/internal/integration/parameters_test.go b/services/horizon/internal/integration/parameters_test.go index ebe3c3bfda..7d487c556f 100644 --- a/services/horizon/internal/integration/parameters_test.go +++ b/services/horizon/internal/integration/parameters_test.go @@ -541,6 +541,39 @@ func TestDeprecatedOutputs(t *testing.T) { "Configuring section in the developer documentation on how to use them - "+ "https://developers.stellar.org/docs/run-api-server/configuring") }) + t.Run("deprecated output for --captive-core-use-db", func(t *testing.T) { + originalStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + stdLog.SetOutput(os.Stderr) + + testConfig := integration.GetTestConfig() + testConfig.HorizonIngestParameters = map[string]string{"captive-core-use-db": "false"} + test := integration.NewTest(t, *testConfig) + err := test.StartHorizon() + assert.NoError(t, err) + test.WaitForHorizon() + + // Use a wait group to wait for the goroutine to finish before proceeding + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + if err := w.Close(); err != nil { + t.Errorf("Failed to close Stdout") + return + } + }() + + outputBytes, _ := io.ReadAll(r) + wg.Wait() // Wait for the goroutine to finish before proceeding + _ = r.Close() + os.Stderr = originalStderr + + assert.Contains(t, string(outputBytes), "The usage of the flag --captive-core-use-db has been deprecated. "+ + "Setting it to false to achieve in-memory functionality on captive core will be removed in "+ + "future releases. We recommend removing usage of this flag now in preparation.") + }) } func TestGlobalFlagsOutput(t *testing.T) { From 42430333346e8eac7ed8b6c8e061f82863148570 Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 25 Mar 2024 19:56:04 +0000 Subject: [PATCH 091/234] .github/workflows: upgrade postgres version to 16 (#5261) --- .github/workflows/go.yml | 2 +- .github/workflows/horizon.yml | 16 +++++----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 31bd95b8ff..a84bcd17df 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -57,7 +57,7 @@ jobs: matrix: os: [ubuntu-22.04] go: ["1.21", "1.22"] - pg: [12] + pg: [12, 16] runs-on: ${{ matrix.os }} services: postgres: diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index 3f95b9e24a..f09563f71d 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -12,9 +12,8 @@ jobs: matrix: os: [ubuntu-20.04, ubuntu-22.04] go: ["1.21", "1.22"] - pg: [12] - ingestion-backend: [captive-core, captive-core-remote-storage] - protocol-version: [19, 20] + pg: [12, 16] + protocol-version: [20] runs-on: ${{ matrix.os }} services: postgres: @@ -72,16 +71,14 @@ jobs: docker pull "$PROTOCOL_${{ matrix.protocol-version }}_CORE_DOCKER_IMG" echo HORIZON_INTEGRATION_TESTS_DOCKER_IMG="$PROTOCOL_${{ matrix.protocol-version }}_CORE_DOCKER_IMG" >> $GITHUB_ENV - - if: ${{ matrix.protocol-version == '20' }} - name: Pull and set Soroban RPC image + - name: Pull and set Soroban RPC image shell: bash run: | docker pull "$PROTOCOL_${{ matrix.protocol-version }}_SOROBAN_RPC_DOCKER_IMG" echo HORIZON_INTEGRATION_TESTS_SOROBAN_RPC_DOCKER_IMG="$PROTOCOL_${{ matrix.protocol-version }}_SOROBAN_RPC_DOCKER_IMG" >> $GITHUB_ENV echo HORIZON_INTEGRATION_TESTS_ENABLE_SOROBAN_RPC=true >> $GITHUB_ENV - - if: ${{ startsWith(matrix.ingestion-backend, 'captive-core') }} - name: Install and enable Captive Core + - name: Install and enable Captive Core run: | # Workaround for https://github.com/actions/virtual-environments/issues/5245, # libc++1-8 won't be installed if another version is installed (but apt won't give you a helpul @@ -95,10 +92,7 @@ jobs: echo "Using stellar core version $(stellar-core version)" echo 'HORIZON_INTEGRATION_TESTS_ENABLE_CAPTIVE_CORE=true' >> $GITHUB_ENV echo 'HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_BIN=/usr/bin/stellar-core' >> $GITHUB_ENV - - - if: ${{ matrix.ingestion-backend == 'captive-core-remote-storage' }} - name: Setup Captive Core Remote Storage - run: echo 'HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_USE_DB=true' >> $GITHUB_ENV + echo 'HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_USE_DB=true' >> $GITHUB_ENV - name: Build Horizon reproducible build run: | From 5e66bb6e6a3547854c96d525ca812fe0042e4588 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Thu, 28 Mar 2024 13:24:06 -0700 Subject: [PATCH 092/234] exp/services/ledgerexporter: Implement Docker based deployment (#5254) --- .github/workflows/horizon.yml | 38 ++++------------ Makefile | 3 ++ exp/services/ledgerexporter/Makefile | 44 +++++++++++++++++++ exp/services/ledgerexporter/docker/Dockerfile | 36 +++++++++++++++ exp/services/ledgerexporter/docker/start | 39 ++++++++++++++++ 5 files changed, 130 insertions(+), 30 deletions(-) create mode 100644 exp/services/ledgerexporter/Makefile create mode 100644 exp/services/ledgerexporter/docker/Dockerfile create mode 100644 exp/services/ledgerexporter/docker/start diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index f09563f71d..74c3f47725 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -156,47 +156,25 @@ jobs: ledger-exporter: name: Test and push the Ledger Exporter images runs-on: ubuntu-latest - if: false # Disable the job steps: - uses: actions/checkout@v3 with: # For pull requests, build and test the PR head not a merge of the PR with the destination. ref: ${{ github.event.pull_request.head.sha || github.ref }} - - name: Build and test Ledger Exporter images - # Any range should do for basic testing, this range was chosen pretty early in history so that it only takes a few mins to run - run: | - chmod 755 ./exp/lighthorizon/build/build.sh - mkdir $PWD/ledgerexport - # mkdir $PWD/index - - ./exp/lighthorizon/build/build.sh ledgerexporter stellar latest false - docker run -e ARCHIVE_TARGET=file:///ledgerexport\ - -e START=5\ - -e END=150\ - -e NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015"\ - -e CAPTIVE_CORE_CONFIG="/captive-core-pubnet.cfg"\ - -e HISTORY_ARCHIVE_URLS="https://history.stellar.org/prd/core-live/core_live_001"\ - -v $PWD/ledgerexport:/ledgerexport\ - stellar/lighthorizon-ledgerexporter - - # # run map job - # docker run -e NETWORK_PASSPHRASE='pubnet' -e JOB_INDEX_ENV=AWS_BATCH_JOB_ARRAY_INDEX -e AWS_BATCH_JOB_ARRAY_INDEX=0 -e BATCH_SIZE=64 -e FIRST_CHECKPOINT=64 \ - # -e WORKER_COUNT=1 -e RUN_MODE=map -v $PWD/ledgerexport:/ledgermeta -e TXMETA_SOURCE=file:///ledgermeta -v $PWD/index:/index -e INDEX_TARGET=file:///index stellar/lighthorizon-index-batch - - # # run reduce job - # docker run -e NETWORK_PASSPHRASE='pubnet' -e JOB_INDEX_ENV=AWS_BATCH_JOB_ARRAY_INDEX -e AWS_BATCH_JOB_ARRAY_INDEX=0 -e MAP_JOB_COUNT=1 -e REDUCE_JOB_COUNT=1 \ - # -e WORKER_COUNT=1 -e RUN_MODE=reduce -v $PWD/index:/index -e INDEX_SOURCE_ROOT=file:///index -e INDEX_TARGET=file:///index stellar/lighthorizon-index-batch + - name: Build Ledger Exporter docker + run: make -C exp/services/ledgerexporter docker-build + + - name: Run Ledger Exporter test + run: make -C exp/services/ledgerexporter docker-test # Push images - - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/lighthorizon' + - if: github.ref == 'refs/heads/master' name: Login to DockerHub uses: docker/login-action@bb984efc561711aaa26e433c32c3521176eae55b with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/lighthorizon' + - if: github.ref == 'refs/heads/master' name: Push to DockerHub - run: | - chmod 755 ./exp/lighthorizon/build/build.sh - ./exp/lighthorizon/build/build.sh ledgerexporter stellar latest true + run: make -C exp/services/ledgerexporter docker-push diff --git a/Makefile b/Makefile index 13486ea7d7..291cc282fc 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,9 @@ friendbot: horizon: $(MAKE) -C services/horizon/ binary-build +ledger-exporter: + $(MAKE) -C exp/services/ledgerexporter/ docker-build + webauth: $(MAKE) -C exp/services/webauth/ docker-build diff --git a/exp/services/ledgerexporter/Makefile b/exp/services/ledgerexporter/Makefile new file mode 100644 index 0000000000..e4d6f647e0 --- /dev/null +++ b/exp/services/ledgerexporter/Makefile @@ -0,0 +1,44 @@ +SUDO := $(shell docker version >/dev/null 2>&1 || echo "sudo") + +# https://github.com/opencontainers/image-spec/blob/master/annotations.md +BUILD_DATE := $(shell date -u +%FT%TZ) +VERSION ?= $(shell git rev-parse --short HEAD) +DOCKER_IMAGE := stellar/ledger-exporter + +docker-build: + cd ../../../ && \ + $(SUDO) docker build --platform linux/amd64 --pull --label org.opencontainers.image.created="$(BUILD_DATE)" \ + --build-arg VERSION=$(VERSION) \ +$(if $(STELLAR_CORE_VERSION), --build-arg STELLAR_CORE_VERSION=$(STELLAR_CORE_VERSION)) \ + -f exp/services/ledgerexporter/docker/Dockerfile \ + -t $(DOCKER_IMAGE):$(VERSION) \ + -t $(DOCKER_IMAGE):latest . + +docker-test: + # Create temp storage dir + $(SUDO) mkdir -p ${PWD}/storage/exporter-test + + # Create test network for docker + $(SUDO) docker network create test-network + + # Run the fake GCS server + $(SUDO) docker run -d --name fake-gcs-server -p 4443:4443 \ + -v ${PWD}/storage:/data --network test-network fsouza/fake-gcs-server -scheme http + + # Run the ledger-exporter + $(SUDO) docker run --platform linux/amd64 -t --network test-network\ + -e NETWORK=pubnet \ + -e ARCHIVE_TARGET=gcs://exporter-test \ + -e START=1000 \ + -e END=2000 \ + -e STORAGE_EMULATOR_HOST=http://fake-gcs-server:4443 \ + $(DOCKER_IMAGE):$(VERSION) + + $(SUDO) docker stop fake-gcs-server + $(SUDO) docker rm fake-gcs-server + $(SUDO) rm -rf ${PWD}/storage + $(SUDO) docker network rm test-network + +docker-push: + $(SUDO) docker push $(DOCKER_IMAGE):$(VERSION) + $(SUDO) docker push $(DOCKER_IMAGE):latest diff --git a/exp/services/ledgerexporter/docker/Dockerfile b/exp/services/ledgerexporter/docker/Dockerfile new file mode 100644 index 0000000000..a862306718 --- /dev/null +++ b/exp/services/ledgerexporter/docker/Dockerfile @@ -0,0 +1,36 @@ +FROM golang:1.22-bullseye AS builder + +WORKDIR /go/src/github.com/stellar/go + +COPY go.mod ./ +COPY go.sum ./ + +RUN go mod download + +COPY . ./ + +RUN go install github.com/stellar/go/exp/services/ledgerexporter + +FROM ubuntu:22.04 +ARG STELLAR_CORE_VERSION +ENV STELLAR_CORE_VERSION=${STELLAR_CORE_VERSION:-*} +ENV STELLAR_CORE_BINARY_PATH /usr/bin/stellar-core + +ENV DEBIAN_FRONTEND=noninteractive +# ca-certificates are required to make tls connections +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl wget gnupg apt-utils +RUN wget -qO - https://apt.stellar.org/SDF.asc | APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=true apt-key add - +RUN echo "deb https://apt.stellar.org focal stable" >/etc/apt/sources.list.d/SDF.list +RUN echo "deb https://apt.stellar.org focal unstable" >/etc/apt/sources.list.d/SDF-unstable.list +RUN apt-get update && apt-get install -y stellar-core=${STELLAR_CORE_VERSION} +RUN apt-get clean + +COPY exp/services/ledgerexporter/docker/start / + +RUN ["chmod", "+x", "/start"] + +COPY --from=builder /go/bin/ledgerexporter /usr/bin/ledgerexporter + +ENTRYPOINT ["/start"] + + diff --git a/exp/services/ledgerexporter/docker/start b/exp/services/ledgerexporter/docker/start new file mode 100644 index 0000000000..8cca164dcd --- /dev/null +++ b/exp/services/ledgerexporter/docker/start @@ -0,0 +1,39 @@ +#! /usr/bin/env bash +set -e + +# Validation +if [ -z "$ARCHIVE_TARGET" ]; then + echo "error: undefined ARCHIVE_TARGET env variable" + exit 1 +fi + +if [ -z "$NETWORK" ]; then + echo "error: undefined NETWORK env variable" + exit 1 +fi + +ledgers_per_file="${LEDGERS_PER_FILE:-1}" +files_per_partition="${FILES_PER_PARTITION:-64000}" + +# Generate TOML configuration +cat < config.toml +network = "${NETWORK}" +destination_url = "${ARCHIVE_TARGET}" + +[exporter_config] + ledgers_per_file = $ledgers_per_file + files_per_partition = $files_per_partition +EOF + +# Check if START or END variables are set +if [[ -n "$START" || -n "$END" ]]; then + echo "START: $START END: $END" + /usr/bin/ledgerexporter --config-file config.toml --start $START --end $END +# Check if FROM_LAST variable is set +elif [[ -n "$FROM_LAST" ]]; then + echo "FROM_LAST: $FROM_LAST" + /usr/bin/ledgerexporter --config-file config.toml --from-last $FROM_LAST +else + echo "Error: No ledger range provided." + exit 1 +fi From 2501c390efb957a57a9e5b613ec2fd0c547b20e8 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Thu, 28 Mar 2024 17:56:54 -0700 Subject: [PATCH 093/234] exp/tools/dump-ledger-state: Bump go version to 1.22 (#5267) --- exp/tools/dump-ledger-state/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exp/tools/dump-ledger-state/Dockerfile b/exp/tools/dump-ledger-state/Dockerfile index dc735ab55e..91b258f0a8 100644 --- a/exp/tools/dump-ledger-state/Dockerfile +++ b/exp/tools/dump-ledger-state/Dockerfile @@ -26,7 +26,7 @@ RUN echo "host all all all trust" > /etc/postgresql/9.6/main/pg_hba.conf # And add `listen_addresses` to `/etc/postgresql/9.6/main/postgresql.conf` RUN echo "listen_addresses='*'" >> /etc/postgresql/9.6/main/postgresql.conf -RUN curl -sL https://storage.googleapis.com/golang/go1.19.linux-amd64.tar.gz | tar -C /usr/local -xz +COPY --from=golang:1.22-bullseye /usr/local/go/ /usr/local/go/ RUN ln -s /usr/local/go/bin/go /usr/local/bin/go WORKDIR /go/src/github.com/stellar/go COPY go.mod go.sum ./ From a30c441b6cd6cbcd8eb3749412094422b1a4ea49 Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 29 Mar 2024 20:07:47 +0000 Subject: [PATCH 094/234] Log tx meta when ingestion failures occur (#5268) --- ingest/change.go | 33 +++++-- .../internal/ingest/change_processors_test.go | 85 +++++++++++++++++++ .../internal/ingest/processor_runner.go | 35 +++++++- .../ingest/processors/change_processors.go | 32 ------- .../processors/change_processors_test.go | 47 ---------- 5 files changed, 146 insertions(+), 86 deletions(-) create mode 100644 services/horizon/internal/ingest/change_processors_test.go delete mode 100644 services/horizon/internal/ingest/processors/change_processors.go delete mode 100644 services/horizon/internal/ingest/processors/change_processors_test.go diff --git a/ingest/change.go b/ingest/change.go index 8d435f4229..0a2c063f1c 100644 --- a/ingest/change.go +++ b/ingest/change.go @@ -2,6 +2,7 @@ package ingest import ( "bytes" + "fmt" "sort" "github.com/stellar/go/support/errors" @@ -21,7 +22,29 @@ type Change struct { Post *xdr.LedgerEntry } -func (c *Change) ledgerKey() (xdr.LedgerKey, error) { +// String returns a best effort string representation of the change. +// If the Pre or Post xdr is invalid, the field will be omitted from the string. +func (c Change) String() string { + var pre, post string + if c.Pre != nil { + if b64, err := xdr.MarshalBase64(c.Pre); err == nil { + pre = b64 + } + } + if c.Post != nil { + if b64, err := xdr.MarshalBase64(c.Post); err == nil { + post = b64 + } + } + return fmt.Sprintf( + "Change{Type: %s, Pre: %s, Post: %s}", + c.Type.String(), + pre, + post, + ) +} + +func (c Change) ledgerKey() (xdr.LedgerKey, error) { if c.Pre != nil { return c.Pre.LedgerKey() } @@ -124,7 +147,7 @@ func sortChanges(changes []Change) { } // LedgerEntryChangeType returns type in terms of LedgerEntryChangeType. -func (c *Change) LedgerEntryChangeType() xdr.LedgerEntryChangeType { +func (c Change) LedgerEntryChangeType() xdr.LedgerEntryChangeType { switch { case c.Pre == nil && c.Post != nil: return xdr.LedgerEntryChangeTypeLedgerEntryCreated @@ -138,7 +161,7 @@ func (c *Change) LedgerEntryChangeType() xdr.LedgerEntryChangeType { } // getLiquidityPool gets the most recent state of the LiquidityPool that exists or existed. -func (c *Change) getLiquidityPool() (*xdr.LiquidityPoolEntry, error) { +func (c Change) getLiquidityPool() (*xdr.LiquidityPoolEntry, error) { var entry *xdr.LiquidityPoolEntry if c.Pre != nil { entry = c.Pre.Data.LiquidityPool @@ -153,7 +176,7 @@ func (c *Change) getLiquidityPool() (*xdr.LiquidityPoolEntry, error) { } // GetLiquidityPoolType returns the liquidity pool type. -func (c *Change) GetLiquidityPoolType() (xdr.LiquidityPoolType, error) { +func (c Change) GetLiquidityPoolType() (xdr.LiquidityPoolType, error) { lp, err := c.getLiquidityPool() if err != nil { return xdr.LiquidityPoolType(0), err @@ -164,7 +187,7 @@ func (c *Change) GetLiquidityPoolType() (xdr.LiquidityPoolType, error) { // AccountChangedExceptSigners returns true if account has changed WITHOUT // checking the signers (except master key weight!). In other words, if the only // change is connected to signers, this function will return false. -func (c *Change) AccountChangedExceptSigners() (bool, error) { +func (c Change) AccountChangedExceptSigners() (bool, error) { if c.Type != xdr.LedgerEntryTypeAccount { panic("This should not be called on changes other than Account changes") } diff --git a/services/horizon/internal/ingest/change_processors_test.go b/services/horizon/internal/ingest/change_processors_test.go new file mode 100644 index 0000000000..b6a8251b71 --- /dev/null +++ b/services/horizon/internal/ingest/change_processors_test.go @@ -0,0 +1,85 @@ +package ingest + +import ( + "context" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/ingest" + "github.com/stellar/go/services/horizon/internal/ingest/processors" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" +) + +func TestStreamReaderError(t *testing.T) { + tt := assert.New(t) + ctx := context.Background() + + mockChangeReader := &ingest.MockChangeReader{} + mockChangeReader. + On("Read"). + Return(ingest.Change{}, errors.New("transient error")).Once() + mockChangeProcessor := &processors.MockChangeProcessor{} + + err := streamChanges(ctx, mockChangeProcessor, 1, mockChangeReader) + tt.EqualError(err, "could not read transaction: transient error") +} + +func TestStreamChangeProcessorError(t *testing.T) { + tt := assert.New(t) + ctx := context.Background() + + change := ingest.Change{ + Type: xdr.LedgerEntryTypeAccount, + Pre: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 10, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + }, + }, + }, + Post: &xdr.LedgerEntry{ + LastModifiedLedgerSeq: 11, + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.AccountEntry{ + AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + Balance: 200, + }, + }, + }, + } + mockChangeReader := &ingest.MockChangeReader{} + mockChangeReader. + On("Read"). + Return(change, nil).Once() + + mockChangeProcessor := &processors.MockChangeProcessor{} + mockChangeProcessor. + On( + "ProcessChange", ctx, + change, + ). + Return(errors.New("transient error")).Once() + + logsGet := log.StartTest(logrus.ErrorLevel) + err := streamChanges(ctx, mockChangeProcessor, 1, mockChangeReader) + tt.EqualError(err, "could not process change: transient error") + logs := logsGet() + line, err := logs[0].String() + tt.NoError(err) + + preB64, err := xdr.MarshalBase64(change.Pre) + tt.NoError(err) + postB64, err := xdr.MarshalBase64(change.Post) + tt.NoError(err) + expectedTokens := []string{"LedgerEntryTypeAccount", preB64, postB64} + + for _, token := range expectedTokens { + tt.Contains(line, token) + } +} diff --git a/services/horizon/internal/ingest/processor_runner.go b/services/horizon/internal/ingest/processor_runner.go index e26db5fe31..b98896fe1f 100644 --- a/services/horizon/internal/ingest/processor_runner.go +++ b/services/horizon/internal/ingest/processor_runner.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "io" "time" "github.com/stellar/go/ingest" @@ -272,7 +273,7 @@ func (s *ProcessorRunner) RunHistoryArchiveIngestion( log.WithField("sequence", checkpointLedger). Info("Processing entries from History Archive Snapshot") - err = processors.StreamChanges(s.ctx, changeProcessor, newloggingChangeReader( + err = streamChanges(s.ctx, changeProcessor, checkpointLedger, newloggingChangeReader( changeReader, "historyArchive", checkpointLedger, @@ -301,7 +302,7 @@ func (s *ProcessorRunner) runChangeProcessorOnLedger( return errors.Wrap(err, "Error creating ledger change reader") } changeReader = ingest.NewCompactingChangeReader(changeReader) - if err = processors.StreamChanges(s.ctx, changeProcessor, changeReader); err != nil { + if err = streamChanges(s.ctx, changeProcessor, ledger.LedgerSequence(), changeReader); err != nil { return errors.Wrap(err, "Error streaming changes from ledger") } @@ -314,6 +315,36 @@ func (s *ProcessorRunner) runChangeProcessorOnLedger( return nil } +func streamChanges( + ctx context.Context, + changeProcessor processors.ChangeProcessor, + ledger uint32, + reader ingest.ChangeReader, +) error { + + for { + change, err := reader.Read() + if err == io.EOF { + return nil + } + if err != nil { + return errors.Wrap(err, "could not read transaction") + } + + if err = changeProcessor.ProcessChange(ctx, change); err != nil { + if !isCancelledError(ctx, err) { + log.WithError(err).WithField("sequence", ledger).WithField( + "change", change.String(), + ).Error("error processing change") + } + return errors.Wrap( + err, + "could not process change", + ) + } + } +} + func (s *ProcessorRunner) streamLedger(ledger xdr.LedgerCloseMeta, groupFilterers *groupTransactionFilterers, groupFilteredOutProcessors *groupTransactionProcessors, diff --git a/services/horizon/internal/ingest/processors/change_processors.go b/services/horizon/internal/ingest/processors/change_processors.go deleted file mode 100644 index ee9eb127f1..0000000000 --- a/services/horizon/internal/ingest/processors/change_processors.go +++ /dev/null @@ -1,32 +0,0 @@ -package processors - -import ( - "context" - "io" - - "github.com/stellar/go/ingest" - "github.com/stellar/go/support/errors" -) - -func StreamChanges( - ctx context.Context, - changeProcessor ChangeProcessor, - reader ingest.ChangeReader, -) error { - for { - change, err := reader.Read() - if err == io.EOF { - return nil - } - if err != nil { - return errors.Wrap(err, "could not read transaction") - } - - if err = changeProcessor.ProcessChange(ctx, change); err != nil { - return errors.Wrap( - err, - "could not process change", - ) - } - } -} diff --git a/services/horizon/internal/ingest/processors/change_processors_test.go b/services/horizon/internal/ingest/processors/change_processors_test.go deleted file mode 100644 index 829552543f..0000000000 --- a/services/horizon/internal/ingest/processors/change_processors_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package processors - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/stellar/go/ingest" - "github.com/stellar/go/support/errors" -) - -func TestStreamReaderError(t *testing.T) { - tt := assert.New(t) - ctx := context.Background() - - mockChangeReader := &ingest.MockChangeReader{} - mockChangeReader. - On("Read"). - Return(ingest.Change{}, errors.New("transient error")).Once() - mockChangeProcessor := &MockChangeProcessor{} - - err := StreamChanges(ctx, mockChangeProcessor, mockChangeReader) - tt.EqualError(err, "could not read transaction: transient error") -} - -func TestStreamChangeProcessorError(t *testing.T) { - tt := assert.New(t) - ctx := context.Background() - - change := ingest.Change{} - mockChangeReader := &ingest.MockChangeReader{} - mockChangeReader. - On("Read"). - Return(change, nil).Once() - - mockChangeProcessor := &MockChangeProcessor{} - mockChangeProcessor. - On( - "ProcessChange", ctx, - change, - ). - Return(errors.New("transient error")).Once() - - err := StreamChanges(ctx, mockChangeProcessor, mockChangeReader) - tt.EqualError(err, "could not process change: transient error") -} From 6bb4a441ebeb5af399c27dd43eb868eb92fcdd49 Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 15 Apr 2024 18:02:55 +0100 Subject: [PATCH 095/234] services/horizon/internal/ingest: Fix deadlock in parallel ingestion (#5263) --- .../ingest/fsm_history_range_state.go | 4 +- .../fsm_reingest_history_range_state.go | 23 +---- .../internal/ingest/group_processors.go | 63 +++++++----- .../internal/ingest/group_processors_test.go | 2 +- .../ingest/ingest_history_range_state_test.go | 96 +++---------------- services/horizon/internal/ingest/main_test.go | 4 +- .../internal/ingest/processor_runner.go | 74 ++++++++------ .../internal/ingest/processor_runner_test.go | 4 +- 8 files changed, 113 insertions(+), 157 deletions(-) diff --git a/services/horizon/internal/ingest/fsm_history_range_state.go b/services/horizon/internal/ingest/fsm_history_range_state.go index 0aa65795a9..f1c2a238b7 100644 --- a/services/horizon/internal/ingest/fsm_history_range_state.go +++ b/services/horizon/internal/ingest/fsm_history_range_state.go @@ -91,7 +91,7 @@ func (h historyRangeState) run(s *system) (transition, error) { ledgers = append(ledgers, ledgerCloseMeta) if len(ledgers) == cap(ledgers) { - if err = s.runner.RunTransactionProcessorsOnLedgers(ledgers); err != nil { + if err = s.runner.RunTransactionProcessorsOnLedgers(ledgers, false); err != nil { return start(), errors.Wrapf(err, "error processing ledger range %d - %d", ledgers[0].LedgerSequence(), ledgers[len(ledgers)-1].LedgerSequence()) } ledgers = ledgers[0:0] @@ -99,7 +99,7 @@ func (h historyRangeState) run(s *system) (transition, error) { } if len(ledgers) > 0 { - if err = s.runner.RunTransactionProcessorsOnLedgers(ledgers); err != nil { + if err = s.runner.RunTransactionProcessorsOnLedgers(ledgers, false); err != nil { return start(), errors.Wrapf(err, "error processing ledger range %d - %d", ledgers[0].LedgerSequence(), ledgers[len(ledgers)-1].LedgerSequence()) } } diff --git a/services/horizon/internal/ingest/fsm_reingest_history_range_state.go b/services/horizon/internal/ingest/fsm_reingest_history_range_state.go index 832898d021..499d15871c 100644 --- a/services/horizon/internal/ingest/fsm_reingest_history_range_state.go +++ b/services/horizon/internal/ingest/fsm_reingest_history_range_state.go @@ -30,11 +30,7 @@ func (reingestHistoryRangeState) GetState() State { return ReingestHistoryRange } -func (h reingestHistoryRangeState) ingestRange(s *system, fromLedger, toLedger uint32) error { - if s.historyQ.GetTx() == nil { - return errors.New("expected transaction to be present") - } - +func (h reingestHistoryRangeState) ingestRange(s *system, fromLedger, toLedger uint32, execBatchInTx bool) error { if s.maxLedgerPerFlush < 1 { return errors.New("invalid maxLedgerPerFlush, must be greater than 0") } @@ -75,7 +71,7 @@ func (h reingestHistoryRangeState) ingestRange(s *system, fromLedger, toLedger u ledgers = append(ledgers, ledgerCloseMeta) if len(ledgers)%int(s.maxLedgerPerFlush) == 0 { - if err = s.runner.RunTransactionProcessorsOnLedgers(ledgers); err != nil { + if err = s.runner.RunTransactionProcessorsOnLedgers(ledgers, execBatchInTx); err != nil { return errors.Wrapf(err, "error processing ledger range %d - %d", ledgers[0].LedgerSequence(), ledgers[len(ledgers)-1].LedgerSequence()) } ledgers = ledgers[0:0] @@ -83,7 +79,7 @@ func (h reingestHistoryRangeState) ingestRange(s *system, fromLedger, toLedger u } if len(ledgers) > 0 { - if err = s.runner.RunTransactionProcessorsOnLedgers(ledgers); err != nil { + if err = s.runner.RunTransactionProcessorsOnLedgers(ledgers, execBatchInTx); err != nil { return errors.Wrapf(err, "error processing ledger range %d - %d", ledgers[0].LedgerSequence(), ledgers[len(ledgers)-1].LedgerSequence()) } } @@ -142,7 +138,7 @@ func (h reingestHistoryRangeState) run(s *system) (transition, error) { return stop(), errors.Wrap(err, getLastIngestedErrMsg) } - if ingestErr := h.ingestRange(s, h.fromLedger, h.toLedger); ingestErr != nil { + if ingestErr := h.ingestRange(s, h.fromLedger, h.toLedger, false); ingestErr != nil { if err := s.historyQ.Commit(); err != nil { return stop(), errors.Wrap(ingestErr, commitErrMsg) } @@ -169,18 +165,9 @@ func (h reingestHistoryRangeState) run(s *system) (transition, error) { } startTime = time.Now() - if err := s.historyQ.Begin(s.ctx); err != nil { - return stop(), errors.Wrap(err, "Error starting a transaction") - } - defer s.historyQ.Rollback() - - if e := h.ingestRange(s, h.fromLedger, h.toLedger); e != nil { + if e := h.ingestRange(s, h.fromLedger, h.toLedger, true); e != nil { return stop(), e } - - if e := s.historyQ.Commit(); e != nil { - return stop(), errors.Wrap(e, commitErrMsg) - } } log.WithFields(logpkg.F{ diff --git a/services/horizon/internal/ingest/group_processors.go b/services/horizon/internal/ingest/group_processors.go index fd0843cc28..8b5d2d337e 100644 --- a/services/horizon/internal/ingest/group_processors.go +++ b/services/horizon/internal/ingest/group_processors.go @@ -56,12 +56,49 @@ func (g groupChangeProcessors) Commit(ctx context.Context) error { return nil } +type groupLoaders struct { + lazyLoaders []horizonLazyLoader + runDurations runDurations + stats map[string]history.LoaderStats +} + +func newGroupLoaders(lazyLoaders []horizonLazyLoader) groupLoaders { + return groupLoaders{ + lazyLoaders: lazyLoaders, + runDurations: make(map[string]time.Duration), + stats: make(map[string]history.LoaderStats), + } +} + +func (g groupLoaders) Flush(ctx context.Context, session db.SessionInterface, execInTx bool) error { + if execInTx { + if err := session.Begin(ctx); err != nil { + return err + } + defer session.Rollback() + } + + for _, loader := range g.lazyLoaders { + startTime := time.Now() + if err := loader.Exec(ctx, session); err != nil { + return errors.Wrapf(err, "error during lazy loader resolution, %T.Exec", loader) + } + name := loader.Name() + g.runDurations.AddRunDuration(name, startTime) + g.stats[name] = loader.Stats() + } + + if execInTx { + if err := session.Commit(); err != nil { + return err + } + } + return nil +} + type groupTransactionProcessors struct { processors []horizonTransactionProcessor - lazyLoaders []horizonLazyLoader processorsRunDurations runDurations - loaderRunDurations runDurations - loaderStats map[string]history.LoaderStats transactionStatsProcessor *processors.StatsLedgerTransactionProcessor tradeProcessor *processors.TradeProcessor } @@ -76,7 +113,6 @@ type groupTransactionProcessors struct { // // so group processing will reset stats as needed func newGroupTransactionProcessors(processors []horizonTransactionProcessor, - lazyLoaders []horizonLazyLoader, transactionStatsProcessor *processors.StatsLedgerTransactionProcessor, tradeProcessor *processors.TradeProcessor, ) *groupTransactionProcessors { @@ -84,9 +120,6 @@ func newGroupTransactionProcessors(processors []horizonTransactionProcessor, return &groupTransactionProcessors{ processors: processors, processorsRunDurations: make(map[string]time.Duration), - loaderRunDurations: make(map[string]time.Duration), - loaderStats: make(map[string]history.LoaderStats), - lazyLoaders: lazyLoaders, transactionStatsProcessor: transactionStatsProcessor, tradeProcessor: tradeProcessor, } @@ -104,20 +137,6 @@ func (g groupTransactionProcessors) ProcessTransaction(lcm xdr.LedgerCloseMeta, } func (g groupTransactionProcessors) Flush(ctx context.Context, session db.SessionInterface) error { - // need to trigger all lazy loaders to now resolve their future placeholders - // with real db values first - for _, loader := range g.lazyLoaders { - startTime := time.Now() - if err := loader.Exec(ctx, session); err != nil { - return errors.Wrapf(err, "error during lazy loader resolution, %T.Exec", loader) - } - name := loader.Name() - g.loaderRunDurations.AddRunDuration(name, startTime) - g.loaderStats[name] = loader.Stats() - } - - // now flush each processor which may call loader.GetNow(), which - // required the prior loader.Exec() to have been called. for _, p := range g.processors { startTime := time.Now() if err := p.Flush(ctx, session); err != nil { @@ -130,8 +149,6 @@ func (g groupTransactionProcessors) Flush(ctx context.Context, session db.Sessio func (g *groupTransactionProcessors) ResetStats() { g.processorsRunDurations = make(map[string]time.Duration) - g.loaderRunDurations = make(map[string]time.Duration) - g.loaderStats = make(map[string]history.LoaderStats) if g.tradeProcessor != nil { g.tradeProcessor.ResetStats() } diff --git a/services/horizon/internal/ingest/group_processors_test.go b/services/horizon/internal/ingest/group_processors_test.go index 9fc5a3acf1..058999420a 100644 --- a/services/horizon/internal/ingest/group_processors_test.go +++ b/services/horizon/internal/ingest/group_processors_test.go @@ -157,7 +157,7 @@ func (s *GroupTransactionProcessorsTestSuiteLedger) SetupTest() { s.processors = newGroupTransactionProcessors([]horizonTransactionProcessor{ s.processorA, s.processorB, - }, nil, statsProcessor, tradesProcessor) + }, statsProcessor, tradesProcessor) s.session = &db.MockSession{} } diff --git a/services/horizon/internal/ingest/ingest_history_range_state_test.go b/services/horizon/internal/ingest/ingest_history_range_state_test.go index 4f7d2c4944..cf248a6a7d 100644 --- a/services/horizon/internal/ingest/ingest_history_range_state_test.go +++ b/services/horizon/internal/ingest/ingest_history_range_state_test.go @@ -6,7 +6,6 @@ import ( "context" "testing" - "github.com/jmoiron/sqlx" "github.com/stretchr/testify/suite" "github.com/stellar/go/ingest/ledgerbackend" @@ -164,7 +163,7 @@ func (s *IngestHistoryRangeStateTestSuite) TestHistoryRangeRunTransactionProcess } s.ledgerBackend.On("GetLedger", s.ctx, uint32(100)).Return(meta, nil).Once() - s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}).Return(errors.New("my error")).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}, false).Return(errors.New("my error")).Once() next, err := historyRangeState{fromLedger: 100, toLedger: 200}.run(s.system) s.Assert().Error(err) @@ -189,7 +188,7 @@ func (s *IngestHistoryRangeStateTestSuite) TestHistoryRangeSuccess() { }, } s.ledgerBackend.On("GetLedger", s.ctx, uint32(i)).Return(meta, nil).Once() - s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}).Return(nil).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}, false).Return(nil).Once() } s.historyQ.On("Commit").Return(nil).Once() @@ -226,8 +225,8 @@ func (s *IngestHistoryRangeStateTestSuite) TestHistoryRangeSuccessWithFlushMax() } s.ledgerBackend.On("GetLedger", s.ctx, uint32(i)).Return(meta, nil).Once() } - s.runner.On("RunTransactionProcessorsOnLedgers", firstLedgersBatch).Return(nil).Once() - s.runner.On("RunTransactionProcessorsOnLedgers", secondLedgersBatch).Return(nil).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", firstLedgersBatch, false).Return(nil).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", secondLedgersBatch, false).Return(nil).Once() s.system.maxLedgerPerFlush = 60 next, err := historyRangeState{fromLedger: 100, toLedger: 200}.run(s.system) @@ -252,7 +251,7 @@ func (s *IngestHistoryRangeStateTestSuite) TestHistoryRangeCommitsWorkOnLedgerBa }, } s.ledgerBackend.On("GetLedger", s.ctx, uint32(100)).Return(meta, nil).Once() - s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}).Return(nil).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}, false).Return(nil).Once() s.ledgerBackend.On("GetLedger", s.ctx, uint32(101)). Return(xdr.LedgerCloseMeta{}, errors.New("my error")).Once() @@ -319,23 +318,11 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateInvali func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateInvalidMaxFlush() { s.historyQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(0), nil).Once() - s.historyQ.On("Begin", s.ctx).Return(nil).Once() - s.historyQ.On("Rollback").Return(nil).Once() - s.historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() s.system.maxLedgerPerFlush = 0 err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) s.Assert().EqualError(err, "invalid maxLedgerPerFlush, must be greater than 0") } -func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateBeginReturnsError() { - // Recreate mock in this single test to remove Rollback assertion. - s.historyQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(0), nil).Once() - s.historyQ.On("Begin", s.ctx).Return(errors.New("my error")).Once() - - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) - s.Assert().EqualError(err, "Error starting a transaction: my error") -} - func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateGetLastLedgerIngestNonBlockingError() { s.historyQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(0), errors.New("my error")).Once() @@ -359,9 +346,6 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStatRangeOv func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateClearHistoryFails() { s.historyQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(0), nil).Once() - s.historyQ.On("Begin", s.ctx).Return(nil).Once() - s.historyQ.On("Rollback").Return(nil).Once() - s.historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() toidFrom := toid.New(100, 0, 0) // the state deletes range once, calc'd by toid.LedgerRangeInclusive(), which adjusts to = to + 1 toidTo := toid.New(201, 0, 0) @@ -375,9 +359,6 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateClearH func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateRunTransactionProcessorsReturnsError() { s.historyQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(0), nil).Once() - s.historyQ.On("Begin", s.ctx).Return(nil).Once() - s.historyQ.On("Rollback").Return(nil).Once() - s.historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() toidFrom := toid.New(100, 0, 0) toidTo := toid.New(201, 0, 0) s.historyQ.On( @@ -395,54 +376,19 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateRunTra } s.ledgerBackend.On("GetLedger", s.ctx, uint32(100)).Return(meta, nil).Once() - s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}).Return(errors.New("my error")).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}, true).Return(errors.New("my error")).Once() err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) s.Assert().EqualError(err, "error processing ledger range 100 - 100: my error") } -func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateCommitFails() { - s.historyQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(0), nil).Once() - s.historyQ.On("Begin", s.ctx).Return(nil).Once() - s.historyQ.On("Rollback").Return(nil).Once() - s.historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() - s.historyQ.On("Commit").Return(errors.New("my error")).Once() - - toidFrom := toid.New(100, 0, 0) - toidTo := toid.New(201, 0, 0) - s.historyQ.On( - "DeleteRangeAll", s.ctx, toidFrom.ToInt64(), toidTo.ToInt64(), - ).Return(nil).Once() - - for i := uint32(100); i <= uint32(200); i++ { - meta := xdr.LedgerCloseMeta{ - V0: &xdr.LedgerCloseMetaV0{ - LedgerHeader: xdr.LedgerHeaderHistoryEntry{ - Header: xdr.LedgerHeader{ - LedgerSeq: xdr.Uint32(i), - }, - }, - }, - } - s.ledgerBackend.On("GetLedger", s.ctx, uint32(i)).Return(meta, nil).Once() - s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}).Return(nil).Once() - } - - err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) - s.Assert().EqualError(err, "Error committing db transaction: my error") -} - func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateSuccess() { s.historyQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(0), nil).Once() - s.historyQ.On("Begin", s.ctx).Return(nil).Once() - s.historyQ.On("Rollback").Return(nil).Once() - s.historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() toidFrom := toid.New(100, 0, 0) toidTo := toid.New(201, 0, 0) s.historyQ.On( "DeleteRangeAll", s.ctx, toidFrom.ToInt64(), toidTo.ToInt64(), ).Return(nil).Once() - s.historyQ.On("Commit").Return(nil).Once() s.historyQ.On("RebuildTradeAggregationBuckets", s.ctx, uint32(100), uint32(200), 0).Return(nil).Once() for i := uint32(100); i <= uint32(200); i++ { @@ -456,7 +402,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateSucces }, } s.ledgerBackend.On("GetLedger", s.ctx, uint32(i)).Return(meta, nil).Once() - s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}).Return(nil).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}, true).Return(nil).Once() } // system.maxLedgerPerFlush has been set by default to 1 in test suite setup @@ -466,15 +412,11 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateSucces func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateSuccessWithFlushMax() { s.historyQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(0), nil).Once() - s.historyQ.On("Begin", s.ctx).Return(nil).Once() - s.historyQ.On("Rollback").Return(nil).Once() - s.historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() toidFrom := toid.New(100, 0, 0) toidTo := toid.New(201, 0, 0) s.historyQ.On( "DeleteRangeAll", s.ctx, toidFrom.ToInt64(), toidTo.ToInt64(), ).Return(nil).Once() - s.historyQ.On("Commit").Return(nil).Once() s.historyQ.On("RebuildTradeAggregationBuckets", s.ctx, uint32(100), uint32(200), 0).Return(nil).Once() firstLedgersBatch := []xdr.LedgerCloseMeta{} @@ -497,8 +439,8 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateSucces } s.ledgerBackend.On("GetLedger", s.ctx, uint32(i)).Return(meta, nil).Once() } - s.runner.On("RunTransactionProcessorsOnLedgers", firstLedgersBatch).Return(nil).Once() - s.runner.On("RunTransactionProcessorsOnLedgers", secondLedgersBatch).Return(nil).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", firstLedgersBatch, true).Return(nil).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", secondLedgersBatch, true).Return(nil).Once() s.system.maxLedgerPerFlush = 60 err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) s.Assert().NoError(err) @@ -506,10 +448,6 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateSucces func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateSuccessOneLedger() { s.historyQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(0), nil).Once() - s.historyQ.On("Begin", s.ctx).Return(nil).Once() - s.historyQ.On("Rollback").Return(nil).Once() - s.historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() - s.historyQ.On("Commit").Return(nil).Once() s.historyQ.On("RebuildTradeAggregationBuckets", s.ctx, uint32(100), uint32(100), 0).Return(nil).Once() // Recreate mock in this single ledger test to remove setup assertion on ledger range. *s.ledgerBackend = mockLedgerBackend{} @@ -532,7 +470,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateSucces } s.ledgerBackend.On("GetLedger", s.ctx, uint32(100)).Return(meta, nil).Once() - s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}).Return(nil).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}, true).Return(nil).Once() err := s.system.ReingestRange([]history.LedgerRange{{100, 100}}, false, true) s.Assert().NoError(err) @@ -550,7 +488,6 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceG func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForce() { s.historyQ.On("Begin", s.ctx).Return(nil).Once() s.historyQ.On("Rollback").Return(nil).Once() - s.historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() s.historyQ.On("GetLastLedgerIngest", s.ctx).Return(uint32(190), nil).Once() s.historyQ.On("Commit").Return(nil).Once() s.historyQ.On("RebuildTradeAggregationBuckets", s.ctx, uint32(100), uint32(200), 0).Return(nil).Once() @@ -572,7 +509,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForce( }, } s.ledgerBackend.On("GetLedger", s.ctx, uint32(i)).Return(meta, nil).Once() - s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}).Return(nil).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}, false).Return(nil).Once() } // system.maxLedgerPerFlush has been set by default to 1 in test suite setup @@ -583,7 +520,6 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForce( func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceLedgerRetrievalError() { s.historyQ.On("Begin", s.ctx).Return(nil).Once() s.historyQ.On("Rollback").Return(nil).Once() - s.historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() s.historyQ.On("GetLastLedgerIngest", s.ctx).Return(uint32(190), nil).Once() s.historyQ.On("Commit").Return(nil).Once() @@ -604,7 +540,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceL }, } s.ledgerBackend.On("GetLedger", s.ctx, uint32(i)).Return(meta, nil).Once() - s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}).Return(nil).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}, false).Return(nil).Once() } s.ledgerBackend.On("GetLedger", s.ctx, uint32(106)).Return(xdr.LedgerCloseMeta{}, errors.New("my error")).Once() @@ -617,7 +553,6 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceL func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceLedgerRetrievalAndCommitError() { s.historyQ.On("Begin", s.ctx).Return(nil).Once() s.historyQ.On("Rollback").Return(nil).Once() - s.historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() s.historyQ.On("GetLastLedgerIngest", s.ctx).Return(uint32(190), nil).Once() s.historyQ.On("Commit").Return(errors.New("commit error")).Once() @@ -638,7 +573,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceL }, } s.ledgerBackend.On("GetLedger", s.ctx, uint32(i)).Return(meta, nil).Once() - s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}).Return(nil).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", []xdr.LedgerCloseMeta{meta}, false).Return(nil).Once() } s.ledgerBackend.On("GetLedger", s.ctx, uint32(106)).Return(xdr.LedgerCloseMeta{}, errors.New("my error")).Once() @@ -651,7 +586,6 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceL func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceWithFlushMax() { s.historyQ.On("Begin", s.ctx).Return(nil).Once() s.historyQ.On("Rollback").Return(nil).Once() - s.historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() s.historyQ.On("GetLastLedgerIngest", s.ctx).Return(uint32(190), nil).Once() s.historyQ.On("Commit").Return(nil).Once() s.historyQ.On("RebuildTradeAggregationBuckets", s.ctx, uint32(100), uint32(200), 0).Return(nil).Once() @@ -682,8 +616,8 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceW } s.ledgerBackend.On("GetLedger", s.ctx, uint32(i)).Return(meta, nil).Once() } - s.runner.On("RunTransactionProcessorsOnLedgers", firstLedgersBatch).Return(nil).Once() - s.runner.On("RunTransactionProcessorsOnLedgers", secondLedgersBatch).Return(nil).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", firstLedgersBatch, false).Return(nil).Once() + s.runner.On("RunTransactionProcessorsOnLedgers", secondLedgersBatch, false).Return(nil).Once() s.system.maxLedgerPerFlush = 60 err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, true, true) diff --git a/services/horizon/internal/ingest/main_test.go b/services/horizon/internal/ingest/main_test.go index 3ba66ef4ba..0db777306c 100644 --- a/services/horizon/internal/ingest/main_test.go +++ b/services/horizon/internal/ingest/main_test.go @@ -528,8 +528,8 @@ func (m *mockProcessorsRunner) RunAllProcessorsOnLedger(ledger xdr.LedgerCloseMe args.Error(1) } -func (m *mockProcessorsRunner) RunTransactionProcessorsOnLedgers(ledgers []xdr.LedgerCloseMeta) error { - args := m.Called(ledgers) +func (m *mockProcessorsRunner) RunTransactionProcessorsOnLedgers(ledgers []xdr.LedgerCloseMeta, execInTx bool) error { + args := m.Called(ledgers, execInTx) return args.Error(0) } diff --git a/services/horizon/internal/ingest/processor_runner.go b/services/horizon/internal/ingest/processor_runner.go index b98896fe1f..6832a5078f 100644 --- a/services/horizon/internal/ingest/processor_runner.go +++ b/services/horizon/internal/ingest/processor_runner.go @@ -77,7 +77,7 @@ type ProcessorRunnerInterface interface { ledgerProtocolVersion uint32, bucketListHash xdr.Hash, ) (ingest.StatsChangeProcessorResults, error) - RunTransactionProcessorsOnLedgers(ledgers []xdr.LedgerCloseMeta) error + RunTransactionProcessorsOnLedgers(ledgers []xdr.LedgerCloseMeta, execInTx bool) error RunAllProcessorsOnLedger(ledger xdr.LedgerCloseMeta) ( stats ledgerStats, err error, @@ -134,13 +134,13 @@ func buildChangeProcessor( }) } -func (s *ProcessorRunner) buildTransactionProcessor(ledgersProcessor *processors.LedgersProcessor) *groupTransactionProcessors { +func (s *ProcessorRunner) buildTransactionProcessor(ledgersProcessor *processors.LedgersProcessor) (groupLoaders, *groupTransactionProcessors) { accountLoader := history.NewAccountLoader() assetLoader := history.NewAssetLoader() lpLoader := history.NewLiquidityPoolLoader() cbLoader := history.NewClaimableBalanceLoader() - lazyLoaders := []horizonLazyLoader{accountLoader, assetLoader, lpLoader, cbLoader} + loaders := newGroupLoaders([]horizonLazyLoader{accountLoader, assetLoader, lpLoader, cbLoader}) statsLedgerTransactionProcessor := processors.NewStatsLedgerTransactionProcessor() tradeProcessor := processors.NewTradeProcessor(accountLoader, @@ -160,7 +160,7 @@ func (s *ProcessorRunner) buildTransactionProcessor(ledgersProcessor *processors processors.NewLiquidityPoolsTransactionProcessor(lpLoader, s.historyQ.NewTransactionLiquidityPoolBatchInsertBuilder(), s.historyQ.NewOperationLiquidityPoolBatchInsertBuilder())} - return newGroupTransactionProcessors(processors, lazyLoaders, statsLedgerTransactionProcessor, tradeProcessor) + return loaders, newGroupTransactionProcessors(processors, statsLedgerTransactionProcessor, tradeProcessor) } func (s *ProcessorRunner) buildTransactionFilterer() *groupTransactionFilterers { @@ -180,7 +180,7 @@ func (s *ProcessorRunner) buildFilteredOutProcessor() *groupTransactionProcessor p = append(p, txSubProc) } - return newGroupTransactionProcessors(p, nil, nil, nil) + return newGroupTransactionProcessors(p, nil, nil) } // checkIfProtocolVersionSupported checks if this Horizon version supports the @@ -408,10 +408,11 @@ func (s *ProcessorRunner) runTransactionProcessorsOnLedger(registry nameRegistry groupTransactionFilterers := s.buildTransactionFilterer() groupFilteredOutProcessors := s.buildFilteredOutProcessor() - groupTransactionProcessors := s.buildTransactionProcessor(ledgersProcessor) + loaders, groupTransactionProcessors := s.buildTransactionProcessor(ledgersProcessor) if err = registerTransactionProcessors( registry, + loaders, groupTransactionFilterers, groupFilteredOutProcessors, groupTransactionProcessors, @@ -429,7 +430,11 @@ func (s *ProcessorRunner) runTransactionProcessorsOnLedger(registry nameRegistry return } - err = s.flushProcessors(groupFilteredOutProcessors, groupTransactionProcessors) + if err = loaders.Flush(s.ctx, s.session, false); err != nil { + return + } + + err = s.flushProcessors(groupFilteredOutProcessors, groupTransactionProcessors, false) if err != nil { return } @@ -441,8 +446,8 @@ func (s *ProcessorRunner) runTransactionProcessorsOnLedger(registry nameRegistry for key, duration := range groupFilteredOutProcessors.processorsRunDurations { transactionDurations[key] = duration } - loaderStats = groupTransactionProcessors.loaderStats - loaderDurations = groupTransactionProcessors.loaderRunDurations + loaderStats = loaders.stats + loaderDurations = loaders.runDurations for key, duration := range groupTransactionFilterers.runDurations { transactionDurations[key] = duration } @@ -478,6 +483,7 @@ func registerChangeProcessors( func registerTransactionProcessors( registry nameRegistry, + loaders groupLoaders, groupTransactionFilterers *groupTransactionFilterers, groupFilteredOutProcessors *groupTransactionProcessors, groupTransactionProcessors *groupTransactionProcessors, @@ -492,7 +498,7 @@ func registerTransactionProcessors( return err } } - for _, l := range groupTransactionProcessors.lazyLoaders { + for _, l := range loaders.lazyLoaders { if err := registry.add(l.Name()); err != nil { return err } @@ -502,20 +508,15 @@ func registerTransactionProcessors( return err } } - for _, l := range groupFilteredOutProcessors.lazyLoaders { - if err := registry.add(l.Name()); err != nil { - return err - } - } return nil } -func (s *ProcessorRunner) RunTransactionProcessorsOnLedgers(ledgers []xdr.LedgerCloseMeta) (err error) { +func (s *ProcessorRunner) RunTransactionProcessorsOnLedgers(ledgers []xdr.LedgerCloseMeta, execInTx bool) (err error) { ledgersProcessor := processors.NewLedgerProcessor(s.historyQ.NewLedgerBatchInsertBuilder(), CurrentVersion) groupTransactionFilterers := s.buildTransactionFilterer() groupFilteredOutProcessors := s.buildFilteredOutProcessor() - groupTransactionProcessors := s.buildTransactionProcessor(ledgersProcessor) + loaders, groupTransactionProcessors := s.buildTransactionProcessor(ledgersProcessor) startTime := time.Now() curHeap, sysHeap := getMemStats() @@ -546,7 +547,11 @@ func (s *ProcessorRunner) RunTransactionProcessorsOnLedgers(ledgers []xdr.Ledger groupTransactionFilterers.ResetStats() } - err = s.flushProcessors(groupFilteredOutProcessors, groupTransactionProcessors) + if err = loaders.Flush(s.ctx, s.session, execInTx); err != nil { + return + } + + err = s.flushProcessors(groupFilteredOutProcessors, groupTransactionProcessors, execInTx) if err != nil { return } @@ -564,23 +569,36 @@ func (s *ProcessorRunner) RunTransactionProcessorsOnLedgers(ledgers []xdr.Ledger return nil } -func (s *ProcessorRunner) flushProcessors(groupFilteredOutProcessors *groupTransactionProcessors, groupTransactionProcessors *groupTransactionProcessors) (err error) { +func (s *ProcessorRunner) flushProcessors(groupFilteredOutProcessors *groupTransactionProcessors, groupTransactionProcessors *groupTransactionProcessors, execInTx bool) error { + if execInTx { + if err := s.session.Begin(s.ctx); err != nil { + return err + } + defer s.session.Rollback() + } + if s.config.EnableIngestionFiltering { - err = groupFilteredOutProcessors.Flush(s.ctx, s.session) - if err != nil { - err = errors.Wrap(err, "Error flushing temp filtered tx from processor") - return + + if err := groupFilteredOutProcessors.Flush(s.ctx, s.session); err != nil { + return errors.Wrap(err, "Error flushing temp filtered tx from processor") } if time.Since(s.lastTransactionsTmpGC) > transactionsFilteredTmpGCPeriod { - s.historyQ.DeleteTransactionsFilteredTmpOlderThan(s.ctx, uint64(transactionsFilteredTmpGCPeriod.Seconds())) + if _, err := s.historyQ.DeleteTransactionsFilteredTmpOlderThan(s.ctx, uint64(transactionsFilteredTmpGCPeriod.Seconds())); err != nil { + return errors.Wrap(err, "Error trimming filtered transactions") + } } } - err = groupTransactionProcessors.Flush(s.ctx, s.session) - if err != nil { - err = errors.Wrap(err, "Error flushing changes from processor") + if err := groupTransactionProcessors.Flush(s.ctx, s.session); err != nil { + return errors.Wrap(err, "Error flushing changes from processor") } - return + + if execInTx { + if err := s.session.Commit(); err != nil { + return err + } + } + return nil } func (s *ProcessorRunner) RunAllProcessorsOnLedger(ledger xdr.LedgerCloseMeta) ( diff --git a/services/horizon/internal/ingest/processor_runner_test.go b/services/horizon/internal/ingest/processor_runner_test.go index cad841b602..8f6eb58d74 100644 --- a/services/horizon/internal/ingest/processor_runner_test.go +++ b/services/horizon/internal/ingest/processor_runner_test.go @@ -249,7 +249,7 @@ func TestProcessorRunnerBuildTransactionProcessor(t *testing.T) { ledgersProcessor := &processors.LedgersProcessor{} - processor := runner.buildTransactionProcessor(ledgersProcessor) + _, processor := runner.buildTransactionProcessor(ledgersProcessor) assert.IsType(t, &groupTransactionProcessors{}, processor) assert.IsType(t, &processors.StatsLedgerTransactionProcessor{}, processor.processors[0]) assert.IsType(t, &processors.EffectProcessor{}, processor.processors[1]) @@ -477,7 +477,7 @@ func TestProcessorRunnerRunTransactionsProcessorsOnLedgers(t *testing.T) { filters: &MockFilters{}, } - err := runner.RunTransactionProcessorsOnLedgers(ledgers) + err := runner.RunTransactionProcessorsOnLedgers(ledgers, false) assert.NoError(t, err) } From f908a424f5d1966b582757a2102d1f8f44b4beed Mon Sep 17 00:00:00 2001 From: tamirms Date: Tue, 16 Apr 2024 00:51:20 +0100 Subject: [PATCH 096/234] ingest: reduce memory consumption of CheckpointChangeReader (#5270) --- ingest/checkpoint_change_reader.go | 218 +++++++----------------- ingest/checkpoint_change_reader_test.go | 212 ++++++++++++++++++++++- ingest/memory_temp_set.go | 38 ----- ingest/memory_temp_set_test.go | 38 ----- 4 files changed, 268 insertions(+), 238 deletions(-) delete mode 100644 ingest/memory_temp_set.go delete mode 100644 ingest/memory_temp_set_test.go diff --git a/ingest/checkpoint_change_reader.go b/ingest/checkpoint_change_reader.go index d1365740c6..1dbbed2a9e 100644 --- a/ingest/checkpoint_change_reader.go +++ b/ingest/checkpoint_change_reader.go @@ -7,6 +7,7 @@ import ( "time" "github.com/stellar/go/historyarchive" + "github.com/stellar/go/support/collections/set" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" ) @@ -21,15 +22,15 @@ type readResult struct { // snapshot. The Changes produced by a CheckpointChangeReader reflect the state of the Stellar // network at a particular checkpoint ledger sequence. type CheckpointChangeReader struct { - ctx context.Context - has *historyarchive.HistoryArchiveState - archive historyarchive.ArchiveInterface - tempStore tempSet - sequence uint32 - readChan chan readResult - streamOnce sync.Once - closeOnce sync.Once - done chan bool + ctx context.Context + has *historyarchive.HistoryArchiveState + archive historyarchive.ArchiveInterface + visitedLedgerKeys set.Set[string] + sequence uint32 + readChan chan readResult + streamOnce sync.Once + closeOnce sync.Once + done chan bool readBytesMutex sync.RWMutex totalRead int64 @@ -45,32 +46,11 @@ type CheckpointChangeReader struct { // Ensure CheckpointChangeReader implements ChangeReader var _ ChangeReader = &CheckpointChangeReader{} -// tempSet is an interface that must be implemented by stores that -// hold temporary set of objects for state reader. The implementation -// does not need to be thread-safe. -type tempSet interface { - Open() error - // Preload batch-loads keys into internal cache (if a store has any) to - // improve execution time by removing many round-trips. - Preload(keys []string) error - // Add adds key to the store. - Add(key string) error - // Exist returns value true if the value is found in the store. - // If the value has not been set, it should return false. - Exist(key string) (bool, error) - Close() error -} - const ( // maxStreamRetries defines how many times should we retry when there are errors in // the xdr stream returned by GetXdrStreamForHash(). maxStreamRetries = 3 msrBufferSize = 50000 - - // preloadedEntries defines a number of bucket entries to preload from a - // bucket in a single run. This is done to allow preloading keys from - // temp set. - preloadedEntries = 20000 ) // NewCheckpointChangeReader constructs a new CheckpointChangeReader instance. @@ -102,24 +82,18 @@ func NewCheckpointChangeReader( return nil, errors.Wrapf(err, "unable to get checkpoint HAS at ledger sequence %d", sequence) } - tempStore := &memoryTempSet{} - err = tempStore.Open() - if err != nil { - return nil, errors.Wrap(err, "unable to get open temp store") - } - return &CheckpointChangeReader{ - ctx: ctx, - has: &has, - archive: archive, - tempStore: tempStore, - sequence: sequence, - readChan: make(chan readResult, msrBufferSize), - streamOnce: sync.Once{}, - closeOnce: sync.Once{}, - done: make(chan bool), - encodingBuffer: xdr.NewEncodingBuffer(), - sleep: time.Sleep, + ctx: ctx, + has: &has, + archive: archive, + visitedLedgerKeys: set.Set[string]{}, + sequence: sequence, + readChan: make(chan readResult, msrBufferSize), + streamOnce: sync.Once{}, + closeOnce: sync.Once{}, + done: make(chan bool), + encodingBuffer: xdr.NewEncodingBuffer(), + sleep: time.Sleep, }, nil } @@ -141,21 +115,18 @@ func (r *CheckpointChangeReader) bucketExists(hash historyarchive.Hash) (bool, e // However, we can modify this algorithm to work from newest to oldest ledgers: // // 1. For each `INITENTRY`/`LIVEENTRY` we check if we've seen the key before -// (stored in `tempStore`). If the key hasn't been seen, we write that bucket -// entry to the stream and add it to the `tempStore` (we don't mark `INITENTRY`, +// (stored in `visitedLedgerKeys`). If the key hasn't been seen, we write that bucket +// entry to the stream and add it to the `visitedLedgerKeys` (we don't mark `INITENTRY`, // see the inline comment or CAP-20). // 2. For each `DEADENTRY` we keep track of removed bucket entries in -// `tempStore` map. +// `visitedLedgerKeys` map. // // In such algorithm we just need to store a set of keys that require much less space. // The memory requirements will be lowered when CAP-0020 is live and older buckets are // rewritten. Then, we will only need to keep track of `DEADENTRY`. func (r *CheckpointChangeReader) streamBuckets() { defer func() { - err := r.tempStore.Close() - if err != nil { - r.readChan <- r.error(errors.New("Error closing tempStore")) - } + r.visitedLedgerKeys = nil r.closeOnce.Do(r.close) close(r.readChan) @@ -305,96 +276,21 @@ func (r *CheckpointChangeReader) streamBucketContents(hash historyarchive.Hash, // No METAENTRY means that bucket originates from before protocol version 11. bucketProtocolVersion := uint32(0) - n := -1 - var batch []xdr.BucketEntry - lastBatch := false - - preloadKeys := make([]string, 0, preloadedEntries) - -LoopBucketEntry: - for { - // Preload entries for faster retrieve from temp store. - if len(batch) == 0 { - if lastBatch { + for n := 0; ; n++ { + var entry xdr.BucketEntry + entry, e = r.readBucketEntry(rdr, hash) + if e != nil { + if e == io.EOF { + // No entries loaded for this batch, nothing more to process return true } - batch = make([]xdr.BucketEntry, 0, preloadedEntries) - - // reset the content of the preloadKeys - preloadKeys = preloadKeys[:0] - - for i := 0; i < preloadedEntries; i++ { - var entry xdr.BucketEntry - entry, e = r.readBucketEntry(rdr, hash) - if e != nil { - if e == io.EOF { - if len(batch) == 0 { - // No entries loaded for this batch, nothing more to process - return true - } - lastBatch = true - break - } - r.readChan <- r.error( - errors.Wrapf(e, "Error on XDR record %d of hash '%s'", n, hash.String()), - ) - return false - } - - batch = append(batch, entry) - - // Generate a key - var key xdr.LedgerKey - var err error - - switch entry.Type { - case xdr.BucketEntryTypeLiveentry, xdr.BucketEntryTypeInitentry: - liveEntry := entry.MustLiveEntry() - key, err = liveEntry.LedgerKey() - if err != nil { - r.readChan <- r.error( - errors.Wrapf(err, "Error generating ledger key for XDR record %d of hash '%s'", n, hash.String()), - ) - return false - } - case xdr.BucketEntryTypeDeadentry: - key = entry.MustDeadEntry() - default: - // No ledger key associated with this entry, continue to the next one. - continue - } - - // We're using compressed keys here - // safe, since we are converting to string right away - keyBytes, e := r.encodingBuffer.LedgerKeyUnsafeMarshalBinaryCompress(key) - if e != nil { - r.readChan <- r.error( - errors.Wrapf(e, "Error marshaling XDR record %d of hash '%s'", n, hash.String()), - ) - return false - } - - h := string(keyBytes) - preloadKeys = append(preloadKeys, h) - } - - err := r.tempStore.Preload(preloadKeys) - if err != nil { - r.readChan <- r.error(errors.Wrap(err, "Error preloading keys")) - return false - } + r.readChan <- r.error( + errors.Wrapf(e, "Error on XDR record %d of hash '%s'", n, hash.String()), + ) + return false } - var entry xdr.BucketEntry - entry, batch = batch[0], batch[1:] - - n++ - - var key xdr.LedgerKey - var err error - - switch entry.Type { - case xdr.BucketEntryTypeMetaentry: + if entry.Type == xdr.BucketEntryTypeMetaentry { if n != 0 { r.readChan <- r.error( errors.Errorf( @@ -407,7 +303,13 @@ LoopBucketEntry: // We can't use MustMetaEntry() here. Check: // https://github.com/golang/go/issues/32560 bucketProtocolVersion = uint32(entry.MetaEntry.LedgerVersion) - continue LoopBucketEntry + continue + } + + var key xdr.LedgerKey + var err error + + switch entry.Type { case xdr.BucketEntryTypeLiveentry, xdr.BucketEntryTypeInitentry: liveEntry := entry.MustLiveEntry() key, err = liveEntry.LedgerKey() @@ -439,6 +341,12 @@ LoopBucketEntry: } h := string(keyBytes) + // claimable balances and offers have unique ids + // once a claimable balance or offer is created we can assume that + // the id can never be recreated again, unlike, for example, trustlines + // which can be deleted and then recreated + unique := key.Type == xdr.LedgerEntryTypeClaimableBalance || + key.Type == xdr.LedgerEntryTypeOffer switch entry.Type { case xdr.BucketEntryTypeLiveentry, xdr.BucketEntryTypeInitentry: @@ -449,13 +357,7 @@ LoopBucketEntry: return false } - seen, err := r.tempStore.Exist(h) - if err != nil { - r.readChan <- r.error(errors.Wrap(err, "Error reading from tempStore")) - return false - } - - if !seen { + if !r.visitedLedgerKeys.Contains(h) { // Return LEDGER_ENTRY_STATE changes only now. liveEntry := entry.MustLiveEntry() entryChange := xdr.LedgerEntryChange{ @@ -464,32 +366,28 @@ LoopBucketEntry: } r.readChan <- readResult{entryChange, nil} - // We don't update `tempStore` for INITENTRY because CAP-20 says: + // We don't update `visitedLedgerKeys` for INITENTRY because CAP-20 says: // > a bucket entry marked INITENTRY implies that either no entry // > with the same ledger key exists in an older bucket, or else // > that the (chronologically) preceding entry with the same ledger // > key was DEADENTRY. if entry.Type == xdr.BucketEntryTypeLiveentry { - // We skip adding entries from the last bucket to tempStore because: + // We skip adding entries from the last bucket to visitedLedgerKeys because: // 1. Ledger keys are unique within a single bucket. // 2. This is the last bucket we process so there's no need to track // seen last entries in this bucket. if oldestBucket { continue } - err := r.tempStore.Add(h) - if err != nil { - r.readChan <- r.error(errors.Wrap(err, "Error updating to tempStore")) - return false - } + r.visitedLedgerKeys.Add(h) } + } else if entry.Type == xdr.BucketEntryTypeInitentry && unique { + // we can remove the ledger key because we know that it's unique in the ledger + // and cannot be recreated + r.visitedLedgerKeys.Remove(h) } case xdr.BucketEntryTypeDeadentry: - err := r.tempStore.Add(h) - if err != nil { - r.readChan <- r.error(errors.Wrap(err, "Error writing to tempStore")) - return false - } + r.visitedLedgerKeys.Add(h) default: r.readChan <- r.error( errors.Errorf("Unexpected entry type %d: %d@%s", entry.Type, n, hash.String()), diff --git a/ingest/checkpoint_change_reader_test.go b/ingest/checkpoint_change_reader_test.go index 958f3199ac..35ec2bb076 100644 --- a/ingest/checkpoint_change_reader_test.go +++ b/ingest/checkpoint_change_reader_test.go @@ -10,11 +10,12 @@ import ( "testing" "time" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "github.com/stellar/go/historyarchive" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" ) func TestSingleLedgerStateReaderTestSuite(t *testing.T) { @@ -220,6 +221,141 @@ func (s *SingleLedgerStateReaderTestSuite) TestEnsureLatestLiveEntry() { s.Require().Equal(err, io.EOF) } +func (s *SingleLedgerStateReaderTestSuite) TestUniqueInitEntryOptimization() { + curr1 := createXdrStream( + metaEntry(20), + entryAccount(xdr.BucketEntryTypeLiveentry, "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", 1), + entryCB(xdr.BucketEntryTypeDeadentry, xdr.Hash{1, 2, 3}, 100), + entryOffer(xdr.BucketEntryTypeDeadentry, "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", 20), + ) + + snap1 := createXdrStream( + metaEntry(20), + entryAccount(xdr.BucketEntryTypeInitentry, "GALPCCZN4YXA3YMJHKL6CVIECKPLJJCTVMSNYWBTKJW4K5HQLYLDMZTB", 1), + entryAccount(xdr.BucketEntryTypeInitentry, "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", 1), + entryAccount(xdr.BucketEntryTypeInitentry, "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", 1), + entryCB(xdr.BucketEntryTypeInitentry, xdr.Hash{1, 2, 3}, 100), + entryOffer(xdr.BucketEntryTypeInitentry, "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", 20), + entryAccount(xdr.BucketEntryTypeInitentry, "GAP2KHWUMOHY7IO37UJY7SEBIITJIDZS5DRIIQRPEUT4VUKHZQGIRWS4", 1), + entryAccount(xdr.BucketEntryTypeInitentry, "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", 1), + ) + + nextBucket := s.getNextBucketChannel() + + // Return curr1 and snap1 stream for the first two bucket... + s.mockArchive. + On("GetXdrStreamForHash", <-nextBucket). + Return(curr1, nil).Once() + + s.mockArchive. + On("GetXdrStreamForHash", <-nextBucket). + Return(snap1, nil).Once() + + // ...and empty streams for the rest of the buckets. + for hash := range nextBucket { + s.mockArchive. + On("GetXdrStreamForHash", hash). + Return(createXdrStream(), nil).Once() + } + + // replace readChan with an unbuffered channel so we can test behavior of when items are added / removed + // from visitedLedgerKeys + s.reader.readChan = make(chan readResult, 0) + + change, err := s.reader.Read() + s.Require().NoError(err) + key, err := change.Post.Data.LedgerKey() + s.Require().NoError(err) + s.Require().True( + key.Equals(xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.LedgerKeyAccount{AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML")}, + }), + ) + + change, err = s.reader.Read() + s.Require().NoError(err) + key, err = change.Post.Data.LedgerKey() + s.Require().NoError(err) + s.Require().True( + key.Equals(xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.LedgerKeyAccount{AccountId: xdr.MustAddress("GALPCCZN4YXA3YMJHKL6CVIECKPLJJCTVMSNYWBTKJW4K5HQLYLDMZTB")}, + }), + ) + s.Require().Equal(len(s.reader.visitedLedgerKeys), 3) + s.assertVisitedLedgerKeysContains(xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.LedgerKeyAccount{AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML")}, + }) + s.assertVisitedLedgerKeysContains(xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.LedgerKeyOffer{ + SellerId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML"), + OfferId: 20, + }, + }) + s.assertVisitedLedgerKeysContains(xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeClaimableBalance, + ClaimableBalance: &xdr.LedgerKeyClaimableBalance{ + BalanceId: xdr.ClaimableBalanceId{ + Type: xdr.ClaimableBalanceIdTypeClaimableBalanceIdTypeV0, + V0: &xdr.Hash{1, 2, 3}, + }, + }, + }) + + change, err = s.reader.Read() + s.Require().NoError(err) + key, err = change.Post.Data.LedgerKey() + s.Require().NoError(err) + s.Require().True( + key.Equals(xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.LedgerKeyAccount{AccountId: xdr.MustAddress("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H")}, + }), + ) + + change, err = s.reader.Read() + s.Require().NoError(err) + key, err = change.Post.Data.LedgerKey() + s.Require().NoError(err) + s.Require().True( + key.Equals(xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.LedgerKeyAccount{AccountId: xdr.MustAddress("GAP2KHWUMOHY7IO37UJY7SEBIITJIDZS5DRIIQRPEUT4VUKHZQGIRWS4")}, + }), + ) + // the offer and cb ledger keys should now be removed from visitedLedgerKeys + // because we encountered the init entries in the bucket + s.Require().Equal(len(s.reader.visitedLedgerKeys), 1) + s.assertVisitedLedgerKeysContains(xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.LedgerKeyAccount{AccountId: xdr.MustAddress("GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML")}, + }) + + change, err = s.reader.Read() + s.Require().NoError(err) + key, err = change.Post.Data.LedgerKey() + s.Require().NoError(err) + s.Require().True( + key.Equals(xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeAccount, + Account: &xdr.LedgerKeyAccount{AccountId: xdr.MustAddress("GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR")}, + }), + ) + + _, err = s.reader.Read() + s.Require().Equal(err, io.EOF) +} + +func (s *SingleLedgerStateReaderTestSuite) assertVisitedLedgerKeysContains(key xdr.LedgerKey) { + encodingBuffer := xdr.NewEncodingBuffer() + keyBytes, err := encodingBuffer.LedgerKeyUnsafeMarshalBinaryCompress(key) + s.Require().NoError(err) + s.Require().True(s.reader.visitedLedgerKeys.Contains(string(keyBytes))) +} + // TestMalformedProtocol11Bucket tests a buggy protocol 11 bucket (meta not the first entry) func (s *SingleLedgerStateReaderTestSuite) TestMalformedProtocol11Bucket() { curr1 := createXdrStream( @@ -733,6 +869,78 @@ func entryAccount(t xdr.BucketEntryType, id string, balance uint32) xdr.BucketEn } } +func entryCB(t xdr.BucketEntryType, id xdr.Hash, balance xdr.Int64) xdr.BucketEntry { + switch t { + case xdr.BucketEntryTypeLiveentry, xdr.BucketEntryTypeInitentry: + return xdr.BucketEntry{ + Type: t, + LiveEntry: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeClaimableBalance, + ClaimableBalance: &xdr.ClaimableBalanceEntry{ + BalanceId: xdr.ClaimableBalanceId{ + Type: xdr.ClaimableBalanceIdTypeClaimableBalanceIdTypeV0, + V0: &id, + }, + Asset: xdr.MustNewNativeAsset(), + Amount: balance, + }, + }, + }, + } + case xdr.BucketEntryTypeDeadentry: + return xdr.BucketEntry{ + Type: xdr.BucketEntryTypeDeadentry, + DeadEntry: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeClaimableBalance, + ClaimableBalance: &xdr.LedgerKeyClaimableBalance{ + BalanceId: xdr.ClaimableBalanceId{ + Type: xdr.ClaimableBalanceIdTypeClaimableBalanceIdTypeV0, + V0: &id, + }, + }, + }, + } + default: + panic("Unknown entry type") + } +} + +func entryOffer(t xdr.BucketEntryType, seller string, id xdr.Int64) xdr.BucketEntry { + switch t { + case xdr.BucketEntryTypeLiveentry, xdr.BucketEntryTypeInitentry: + return xdr.BucketEntry{ + Type: t, + LiveEntry: &xdr.LedgerEntry{ + Data: xdr.LedgerEntryData{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.OfferEntry{ + OfferId: id, + SellerId: xdr.MustAddress(seller), + Selling: xdr.MustNewNativeAsset(), + Buying: xdr.MustNewCreditAsset("USD", "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN7"), + Amount: 100, + Price: xdr.Price{1, 1}, + }, + }, + }, + } + case xdr.BucketEntryTypeDeadentry: + return xdr.BucketEntry{ + Type: xdr.BucketEntryTypeDeadentry, + DeadEntry: &xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeOffer, + Offer: &xdr.LedgerKeyOffer{ + OfferId: id, + SellerId: xdr.MustAddress(seller), + }, + }, + } + default: + panic("Unknown entry type") + } +} + type errCloser struct { io.Reader err error diff --git a/ingest/memory_temp_set.go b/ingest/memory_temp_set.go deleted file mode 100644 index 26096e57e6..0000000000 --- a/ingest/memory_temp_set.go +++ /dev/null @@ -1,38 +0,0 @@ -package ingest - -// memoryTempSet is an in-memory implementation of TempSet interface. -// As of July 2019 this requires up to ~4GB of memory for pubnet ledger -// state processing. The internal structure is dereferenced after the -// store is closed. -type memoryTempSet struct { - m map[string]bool -} - -// Open initialize internals data structure. -func (s *memoryTempSet) Open() error { - s.m = make(map[string]bool) - return nil -} - -// Add adds a key to TempSet. -func (s *memoryTempSet) Add(key string) error { - s.m[key] = true - return nil -} - -// Preload does not do anything. This TempSet keeps everything in memory -// so no preloading needed. -func (s *memoryTempSet) Preload(keys []string) error { - return nil -} - -// Exist check if the key exists in a TempSet. -func (s *memoryTempSet) Exist(key string) (bool, error) { - return s.m[key], nil -} - -// Close removes reference to internal data structure. -func (s *memoryTempSet) Close() error { - s.m = nil - return nil -} diff --git a/ingest/memory_temp_set_test.go b/ingest/memory_temp_set_test.go deleted file mode 100644 index 8f56c0f86f..0000000000 --- a/ingest/memory_temp_set_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package ingest - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestMemoryTempSet(t *testing.T) { - s := memoryTempSet{} - assert.Nil(t, s.m) - err := s.Open() - assert.NoError(t, err) - assert.NotNil(t, s.m) - - err = s.Add("a") - assert.NoError(t, err) - - err = s.Add("b") - assert.NoError(t, err) - - v, err := s.Exist("a") - assert.NoError(t, err) - assert.True(t, v) - - v, err = s.Exist("b") - assert.NoError(t, err) - assert.True(t, v) - - // Get for not-set key should return false - v, err = s.Exist("c") - assert.NoError(t, err) - assert.False(t, v) - - err = s.Close() - assert.NoError(t, err) - assert.Nil(t, s.m) -} From 865a3385c79fa519a8bee2c478eb33ce00207dc4 Mon Sep 17 00:00:00 2001 From: Anup Pani <127880479+anupsdf@users.noreply.github.com> Date: Tue, 16 Apr 2024 07:33:52 -0700 Subject: [PATCH 097/234] refresh quorum config based off https://www.stellarbeat.io/ (#5276) Co-authored-by: Molly Karcher Co-authored-by: George --- .../ledgerexporter/captive-core-pubnet.cfg | 242 +++++++++--------- exp/tools/dump-ledger-state/stellar-core.cfg | 218 ++++++++++------ .../configs/captive-core-pubnet.cfg | 242 +++++++++--------- .../horizon/docker/stellar-core-pubnet.cfg | 242 +++++++++--------- .../internal/configs/captive-core-pubnet.cfg | 242 +++++++++--------- 5 files changed, 618 insertions(+), 568 deletions(-) diff --git a/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg b/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg index c59b411c5d..d891626756 100644 --- a/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg +++ b/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg @@ -9,190 +9,190 @@ NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" HTTP_PORT=11626 [[HOME_DOMAINS]] -HOME_DOMAIN="stellar.org" +HOME_DOMAIN="publicnode.org" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="satoshipay.io" +HOME_DOMAIN="lobstr.co" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="lobstr.co" +HOME_DOMAIN="www.franklintempleton.com" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="www.coinqvest.com" +HOME_DOMAIN="satoshipay.io" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="publicnode.org" +HOME_DOMAIN="whalestack.com" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="stellar.blockdaemon.com" +HOME_DOMAIN="www.stellar.org" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN = "www.franklintempleton.com" -QUALITY = "HIGH" - -[[VALIDATORS]] -NAME="sdf_1" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" -ADDRESS="core-live-a.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" - -[[VALIDATORS]] -NAME="sdf_2" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" -ADDRESS="core-live-b.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" +QUALITY="HIGH" [[VALIDATORS]] -NAME="sdf_3" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" -ADDRESS="core-live-c.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" +NAME="Boötes" +PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" +ADDRESS="bootes.publicnode.org:11625" +HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_singapore" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" -ADDRESS="stellar-sg-sin.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" +NAME="Lyra by BP Ventures" +PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" +ADDRESS="lyra.publicnode.org:11625" +HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_iowa" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" -ADDRESS="stellar-us-iowa.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" +NAME="Hercules by OG Technologies" +PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" +ADDRESS="hercules.publicnode.org:11625" +HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_frankfurt" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" -ADDRESS="stellar-de-fra.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" +NAME="LOBSTR 3 (North America)" +PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" +ADDRESS="v3.stellar.lobstr.co:11625" +HISTORY="curl -sf https://archive.v3.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="lobstr_1_europe" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 1 (Europe)" PUBLIC_KEY="GCFONE23AB7Y6C5YZOMKUKGETPIAJA4QOYLS5VNS4JHBGKRZCPYHDLW7" ADDRESS="v1.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-1-lobstr.s3.amazonaws.com/{0} -o {1}" - -[[VALIDATORS]] -NAME="lobstr_2_europe" +HISTORY="curl -sf https://archive.v1.stellar.lobstr.co/{0} -o {1}" HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GDXQB3OMMQ6MGG43PWFBZWBFKBBDUZIVSUDAZZTRAWQZKES2CDSE5HKJ" -ADDRESS="v2.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-2-lobstr.s3.amazonaws.com/{0} -o {1}" [[VALIDATORS]] -NAME="lobstr_3_north_america" +NAME="LOBSTR 2 (Europe)" +PUBLIC_KEY="GCB2VSADESRV2DDTIVTFLBDI562K6KE3KMKILBHUHUWFXCUBHGQDI7VL" +ADDRESS="v2.stellar.lobstr.co:11625" +HISTORY="curl -sf https://archive.v2.stellar.lobstr.co/{0} -o {1}" HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" -ADDRESS="v3.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-3-lobstr.s3.amazonaws.com/{0} -o {1}" [[VALIDATORS]] -NAME="lobstr_4_asia" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 4 (Asia)" PUBLIC_KEY="GA7TEPCBDQKI7JQLQ34ZURRMK44DVYCIGVXQQWNSWAEQR6KB4FMCBT7J" ADDRESS="v4.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-4-lobstr.s3.amazonaws.com/{0} -o {1}" +HISTORY="curl -sf https://archive.v4.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="lobstr_5_australia" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 5 (India)" PUBLIC_KEY="GA5STBMV6QDXFDGD62MEHLLHZTPDI77U3PFOD2SELU5RJDHQWBR5NNK7" ADDRESS="v5.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-5-lobstr.s3.amazonaws.com/{0} -o {1}" +HISTORY="curl -sf https://archive.v5.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="coinqvest_hong_kong" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" -ADDRESS="hongkong.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://hongkong.stellar.coinqvest.com/history/{0} -o {1}" +NAME="FT SCV 2" +PUBLIC_KEY="GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" +ADDRESS="stellar2.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" [[VALIDATORS]] -NAME="coinqvest_germany" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" -ADDRESS="germany.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://germany.stellar.coinqvest.com/history/{0} -o {1}" +NAME="FT SCV 3" +PUBLIC_KEY="GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" +ADDRESS="stellar3.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" [[VALIDATORS]] -NAME="coinqvest_finland" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" -ADDRESS="finland.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://finland.stellar.coinqvest.com/history/{0} -o {1}" +NAME="FT SCV 1" +PUBLIC_KEY="GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" +ADDRESS="stellar1.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" [[VALIDATORS]] -NAME="bootes" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" -ADDRESS="bootes.publicnode.org" -HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" +NAME="SatoshiPay Frankfurt" +PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" +ADDRESS="stellar-de-fra.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" [[VALIDATORS]] -NAME="hercules" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" -ADDRESS="hercules.publicnode.org" -HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" +NAME="SatoshiPay Singapore" +PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" +ADDRESS="stellar-sg-sin.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" [[VALIDATORS]] -NAME="lyra" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" -ADDRESS="lyra.publicnode.org" -HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" +NAME="SatoshiPay Iowa" +PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" +ADDRESS="stellar-us-iowa.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" [[VALIDATORS]] -NAME="Blockdaemon_Validator_1" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" -ADDRESS="stellar-full-validator1.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" +NAME="Whalestack (Germany)" +PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" +ADDRESS="germany.stellar.whalestack.com:11625" +HISTORY="curl -sf https://germany.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" [[VALIDATORS]] -NAME="Blockdaemon_Validator_2" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" -ADDRESS="stellar-full-validator2.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" +NAME="Whalestack (Hong Kong)" +PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" +ADDRESS="hongkong.stellar.whalestack.com:11625" +HISTORY="curl -sf https://hongkong.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" [[VALIDATORS]] -NAME="Blockdaemon_Validator_3" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" -ADDRESS="stellar-full-validator3.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" +NAME="Whalestack (Finland)" +PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" +ADDRESS="finland.stellar.whalestack.com:11625" +HISTORY="curl -sf https://finland.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" [[VALIDATORS]] -NAME = "FT_SCV_1" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" -ADDRESS = "stellar1.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" +NAME="SDF 2" +PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" +ADDRESS="core-live-b.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" [[VALIDATORS]] -NAME = "FT_SCV_2" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" -ADDRESS = "stellar2.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" +NAME="SDF 1" +PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" +ADDRESS="core-live-a.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" [[VALIDATORS]] -NAME = "FT_SCV_3" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" -ADDRESS = "stellar3.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" +NAME="SDF 3" +PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" +ADDRESS="core-live-c.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 3" +PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" +ADDRESS="stellar-full-validator3.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 2" +PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" +ADDRESS="stellar-full-validator2.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 1" +PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" +ADDRESS="stellar-full-validator1.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" \ No newline at end of file diff --git a/exp/tools/dump-ledger-state/stellar-core.cfg b/exp/tools/dump-ledger-state/stellar-core.cfg index 479cae5d5d..0b4e454681 100644 --- a/exp/tools/dump-ledger-state/stellar-core.cfg +++ b/exp/tools/dump-ledger-state/stellar-core.cfg @@ -10,140 +10,190 @@ CATCHUP_RECENT=1 get="cp /opt/stellar/history-cache/{0} {1}" [[HOME_DOMAINS]] -HOME_DOMAIN="stellar.org" +HOME_DOMAIN="publicnode.org" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="satoshipay.io" +HOME_DOMAIN="lobstr.co" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="lobstr.co" +HOME_DOMAIN="www.franklintempleton.com" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="www.coinqvest.com" +HOME_DOMAIN="satoshipay.io" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="publicnode.org" +HOME_DOMAIN="whalestack.com" QUALITY="HIGH" -[[VALIDATORS]] -NAME="sdf_1" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" -ADDRESS="core-live-a.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" +[[HOME_DOMAINS]] +HOME_DOMAIN="www.stellar.org" +QUALITY="HIGH" -[[VALIDATORS]] -NAME="sdf_2" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" -ADDRESS="core-live-b.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" +[[HOME_DOMAINS]] +HOME_DOMAIN="stellar.blockdaemon.com" +QUALITY="HIGH" [[VALIDATORS]] -NAME="sdf_3" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" -ADDRESS="core-live-c.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" +NAME="Boötes" +PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" +ADDRESS="bootes.publicnode.org:11625" +HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_singapore" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" -ADDRESS="stellar-sg-sin.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" +NAME="Lyra by BP Ventures" +PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" +ADDRESS="lyra.publicnode.org:11625" +HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_iowa" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" -ADDRESS="stellar-us-iowa.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" +NAME="Hercules by OG Technologies" +PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" +ADDRESS="hercules.publicnode.org:11625" +HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_frankfurt" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" -ADDRESS="stellar-de-fra.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" +NAME="LOBSTR 3 (North America)" +PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" +ADDRESS="v3.stellar.lobstr.co:11625" +HISTORY="curl -sf https://archive.v3.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="lobstr_1_europe" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 1 (Europe)" PUBLIC_KEY="GCFONE23AB7Y6C5YZOMKUKGETPIAJA4QOYLS5VNS4JHBGKRZCPYHDLW7" ADDRESS="v1.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-1-lobstr.s3.amazonaws.com/{0} -o {1}" - -[[VALIDATORS]] -NAME="lobstr_2_europe" +HISTORY="curl -sf https://archive.v1.stellar.lobstr.co/{0} -o {1}" HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GDXQB3OMMQ6MGG43PWFBZWBFKBBDUZIVSUDAZZTRAWQZKES2CDSE5HKJ" -ADDRESS="v2.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-2-lobstr.s3.amazonaws.com/{0} -o {1}" [[VALIDATORS]] -NAME="lobstr_3_north_america" +NAME="LOBSTR 2 (Europe)" +PUBLIC_KEY="GCB2VSADESRV2DDTIVTFLBDI562K6KE3KMKILBHUHUWFXCUBHGQDI7VL" +ADDRESS="v2.stellar.lobstr.co:11625" +HISTORY="curl -sf https://archive.v2.stellar.lobstr.co/{0} -o {1}" HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" -ADDRESS="v3.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-3-lobstr.s3.amazonaws.com/{0} -o {1}" [[VALIDATORS]] -NAME="lobstr_4_asia" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 4 (Asia)" PUBLIC_KEY="GA7TEPCBDQKI7JQLQ34ZURRMK44DVYCIGVXQQWNSWAEQR6KB4FMCBT7J" ADDRESS="v4.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-4-lobstr.s3.amazonaws.com/{0} -o {1}" +HISTORY="curl -sf https://archive.v4.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="lobstr_5_australia" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 5 (India)" PUBLIC_KEY="GA5STBMV6QDXFDGD62MEHLLHZTPDI77U3PFOD2SELU5RJDHQWBR5NNK7" ADDRESS="v5.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-5-lobstr.s3.amazonaws.com/{0} -o {1}" +HISTORY="curl -sf https://archive.v5.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="coinqvest_hong_kong" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" -ADDRESS="hongkong.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://hongkong.stellar.coinqvest.com/history/{0} -o {1}" +NAME="FT SCV 2" +PUBLIC_KEY="GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" +ADDRESS="stellar2.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" [[VALIDATORS]] -NAME="coinqvest_germany" -HOME_DOMAIN="www.coinqvest.com" +NAME="FT SCV 3" +PUBLIC_KEY="GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" +ADDRESS="stellar3.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" + +[[VALIDATORS]] +NAME="FT SCV 1" +PUBLIC_KEY="GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" +ADDRESS="stellar1.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" + +[[VALIDATORS]] +NAME="SatoshiPay Frankfurt" +PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" +ADDRESS="stellar-de-fra.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" + +[[VALIDATORS]] +NAME="SatoshiPay Singapore" +PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" +ADDRESS="stellar-sg-sin.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" + +[[VALIDATORS]] +NAME="SatoshiPay Iowa" +PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" +ADDRESS="stellar-us-iowa.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" + +[[VALIDATORS]] +NAME="Whalestack (Germany)" PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" -ADDRESS="germany.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://germany.stellar.coinqvest.com/history/{0} -o {1}" +ADDRESS="germany.stellar.whalestack.com:11625" +HISTORY="curl -sf https://germany.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" + +[[VALIDATORS]] +NAME="Whalestack (Hong Kong)" +PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" +ADDRESS="hongkong.stellar.whalestack.com:11625" +HISTORY="curl -sf https://hongkong.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" [[VALIDATORS]] -NAME="coinqvest_finland" -HOME_DOMAIN="www.coinqvest.com" +NAME="Whalestack (Finland)" PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" -ADDRESS="finland.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://finland.stellar.coinqvest.com/history/{0} -o {1}" +ADDRESS="finland.stellar.whalestack.com:11625" +HISTORY="curl -sf https://finland.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" [[VALIDATORS]] -NAME="bootes" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" -ADDRESS="bootes.publicnode.org" -HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" +NAME="SDF 2" +PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" +ADDRESS="core-live-b.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" [[VALIDATORS]] -NAME="hercules" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" -ADDRESS="hercules.publicnode.org" -HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" +NAME="SDF 1" +PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" +ADDRESS="core-live-a.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" [[VALIDATORS]] -NAME="lyra" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" -ADDRESS="lyra.publicnode.org" -HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" \ No newline at end of file +NAME="SDF 3" +PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" +ADDRESS="core-live-c.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 3" +PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" +ADDRESS="stellar-full-validator3.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 2" +PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" +ADDRESS="stellar-full-validator2.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 1" +PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" +ADDRESS="stellar-full-validator1.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" \ No newline at end of file diff --git a/ingest/ledgerbackend/configs/captive-core-pubnet.cfg b/ingest/ledgerbackend/configs/captive-core-pubnet.cfg index f8b9a33985..6e61aa2603 100644 --- a/ingest/ledgerbackend/configs/captive-core-pubnet.cfg +++ b/ingest/ledgerbackend/configs/captive-core-pubnet.cfg @@ -6,190 +6,190 @@ HTTP_PORT=11626 PEER_PORT=11725 [[HOME_DOMAINS]] -HOME_DOMAIN="stellar.org" +HOME_DOMAIN="publicnode.org" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="satoshipay.io" +HOME_DOMAIN="lobstr.co" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="lobstr.co" +HOME_DOMAIN="www.franklintempleton.com" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="www.coinqvest.com" +HOME_DOMAIN="satoshipay.io" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="publicnode.org" +HOME_DOMAIN="whalestack.com" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="stellar.blockdaemon.com" +HOME_DOMAIN="www.stellar.org" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN = "www.franklintempleton.com" -QUALITY = "HIGH" - -[[VALIDATORS]] -NAME="sdf_1" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" -ADDRESS="core-live-a.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" - -[[VALIDATORS]] -NAME="sdf_2" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" -ADDRESS="core-live-b.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" +QUALITY="HIGH" [[VALIDATORS]] -NAME="sdf_3" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" -ADDRESS="core-live-c.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" +NAME="Boötes" +PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" +ADDRESS="bootes.publicnode.org:11625" +HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_singapore" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" -ADDRESS="stellar-sg-sin.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" +NAME="Lyra by BP Ventures" +PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" +ADDRESS="lyra.publicnode.org:11625" +HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_iowa" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" -ADDRESS="stellar-us-iowa.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" +NAME="Hercules by OG Technologies" +PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" +ADDRESS="hercules.publicnode.org:11625" +HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_frankfurt" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" -ADDRESS="stellar-de-fra.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" +NAME="LOBSTR 3 (North America)" +PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" +ADDRESS="v3.stellar.lobstr.co:11625" +HISTORY="curl -sf https://archive.v3.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="lobstr_1_europe" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 1 (Europe)" PUBLIC_KEY="GCFONE23AB7Y6C5YZOMKUKGETPIAJA4QOYLS5VNS4JHBGKRZCPYHDLW7" ADDRESS="v1.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-1-lobstr.s3.amazonaws.com/{0} -o {1}" - -[[VALIDATORS]] -NAME="lobstr_2_europe" +HISTORY="curl -sf https://archive.v1.stellar.lobstr.co/{0} -o {1}" HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GDXQB3OMMQ6MGG43PWFBZWBFKBBDUZIVSUDAZZTRAWQZKES2CDSE5HKJ" -ADDRESS="v2.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-2-lobstr.s3.amazonaws.com/{0} -o {1}" [[VALIDATORS]] -NAME="lobstr_3_north_america" +NAME="LOBSTR 2 (Europe)" +PUBLIC_KEY="GCB2VSADESRV2DDTIVTFLBDI562K6KE3KMKILBHUHUWFXCUBHGQDI7VL" +ADDRESS="v2.stellar.lobstr.co:11625" +HISTORY="curl -sf https://archive.v2.stellar.lobstr.co/{0} -o {1}" HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" -ADDRESS="v3.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-3-lobstr.s3.amazonaws.com/{0} -o {1}" [[VALIDATORS]] -NAME="lobstr_4_asia" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 4 (Asia)" PUBLIC_KEY="GA7TEPCBDQKI7JQLQ34ZURRMK44DVYCIGVXQQWNSWAEQR6KB4FMCBT7J" ADDRESS="v4.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-4-lobstr.s3.amazonaws.com/{0} -o {1}" +HISTORY="curl -sf https://archive.v4.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="lobstr_5_australia" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 5 (India)" PUBLIC_KEY="GA5STBMV6QDXFDGD62MEHLLHZTPDI77U3PFOD2SELU5RJDHQWBR5NNK7" ADDRESS="v5.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-5-lobstr.s3.amazonaws.com/{0} -o {1}" +HISTORY="curl -sf https://archive.v5.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="coinqvest_hong_kong" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" -ADDRESS="hongkong.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://hongkong.stellar.coinqvest.com/history/{0} -o {1}" +NAME="FT SCV 2" +PUBLIC_KEY="GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" +ADDRESS="stellar2.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" [[VALIDATORS]] -NAME="coinqvest_germany" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" -ADDRESS="germany.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://germany.stellar.coinqvest.com/history/{0} -o {1}" +NAME="FT SCV 3" +PUBLIC_KEY="GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" +ADDRESS="stellar3.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" [[VALIDATORS]] -NAME="coinqvest_finland" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" -ADDRESS="finland.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://finland.stellar.coinqvest.com/history/{0} -o {1}" +NAME="FT SCV 1" +PUBLIC_KEY="GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" +ADDRESS="stellar1.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" [[VALIDATORS]] -NAME="bootes" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" -ADDRESS="bootes.publicnode.org" -HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" +NAME="SatoshiPay Frankfurt" +PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" +ADDRESS="stellar-de-fra.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" [[VALIDATORS]] -NAME="hercules" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" -ADDRESS="hercules.publicnode.org" -HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" +NAME="SatoshiPay Singapore" +PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" +ADDRESS="stellar-sg-sin.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" [[VALIDATORS]] -NAME="lyra" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" -ADDRESS="lyra.publicnode.org" -HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" +NAME="SatoshiPay Iowa" +PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" +ADDRESS="stellar-us-iowa.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" [[VALIDATORS]] -NAME="Blockdaemon_Validator_1" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" -ADDRESS="stellar-full-validator1.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" +NAME="Whalestack (Germany)" +PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" +ADDRESS="germany.stellar.whalestack.com:11625" +HISTORY="curl -sf https://germany.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" [[VALIDATORS]] -NAME="Blockdaemon_Validator_2" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" -ADDRESS="stellar-full-validator2.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" +NAME="Whalestack (Hong Kong)" +PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" +ADDRESS="hongkong.stellar.whalestack.com:11625" +HISTORY="curl -sf https://hongkong.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" [[VALIDATORS]] -NAME="Blockdaemon_Validator_3" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" -ADDRESS="stellar-full-validator3.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" +NAME="Whalestack (Finland)" +PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" +ADDRESS="finland.stellar.whalestack.com:11625" +HISTORY="curl -sf https://finland.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" [[VALIDATORS]] -NAME = "FT_SCV_1" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" -ADDRESS = "stellar1.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" +NAME="SDF 2" +PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" +ADDRESS="core-live-b.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" [[VALIDATORS]] -NAME = "FT_SCV_2" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" -ADDRESS = "stellar2.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" +NAME="SDF 1" +PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" +ADDRESS="core-live-a.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" [[VALIDATORS]] -NAME = "FT_SCV_3" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" -ADDRESS = "stellar3.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" +NAME="SDF 3" +PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" +ADDRESS="core-live-c.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 3" +PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" +ADDRESS="stellar-full-validator3.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 2" +PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" +ADDRESS="stellar-full-validator2.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 1" +PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" +ADDRESS="stellar-full-validator1.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" \ No newline at end of file diff --git a/services/horizon/docker/stellar-core-pubnet.cfg b/services/horizon/docker/stellar-core-pubnet.cfg index b851fbbf33..94b29c4b4e 100644 --- a/services/horizon/docker/stellar-core-pubnet.cfg +++ b/services/horizon/docker/stellar-core-pubnet.cfg @@ -13,190 +13,190 @@ CATCHUP_RECENT=100 get="cp /opt/stellar/history-cache/{0} {1}" [[HOME_DOMAINS]] -HOME_DOMAIN="stellar.org" +HOME_DOMAIN="publicnode.org" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="satoshipay.io" +HOME_DOMAIN="lobstr.co" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="lobstr.co" +HOME_DOMAIN="www.franklintempleton.com" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="www.coinqvest.com" +HOME_DOMAIN="satoshipay.io" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="publicnode.org" +HOME_DOMAIN="whalestack.com" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="stellar.blockdaemon.com" +HOME_DOMAIN="www.stellar.org" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN = "www.franklintempleton.com" -QUALITY = "HIGH" - -[[VALIDATORS]] -NAME="sdf_1" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" -ADDRESS="core-live-a.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" - -[[VALIDATORS]] -NAME="sdf_2" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" -ADDRESS="core-live-b.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" +QUALITY="HIGH" [[VALIDATORS]] -NAME="sdf_3" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" -ADDRESS="core-live-c.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" +NAME="Boötes" +PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" +ADDRESS="bootes.publicnode.org:11625" +HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_singapore" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" -ADDRESS="stellar-sg-sin.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" +NAME="Lyra by BP Ventures" +PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" +ADDRESS="lyra.publicnode.org:11625" +HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_iowa" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" -ADDRESS="stellar-us-iowa.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" +NAME="Hercules by OG Technologies" +PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" +ADDRESS="hercules.publicnode.org:11625" +HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_frankfurt" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" -ADDRESS="stellar-de-fra.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" +NAME="LOBSTR 3 (North America)" +PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" +ADDRESS="v3.stellar.lobstr.co:11625" +HISTORY="curl -sf https://archive.v3.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="lobstr_1_europe" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 1 (Europe)" PUBLIC_KEY="GCFONE23AB7Y6C5YZOMKUKGETPIAJA4QOYLS5VNS4JHBGKRZCPYHDLW7" ADDRESS="v1.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-1-lobstr.s3.amazonaws.com/{0} -o {1}" - -[[VALIDATORS]] -NAME="lobstr_2_europe" +HISTORY="curl -sf https://archive.v1.stellar.lobstr.co/{0} -o {1}" HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GDXQB3OMMQ6MGG43PWFBZWBFKBBDUZIVSUDAZZTRAWQZKES2CDSE5HKJ" -ADDRESS="v2.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-2-lobstr.s3.amazonaws.com/{0} -o {1}" [[VALIDATORS]] -NAME="lobstr_3_north_america" +NAME="LOBSTR 2 (Europe)" +PUBLIC_KEY="GCB2VSADESRV2DDTIVTFLBDI562K6KE3KMKILBHUHUWFXCUBHGQDI7VL" +ADDRESS="v2.stellar.lobstr.co:11625" +HISTORY="curl -sf https://archive.v2.stellar.lobstr.co/{0} -o {1}" HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" -ADDRESS="v3.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-3-lobstr.s3.amazonaws.com/{0} -o {1}" [[VALIDATORS]] -NAME="lobstr_4_asia" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 4 (Asia)" PUBLIC_KEY="GA7TEPCBDQKI7JQLQ34ZURRMK44DVYCIGVXQQWNSWAEQR6KB4FMCBT7J" ADDRESS="v4.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-4-lobstr.s3.amazonaws.com/{0} -o {1}" +HISTORY="curl -sf https://archive.v4.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="lobstr_5_australia" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 5 (India)" PUBLIC_KEY="GA5STBMV6QDXFDGD62MEHLLHZTPDI77U3PFOD2SELU5RJDHQWBR5NNK7" ADDRESS="v5.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-5-lobstr.s3.amazonaws.com/{0} -o {1}" +HISTORY="curl -sf https://archive.v5.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="coinqvest_hong_kong" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" -ADDRESS="hongkong.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://hongkong.stellar.coinqvest.com/history/{0} -o {1}" +NAME="FT SCV 2" +PUBLIC_KEY="GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" +ADDRESS="stellar2.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" [[VALIDATORS]] -NAME="coinqvest_germany" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" -ADDRESS="germany.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://germany.stellar.coinqvest.com/history/{0} -o {1}" +NAME="FT SCV 3" +PUBLIC_KEY="GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" +ADDRESS="stellar3.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" [[VALIDATORS]] -NAME="coinqvest_finland" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" -ADDRESS="finland.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://finland.stellar.coinqvest.com/history/{0} -o {1}" +NAME="FT SCV 1" +PUBLIC_KEY="GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" +ADDRESS="stellar1.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" [[VALIDATORS]] -NAME="bootes" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" -ADDRESS="bootes.publicnode.org" -HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" +NAME="SatoshiPay Frankfurt" +PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" +ADDRESS="stellar-de-fra.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" [[VALIDATORS]] -NAME="hercules" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" -ADDRESS="hercules.publicnode.org" -HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" +NAME="SatoshiPay Singapore" +PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" +ADDRESS="stellar-sg-sin.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" [[VALIDATORS]] -NAME="lyra" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" -ADDRESS="lyra.publicnode.org" -HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" +NAME="SatoshiPay Iowa" +PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" +ADDRESS="stellar-us-iowa.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" [[VALIDATORS]] -NAME="Blockdaemon_Validator_1" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" -ADDRESS="stellar-full-validator1.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" +NAME="Whalestack (Germany)" +PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" +ADDRESS="germany.stellar.whalestack.com:11625" +HISTORY="curl -sf https://germany.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" [[VALIDATORS]] -NAME="Blockdaemon_Validator_2" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" -ADDRESS="stellar-full-validator2.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" +NAME="Whalestack (Hong Kong)" +PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" +ADDRESS="hongkong.stellar.whalestack.com:11625" +HISTORY="curl -sf https://hongkong.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" [[VALIDATORS]] -NAME="Blockdaemon_Validator_3" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" -ADDRESS="stellar-full-validator3.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" +NAME="Whalestack (Finland)" +PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" +ADDRESS="finland.stellar.whalestack.com:11625" +HISTORY="curl -sf https://finland.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" [[VALIDATORS]] -NAME = "FT_SCV_1" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" -ADDRESS = "stellar1.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" +NAME="SDF 2" +PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" +ADDRESS="core-live-b.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" [[VALIDATORS]] -NAME = "FT_SCV_2" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" -ADDRESS = "stellar2.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" +NAME="SDF 1" +PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" +ADDRESS="core-live-a.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" [[VALIDATORS]] -NAME = "FT_SCV_3" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" -ADDRESS = "stellar3.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" \ No newline at end of file +NAME="SDF 3" +PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" +ADDRESS="core-live-c.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 3" +PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" +ADDRESS="stellar-full-validator3.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 2" +PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" +ADDRESS="stellar-full-validator2.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 1" +PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" +ADDRESS="stellar-full-validator1.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" \ No newline at end of file diff --git a/services/horizon/internal/configs/captive-core-pubnet.cfg b/services/horizon/internal/configs/captive-core-pubnet.cfg index f8b9a33985..6e61aa2603 100644 --- a/services/horizon/internal/configs/captive-core-pubnet.cfg +++ b/services/horizon/internal/configs/captive-core-pubnet.cfg @@ -6,190 +6,190 @@ HTTP_PORT=11626 PEER_PORT=11725 [[HOME_DOMAINS]] -HOME_DOMAIN="stellar.org" +HOME_DOMAIN="publicnode.org" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="satoshipay.io" +HOME_DOMAIN="lobstr.co" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="lobstr.co" +HOME_DOMAIN="www.franklintempleton.com" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="www.coinqvest.com" +HOME_DOMAIN="satoshipay.io" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="publicnode.org" +HOME_DOMAIN="whalestack.com" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN="stellar.blockdaemon.com" +HOME_DOMAIN="www.stellar.org" QUALITY="HIGH" [[HOME_DOMAINS]] -HOME_DOMAIN = "www.franklintempleton.com" -QUALITY = "HIGH" - -[[VALIDATORS]] -NAME="sdf_1" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" -ADDRESS="core-live-a.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" - -[[VALIDATORS]] -NAME="sdf_2" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" -ADDRESS="core-live-b.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" +QUALITY="HIGH" [[VALIDATORS]] -NAME="sdf_3" -HOME_DOMAIN="stellar.org" -PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" -ADDRESS="core-live-c.stellar.org:11625" -HISTORY="curl -sf https://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" +NAME="Boötes" +PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" +ADDRESS="bootes.publicnode.org:11625" +HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_singapore" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" -ADDRESS="stellar-sg-sin.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" +NAME="Lyra by BP Ventures" +PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" +ADDRESS="lyra.publicnode.org:11625" +HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_iowa" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" -ADDRESS="stellar-us-iowa.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" +NAME="Hercules by OG Technologies" +PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" +ADDRESS="hercules.publicnode.org:11625" +HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" +HOME_DOMAIN="publicnode.org" [[VALIDATORS]] -NAME="satoshipay_frankfurt" -HOME_DOMAIN="satoshipay.io" -PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" -ADDRESS="stellar-de-fra.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" +NAME="LOBSTR 3 (North America)" +PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" +ADDRESS="v3.stellar.lobstr.co:11625" +HISTORY="curl -sf https://archive.v3.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="lobstr_1_europe" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 1 (Europe)" PUBLIC_KEY="GCFONE23AB7Y6C5YZOMKUKGETPIAJA4QOYLS5VNS4JHBGKRZCPYHDLW7" ADDRESS="v1.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-1-lobstr.s3.amazonaws.com/{0} -o {1}" - -[[VALIDATORS]] -NAME="lobstr_2_europe" +HISTORY="curl -sf https://archive.v1.stellar.lobstr.co/{0} -o {1}" HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GDXQB3OMMQ6MGG43PWFBZWBFKBBDUZIVSUDAZZTRAWQZKES2CDSE5HKJ" -ADDRESS="v2.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-2-lobstr.s3.amazonaws.com/{0} -o {1}" [[VALIDATORS]] -NAME="lobstr_3_north_america" +NAME="LOBSTR 2 (Europe)" +PUBLIC_KEY="GCB2VSADESRV2DDTIVTFLBDI562K6KE3KMKILBHUHUWFXCUBHGQDI7VL" +ADDRESS="v2.stellar.lobstr.co:11625" +HISTORY="curl -sf https://archive.v2.stellar.lobstr.co/{0} -o {1}" HOME_DOMAIN="lobstr.co" -PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" -ADDRESS="v3.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-3-lobstr.s3.amazonaws.com/{0} -o {1}" [[VALIDATORS]] -NAME="lobstr_4_asia" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 4 (Asia)" PUBLIC_KEY="GA7TEPCBDQKI7JQLQ34ZURRMK44DVYCIGVXQQWNSWAEQR6KB4FMCBT7J" ADDRESS="v4.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-4-lobstr.s3.amazonaws.com/{0} -o {1}" +HISTORY="curl -sf https://archive.v4.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="lobstr_5_australia" -HOME_DOMAIN="lobstr.co" +NAME="LOBSTR 5 (India)" PUBLIC_KEY="GA5STBMV6QDXFDGD62MEHLLHZTPDI77U3PFOD2SELU5RJDHQWBR5NNK7" ADDRESS="v5.stellar.lobstr.co:11625" -HISTORY="curl -sf https://stellar-archive-5-lobstr.s3.amazonaws.com/{0} -o {1}" +HISTORY="curl -sf https://archive.v5.stellar.lobstr.co/{0} -o {1}" +HOME_DOMAIN="lobstr.co" [[VALIDATORS]] -NAME="coinqvest_hong_kong" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" -ADDRESS="hongkong.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://hongkong.stellar.coinqvest.com/history/{0} -o {1}" +NAME="FT SCV 2" +PUBLIC_KEY="GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" +ADDRESS="stellar2.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" [[VALIDATORS]] -NAME="coinqvest_germany" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" -ADDRESS="germany.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://germany.stellar.coinqvest.com/history/{0} -o {1}" +NAME="FT SCV 3" +PUBLIC_KEY="GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" +ADDRESS="stellar3.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" [[VALIDATORS]] -NAME="coinqvest_finland" -HOME_DOMAIN="www.coinqvest.com" -PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" -ADDRESS="finland.stellar.coinqvest.com:11625" -HISTORY="curl -sf https://finland.stellar.coinqvest.com/history/{0} -o {1}" +NAME="FT SCV 1" +PUBLIC_KEY="GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" +ADDRESS="stellar1.franklintempleton.com:11625" +HISTORY="curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" +HOME_DOMAIN="www.franklintempleton.com" [[VALIDATORS]] -NAME="bootes" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" -ADDRESS="bootes.publicnode.org" -HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" +NAME="SatoshiPay Frankfurt" +PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" +ADDRESS="stellar-de-fra.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" [[VALIDATORS]] -NAME="hercules" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" -ADDRESS="hercules.publicnode.org" -HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" +NAME="SatoshiPay Singapore" +PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" +ADDRESS="stellar-sg-sin.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" [[VALIDATORS]] -NAME="lyra" -HOME_DOMAIN="publicnode.org" -PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" -ADDRESS="lyra.publicnode.org" -HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" +NAME="SatoshiPay Iowa" +PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" +ADDRESS="stellar-us-iowa.satoshipay.io:11625" +HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" +HOME_DOMAIN="satoshipay.io" [[VALIDATORS]] -NAME="Blockdaemon_Validator_1" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" -ADDRESS="stellar-full-validator1.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" +NAME="Whalestack (Germany)" +PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" +ADDRESS="germany.stellar.whalestack.com:11625" +HISTORY="curl -sf https://germany.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" [[VALIDATORS]] -NAME="Blockdaemon_Validator_2" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" -ADDRESS="stellar-full-validator2.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" +NAME="Whalestack (Hong Kong)" +PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" +ADDRESS="hongkong.stellar.whalestack.com:11625" +HISTORY="curl -sf https://hongkong.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" [[VALIDATORS]] -NAME="Blockdaemon_Validator_3" -HOME_DOMAIN="stellar.blockdaemon.com" -PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" -ADDRESS="stellar-full-validator3.bdnodes.net" -HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" +NAME="Whalestack (Finland)" +PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" +ADDRESS="finland.stellar.whalestack.com:11625" +HISTORY="curl -sf https://finland.stellar.whalestack.com/history/{0} -o {1}" +HOME_DOMAIN="whalestack.com" [[VALIDATORS]] -NAME = "FT_SCV_1" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" -ADDRESS = "stellar1.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" +NAME="SDF 2" +PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" +ADDRESS="core-live-b.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" [[VALIDATORS]] -NAME = "FT_SCV_2" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" -ADDRESS = "stellar2.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" +NAME="SDF 1" +PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" +ADDRESS="core-live-a.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" [[VALIDATORS]] -NAME = "FT_SCV_3" -HOME_DOMAIN = "www.franklintempleton.com" -PUBLIC_KEY = "GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" -ADDRESS = "stellar3.franklintempleton.com:11625" -HISTORY = "curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" +NAME="SDF 3" +PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" +ADDRESS="core-live-c.stellar.org:11625" +HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" +HOME_DOMAIN="www.stellar.org" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 3" +PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" +ADDRESS="stellar-full-validator3.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 2" +PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" +ADDRESS="stellar-full-validator2.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" + +[[VALIDATORS]] +NAME="Blockdaemon Validator 1" +PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" +ADDRESS="stellar-full-validator1.bdnodes.net:11625" +HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" +HOME_DOMAIN="stellar.blockdaemon.com" \ No newline at end of file From 927ab6b5337e00bd197fd0f98a0072b38bbe0847 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Tue, 16 Apr 2024 17:11:04 +0200 Subject: [PATCH 098/234] Bump XDR for protocol 21 (#5275) --- .github/workflows/horizon.yml | 12 +- Makefile | 2 +- gxdr/xdr_generated.go | 651 ++++++++-- services/horizon/internal/ingest/main.go | 2 +- .../integration/extend_footprint_ttl_test.go | 1 - .../integration/invokehostfunction_test.go | 4 - .../horizon/internal/integration/sac_test.go | 10 - .../internal/integration/transaction_test.go | 2 - .../internal/integration/txsub_test.go | 1 - xdr/Stellar-contract-config-setting.x | 55 +- xdr/Stellar-ledger-entries.x | 26 +- xdr/Stellar-ledger.x | 62 +- xdr/xdr_commit_generated.txt | 2 +- xdr/xdr_generated.go | 1078 ++++++++++++++++- 14 files changed, 1754 insertions(+), 154 deletions(-) diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index 74c3f47725..c1618d21c8 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -13,7 +13,7 @@ jobs: os: [ubuntu-20.04, ubuntu-22.04] go: ["1.21", "1.22"] pg: [12, 16] - protocol-version: [20] + protocol-version: [20, 21] runs-on: ${{ matrix.os }} services: postgres: @@ -32,11 +32,13 @@ jobs: env: HORIZON_INTEGRATION_TESTS_ENABLED: true HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL: ${{ matrix.protocol-version }} - PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 20.2.0-1716.rc3.34d82fc00.focal - PROTOCOL_20_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:20.2.0-1716.rc3.34d82fc00.focal + PROTOCOL_21_CORE_DEBIAN_PKG_VERSION: 20.4.1-1807.b152dc51d.focal + PROTOCOL_21_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:20.4.1-1807.b152dc51d.focal + PROTOCOL_21_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:21.0.0-rc1-68 + PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 20.4.1-1807.b152dc51d.focal + PROTOCOL_20_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:20.4.1-1807.b152dc51d.focal + # TODO: bump soroban-rpc to the p21 version once it supports it PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:20.3.0-rc1-52 - PROTOCOL_19_CORE_DEBIAN_PKG_VERSION: 19.14.0-1500.5664eff4e.focal - PROTOCOL_19_CORE_DOCKER_IMG: stellar/stellar-core:19.14.0-1500.5664eff4e.focal PGHOST: localhost PGPORT: 5432 PGUSER: postgres diff --git a/Makefile b/Makefile index 291cc282fc..8ac1abb337 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ XDRS = $(DOWNLOADABLE_XDRS) xdr/Stellar-lighthorizon.x \ XDRGEN_COMMIT=e2cac557162d99b12ae73b846cf3d5bfe16636de -XDR_COMMIT=b96148cd4acc372cc9af17b909ffe4b12c43ecb6 +XDR_COMMIT=59062438237d5f77fd6feb060b76288e88b7e222 .PHONY: xdr xdr-clean xdr-update diff --git a/gxdr/xdr_generated.go b/gxdr/xdr_generated.go index d4cd83cdf0..e7c8f3ed58 100644 --- a/gxdr/xdr_generated.go +++ b/gxdr/xdr_generated.go @@ -639,11 +639,38 @@ type ContractDataEntry struct { Val SCVal } +type ContractCodeCostInputs struct { + Ext ExtensionPoint + NInstructions Uint32 + NFunctions Uint32 + NGlobals Uint32 + NTableEntries Uint32 + NTypes Uint32 + NDataSegments Uint32 + NElemSegments Uint32 + NImports Uint32 + NExports Uint32 + NDataSegmentBytes Uint32 +} + type ContractCodeEntry struct { - Ext ExtensionPoint + Ext XdrAnon_ContractCodeEntry_Ext Hash Hash Code []byte } +type XdrAnon_ContractCodeEntry_Ext struct { + // The union discriminant V selects among the following arms: + // 0: + // void + // 1: + // V1() *XdrAnon_ContractCodeEntry_Ext_V1 + V int32 + _u interface{} +} +type XdrAnon_ContractCodeEntry_Ext_V1 struct { + Ext ExtensionPoint + CostInputs ContractCodeCostInputs +} type TTLEntry struct { // Hash of the LedgerKey that is associated with this TTLEntry @@ -1197,8 +1224,39 @@ type DiagnosticEvent struct { Event ContractEvent } -type SorobanTransactionMeta struct { +type SorobanTransactionMetaExtV1 struct { Ext ExtensionPoint + // Total amount (in stroops) that has been charged for non-refundable + // Soroban resources. + // Non-refundable resources are charged based on the usage declared in + // the transaction envelope (such as `instructions`, `readBytes` etc.) and + // is charged regardless of the success of the transaction. + TotalNonRefundableResourceFeeCharged Int64 + // Total amount (in stroops) that has been charged for refundable + // Soroban resource fees. + // Currently this comprises the rent fee (`rentFeeCharged`) and the + // fee for the events and return value. + // Refundable resources are charged based on the actual resources usage. + // Since currently refundable resources are only used for the successful + // transactions, this will be `0` for failed transactions. + TotalRefundableResourceFeeCharged Int64 + // Amount (in stroops) that has been charged for rent. + // This is a part of `totalNonRefundableResourceFeeCharged`. + RentFeeCharged Int64 +} + +type SorobanTransactionMetaExt struct { + // The union discriminant V selects among the following arms: + // 0: + // void + // 1: + // V1() *SorobanTransactionMetaExtV1 + V int32 + _u interface{} +} + +type SorobanTransactionMeta struct { + Ext SorobanTransactionMetaExt // custom events populated by the Events []ContractEvent // contracts themselves. @@ -1273,10 +1331,23 @@ type LedgerCloseMetaV0 struct { ScpInfo []SCPHistoryEntry } +type LedgerCloseMetaExtV1 struct { + Ext ExtensionPoint + SorobanFeeWrite1KB Int64 +} + +type LedgerCloseMetaExt struct { + // The union discriminant V selects among the following arms: + // 0: + // void + // 1: + // V1() *LedgerCloseMetaExtV1 + V int32 + _u interface{} +} + type LedgerCloseMetaV1 struct { - // We forgot to add an ExtensionPoint in v0 but at least - // we can add one now in v1. - Ext ExtensionPoint + Ext LedgerCloseMetaExt LedgerHeader LedgerHeaderHistoryEntry TxSet GeneralizedTransactionSet // NB: transactions are sorted in apply order here @@ -4305,8 +4376,9 @@ const ( InvokeVmFunction ContractCostType = 13 // Cost of computing a keccak256 hash from bytes. ComputeKeccak256Hash ContractCostType = 14 - // Cost of computing an ECDSA secp256k1 signature from bytes. - ComputeEcdsaSecp256k1Sig ContractCostType = 15 + // Cost of decoding an ECDSA signature computed from a 256-bit prime modulus + // curve (e.g. secp256k1 and secp256r1) + DecodeEcdsaCurve256Sig ContractCostType = 15 // Cost of recovering an ECDSA secp256k1 key from a signature. RecoverEcdsaSecp256k1Key ContractCostType = 16 // Cost of int256 addition (`+`) and subtraction (`-`) operations @@ -4321,6 +4393,51 @@ const ( Int256Shift ContractCostType = 21 // Cost of drawing random bytes using a ChaCha20 PRNG ChaCha20DrawBytes ContractCostType = 22 + // Cost of parsing wasm bytes that only encode instructions. + ParseWasmInstructions ContractCostType = 23 + // Cost of parsing a known number of wasm functions. + ParseWasmFunctions ContractCostType = 24 + // Cost of parsing a known number of wasm globals. + ParseWasmGlobals ContractCostType = 25 + // Cost of parsing a known number of wasm table entries. + ParseWasmTableEntries ContractCostType = 26 + // Cost of parsing a known number of wasm types. + ParseWasmTypes ContractCostType = 27 + // Cost of parsing a known number of wasm data segments. + ParseWasmDataSegments ContractCostType = 28 + // Cost of parsing a known number of wasm element segments. + ParseWasmElemSegments ContractCostType = 29 + // Cost of parsing a known number of wasm imports. + ParseWasmImports ContractCostType = 30 + // Cost of parsing a known number of wasm exports. + ParseWasmExports ContractCostType = 31 + // Cost of parsing a known number of data segment bytes. + ParseWasmDataSegmentBytes ContractCostType = 32 + // Cost of instantiating wasm bytes that only encode instructions. + InstantiateWasmInstructions ContractCostType = 33 + // Cost of instantiating a known number of wasm functions. + InstantiateWasmFunctions ContractCostType = 34 + // Cost of instantiating a known number of wasm globals. + InstantiateWasmGlobals ContractCostType = 35 + // Cost of instantiating a known number of wasm table entries. + InstantiateWasmTableEntries ContractCostType = 36 + // Cost of instantiating a known number of wasm types. + InstantiateWasmTypes ContractCostType = 37 + // Cost of instantiating a known number of wasm data segments. + InstantiateWasmDataSegments ContractCostType = 38 + // Cost of instantiating a known number of wasm element segments. + InstantiateWasmElemSegments ContractCostType = 39 + // Cost of instantiating a known number of wasm imports. + InstantiateWasmImports ContractCostType = 40 + // Cost of instantiating a known number of wasm exports. + InstantiateWasmExports ContractCostType = 41 + // Cost of instantiating a known number of data segment bytes. + InstantiateWasmDataSegmentBytes ContractCostType = 42 + // Cost of decoding a bytes array representing an uncompressed SEC-1 encoded + // point on a 256-bit elliptic curve + Sec1DecodePointUncompressed ContractCostType = 43 + // Cost of verifying an ECDSA Secp256r1 signature + VerifyEcdsaSecp256r1Sig ContractCostType = 44 ) type ContractCostParamEntry struct { @@ -7966,6 +8083,128 @@ func (v *ContractDataEntry) XdrRecurse(x XDR, name string) { } func XDR_ContractDataEntry(v *ContractDataEntry) *ContractDataEntry { return v } +type XdrType_ContractCodeCostInputs = *ContractCodeCostInputs + +func (v *ContractCodeCostInputs) XdrPointer() interface{} { return v } +func (ContractCodeCostInputs) XdrTypeName() string { return "ContractCodeCostInputs" } +func (v ContractCodeCostInputs) XdrValue() interface{} { return v } +func (v *ContractCodeCostInputs) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *ContractCodeCostInputs) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%sext", name), XDR_ExtensionPoint(&v.Ext)) + x.Marshal(x.Sprintf("%snInstructions", name), XDR_Uint32(&v.NInstructions)) + x.Marshal(x.Sprintf("%snFunctions", name), XDR_Uint32(&v.NFunctions)) + x.Marshal(x.Sprintf("%snGlobals", name), XDR_Uint32(&v.NGlobals)) + x.Marshal(x.Sprintf("%snTableEntries", name), XDR_Uint32(&v.NTableEntries)) + x.Marshal(x.Sprintf("%snTypes", name), XDR_Uint32(&v.NTypes)) + x.Marshal(x.Sprintf("%snDataSegments", name), XDR_Uint32(&v.NDataSegments)) + x.Marshal(x.Sprintf("%snElemSegments", name), XDR_Uint32(&v.NElemSegments)) + x.Marshal(x.Sprintf("%snImports", name), XDR_Uint32(&v.NImports)) + x.Marshal(x.Sprintf("%snExports", name), XDR_Uint32(&v.NExports)) + x.Marshal(x.Sprintf("%snDataSegmentBytes", name), XDR_Uint32(&v.NDataSegmentBytes)) +} +func XDR_ContractCodeCostInputs(v *ContractCodeCostInputs) *ContractCodeCostInputs { return v } + +type XdrType_XdrAnon_ContractCodeEntry_Ext_V1 = *XdrAnon_ContractCodeEntry_Ext_V1 + +func (v *XdrAnon_ContractCodeEntry_Ext_V1) XdrPointer() interface{} { return v } +func (XdrAnon_ContractCodeEntry_Ext_V1) XdrTypeName() string { + return "XdrAnon_ContractCodeEntry_Ext_V1" +} +func (v XdrAnon_ContractCodeEntry_Ext_V1) XdrValue() interface{} { return v } +func (v *XdrAnon_ContractCodeEntry_Ext_V1) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *XdrAnon_ContractCodeEntry_Ext_V1) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%sext", name), XDR_ExtensionPoint(&v.Ext)) + x.Marshal(x.Sprintf("%scostInputs", name), XDR_ContractCodeCostInputs(&v.CostInputs)) +} +func XDR_XdrAnon_ContractCodeEntry_Ext_V1(v *XdrAnon_ContractCodeEntry_Ext_V1) *XdrAnon_ContractCodeEntry_Ext_V1 { + return v +} + +var _XdrTags_XdrAnon_ContractCodeEntry_Ext = map[int32]bool{ + XdrToI32(0): true, + XdrToI32(1): true, +} + +func (_ XdrAnon_ContractCodeEntry_Ext) XdrValidTags() map[int32]bool { + return _XdrTags_XdrAnon_ContractCodeEntry_Ext +} +func (u *XdrAnon_ContractCodeEntry_Ext) V1() *XdrAnon_ContractCodeEntry_Ext_V1 { + switch u.V { + case 1: + if v, ok := u._u.(*XdrAnon_ContractCodeEntry_Ext_V1); ok { + return v + } else { + var zero XdrAnon_ContractCodeEntry_Ext_V1 + u._u = &zero + return &zero + } + default: + XdrPanic("XdrAnon_ContractCodeEntry_Ext.V1 accessed when V == %v", u.V) + return nil + } +} +func (u XdrAnon_ContractCodeEntry_Ext) XdrValid() bool { + switch u.V { + case 0, 1: + return true + } + return false +} +func (u *XdrAnon_ContractCodeEntry_Ext) XdrUnionTag() XdrNum32 { + return XDR_int32(&u.V) +} +func (u *XdrAnon_ContractCodeEntry_Ext) XdrUnionTagName() string { + return "V" +} +func (u *XdrAnon_ContractCodeEntry_Ext) XdrUnionBody() XdrType { + switch u.V { + case 0: + return nil + case 1: + return XDR_XdrAnon_ContractCodeEntry_Ext_V1(u.V1()) + } + return nil +} +func (u *XdrAnon_ContractCodeEntry_Ext) XdrUnionBodyName() string { + switch u.V { + case 0: + return "" + case 1: + return "V1" + } + return "" +} + +type XdrType_XdrAnon_ContractCodeEntry_Ext = *XdrAnon_ContractCodeEntry_Ext + +func (v *XdrAnon_ContractCodeEntry_Ext) XdrPointer() interface{} { return v } +func (XdrAnon_ContractCodeEntry_Ext) XdrTypeName() string { return "XdrAnon_ContractCodeEntry_Ext" } +func (v XdrAnon_ContractCodeEntry_Ext) XdrValue() interface{} { return v } +func (v *XdrAnon_ContractCodeEntry_Ext) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (u *XdrAnon_ContractCodeEntry_Ext) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + XDR_int32(&u.V).XdrMarshal(x, x.Sprintf("%sv", name)) + switch u.V { + case 0: + return + case 1: + x.Marshal(x.Sprintf("%sv1", name), XDR_XdrAnon_ContractCodeEntry_Ext_V1(u.V1())) + return + } + XdrPanic("invalid V (%v) in XdrAnon_ContractCodeEntry_Ext", u.V) +} +func XDR_XdrAnon_ContractCodeEntry_Ext(v *XdrAnon_ContractCodeEntry_Ext) *XdrAnon_ContractCodeEntry_Ext { + return v +} + type XdrType_ContractCodeEntry = *ContractCodeEntry func (v *ContractCodeEntry) XdrPointer() interface{} { return v } @@ -7976,7 +8215,7 @@ func (v *ContractCodeEntry) XdrRecurse(x XDR, name string) { if name != "" { name = x.Sprintf("%s.", name) } - x.Marshal(x.Sprintf("%sext", name), XDR_ExtensionPoint(&v.Ext)) + x.Marshal(x.Sprintf("%sext", name), XDR_XdrAnon_ContractCodeEntry_Ext(&v.Ext)) x.Marshal(x.Sprintf("%shash", name), XDR_Hash(&v.Hash)) x.Marshal(x.Sprintf("%scode", name), XdrVecOpaque{&v.Code, 0xffffffff}) } @@ -11779,6 +12018,102 @@ func (v *DiagnosticEvent) XdrRecurse(x XDR, name string) { } func XDR_DiagnosticEvent(v *DiagnosticEvent) *DiagnosticEvent { return v } +type XdrType_SorobanTransactionMetaExtV1 = *SorobanTransactionMetaExtV1 + +func (v *SorobanTransactionMetaExtV1) XdrPointer() interface{} { return v } +func (SorobanTransactionMetaExtV1) XdrTypeName() string { return "SorobanTransactionMetaExtV1" } +func (v SorobanTransactionMetaExtV1) XdrValue() interface{} { return v } +func (v *SorobanTransactionMetaExtV1) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *SorobanTransactionMetaExtV1) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%sext", name), XDR_ExtensionPoint(&v.Ext)) + x.Marshal(x.Sprintf("%stotalNonRefundableResourceFeeCharged", name), XDR_Int64(&v.TotalNonRefundableResourceFeeCharged)) + x.Marshal(x.Sprintf("%stotalRefundableResourceFeeCharged", name), XDR_Int64(&v.TotalRefundableResourceFeeCharged)) + x.Marshal(x.Sprintf("%srentFeeCharged", name), XDR_Int64(&v.RentFeeCharged)) +} +func XDR_SorobanTransactionMetaExtV1(v *SorobanTransactionMetaExtV1) *SorobanTransactionMetaExtV1 { + return v +} + +var _XdrTags_SorobanTransactionMetaExt = map[int32]bool{ + XdrToI32(0): true, + XdrToI32(1): true, +} + +func (_ SorobanTransactionMetaExt) XdrValidTags() map[int32]bool { + return _XdrTags_SorobanTransactionMetaExt +} +func (u *SorobanTransactionMetaExt) V1() *SorobanTransactionMetaExtV1 { + switch u.V { + case 1: + if v, ok := u._u.(*SorobanTransactionMetaExtV1); ok { + return v + } else { + var zero SorobanTransactionMetaExtV1 + u._u = &zero + return &zero + } + default: + XdrPanic("SorobanTransactionMetaExt.V1 accessed when V == %v", u.V) + return nil + } +} +func (u SorobanTransactionMetaExt) XdrValid() bool { + switch u.V { + case 0, 1: + return true + } + return false +} +func (u *SorobanTransactionMetaExt) XdrUnionTag() XdrNum32 { + return XDR_int32(&u.V) +} +func (u *SorobanTransactionMetaExt) XdrUnionTagName() string { + return "V" +} +func (u *SorobanTransactionMetaExt) XdrUnionBody() XdrType { + switch u.V { + case 0: + return nil + case 1: + return XDR_SorobanTransactionMetaExtV1(u.V1()) + } + return nil +} +func (u *SorobanTransactionMetaExt) XdrUnionBodyName() string { + switch u.V { + case 0: + return "" + case 1: + return "V1" + } + return "" +} + +type XdrType_SorobanTransactionMetaExt = *SorobanTransactionMetaExt + +func (v *SorobanTransactionMetaExt) XdrPointer() interface{} { return v } +func (SorobanTransactionMetaExt) XdrTypeName() string { return "SorobanTransactionMetaExt" } +func (v SorobanTransactionMetaExt) XdrValue() interface{} { return v } +func (v *SorobanTransactionMetaExt) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (u *SorobanTransactionMetaExt) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + XDR_int32(&u.V).XdrMarshal(x, x.Sprintf("%sv", name)) + switch u.V { + case 0: + return + case 1: + x.Marshal(x.Sprintf("%sv1", name), XDR_SorobanTransactionMetaExtV1(u.V1())) + return + } + XdrPanic("invalid V (%v) in SorobanTransactionMetaExt", u.V) +} +func XDR_SorobanTransactionMetaExt(v *SorobanTransactionMetaExt) *SorobanTransactionMetaExt { return v } + type _XdrVec_unbounded_ContractEvent []ContractEvent func (_XdrVec_unbounded_ContractEvent) XdrBound() uint32 { @@ -11903,7 +12238,7 @@ func (v *SorobanTransactionMeta) XdrRecurse(x XDR, name string) { if name != "" { name = x.Sprintf("%s.", name) } - x.Marshal(x.Sprintf("%sext", name), XDR_ExtensionPoint(&v.Ext)) + x.Marshal(x.Sprintf("%sext", name), XDR_SorobanTransactionMetaExt(&v.Ext)) x.Marshal(x.Sprintf("%sevents", name), (*_XdrVec_unbounded_ContractEvent)(&v.Events)) x.Marshal(x.Sprintf("%sreturnValue", name), XDR_SCVal(&v.ReturnValue)) x.Marshal(x.Sprintf("%sdiagnosticEvents", name), (*_XdrVec_unbounded_DiagnosticEvent)(&v.DiagnosticEvents)) @@ -12385,6 +12720,98 @@ func (v *LedgerCloseMetaV0) XdrRecurse(x XDR, name string) { } func XDR_LedgerCloseMetaV0(v *LedgerCloseMetaV0) *LedgerCloseMetaV0 { return v } +type XdrType_LedgerCloseMetaExtV1 = *LedgerCloseMetaExtV1 + +func (v *LedgerCloseMetaExtV1) XdrPointer() interface{} { return v } +func (LedgerCloseMetaExtV1) XdrTypeName() string { return "LedgerCloseMetaExtV1" } +func (v LedgerCloseMetaExtV1) XdrValue() interface{} { return v } +func (v *LedgerCloseMetaExtV1) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *LedgerCloseMetaExtV1) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%sext", name), XDR_ExtensionPoint(&v.Ext)) + x.Marshal(x.Sprintf("%ssorobanFeeWrite1KB", name), XDR_Int64(&v.SorobanFeeWrite1KB)) +} +func XDR_LedgerCloseMetaExtV1(v *LedgerCloseMetaExtV1) *LedgerCloseMetaExtV1 { return v } + +var _XdrTags_LedgerCloseMetaExt = map[int32]bool{ + XdrToI32(0): true, + XdrToI32(1): true, +} + +func (_ LedgerCloseMetaExt) XdrValidTags() map[int32]bool { + return _XdrTags_LedgerCloseMetaExt +} +func (u *LedgerCloseMetaExt) V1() *LedgerCloseMetaExtV1 { + switch u.V { + case 1: + if v, ok := u._u.(*LedgerCloseMetaExtV1); ok { + return v + } else { + var zero LedgerCloseMetaExtV1 + u._u = &zero + return &zero + } + default: + XdrPanic("LedgerCloseMetaExt.V1 accessed when V == %v", u.V) + return nil + } +} +func (u LedgerCloseMetaExt) XdrValid() bool { + switch u.V { + case 0, 1: + return true + } + return false +} +func (u *LedgerCloseMetaExt) XdrUnionTag() XdrNum32 { + return XDR_int32(&u.V) +} +func (u *LedgerCloseMetaExt) XdrUnionTagName() string { + return "V" +} +func (u *LedgerCloseMetaExt) XdrUnionBody() XdrType { + switch u.V { + case 0: + return nil + case 1: + return XDR_LedgerCloseMetaExtV1(u.V1()) + } + return nil +} +func (u *LedgerCloseMetaExt) XdrUnionBodyName() string { + switch u.V { + case 0: + return "" + case 1: + return "V1" + } + return "" +} + +type XdrType_LedgerCloseMetaExt = *LedgerCloseMetaExt + +func (v *LedgerCloseMetaExt) XdrPointer() interface{} { return v } +func (LedgerCloseMetaExt) XdrTypeName() string { return "LedgerCloseMetaExt" } +func (v LedgerCloseMetaExt) XdrValue() interface{} { return v } +func (v *LedgerCloseMetaExt) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (u *LedgerCloseMetaExt) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + XDR_int32(&u.V).XdrMarshal(x, x.Sprintf("%sv", name)) + switch u.V { + case 0: + return + case 1: + x.Marshal(x.Sprintf("%sv1", name), XDR_LedgerCloseMetaExtV1(u.V1())) + return + } + XdrPanic("invalid V (%v) in LedgerCloseMetaExt", u.V) +} +func XDR_LedgerCloseMetaExt(v *LedgerCloseMetaExt) *LedgerCloseMetaExt { return v } + type _XdrVec_unbounded_LedgerKey []LedgerKey func (_XdrVec_unbounded_LedgerKey) XdrBound() uint32 { @@ -12509,7 +12936,7 @@ func (v *LedgerCloseMetaV1) XdrRecurse(x XDR, name string) { if name != "" { name = x.Sprintf("%s.", name) } - x.Marshal(x.Sprintf("%sext", name), XDR_ExtensionPoint(&v.Ext)) + x.Marshal(x.Sprintf("%sext", name), XDR_LedgerCloseMetaExt(&v.Ext)) x.Marshal(x.Sprintf("%sledgerHeader", name), XDR_LedgerHeaderHistoryEntry(&v.LedgerHeader)) x.Marshal(x.Sprintf("%stxSet", name), XDR_GeneralizedTransactionSet(&v.TxSet)) x.Marshal(x.Sprintf("%stxProcessing", name), (*_XdrVec_unbounded_TransactionResultMeta)(&v.TxProcessing)) @@ -28467,54 +28894,98 @@ func XDR_ConfigSettingContractBandwidthV0(v *ConfigSettingContractBandwidthV0) * } var _XdrNames_ContractCostType = map[int32]string{ - int32(WasmInsnExec): "WasmInsnExec", - int32(MemAlloc): "MemAlloc", - int32(MemCpy): "MemCpy", - int32(MemCmp): "MemCmp", - int32(DispatchHostFunction): "DispatchHostFunction", - int32(VisitObject): "VisitObject", - int32(ValSer): "ValSer", - int32(ValDeser): "ValDeser", - int32(ComputeSha256Hash): "ComputeSha256Hash", - int32(ComputeEd25519PubKey): "ComputeEd25519PubKey", - int32(VerifyEd25519Sig): "VerifyEd25519Sig", - int32(VmInstantiation): "VmInstantiation", - int32(VmCachedInstantiation): "VmCachedInstantiation", - int32(InvokeVmFunction): "InvokeVmFunction", - int32(ComputeKeccak256Hash): "ComputeKeccak256Hash", - int32(ComputeEcdsaSecp256k1Sig): "ComputeEcdsaSecp256k1Sig", - int32(RecoverEcdsaSecp256k1Key): "RecoverEcdsaSecp256k1Key", - int32(Int256AddSub): "Int256AddSub", - int32(Int256Mul): "Int256Mul", - int32(Int256Div): "Int256Div", - int32(Int256Pow): "Int256Pow", - int32(Int256Shift): "Int256Shift", - int32(ChaCha20DrawBytes): "ChaCha20DrawBytes", + int32(WasmInsnExec): "WasmInsnExec", + int32(MemAlloc): "MemAlloc", + int32(MemCpy): "MemCpy", + int32(MemCmp): "MemCmp", + int32(DispatchHostFunction): "DispatchHostFunction", + int32(VisitObject): "VisitObject", + int32(ValSer): "ValSer", + int32(ValDeser): "ValDeser", + int32(ComputeSha256Hash): "ComputeSha256Hash", + int32(ComputeEd25519PubKey): "ComputeEd25519PubKey", + int32(VerifyEd25519Sig): "VerifyEd25519Sig", + int32(VmInstantiation): "VmInstantiation", + int32(VmCachedInstantiation): "VmCachedInstantiation", + int32(InvokeVmFunction): "InvokeVmFunction", + int32(ComputeKeccak256Hash): "ComputeKeccak256Hash", + int32(DecodeEcdsaCurve256Sig): "DecodeEcdsaCurve256Sig", + int32(RecoverEcdsaSecp256k1Key): "RecoverEcdsaSecp256k1Key", + int32(Int256AddSub): "Int256AddSub", + int32(Int256Mul): "Int256Mul", + int32(Int256Div): "Int256Div", + int32(Int256Pow): "Int256Pow", + int32(Int256Shift): "Int256Shift", + int32(ChaCha20DrawBytes): "ChaCha20DrawBytes", + int32(ParseWasmInstructions): "ParseWasmInstructions", + int32(ParseWasmFunctions): "ParseWasmFunctions", + int32(ParseWasmGlobals): "ParseWasmGlobals", + int32(ParseWasmTableEntries): "ParseWasmTableEntries", + int32(ParseWasmTypes): "ParseWasmTypes", + int32(ParseWasmDataSegments): "ParseWasmDataSegments", + int32(ParseWasmElemSegments): "ParseWasmElemSegments", + int32(ParseWasmImports): "ParseWasmImports", + int32(ParseWasmExports): "ParseWasmExports", + int32(ParseWasmDataSegmentBytes): "ParseWasmDataSegmentBytes", + int32(InstantiateWasmInstructions): "InstantiateWasmInstructions", + int32(InstantiateWasmFunctions): "InstantiateWasmFunctions", + int32(InstantiateWasmGlobals): "InstantiateWasmGlobals", + int32(InstantiateWasmTableEntries): "InstantiateWasmTableEntries", + int32(InstantiateWasmTypes): "InstantiateWasmTypes", + int32(InstantiateWasmDataSegments): "InstantiateWasmDataSegments", + int32(InstantiateWasmElemSegments): "InstantiateWasmElemSegments", + int32(InstantiateWasmImports): "InstantiateWasmImports", + int32(InstantiateWasmExports): "InstantiateWasmExports", + int32(InstantiateWasmDataSegmentBytes): "InstantiateWasmDataSegmentBytes", + int32(Sec1DecodePointUncompressed): "Sec1DecodePointUncompressed", + int32(VerifyEcdsaSecp256r1Sig): "VerifyEcdsaSecp256r1Sig", } var _XdrValues_ContractCostType = map[string]int32{ - "WasmInsnExec": int32(WasmInsnExec), - "MemAlloc": int32(MemAlloc), - "MemCpy": int32(MemCpy), - "MemCmp": int32(MemCmp), - "DispatchHostFunction": int32(DispatchHostFunction), - "VisitObject": int32(VisitObject), - "ValSer": int32(ValSer), - "ValDeser": int32(ValDeser), - "ComputeSha256Hash": int32(ComputeSha256Hash), - "ComputeEd25519PubKey": int32(ComputeEd25519PubKey), - "VerifyEd25519Sig": int32(VerifyEd25519Sig), - "VmInstantiation": int32(VmInstantiation), - "VmCachedInstantiation": int32(VmCachedInstantiation), - "InvokeVmFunction": int32(InvokeVmFunction), - "ComputeKeccak256Hash": int32(ComputeKeccak256Hash), - "ComputeEcdsaSecp256k1Sig": int32(ComputeEcdsaSecp256k1Sig), - "RecoverEcdsaSecp256k1Key": int32(RecoverEcdsaSecp256k1Key), - "Int256AddSub": int32(Int256AddSub), - "Int256Mul": int32(Int256Mul), - "Int256Div": int32(Int256Div), - "Int256Pow": int32(Int256Pow), - "Int256Shift": int32(Int256Shift), - "ChaCha20DrawBytes": int32(ChaCha20DrawBytes), + "WasmInsnExec": int32(WasmInsnExec), + "MemAlloc": int32(MemAlloc), + "MemCpy": int32(MemCpy), + "MemCmp": int32(MemCmp), + "DispatchHostFunction": int32(DispatchHostFunction), + "VisitObject": int32(VisitObject), + "ValSer": int32(ValSer), + "ValDeser": int32(ValDeser), + "ComputeSha256Hash": int32(ComputeSha256Hash), + "ComputeEd25519PubKey": int32(ComputeEd25519PubKey), + "VerifyEd25519Sig": int32(VerifyEd25519Sig), + "VmInstantiation": int32(VmInstantiation), + "VmCachedInstantiation": int32(VmCachedInstantiation), + "InvokeVmFunction": int32(InvokeVmFunction), + "ComputeKeccak256Hash": int32(ComputeKeccak256Hash), + "DecodeEcdsaCurve256Sig": int32(DecodeEcdsaCurve256Sig), + "RecoverEcdsaSecp256k1Key": int32(RecoverEcdsaSecp256k1Key), + "Int256AddSub": int32(Int256AddSub), + "Int256Mul": int32(Int256Mul), + "Int256Div": int32(Int256Div), + "Int256Pow": int32(Int256Pow), + "Int256Shift": int32(Int256Shift), + "ChaCha20DrawBytes": int32(ChaCha20DrawBytes), + "ParseWasmInstructions": int32(ParseWasmInstructions), + "ParseWasmFunctions": int32(ParseWasmFunctions), + "ParseWasmGlobals": int32(ParseWasmGlobals), + "ParseWasmTableEntries": int32(ParseWasmTableEntries), + "ParseWasmTypes": int32(ParseWasmTypes), + "ParseWasmDataSegments": int32(ParseWasmDataSegments), + "ParseWasmElemSegments": int32(ParseWasmElemSegments), + "ParseWasmImports": int32(ParseWasmImports), + "ParseWasmExports": int32(ParseWasmExports), + "ParseWasmDataSegmentBytes": int32(ParseWasmDataSegmentBytes), + "InstantiateWasmInstructions": int32(InstantiateWasmInstructions), + "InstantiateWasmFunctions": int32(InstantiateWasmFunctions), + "InstantiateWasmGlobals": int32(InstantiateWasmGlobals), + "InstantiateWasmTableEntries": int32(InstantiateWasmTableEntries), + "InstantiateWasmTypes": int32(InstantiateWasmTypes), + "InstantiateWasmDataSegments": int32(InstantiateWasmDataSegments), + "InstantiateWasmElemSegments": int32(InstantiateWasmElemSegments), + "InstantiateWasmImports": int32(InstantiateWasmImports), + "InstantiateWasmExports": int32(InstantiateWasmExports), + "InstantiateWasmDataSegmentBytes": int32(InstantiateWasmDataSegmentBytes), + "Sec1DecodePointUncompressed": int32(Sec1DecodePointUncompressed), + "VerifyEcdsaSecp256r1Sig": int32(VerifyEcdsaSecp256r1Sig), } func (ContractCostType) XdrEnumNames() map[int32]string { @@ -28554,29 +29025,51 @@ type XdrType_ContractCostType = *ContractCostType func XDR_ContractCostType(v *ContractCostType) *ContractCostType { return v } var _XdrComments_ContractCostType = map[int32]string{ - int32(WasmInsnExec): "Cost of running 1 wasm instruction", - int32(MemAlloc): "Cost of allocating a slice of memory (in bytes)", - int32(MemCpy): "Cost of copying a slice of bytes into a pre-allocated memory", - int32(MemCmp): "Cost of comparing two slices of memory", - int32(DispatchHostFunction): "Cost of a host function dispatch, not including the actual work done by the function nor the cost of VM invocation machinary", - int32(VisitObject): "Cost of visiting a host object from the host object storage. Exists to make sure some baseline cost coverage, i.e. repeatly visiting objects by the guest will always incur some charges.", - int32(ValSer): "Cost of serializing an xdr object to bytes", - int32(ValDeser): "Cost of deserializing an xdr object from bytes", - int32(ComputeSha256Hash): "Cost of computing the sha256 hash from bytes", - int32(ComputeEd25519PubKey): "Cost of computing the ed25519 pubkey from bytes", - int32(VerifyEd25519Sig): "Cost of verifying ed25519 signature of a payload.", - int32(VmInstantiation): "Cost of instantiation a VM from wasm bytes code.", - int32(VmCachedInstantiation): "Cost of instantiation a VM from a cached state.", - int32(InvokeVmFunction): "Cost of invoking a function on the VM. If the function is a host function, additional cost will be covered by `DispatchHostFunction`.", - int32(ComputeKeccak256Hash): "Cost of computing a keccak256 hash from bytes.", - int32(ComputeEcdsaSecp256k1Sig): "Cost of computing an ECDSA secp256k1 signature from bytes.", - int32(RecoverEcdsaSecp256k1Key): "Cost of recovering an ECDSA secp256k1 key from a signature.", - int32(Int256AddSub): "Cost of int256 addition (`+`) and subtraction (`-`) operations", - int32(Int256Mul): "Cost of int256 multiplication (`*`) operation", - int32(Int256Div): "Cost of int256 division (`/`) operation", - int32(Int256Pow): "Cost of int256 power (`exp`) operation", - int32(Int256Shift): "Cost of int256 shift (`shl`, `shr`) operation", - int32(ChaCha20DrawBytes): "Cost of drawing random bytes using a ChaCha20 PRNG", + int32(WasmInsnExec): "Cost of running 1 wasm instruction", + int32(MemAlloc): "Cost of allocating a slice of memory (in bytes)", + int32(MemCpy): "Cost of copying a slice of bytes into a pre-allocated memory", + int32(MemCmp): "Cost of comparing two slices of memory", + int32(DispatchHostFunction): "Cost of a host function dispatch, not including the actual work done by the function nor the cost of VM invocation machinary", + int32(VisitObject): "Cost of visiting a host object from the host object storage. Exists to make sure some baseline cost coverage, i.e. repeatly visiting objects by the guest will always incur some charges.", + int32(ValSer): "Cost of serializing an xdr object to bytes", + int32(ValDeser): "Cost of deserializing an xdr object from bytes", + int32(ComputeSha256Hash): "Cost of computing the sha256 hash from bytes", + int32(ComputeEd25519PubKey): "Cost of computing the ed25519 pubkey from bytes", + int32(VerifyEd25519Sig): "Cost of verifying ed25519 signature of a payload.", + int32(VmInstantiation): "Cost of instantiation a VM from wasm bytes code.", + int32(VmCachedInstantiation): "Cost of instantiation a VM from a cached state.", + int32(InvokeVmFunction): "Cost of invoking a function on the VM. If the function is a host function, additional cost will be covered by `DispatchHostFunction`.", + int32(ComputeKeccak256Hash): "Cost of computing a keccak256 hash from bytes.", + int32(DecodeEcdsaCurve256Sig): "Cost of decoding an ECDSA signature computed from a 256-bit prime modulus curve (e.g. secp256k1 and secp256r1)", + int32(RecoverEcdsaSecp256k1Key): "Cost of recovering an ECDSA secp256k1 key from a signature.", + int32(Int256AddSub): "Cost of int256 addition (`+`) and subtraction (`-`) operations", + int32(Int256Mul): "Cost of int256 multiplication (`*`) operation", + int32(Int256Div): "Cost of int256 division (`/`) operation", + int32(Int256Pow): "Cost of int256 power (`exp`) operation", + int32(Int256Shift): "Cost of int256 shift (`shl`, `shr`) operation", + int32(ChaCha20DrawBytes): "Cost of drawing random bytes using a ChaCha20 PRNG", + int32(ParseWasmInstructions): "Cost of parsing wasm bytes that only encode instructions.", + int32(ParseWasmFunctions): "Cost of parsing a known number of wasm functions.", + int32(ParseWasmGlobals): "Cost of parsing a known number of wasm globals.", + int32(ParseWasmTableEntries): "Cost of parsing a known number of wasm table entries.", + int32(ParseWasmTypes): "Cost of parsing a known number of wasm types.", + int32(ParseWasmDataSegments): "Cost of parsing a known number of wasm data segments.", + int32(ParseWasmElemSegments): "Cost of parsing a known number of wasm element segments.", + int32(ParseWasmImports): "Cost of parsing a known number of wasm imports.", + int32(ParseWasmExports): "Cost of parsing a known number of wasm exports.", + int32(ParseWasmDataSegmentBytes): "Cost of parsing a known number of data segment bytes.", + int32(InstantiateWasmInstructions): "Cost of instantiating wasm bytes that only encode instructions.", + int32(InstantiateWasmFunctions): "Cost of instantiating a known number of wasm functions.", + int32(InstantiateWasmGlobals): "Cost of instantiating a known number of wasm globals.", + int32(InstantiateWasmTableEntries): "Cost of instantiating a known number of wasm table entries.", + int32(InstantiateWasmTypes): "Cost of instantiating a known number of wasm types.", + int32(InstantiateWasmDataSegments): "Cost of instantiating a known number of wasm data segments.", + int32(InstantiateWasmElemSegments): "Cost of instantiating a known number of wasm element segments.", + int32(InstantiateWasmImports): "Cost of instantiating a known number of wasm imports.", + int32(InstantiateWasmExports): "Cost of instantiating a known number of wasm exports.", + int32(InstantiateWasmDataSegmentBytes): "Cost of instantiating a known number of data segment bytes.", + int32(Sec1DecodePointUncompressed): "Cost of decoding a bytes array representing an uncompressed SEC-1 encoded point on a 256-bit elliptic curve", + int32(VerifyEcdsaSecp256r1Sig): "Cost of verifying an ECDSA Secp256r1 signature", } func (e ContractCostType) XdrEnumComments() map[int32]string { diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 127d8f0293..fe9c62eba4 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -29,7 +29,7 @@ import ( const ( // MaxSupportedProtocolVersion defines the maximum supported version of // the Stellar protocol. - MaxSupportedProtocolVersion uint32 = 20 + MaxSupportedProtocolVersion uint32 = 21 // CurrentVersion reflects the latest version of the ingestion // algorithm. This value is stored in KV store and is used to decide diff --git a/services/horizon/internal/integration/extend_footprint_ttl_test.go b/services/horizon/internal/integration/extend_footprint_ttl_test.go index e280184b65..cc6f947a14 100644 --- a/services/horizon/internal/integration/extend_footprint_ttl_test.go +++ b/services/horizon/internal/integration/extend_footprint_ttl_test.go @@ -17,7 +17,6 @@ func TestExtendFootprintTtl(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, }) diff --git a/services/horizon/internal/integration/invokehostfunction_test.go b/services/horizon/internal/integration/invokehostfunction_test.go index 275f0de23b..6447b98147 100644 --- a/services/horizon/internal/integration/invokehostfunction_test.go +++ b/services/horizon/internal/integration/invokehostfunction_test.go @@ -30,7 +30,6 @@ func TestContractInvokeHostFunctionInstallContract(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -80,7 +79,6 @@ func TestContractInvokeHostFunctionCreateContractByAddress(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -134,7 +132,6 @@ func TestContractInvokeHostFunctionInvokeStatelessContractFn(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -243,7 +240,6 @@ func TestContractInvokeHostFunctionInvokeStatefulContractFn(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, }) diff --git a/services/horizon/internal/integration/sac_test.go b/services/horizon/internal/integration/sac_test.go index 64c772b44c..7eb91a201c 100644 --- a/services/horizon/internal/integration/sac_test.go +++ b/services/horizon/internal/integration/sac_test.go @@ -40,7 +40,6 @@ func TestContractMintToAccount(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, HorizonEnvironment: map[string]string{"INGEST_DISABLE_STATE_VERIFICATION": "true", "CONNECTION_TIMEOUT": "360000"}, EnableSorobanRPC: true, }) @@ -145,7 +144,6 @@ func TestContractMintToContract(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -225,7 +223,6 @@ func TestExpirationAndRestoration(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, HorizonIngestParameters: map[string]string{ // disable state verification because we will insert @@ -506,7 +503,6 @@ func TestContractTransferBetweenAccounts(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -581,7 +577,6 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -702,7 +697,6 @@ func TestContractTransferBetweenContracts(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -784,7 +778,6 @@ func TestContractBurnFromAccount(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -860,7 +853,6 @@ func TestContractBurnFromContract(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -928,7 +920,6 @@ func TestContractClawbackFromAccount(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -1006,7 +997,6 @@ func TestContractClawbackFromContract(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, }) diff --git a/services/horizon/internal/integration/transaction_test.go b/services/horizon/internal/integration/transaction_test.go index a0db5816dd..8c3fa369fb 100644 --- a/services/horizon/internal/integration/transaction_test.go +++ b/services/horizon/internal/integration/transaction_test.go @@ -72,7 +72,6 @@ func TestP20MetaTransaction(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, }) @@ -102,7 +101,6 @@ func TestP20MetaDisabledTransaction(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, HorizonEnvironment: map[string]string{"SKIP_TXMETA": "TRUE"}, EnableSorobanRPC: true, }) diff --git a/services/horizon/internal/integration/txsub_test.go b/services/horizon/internal/integration/txsub_test.go index 2f6b98e905..e253522893 100644 --- a/services/horizon/internal/integration/txsub_test.go +++ b/services/horizon/internal/integration/txsub_test.go @@ -60,7 +60,6 @@ func TestTxSubLimitsBodySize(t *testing.T) { } itest := integration.NewTest(t, integration.Config{ - ProtocolVersion: 20, EnableSorobanRPC: true, HorizonEnvironment: map[string]string{ "MAX_HTTP_REQUEST_SIZE": "1800", diff --git a/xdr/Stellar-contract-config-setting.x b/xdr/Stellar-contract-config-setting.x index 6b5074735d..52cc0224df 100644 --- a/xdr/Stellar-contract-config-setting.x +++ b/xdr/Stellar-contract-config-setting.x @@ -124,8 +124,9 @@ enum ContractCostType { InvokeVmFunction = 13, // Cost of computing a keccak256 hash from bytes. ComputeKeccak256Hash = 14, - // Cost of computing an ECDSA secp256k1 signature from bytes. - ComputeEcdsaSecp256k1Sig = 15, + // Cost of decoding an ECDSA signature computed from a 256-bit prime modulus + // curve (e.g. secp256k1 and secp256r1) + DecodeEcdsaCurve256Sig = 15, // Cost of recovering an ECDSA secp256k1 key from a signature. RecoverEcdsaSecp256k1Key = 16, // Cost of int256 addition (`+`) and subtraction (`-`) operations @@ -139,7 +140,55 @@ enum ContractCostType { // Cost of int256 shift (`shl`, `shr`) operation Int256Shift = 21, // Cost of drawing random bytes using a ChaCha20 PRNG - ChaCha20DrawBytes = 22 + ChaCha20DrawBytes = 22, + + // Cost of parsing wasm bytes that only encode instructions. + ParseWasmInstructions = 23, + // Cost of parsing a known number of wasm functions. + ParseWasmFunctions = 24, + // Cost of parsing a known number of wasm globals. + ParseWasmGlobals = 25, + // Cost of parsing a known number of wasm table entries. + ParseWasmTableEntries = 26, + // Cost of parsing a known number of wasm types. + ParseWasmTypes = 27, + // Cost of parsing a known number of wasm data segments. + ParseWasmDataSegments = 28, + // Cost of parsing a known number of wasm element segments. + ParseWasmElemSegments = 29, + // Cost of parsing a known number of wasm imports. + ParseWasmImports = 30, + // Cost of parsing a known number of wasm exports. + ParseWasmExports = 31, + // Cost of parsing a known number of data segment bytes. + ParseWasmDataSegmentBytes = 32, + + // Cost of instantiating wasm bytes that only encode instructions. + InstantiateWasmInstructions = 33, + // Cost of instantiating a known number of wasm functions. + InstantiateWasmFunctions = 34, + // Cost of instantiating a known number of wasm globals. + InstantiateWasmGlobals = 35, + // Cost of instantiating a known number of wasm table entries. + InstantiateWasmTableEntries = 36, + // Cost of instantiating a known number of wasm types. + InstantiateWasmTypes = 37, + // Cost of instantiating a known number of wasm data segments. + InstantiateWasmDataSegments = 38, + // Cost of instantiating a known number of wasm element segments. + InstantiateWasmElemSegments = 39, + // Cost of instantiating a known number of wasm imports. + InstantiateWasmImports = 40, + // Cost of instantiating a known number of wasm exports. + InstantiateWasmExports = 41, + // Cost of instantiating a known number of data segment bytes. + InstantiateWasmDataSegmentBytes = 42, + + // Cost of decoding a bytes array representing an uncompressed SEC-1 encoded + // point on a 256-bit elliptic curve + Sec1DecodePointUncompressed = 43, + // Cost of verifying an ECDSA Secp256r1 signature + VerifyEcdsaSecp256r1Sig = 44 }; struct ContractCostParamEntry { diff --git a/xdr/Stellar-ledger-entries.x b/xdr/Stellar-ledger-entries.x index 8a8784e2bb..3a137ae60d 100644 --- a/xdr/Stellar-ledger-entries.x +++ b/xdr/Stellar-ledger-entries.x @@ -508,8 +508,32 @@ struct ContractDataEntry { SCVal val; }; -struct ContractCodeEntry { +struct ContractCodeCostInputs { ExtensionPoint ext; + uint32 nInstructions; + uint32 nFunctions; + uint32 nGlobals; + uint32 nTableEntries; + uint32 nTypes; + uint32 nDataSegments; + uint32 nElemSegments; + uint32 nImports; + uint32 nExports; + uint32 nDataSegmentBytes; +}; + +struct ContractCodeEntry { + union switch (int v) + { + case 0: + void; + case 1: + struct + { + ExtensionPoint ext; + ContractCodeCostInputs costInputs; + } v1; + } ext; Hash hash; opaque code<>; diff --git a/xdr/Stellar-ledger.x b/xdr/Stellar-ledger.x index b18a3a0d57..dd58ae8d9e 100644 --- a/xdr/Stellar-ledger.x +++ b/xdr/Stellar-ledger.x @@ -400,10 +400,52 @@ struct DiagnosticEvent ContractEvent event; }; -struct SorobanTransactionMeta +struct SorobanTransactionMetaExtV1 { ExtensionPoint ext; + // The following are the components of the overall Soroban resource fee + // charged for the transaction. + // The following relation holds: + // `resourceFeeCharged = totalNonRefundableResourceFeeCharged + totalRefundableResourceFeeCharged` + // where `resourceFeeCharged` is the overall fee charged for the + // transaction. Also, `resourceFeeCharged` <= `sorobanData.resourceFee` + // i.e.we never charge more than the declared resource fee. + // The inclusion fee for charged the Soroban transaction can be found using + // the following equation: + // `result.feeCharged = resourceFeeCharged + inclusionFeeCharged`. + + // Total amount (in stroops) that has been charged for non-refundable + // Soroban resources. + // Non-refundable resources are charged based on the usage declared in + // the transaction envelope (such as `instructions`, `readBytes` etc.) and + // is charged regardless of the success of the transaction. + int64 totalNonRefundableResourceFeeCharged; + // Total amount (in stroops) that has been charged for refundable + // Soroban resource fees. + // Currently this comprises the rent fee (`rentFeeCharged`) and the + // fee for the events and return value. + // Refundable resources are charged based on the actual resources usage. + // Since currently refundable resources are only used for the successful + // transactions, this will be `0` for failed transactions. + int64 totalRefundableResourceFeeCharged; + // Amount (in stroops) that has been charged for rent. + // This is a part of `totalNonRefundableResourceFeeCharged`. + int64 rentFeeCharged; +}; + +union SorobanTransactionMetaExt switch (int v) +{ +case 0: + void; +case 1: + SorobanTransactionMetaExtV1 v1; +}; + +struct SorobanTransactionMeta +{ + SorobanTransactionMetaExt ext; + ContractEvent events<>; // custom events populated by the // contracts themselves. SCVal returnValue; // return value of the host fn invocation @@ -484,11 +526,23 @@ struct LedgerCloseMetaV0 SCPHistoryEntry scpInfo<>; }; -struct LedgerCloseMetaV1 +struct LedgerCloseMetaExtV1 { - // We forgot to add an ExtensionPoint in v0 but at least - // we can add one now in v1. ExtensionPoint ext; + int64 sorobanFeeWrite1KB; +}; + +union LedgerCloseMetaExt switch (int v) +{ +case 0: + void; +case 1: + LedgerCloseMetaExtV1 v1; +}; + +struct LedgerCloseMetaV1 +{ + LedgerCloseMetaExt ext; LedgerHeaderHistoryEntry ledgerHeader; diff --git a/xdr/xdr_commit_generated.txt b/xdr/xdr_commit_generated.txt index b7c3979571..610c71d294 100644 --- a/xdr/xdr_commit_generated.txt +++ b/xdr/xdr_commit_generated.txt @@ -1 +1 @@ -b96148cd4acc372cc9af17b909ffe4b12c43ecb6 \ No newline at end of file +59062438237d5f77fd6feb060b76288e88b7e222 \ No newline at end of file diff --git a/xdr/xdr_generated.go b/xdr/xdr_generated.go index ad832d79b4..b336714562 100644 --- a/xdr/xdr_generated.go +++ b/xdr/xdr_generated.go @@ -34,15 +34,15 @@ import ( // XdrFilesSHA256 is the SHA256 hashes of source files. var XdrFilesSHA256 = map[string]string{ "xdr/Stellar-SCP.x": "8f32b04d008f8bc33b8843d075e69837231a673691ee41d8b821ca229a6e802a", - "xdr/Stellar-contract-config-setting.x": "fc42980e8710514679477f767ecad6f9348c38d24b1e4476fdd7e73e8e672ea8", + "xdr/Stellar-contract-config-setting.x": "393369678663cb0f9471a0b69e2a9cfa3ac93c4415fa40cec166e9a231ecbe0d", "xdr/Stellar-contract-env-meta.x": "928a30de814ee589bc1d2aadd8dd81c39f71b7e6f430f56974505ccb1f49654b", "xdr/Stellar-contract-meta.x": "f01532c11ca044e19d9f9f16fe373e9af64835da473be556b9a807ee3319ae0d", "xdr/Stellar-contract-spec.x": "c7ffa21d2e91afb8e666b33524d307955426ff553a486d670c29217ed9888d49", "xdr/Stellar-contract.x": "7f665e4103e146a88fcdabce879aaaacd3bf9283feb194cc47ff986264c1e315", "xdr/Stellar-exporter.x": "a00c83d02e8c8382e06f79a191f1fb5abd097a4bbcab8481c67467e3270e0529", "xdr/Stellar-internal.x": "227835866c1b2122d1eaf28839ba85ea7289d1cb681dda4ca619c2da3d71fe00", - "xdr/Stellar-ledger-entries.x": "4f8f2324f567a40065f54f696ea1428740f043ea4154f5986d9f499ad00ac333", - "xdr/Stellar-ledger.x": "2c842f3fe6e269498af5467f849cf6818554e90babc845f34c87cda471298d0f", + "xdr/Stellar-ledger-entries.x": "77dc7062ae6d0812136333e12e35b2294d7c2896a536be9c811eb0ed2abbbccb", + "xdr/Stellar-ledger.x": "888152fb940b79a01ac00a5218ca91360cb0f01af7acc030d5805ebfec280203", "xdr/Stellar-lighthorizon.x": "1aac09eaeda224154f653a0c95f02167be0c110fc295bb41b756a080eb8c06df", "xdr/Stellar-overlay.x": "de3957c58b96ae07968b3d3aebea84f83603e95322d1fa336360e13e3aba737a", "xdr/Stellar-transaction.x": "0d2b35a331a540b48643925d0869857236eb2487c02d340ea32e365e784ea2b8", @@ -8049,16 +8049,421 @@ func (s ContractDataEntry) xdrType() {} var _ xdrType = (*ContractDataEntry)(nil) +// ContractCodeCostInputs is an XDR Struct defines as: +// +// struct ContractCodeCostInputs { +// ExtensionPoint ext; +// uint32 nInstructions; +// uint32 nFunctions; +// uint32 nGlobals; +// uint32 nTableEntries; +// uint32 nTypes; +// uint32 nDataSegments; +// uint32 nElemSegments; +// uint32 nImports; +// uint32 nExports; +// uint32 nDataSegmentBytes; +// }; +type ContractCodeCostInputs struct { + Ext ExtensionPoint + NInstructions Uint32 + NFunctions Uint32 + NGlobals Uint32 + NTableEntries Uint32 + NTypes Uint32 + NDataSegments Uint32 + NElemSegments Uint32 + NImports Uint32 + NExports Uint32 + NDataSegmentBytes Uint32 +} + +// EncodeTo encodes this value using the Encoder. +func (s *ContractCodeCostInputs) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.Ext.EncodeTo(e); err != nil { + return err + } + if err = s.NInstructions.EncodeTo(e); err != nil { + return err + } + if err = s.NFunctions.EncodeTo(e); err != nil { + return err + } + if err = s.NGlobals.EncodeTo(e); err != nil { + return err + } + if err = s.NTableEntries.EncodeTo(e); err != nil { + return err + } + if err = s.NTypes.EncodeTo(e); err != nil { + return err + } + if err = s.NDataSegments.EncodeTo(e); err != nil { + return err + } + if err = s.NElemSegments.EncodeTo(e); err != nil { + return err + } + if err = s.NImports.EncodeTo(e); err != nil { + return err + } + if err = s.NExports.EncodeTo(e); err != nil { + return err + } + if err = s.NDataSegmentBytes.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*ContractCodeCostInputs)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *ContractCodeCostInputs) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding ContractCodeCostInputs: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.Ext.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding ExtensionPoint: %w", err) + } + nTmp, err = s.NInstructions.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.NFunctions.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.NGlobals.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.NTableEntries.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.NTypes.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.NDataSegments.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.NElemSegments.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.NImports.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.NExports.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.NDataSegmentBytes.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s ContractCodeCostInputs) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *ContractCodeCostInputs) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*ContractCodeCostInputs)(nil) + _ encoding.BinaryUnmarshaler = (*ContractCodeCostInputs)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s ContractCodeCostInputs) xdrType() {} + +var _ xdrType = (*ContractCodeCostInputs)(nil) + +// ContractCodeEntryV1 is an XDR NestedStruct defines as: +// +// struct +// { +// ExtensionPoint ext; +// ContractCodeCostInputs costInputs; +// } +type ContractCodeEntryV1 struct { + Ext ExtensionPoint + CostInputs ContractCodeCostInputs +} + +// EncodeTo encodes this value using the Encoder. +func (s *ContractCodeEntryV1) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.Ext.EncodeTo(e); err != nil { + return err + } + if err = s.CostInputs.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*ContractCodeEntryV1)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *ContractCodeEntryV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding ContractCodeEntryV1: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.Ext.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding ExtensionPoint: %w", err) + } + nTmp, err = s.CostInputs.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding ContractCodeCostInputs: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s ContractCodeEntryV1) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *ContractCodeEntryV1) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*ContractCodeEntryV1)(nil) + _ encoding.BinaryUnmarshaler = (*ContractCodeEntryV1)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s ContractCodeEntryV1) xdrType() {} + +var _ xdrType = (*ContractCodeEntryV1)(nil) + +// ContractCodeEntryExt is an XDR NestedUnion defines as: +// +// union switch (int v) +// { +// case 0: +// void; +// case 1: +// struct +// { +// ExtensionPoint ext; +// ContractCodeCostInputs costInputs; +// } v1; +// } +type ContractCodeEntryExt struct { + V int32 + V1 *ContractCodeEntryV1 +} + +// SwitchFieldName returns the field name in which this union's +// discriminant is stored +func (u ContractCodeEntryExt) SwitchFieldName() string { + return "V" +} + +// ArmForSwitch returns which field name should be used for storing +// the value for an instance of ContractCodeEntryExt +func (u ContractCodeEntryExt) ArmForSwitch(sw int32) (string, bool) { + switch int32(sw) { + case 0: + return "", true + case 1: + return "V1", true + } + return "-", false +} + +// NewContractCodeEntryExt creates a new ContractCodeEntryExt. +func NewContractCodeEntryExt(v int32, value interface{}) (result ContractCodeEntryExt, err error) { + result.V = v + switch int32(v) { + case 0: + // void + case 1: + tv, ok := value.(ContractCodeEntryV1) + if !ok { + err = errors.New("invalid value, must be ContractCodeEntryV1") + return + } + result.V1 = &tv + } + return +} + +// MustV1 retrieves the V1 value from the union, +// panicing if the value is not set. +func (u ContractCodeEntryExt) MustV1() ContractCodeEntryV1 { + val, ok := u.GetV1() + + if !ok { + panic("arm V1 is not set") + } + + return val +} + +// GetV1 retrieves the V1 value from the union, +// returning ok if the union's switch indicated the value is valid. +func (u ContractCodeEntryExt) GetV1() (result ContractCodeEntryV1, ok bool) { + armName, _ := u.ArmForSwitch(int32(u.V)) + + if armName == "V1" { + result = *u.V1 + ok = true + } + + return +} + +// EncodeTo encodes this value using the Encoder. +func (u ContractCodeEntryExt) EncodeTo(e *xdr.Encoder) error { + var err error + if _, err = e.EncodeInt(int32(u.V)); err != nil { + return err + } + switch int32(u.V) { + case 0: + // Void + return nil + case 1: + if err = (*u.V1).EncodeTo(e); err != nil { + return err + } + return nil + } + return fmt.Errorf("V (int32) switch value '%d' is not valid for union ContractCodeEntryExt", u.V) +} + +var _ decoderFrom = (*ContractCodeEntryExt)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (u *ContractCodeEntryExt) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding ContractCodeEntryExt: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + u.V, nTmp, err = d.DecodeInt() + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Int: %w", err) + } + switch int32(u.V) { + case 0: + // Void + return n, nil + case 1: + u.V1 = new(ContractCodeEntryV1) + nTmp, err = (*u.V1).DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding ContractCodeEntryV1: %w", err) + } + return n, nil + } + return n, fmt.Errorf("union ContractCodeEntryExt has invalid V (int32) switch value '%d'", u.V) +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s ContractCodeEntryExt) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *ContractCodeEntryExt) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*ContractCodeEntryExt)(nil) + _ encoding.BinaryUnmarshaler = (*ContractCodeEntryExt)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s ContractCodeEntryExt) xdrType() {} + +var _ xdrType = (*ContractCodeEntryExt)(nil) + // ContractCodeEntry is an XDR Struct defines as: // // struct ContractCodeEntry { -// ExtensionPoint ext; +// union switch (int v) +// { +// case 0: +// void; +// case 1: +// struct +// { +// ExtensionPoint ext; +// ContractCodeCostInputs costInputs; +// } v1; +// } ext; // // Hash hash; // opaque code<>; // }; type ContractCodeEntry struct { - Ext ExtensionPoint + Ext ContractCodeEntryExt Hash Hash Code []byte } @@ -8091,7 +8496,7 @@ func (s *ContractCodeEntry) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro nTmp, err = s.Ext.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding ExtensionPoint: %w", err) + return n, fmt.Errorf("decoding ContractCodeEntryExt: %w", err) } nTmp, err = s.Hash.DecodeFrom(d, maxDepth) n += nTmp @@ -16349,11 +16754,284 @@ func (s DiagnosticEvent) xdrType() {} var _ xdrType = (*DiagnosticEvent)(nil) +// SorobanTransactionMetaExtV1 is an XDR Struct defines as: +// +// struct SorobanTransactionMetaExtV1 +// { +// ExtensionPoint ext; +// +// // The following are the components of the overall Soroban resource fee +// // charged for the transaction. +// // The following relation holds: +// // `resourceFeeCharged = totalNonRefundableResourceFeeCharged + totalRefundableResourceFeeCharged` +// // where `resourceFeeCharged` is the overall fee charged for the +// // transaction. Also, `resourceFeeCharged` <= `sorobanData.resourceFee` +// // i.e.we never charge more than the declared resource fee. +// // The inclusion fee for charged the Soroban transaction can be found using +// // the following equation: +// // `result.feeCharged = resourceFeeCharged + inclusionFeeCharged`. +// +// // Total amount (in stroops) that has been charged for non-refundable +// // Soroban resources. +// // Non-refundable resources are charged based on the usage declared in +// // the transaction envelope (such as `instructions`, `readBytes` etc.) and +// // is charged regardless of the success of the transaction. +// int64 totalNonRefundableResourceFeeCharged; +// // Total amount (in stroops) that has been charged for refundable +// // Soroban resource fees. +// // Currently this comprises the rent fee (`rentFeeCharged`) and the +// // fee for the events and return value. +// // Refundable resources are charged based on the actual resources usage. +// // Since currently refundable resources are only used for the successful +// // transactions, this will be `0` for failed transactions. +// int64 totalRefundableResourceFeeCharged; +// // Amount (in stroops) that has been charged for rent. +// // This is a part of `totalNonRefundableResourceFeeCharged`. +// int64 rentFeeCharged; +// }; +type SorobanTransactionMetaExtV1 struct { + Ext ExtensionPoint + TotalNonRefundableResourceFeeCharged Int64 + TotalRefundableResourceFeeCharged Int64 + RentFeeCharged Int64 +} + +// EncodeTo encodes this value using the Encoder. +func (s *SorobanTransactionMetaExtV1) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.Ext.EncodeTo(e); err != nil { + return err + } + if err = s.TotalNonRefundableResourceFeeCharged.EncodeTo(e); err != nil { + return err + } + if err = s.TotalRefundableResourceFeeCharged.EncodeTo(e); err != nil { + return err + } + if err = s.RentFeeCharged.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*SorobanTransactionMetaExtV1)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *SorobanTransactionMetaExtV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding SorobanTransactionMetaExtV1: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.Ext.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding ExtensionPoint: %w", err) + } + nTmp, err = s.TotalNonRefundableResourceFeeCharged.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Int64: %w", err) + } + nTmp, err = s.TotalRefundableResourceFeeCharged.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Int64: %w", err) + } + nTmp, err = s.RentFeeCharged.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Int64: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s SorobanTransactionMetaExtV1) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *SorobanTransactionMetaExtV1) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*SorobanTransactionMetaExtV1)(nil) + _ encoding.BinaryUnmarshaler = (*SorobanTransactionMetaExtV1)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s SorobanTransactionMetaExtV1) xdrType() {} + +var _ xdrType = (*SorobanTransactionMetaExtV1)(nil) + +// SorobanTransactionMetaExt is an XDR Union defines as: +// +// union SorobanTransactionMetaExt switch (int v) +// { +// case 0: +// void; +// case 1: +// SorobanTransactionMetaExtV1 v1; +// }; +type SorobanTransactionMetaExt struct { + V int32 + V1 *SorobanTransactionMetaExtV1 +} + +// SwitchFieldName returns the field name in which this union's +// discriminant is stored +func (u SorobanTransactionMetaExt) SwitchFieldName() string { + return "V" +} + +// ArmForSwitch returns which field name should be used for storing +// the value for an instance of SorobanTransactionMetaExt +func (u SorobanTransactionMetaExt) ArmForSwitch(sw int32) (string, bool) { + switch int32(sw) { + case 0: + return "", true + case 1: + return "V1", true + } + return "-", false +} + +// NewSorobanTransactionMetaExt creates a new SorobanTransactionMetaExt. +func NewSorobanTransactionMetaExt(v int32, value interface{}) (result SorobanTransactionMetaExt, err error) { + result.V = v + switch int32(v) { + case 0: + // void + case 1: + tv, ok := value.(SorobanTransactionMetaExtV1) + if !ok { + err = errors.New("invalid value, must be SorobanTransactionMetaExtV1") + return + } + result.V1 = &tv + } + return +} + +// MustV1 retrieves the V1 value from the union, +// panicing if the value is not set. +func (u SorobanTransactionMetaExt) MustV1() SorobanTransactionMetaExtV1 { + val, ok := u.GetV1() + + if !ok { + panic("arm V1 is not set") + } + + return val +} + +// GetV1 retrieves the V1 value from the union, +// returning ok if the union's switch indicated the value is valid. +func (u SorobanTransactionMetaExt) GetV1() (result SorobanTransactionMetaExtV1, ok bool) { + armName, _ := u.ArmForSwitch(int32(u.V)) + + if armName == "V1" { + result = *u.V1 + ok = true + } + + return +} + +// EncodeTo encodes this value using the Encoder. +func (u SorobanTransactionMetaExt) EncodeTo(e *xdr.Encoder) error { + var err error + if _, err = e.EncodeInt(int32(u.V)); err != nil { + return err + } + switch int32(u.V) { + case 0: + // Void + return nil + case 1: + if err = (*u.V1).EncodeTo(e); err != nil { + return err + } + return nil + } + return fmt.Errorf("V (int32) switch value '%d' is not valid for union SorobanTransactionMetaExt", u.V) +} + +var _ decoderFrom = (*SorobanTransactionMetaExt)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (u *SorobanTransactionMetaExt) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding SorobanTransactionMetaExt: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + u.V, nTmp, err = d.DecodeInt() + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Int: %w", err) + } + switch int32(u.V) { + case 0: + // Void + return n, nil + case 1: + u.V1 = new(SorobanTransactionMetaExtV1) + nTmp, err = (*u.V1).DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding SorobanTransactionMetaExtV1: %w", err) + } + return n, nil + } + return n, fmt.Errorf("union SorobanTransactionMetaExt has invalid V (int32) switch value '%d'", u.V) +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s SorobanTransactionMetaExt) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *SorobanTransactionMetaExt) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*SorobanTransactionMetaExt)(nil) + _ encoding.BinaryUnmarshaler = (*SorobanTransactionMetaExt)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s SorobanTransactionMetaExt) xdrType() {} + +var _ xdrType = (*SorobanTransactionMetaExt)(nil) + // SorobanTransactionMeta is an XDR Struct defines as: // // struct SorobanTransactionMeta // { -// ExtensionPoint ext; +// SorobanTransactionMetaExt ext; // // ContractEvent events<>; // custom events populated by the // // contracts themselves. @@ -16365,7 +17043,7 @@ var _ xdrType = (*DiagnosticEvent)(nil) // DiagnosticEvent diagnosticEvents<>; // }; type SorobanTransactionMeta struct { - Ext ExtensionPoint + Ext SorobanTransactionMetaExt Events []ContractEvent ReturnValue ScVal DiagnosticEvents []DiagnosticEvent @@ -16412,7 +17090,7 @@ func (s *SorobanTransactionMeta) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, nTmp, err = s.Ext.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding ExtensionPoint: %w", err) + return n, fmt.Errorf("decoding SorobanTransactionMetaExt: %w", err) } var l uint32 l, nTmp, err = d.DecodeUint() @@ -17370,13 +18048,238 @@ func (s LedgerCloseMetaV0) xdrType() {} var _ xdrType = (*LedgerCloseMetaV0)(nil) +// LedgerCloseMetaExtV1 is an XDR Struct defines as: +// +// struct LedgerCloseMetaExtV1 +// { +// ExtensionPoint ext; +// int64 sorobanFeeWrite1KB; +// }; +type LedgerCloseMetaExtV1 struct { + Ext ExtensionPoint + SorobanFeeWrite1Kb Int64 +} + +// EncodeTo encodes this value using the Encoder. +func (s *LedgerCloseMetaExtV1) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.Ext.EncodeTo(e); err != nil { + return err + } + if err = s.SorobanFeeWrite1Kb.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*LedgerCloseMetaExtV1)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *LedgerCloseMetaExtV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding LedgerCloseMetaExtV1: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.Ext.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding ExtensionPoint: %w", err) + } + nTmp, err = s.SorobanFeeWrite1Kb.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Int64: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s LedgerCloseMetaExtV1) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *LedgerCloseMetaExtV1) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*LedgerCloseMetaExtV1)(nil) + _ encoding.BinaryUnmarshaler = (*LedgerCloseMetaExtV1)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s LedgerCloseMetaExtV1) xdrType() {} + +var _ xdrType = (*LedgerCloseMetaExtV1)(nil) + +// LedgerCloseMetaExt is an XDR Union defines as: +// +// union LedgerCloseMetaExt switch (int v) +// { +// case 0: +// void; +// case 1: +// LedgerCloseMetaExtV1 v1; +// }; +type LedgerCloseMetaExt struct { + V int32 + V1 *LedgerCloseMetaExtV1 +} + +// SwitchFieldName returns the field name in which this union's +// discriminant is stored +func (u LedgerCloseMetaExt) SwitchFieldName() string { + return "V" +} + +// ArmForSwitch returns which field name should be used for storing +// the value for an instance of LedgerCloseMetaExt +func (u LedgerCloseMetaExt) ArmForSwitch(sw int32) (string, bool) { + switch int32(sw) { + case 0: + return "", true + case 1: + return "V1", true + } + return "-", false +} + +// NewLedgerCloseMetaExt creates a new LedgerCloseMetaExt. +func NewLedgerCloseMetaExt(v int32, value interface{}) (result LedgerCloseMetaExt, err error) { + result.V = v + switch int32(v) { + case 0: + // void + case 1: + tv, ok := value.(LedgerCloseMetaExtV1) + if !ok { + err = errors.New("invalid value, must be LedgerCloseMetaExtV1") + return + } + result.V1 = &tv + } + return +} + +// MustV1 retrieves the V1 value from the union, +// panicing if the value is not set. +func (u LedgerCloseMetaExt) MustV1() LedgerCloseMetaExtV1 { + val, ok := u.GetV1() + + if !ok { + panic("arm V1 is not set") + } + + return val +} + +// GetV1 retrieves the V1 value from the union, +// returning ok if the union's switch indicated the value is valid. +func (u LedgerCloseMetaExt) GetV1() (result LedgerCloseMetaExtV1, ok bool) { + armName, _ := u.ArmForSwitch(int32(u.V)) + + if armName == "V1" { + result = *u.V1 + ok = true + } + + return +} + +// EncodeTo encodes this value using the Encoder. +func (u LedgerCloseMetaExt) EncodeTo(e *xdr.Encoder) error { + var err error + if _, err = e.EncodeInt(int32(u.V)); err != nil { + return err + } + switch int32(u.V) { + case 0: + // Void + return nil + case 1: + if err = (*u.V1).EncodeTo(e); err != nil { + return err + } + return nil + } + return fmt.Errorf("V (int32) switch value '%d' is not valid for union LedgerCloseMetaExt", u.V) +} + +var _ decoderFrom = (*LedgerCloseMetaExt)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (u *LedgerCloseMetaExt) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding LedgerCloseMetaExt: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + u.V, nTmp, err = d.DecodeInt() + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Int: %w", err) + } + switch int32(u.V) { + case 0: + // Void + return n, nil + case 1: + u.V1 = new(LedgerCloseMetaExtV1) + nTmp, err = (*u.V1).DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding LedgerCloseMetaExtV1: %w", err) + } + return n, nil + } + return n, fmt.Errorf("union LedgerCloseMetaExt has invalid V (int32) switch value '%d'", u.V) +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s LedgerCloseMetaExt) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *LedgerCloseMetaExt) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*LedgerCloseMetaExt)(nil) + _ encoding.BinaryUnmarshaler = (*LedgerCloseMetaExt)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s LedgerCloseMetaExt) xdrType() {} + +var _ xdrType = (*LedgerCloseMetaExt)(nil) + // LedgerCloseMetaV1 is an XDR Struct defines as: // // struct LedgerCloseMetaV1 // { -// // We forgot to add an ExtensionPoint in v0 but at least -// // we can add one now in v1. -// ExtensionPoint ext; +// LedgerCloseMetaExt ext; // // LedgerHeaderHistoryEntry ledgerHeader; // @@ -17405,7 +18308,7 @@ var _ xdrType = (*LedgerCloseMetaV0)(nil) // LedgerEntry evictedPersistentLedgerEntries<>; // }; type LedgerCloseMetaV1 struct { - Ext ExtensionPoint + Ext LedgerCloseMetaExt LedgerHeader LedgerHeaderHistoryEntry TxSet GeneralizedTransactionSet TxProcessing []TransactionResultMeta @@ -17487,7 +18390,7 @@ func (s *LedgerCloseMetaV1) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, erro nTmp, err = s.Ext.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding ExtensionPoint: %w", err) + return n, fmt.Errorf("decoding LedgerCloseMetaExt: %w", err) } nTmp, err = s.LedgerHeader.DecodeFrom(d, maxDepth) n += nTmp @@ -55059,8 +55962,9 @@ var _ xdrType = (*ConfigSettingContractBandwidthV0)(nil) // InvokeVmFunction = 13, // // Cost of computing a keccak256 hash from bytes. // ComputeKeccak256Hash = 14, -// // Cost of computing an ECDSA secp256k1 signature from bytes. -// ComputeEcdsaSecp256k1Sig = 15, +// // Cost of decoding an ECDSA signature computed from a 256-bit prime modulus +// // curve (e.g. secp256k1 and secp256r1) +// DecodeEcdsaCurve256Sig = 15, // // Cost of recovering an ECDSA secp256k1 key from a signature. // RecoverEcdsaSecp256k1Key = 16, // // Cost of int256 addition (`+`) and subtraction (`-`) operations @@ -55074,34 +55978,104 @@ var _ xdrType = (*ConfigSettingContractBandwidthV0)(nil) // // Cost of int256 shift (`shl`, `shr`) operation // Int256Shift = 21, // // Cost of drawing random bytes using a ChaCha20 PRNG -// ChaCha20DrawBytes = 22 +// ChaCha20DrawBytes = 22, +// +// // Cost of parsing wasm bytes that only encode instructions. +// ParseWasmInstructions = 23, +// // Cost of parsing a known number of wasm functions. +// ParseWasmFunctions = 24, +// // Cost of parsing a known number of wasm globals. +// ParseWasmGlobals = 25, +// // Cost of parsing a known number of wasm table entries. +// ParseWasmTableEntries = 26, +// // Cost of parsing a known number of wasm types. +// ParseWasmTypes = 27, +// // Cost of parsing a known number of wasm data segments. +// ParseWasmDataSegments = 28, +// // Cost of parsing a known number of wasm element segments. +// ParseWasmElemSegments = 29, +// // Cost of parsing a known number of wasm imports. +// ParseWasmImports = 30, +// // Cost of parsing a known number of wasm exports. +// ParseWasmExports = 31, +// // Cost of parsing a known number of data segment bytes. +// ParseWasmDataSegmentBytes = 32, +// +// // Cost of instantiating wasm bytes that only encode instructions. +// InstantiateWasmInstructions = 33, +// // Cost of instantiating a known number of wasm functions. +// InstantiateWasmFunctions = 34, +// // Cost of instantiating a known number of wasm globals. +// InstantiateWasmGlobals = 35, +// // Cost of instantiating a known number of wasm table entries. +// InstantiateWasmTableEntries = 36, +// // Cost of instantiating a known number of wasm types. +// InstantiateWasmTypes = 37, +// // Cost of instantiating a known number of wasm data segments. +// InstantiateWasmDataSegments = 38, +// // Cost of instantiating a known number of wasm element segments. +// InstantiateWasmElemSegments = 39, +// // Cost of instantiating a known number of wasm imports. +// InstantiateWasmImports = 40, +// // Cost of instantiating a known number of wasm exports. +// InstantiateWasmExports = 41, +// // Cost of instantiating a known number of data segment bytes. +// InstantiateWasmDataSegmentBytes = 42, +// +// // Cost of decoding a bytes array representing an uncompressed SEC-1 encoded +// // point on a 256-bit elliptic curve +// Sec1DecodePointUncompressed = 43, +// // Cost of verifying an ECDSA Secp256r1 signature +// VerifyEcdsaSecp256r1Sig = 44 // }; type ContractCostType int32 const ( - ContractCostTypeWasmInsnExec ContractCostType = 0 - ContractCostTypeMemAlloc ContractCostType = 1 - ContractCostTypeMemCpy ContractCostType = 2 - ContractCostTypeMemCmp ContractCostType = 3 - ContractCostTypeDispatchHostFunction ContractCostType = 4 - ContractCostTypeVisitObject ContractCostType = 5 - ContractCostTypeValSer ContractCostType = 6 - ContractCostTypeValDeser ContractCostType = 7 - ContractCostTypeComputeSha256Hash ContractCostType = 8 - ContractCostTypeComputeEd25519PubKey ContractCostType = 9 - ContractCostTypeVerifyEd25519Sig ContractCostType = 10 - ContractCostTypeVmInstantiation ContractCostType = 11 - ContractCostTypeVmCachedInstantiation ContractCostType = 12 - ContractCostTypeInvokeVmFunction ContractCostType = 13 - ContractCostTypeComputeKeccak256Hash ContractCostType = 14 - ContractCostTypeComputeEcdsaSecp256k1Sig ContractCostType = 15 - ContractCostTypeRecoverEcdsaSecp256k1Key ContractCostType = 16 - ContractCostTypeInt256AddSub ContractCostType = 17 - ContractCostTypeInt256Mul ContractCostType = 18 - ContractCostTypeInt256Div ContractCostType = 19 - ContractCostTypeInt256Pow ContractCostType = 20 - ContractCostTypeInt256Shift ContractCostType = 21 - ContractCostTypeChaCha20DrawBytes ContractCostType = 22 + ContractCostTypeWasmInsnExec ContractCostType = 0 + ContractCostTypeMemAlloc ContractCostType = 1 + ContractCostTypeMemCpy ContractCostType = 2 + ContractCostTypeMemCmp ContractCostType = 3 + ContractCostTypeDispatchHostFunction ContractCostType = 4 + ContractCostTypeVisitObject ContractCostType = 5 + ContractCostTypeValSer ContractCostType = 6 + ContractCostTypeValDeser ContractCostType = 7 + ContractCostTypeComputeSha256Hash ContractCostType = 8 + ContractCostTypeComputeEd25519PubKey ContractCostType = 9 + ContractCostTypeVerifyEd25519Sig ContractCostType = 10 + ContractCostTypeVmInstantiation ContractCostType = 11 + ContractCostTypeVmCachedInstantiation ContractCostType = 12 + ContractCostTypeInvokeVmFunction ContractCostType = 13 + ContractCostTypeComputeKeccak256Hash ContractCostType = 14 + ContractCostTypeDecodeEcdsaCurve256Sig ContractCostType = 15 + ContractCostTypeRecoverEcdsaSecp256k1Key ContractCostType = 16 + ContractCostTypeInt256AddSub ContractCostType = 17 + ContractCostTypeInt256Mul ContractCostType = 18 + ContractCostTypeInt256Div ContractCostType = 19 + ContractCostTypeInt256Pow ContractCostType = 20 + ContractCostTypeInt256Shift ContractCostType = 21 + ContractCostTypeChaCha20DrawBytes ContractCostType = 22 + ContractCostTypeParseWasmInstructions ContractCostType = 23 + ContractCostTypeParseWasmFunctions ContractCostType = 24 + ContractCostTypeParseWasmGlobals ContractCostType = 25 + ContractCostTypeParseWasmTableEntries ContractCostType = 26 + ContractCostTypeParseWasmTypes ContractCostType = 27 + ContractCostTypeParseWasmDataSegments ContractCostType = 28 + ContractCostTypeParseWasmElemSegments ContractCostType = 29 + ContractCostTypeParseWasmImports ContractCostType = 30 + ContractCostTypeParseWasmExports ContractCostType = 31 + ContractCostTypeParseWasmDataSegmentBytes ContractCostType = 32 + ContractCostTypeInstantiateWasmInstructions ContractCostType = 33 + ContractCostTypeInstantiateWasmFunctions ContractCostType = 34 + ContractCostTypeInstantiateWasmGlobals ContractCostType = 35 + ContractCostTypeInstantiateWasmTableEntries ContractCostType = 36 + ContractCostTypeInstantiateWasmTypes ContractCostType = 37 + ContractCostTypeInstantiateWasmDataSegments ContractCostType = 38 + ContractCostTypeInstantiateWasmElemSegments ContractCostType = 39 + ContractCostTypeInstantiateWasmImports ContractCostType = 40 + ContractCostTypeInstantiateWasmExports ContractCostType = 41 + ContractCostTypeInstantiateWasmDataSegmentBytes ContractCostType = 42 + ContractCostTypeSec1DecodePointUncompressed ContractCostType = 43 + ContractCostTypeVerifyEcdsaSecp256r1Sig ContractCostType = 44 ) var contractCostTypeMap = map[int32]string{ @@ -55120,7 +56094,7 @@ var contractCostTypeMap = map[int32]string{ 12: "ContractCostTypeVmCachedInstantiation", 13: "ContractCostTypeInvokeVmFunction", 14: "ContractCostTypeComputeKeccak256Hash", - 15: "ContractCostTypeComputeEcdsaSecp256k1Sig", + 15: "ContractCostTypeDecodeEcdsaCurve256Sig", 16: "ContractCostTypeRecoverEcdsaSecp256k1Key", 17: "ContractCostTypeInt256AddSub", 18: "ContractCostTypeInt256Mul", @@ -55128,6 +56102,28 @@ var contractCostTypeMap = map[int32]string{ 20: "ContractCostTypeInt256Pow", 21: "ContractCostTypeInt256Shift", 22: "ContractCostTypeChaCha20DrawBytes", + 23: "ContractCostTypeParseWasmInstructions", + 24: "ContractCostTypeParseWasmFunctions", + 25: "ContractCostTypeParseWasmGlobals", + 26: "ContractCostTypeParseWasmTableEntries", + 27: "ContractCostTypeParseWasmTypes", + 28: "ContractCostTypeParseWasmDataSegments", + 29: "ContractCostTypeParseWasmElemSegments", + 30: "ContractCostTypeParseWasmImports", + 31: "ContractCostTypeParseWasmExports", + 32: "ContractCostTypeParseWasmDataSegmentBytes", + 33: "ContractCostTypeInstantiateWasmInstructions", + 34: "ContractCostTypeInstantiateWasmFunctions", + 35: "ContractCostTypeInstantiateWasmGlobals", + 36: "ContractCostTypeInstantiateWasmTableEntries", + 37: "ContractCostTypeInstantiateWasmTypes", + 38: "ContractCostTypeInstantiateWasmDataSegments", + 39: "ContractCostTypeInstantiateWasmElemSegments", + 40: "ContractCostTypeInstantiateWasmImports", + 41: "ContractCostTypeInstantiateWasmExports", + 42: "ContractCostTypeInstantiateWasmDataSegmentBytes", + 43: "ContractCostTypeSec1DecodePointUncompressed", + 44: "ContractCostTypeVerifyEcdsaSecp256r1Sig", } // ValidEnum validates a proposed value for this enum. Implements From 73233da7c30e3058cec8e7958a93bd5dd419ba85 Mon Sep 17 00:00:00 2001 From: George Date: Tue, 16 Apr 2024 09:23:39 -0700 Subject: [PATCH 099/234] ingest: Make `LedgerTransactionReader` seekable and lazy (#5274) * Adapt LedgerTransactionReader to support seeking and on-the-fly reads * Add a test suite * Update changelog accordingly --- ingest/CHANGELOG.md | 19 ++- ingest/ledger_change_reader.go | 6 +- ingest/ledger_transaction_reader.go | 140 +++++++++++++--------- ingest/ledger_transaction_reader_test.go | 143 +++++++++++++++++++++++ 4 files changed, 249 insertions(+), 59 deletions(-) create mode 100644 ingest/ledger_transaction_reader_test.go diff --git a/ingest/CHANGELOG.md b/ingest/CHANGELOG.md index edd428a62f..1aab4ae542 100644 --- a/ingest/CHANGELOG.md +++ b/ingest/CHANGELOG.md @@ -5,14 +5,27 @@ All notable changes to this project will be documented in this file. This projec ## Unreleased -* Let filewatcher use binary hash instead of timestamp to detect core version update [4050](https://github.com/stellar/go/pull/4050) - ### New Features -* **Performance improvement**: the Captive Core backend now reuses bucket files whenever it finds existing ones in the corresponding `--captive-core-storage-path` (introduced in [v2.0](#v2.0.0)) rather than generating a one-time temporary sub-directory ([#3670](https://github.com/stellar/go/pull/3670)). Note that taking advantage of this feature requires [Stellar-Core v17.1.0](https://github.com/stellar/stellar-core/releases/tag/v17.1.0) or later. +* Support for Soroban and Protocol 20! +* The `LedgerTransactionReader` now has a `Seek(index int)` method to provide reading from arbitrary parts of the ledger [5274](https://github.com/stellar/go/pull/5274). +* `Change` now has a canonical stringification and a set of them is deterministically sortable. +* `NewCompactingChangeReader` will give you a wrapped `ChangeReader` that compacts the changes. +* Let filewatcher use binary hash instead of timestamp to detect core version update [4050](https://github.com/stellar/go/pull/4050). + +### Performance Improvements +* The Captive Core backend now reuses bucket files whenever it finds existing ones in the corresponding `--captive-core-storage-path` (introduced in [v2.0](#v2.0.0)) rather than generating a one-time temporary sub-directory ([#3670](https://github.com/stellar/go/pull/3670)). Note that taking advantage of this feature requires [Stellar-Core v17.1.0](https://github.com/stellar/stellar-core/releases/tag/v17.1.0) or later. +* There have been miscallaneous memory and processing speed improvements. ### Bug Fixes * The Stellar Core runner now parses logs from its underlying subprocess better [#3746](https://github.com/stellar/go/pull/3746). +* Ensures that the underlying Stellar Core is terminated before restarting. +* Backends will now connect with a user agent. +* Better handling of various error and restart scenarios. +### Breaking Changes +* **Captive Core is now the only available backend.** +* The Captive Core configuration should be provided via a TOML file. +* `Change.AccountSignersChanged` has been removed. ## v2.0.0 diff --git a/ingest/ledger_change_reader.go b/ingest/ledger_change_reader.go index a282a04d87..496dc98b40 100644 --- a/ingest/ledger_change_reader.go +++ b/ingest/ledger_change_reader.go @@ -176,7 +176,7 @@ func (r *LedgerChangeReader) Read() (Change, error) { } return r.Read() case evictionChangesState: - entries, err := r.ledgerCloseMeta.EvictedPersistentLedgerEntries() + entries, err := r.lcm.EvictedPersistentLedgerEntries() if err != nil { return Change{}, err } @@ -196,9 +196,9 @@ func (r *LedgerChangeReader) Read() (Change, error) { return r.Read() case upgradeChangesState: // Get upgrade changes - if r.upgradeIndex < len(r.LedgerTransactionReader.ledgerCloseMeta.UpgradesProcessing()) { + if r.upgradeIndex < len(r.LedgerTransactionReader.lcm.UpgradesProcessing()) { changes := GetChangesFromLedgerEntryChanges( - r.LedgerTransactionReader.ledgerCloseMeta.UpgradesProcessing()[r.upgradeIndex].Changes, + r.LedgerTransactionReader.lcm.UpgradesProcessing()[r.upgradeIndex].Changes, ) r.pending = append(r.pending, changes...) r.upgradeIndex++ diff --git a/ingest/ledger_transaction_reader.go b/ingest/ledger_transaction_reader.go index 8199309944..5d2ad1d237 100644 --- a/ingest/ledger_transaction_reader.go +++ b/ingest/ledger_transaction_reader.go @@ -11,17 +11,29 @@ import ( "github.com/stellar/go/xdr" ) -// LedgerTransactionReader reads transactions for a given ledger sequence from a backend. -// Use NewTransactionReader to create a new instance. +var badMetaVersionErr = errors.New( + "TransactionMeta.V=2 is required in protocol version older than version 10. " + + "Please process ledgers again using the latest stellar-core version.", +) + +// LedgerTransactionReader reads transactions for a given ledger sequence from a +// backend. Use NewTransactionReader to create a new instance. type LedgerTransactionReader struct { - ledgerCloseMeta xdr.LedgerCloseMeta - transactions []LedgerTransaction - readIdx int + lcm xdr.LedgerCloseMeta // read-only + envelopesByHash map[xdr.Hash]xdr.TransactionEnvelope // set once + + readIdx int // tracks iteration & seeking } -// NewLedgerTransactionReader creates a new TransactionReader instance. -// Note that TransactionReader is not thread safe and should not be shared by multiple goroutines. -func NewLedgerTransactionReader(ctx context.Context, backend ledgerbackend.LedgerBackend, networkPassphrase string, sequence uint32) (*LedgerTransactionReader, error) { +// NewLedgerTransactionReader creates a new TransactionReader instance. Note +// that TransactionReader is not thread safe and should not be shared by +// multiple goroutines. +func NewLedgerTransactionReader( + ctx context.Context, + backend ledgerbackend.LedgerBackend, + networkPassphrase string, + sequence uint32, +) (*LedgerTransactionReader, error) { ledgerCloseMeta, err := backend.GetLedger(ctx, sequence) if err != nil { return nil, errors.Wrap(err, "error getting ledger from the backend") @@ -30,11 +42,20 @@ func NewLedgerTransactionReader(ctx context.Context, backend ledgerbackend.Ledge return NewLedgerTransactionReaderFromLedgerCloseMeta(networkPassphrase, ledgerCloseMeta) } -// NewLedgerTransactionReaderFromLedgerCloseMeta creates a new TransactionReader instance from xdr.LedgerCloseMeta. -// Note that TransactionReader is not thread safe and should not be shared by multiple goroutines. -func NewLedgerTransactionReaderFromLedgerCloseMeta(networkPassphrase string, ledgerCloseMeta xdr.LedgerCloseMeta) (*LedgerTransactionReader, error) { - reader := &LedgerTransactionReader{ledgerCloseMeta: ledgerCloseMeta} - if err := reader.storeTransactions(ledgerCloseMeta, networkPassphrase); err != nil { +// NewLedgerTransactionReaderFromLedgerCloseMeta creates a new TransactionReader +// instance from xdr.LedgerCloseMeta. Note that TransactionReader is not thread +// safe and should not be shared by multiple goroutines. +func NewLedgerTransactionReaderFromLedgerCloseMeta( + networkPassphrase string, + ledgerCloseMeta xdr.LedgerCloseMeta, +) (*LedgerTransactionReader, error) { + reader := &LedgerTransactionReader{ + lcm: ledgerCloseMeta, + envelopesByHash: make(map[xdr.Hash]xdr.TransactionEnvelope, ledgerCloseMeta.CountTransactions()), + readIdx: 0, + } + + if err := reader.storeTransactions(networkPassphrase); err != nil { return nil, errors.Wrap(err, "error extracting transactions from ledger close meta") } return reader, nil @@ -42,68 +63,81 @@ func NewLedgerTransactionReaderFromLedgerCloseMeta(networkPassphrase string, led // GetSequence returns the sequence number of the ledger data stored by this object. func (reader *LedgerTransactionReader) GetSequence() uint32 { - return reader.ledgerCloseMeta.LedgerSequence() + return reader.lcm.LedgerSequence() } // GetHeader returns the XDR Header data associated with the stored ledger. func (reader *LedgerTransactionReader) GetHeader() xdr.LedgerHeaderHistoryEntry { - return reader.ledgerCloseMeta.LedgerHeaderHistoryEntry() + return reader.lcm.LedgerHeaderHistoryEntry() } // Read returns the next transaction in the ledger, ordered by tx number, each time // it is called. When there are no more transactions to return, an EOF error is returned. func (reader *LedgerTransactionReader) Read() (LedgerTransaction, error) { - if reader.readIdx < len(reader.transactions) { - reader.readIdx++ - return reader.transactions[reader.readIdx-1], nil + if reader.readIdx >= reader.lcm.CountTransactions() { + return LedgerTransaction{}, io.EOF } - return LedgerTransaction{}, io.EOF + i := reader.readIdx + reader.readIdx++ // next read will advance even on error + + hash := reader.lcm.TransactionHash(i) + envelope, ok := reader.envelopesByHash[hash] + if !ok { + hexHash := hex.EncodeToString(hash[:]) + return LedgerTransaction{}, errors.Errorf("unknown tx hash in LedgerCloseMeta: %v", hexHash) + } + + return LedgerTransaction{ + Index: uint32(i + 1), // Transactions start at '1' + Envelope: envelope, + Result: reader.lcm.TransactionResultPair(i), + UnsafeMeta: reader.lcm.TxApplyProcessing(i), + FeeChanges: reader.lcm.FeeProcessing(i), + LedgerVersion: uint32(reader.lcm.LedgerHeaderHistoryEntry().Header.LedgerVersion), + }, nil } // Rewind resets the reader back to the first transaction in the ledger func (reader *LedgerTransactionReader) Rewind() { - reader.readIdx = 0 + reader.Seek(0) +} + +// Seek sets the reader back to a specific transaction in the ledger +func (reader *LedgerTransactionReader) Seek(index int) error { + if index >= reader.lcm.CountTransactions() || index < 0 { + return io.EOF + } + + reader.readIdx = index + return nil } -// storeTransactions maps the close meta data into a slice of LedgerTransaction structs, to provide -// a per-transaction view of the data when Read() is called. -func (reader *LedgerTransactionReader) storeTransactions(lcm xdr.LedgerCloseMeta, networkPassphrase string) error { - byHash := map[xdr.Hash]xdr.TransactionEnvelope{} - for i, tx := range lcm.TransactionEnvelopes() { +// storeHashes creates a mapping between hashes and envelopes in order to +// correctly provide a per-transaction view on-the-fly when Read() is called. +func (reader *LedgerTransactionReader) storeTransactions(networkPassphrase string) error { + // See https://github.com/stellar/go/pull/2720: envelopes in the meta (which + // just come straight from the agreed-upon transaction set) are not in the + // same order as the actual list of metas (which are sorted by hash), so we + // need to hash the envelopes *first* to properly associate them with their + // metas. + for i, tx := range reader.lcm.TransactionEnvelopes() { hash, err := network.HashTransactionInEnvelope(tx, networkPassphrase) if err != nil { return errors.Wrapf(err, "could not hash transaction %d in TxSet", i) } - byHash[hash] = tx - } + reader.envelopesByHash[xdr.Hash(hash)] = tx - for i := 0; i < lcm.CountTransactions(); i++ { - hash := lcm.TransactionHash(i) - envelope, ok := byHash[hash] - if !ok { - hexHash := hex.EncodeToString(hash[:]) - return errors.Errorf("unknown tx hash in LedgerCloseMeta: %v", hexHash) + // We check the version only if FeeProcessing is non-empty, because some + // backends (like HistoryArchiveBackend) do not return meta. + // + // Note that the ordering differences are irrelevant here because all we + // care about is checking every meta for this condition. + if reader.lcm.ProtocolVersion() < 10 && reader.lcm.TxApplyProcessing(i).V < 2 && + len(reader.lcm.FeeProcessing(i)) > 0 { + return badMetaVersionErr } - - // We check the version only if FeeProcessing are non empty because some backends - // (like HistoryArchiveBackend) do not return meta. - if lcm.ProtocolVersion() < 10 && lcm.TxApplyProcessing(i).V < 2 && - len(lcm.FeeProcessing(i)) > 0 { - return errors.New( - "TransactionMeta.V=2 is required in protocol version older than version 10. " + - "Please process ledgers again using the latest stellar-core version.", - ) - } - - reader.transactions = append(reader.transactions, LedgerTransaction{ - Index: uint32(i + 1), // Transactions start at '1' - Envelope: envelope, - Result: lcm.TransactionResultPair(i), - UnsafeMeta: lcm.TxApplyProcessing(i), - FeeChanges: lcm.FeeProcessing(i), - LedgerVersion: uint32(lcm.LedgerHeaderHistoryEntry().Header.LedgerVersion), - }) } + return nil } @@ -111,6 +145,6 @@ func (reader *LedgerTransactionReader) storeTransactions(lcm xdr.LedgerCloseMeta // helpful when there are still some transactions available so reader can stop // streaming them. func (reader *LedgerTransactionReader) Close() error { - reader.transactions = nil + reader.envelopesByHash = nil return nil } diff --git a/ingest/ledger_transaction_reader_test.go b/ingest/ledger_transaction_reader_test.go new file mode 100644 index 0000000000..041f6a9aae --- /dev/null +++ b/ingest/ledger_transaction_reader_test.go @@ -0,0 +1,143 @@ +package ingest + +import ( + "io" + "testing" + + "github.com/stellar/go/keypair" + "github.com/stellar/go/network" + "github.com/stellar/go/support/collections/set" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + passphrase = network.TestNetworkPassphrase + // Test prep: + // - two different envelopes which resolve to two different hashes + // - two basically-empty metas that contain the corresponding hashes + // - a ledger that has 5 txs with metas corresponding to these two envs + // - specifically, in the order [first, first, second, second, second] + // + // This tests both hash <--> envelope mapping and indexed iteration. + txEnvs, txHashes, txMetas = makeTransactions(5) + // barebones LCM structure so that the tx reader works w/o nil derefs, 5 txs + ledgerCloseMeta = xdr.LedgerCloseMeta{V: 1, + V1: &xdr.LedgerCloseMetaV1{ + TxProcessing: txMetas, + TxSet: xdr.GeneralizedTransactionSet{V: 1, + V1TxSet: &xdr.TransactionSetV1{ + Phases: []xdr.TransactionPhase{{V: 0, + V0Components: &[]xdr.TxSetComponent{{ + TxsMaybeDiscountedFee: &xdr.TxSetComponentTxsMaybeDiscountedFee{ + Txs: txEnvs, + }}, + }, + }}, + }, + }, + }, + } +) + +func TestTransactionReader(t *testing.T) { + s := set.NewSet[xdr.Hash](5) + for _, hash := range txHashes { + s.Add(hash) + } + require.Lenf(t, s, len(txHashes), "precondition: hashes aren't unique, envs: %+v", txEnvs) + + // simplest case: read from start + + reader, err := NewLedgerTransactionReaderFromLedgerCloseMeta(passphrase, ledgerCloseMeta) + require.NoError(t, err) + + for i := 0; i < 5; i++ { + tx, ierr := reader.Read() + require.NoError(t, ierr) + assert.EqualValues(t, i+1, tx.Index, "iteration i=%d", i) + + thisHash, ierr := network.HashTransactionInEnvelope(tx.Envelope, passphrase) + require.NoError(t, ierr) + assert.Equal(t, txEnvs[tx.Index-1], tx.Envelope) + assert.Equal(t, txHashes[tx.Index-1], thisHash) + } + _, err = reader.Read() + require.ErrorIs(t, err, io.EOF) + + // start reading from the middle set of txs + + require.NoError(t, reader.Seek(2)) + for i := 0; i < 3; i++ { + tx, ierr := reader.Read() + require.NoError(t, ierr) + assert.EqualValues(t, + /* txIndex is 1-based, iter is 0-based, start at 3rd tx, 5 total */ + 1+(i+2)%5, + tx.Index, + "iteration i=%d", i) + + thisHash, ierr := network.HashTransactionInEnvelope(tx.Envelope, passphrase) + require.NoError(t, ierr) + assert.Equal(t, txEnvs[tx.Index-1], tx.Envelope) + assert.Equal(t, txHashes[tx.Index-1], thisHash) + } + _, err = reader.Read() + require.ErrorIs(t, err, io.EOF) + + // edge case: start from the last tx + require.NoError(t, reader.Seek(4)) + tx, ierr := reader.Read() + require.NoError(t, ierr) + assert.EqualValues(t, 5, tx.Index) + + thisHash, ierr := network.HashTransactionInEnvelope(tx.Envelope, passphrase) + require.NoError(t, ierr) + assert.Equal(t, txEnvs[4], tx.Envelope) + assert.Equal(t, txHashes[4], thisHash) + _, err = reader.Read() + require.ErrorIs(t, err, io.EOF) + + // error case: too far or too close + for _, idx := range []int{-1, 5, 6} { + rdr, err := NewLedgerTransactionReaderFromLedgerCloseMeta(passphrase, ledgerCloseMeta) + require.NoError(t, err) + require.Error(t, rdr.Seek(idx), "no error when trying seek=%d", idx) + } +} + +func makeTransactions(count int) ( + envs []xdr.TransactionEnvelope, + hashes [][32]byte, + metas []xdr.TransactionResultMeta, +) { + seqNum := 123_456 + for i := 0; i < count; i++ { + txEnv := xdr.TransactionEnvelope{ + Type: xdr.EnvelopeTypeEnvelopeTypeTx, + V1: &xdr.TransactionV1Envelope{ + Tx: xdr.Transaction{ + Ext: xdr.TransactionExt{V: 0}, + SourceAccount: xdr.MustMuxedAddress(keypair.MustRandom().Address()), + Operations: []xdr.Operation{}, + Fee: xdr.Uint32(seqNum + i), + SeqNum: xdr.SequenceNumber(seqNum + i), + }, + Signatures: []xdr.DecoratedSignature{}, + }, + } + + txHash, _ := network.HashTransactionInEnvelope(txEnv, passphrase) + txMeta := xdr.TransactionResultMeta{ + Result: xdr.TransactionResultPair{TransactionHash: xdr.Hash(txHash)}, + TxApplyProcessing: xdr.TransactionMeta{V: 3, V3: &xdr.TransactionMetaV3{}}, + } + + envs = append(envs, txEnv) + hashes = append(hashes, txHash) + metas = append(metas, txMeta) + } + + return +} From fd107948e6c4538eb8d51ad9e0f41a8be3440f33 Mon Sep 17 00:00:00 2001 From: George Date: Tue, 16 Apr 2024 15:26:46 -0700 Subject: [PATCH 100/234] xdr: Add wrapper to fetch a ledger's close time (#5279) --- xdr/ledger_close_meta.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/xdr/ledger_close_meta.go b/xdr/ledger_close_meta.go index 2290f3bee1..30e80b2e38 100644 --- a/xdr/ledger_close_meta.go +++ b/xdr/ledger_close_meta.go @@ -19,6 +19,10 @@ func (l LedgerCloseMeta) LedgerSequence() uint32 { return uint32(l.LedgerHeaderHistoryEntry().Header.LedgerSeq) } +func (l LedgerCloseMeta) LedgerCloseTime() int64 { + return int64(l.LedgerHeaderHistoryEntry().Header.ScpValue.CloseTime) +} + func (l LedgerCloseMeta) LedgerHash() Hash { return l.LedgerHeaderHistoryEntry().Hash } From 36d7a6cff86a320eb86b17aed8e13f71101de3e2 Mon Sep 17 00:00:00 2001 From: George Date: Wed, 17 Apr 2024 11:21:01 -0700 Subject: [PATCH 101/234] services/horizon: Make reaping batch sizes configurable via `--history-retention-reap-count`. (#5272) * Make reaper batch size configurable * Add --history-retention-reap-count and change default to 50k --- services/horizon/internal/app.go | 6 ++++- services/horizon/internal/config.go | 6 +++++ .../horizon/internal/db2/history/reap_test.go | 3 ++- services/horizon/internal/flags.go | 16 +++++++++++- services/horizon/internal/httpt_test.go | 1 + services/horizon/internal/reap/main.go | 11 +++++--- services/horizon/internal/reap/system.go | 25 ++++++++++++------- services/horizon/internal/reap/system_test.go | 2 +- 8 files changed, 53 insertions(+), 17 deletions(-) diff --git a/services/horizon/internal/app.go b/services/horizon/internal/app.go index 8fb86b5f54..927cffa773 100644 --- a/services/horizon/internal/app.go +++ b/services/horizon/internal/app.go @@ -506,7 +506,11 @@ func (a *App) init() error { initSubmissionSystem(a) // reaper - a.reaper = reap.New(a.config.HistoryRetentionCount, a.HorizonSession(), a.ledgerState) + a.reaper = reap.New( + a.config.HistoryRetentionCount, + a.config.HistoryRetentionReapCount, + a.HorizonSession(), + a.ledgerState) // go metrics initGoMetrics(a) diff --git a/services/horizon/internal/config.go b/services/horizon/internal/config.go index 94b6f5514b..4c8a45514f 100644 --- a/services/horizon/internal/config.go +++ b/services/horizon/internal/config.go @@ -75,6 +75,12 @@ type Config struct { // determining a "retention duration", each ledger roughly corresponds to 10 // seconds of real time. HistoryRetentionCount uint + // HistoryRetentionReapCount is the number of ledgers worth of history data + // to remove per second from the Horizon database. It is intended to allow + // control over the amount of CPU and database load caused by reaping, + // especially if enabling reaping for the first time or in times of + // increased ledger load. + HistoryRetentionReapCount uint // StaleThreshold represents the number of ledgers a history database may be // out-of-date by before horizon begins to respond with an error to history // requests. diff --git a/services/horizon/internal/db2/history/reap_test.go b/services/horizon/internal/db2/history/reap_test.go index b6ed16c76b..6aa271e483 100644 --- a/services/horizon/internal/db2/history/reap_test.go +++ b/services/horizon/internal/db2/history/reap_test.go @@ -17,7 +17,7 @@ func TestReapLookupTables(t *testing.T) { db := tt.HorizonSession() - sys := reap.New(0, db, ledgerState) + sys := reap.New(0, 0, db, ledgerState) var ( prevLedgers, curLedgers int @@ -43,6 +43,7 @@ func TestReapLookupTables(t *testing.T) { ledgerState.SetStatus(tt.LoadLedgerStatus()) sys.RetentionCount = 1 + sys.RetentionBatch = 50 err := sys.DeleteUnretainedHistory(tt.Ctx) tt.Require.NoError(err) diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index 54e76fbc56..774140bb53 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -654,9 +654,23 @@ func Flags() (*Config, support.ConfigOptions) { ConfigKey: &config.HistoryRetentionCount, OptType: types.Uint, FlagDefault: uint(0), - Usage: "the minimum number of ledgers to maintain within horizon's history tables. 0 signifies an unlimited number of ledgers will be retained", + Usage: "the minimum number of ledgers to maintain within Horizon's history tables (0 = retain an unlimited number of ledgers)", UsedInCommands: IngestionCommands, }, + &support.ConfigOption{ + Name: "history-retention-reap-count", + ConfigKey: &config.HistoryRetentionReapCount, + OptType: types.Uint, + FlagDefault: uint(50_000), + Usage: "the batch size (in ledgers) to remove per reap from the Horizon database", + UsedInCommands: IngestionCommands, + CustomSetValue: func(opt *support.ConfigOption) error { + if val := viper.GetUint(opt.Name); val <= 0 || val > 500_000 { + return fmt.Errorf("flag --history-retention-reap-count must be in range [1, 500,000]") + } + return nil + }, + }, &support.ConfigOption{ Name: "history-stale-threshold", ConfigKey: &config.StaleThreshold, diff --git a/services/horizon/internal/httpt_test.go b/services/horizon/internal/httpt_test.go index 7ac8525d21..587d401d8a 100644 --- a/services/horizon/internal/httpt_test.go +++ b/services/horizon/internal/httpt_test.go @@ -104,6 +104,7 @@ func (ht *HTTPT) Post( // setting the retention count to the provided number. func (ht *HTTPT) ReapHistory(retention uint) { ht.App.reaper.RetentionCount = retention + ht.App.reaper.RetentionBatch = 50_000 ht.App.reaper.HistoryQ = &history.Q{ht.HorizonSession()} err := ht.App.DeleteUnretainedHistory(context.Background()) ht.Require.NoError(err) diff --git a/services/horizon/internal/reap/main.go b/services/horizon/internal/reap/main.go index bcf71ecf87..7d462e7e52 100644 --- a/services/horizon/internal/reap/main.go +++ b/services/horizon/internal/reap/main.go @@ -16,19 +16,22 @@ import ( type System struct { HistoryQ *history.Q RetentionCount uint - ledgerState *ledger.State - ctx context.Context - cancel context.CancelFunc + RetentionBatch uint + + ledgerState *ledger.State + ctx context.Context + cancel context.CancelFunc } // New initializes the reaper, causing it to begin polling the stellar-core // database for now ledgers and ingesting data into the horizon database. -func New(retention uint, dbSession db.SessionInterface, ledgerState *ledger.State) *System { +func New(retention, retentionBatchSize uint, dbSession db.SessionInterface, ledgerState *ledger.State) *System { ctx, cancel := context.WithCancel(context.Background()) r := &System{ HistoryQ: &history.Q{dbSession.Clone()}, RetentionCount: retention, + RetentionBatch: retentionBatchSize, ledgerState: ledgerState, ctx: ctx, cancel: cancel, diff --git a/services/horizon/internal/reap/system.go b/services/horizon/internal/reap/system.go index e5eeb5cd43..f08dae37a7 100644 --- a/services/horizon/internal/reap/system.go +++ b/services/horizon/internal/reap/system.go @@ -2,6 +2,7 @@ package reap import ( "context" + "fmt" "time" herrors "github.com/stellar/go/services/horizon/internal/errors" @@ -70,25 +71,31 @@ func (r *System) runOnce(ctx context.Context) { } } -// Work backwards in 100k ledger blocks to prevent using all the CPU. +// Work backwards in 50k (by default, otherwise configurable via the CLI) ledger +// blocks to prevent using all the CPU. // -// This runs every hour, so we need to make sure it doesn't -// run for longer than an hour. +// This runs every hour, so we need to make sure it doesn't run for longer than +// an hour. // -// Current ledger at 2021-08-12 is 36,827,497, so 100k means 368 batches. At 1 -// batch/second, that seems like a reasonable balance between running well -// under an hour, and slowing it down enough to leave some CPU for other -// processes. -var batchSize = int32(100_000) +// Current ledger at 2024-04-04s is 51,092,283, so 50k means 1021 batches. At 1 +// batch/second, that seems like a reasonable balance between running under an +// hour, and slowing it down enough to leave some CPU for other processes. var sleep = 1 * time.Second func (r *System) clearBefore(ctx context.Context, startSeq, endSeq int32) error { + batchSize := int32(r.RetentionBatch) + if batchSize <= 0 { + return fmt.Errorf("invalid batch size for reaping (%d)", batchSize) + } + for batchEndSeq := endSeq - 1; batchEndSeq >= startSeq; batchEndSeq -= batchSize { batchStartSeq := batchEndSeq - batchSize if batchStartSeq < startSeq { batchStartSeq = startSeq } - log.WithField("start_ledger", batchStartSeq).WithField("end_ledger", batchEndSeq).Info("reaper: clearing") + log.WithField("start_ledger", batchStartSeq). + WithField("end_ledger", batchEndSeq). + Info("reaper: clearing") batchStart, batchEnd, err := toid.LedgerRangeInclusive(batchStartSeq, batchEndSeq) if err != nil { diff --git a/services/horizon/internal/reap/system_test.go b/services/horizon/internal/reap/system_test.go index 7d5d5a70b2..1595171d89 100644 --- a/services/horizon/internal/reap/system_test.go +++ b/services/horizon/internal/reap/system_test.go @@ -15,7 +15,7 @@ func TestDeleteUnretainedHistory(t *testing.T) { db := tt.HorizonSession() - sys := New(0, db, ledgerState) + sys := New(0, 50, db, ledgerState) // Disable sleeps for this. sleep = 0 From c63ad05d3e3bc9f2dd5f5747bfa2542da714b04e Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Wed, 17 Apr 2024 21:24:51 +0200 Subject: [PATCH 102/234] Bump XDR definitions again (#5282) --- Makefile | 2 +- gxdr/xdr_generated.go | 48 +++++++++++++- xdr/Stellar-contract-spec.x | 8 +++ xdr/xdr_commit_generated.txt | 2 +- xdr/xdr_generated.go | 120 ++++++++++++++++++++++++++++++++++- 5 files changed, 176 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 8ac1abb337..fe2021d39a 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ XDRS = $(DOWNLOADABLE_XDRS) xdr/Stellar-lighthorizon.x \ XDRGEN_COMMIT=e2cac557162d99b12ae73b846cf3d5bfe16636de -XDR_COMMIT=59062438237d5f77fd6feb060b76288e88b7e222 +XDR_COMMIT=2ba4049554bb0564950e6d9213e01a60fc190f54 .PHONY: xdr xdr-clean xdr-update diff --git a/gxdr/xdr_generated.go b/gxdr/xdr_generated.go index e7c8f3ed58..214d405e80 100644 --- a/gxdr/xdr_generated.go +++ b/gxdr/xdr_generated.go @@ -3804,6 +3804,7 @@ const ( SC_SPEC_TYPE_MAP SCSpecType = 1004 SC_SPEC_TYPE_TUPLE SCSpecType = 1005 SC_SPEC_TYPE_BYTES_N SCSpecType = 1006 + SC_SPEC_TYPE_HASH SCSpecType = 1007 // User defined types. SC_SPEC_TYPE_UDT SCSpecType = 2000 ) @@ -3834,6 +3835,10 @@ type SCSpecTypeBytesN struct { N Uint32 } +type SCSpectTypeHash struct { + N Uint32 +} + type SCSpecTypeUDT struct { Name string // bound 60 } @@ -3854,6 +3859,8 @@ type SCSpecTypeDef struct { // Tuple() *SCSpecTypeTuple // SC_SPEC_TYPE_BYTES_N: // BytesN() *SCSpecTypeBytesN + // SC_SPEC_TYPE_HASH: + // Hash() *SCSpectTypeHash // SC_SPEC_TYPE_UDT: // Udt() *SCSpecTypeUDT Type SCSpecType @@ -25624,6 +25631,7 @@ var _XdrNames_SCSpecType = map[int32]string{ int32(SC_SPEC_TYPE_MAP): "SC_SPEC_TYPE_MAP", int32(SC_SPEC_TYPE_TUPLE): "SC_SPEC_TYPE_TUPLE", int32(SC_SPEC_TYPE_BYTES_N): "SC_SPEC_TYPE_BYTES_N", + int32(SC_SPEC_TYPE_HASH): "SC_SPEC_TYPE_HASH", int32(SC_SPEC_TYPE_UDT): "SC_SPEC_TYPE_UDT", } var _XdrValues_SCSpecType = map[string]int32{ @@ -25651,6 +25659,7 @@ var _XdrValues_SCSpecType = map[string]int32{ "SC_SPEC_TYPE_MAP": int32(SC_SPEC_TYPE_MAP), "SC_SPEC_TYPE_TUPLE": int32(SC_SPEC_TYPE_TUPLE), "SC_SPEC_TYPE_BYTES_N": int32(SC_SPEC_TYPE_BYTES_N), + "SC_SPEC_TYPE_HASH": int32(SC_SPEC_TYPE_HASH), "SC_SPEC_TYPE_UDT": int32(SC_SPEC_TYPE_UDT), } @@ -25843,6 +25852,20 @@ func (v *SCSpecTypeBytesN) XdrRecurse(x XDR, name string) { } func XDR_SCSpecTypeBytesN(v *SCSpecTypeBytesN) *SCSpecTypeBytesN { return v } +type XdrType_SCSpectTypeHash = *SCSpectTypeHash + +func (v *SCSpectTypeHash) XdrPointer() interface{} { return v } +func (SCSpectTypeHash) XdrTypeName() string { return "SCSpectTypeHash" } +func (v SCSpectTypeHash) XdrValue() interface{} { return v } +func (v *SCSpectTypeHash) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *SCSpectTypeHash) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%sn", name), XDR_Uint32(&v.N)) +} +func XDR_SCSpectTypeHash(v *SCSpectTypeHash) *SCSpectTypeHash { return v } + type XdrType_SCSpecTypeUDT = *SCSpecTypeUDT func (v *SCSpecTypeUDT) XdrPointer() interface{} { return v } @@ -25882,6 +25905,7 @@ var _XdrTags_SCSpecTypeDef = map[int32]bool{ XdrToI32(SC_SPEC_TYPE_MAP): true, XdrToI32(SC_SPEC_TYPE_TUPLE): true, XdrToI32(SC_SPEC_TYPE_BYTES_N): true, + XdrToI32(SC_SPEC_TYPE_HASH): true, XdrToI32(SC_SPEC_TYPE_UDT): true, } @@ -25978,6 +26002,21 @@ func (u *SCSpecTypeDef) BytesN() *SCSpecTypeBytesN { return nil } } +func (u *SCSpecTypeDef) Hash() *SCSpectTypeHash { + switch u.Type { + case SC_SPEC_TYPE_HASH: + if v, ok := u._u.(*SCSpectTypeHash); ok { + return v + } else { + var zero SCSpectTypeHash + u._u = &zero + return &zero + } + default: + XdrPanic("SCSpecTypeDef.Hash accessed when Type == %v", u.Type) + return nil + } +} func (u *SCSpecTypeDef) Udt() *SCSpecTypeUDT { switch u.Type { case SC_SPEC_TYPE_UDT: @@ -25995,7 +26034,7 @@ func (u *SCSpecTypeDef) Udt() *SCSpecTypeUDT { } func (u SCSpecTypeDef) XdrValid() bool { switch u.Type { - case SC_SPEC_TYPE_VAL, SC_SPEC_TYPE_BOOL, SC_SPEC_TYPE_VOID, SC_SPEC_TYPE_ERROR, SC_SPEC_TYPE_U32, SC_SPEC_TYPE_I32, SC_SPEC_TYPE_U64, SC_SPEC_TYPE_I64, SC_SPEC_TYPE_TIMEPOINT, SC_SPEC_TYPE_DURATION, SC_SPEC_TYPE_U128, SC_SPEC_TYPE_I128, SC_SPEC_TYPE_U256, SC_SPEC_TYPE_I256, SC_SPEC_TYPE_BYTES, SC_SPEC_TYPE_STRING, SC_SPEC_TYPE_SYMBOL, SC_SPEC_TYPE_ADDRESS, SC_SPEC_TYPE_OPTION, SC_SPEC_TYPE_RESULT, SC_SPEC_TYPE_VEC, SC_SPEC_TYPE_MAP, SC_SPEC_TYPE_TUPLE, SC_SPEC_TYPE_BYTES_N, SC_SPEC_TYPE_UDT: + case SC_SPEC_TYPE_VAL, SC_SPEC_TYPE_BOOL, SC_SPEC_TYPE_VOID, SC_SPEC_TYPE_ERROR, SC_SPEC_TYPE_U32, SC_SPEC_TYPE_I32, SC_SPEC_TYPE_U64, SC_SPEC_TYPE_I64, SC_SPEC_TYPE_TIMEPOINT, SC_SPEC_TYPE_DURATION, SC_SPEC_TYPE_U128, SC_SPEC_TYPE_I128, SC_SPEC_TYPE_U256, SC_SPEC_TYPE_I256, SC_SPEC_TYPE_BYTES, SC_SPEC_TYPE_STRING, SC_SPEC_TYPE_SYMBOL, SC_SPEC_TYPE_ADDRESS, SC_SPEC_TYPE_OPTION, SC_SPEC_TYPE_RESULT, SC_SPEC_TYPE_VEC, SC_SPEC_TYPE_MAP, SC_SPEC_TYPE_TUPLE, SC_SPEC_TYPE_BYTES_N, SC_SPEC_TYPE_HASH, SC_SPEC_TYPE_UDT: return true } return false @@ -26022,6 +26061,8 @@ func (u *SCSpecTypeDef) XdrUnionBody() XdrType { return XDR_SCSpecTypeTuple(u.Tuple()) case SC_SPEC_TYPE_BYTES_N: return XDR_SCSpecTypeBytesN(u.BytesN()) + case SC_SPEC_TYPE_HASH: + return XDR_SCSpectTypeHash(u.Hash()) case SC_SPEC_TYPE_UDT: return XDR_SCSpecTypeUDT(u.Udt()) } @@ -26043,6 +26084,8 @@ func (u *SCSpecTypeDef) XdrUnionBodyName() string { return "Tuple" case SC_SPEC_TYPE_BYTES_N: return "BytesN" + case SC_SPEC_TYPE_HASH: + return "Hash" case SC_SPEC_TYPE_UDT: return "Udt" } @@ -26081,6 +26124,9 @@ func (u *SCSpecTypeDef) XdrRecurse(x XDR, name string) { case SC_SPEC_TYPE_BYTES_N: x.Marshal(x.Sprintf("%sbytesN", name), XDR_SCSpecTypeBytesN(u.BytesN())) return + case SC_SPEC_TYPE_HASH: + x.Marshal(x.Sprintf("%shash", name), XDR_SCSpectTypeHash(u.Hash())) + return case SC_SPEC_TYPE_UDT: x.Marshal(x.Sprintf("%sudt", name), XDR_SCSpecTypeUDT(u.Udt())) return diff --git a/xdr/Stellar-contract-spec.x b/xdr/Stellar-contract-spec.x index 6988a63385..5d3e029951 100644 --- a/xdr/Stellar-contract-spec.x +++ b/xdr/Stellar-contract-spec.x @@ -42,6 +42,7 @@ enum SCSpecType SC_SPEC_TYPE_MAP = 1004, SC_SPEC_TYPE_TUPLE = 1005, SC_SPEC_TYPE_BYTES_N = 1006, + SC_SPEC_TYPE_HASH = 1007, // User defined types. SC_SPEC_TYPE_UDT = 2000 @@ -79,6 +80,11 @@ struct SCSpecTypeBytesN uint32 n; }; +struct SCSpectTypeHash +{ + uint32 n; +}; + struct SCSpecTypeUDT { string name<60>; @@ -117,6 +123,8 @@ case SC_SPEC_TYPE_TUPLE: SCSpecTypeTuple tuple; case SC_SPEC_TYPE_BYTES_N: SCSpecTypeBytesN bytesN; +case SC_SPEC_TYPE_HASH: + SCSpectTypeHash hash; case SC_SPEC_TYPE_UDT: SCSpecTypeUDT udt; }; diff --git a/xdr/xdr_commit_generated.txt b/xdr/xdr_commit_generated.txt index 610c71d294..8c75a203d4 100644 --- a/xdr/xdr_commit_generated.txt +++ b/xdr/xdr_commit_generated.txt @@ -1 +1 @@ -59062438237d5f77fd6feb060b76288e88b7e222 \ No newline at end of file +2ba4049554bb0564950e6d9213e01a60fc190f54 \ No newline at end of file diff --git a/xdr/xdr_generated.go b/xdr/xdr_generated.go index b336714562..49f8c691a3 100644 --- a/xdr/xdr_generated.go +++ b/xdr/xdr_generated.go @@ -37,7 +37,7 @@ var XdrFilesSHA256 = map[string]string{ "xdr/Stellar-contract-config-setting.x": "393369678663cb0f9471a0b69e2a9cfa3ac93c4415fa40cec166e9a231ecbe0d", "xdr/Stellar-contract-env-meta.x": "928a30de814ee589bc1d2aadd8dd81c39f71b7e6f430f56974505ccb1f49654b", "xdr/Stellar-contract-meta.x": "f01532c11ca044e19d9f9f16fe373e9af64835da473be556b9a807ee3319ae0d", - "xdr/Stellar-contract-spec.x": "c7ffa21d2e91afb8e666b33524d307955426ff553a486d670c29217ed9888d49", + "xdr/Stellar-contract-spec.x": "8d7f6bdd82c3e529cd8c6f035202ca0e7677cc05e4727492a165dfdc51a9cb3e", "xdr/Stellar-contract.x": "7f665e4103e146a88fcdabce879aaaacd3bf9283feb194cc47ff986264c1e315", "xdr/Stellar-exporter.x": "a00c83d02e8c8382e06f79a191f1fb5abd097a4bbcab8481c67467e3270e0529", "xdr/Stellar-internal.x": "227835866c1b2122d1eaf28839ba85ea7289d1cb681dda4ca619c2da3d71fe00", @@ -48095,6 +48095,7 @@ const ScSpecDocLimit = 1024 // SC_SPEC_TYPE_MAP = 1004, // SC_SPEC_TYPE_TUPLE = 1005, // SC_SPEC_TYPE_BYTES_N = 1006, +// SC_SPEC_TYPE_HASH = 1007, // // // User defined types. // SC_SPEC_TYPE_UDT = 2000 @@ -48126,6 +48127,7 @@ const ( ScSpecTypeScSpecTypeMap ScSpecType = 1004 ScSpecTypeScSpecTypeTuple ScSpecType = 1005 ScSpecTypeScSpecTypeBytesN ScSpecType = 1006 + ScSpecTypeScSpecTypeHash ScSpecType = 1007 ScSpecTypeScSpecTypeUdt ScSpecType = 2000 ) @@ -48154,6 +48156,7 @@ var scSpecTypeMap = map[int32]string{ 1004: "ScSpecTypeScSpecTypeMap", 1005: "ScSpecTypeScSpecTypeTuple", 1006: "ScSpecTypeScSpecTypeBytesN", + 1007: "ScSpecTypeScSpecTypeHash", 2000: "ScSpecTypeScSpecTypeUdt", } @@ -48659,6 +48662,71 @@ func (s ScSpecTypeBytesN) xdrType() {} var _ xdrType = (*ScSpecTypeBytesN)(nil) +// ScSpectTypeHash is an XDR Struct defines as: +// +// struct SCSpectTypeHash +// { +// uint32 n; +// }; +type ScSpectTypeHash struct { + N Uint32 +} + +// EncodeTo encodes this value using the Encoder. +func (s *ScSpectTypeHash) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.N.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*ScSpectTypeHash)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *ScSpectTypeHash) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding ScSpectTypeHash: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.N.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s ScSpectTypeHash) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *ScSpectTypeHash) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*ScSpectTypeHash)(nil) + _ encoding.BinaryUnmarshaler = (*ScSpectTypeHash)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s ScSpectTypeHash) xdrType() {} + +var _ xdrType = (*ScSpectTypeHash)(nil) + // ScSpecTypeUdt is an XDR Struct defines as: // // struct SCSpecTypeUDT @@ -48759,6 +48827,8 @@ var _ xdrType = (*ScSpecTypeUdt)(nil) // SCSpecTypeTuple tuple; // case SC_SPEC_TYPE_BYTES_N: // SCSpecTypeBytesN bytesN; +// case SC_SPEC_TYPE_HASH: +// SCSpectTypeHash hash; // case SC_SPEC_TYPE_UDT: // SCSpecTypeUDT udt; // }; @@ -48770,6 +48840,7 @@ type ScSpecTypeDef struct { Map *ScSpecTypeMap Tuple *ScSpecTypeTuple BytesN *ScSpecTypeBytesN + Hash *ScSpectTypeHash Udt *ScSpecTypeUdt } @@ -48831,6 +48902,8 @@ func (u ScSpecTypeDef) ArmForSwitch(sw int32) (string, bool) { return "Tuple", true case ScSpecTypeScSpecTypeBytesN: return "BytesN", true + case ScSpecTypeScSpecTypeHash: + return "Hash", true case ScSpecTypeScSpecTypeUdt: return "Udt", true } @@ -48919,6 +48992,13 @@ func NewScSpecTypeDef(aType ScSpecType, value interface{}) (result ScSpecTypeDef return } result.BytesN = &tv + case ScSpecTypeScSpecTypeHash: + tv, ok := value.(ScSpectTypeHash) + if !ok { + err = errors.New("invalid value, must be ScSpectTypeHash") + return + } + result.Hash = &tv case ScSpecTypeScSpecTypeUdt: tv, ok := value.(ScSpecTypeUdt) if !ok { @@ -49080,6 +49160,31 @@ func (u ScSpecTypeDef) GetBytesN() (result ScSpecTypeBytesN, ok bool) { return } +// MustHash retrieves the Hash value from the union, +// panicing if the value is not set. +func (u ScSpecTypeDef) MustHash() ScSpectTypeHash { + val, ok := u.GetHash() + + if !ok { + panic("arm Hash is not set") + } + + return val +} + +// GetHash retrieves the Hash value from the union, +// returning ok if the union's switch indicated the value is valid. +func (u ScSpecTypeDef) GetHash() (result ScSpectTypeHash, ok bool) { + armName, _ := u.ArmForSwitch(int32(u.Type)) + + if armName == "Hash" { + result = *u.Hash + ok = true + } + + return +} + // MustUdt retrieves the Udt value from the union, // panicing if the value is not set. func (u ScSpecTypeDef) MustUdt() ScSpecTypeUdt { @@ -49196,6 +49301,11 @@ func (u ScSpecTypeDef) EncodeTo(e *xdr.Encoder) error { return err } return nil + case ScSpecTypeScSpecTypeHash: + if err = (*u.Hash).EncodeTo(e); err != nil { + return err + } + return nil case ScSpecTypeScSpecTypeUdt: if err = (*u.Udt).EncodeTo(e); err != nil { return err @@ -49323,6 +49433,14 @@ func (u *ScSpecTypeDef) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding ScSpecTypeBytesN: %w", err) } return n, nil + case ScSpecTypeScSpecTypeHash: + u.Hash = new(ScSpectTypeHash) + nTmp, err = (*u.Hash).DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding ScSpectTypeHash: %w", err) + } + return n, nil case ScSpecTypeScSpecTypeUdt: u.Udt = new(ScSpecTypeUdt) nTmp, err = (*u.Udt).DecodeFrom(d, maxDepth) From 12bbc681b7dd6d54a47658c5a62c97dfd05a0a9c Mon Sep 17 00:00:00 2001 From: tamirms Date: Thu, 18 Apr 2024 10:51:27 +0100 Subject: [PATCH 103/234] ingest: Verify the bucket list hash in CheckpointChangeReader (#5280) --- ingest/checkpoint_change_reader.go | 25 +++++++++ ingest/mock_change_reader.go | 7 +++ .../internal/ingest/db_integration_test.go | 53 +++++++++++++------ services/horizon/internal/ingest/fsm.go | 9 ++-- .../ingest/history_archive_adapter.go | 29 +++------- .../ingest/history_archive_adapter_test.go | 10 ++-- services/horizon/internal/ingest/main.go | 5 +- services/horizon/internal/ingest/main_test.go | 10 ++-- .../internal/ingest/processor_runner.go | 35 +++--------- .../internal/ingest/processor_runner_test.go | 5 +- services/horizon/internal/ingest/verify.go | 11 +++- .../ingest/verify_range_state_test.go | 12 +++-- .../horizon/internal/ingest/verify_test.go | 43 ++++++++++++++- 13 files changed, 163 insertions(+), 91 deletions(-) diff --git a/ingest/checkpoint_change_reader.go b/ingest/checkpoint_change_reader.go index 1dbbed2a9e..e84e7631cf 100644 --- a/ingest/checkpoint_change_reader.go +++ b/ingest/checkpoint_change_reader.go @@ -1,7 +1,9 @@ package ingest import ( + "bytes" "context" + "fmt" "io" "sync" "time" @@ -97,6 +99,29 @@ func NewCheckpointChangeReader( }, nil } +// VerifyBucketList verifies that the bucket list hash computed from the history archive snapshot +// associated with the CheckpointChangeReader matches the expectedHash. +// Assuming expectedHash comes from a trusted source (captive-core running in unbounded mode), this +// check will give you full security that the data returned by the CheckpointChangeReader can be trusted. +// Note that XdrStream will verify all the ledger entries from an individual bucket and +// VerifyBucketList() verifies the entire list of bucket hashes. +func (r *CheckpointChangeReader) VerifyBucketList(expectedHash xdr.Hash) error { + historyBucketListHash, err := r.has.BucketListHash() + if err != nil { + return errors.Wrap(err, "Error getting bucket list hash") + } + + if !bytes.Equal(historyBucketListHash[:], expectedHash[:]) { + return fmt.Errorf( + "bucket list hash of history archive does not match expected hash: %#x %#x", + historyBucketListHash, + expectedHash, + ) + } + + return nil +} + func (r *CheckpointChangeReader) bucketExists(hash historyarchive.Hash) (bool, error) { return r.archive.BucketExists(hash) } diff --git a/ingest/mock_change_reader.go b/ingest/mock_change_reader.go index c70d78d397..8616b86a04 100644 --- a/ingest/mock_change_reader.go +++ b/ingest/mock_change_reader.go @@ -2,6 +2,8 @@ package ingest import ( "github.com/stretchr/testify/mock" + + "github.com/stellar/go/xdr" ) var _ ChangeReader = (*MockChangeReader)(nil) @@ -19,3 +21,8 @@ func (m *MockChangeReader) Close() error { args := m.Called() return args.Error(0) } + +func (m *MockChangeReader) VerifyBucketList(expectedHash xdr.Hash) error { + args := m.Called(expectedHash) + return args.Error(0) +} diff --git a/services/horizon/internal/ingest/db_integration_test.go b/services/horizon/internal/ingest/db_integration_test.go index 60a45f158e..606cd9fb2b 100644 --- a/services/horizon/internal/ingest/db_integration_test.go +++ b/services/horizon/internal/ingest/db_integration_test.go @@ -3,22 +3,29 @@ package ingest import ( + "bytes" "context" + "fmt" "io" "io/ioutil" "path/filepath" "testing" + "github.com/stretchr/testify/suite" + "github.com/stellar/go/ingest" "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/services/horizon/internal/test" "github.com/stellar/go/xdr" - "github.com/stretchr/testify/suite" ) -type memoryChangeReader xdr.LedgerEntryChanges +type memoryChangeReader struct { + changes xdr.LedgerEntryChanges + bucketListHash xdr.Hash + verified bool +} -func loadChanges(path string) (*memoryChangeReader, error) { +func loadChanges(bucketListHash xdr.Hash, path string) (*memoryChangeReader, error) { contents, err := ioutil.ReadFile(path) if err != nil { return nil, err @@ -29,18 +36,19 @@ func loadChanges(path string) (*memoryChangeReader, error) { return nil, err } - reader := memoryChangeReader(entryChanges) - return &reader, nil + return &memoryChangeReader{ + changes: entryChanges, + bucketListHash: bucketListHash, + }, nil } func (r *memoryChangeReader) Read() (ingest.Change, error) { - entryChanges := *r - if len(entryChanges) == 0 { + if len(r.changes) == 0 { return ingest.Change{}, io.EOF } - change := entryChanges[0] - *r = entryChanges[1:] + change := r.changes[0] + r.changes = r.changes[1:] return ingest.Change{ Type: change.State.Data.Type, Post: change.State, @@ -48,6 +56,18 @@ func (r *memoryChangeReader) Read() (ingest.Change, error) { }, nil } +func (r *memoryChangeReader) VerifyBucketList(expectedHash xdr.Hash) error { + if !bytes.Equal(r.bucketListHash[:], expectedHash[:]) { + return fmt.Errorf( + "bucket list hash of history archive does not match expected hash: %#x %#x", + r.bucketListHash, + expectedHash, + ) + } + r.verified = true + return nil +} + func (r *memoryChangeReader) Close() error { return nil } @@ -61,6 +81,7 @@ type DBTestSuite struct { ctx context.Context sampleFile string sequence uint32 + checkpointHash xdr.Hash ledgerBackend *ledgerbackend.MockDatabaseBackend historyAdapter *mockHistoryArchiveAdapter system *system @@ -76,7 +97,7 @@ func (s *DBTestSuite) SetupTest() { // go test -v -timeout 5m --tags=update github.com/stellar/go/services/horizon/internal/ingest -run "^(TestUpdateSampleChanges)$" // and commit the new file to the git repo. s.sampleFile = filepath.Join("testdata", "sample-changes.xdr") - + s.checkpointHash = xdr.Hash{1, 2, 3} s.ledgerBackend = &ledgerbackend.MockDatabaseBackend{} s.historyAdapter = &mockHistoryArchiveAdapter{} var err error @@ -99,18 +120,18 @@ func (s *DBTestSuite) SetupTest() { } func (s *DBTestSuite) mockChangeReader() { - changeReader, err := loadChanges(s.sampleFile) + changeReader, err := loadChanges(s.checkpointHash, s.sampleFile) s.Assert().NoError(err) + s.T().Cleanup(func() { + s.tt.Assert.True(changeReader.verified) + }) s.historyAdapter.On("GetState", s.ctx, s.sequence). Return(ingest.ChangeReader(changeReader), nil).Once() } func (s *DBTestSuite) setupMocksForBuildState() { - checkpointHash := xdr.Hash{1, 2, 3} s.historyAdapter.On("GetLatestLedgerSequence"). Return(s.sequence, nil).Once() s.mockChangeReader() - s.historyAdapter.On("BucketListHash", s.sequence). - Return(checkpointHash, nil).Once() s.ledgerBackend.On("IsPrepared", s.ctx, ledgerbackend.UnboundedRange(s.sequence)).Return(true, nil).Once() s.ledgerBackend.On("GetLedger", s.ctx, s.sequence). @@ -120,7 +141,7 @@ func (s *DBTestSuite) setupMocksForBuildState() { LedgerHeader: xdr.LedgerHeaderHistoryEntry{ Header: xdr.LedgerHeader{ LedgerSeq: xdr.Uint32(s.sequence), - BucketListHash: checkpointHash, + BucketListHash: s.checkpointHash, }, }, }, @@ -148,7 +169,7 @@ func (s *DBTestSuite) TestBuildState() { s.Assert().Equal(s.sequence, resume.latestSuccessfullyProcessedLedger) s.mockChangeReader() - s.Assert().NoError(s.system.verifyState(false)) + s.Assert().NoError(s.system.verifyState(false, s.sequence, s.checkpointHash)) } func (s *DBTestSuite) TestVersionMismatchTriggersRebuild() { diff --git a/services/horizon/internal/ingest/fsm.go b/services/horizon/internal/ingest/fsm.go index 3ce02864c6..5dce974e35 100644 --- a/services/horizon/internal/ingest/fsm.go +++ b/services/horizon/internal/ingest/fsm.go @@ -551,7 +551,7 @@ func (r resumeState) run(s *system) (transition, error) { localLog.Info("Processed ledger") - s.maybeVerifyState(ingestLedger) + s.maybeVerifyState(ingestLedger, ledgerCloseMeta.BucketListHash()) s.maybeReapLookupTables(ingestLedger) return resumeImmediately(ingestLedger), nil @@ -745,7 +745,6 @@ func (v verifyRangeState) run(s *system) (transition, error) { return stop(), err } - var ledgerCloseMeta xdr.LedgerCloseMeta ledgerCloseMeta, err = s.ledgerBackend.GetLedger(s.ctx, sequence) if err != nil { return stop(), errors.Wrap(err, "error getting ledger") @@ -782,7 +781,11 @@ func (v verifyRangeState) run(s *system) (transition, error) { } if v.verifyState { - err = s.verifyState(false) + err = s.verifyState( + false, + ledgerCloseMeta.LedgerSequence(), + ledgerCloseMeta.BucketListHash(), + ) } return stop(), err diff --git a/services/horizon/internal/ingest/history_archive_adapter.go b/services/horizon/internal/ingest/history_archive_adapter.go index 7e415787e3..71c72f91a5 100644 --- a/services/horizon/internal/ingest/history_archive_adapter.go +++ b/services/horizon/internal/ingest/history_archive_adapter.go @@ -14,10 +14,14 @@ type historyArchiveAdapter struct { archive historyarchive.ArchiveInterface } +type verifiableChangeReader interface { + ingest.ChangeReader + VerifyBucketList(expectedHash xdr.Hash) error +} + type historyArchiveAdapterInterface interface { GetLatestLedgerSequence() (uint32, error) - BucketListHash(sequence uint32) (xdr.Hash, error) - GetState(ctx context.Context, sequence uint32) (ingest.ChangeReader, error) + GetState(ctx context.Context, sequence uint32) (verifiableChangeReader, error) GetStats() []historyarchive.ArchiveStats } @@ -36,27 +40,8 @@ func (haa *historyArchiveAdapter) GetLatestLedgerSequence() (uint32, error) { return has.CurrentLedger, nil } -// BucketListHash returns the bucket list hash to compare with hash in the -// ledger header fetched from Stellar-Core. -func (haa *historyArchiveAdapter) BucketListHash(sequence uint32) (xdr.Hash, error) { - exists, err := haa.archive.CategoryCheckpointExists("history", sequence) - if err != nil { - return xdr.Hash{}, errors.Wrap(err, "error checking if category checkpoint exists") - } - if !exists { - return xdr.Hash{}, errors.Errorf("history checkpoint does not exist for ledger %d", sequence) - } - - has, err := haa.archive.GetCheckpointHAS(sequence) - if err != nil { - return xdr.Hash{}, errors.Wrapf(err, "unable to get checkpoint HAS at ledger sequence %d", sequence) - } - - return has.BucketListHash() -} - // GetState returns a reader with the state of the ledger at the provided sequence number. -func (haa *historyArchiveAdapter) GetState(ctx context.Context, sequence uint32) (ingest.ChangeReader, error) { +func (haa *historyArchiveAdapter) GetState(ctx context.Context, sequence uint32) (verifiableChangeReader, error) { exists, err := haa.archive.CategoryCheckpointExists("history", sequence) if err != nil { return nil, errors.Wrap(err, "error checking if category checkpoint exists") diff --git a/services/horizon/internal/ingest/history_archive_adapter_test.go b/services/horizon/internal/ingest/history_archive_adapter_test.go index 20d84149fa..168c812d5d 100644 --- a/services/horizon/internal/ingest/history_archive_adapter_test.go +++ b/services/horizon/internal/ingest/history_archive_adapter_test.go @@ -6,12 +6,12 @@ import ( stdio "io" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stellar/go/historyarchive" - "github.com/stellar/go/ingest" "github.com/stellar/go/support/errors" "github.com/stellar/go/xdr" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" ) type mockHistoryArchiveAdapter struct { @@ -28,9 +28,9 @@ func (m *mockHistoryArchiveAdapter) BucketListHash(sequence uint32) (xdr.Hash, e return args.Get(0).(xdr.Hash), args.Error(1) } -func (m *mockHistoryArchiveAdapter) GetState(ctx context.Context, sequence uint32) (ingest.ChangeReader, error) { +func (m *mockHistoryArchiveAdapter) GetState(ctx context.Context, sequence uint32) (verifiableChangeReader, error) { args := m.Called(ctx, sequence) - return args.Get(0).(ingest.ChangeReader), args.Error(1) + return args.Get(0).(verifiableChangeReader), args.Error(1) } func (m *mockHistoryArchiveAdapter) GetStats() []historyarchive.ArchiveStats { diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index fe9c62eba4..7dbaacaadb 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -24,6 +24,7 @@ import ( "github.com/stellar/go/support/errors" logpkg "github.com/stellar/go/support/log" "github.com/stellar/go/support/storage" + "github.com/stellar/go/xdr" ) const ( @@ -669,7 +670,7 @@ func (s *system) runStateMachine(cur stateMachineNode) error { } } -func (s *system) maybeVerifyState(lastIngestedLedger uint32) { +func (s *system) maybeVerifyState(lastIngestedLedger uint32, expectedBucketListHash xdr.Hash) { stateInvalid, err := s.historyQ.GetExpStateInvalid(s.ctx) if err != nil { if !isCancelledError(s.ctx, err) { @@ -686,7 +687,7 @@ func (s *system) maybeVerifyState(lastIngestedLedger uint32) { go func() { defer s.wg.Done() - err := s.verifyState(true) + err := s.verifyState(true, lastIngestedLedger, expectedBucketListHash) if err != nil { if isCancelledError(s.ctx, err) { return diff --git a/services/horizon/internal/ingest/main_test.go b/services/horizon/internal/ingest/main_test.go index 0db777306c..6f5d89bd6a 100644 --- a/services/horizon/internal/ingest/main_test.go +++ b/services/horizon/internal/ingest/main_test.go @@ -188,11 +188,11 @@ func TestMaybeVerifyStateGetExpStateInvalidError(t *testing.T) { defer func() { log = oldLogger }() historyQ.On("GetExpStateInvalid", system.ctx).Return(false, db.ErrCancelled).Once() - system.maybeVerifyState(63) + system.maybeVerifyState(63, xdr.Hash{}) system.wg.Wait() historyQ.On("GetExpStateInvalid", system.ctx).Return(false, context.Canceled).Once() - system.maybeVerifyState(63) + system.maybeVerifyState(63, xdr.Hash{}) system.wg.Wait() logged := done() @@ -200,7 +200,7 @@ func TestMaybeVerifyStateGetExpStateInvalidError(t *testing.T) { // Ensure state verifier does not start also for any other error historyQ.On("GetExpStateInvalid", system.ctx).Return(false, errors.New("my error")).Once() - system.maybeVerifyState(63) + system.maybeVerifyState(63, xdr.Hash{}) system.wg.Wait() historyQ.AssertExpectations(t) @@ -227,11 +227,11 @@ func TestMaybeVerifyInternalDBErrCancelOrContextCanceled(t *testing.T) { historyQ.On("CloneIngestionQ").Return(historyQ).Twice() historyQ.On("BeginTx", mock.Anything, mock.Anything).Return(db.ErrCancelled).Once() - system.maybeVerifyState(63) + system.maybeVerifyState(63, xdr.Hash{}) system.wg.Wait() historyQ.On("BeginTx", mock.Anything, mock.Anything).Return(context.Canceled).Once() - system.maybeVerifyState(63) + system.maybeVerifyState(63, xdr.Hash{}) system.wg.Wait() logged := done() diff --git a/services/horizon/internal/ingest/processor_runner.go b/services/horizon/internal/ingest/processor_runner.go index 6832a5078f..9c38c01154 100644 --- a/services/horizon/internal/ingest/processor_runner.go +++ b/services/horizon/internal/ingest/processor_runner.go @@ -1,7 +1,6 @@ package ingest import ( - "bytes" "context" "fmt" "io" @@ -198,30 +197,6 @@ func (s *ProcessorRunner) checkIfProtocolVersionSupported(ledgerProtocolVersion return nil } -// validateBucketList validates if the bucket list hash in history archive -// matches the one in corresponding ledger header in stellar-core backend. -// This gives you full security if data in stellar-core backend can be trusted -// (ex. you run it in your infrastructure). -// The hashes of actual buckets of this HAS file are checked using -// historyarchive.XdrStream.SetExpectedHash (this is done in -// CheckpointChangeReader). -func (s *ProcessorRunner) validateBucketList(ledgerSequence uint32, ledgerBucketHashList xdr.Hash) error { - historyBucketListHash, err := s.historyAdapter.BucketListHash(ledgerSequence) - if err != nil { - return errors.Wrap(err, "Error getting bucket list hash") - } - - if !bytes.Equal(historyBucketListHash[:], ledgerBucketHashList[:]) { - return fmt.Errorf( - "Bucket list hash of history archive and ledger header does not match: %#x %#x", - historyBucketListHash, - ledgerBucketHashList, - ) - } - - return nil -} - func (s *ProcessorRunner) RunGenesisStateIngestion() (ingest.StatsChangeProcessorResults, error) { return s.RunHistoryArchiveIngestion(1, false, 0, xdr.Hash{}) } @@ -257,10 +232,6 @@ func (s *ProcessorRunner) RunHistoryArchiveIngestion( if err := s.checkIfProtocolVersionSupported(ledgerProtocolVersion); err != nil { return changeStats.GetResults(), errors.Wrap(err, "Error while checking for supported protocol version") } - - if err := s.validateBucketList(checkpointLedger, bucketListHash); err != nil { - return changeStats.GetResults(), errors.Wrap(err, "Error validating bucket list from HAS") - } } changeReader, err := s.historyAdapter.GetState(s.ctx, checkpointLedger) @@ -268,6 +239,12 @@ func (s *ProcessorRunner) RunHistoryArchiveIngestion( return changeStats.GetResults(), errors.Wrap(err, "Error creating HAS reader") } + if !skipChecks { + if err = changeReader.VerifyBucketList(bucketListHash); err != nil { + return changeStats.GetResults(), errors.Wrap(err, "Error validating bucket list from HAS") + } + } + defer changeReader.Close() log.WithField("sequence", checkpointLedger). diff --git a/services/horizon/internal/ingest/processor_runner_test.go b/services/horizon/internal/ingest/processor_runner_test.go index 8f6eb58d74..78faf853f3 100644 --- a/services/horizon/internal/ingest/processor_runner_test.go +++ b/services/horizon/internal/ingest/processor_runner_test.go @@ -78,13 +78,12 @@ func TestProcessorRunnerRunHistoryArchiveIngestionHistoryArchive(t *testing.T) { historyAdapter := &mockHistoryArchiveAdapter{} defer mock.AssertExpectationsForObjects(t, historyAdapter) - bucketListHash := xdr.Hash([32]byte{0, 1, 2}) - historyAdapter.On("BucketListHash", uint32(63)).Return(bucketListHash, nil).Once() - m := &ingest.MockChangeReader{} m.On("Read").Return(ingest.GenesisChange(network.PublicNetworkPassphrase), nil).Once() m.On("Read").Return(ingest.Change{}, io.EOF).Once() m.On("Close").Return(nil).Once() + bucketListHash := xdr.Hash([32]byte{0, 1, 2}) + m.On("VerifyBucketList", bucketListHash).Return(nil).Once() historyAdapter. On("GetState", ctx, uint32(63)). diff --git a/services/horizon/internal/ingest/verify.go b/services/horizon/internal/ingest/verify.go index 41b0eb98c5..294acd2a51 100644 --- a/services/horizon/internal/ingest/verify.go +++ b/services/horizon/internal/ingest/verify.go @@ -35,7 +35,7 @@ const stateVerifierExpectedIngestionVersion = 18 // verifyState is called as a go routine from pipeline post hook every 64 // ledgers. It checks if the state is correct. If another go routine is already // running it exits. -func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { +func (s *system) verifyState(verifyAgainstLatestCheckpoint bool, checkpointSequence uint32, expectedBucketListHash xdr.Hash) error { s.stateVerificationMutex.Lock() if s.stateVerificationRunning { log.Warn("State verification is already running...") @@ -94,6 +94,12 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { return nil } + if ledgerSequence != checkpointSequence { + localLog.WithField("checkpointSequence", checkpointSequence). + Info("Current ledger does not match checkpoint sequence. Canceling...") + return nil + } + ok, err := historyQ.TryStateVerificationLock(ctx) if err != nil { return errors.Wrap(err, "Error acquiring state verification lock") @@ -168,6 +174,9 @@ func (s *system) verifyState(verifyAgainstLatestCheckpoint bool) error { return errors.Wrap(err, "Error running GetState") } defer stateReader.Close() + if err = stateReader.VerifyBucketList(expectedBucketListHash); err != nil { + return ingest.NewStateError(err) + } verifier := verify.NewStateVerifier(stateReader, func(entry xdr.LedgerEntry) (bool, xdr.LedgerEntry) { entryType := entry.Data.Type diff --git a/services/horizon/internal/ingest/verify_range_state_test.go b/services/horizon/internal/ingest/verify_range_state_test.go index 7440f7dce0..a1df30d854 100644 --- a/services/horizon/internal/ingest/verify_range_state_test.go +++ b/services/horizon/internal/ingest/verify_range_state_test.go @@ -255,7 +255,9 @@ func (s *VerifyRangeStateTestSuite) TestSuccessWithVerify() { V0: &xdr.LedgerCloseMetaV0{ LedgerHeader: xdr.LedgerHeaderHistoryEntry{ Header: xdr.LedgerHeader{ - LedgerSeq: xdr.Uint32(i), + LedgerSeq: xdr.Uint32(i), + LedgerVersion: xdr.Uint32(MaxSupportedProtocolVersion), + BucketListHash: xdr.Hash{byte(i), 2, 3}, }, }, }, @@ -281,7 +283,10 @@ func (s *VerifyRangeStateTestSuite) TestSuccessWithVerify() { s.Assert().True(arg.ReadOnly) }).Return(nil).Once() clonedQ.On("Rollback").Return(nil).Once() - clonedQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(63), nil).Once() + clonedQ.On("GetLastLedgerIngestNonBlocking", s.ctx).Return(uint32(110), nil).Once() + s.system.runStateVerificationOnLedger = func(u uint32) bool { + return u == 110 + } clonedQ.On("TryStateVerificationLock", s.ctx).Return(true, nil).Once() mockChangeReader := &ingest.MockChangeReader{} mockChangeReader.On("Close").Return(nil).Once() @@ -482,7 +487,8 @@ func (s *VerifyRangeStateTestSuite) TestSuccessWithVerify() { mockChangeReader.On("Read").Return(liquidityPoolChange, nil).Once() mockChangeReader.On("Read").Return(ingest.Change{}, io.EOF).Once() mockChangeReader.On("Read").Return(ingest.Change{}, io.EOF).Once() - s.historyAdapter.On("GetState", s.ctx, uint32(63)).Return(mockChangeReader, nil).Once() + mockChangeReader.On("VerifyBucketList", xdr.Hash{110, 2, 3}).Return(nil).Once() + s.historyAdapter.On("GetState", s.ctx, uint32(110)).Return(mockChangeReader, nil).Once() mockAccount := history.AccountEntry{ AccountID: mockAccountID, Balance: 600, diff --git a/services/horizon/internal/ingest/verify_test.go b/services/horizon/internal/ingest/verify_test.go index f32bccdc8f..b86eaf4db1 100644 --- a/services/horizon/internal/ingest/verify_test.go +++ b/services/horizon/internal/ingest/verify_test.go @@ -3,6 +3,7 @@ package ingest import ( "crypto/sha256" "database/sql" + "fmt" "io" "math/rand" "regexp" @@ -355,7 +356,7 @@ func TestStateVerifierLockBusy(t *testing.T) { tt.Assert.NoError(err) tt.Assert.True(ok) - tt.Assert.NoError(sys.verifyState(false)) + tt.Assert.NoError(sys.verifyState(false, checkpointLedger, xdr.Hash{})) mockHistoryAdapter.AssertExpectations(t) tt.Assert.NoError(otherQ.Rollback()) @@ -386,6 +387,8 @@ func TestStateVerifier(t *testing.T) { mockChangeReader.On("Read").Return(ingest.Change{}, io.EOF).Twice() mockChangeReader.On("Close").Return(nil).Once() + bucketListHash := xdr.Hash{1, 2, 3} + mockChangeReader.On("VerifyBucketList", bucketListHash).Return(nil).Once() mockHistoryAdapter := &mockHistoryArchiveAdapter{} mockHistoryAdapter.On("GetState", mock.AnythingOfType("*context.timerCtx"), uint32(checkpointLedger)).Return(mockChangeReader, nil).Once() @@ -399,7 +402,43 @@ func TestStateVerifier(t *testing.T) { } sys.initMetrics() - tt.Assert.NoError(sys.verifyState(false)) + tt.Assert.NoError(sys.verifyState(false, checkpointLedger, bucketListHash)) + mockChangeReader.AssertExpectations(t) + mockHistoryAdapter.AssertExpectations(t) +} + +func TestStateVerifierHashError(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + q := &history.Q{&db.Session{DB: tt.HorizonDB}} + + ledger := rand.Int31() + checkpointLedger := uint32(ledger - (ledger % 64) - 1) + mockChangeReader := &ingest.MockChangeReader{} + + q.UpdateLastLedgerIngest(tt.Ctx, checkpointLedger) + + mockChangeReader.On("Close").Return(nil).Once() + bucketListHash := xdr.Hash{1, 2, 3} + mockChangeReader.On("VerifyBucketList", bucketListHash).Return(fmt.Errorf("hash mismatch error")).Once() + + mockHistoryAdapter := &mockHistoryArchiveAdapter{} + mockHistoryAdapter.On("GetState", mock.AnythingOfType("*context.timerCtx"), uint32(checkpointLedger)).Return(mockChangeReader, nil).Once() + + sys := &system{ + ctx: tt.Ctx, + historyQ: q, + historyAdapter: mockHistoryAdapter, + runStateVerificationOnLedger: ledgerEligibleForStateVerification(64, 1), + config: Config{StateVerificationTimeout: time.Hour}, + } + sys.initMetrics() + + err := sys.verifyState(false, checkpointLedger, bucketListHash) + tt.Assert.EqualError(err, "hash mismatch error") + _, isStateError := err.(ingest.StateError) + tt.Assert.True(isStateError) mockChangeReader.AssertExpectations(t) mockHistoryAdapter.AssertExpectations(t) } From d478c1027f8761b3f9331921dad6ee82d57da837 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Thu, 18 Apr 2024 11:08:07 -0400 Subject: [PATCH 104/234] Add GetLatestLedgerSequence --- ingest/ledgerbackend/cloud_storage_backend.go | 43 +++++++++++++++++-- support/datastore/datastore.go | 3 +- support/datastore/gcs_datastore.go | 33 +++++++++++--- support/datastore/mock_datastore.go | 7 ++- 4 files changed, 75 insertions(+), 11 deletions(-) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index dadd9a10f9..1686f07083 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -5,6 +5,9 @@ import ( "context" "fmt" "io" + "path/filepath" + "strconv" + "strings" "github.com/pkg/errors" "github.com/stellar/go/support/datastore" @@ -40,9 +43,42 @@ func NewCloudStorageBackend(ctx context.Context, storageURL string) (*CloudStora // GetLatestLedgerSequence returns the most recent ledger sequence number in the cloud storage bucket. func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - // TODO: Can probably copy the code that the ledger exporter will use for resumability - // Otherwise can use the ListObject function in datastore and find the largest valued filename - return uint32(0), nil + directories, err := csb.lcmDataStore.ListDirectoryNames(ctx, "") + if err != nil { + return 0, errors.Wrapf(err, "failed getting list of directory names") + } + + var latestDirectory string + largestDirectoryLedger := 0 + for _, dir := range directories { + dirParts := strings.Split(dir, "/") + lastDirPart := dirParts[len(dirParts)-1] + parts := strings.Split(lastDirPart, "-") + if len(parts) == 2 { + upper, _ := strconv.Atoi(parts[1]) + if upper > largestDirectoryLedger { + latestDirectory = dir + largestDirectoryLedger = upper + } + } + } + + latestLedgerSequence := uint32(0) + fileNames, err := csb.lcmDataStore.ListFileNames(ctx, latestDirectory) + if err != nil { + return 0, errors.Wrapf(err, "failed getting filenames in dir %s", latestDirectory) + } + for _, fileName := range fileNames { + ledgerSequence, err := strconv.ParseUint(strings.TrimSuffix(fileName, filepath.Ext(fileName)), 10, 32) + if err != nil { + return 0, errors.Wrapf(err, "failed converting filename to uint32 %s", fileName) + } + if uint32(ledgerSequence) > latestLedgerSequence { + latestLedgerSequence = uint32(ledgerSequence) + } + } + + return latestLedgerSequence, nil } // GetLedger returns the LedgerCloseMeta for the specified ledger sequence number @@ -55,7 +91,6 @@ func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) } reader, err := csb.lcmDataStore.GetFile(ctx, objectKey) - if err != nil { return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed getting file: %s", objectKey) } diff --git a/support/datastore/datastore.go b/support/datastore/datastore.go index 4481394223..eecf67d09b 100644 --- a/support/datastore/datastore.go +++ b/support/datastore/datastore.go @@ -20,7 +20,8 @@ type DataStore interface { Exists(ctx context.Context, path string) (bool, error) Size(ctx context.Context, path string) (int64, error) Close() error - ListObjects(ctx context.Context, path string) ([]string, error) + ListDirectoryNames(ctx context.Context, path string) ([]string, error) + ListFileNames(ctx context.Context, path string) ([]string, error) } // NewDataStore creates a new DataStore based on the destination URL. diff --git a/support/datastore/gcs_datastore.go b/support/datastore/gcs_datastore.go index 240efaa59a..e0324589f4 100644 --- a/support/datastore/gcs_datastore.go +++ b/support/datastore/gcs_datastore.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "path" + "strings" "google.golang.org/api/googleapi" "google.golang.org/api/iterator" @@ -106,10 +107,10 @@ func (b *GCSDataStore) putFile(ctx context.Context, filePath string, in io.Write return w.Close() } -func (b *GCSDataStore) ListObjects(ctx context.Context, path string) ([]string, error) { - var objectNames []string +func (b *GCSDataStore) ListDirectoryNames(ctx context.Context, path string) ([]string, error) { + var directories []string - o := b.bucket.Objects(ctx, nil) + o := b.bucket.Objects(ctx, &storage.Query{Delimiter: "/"}) for { attrs, err := o.Next() if err == iterator.Done { @@ -118,8 +119,30 @@ func (b *GCSDataStore) ListObjects(ctx context.Context, path string) ([]string, if err != nil { log.Fatal(err) } - objectNames = append(objectNames, attrs.Name) + if attrs.Prefix != "" { + directories = append(directories, strings.TrimSuffix(attrs.Prefix, "/")) + } + } + + return directories, nil +} + +func (b *GCSDataStore) ListFileNames(ctx context.Context, path string) ([]string, error) { + var files []string + + o := b.bucket.Objects(ctx, &storage.Query{Prefix: path + "/", Delimiter: "/"}) + for { + attrs, err := o.Next() + if err == iterator.Done { + break + } + if err != nil { + log.Fatal(err) + } + if attrs.Name != "" && !strings.HasSuffix(attrs.Name, "/") { + files = append(files, attrs.Name) + } } - return nil, nil + return files, nil } diff --git a/support/datastore/mock_datastore.go b/support/datastore/mock_datastore.go index 8e503bfa05..2788c81447 100644 --- a/support/datastore/mock_datastore.go +++ b/support/datastore/mock_datastore.go @@ -42,7 +42,12 @@ func (m *MockDataStore) Close() error { return args.Error(0) } -func (m *MockDataStore) ListObjects(ctx context.Context, path string) ([]string, error) { +func (m *MockDataStore) ListDirectoryNames(ctx context.Context, path string) ([]string, error) { + args := m.Called(ctx, path) + return args.Get(0).([]string), args.Error(0) +} + +func (m *MockDataStore) ListFileNames(ctx context.Context, path string) ([]string, error) { args := m.Called(ctx, path) return args.Get(0).([]string), args.Error(0) } From 54135f822330a468f80773bd28841b4122523c0c Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Thu, 18 Apr 2024 21:44:57 +0200 Subject: [PATCH 105/234] horizon: Bump integration test images (#5285) --- .github/workflows/horizon.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index c1618d21c8..98c71e89f7 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -32,13 +32,12 @@ jobs: env: HORIZON_INTEGRATION_TESTS_ENABLED: true HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL: ${{ matrix.protocol-version }} - PROTOCOL_21_CORE_DEBIAN_PKG_VERSION: 20.4.1-1807.b152dc51d.focal - PROTOCOL_21_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:20.4.1-1807.b152dc51d.focal - PROTOCOL_21_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:21.0.0-rc1-68 - PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 20.4.1-1807.b152dc51d.focal - PROTOCOL_20_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:20.4.1-1807.b152dc51d.focal - # TODO: bump soroban-rpc to the p21 version once it supports it - PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:20.3.0-rc1-52 + PROTOCOL_21_CORE_DEBIAN_PKG_VERSION: 21.0.0-1812.rc1.a10329cca.focal + PROTOCOL_21_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:21.0.0-1812.rc1.a10329cca.focal + PROTOCOL_21_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:21.0.0-rc2-73 + PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 21.0.0-1812.rc1.a10329cca.focal + PROTOCOL_20_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:21.0.0-1812.rc1.a10329cca.focal + PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:21.0.0-rc2-73 PGHOST: localhost PGPORT: 5432 PGUSER: postgres From f2cd035e103114f47bd6dff924b92cab85687c96 Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 18 Apr 2024 14:45:14 -0700 Subject: [PATCH 106/234] don't run filtered tmp processor when reingestion is enabled (#5283) --- services/horizon/cmd/db.go | 6 +- services/horizon/cmd/ingest.go | 57 +++---- services/horizon/internal/app.go | 53 +++--- services/horizon/internal/config.go | 1 - services/horizon/internal/flags.go | 10 +- services/horizon/internal/httpx/router.go | 53 +++--- .../internal/ingest/group_processors.go | 4 + services/horizon/internal/ingest/main.go | 6 +- .../internal/ingest/processor_runner.go | 37 ++-- .../internal/ingest/processor_runner_test.go | 89 +++------- services/horizon/internal/init.go | 1 - .../horizon/internal/integration/db_test.go | 160 +++++++++++++++++- .../internal/integration/parameters_test.go | 21 --- .../internal/test/integration/integration.go | 6 + 14 files changed, 299 insertions(+), 205 deletions(-) diff --git a/services/horizon/cmd/db.go b/services/horizon/cmd/db.go index 965d5d7173..cec51543c9 100644 --- a/services/horizon/cmd/db.go +++ b/services/horizon/cmd/db.go @@ -224,6 +224,10 @@ var dbReapCmd = &cobra.Command{ if err != nil { return err } + defer func() { + app.Shutdown() + app.CloseDB() + }() ctx := context.Background() app.UpdateHorizonLedgerState(ctx) return app.DeleteUnretainedHistory(ctx) @@ -409,7 +413,6 @@ func runDBReingestRange(ledgerRanges []history.LedgerRange, reingestForce bool, HistoryArchiveURLs: config.HistoryArchiveURLs, HistoryArchiveCaching: config.HistoryArchiveCaching, CheckpointFrequency: config.CheckpointFrequency, - ReingestEnabled: true, MaxReingestRetries: int(retries), ReingestRetryBackoffSeconds: int(retryBackoffSeconds), CaptiveCoreBinaryPath: config.CaptiveCoreBinaryPath, @@ -418,7 +421,6 @@ func runDBReingestRange(ledgerRanges []history.LedgerRange, reingestForce bool, CaptiveCoreStoragePath: config.CaptiveCoreStoragePath, StellarCoreURL: config.StellarCoreURL, RoundingSlippageFilter: config.RoundingSlippageFilter, - EnableIngestionFiltering: config.EnableIngestionFiltering, MaxLedgerPerFlush: maxLedgersPerFlush, SkipTxmeta: config.SkipTxmeta, } diff --git a/services/horizon/cmd/ingest.go b/services/horizon/cmd/ingest.go index 18452dc74a..864067da8f 100644 --- a/services/horizon/cmd/ingest.go +++ b/services/horizon/cmd/ingest.go @@ -125,17 +125,16 @@ var ingestVerifyRangeCmd = &cobra.Command{ } ingestConfig := ingest.Config{ - NetworkPassphrase: globalConfig.NetworkPassphrase, - HistorySession: horizonSession, - HistoryArchiveURLs: globalConfig.HistoryArchiveURLs, - HistoryArchiveCaching: globalConfig.HistoryArchiveCaching, - CaptiveCoreBinaryPath: globalConfig.CaptiveCoreBinaryPath, - CaptiveCoreConfigUseDB: globalConfig.CaptiveCoreConfigUseDB, - CheckpointFrequency: globalConfig.CheckpointFrequency, - CaptiveCoreToml: globalConfig.CaptiveCoreToml, - CaptiveCoreStoragePath: globalConfig.CaptiveCoreStoragePath, - RoundingSlippageFilter: globalConfig.RoundingSlippageFilter, - EnableIngestionFiltering: globalConfig.EnableIngestionFiltering, + NetworkPassphrase: globalConfig.NetworkPassphrase, + HistorySession: horizonSession, + HistoryArchiveURLs: globalConfig.HistoryArchiveURLs, + HistoryArchiveCaching: globalConfig.HistoryArchiveCaching, + CaptiveCoreBinaryPath: globalConfig.CaptiveCoreBinaryPath, + CaptiveCoreConfigUseDB: globalConfig.CaptiveCoreConfigUseDB, + CheckpointFrequency: globalConfig.CheckpointFrequency, + CaptiveCoreToml: globalConfig.CaptiveCoreToml, + CaptiveCoreStoragePath: globalConfig.CaptiveCoreStoragePath, + RoundingSlippageFilter: globalConfig.RoundingSlippageFilter, } system, err := ingest.NewSystem(ingestConfig) @@ -285,14 +284,13 @@ var ingestInitGenesisStateCmd = &cobra.Command{ } ingestConfig := ingest.Config{ - NetworkPassphrase: globalConfig.NetworkPassphrase, - HistorySession: horizonSession, - HistoryArchiveURLs: globalConfig.HistoryArchiveURLs, - CheckpointFrequency: globalConfig.CheckpointFrequency, - RoundingSlippageFilter: globalConfig.RoundingSlippageFilter, - EnableIngestionFiltering: globalConfig.EnableIngestionFiltering, - CaptiveCoreBinaryPath: globalConfig.CaptiveCoreBinaryPath, - CaptiveCoreConfigUseDB: globalConfig.CaptiveCoreConfigUseDB, + NetworkPassphrase: globalConfig.NetworkPassphrase, + HistorySession: horizonSession, + HistoryArchiveURLs: globalConfig.HistoryArchiveURLs, + CheckpointFrequency: globalConfig.CheckpointFrequency, + RoundingSlippageFilter: globalConfig.RoundingSlippageFilter, + CaptiveCoreBinaryPath: globalConfig.CaptiveCoreBinaryPath, + CaptiveCoreConfigUseDB: globalConfig.CaptiveCoreConfigUseDB, } system, err := ingest.NewSystem(ingestConfig) @@ -348,17 +346,16 @@ var ingestBuildStateCmd = &cobra.Command{ } ingestConfig := ingest.Config{ - NetworkPassphrase: globalConfig.NetworkPassphrase, - HistorySession: horizonSession, - HistoryArchiveURLs: globalConfig.HistoryArchiveURLs, - HistoryArchiveCaching: globalConfig.HistoryArchiveCaching, - CaptiveCoreBinaryPath: globalConfig.CaptiveCoreBinaryPath, - CaptiveCoreConfigUseDB: globalConfig.CaptiveCoreConfigUseDB, - CheckpointFrequency: globalConfig.CheckpointFrequency, - CaptiveCoreToml: globalConfig.CaptiveCoreToml, - CaptiveCoreStoragePath: globalConfig.CaptiveCoreStoragePath, - RoundingSlippageFilter: globalConfig.RoundingSlippageFilter, - EnableIngestionFiltering: globalConfig.EnableIngestionFiltering, + NetworkPassphrase: globalConfig.NetworkPassphrase, + HistorySession: horizonSession, + HistoryArchiveURLs: globalConfig.HistoryArchiveURLs, + HistoryArchiveCaching: globalConfig.HistoryArchiveCaching, + CaptiveCoreBinaryPath: globalConfig.CaptiveCoreBinaryPath, + CaptiveCoreConfigUseDB: globalConfig.CaptiveCoreConfigUseDB, + CheckpointFrequency: globalConfig.CheckpointFrequency, + CaptiveCoreToml: globalConfig.CaptiveCoreToml, + CaptiveCoreStoragePath: globalConfig.CaptiveCoreStoragePath, + RoundingSlippageFilter: globalConfig.RoundingSlippageFilter, } system, err := ingest.NewSystem(ingestConfig) diff --git a/services/horizon/internal/app.go b/services/horizon/internal/app.go index 927cffa773..36fc2031e8 100644 --- a/services/horizon/internal/app.go +++ b/services/horizon/internal/app.go @@ -156,9 +156,15 @@ func (a *App) Close() { func (a *App) waitForDone() { <-a.done - webShutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - a.webServer.Shutdown(webShutdownCtx) + a.Shutdown() +} + +func (a *App) Shutdown() { + if a.webServer != nil { + webShutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + a.webServer.Shutdown(webShutdownCtx) + } a.cancel() if a.ingester != nil { a.ingester.Shutdown() @@ -528,27 +534,26 @@ func (a *App) init() error { initTxSubMetrics(a) routerConfig := httpx.RouterConfig{ - DBSession: a.historyQ.SessionInterface, - TxSubmitter: a.submitter, - RateQuota: a.config.RateQuota, - BehindCloudflare: a.config.BehindCloudflare, - BehindAWSLoadBalancer: a.config.BehindAWSLoadBalancer, - SSEUpdateFrequency: a.config.SSEUpdateFrequency, - StaleThreshold: a.config.StaleThreshold, - ConnectionTimeout: a.config.ConnectionTimeout, - ClientQueryTimeout: a.config.ClientQueryTimeout, - MaxConcurrentRequests: a.config.MaxConcurrentRequests, - MaxHTTPRequestSize: a.config.MaxHTTPRequestSize, - NetworkPassphrase: a.config.NetworkPassphrase, - MaxPathLength: a.config.MaxPathLength, - MaxAssetsPerPathRequest: a.config.MaxAssetsPerPathRequest, - PathFinder: a.paths, - PrometheusRegistry: a.prometheusRegistry, - CoreGetter: a, - HorizonVersion: a.horizonVersion, - FriendbotURL: a.config.FriendbotURL, - EnableIngestionFiltering: a.config.EnableIngestionFiltering, - DisableTxSub: a.config.DisableTxSub, + DBSession: a.historyQ.SessionInterface, + TxSubmitter: a.submitter, + RateQuota: a.config.RateQuota, + BehindCloudflare: a.config.BehindCloudflare, + BehindAWSLoadBalancer: a.config.BehindAWSLoadBalancer, + SSEUpdateFrequency: a.config.SSEUpdateFrequency, + StaleThreshold: a.config.StaleThreshold, + ConnectionTimeout: a.config.ConnectionTimeout, + ClientQueryTimeout: a.config.ClientQueryTimeout, + MaxConcurrentRequests: a.config.MaxConcurrentRequests, + MaxHTTPRequestSize: a.config.MaxHTTPRequestSize, + NetworkPassphrase: a.config.NetworkPassphrase, + MaxPathLength: a.config.MaxPathLength, + MaxAssetsPerPathRequest: a.config.MaxAssetsPerPathRequest, + PathFinder: a.paths, + PrometheusRegistry: a.prometheusRegistry, + CoreGetter: a, + HorizonVersion: a.horizonVersion, + FriendbotURL: a.config.FriendbotURL, + DisableTxSub: a.config.DisableTxSub, HealthCheck: healthCheck{ session: a.historyQ.SessionInterface, ctx: a.ctx, diff --git a/services/horizon/internal/config.go b/services/horizon/internal/config.go index 4c8a45514f..2e4192a1c9 100644 --- a/services/horizon/internal/config.go +++ b/services/horizon/internal/config.go @@ -19,7 +19,6 @@ type Config struct { Port uint AdminPort uint - EnableIngestionFiltering bool CaptiveCoreBinaryPath string CaptiveCoreConfigPath string CaptiveCoreTomlParams ledgerbackend.CaptiveCoreTomlParams diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index 774140bb53..5489b57d50 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -279,12 +279,8 @@ func Flags() (*Config, support.ConfigOptions) { OptType: types.String, FlagDefault: "", Required: false, - ConfigKey: &config.EnableIngestionFiltering, CustomSetValue: func(opt *support.ConfigOption) error { - - // Always enable ingestion filtering by default. - config.EnableIngestionFiltering = true - + // ingestion filtering is always enabled, it has no rules by default. if val := viper.GetString(opt.Name); val != "" { stdLog.Printf( "DEPRECATED - No ingestion filter rules are defined by default, which equates to " + @@ -665,9 +661,11 @@ func Flags() (*Config, support.ConfigOptions) { Usage: "the batch size (in ledgers) to remove per reap from the Horizon database", UsedInCommands: IngestionCommands, CustomSetValue: func(opt *support.ConfigOption) error { - if val := viper.GetUint(opt.Name); val <= 0 || val > 500_000 { + val := viper.GetUint(opt.Name) + if val <= 0 || val > 500_000 { return fmt.Errorf("flag --history-retention-reap-count must be in range [1, 500,000]") } + *(opt.ConfigKey.(*uint)) = val return nil }, }, diff --git a/services/horizon/internal/httpx/router.go b/services/horizon/internal/httpx/router.go index 4ba978a96a..cd3b6821b0 100644 --- a/services/horizon/internal/httpx/router.go +++ b/services/horizon/internal/httpx/router.go @@ -34,25 +34,24 @@ type RouterConfig struct { RateQuota *throttled.RateQuota MaxConcurrentRequests uint - BehindCloudflare bool - BehindAWSLoadBalancer bool - SSEUpdateFrequency time.Duration - StaleThreshold uint - ConnectionTimeout time.Duration - ClientQueryTimeout time.Duration - MaxHTTPRequestSize uint - NetworkPassphrase string - MaxPathLength uint - MaxAssetsPerPathRequest int - PathFinder paths.Finder - PrometheusRegistry *prometheus.Registry - CoreGetter actions.CoreStateGetter - HorizonVersion string - FriendbotURL *url.URL - HealthCheck http.Handler - EnableIngestionFiltering bool - DisableTxSub bool - SkipTxMeta bool + BehindCloudflare bool + BehindAWSLoadBalancer bool + SSEUpdateFrequency time.Duration + StaleThreshold uint + ConnectionTimeout time.Duration + ClientQueryTimeout time.Duration + MaxHTTPRequestSize uint + NetworkPassphrase string + MaxPathLength uint + MaxAssetsPerPathRequest int + PathFinder paths.Finder + PrometheusRegistry *prometheus.Registry + CoreGetter actions.CoreStateGetter + HorizonVersion string + FriendbotURL *url.URL + HealthCheck http.Handler + DisableTxSub bool + SkipTxMeta bool } type Router struct { @@ -375,13 +374,11 @@ func (r *Router) addRoutes(config *RouterConfig, rateLimiter *throttled.HTTPRate r.Internal.Get("/metrics", promhttp.HandlerFor(config.PrometheusRegistry, promhttp.HandlerOpts{}).ServeHTTP) r.Internal.Get("/debug/pprof/heap", pprof.Index) r.Internal.Get("/debug/pprof/profile", pprof.Profile) - if config.EnableIngestionFiltering { - r.Internal.Route("/ingestion/filters", func(r chi.Router) { - handler := actions.FilterConfigHandler{} - r.With(historyMiddleware).Put("/asset", handler.UpdateAssetConfig) - r.With(historyMiddleware).Put("/account", handler.UpdateAccountConfig) - r.With(historyMiddleware).Get("/asset", handler.GetAssetConfig) - r.With(historyMiddleware).Get("/account", handler.GetAccountConfig) - }) - } + r.Internal.Route("/ingestion/filters", func(r chi.Router) { + handler := actions.FilterConfigHandler{} + r.With(historyMiddleware).Put("/asset", handler.UpdateAssetConfig) + r.With(historyMiddleware).Put("/account", handler.UpdateAccountConfig) + r.With(historyMiddleware).Get("/asset", handler.GetAssetConfig) + r.With(historyMiddleware).Get("/account", handler.GetAccountConfig) + }) } diff --git a/services/horizon/internal/ingest/group_processors.go b/services/horizon/internal/ingest/group_processors.go index 8b5d2d337e..00f8adde8a 100644 --- a/services/horizon/internal/ingest/group_processors.go +++ b/services/horizon/internal/ingest/group_processors.go @@ -125,6 +125,10 @@ func newGroupTransactionProcessors(processors []horizonTransactionProcessor, } } +func (g groupTransactionProcessors) IsEmpty() bool { + return len(g.processors) == 0 +} + func (g groupTransactionProcessors) ProcessTransaction(lcm xdr.LedgerCloseMeta, tx ingest.LedgerTransaction) error { for _, p := range g.processors { startTime := time.Now() diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 7dbaacaadb..d88cc3a3ce 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -97,7 +97,6 @@ type Config struct { EnableReapLookupTables bool EnableExtendedLogLedgerStats bool - ReingestEnabled bool MaxReingestRetries int ReingestRetryBackoffSeconds int @@ -108,9 +107,8 @@ type Config struct { RoundingSlippageFilter int - EnableIngestionFiltering bool - MaxLedgerPerFlush uint32 - SkipTxmeta bool + MaxLedgerPerFlush uint32 + SkipTxmeta bool } const ( diff --git a/services/horizon/internal/ingest/processor_runner.go b/services/horizon/internal/ingest/processor_runner.go index 9c38c01154..e6f0e0cf74 100644 --- a/services/horizon/internal/ingest/processor_runner.go +++ b/services/horizon/internal/ingest/processor_runner.go @@ -164,20 +164,15 @@ func (s *ProcessorRunner) buildTransactionProcessor(ledgersProcessor *processors func (s *ProcessorRunner) buildTransactionFilterer() *groupTransactionFilterers { var f []processors.LedgerTransactionFilterer - if s.config.EnableIngestionFiltering { - f = append(f, s.filters.GetFilters(s.historyQ, s.ctx)...) - } - + f = append(f, s.filters.GetFilters(s.historyQ, s.ctx)...) return newGroupTransactionFilterers(f) } func (s *ProcessorRunner) buildFilteredOutProcessor() *groupTransactionProcessors { - // when in online mode, the submission result processor must always run (regardless of filtering) var p []horizonTransactionProcessor - if s.config.EnableIngestionFiltering { - txSubProc := processors.NewTransactionFilteredTmpProcessor(s.historyQ.NewTransactionFilteredTmpBatchInsertBuilder(), s.config.SkipTxmeta) - p = append(p, txSubProc) - } + + txSubProc := processors.NewTransactionFilteredTmpProcessor(s.historyQ.NewTransactionFilteredTmpBatchInsertBuilder(), s.config.SkipTxmeta) + p = append(p, txSubProc) return newGroupTransactionProcessors(p, nil, nil) } @@ -384,6 +379,7 @@ func (s *ProcessorRunner) runTransactionProcessorsOnLedger(registry nameRegistry ledgersProcessor.ProcessLedger(ledger) groupTransactionFilterers := s.buildTransactionFilterer() + // when in online mode, the submission result processor must always run (regardless of whether filter rules exist or not) groupFilteredOutProcessors := s.buildFilteredOutProcessor() loaders, groupTransactionProcessors := s.buildTransactionProcessor(ledgersProcessor) @@ -488,11 +484,16 @@ func registerTransactionProcessors( return nil } +// Runs only transaction processors on the inbound list of ledgers. +// Updates history tables based on transactions. +// Intentionally do not make effort to insert or purge tx's on history_transactions_filtered_tmp +// Thus, using this method does not support tx sub processing for the ledgers passed in, i.e. tx submission queue will not see these. func (s *ProcessorRunner) RunTransactionProcessorsOnLedgers(ledgers []xdr.LedgerCloseMeta, execInTx bool) (err error) { ledgersProcessor := processors.NewLedgerProcessor(s.historyQ.NewLedgerBatchInsertBuilder(), CurrentVersion) groupTransactionFilterers := s.buildTransactionFilterer() - groupFilteredOutProcessors := s.buildFilteredOutProcessor() + // intentionally skip filtered out processor + groupFilteredOutProcessors := newGroupTransactionProcessors(nil, nil, nil) loaders, groupTransactionProcessors := s.buildTransactionProcessor(ledgersProcessor) startTime := time.Now() @@ -554,16 +555,16 @@ func (s *ProcessorRunner) flushProcessors(groupFilteredOutProcessors *groupTrans defer s.session.Rollback() } - if s.config.EnableIngestionFiltering { + if err := groupFilteredOutProcessors.Flush(s.ctx, s.session); err != nil { + return errors.Wrap(err, "Error flushing temp filtered tx from processor") + } - if err := groupFilteredOutProcessors.Flush(s.ctx, s.session); err != nil { - return errors.Wrap(err, "Error flushing temp filtered tx from processor") - } - if time.Since(s.lastTransactionsTmpGC) > transactionsFilteredTmpGCPeriod { - if _, err := s.historyQ.DeleteTransactionsFilteredTmpOlderThan(s.ctx, uint64(transactionsFilteredTmpGCPeriod.Seconds())); err != nil { - return errors.Wrap(err, "Error trimming filtered transactions") - } + if !groupFilteredOutProcessors.IsEmpty() && + time.Since(s.lastTransactionsTmpGC) > transactionsFilteredTmpGCPeriod { + if _, err := s.historyQ.DeleteTransactionsFilteredTmpOlderThan(s.ctx, uint64(transactionsFilteredTmpGCPeriod.Seconds())); err != nil { + return errors.Wrap(err, "Error trimming filtered transactions") } + s.lastTransactionsTmpGC = time.Now() } if err := groupTransactionProcessors.Flush(s.ctx, s.session); err != nil { diff --git a/services/horizon/internal/ingest/processor_runner_test.go b/services/horizon/internal/ingest/processor_runner_test.go index 78faf853f3..e6ce6b512c 100644 --- a/services/horizon/internal/ingest/processor_runner_test.go +++ b/services/horizon/internal/ingest/processor_runner_test.go @@ -260,75 +260,6 @@ func TestProcessorRunnerBuildTransactionProcessor(t *testing.T) { assert.IsType(t, &processors.LiquidityPoolsTransactionProcessor{}, processor.processors[8]) } -func TestProcessorRunnerWithFilterEnabled(t *testing.T) { - ctx := context.Background() - - config := Config{ - NetworkPassphrase: network.PublicNetworkPassphrase, - EnableIngestionFiltering: true, - } - - q := &mockDBQ{} - mockSession := &db.MockSession{} - defer mock.AssertExpectationsForObjects(t, q) - - ledger := xdr.LedgerCloseMeta{ - V0: &xdr.LedgerCloseMetaV0{ - LedgerHeader: xdr.LedgerHeaderHistoryEntry{ - Header: xdr.LedgerHeader{ - BucketListHash: xdr.Hash([32]byte{0, 1, 2}), - LedgerSeq: 23, - }, - }, - }, - } - - mockTransactionsFilteredTmpBatchInsertBuilder := &history.MockTransactionsBatchInsertBuilder{} - defer mock.AssertExpectationsForObjects(t, mockTransactionsFilteredTmpBatchInsertBuilder) - mockTransactionsFilteredTmpBatchInsertBuilder.On("Exec", ctx, mockSession).Return(nil).Once() - q.MockQTransactions.On("NewTransactionFilteredTmpBatchInsertBuilder"). - Return(mockTransactionsFilteredTmpBatchInsertBuilder) - q.On("DeleteTransactionsFilteredTmpOlderThan", ctx, mock.AnythingOfType("uint64")). - Return(int64(0), nil) - - defer mock.AssertExpectationsForObjects(t, mockTxProcessorBatchBuilders(q, mockSession, ctx)...) - defer mock.AssertExpectationsForObjects(t, mockChangeProcessorBatchBuilders(q, ctx, true)...) - - mockBatchInsertBuilder := &history.MockLedgersBatchInsertBuilder{} - q.MockQLedgers.On("NewLedgerBatchInsertBuilder").Return(mockBatchInsertBuilder) - mockBatchInsertBuilder.On( - "Add", - ledger.V0.LedgerHeader, 0, 0, 0, 0, CurrentVersion).Return(nil) - mockBatchInsertBuilder.On( - "Exec", - ctx, - mockSession, - ).Return(nil) - defer mock.AssertExpectationsForObjects(t, mockBatchInsertBuilder) - - q.MockQAssetStats.On("RemoveContractAssetBalances", ctx, []xdr.Hash(nil)). - Return(nil).Once() - q.MockQAssetStats.On("UpdateContractAssetBalanceAmounts", ctx, []xdr.Hash{}, []string{}). - Return(nil).Once() - q.MockQAssetStats.On("InsertContractAssetBalances", ctx, []history.ContractAssetBalance(nil)). - Return(nil).Once() - q.MockQAssetStats.On("UpdateContractAssetBalanceExpirations", ctx, []xdr.Hash{}, []uint32{}). - Return(nil).Once() - q.MockQAssetStats.On("GetContractAssetBalancesExpiringAt", ctx, uint32(22)). - Return([]history.ContractAssetBalance{}, nil).Once() - - runner := ProcessorRunner{ - ctx: ctx, - config: config, - historyQ: q, - session: mockSession, - filters: &MockFilters{}, - } - - _, err := runner.RunAllProcessorsOnLedger(ledger) - assert.NoError(t, err) -} - func TestProcessorRunnerRunAllProcessorsOnLedger(t *testing.T) { ctx := context.Background() @@ -354,6 +285,7 @@ func TestProcessorRunnerRunAllProcessorsOnLedger(t *testing.T) { // Batches defer mock.AssertExpectationsForObjects(t, mockTxProcessorBatchBuilders(q, mockSession, ctx)...) defer mock.AssertExpectationsForObjects(t, mockChangeProcessorBatchBuilders(q, ctx, true)...) + defer mock.AssertExpectationsForObjects(t, mockFilteredOutProcessorsForNoRules(q, mockSession, ctx)...) mockBatchInsertBuilder := &history.MockLedgersBatchInsertBuilder{} q.MockQLedgers.On("NewLedgerBatchInsertBuilder").Return(mockBatchInsertBuilder) @@ -434,6 +366,10 @@ func TestProcessorRunnerRunTransactionsProcessorsOnLedgers(t *testing.T) { }, } + // filtered out processor should not be created + q.MockQTransactions.AssertNotCalled(t, "NewTransactionFilteredTmpBatchInsertBuilder") + q.AssertNotCalled(t, "DeleteTransactionsFilteredTmpOlderThan", ctx, mock.AnythingOfType("uint64")) + // Batches defer mock.AssertExpectationsForObjects(t, mockTxProcessorBatchBuilders(q, mockSession, ctx)...) @@ -660,3 +596,18 @@ func mockChangeProcessorBatchBuilders(q *mockDBQ, ctx context.Context, mockExec mockTrustLinesBatchInsertBuilder, } } + +func mockFilteredOutProcessorsForNoRules(q *mockDBQ, mockSession *db.MockSession, ctx context.Context) []interface{} { + mockTransactionsFilteredTmpBatchInsertBuilder := &history.MockTransactionsBatchInsertBuilder{} + // since no filter rules are used on tests in this suite, we do not need to mock the "Add" call + // the "Exec" call gets run by flush all the time + mockTransactionsFilteredTmpBatchInsertBuilder.On("Exec", ctx, mockSession).Return(nil).Once() + q.MockQTransactions.On("NewTransactionFilteredTmpBatchInsertBuilder"). + Return(mockTransactionsFilteredTmpBatchInsertBuilder) + q.On("DeleteTransactionsFilteredTmpOlderThan", ctx, mock.AnythingOfType("uint64")). + Return(int64(0), nil) + + return []interface{}{ + mockTransactionsFilteredTmpBatchInsertBuilder, + } +} diff --git a/services/horizon/internal/init.go b/services/horizon/internal/init.go index 2311833508..93580fed54 100644 --- a/services/horizon/internal/init.go +++ b/services/horizon/internal/init.go @@ -102,7 +102,6 @@ func initIngester(app *App) { EnableReapLookupTables: app.config.HistoryRetentionCount > 0, EnableExtendedLogLedgerStats: app.config.IngestEnableExtendedLogLedgerStats, RoundingSlippageFilter: app.config.RoundingSlippageFilter, - EnableIngestionFiltering: app.config.EnableIngestionFiltering, SkipTxmeta: app.config.SkipTxmeta, }) diff --git a/services/horizon/internal/integration/db_test.go b/services/horizon/internal/integration/db_test.go index f6c32f8cc7..98d584c8e6 100644 --- a/services/horizon/internal/integration/db_test.go +++ b/services/horizon/internal/integration/db_test.go @@ -14,10 +14,12 @@ import ( "github.com/stellar/go/historyarchive" "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/keypair" + hProtocol "github.com/stellar/go/protocols/horizon" horizoncmd "github.com/stellar/go/services/horizon/cmd" horizon "github.com/stellar/go/services/horizon/internal" "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/services/horizon/internal/db2/schema" + "github.com/stellar/go/services/horizon/internal/ingest/filters" "github.com/stellar/go/services/horizon/internal/test/integration" "github.com/stellar/go/support/collections/set" "github.com/stellar/go/support/db" @@ -416,7 +418,10 @@ func submitAccountOps(itest *integration.Test, tt *assert.Assertions) (submitted } func initializeDBIntegrationTest(t *testing.T) (*integration.Test, int32) { - itest := integration.NewTest(t, integration.Config{}) + itest := integration.NewTest(t, integration.Config{ + HorizonIngestParameters: map[string]string{ + "admin-port": strconv.Itoa(6000), + }}) tt := assert.New(t) // Make sure all possible operations are covered by reingestion @@ -545,6 +550,159 @@ func TestReingestDB(t *testing.T) { tt.NoError(horizoncmd.RootCmd.Execute(), "Repeat the same reingest range against db, should not have errors.") } +func TestReingestDBWithFilterRules(t *testing.T) { + itest, _ := initializeDBIntegrationTest(t) + tt := assert.New(t) + + archive, err := historyarchive.Connect( + itest.GetHorizonIngestConfig().HistoryArchiveURLs[0], + historyarchive.ArchiveOptions{ + NetworkPassphrase: itest.GetHorizonIngestConfig().NetworkPassphrase, + CheckpointFrequency: itest.GetHorizonIngestConfig().CheckpointFrequency, + }) + tt.NoError(err) + + // make sure one full checkpoint has elapsed before making ledger entries + // as test can't reap before first checkpoint in general later in test + publishedFirstCheckpoint := func() bool { + has, requestErr := archive.GetRootHAS() + if requestErr != nil { + t.Logf("request to fetch checkpoint failed: %v", requestErr) + return false + } + return has.CurrentLedger > 1 + } + tt.Eventually(publishedFirstCheckpoint, 10*time.Second, time.Second) + + fullKeys, accounts := itest.CreateAccounts(2, "10000") + whitelistedAccount := accounts[0] + whitelistedAccountKey := fullKeys[0] + nonWhitelistedAccount := accounts[1] + nonWhitelistedAccountKey := fullKeys[1] + enabled := true + + // all assets are allowed by default because the asset filter config is empty. + defaultAllowedAsset := txnbuild.CreditAsset{Code: "PTS", Issuer: itest.Master().Address()} + itest.MustEstablishTrustline(whitelistedAccountKey, whitelistedAccount, defaultAllowedAsset) + itest.MustEstablishTrustline(nonWhitelistedAccountKey, nonWhitelistedAccount, defaultAllowedAsset) + + // Setup a whitelisted account rule, force refresh of filter configs to be quick + filters.SetFilterConfigCheckIntervalSeconds(1) + + expectedAccountFilter := hProtocol.AccountFilterConfig{ + Whitelist: []string{whitelistedAccount.GetAccountID()}, + Enabled: &enabled, + } + err = itest.AdminClient().SetIngestionAccountFilter(expectedAccountFilter) + tt.NoError(err) + + accountFilter, err := itest.AdminClient().GetIngestionAccountFilter() + tt.NoError(err) + + tt.ElementsMatch(expectedAccountFilter.Whitelist, accountFilter.Whitelist) + tt.Equal(expectedAccountFilter.Enabled, accountFilter.Enabled) + + // Ensure the latest filter configs are reloaded by the ingestion state machine processor + time.Sleep(time.Duration(filters.GetFilterConfigCheckIntervalSeconds()) * time.Second) + + // Make sure that when using a non-whitelisted account, the transaction is not stored + nonWhiteListTxResp := itest.MustSubmitOperations(itest.MasterAccount(), itest.Master(), + &txnbuild.Payment{ + Destination: nonWhitelistedAccount.GetAccountID(), + Amount: "10", + Asset: defaultAllowedAsset, + }, + ) + _, err = itest.Client().TransactionDetail(nonWhiteListTxResp.Hash) + tt.True(horizonclient.IsNotFoundError(err)) + + // Make sure that when using a whitelisted account, the transaction is stored + whiteListTxResp := itest.MustSubmitOperations(itest.MasterAccount(), itest.Master(), + &txnbuild.Payment{ + Destination: whitelistedAccount.GetAccountID(), + Amount: "10", + Asset: defaultAllowedAsset, + }, + ) + lastTx, err := itest.Client().TransactionDetail(whiteListTxResp.Hash) + tt.NoError(err) + + reachedLedger := uint32(lastTx.Ledger) + + t.Logf("reached ledger is %v", reachedLedger) + + // make sure a checkpoint has elapsed to lock in the chagnes made on network for reingest later + var latestCheckpoint uint32 + publishedNextCheckpoint := func() bool { + has, requestErr := archive.GetRootHAS() + if requestErr != nil { + t.Logf("request to fetch checkpoint failed: %v", requestErr) + return false + } + latestCheckpoint = has.CurrentLedger + return latestCheckpoint > reachedLedger + } + tt.Eventually(publishedNextCheckpoint, 10*time.Second, time.Second) + + // to test reingestion, stop horizon web and captive core, + // it was used to create ledger entries for test. + itest.StopHorizon() + + // clear the db with reaping all ledgers + horizoncmd.RootCmd.SetArgs(command(t, itest.GetHorizonIngestConfig(), "db", + "reap", + "--history-retention-count=1", + )) + tt.NoError(horizoncmd.RootCmd.Execute()) + + // repopulate the db with reingestion which should catchup using core reapply filter rules + // correctly on reingestion ranged + horizoncmd.RootCmd.SetArgs(command(t, itest.GetHorizonIngestConfig(), "db", + "reingest", + "range", + "1", + fmt.Sprintf("%d", reachedLedger), + )) + + tt.NoError(horizoncmd.RootCmd.Execute()) + + // bring up horizon, just the api server no ingestion, to query + // for tx's that should have been repopulated on db from reingestion per + // filter rule expectations + webApp, err := horizon.NewApp(itest.GetHorizonWebConfig()) + tt.NoError(err) + + webAppDone := make(chan struct{}) + go func() { + webApp.Serve() + close(webAppDone) + }() + + // wait until the web server is up before continuing to test requests + itest.WaitForHorizon() + + // Make sure that a tx from non-whitelisted account is not stored after reingestion + _, err = itest.Client().TransactionDetail(nonWhiteListTxResp.Hash) + tt.True(horizonclient.IsNotFoundError(err)) + + // Make sure that a tx from whitelisted account is stored after reingestion + _, err = itest.Client().TransactionDetail(whiteListTxResp.Hash) + tt.NoError(err) + + // tell the horizon web server to shutdown + webApp.Close() + + // wait for horizon to finish shutdown + tt.Eventually(func() bool { + select { + case <-webAppDone: + return true + default: + return false + } + }, 30*time.Second, time.Second) +} + func getCoreConfigFile(itest *integration.Test) string { coreConfigFile := "captive-core-reingest-range-classic-integration-tests.cfg" if itest.Config().ProtocolVersion >= ledgerbackend.MinimalSorobanProtocolSupport { diff --git a/services/horizon/internal/integration/parameters_test.go b/services/horizon/internal/integration/parameters_test.go index 7d487c556f..2ca05ba351 100644 --- a/services/horizon/internal/integration/parameters_test.go +++ b/services/horizon/internal/integration/parameters_test.go @@ -375,27 +375,6 @@ func TestDisablePathFinding(t *testing.T) { }) } -func TestIngestionFilteringAlwaysDefaultingToTrue(t *testing.T) { - t.Run("ingestion filtering flag set to default value", func(t *testing.T) { - test := integration.NewTest(t, *integration.GetTestConfig()) - err := test.StartHorizon() - assert.NoError(t, err) - test.WaitForHorizon() - assert.Equal(t, test.HorizonIngest().Config().EnableIngestionFiltering, true) - test.Shutdown() - }) - t.Run("ingestion filtering flag set to false", func(t *testing.T) { - testConfig := integration.GetTestConfig() - testConfig.HorizonIngestParameters = map[string]string{"exp-enable-ingestion-filtering": "false"} - test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() - assert.NoError(t, err) - test.WaitForHorizon() - assert.Equal(t, test.HorizonIngest().Config().EnableIngestionFiltering, true) - test.Shutdown() - }) -} - func TestDisableTxSub(t *testing.T) { t.Run("require stellar-core-url when both DISABLE_TX_SUB=false and INGEST=false", func(t *testing.T) { localParams := integration.MergeMaps(networkParamArgs, map[string]string{ diff --git a/services/horizon/internal/test/integration/integration.go b/services/horizon/internal/test/integration/integration.go index 87718d5a67..4c76afe04a 100644 --- a/services/horizon/internal/test/integration/integration.go +++ b/services/horizon/internal/test/integration/integration.go @@ -96,6 +96,7 @@ type Test struct { config Config coreConfig CaptiveConfig horizonIngestConfig horizon.Config + horizonWebConfig horizon.Config environment *test.EnvironmentManager horizonClient *sdk.Client @@ -334,6 +335,10 @@ func (i *Test) GetHorizonIngestConfig() horizon.Config { return i.horizonIngestConfig } +func (i *Test) GetHorizonWebConfig() horizon.Config { + return i.horizonWebConfig +} + // Shutdown stops the integration tests and destroys all its associated // resources. It will be implicitly called when the calling test (i.e. the // `testing.Test` passed to `New()`) is finished if it hasn't been explicitly @@ -401,6 +406,7 @@ func (i *Test) StartHorizon() error { } i.horizonIngestConfig = *ingestConfig + i.horizonWebConfig = *webConfig i.appStopped = &sync.WaitGroup{} i.appStopped.Add(2) From 17e1acc6ba990b62b3c364a30ad9d866e562c56e Mon Sep 17 00:00:00 2001 From: urvisavla Date: Thu, 18 Apr 2024 18:24:57 -0700 Subject: [PATCH 107/234] services/horizon: Update Changelog (#5286) Co-authored-by: shawn --- services/horizon/CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 003bab1d5a..244a8c15c2 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased + +## 2.30.0 + +### Added + +- Bump XDR for [protocol 21](https://github.com/stellar/stellar-xdr/releases/tag/v21.0) +- Make reaping batch sizes configurable via `--history-retention-reap-count` ([5272](https://github.com/stellar/go/pull/5272)) +- Log tx meta when ingestion failures occur ([5268](https://github.com/stellar/go/pull/5268)) +- Add deprecation warning for `--captive-core-use-db` ([5231](https://github.com/stellar/go/pull/5231)) + +### Fixed +- Optimized reingestion by addressing performance slowdown due to unnecessary operations on `history_transactions_filtered_tmp`. Removed obsolete `EnableIngestionFiltering` flag. ([5283](https://github.com/stellar/go/pull/5283)) +- Fix deadlock in parallel ingestion ([5263](https://github.com/stellar/go/pull/5263)) +- Add missing tables to TruncateIngestStateTables() ([5253](https://github.com/stellar/go/pull/5253)) +- Performance improvements in ingest library + - Reduce memory consumption of CheckpointChangeReader during state verification and state rebuild ([5270](https://github.com/stellar/go/pull/5270)) + - Remove unnecessary use of ChangeCompactor to reduce memory bloat during ingestion ([5252](https://github.com/stellar/go/pull/5252)) ## 2.29.0 From 65b1f89229406466e7dc2f90c6c8012a0c412108 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 19 Apr 2024 00:11:13 -0400 Subject: [PATCH 108/234] Fix filename path issues --- ingest/ledgerbackend/cloud_storage_backend.go | 54 ++++++++++++------- support/datastore/datastore.go | 2 +- support/datastore/gcs_datastore.go | 4 +- support/datastore/mock_datastore.go | 4 +- 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index 1686f07083..a162a96151 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "io" - "path/filepath" "strconv" "strings" @@ -43,36 +42,31 @@ func NewCloudStorageBackend(ctx context.Context, storageURL string) (*CloudStora // GetLatestLedgerSequence returns the most recent ledger sequence number in the cloud storage bucket. func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - directories, err := csb.lcmDataStore.ListDirectoryNames(ctx, "") + // Get the latest parition directory from the bucket + directories, err := csb.lcmDataStore.ListDirectoryNames(ctx) if err != nil { return 0, errors.Wrapf(err, "failed getting list of directory names") } - var latestDirectory string - largestDirectoryLedger := 0 - for _, dir := range directories { - dirParts := strings.Split(dir, "/") - lastDirPart := dirParts[len(dirParts)-1] - parts := strings.Split(lastDirPart, "-") - if len(parts) == 2 { - upper, _ := strconv.Atoi(parts[1]) - if upper > largestDirectoryLedger { - latestDirectory = dir - largestDirectoryLedger = upper - } - } - } + latestDirectory := getLatestDirectory(directories) - latestLedgerSequence := uint32(0) + // Search through the latest partition to find the latest file which would be the latestLedgerSequence fileNames, err := csb.lcmDataStore.ListFileNames(ctx, latestDirectory) if err != nil { return 0, errors.Wrapf(err, "failed getting filenames in dir %s", latestDirectory) } + + latestLedgerSequence := uint32(0) + for _, fileName := range fileNames { - ledgerSequence, err := strconv.ParseUint(strings.TrimSuffix(fileName, filepath.Ext(fileName)), 10, 32) + // Trim file down to just the ledgerSequence + fileNameTrimExt := strings.TrimSuffix(fileName, ".xdr.gz") + fileNameTrimPath := strings.TrimPrefix(fileNameTrimExt, latestDirectory+"/") + ledgerSequence, err := strconv.ParseUint(fileNameTrimPath, 10, 32) if err != nil { return 0, errors.Wrapf(err, "failed converting filename to uint32 %s", fileName) } + if uint32(ledgerSequence) > latestLedgerSequence { latestLedgerSequence = uint32(ledgerSequence) } @@ -175,3 +169,27 @@ func GetObjectKeyFromSequenceNumber(ledgerSeq uint32, ledgersPerFile uint32, fil return objectKey, nil } + +func getLatestDirectory(directories []string) string { + var latestDirectory string + largestDirectoryLedger := 0 + + for _, dir := range directories { + // dir follows the format of "ledgers//-" + // Need to split the dir string to retrieve the ledger value to get the latest directory + dirParts := strings.Split(dir, "/") + lastDirPart := dirParts[len(dirParts)-1] + parts := strings.Split(lastDirPart, "-") + + if len(parts) == 2 { + upper, _ := strconv.Atoi(parts[1]) + + if upper > largestDirectoryLedger { + latestDirectory = dir + largestDirectoryLedger = upper + } + } + } + + return latestDirectory +} diff --git a/support/datastore/datastore.go b/support/datastore/datastore.go index eecf67d09b..50eefa4181 100644 --- a/support/datastore/datastore.go +++ b/support/datastore/datastore.go @@ -20,7 +20,7 @@ type DataStore interface { Exists(ctx context.Context, path string) (bool, error) Size(ctx context.Context, path string) (int64, error) Close() error - ListDirectoryNames(ctx context.Context, path string) ([]string, error) + ListDirectoryNames(ctx context.Context) ([]string, error) ListFileNames(ctx context.Context, path string) ([]string, error) } diff --git a/support/datastore/gcs_datastore.go b/support/datastore/gcs_datastore.go index e0324589f4..4a4515e7d5 100644 --- a/support/datastore/gcs_datastore.go +++ b/support/datastore/gcs_datastore.go @@ -107,10 +107,10 @@ func (b *GCSDataStore) putFile(ctx context.Context, filePath string, in io.Write return w.Close() } -func (b *GCSDataStore) ListDirectoryNames(ctx context.Context, path string) ([]string, error) { +func (b *GCSDataStore) ListDirectoryNames(ctx context.Context) ([]string, error) { var directories []string - o := b.bucket.Objects(ctx, &storage.Query{Delimiter: "/"}) + o := b.bucket.Objects(ctx, &storage.Query{Prefix: b.prefix, Delimiter: "/"}) for { attrs, err := o.Next() if err == iterator.Done { diff --git a/support/datastore/mock_datastore.go b/support/datastore/mock_datastore.go index 2788c81447..7e97edf3fd 100644 --- a/support/datastore/mock_datastore.go +++ b/support/datastore/mock_datastore.go @@ -42,8 +42,8 @@ func (m *MockDataStore) Close() error { return args.Error(0) } -func (m *MockDataStore) ListDirectoryNames(ctx context.Context, path string) ([]string, error) { - args := m.Called(ctx, path) +func (m *MockDataStore) ListDirectoryNames(ctx context.Context) ([]string, error) { + args := m.Called(ctx) return args.Get(0).([]string), args.Error(0) } From d1a4126f1aaae36f2eb6d8492f11a13b51287f44 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 19 Apr 2024 00:18:39 -0400 Subject: [PATCH 109/234] debug statements --- ingest/ledgerbackend/cloud_storage_backend.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index a162a96151..543bceaac5 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -49,6 +49,7 @@ func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (ui } latestDirectory := getLatestDirectory(directories) + fmt.Printf("latestDirectory %s\n", latestDirectory) // Search through the latest partition to find the latest file which would be the latestLedgerSequence fileNames, err := csb.lcmDataStore.ListFileNames(ctx, latestDirectory) @@ -62,6 +63,7 @@ func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (ui // Trim file down to just the ledgerSequence fileNameTrimExt := strings.TrimSuffix(fileName, ".xdr.gz") fileNameTrimPath := strings.TrimPrefix(fileNameTrimExt, latestDirectory+"/") + fmt.Printf("fileNameTrimPath %s \n", fileNameTrimPath) ledgerSequence, err := strconv.ParseUint(fileNameTrimPath, 10, 32) if err != nil { return 0, errors.Wrapf(err, "failed converting filename to uint32 %s", fileName) From 70b2b29d22bad03019913a1b47256d619b438318 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 19 Apr 2024 00:34:50 -0400 Subject: [PATCH 110/234] debug --- ingest/ledgerbackend/cloud_storage_backend.go | 1 + 1 file changed, 1 insertion(+) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index 543bceaac5..fa9b4118e4 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -177,6 +177,7 @@ func getLatestDirectory(directories []string) string { largestDirectoryLedger := 0 for _, dir := range directories { + fmt.Printf("dir %s\n", dir) // dir follows the format of "ledgers//-" // Need to split the dir string to retrieve the ledger value to get the latest directory dirParts := strings.Split(dir, "/") From f9cf5b6dfee7f5c2dd8527104fd06e757940c7d5 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 19 Apr 2024 00:41:46 -0400 Subject: [PATCH 111/234] debug --- support/datastore/gcs_datastore.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/support/datastore/gcs_datastore.go b/support/datastore/gcs_datastore.go index 4a4515e7d5..7d2a54372b 100644 --- a/support/datastore/gcs_datastore.go +++ b/support/datastore/gcs_datastore.go @@ -110,7 +110,7 @@ func (b *GCSDataStore) putFile(ctx context.Context, filePath string, in io.Write func (b *GCSDataStore) ListDirectoryNames(ctx context.Context) ([]string, error) { var directories []string - o := b.bucket.Objects(ctx, &storage.Query{Prefix: b.prefix, Delimiter: "/"}) + o := b.bucket.Objects(ctx, &storage.Query{Prefix: b.prefix + "/", Delimiter: "/"}) for { attrs, err := o.Next() if err == iterator.Done { From 2d7308b67c07f2d2f96fa05a6356baa96e3f7b56 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 19 Apr 2024 00:44:05 -0400 Subject: [PATCH 112/234] Remove debug prints --- ingest/ledgerbackend/cloud_storage_backend.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index fa9b4118e4..a162a96151 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -49,7 +49,6 @@ func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (ui } latestDirectory := getLatestDirectory(directories) - fmt.Printf("latestDirectory %s\n", latestDirectory) // Search through the latest partition to find the latest file which would be the latestLedgerSequence fileNames, err := csb.lcmDataStore.ListFileNames(ctx, latestDirectory) @@ -63,7 +62,6 @@ func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (ui // Trim file down to just the ledgerSequence fileNameTrimExt := strings.TrimSuffix(fileName, ".xdr.gz") fileNameTrimPath := strings.TrimPrefix(fileNameTrimExt, latestDirectory+"/") - fmt.Printf("fileNameTrimPath %s \n", fileNameTrimPath) ledgerSequence, err := strconv.ParseUint(fileNameTrimPath, 10, 32) if err != nil { return 0, errors.Wrapf(err, "failed converting filename to uint32 %s", fileName) @@ -177,7 +175,6 @@ func getLatestDirectory(directories []string) string { largestDirectoryLedger := 0 for _, dir := range directories { - fmt.Printf("dir %s\n", dir) // dir follows the format of "ledgers//-" // Need to split the dir string to retrieve the ledger value to get the latest directory dirParts := strings.Split(dir, "/") From 20695352368ed97744d06bb367edc16993e035b1 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 19 Apr 2024 01:45:45 -0400 Subject: [PATCH 113/234] Add tests --- ingest/ledgerbackend/cloud_storage_backend.go | 51 ++++++++++++------- .../cloud_storage_backend_test.go | 26 ++++++++++ 2 files changed, 59 insertions(+), 18 deletions(-) create mode 100644 ingest/ledgerbackend/cloud_storage_backend_test.go diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index a162a96151..445008ab8c 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -48,7 +48,10 @@ func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (ui return 0, errors.Wrapf(err, "failed getting list of directory names") } - latestDirectory := getLatestDirectory(directories) + latestDirectory, err := getLatestDirectory(directories) + if err != nil { + return 0, errors.Wrapf(err, "failed getting latest directory") + } // Search through the latest partition to find the latest file which would be the latestLedgerSequence fileNames, err := csb.lcmDataStore.ListFileNames(ctx, latestDirectory) @@ -56,20 +59,9 @@ func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (ui return 0, errors.Wrapf(err, "failed getting filenames in dir %s", latestDirectory) } - latestLedgerSequence := uint32(0) - - for _, fileName := range fileNames { - // Trim file down to just the ledgerSequence - fileNameTrimExt := strings.TrimSuffix(fileName, ".xdr.gz") - fileNameTrimPath := strings.TrimPrefix(fileNameTrimExt, latestDirectory+"/") - ledgerSequence, err := strconv.ParseUint(fileNameTrimPath, 10, 32) - if err != nil { - return 0, errors.Wrapf(err, "failed converting filename to uint32 %s", fileName) - } - - if uint32(ledgerSequence) > latestLedgerSequence { - latestLedgerSequence = uint32(ledgerSequence) - } + latestLedgerSequence, err := getLatestFileNameLedgerSequence(fileNames, latestDirectory) + if err != nil { + return 0, errors.Wrapf(err, "failed converting filename to ledger sequence") } return latestLedgerSequence, nil @@ -170,7 +162,7 @@ func GetObjectKeyFromSequenceNumber(ledgerSeq uint32, ledgersPerFile uint32, fil return objectKey, nil } -func getLatestDirectory(directories []string) string { +func getLatestDirectory(directories []string) (string, error) { var latestDirectory string largestDirectoryLedger := 0 @@ -182,7 +174,10 @@ func getLatestDirectory(directories []string) string { parts := strings.Split(lastDirPart, "-") if len(parts) == 2 { - upper, _ := strconv.Atoi(parts[1]) + upper, err := strconv.Atoi(parts[1]) + if err != nil { + return "", errors.Wrapf(err, "failed getting latest directory %s", dir) + } if upper > largestDirectoryLedger { latestDirectory = dir @@ -191,5 +186,25 @@ func getLatestDirectory(directories []string) string { } } - return latestDirectory + return latestDirectory, nil +} + +func getLatestFileNameLedgerSequence(fileNames []string, directory string) (uint32, error) { + latestLedgerSequence := uint32(0) + + for _, fileName := range fileNames { + // Trim file down to just the ledgerSequence + fileNameTrimExt := strings.TrimSuffix(fileName, ".xdr.gz") + fileNameTrimPath := strings.TrimPrefix(fileNameTrimExt, directory+"/") + ledgerSequence, err := strconv.ParseUint(fileNameTrimPath, 10, 32) + if err != nil { + return uint32(0), errors.Wrapf(err, "failed converting filename to uint32 %s", fileName) + } + + if uint32(ledgerSequence) > latestLedgerSequence { + latestLedgerSequence = uint32(ledgerSequence) + } + } + + return latestLedgerSequence, nil } diff --git a/ingest/ledgerbackend/cloud_storage_backend_test.go b/ingest/ledgerbackend/cloud_storage_backend_test.go new file mode 100644 index 0000000000..cdbd0e4dc7 --- /dev/null +++ b/ingest/ledgerbackend/cloud_storage_backend_test.go @@ -0,0 +1,26 @@ +package ledgerbackend + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetLatestFileNameLedgerSequence(t *testing.T) { + directory := "ledgers/pubnet/21-30" + filenames := []string{ + "ledgers/pubnet/21-30/21.xdr.gz", + "ledgers/pubnet/21-30/22.xdr.gz", + "ledgers/pubnet/21-30/23.xdr.gz", + } + latestLedgerSequence, _ := getLatestFileNameLedgerSequence(filenames, directory) + + assert.Equal(t, uint32(23), latestLedgerSequence) +} + +func TestGetLatestDirectory(t *testing.T) { + directories := []string{"ledgers/pubnet/1-10", "ledgers/pubnet/11-20", "ledgers/pubnet/21-30"} + latestDirectory, _ := getLatestDirectory(directories) + + assert.Equal(t, "ledgers/pubnet/21-30", latestDirectory) +} From dad2ee55a8b3eab75e34fccbf04d8d460ef22710 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 19 Apr 2024 10:57:12 -0400 Subject: [PATCH 114/234] Update filesuffix --- ingest/ledgerbackend/cloud_storage_backend.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index 445008ab8c..552eaf8441 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -194,7 +194,7 @@ func getLatestFileNameLedgerSequence(fileNames []string, directory string) (uint for _, fileName := range fileNames { // Trim file down to just the ledgerSequence - fileNameTrimExt := strings.TrimSuffix(fileName, ".xdr.gz") + fileNameTrimExt := strings.TrimSuffix(fileName, fileSuffix) fileNameTrimPath := strings.TrimPrefix(fileNameTrimExt, directory+"/") ledgerSequence, err := strconv.ParseUint(fileNameTrimPath, 10, 32) if err != nil { From e9fc4e403b77e3f7de7612c039595e3c9654e7a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 18:21:50 +0100 Subject: [PATCH 115/234] build(deps): bump golang.org/x/net from 0.20.0 to 0.23.0 (#5287) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.20.0 to 0.23.0. - [Commits](https://github.com/golang/net/compare/v0.20.0...v0.23.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 65d73fa328..5c3c8c3189 100644 --- a/go.mod +++ b/go.mod @@ -147,12 +147,12 @@ require ( github.com/yudai/golcs v0.0.0-20150405163532-d1c525dea8ce // indirect github.com/yudai/pp v2.0.1+incompatible // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.18.0 // indirect + golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d - golang.org/x/net v0.20.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/term v0.16.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 google.golang.org/appengine v1.6.8 // indirect diff --git a/go.sum b/go.sum index 0fe77e5201..531fa5ea13 100644 --- a/go.sum +++ b/go.sum @@ -502,8 +502,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -580,8 +580,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -655,13 +655,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 50804c5151f5404e4f2d63dd9751c0ce701b6145 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Apr 2024 17:33:06 +0000 Subject: [PATCH 116/234] build(deps): bump google.golang.org/protobuf from 1.32.0 to 1.33.0 (#5245) Bumps google.golang.org/protobuf from 1.32.0 to 1.33.0. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5c3c8c3189..f45b68de3c 100644 --- a/go.mod +++ b/go.mod @@ -158,7 +158,7 @@ require ( google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect google.golang.org/grpc v1.60.1 // indirect - google.golang.org/protobuf v1.32.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 531fa5ea13..2ea7e9a01c 100644 --- a/go.sum +++ b/go.sum @@ -839,8 +839,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From f18e32fd1ced14ed8a2c6e8ecdb870bd1fabbbb2 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 19 Apr 2024 18:09:40 -0400 Subject: [PATCH 117/234] Address comments/fixes --- ingest/ledgerbackend/cloud_storage_backend.go | 68 ++++++++++++------- .../cloud_storage_backend_test.go | 12 +++- support/datastore/gcs_datastore.go | 2 +- 3 files changed, 54 insertions(+), 28 deletions(-) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index 552eaf8441..fd5c1e282b 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io" + "path" "strconv" "strings" @@ -13,31 +14,42 @@ import ( "github.com/stellar/go/xdr" ) -// Suffix for TxMeta files -const ( - fileSuffix = ".xdr.gz" - ledgersPerFile = 1 - filesPerPartition = 64000 -) - // Ensure CloudStorageBackend implements LedgerBackend var _ LedgerBackend = (*CloudStorageBackend)(nil) +type LCMFileConfig struct { + StorageURL string + FileSuffix string + LedgersPerFile uint32 + FilesPerPartition uint32 +} + // CloudStorageBackend is a ledger backend that reads from a cloud storage service. // The cloud storage service contains files generated from the ledgerExporter. type CloudStorageBackend struct { - lcmDataStore datastore.DataStore - storageURL string + lcmDataStore datastore.DataStore + storageURL string + fileSuffix string + ledgersPerFile uint32 + filesPerPartition uint32 } // Return a new CloudStorageBackend instance. -func NewCloudStorageBackend(ctx context.Context, storageURL string) (*CloudStorageBackend, error) { - lcmDataStore, err := datastore.NewDataStore(ctx, storageURL) +func NewCloudStorageBackend(ctx context.Context, fileConfig LCMFileConfig) (*CloudStorageBackend, error) { + lcmDataStore, err := datastore.NewDataStore(ctx, fileConfig.StorageURL) if err != nil { return nil, err } - return &CloudStorageBackend{lcmDataStore: lcmDataStore, storageURL: storageURL}, nil + cloudStorageBackend := &CloudStorageBackend{ + lcmDataStore: lcmDataStore, + storageURL: fileConfig.StorageURL, + fileSuffix: fileConfig.FileSuffix, + ledgersPerFile: fileConfig.LedgersPerFile, + filesPerPartition: fileConfig.FilesPerPartition, + } + + return cloudStorageBackend, nil } // GetLatestLedgerSequence returns the most recent ledger sequence number in the cloud storage bucket. @@ -48,7 +60,7 @@ func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (ui return 0, errors.Wrapf(err, "failed getting list of directory names") } - latestDirectory, err := getLatestDirectory(directories) + latestDirectory, err := csb.GetLatestDirectory(directories) if err != nil { return 0, errors.Wrapf(err, "failed getting latest directory") } @@ -59,7 +71,7 @@ func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (ui return 0, errors.Wrapf(err, "failed getting filenames in dir %s", latestDirectory) } - latestLedgerSequence, err := getLatestFileNameLedgerSequence(fileNames, latestDirectory) + latestLedgerSequence, err := csb.GetLatestFileNameLedgerSequence(fileNames, latestDirectory) if err != nil { return 0, errors.Wrapf(err, "failed converting filename to ledger sequence") } @@ -71,7 +83,7 @@ func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (ui func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { var ledgerCloseMetaBatch xdr.LedgerCloseMetaBatch - objectKey, err := GetObjectKeyFromSequenceNumber(sequence, ledgersPerFile, filesPerPartition) + objectKey, err := csb.GetObjectKeyFromSequenceNumber(sequence, csb.ledgersPerFile, csb.filesPerPartition) if err != nil { return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed to get object key for ledger %d", sequence) } @@ -100,7 +112,12 @@ func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed unmarshalling file: %s", objectKey) } - ledgerCloseMetasIndex := sequence - uint32(ledgerCloseMetaBatch.StartSequence) + startSequence := uint32(ledgerCloseMetaBatch.StartSequence) + if startSequence > sequence { + return xdr.LedgerCloseMeta{}, errors.Errorf("start sequence: %d; greater than sequence to get: %d", startSequence, sequence) + } + + ledgerCloseMetasIndex := sequence - startSequence ledgerCloseMeta := ledgerCloseMetaBatch.LedgerCloseMetas[ledgerCloseMetasIndex] return ledgerCloseMeta, nil @@ -135,7 +152,7 @@ func (csb *CloudStorageBackend) Close() error { // TODO: Should this function also be modified and added to support/datastore? // This function should be shared between ledger exporter and this legerbackend reader -func GetObjectKeyFromSequenceNumber(ledgerSeq uint32, ledgersPerFile uint32, filesPerPartition uint32) (string, error) { +func (csb *CloudStorageBackend) GetObjectKeyFromSequenceNumber(ledgerSeq uint32, ledgersPerFile uint32, filesPerPartition uint32) (string, error) { var objectKey string if ledgersPerFile < 1 { @@ -157,21 +174,21 @@ func GetObjectKeyFromSequenceNumber(ledgerSeq uint32, ledgersPerFile uint32, fil if fileStart != fileEnd { objectKey += fmt.Sprintf("-%d", fileEnd) } - objectKey += fileSuffix + objectKey += csb.fileSuffix return objectKey, nil } -func getLatestDirectory(directories []string) (string, error) { +func (csb *CloudStorageBackend) GetLatestDirectory(directories []string) (string, error) { var latestDirectory string largestDirectoryLedger := 0 for _, dir := range directories { // dir follows the format of "ledgers//-" // Need to split the dir string to retrieve the ledger value to get the latest directory - dirParts := strings.Split(dir, "/") - lastDirPart := dirParts[len(dirParts)-1] - parts := strings.Split(lastDirPart, "-") + dirTruncSlash := strings.TrimSuffix(dir, "/") + _, dirName := path.Split(dirTruncSlash) + parts := strings.Split(dirName, "-") if len(parts) == 2 { upper, err := strconv.Atoi(parts[1]) @@ -189,12 +206,13 @@ func getLatestDirectory(directories []string) (string, error) { return latestDirectory, nil } -func getLatestFileNameLedgerSequence(fileNames []string, directory string) (uint32, error) { +func (csb *CloudStorageBackend) GetLatestFileNameLedgerSequence(fileNames []string, directory string) (uint32, error) { latestLedgerSequence := uint32(0) for _, fileName := range fileNames { - // Trim file down to just the ledgerSequence - fileNameTrimExt := strings.TrimSuffix(fileName, fileSuffix) + // fileName follows the format of "ledgers//-/." + // Trim the file down to just the + fileNameTrimExt := strings.TrimSuffix(fileName, csb.fileSuffix) fileNameTrimPath := strings.TrimPrefix(fileNameTrimExt, directory+"/") ledgerSequence, err := strconv.ParseUint(fileNameTrimPath, 10, 32) if err != nil { diff --git a/ingest/ledgerbackend/cloud_storage_backend_test.go b/ingest/ledgerbackend/cloud_storage_backend_test.go index cdbd0e4dc7..78a0367b84 100644 --- a/ingest/ledgerbackend/cloud_storage_backend_test.go +++ b/ingest/ledgerbackend/cloud_storage_backend_test.go @@ -6,21 +6,29 @@ import ( "github.com/stretchr/testify/assert" ) +func MockCloudStorageBackend() CloudStorageBackend { + return CloudStorageBackend{ + fileSuffix: ".xdr.gz", + } +} + func TestGetLatestFileNameLedgerSequence(t *testing.T) { + csb := MockCloudStorageBackend() directory := "ledgers/pubnet/21-30" filenames := []string{ "ledgers/pubnet/21-30/21.xdr.gz", "ledgers/pubnet/21-30/22.xdr.gz", "ledgers/pubnet/21-30/23.xdr.gz", } - latestLedgerSequence, _ := getLatestFileNameLedgerSequence(filenames, directory) + latestLedgerSequence, _ := csb.GetLatestFileNameLedgerSequence(filenames, directory) assert.Equal(t, uint32(23), latestLedgerSequence) } func TestGetLatestDirectory(t *testing.T) { + csb := MockCloudStorageBackend() directories := []string{"ledgers/pubnet/1-10", "ledgers/pubnet/11-20", "ledgers/pubnet/21-30"} - latestDirectory, _ := getLatestDirectory(directories) + latestDirectory, _ := csb.GetLatestDirectory(directories) assert.Equal(t, "ledgers/pubnet/21-30", latestDirectory) } diff --git a/support/datastore/gcs_datastore.go b/support/datastore/gcs_datastore.go index 7d2a54372b..f086f56e21 100644 --- a/support/datastore/gcs_datastore.go +++ b/support/datastore/gcs_datastore.go @@ -117,7 +117,7 @@ func (b *GCSDataStore) ListDirectoryNames(ctx context.Context) ([]string, error) break } if err != nil { - log.Fatal(err) + return nil, err } if attrs.Prefix != "" { directories = append(directories, strings.TrimSuffix(attrs.Prefix, "/")) From 2346a6c7bb55c6eed6144e3def99588d8d4df91b Mon Sep 17 00:00:00 2001 From: chowbao Date: Fri, 19 Apr 2024 18:10:12 -0400 Subject: [PATCH 118/234] Update ingest/ledgerbackend/cloud_storage_backend.go Co-authored-by: George --- ingest/ledgerbackend/cloud_storage_backend.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index fd5c1e282b..f902e09b7c 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -219,9 +219,7 @@ func (csb *CloudStorageBackend) GetLatestFileNameLedgerSequence(fileNames []stri return uint32(0), errors.Wrapf(err, "failed converting filename to uint32 %s", fileName) } - if uint32(ledgerSequence) > latestLedgerSequence { - latestLedgerSequence = uint32(ledgerSequence) - } + latestLedgerSequence = ordered.Max(latestLedgerSequence, uint32(ledgerSequence)) } return latestLedgerSequence, nil From 3a79646669abed6c704a29e81c30933d6cd946d6 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 19 Apr 2024 18:26:46 -0400 Subject: [PATCH 119/234] Address comments --- ...loud_storage_backend.go => gcs_backend.go} | 55 ++++++++++--------- ...ge_backend_test.go => gcs_backend_test.go} | 8 +-- 2 files changed, 32 insertions(+), 31 deletions(-) rename ingest/ledgerbackend/{cloud_storage_backend.go => gcs_backend.go} (73%) rename ingest/ledgerbackend/{cloud_storage_backend_test.go => gcs_backend_test.go} (83%) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/gcs_backend.go similarity index 73% rename from ingest/ledgerbackend/cloud_storage_backend.go rename to ingest/ledgerbackend/gcs_backend.go index f902e09b7c..08047bc341 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/gcs_backend.go @@ -11,11 +11,12 @@ import ( "github.com/pkg/errors" "github.com/stellar/go/support/datastore" + "github.com/stellar/go/support/ordered" "github.com/stellar/go/xdr" ) -// Ensure CloudStorageBackend implements LedgerBackend -var _ LedgerBackend = (*CloudStorageBackend)(nil) +// Ensure GCSBackend implements LedgerBackend +var _ LedgerBackend = (*GCSBackend)(nil) type LCMFileConfig struct { StorageURL string @@ -24,9 +25,9 @@ type LCMFileConfig struct { FilesPerPartition uint32 } -// CloudStorageBackend is a ledger backend that reads from a cloud storage service. +// GCSBackend is a ledger backend that reads from a cloud storage service. // The cloud storage service contains files generated from the ledgerExporter. -type CloudStorageBackend struct { +type GCSBackend struct { lcmDataStore datastore.DataStore storageURL string fileSuffix string @@ -34,14 +35,14 @@ type CloudStorageBackend struct { filesPerPartition uint32 } -// Return a new CloudStorageBackend instance. -func NewCloudStorageBackend(ctx context.Context, fileConfig LCMFileConfig) (*CloudStorageBackend, error) { +// Return a new GCSBackend instance. +func NewGCSBackend(ctx context.Context, fileConfig LCMFileConfig) (*GCSBackend, error) { lcmDataStore, err := datastore.NewDataStore(ctx, fileConfig.StorageURL) if err != nil { return nil, err } - cloudStorageBackend := &CloudStorageBackend{ + cloudStorageBackend := &GCSBackend{ lcmDataStore: lcmDataStore, storageURL: fileConfig.StorageURL, fileSuffix: fileConfig.FileSuffix, @@ -53,25 +54,25 @@ func NewCloudStorageBackend(ctx context.Context, fileConfig LCMFileConfig) (*Clo } // GetLatestLedgerSequence returns the most recent ledger sequence number in the cloud storage bucket. -func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { +func (gcsb *GCSBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { // Get the latest parition directory from the bucket - directories, err := csb.lcmDataStore.ListDirectoryNames(ctx) + directories, err := gcsb.lcmDataStore.ListDirectoryNames(ctx) if err != nil { return 0, errors.Wrapf(err, "failed getting list of directory names") } - latestDirectory, err := csb.GetLatestDirectory(directories) + latestDirectory, err := gcsb.GetLatestDirectory(directories) if err != nil { return 0, errors.Wrapf(err, "failed getting latest directory") } // Search through the latest partition to find the latest file which would be the latestLedgerSequence - fileNames, err := csb.lcmDataStore.ListFileNames(ctx, latestDirectory) + fileNames, err := gcsb.lcmDataStore.ListFileNames(ctx, latestDirectory) if err != nil { return 0, errors.Wrapf(err, "failed getting filenames in dir %s", latestDirectory) } - latestLedgerSequence, err := csb.GetLatestFileNameLedgerSequence(fileNames, latestDirectory) + latestLedgerSequence, err := gcsb.GetLatestFileNameLedgerSequence(fileNames, latestDirectory) if err != nil { return 0, errors.Wrapf(err, "failed converting filename to ledger sequence") } @@ -80,15 +81,15 @@ func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (ui } // GetLedger returns the LedgerCloseMeta for the specified ledger sequence number -func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { +func (gcsb *GCSBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { var ledgerCloseMetaBatch xdr.LedgerCloseMetaBatch - objectKey, err := csb.GetObjectKeyFromSequenceNumber(sequence, csb.ledgersPerFile, csb.filesPerPartition) + objectKey, err := gcsb.GetObjectKeyFromSequenceNumber(sequence, gcsb.ledgersPerFile, gcsb.filesPerPartition) if err != nil { return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed to get object key for ledger %d", sequence) } - reader, err := csb.lcmDataStore.GetFile(ctx, objectKey) + reader, err := gcsb.lcmDataStore.GetFile(ctx, objectKey) if err != nil { return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed getting file: %s", objectKey) } @@ -124,14 +125,14 @@ func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) } // PrepareRange checks if the starting and ending (if bounded) ledgers exist. -func (csb *CloudStorageBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { - _, err := csb.GetLedger(ctx, ledgerRange.from) +func (gcsb *GCSBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { + _, err := gcsb.GetLedger(ctx, ledgerRange.from) if err != nil { return errors.Wrapf(err, "error getting ledger %d", ledgerRange.from) } if ledgerRange.bounded { - _, err := csb.GetLedger(ctx, ledgerRange.to) + _, err := gcsb.GetLedger(ctx, ledgerRange.to) if err != nil { return errors.Wrapf(err, "error getting ending ledger %d", ledgerRange.to) } @@ -140,19 +141,19 @@ func (csb *CloudStorageBackend) PrepareRange(ctx context.Context, ledgerRange Ra return nil } -// IsPrepared is a no-op for CloudStorageBackend. -func (csb *CloudStorageBackend) IsPrepared(ctx context.Context, ledgerRange Range) (bool, error) { +// IsPrepared is a no-op for GCSBackend. +func (gcsb *GCSBackend) IsPrepared(ctx context.Context, ledgerRange Range) (bool, error) { return true, nil } -// Close is a no-op for CloudStorageBackend. -func (csb *CloudStorageBackend) Close() error { +// Close is a no-op for GCSBackend. +func (gcsb *GCSBackend) Close() error { return nil } // TODO: Should this function also be modified and added to support/datastore? // This function should be shared between ledger exporter and this legerbackend reader -func (csb *CloudStorageBackend) GetObjectKeyFromSequenceNumber(ledgerSeq uint32, ledgersPerFile uint32, filesPerPartition uint32) (string, error) { +func (gcsb *GCSBackend) GetObjectKeyFromSequenceNumber(ledgerSeq uint32, ledgersPerFile uint32, filesPerPartition uint32) (string, error) { var objectKey string if ledgersPerFile < 1 { @@ -174,12 +175,12 @@ func (csb *CloudStorageBackend) GetObjectKeyFromSequenceNumber(ledgerSeq uint32, if fileStart != fileEnd { objectKey += fmt.Sprintf("-%d", fileEnd) } - objectKey += csb.fileSuffix + objectKey += gcsb.fileSuffix return objectKey, nil } -func (csb *CloudStorageBackend) GetLatestDirectory(directories []string) (string, error) { +func (gcsb *GCSBackend) GetLatestDirectory(directories []string) (string, error) { var latestDirectory string largestDirectoryLedger := 0 @@ -206,13 +207,13 @@ func (csb *CloudStorageBackend) GetLatestDirectory(directories []string) (string return latestDirectory, nil } -func (csb *CloudStorageBackend) GetLatestFileNameLedgerSequence(fileNames []string, directory string) (uint32, error) { +func (gcsb *GCSBackend) GetLatestFileNameLedgerSequence(fileNames []string, directory string) (uint32, error) { latestLedgerSequence := uint32(0) for _, fileName := range fileNames { // fileName follows the format of "ledgers//-/." // Trim the file down to just the - fileNameTrimExt := strings.TrimSuffix(fileName, csb.fileSuffix) + fileNameTrimExt := strings.TrimSuffix(fileName, gcsb.fileSuffix) fileNameTrimPath := strings.TrimPrefix(fileNameTrimExt, directory+"/") ledgerSequence, err := strconv.ParseUint(fileNameTrimPath, 10, 32) if err != nil { diff --git a/ingest/ledgerbackend/cloud_storage_backend_test.go b/ingest/ledgerbackend/gcs_backend_test.go similarity index 83% rename from ingest/ledgerbackend/cloud_storage_backend_test.go rename to ingest/ledgerbackend/gcs_backend_test.go index 78a0367b84..71d3e23f12 100644 --- a/ingest/ledgerbackend/cloud_storage_backend_test.go +++ b/ingest/ledgerbackend/gcs_backend_test.go @@ -6,14 +6,14 @@ import ( "github.com/stretchr/testify/assert" ) -func MockCloudStorageBackend() CloudStorageBackend { - return CloudStorageBackend{ +func MockGCSBackend() GCSBackend { + return GCSBackend{ fileSuffix: ".xdr.gz", } } func TestGetLatestFileNameLedgerSequence(t *testing.T) { - csb := MockCloudStorageBackend() + csb := MockGCSBackend() directory := "ledgers/pubnet/21-30" filenames := []string{ "ledgers/pubnet/21-30/21.xdr.gz", @@ -26,7 +26,7 @@ func TestGetLatestFileNameLedgerSequence(t *testing.T) { } func TestGetLatestDirectory(t *testing.T) { - csb := MockCloudStorageBackend() + csb := MockGCSBackend() directories := []string{"ledgers/pubnet/1-10", "ledgers/pubnet/11-20", "ledgers/pubnet/21-30"} latestDirectory, _ := csb.GetLatestDirectory(directories) From 1042b624e994ef03a1cc52f2c2f9cd6660335b8c Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Sun, 21 Apr 2024 22:07:19 +0200 Subject: [PATCH 120/234] horizon: Clean up integration test variables (#5288) All tests use captive-core so there is no need to specify it explicitly. Soroban-rpc is enabled on a test by test based so there is no need to guard it through its own env variable. --- .github/workflows/horizon.yml | 5 +-- .../internal/integration/parameters_test.go | 24 +------------ .../internal/test/integration/integration.go | 35 +++++++------------ 3 files changed, 14 insertions(+), 50 deletions(-) diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index 98c71e89f7..5ec7c934a5 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -77,9 +77,8 @@ jobs: run: | docker pull "$PROTOCOL_${{ matrix.protocol-version }}_SOROBAN_RPC_DOCKER_IMG" echo HORIZON_INTEGRATION_TESTS_SOROBAN_RPC_DOCKER_IMG="$PROTOCOL_${{ matrix.protocol-version }}_SOROBAN_RPC_DOCKER_IMG" >> $GITHUB_ENV - echo HORIZON_INTEGRATION_TESTS_ENABLE_SOROBAN_RPC=true >> $GITHUB_ENV - - name: Install and enable Captive Core + - name: Install core run: | # Workaround for https://github.com/actions/virtual-environments/issues/5245, # libc++1-8 won't be installed if another version is installed (but apt won't give you a helpul @@ -91,9 +90,7 @@ jobs: sudo bash -c 'echo "deb https://apt.stellar.org focal unstable" > /etc/apt/sources.list.d/SDF-unstable.list' sudo apt-get update && sudo apt-get install -y stellar-core="$PROTOCOL_${{ matrix.protocol-version }}_CORE_DEBIAN_PKG_VERSION" echo "Using stellar core version $(stellar-core version)" - echo 'HORIZON_INTEGRATION_TESTS_ENABLE_CAPTIVE_CORE=true' >> $GITHUB_ENV echo 'HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_BIN=/usr/bin/stellar-core' >> $GITHUB_ENV - echo 'HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_USE_DB=true' >> $GITHUB_ENV - name: Build Horizon reproducible build run: | diff --git a/services/horizon/internal/integration/parameters_test.go b/services/horizon/internal/integration/parameters_test.go index 2ca05ba351..eb22e9a068 100644 --- a/services/horizon/internal/integration/parameters_test.go +++ b/services/horizon/internal/integration/parameters_test.go @@ -15,6 +15,7 @@ import ( "time" "github.com/spf13/cobra" + "github.com/stellar/go/services/horizon/internal/paths" "github.com/stellar/go/services/horizon/internal/simplepath" @@ -63,15 +64,6 @@ var ( // Ensures that BUCKET_DIR_PATH is not an allowed value for Captive Core. func TestBucketDirDisallowed(t *testing.T) { - // This is a bit of a hacky workaround. - // - // In CI, we run our integration tests twice: once with Captive Core - // enabled, and once without. *These* tests only run with Captive Core - // configured properly (specifically, w/ the CAPTIVE_CORE_BIN envvar set). - if !integration.RunWithCaptiveCore { - t.Skip() - } - config := `BUCKET_DIR_PATH="/tmp" ` + SimpleCaptiveCoreToml @@ -134,10 +126,6 @@ func TestEnvironmentPreserved(t *testing.T) { // using NETWORK environment variables, history archive urls or network passphrase // parameters are also set. func TestInvalidNetworkParameters(t *testing.T) { - if !integration.RunWithCaptiveCore { - t.Skip() - } - var captiveCoreConfigErrMsg = integration.HorizonInitErrStr + ": error generating captive " + "core configuration: invalid config: %s parameter not allowed with the %s parameter" testCases := []struct { @@ -191,9 +179,6 @@ func TestInvalidNetworkParameters(t *testing.T) { // success. However, for "pubnet" or "testnet," we can not wait for Horizon to catch up, // so we skip starting stellar-core containers. func TestNetworkParameter(t *testing.T) { - if !integration.RunWithCaptiveCore { - t.Skip() - } testCases := []struct { networkValue string networkPassphrase string @@ -241,9 +226,6 @@ func TestNetworkParameter(t *testing.T) { // success. However, for "pubnet" or "testnet," we can not wait for Horizon to catch up, // so we skip starting stellar-core containers. func TestNetworkEnvironmentVariable(t *testing.T) { - if !integration.RunWithCaptiveCore { - t.Skip() - } testCases := []string{ horizon.StellarPubnet, horizon.StellarTestnet, @@ -277,10 +259,6 @@ func TestNetworkEnvironmentVariable(t *testing.T) { // Ensures that the filesystem ends up in the correct state with Captive Core. func TestCaptiveCoreConfigFilesystemState(t *testing.T) { - if !integration.RunWithCaptiveCore { - t.Skip() // explained above - } - confName, storagePath, cleanup := createCaptiveCoreConfig(SimpleCaptiveCoreToml) defer cleanup() diff --git a/services/horizon/internal/test/integration/integration.go b/services/horizon/internal/test/integration/integration.go index 4c76afe04a..a661c9edf8 100644 --- a/services/horizon/internal/test/integration/integration.go +++ b/services/horizon/internal/test/integration/integration.go @@ -49,12 +49,7 @@ const ( sorobanRPCPort = 8080 ) -var ( - HorizonInitErrStr = "cannot initialize Horizon" - RunWithCaptiveCore = os.Getenv("HORIZON_INTEGRATION_TESTS_ENABLE_CAPTIVE_CORE") != "" - RunWithSorobanRPC = os.Getenv("HORIZON_INTEGRATION_TESTS_ENABLE_SOROBAN_RPC") != "" - RunWithCaptiveCoreUseDB = os.Getenv("HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_USE_DB") != "" -) +const HorizonInitErrStr = "cannot initialize Horizon" type Config struct { ProtocolVersion uint32 @@ -169,7 +164,7 @@ func NewTest(t *testing.T, config Config) *Test { i.coreClient = &stellarcore.Client{URL: "http://localhost:" + strconv.Itoa(stellarCorePort)} if !config.SkipCoreContainerCreation { i.waitForCore() - if RunWithSorobanRPC && i.config.EnableSorobanRPC { + if i.config.EnableSorobanRPC { i.runComposeCommand("up", "--detach", "--quiet-pull", "--no-color", "soroban-rpc") i.waitForSorobanRPC() } @@ -187,21 +182,15 @@ func NewTest(t *testing.T, config Config) *Test { } func (i *Test) configureCaptiveCore() { - // We either test Captive Core through environment variables or through - // custom Horizon parameters. - if RunWithCaptiveCore { - composePath := findDockerComposePath() - i.coreConfig.binaryPath = os.Getenv("HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") - coreConfigFile := "captive-core-classic-integration-tests.cfg" - if i.config.ProtocolVersion >= ledgerbackend.MinimalSorobanProtocolSupport { - coreConfigFile = "captive-core-integration-tests.cfg" - } - i.coreConfig.configPath = filepath.Join(composePath, coreConfigFile) - i.coreConfig.storagePath = i.CurrentTest().TempDir() - if RunWithCaptiveCoreUseDB { - i.coreConfig.useDB = true - } + composePath := findDockerComposePath() + i.coreConfig.binaryPath = os.Getenv("HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") + coreConfigFile := "captive-core-classic-integration-tests.cfg" + if i.config.ProtocolVersion >= ledgerbackend.MinimalSorobanProtocolSupport { + coreConfigFile = "captive-core-integration-tests.cfg" } + i.coreConfig.configPath = filepath.Join(composePath, coreConfigFile) + i.coreConfig.storagePath = i.CurrentTest().TempDir() + i.coreConfig.useDB = true if value := i.getIngestParameter( horizon.StellarCoreBinaryPathName, @@ -233,7 +222,7 @@ func (i *Test) runComposeCommand(args ...string) { integrationSorobanRPCYaml := filepath.Join(i.composePath, "docker-compose.integration-tests.soroban-rpc.yml") cmdline := args - if RunWithSorobanRPC { + if i.config.EnableSorobanRPC { cmdline = append([]string{"-f", integrationSorobanRPCYaml}, cmdline...) } cmdline = append([]string{"-f", integrationYaml}, cmdline...) @@ -298,7 +287,7 @@ func (i *Test) prepareShutdownHandlers() { if !i.config.SkipCoreContainerCreation { i.runComposeCommand("rm", "-fvs", "core") i.runComposeCommand("rm", "-fvs", "core-postgres") - if os.Getenv("HORIZON_INTEGRATION_TESTS_ENABLE_SOROBAN_RPC") != "" { + if i.config.EnableSorobanRPC { i.runComposeCommand("logs", "soroban-rpc") i.runComposeCommand("rm", "-fvs", "soroban-rpc") } From e1c5206ad1baa651ef6668aef081075892d6b952 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Mon, 22 Apr 2024 23:16:11 -0400 Subject: [PATCH 121/234] Move GetObjectKeyFromSequenceNumber --- .../ledgerexporter/internal/exportmanager.go | 3 +- .../internal/exportmanager_test.go | 5 ++- exp/services/ledgerexporter/internal/utils.go | 29 ------------ .../ledgerexporter/internal/utils_test.go | 38 ---------------- ingest/ledgerbackend/gcs_backend.go | 32 +------------ support/datastore/datastore.go | 29 ++++++++++++ support/datastore/datastore_test.go | 45 +++++++++++++++++++ 7 files changed, 80 insertions(+), 101 deletions(-) create mode 100644 support/datastore/datastore_test.go diff --git a/exp/services/ledgerexporter/internal/exportmanager.go b/exp/services/ledgerexporter/internal/exportmanager.go index de322aa30c..2edf0196f2 100644 --- a/exp/services/ledgerexporter/internal/exportmanager.go +++ b/exp/services/ledgerexporter/internal/exportmanager.go @@ -5,6 +5,7 @@ import ( "github.com/pkg/errors" "github.com/stellar/go/ingest/ledgerbackend" + "github.com/stellar/go/support/datastore" "github.com/stellar/go/xdr" ) @@ -46,7 +47,7 @@ func (e *exportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta ledgerSeq := ledgerCloseMeta.LedgerSequence() // Determine the object key for the given ledger sequence - objectKey, err := GetObjectKeyFromSequenceNumber(e.config, ledgerSeq) + objectKey, err := datastore.GetObjectKeyFromSequenceNumber(ledgerSeq, e.config.LedgersPerFile, e.config.FilesPerPartition, fileSuffix) if err != nil { return errors.Wrapf(err, "failed to get object key for ledger %d", ledgerSeq) } diff --git a/exp/services/ledgerexporter/internal/exportmanager_test.go b/exp/services/ledgerexporter/internal/exportmanager_test.go index f6f330ec08..32d74327f8 100644 --- a/exp/services/ledgerexporter/internal/exportmanager_test.go +++ b/exp/services/ledgerexporter/internal/exportmanager_test.go @@ -10,6 +10,7 @@ import ( "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/support/collections/set" + "github.com/stellar/go/support/datastore" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -44,7 +45,7 @@ func (s *ExportManagerSuite) TestRun() { for i := start; i <= end; i++ { s.mockBackend.On("GetLedger", s.ctx, i). Return(createLedgerCloseMeta(i), nil) - key, _ := GetObjectKeyFromSequenceNumber(config, i) + key, _ := datastore.GetObjectKeyFromSequenceNumber(i, config.LedgersPerFile, config.FilesPerPartition, fileSuffix) expectedKeys.Add(key) } @@ -121,7 +122,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { for i := start; i <= end; i++ { require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(i))) - key, err := GetObjectKeyFromSequenceNumber(config, i) + key, err := datastore.GetObjectKeyFromSequenceNumber(i, config.LedgersPerFile, config.FilesPerPartition, fileSuffix) require.NoError(s.T(), err) expectedkeys.Add(key) } diff --git a/exp/services/ledgerexporter/internal/utils.go b/exp/services/ledgerexporter/internal/utils.go index d1bc8e20d1..74505d409c 100644 --- a/exp/services/ledgerexporter/internal/utils.go +++ b/exp/services/ledgerexporter/internal/utils.go @@ -2,7 +2,6 @@ package ledgerexporter import ( "compress/gzip" - "fmt" "io" xdr3 "github.com/stellar/go-xdr/xdr3" @@ -15,34 +14,6 @@ const ( fileSuffix = ".xdr.gz" ) -// GetObjectKeyFromSequenceNumber generates the file name from the ledger sequence number based on configuration. -func GetObjectKeyFromSequenceNumber(config ExporterConfig, ledgerSeq uint32) (string, error) { - var objectKey string - - if config.LedgersPerFile < 1 { - return "", errors.Errorf("Invalid ledgers per file (%d): must be at least 1", config.LedgersPerFile) - } - - if config.FilesPerPartition > 1 { - partitionSize := config.LedgersPerFile * config.FilesPerPartition - partitionStart := (ledgerSeq / partitionSize) * partitionSize - partitionEnd := partitionStart + partitionSize - 1 - objectKey = fmt.Sprintf("%d-%d/", partitionStart, partitionEnd) - } - - fileStart := (ledgerSeq / config.LedgersPerFile) * config.LedgersPerFile - fileEnd := fileStart + config.LedgersPerFile - 1 - objectKey += fmt.Sprintf("%d", fileStart) - - // Multiple ledgers per file - if fileStart != fileEnd { - objectKey += fmt.Sprintf("-%d", fileEnd) - } - objectKey += fileSuffix - - return objectKey, nil -} - // getLatestLedgerSequenceFromHistoryArchives returns the most recent ledger sequence (checkpoint ledger) // number present in the history archives. func getLatestLedgerSequenceFromHistoryArchives(historyArchivesURLs []string) (uint32, error) { diff --git a/exp/services/ledgerexporter/internal/utils_test.go b/exp/services/ledgerexporter/internal/utils_test.go index c11b500c21..2bf61bcee7 100644 --- a/exp/services/ledgerexporter/internal/utils_test.go +++ b/exp/services/ledgerexporter/internal/utils_test.go @@ -2,50 +2,12 @@ package ledgerexporter import ( "bytes" - "fmt" "testing" "github.com/stellar/go/xdr" "github.com/stretchr/testify/require" ) -func TestGetObjectKeyFromSequenceNumber(t *testing.T) { - testCases := []struct { - filesPerPartition uint32 - ledgerSeq uint32 - ledgersPerFile uint32 - expectedKey string - expectedError bool - }{ - {0, 5, 1, "5.xdr.gz", false}, - {0, 5, 10, "0-9.xdr.gz", false}, - {2, 5, 0, "", true}, - {2, 10, 100, "0-199/0-99.xdr.gz", false}, - {2, 150, 50, "100-199/150-199.xdr.gz", false}, - {2, 300, 200, "0-399/200-399.xdr.gz", false}, - {2, 1, 1, "0-1/1.xdr.gz", false}, - {4, 10, 100, "0-399/0-99.xdr.gz", false}, - {4, 250, 50, "200-399/250-299.xdr.gz", false}, - {1, 300, 200, "200-399.xdr.gz", false}, - {1, 1, 1, "1.xdr.gz", false}, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("LedgerSeq-%d-LedgersPerFile-%d", tc.ledgerSeq, tc.ledgersPerFile), func(t *testing.T) { - config := ExporterConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile} - key, err := GetObjectKeyFromSequenceNumber(config, tc.ledgerSeq) - - if tc.expectedError { - require.Error(t, err) - require.Empty(t, key) - } else { - require.NoError(t, err) - require.Equal(t, tc.expectedKey, key) - } - }) - } -} - func createTestLedgerCloseMetaBatch(startSeq, endSeq uint32, count int) xdr.LedgerCloseMetaBatch { var ledgerCloseMetas []xdr.LedgerCloseMeta for i := 0; i < count; i++ { diff --git a/ingest/ledgerbackend/gcs_backend.go b/ingest/ledgerbackend/gcs_backend.go index 08047bc341..0c24707d20 100644 --- a/ingest/ledgerbackend/gcs_backend.go +++ b/ingest/ledgerbackend/gcs_backend.go @@ -3,7 +3,6 @@ package ledgerbackend import ( "compress/gzip" "context" - "fmt" "io" "path" "strconv" @@ -84,7 +83,7 @@ func (gcsb *GCSBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, er func (gcsb *GCSBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { var ledgerCloseMetaBatch xdr.LedgerCloseMetaBatch - objectKey, err := gcsb.GetObjectKeyFromSequenceNumber(sequence, gcsb.ledgersPerFile, gcsb.filesPerPartition) + objectKey, err := datastore.GetObjectKeyFromSequenceNumber(sequence, gcsb.ledgersPerFile, gcsb.filesPerPartition, gcsb.fileSuffix) if err != nil { return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed to get object key for ledger %d", sequence) } @@ -151,35 +150,6 @@ func (gcsb *GCSBackend) Close() error { return nil } -// TODO: Should this function also be modified and added to support/datastore? -// This function should be shared between ledger exporter and this legerbackend reader -func (gcsb *GCSBackend) GetObjectKeyFromSequenceNumber(ledgerSeq uint32, ledgersPerFile uint32, filesPerPartition uint32) (string, error) { - var objectKey string - - if ledgersPerFile < 1 { - return "", errors.Errorf("Invalid ledgers per file (%d): must be at least 1", ledgersPerFile) - } - - if filesPerPartition > 1 { - partitionSize := ledgersPerFile * filesPerPartition - partitionStart := (ledgerSeq / partitionSize) * partitionSize - partitionEnd := partitionStart + partitionSize - 1 - objectKey = fmt.Sprintf("%d-%d/", partitionStart, partitionEnd) - } - - fileStart := (ledgerSeq / ledgersPerFile) * ledgersPerFile - fileEnd := fileStart + ledgersPerFile - 1 - objectKey += fmt.Sprintf("%d", fileStart) - - // Multiple ledgers per file - if fileStart != fileEnd { - objectKey += fmt.Sprintf("-%d", fileEnd) - } - objectKey += gcsb.fileSuffix - - return objectKey, nil -} - func (gcsb *GCSBackend) GetLatestDirectory(directories []string) (string, error) { var latestDirectory string largestDirectoryLedger := 0 diff --git a/support/datastore/datastore.go b/support/datastore/datastore.go index 50eefa4181..dc382c3ac7 100644 --- a/support/datastore/datastore.go +++ b/support/datastore/datastore.go @@ -2,6 +2,7 @@ package datastore import ( "context" + "fmt" "io" "strings" @@ -58,3 +59,31 @@ func NewDataStore(ctx context.Context, destinationURL string) (DataStore, error) return &GCSDataStore{client: client, bucket: bucket, prefix: prefix}, nil } + +// GetObjectKeyFromSequenceNumber generates the file name from the ledger sequence number. +func GetObjectKeyFromSequenceNumber(ledgerSeq uint32, ledgersPerFile uint32, filesPerPartition uint32, fileSuffix string) (string, error) { + var objectKey string + + if ledgersPerFile < 1 { + return "", errors.Errorf("Invalid ledgers per file (%d): must be at least 1", ledgersPerFile) + } + + if filesPerPartition > 1 { + partitionSize := ledgersPerFile * filesPerPartition + partitionStart := (ledgerSeq / partitionSize) * partitionSize + partitionEnd := partitionStart + partitionSize - 1 + objectKey = fmt.Sprintf("%d-%d/", partitionStart, partitionEnd) + } + + fileStart := (ledgerSeq / ledgersPerFile) * ledgersPerFile + fileEnd := fileStart + ledgersPerFile - 1 + objectKey += fmt.Sprintf("%d", fileStart) + + // Multiple ledgers per file + if fileStart != fileEnd { + objectKey += fmt.Sprintf("-%d", fileEnd) + } + objectKey += fileSuffix + + return objectKey, nil +} diff --git a/support/datastore/datastore_test.go b/support/datastore/datastore_test.go new file mode 100644 index 0000000000..b6eaf7670e --- /dev/null +++ b/support/datastore/datastore_test.go @@ -0,0 +1,45 @@ +package datastore + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetObjectKeyFromSequenceNumber(t *testing.T) { + testCases := []struct { + filesPerPartition uint32 + ledgerSeq uint32 + ledgersPerFile uint32 + fileSuffix string + expectedKey string + expectedError bool + }{ + {0, 5, 1, ".xdr.gz", "5.xdr.gz", false}, + {0, 5, 10, ".xdr.gz", "0-9.xdr.gz", false}, + {2, 5, 0, ".xdr.gz", "", true}, + {2, 10, 100, ".xdr.gz", "0-199/0-99.xdr.gz", false}, + {2, 150, 50, ".xdr.gz", "100-199/150-199.xdr.gz", false}, + {2, 300, 200, ".xdr.gz", "0-399/200-399.xdr.gz", false}, + {2, 1, 1, ".xdr.gz", "0-1/1.xdr.gz", false}, + {4, 10, 100, ".xdr.gz", "0-399/0-99.xdr.gz", false}, + {4, 250, 50, ".xdr.gz", "200-399/250-299.xdr.gz", false}, + {1, 300, 200, ".xdr.gz", "200-399.xdr.gz", false}, + {1, 1, 1, ".xdr.gz", "1.xdr.gz", false}, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("LedgerSeq-%d-LedgersPerFile-%d", tc.ledgerSeq, tc.ledgersPerFile), func(t *testing.T) { + key, err := GetObjectKeyFromSequenceNumber(tc.ledgerSeq, tc.ledgersPerFile, tc.filesPerPartition, tc.fileSuffix) + + if tc.expectedError { + require.Error(t, err) + require.Empty(t, key) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedKey, key) + } + }) + } +} From 1965400894418f017ceb9ea2ef05802e7cede976 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Wed, 24 Apr 2024 00:30:42 -0400 Subject: [PATCH 122/234] Update PrepareRange --- ingest/ledgerbackend/gcs_backend.go | 308 +++++++++++++++++++---- ingest/ledgerbackend/gcs_backend_test.go | 26 +- 2 files changed, 287 insertions(+), 47 deletions(-) diff --git a/ingest/ledgerbackend/gcs_backend.go b/ingest/ledgerbackend/gcs_backend.go index 0c24707d20..6252c9ff9b 100644 --- a/ingest/ledgerbackend/gcs_backend.go +++ b/ingest/ledgerbackend/gcs_backend.go @@ -7,9 +7,12 @@ import ( "path" "strconv" "strings" + "sync" + "time" "github.com/pkg/errors" "github.com/stellar/go/support/datastore" + "github.com/stellar/go/support/log" "github.com/stellar/go/support/ordered" "github.com/stellar/go/xdr" ) @@ -17,11 +20,17 @@ import ( // Ensure GCSBackend implements LedgerBackend var _ LedgerBackend = (*GCSBackend)(nil) +type LCMCache struct { + mu sync.Mutex + lcm map[uint32]xdr.LedgerCloseMeta +} + type LCMFileConfig struct { StorageURL string FileSuffix string LedgersPerFile uint32 FilesPerPartition uint32 + parallelReaders uint32 } // GCSBackend is a ledger backend that reads from a cloud storage service. @@ -32,6 +41,26 @@ type GCSBackend struct { fileSuffix string ledgersPerFile uint32 filesPerPartition uint32 + + // cancel is the CancelFunc for context which controls the lifetime of a GCSBackend instance. + // Once it is invoked GCSBackend will not be able to stream ledgers from GCSBackend or + // spawn new instances of Stellar Core. + cancel context.CancelFunc + + // gcsBackendLock protects access to gcsBackendRunner. When the read lock + // is acquired gcsBackendRunner can be accessed. When the write lock is acquired + // gcsBackendRunner can be updated. + gcsBackendLock sync.RWMutex + + // lcmCache keeps that ledger close meta in-memory. + lcmCache *LCMCache + + prepared *Range // non-nil if any range is prepared + closed bool // False until the core is closed + parallelReaders uint32 // Number of parallel GCS readers + context context.Context + nextLedger uint32 // next ledger expected, error w/ restart if not seen + lastLedger *uint32 // end of current segment if offline, nil if online } // Return a new GCSBackend instance. @@ -41,12 +70,31 @@ func NewGCSBackend(ctx context.Context, fileConfig LCMFileConfig) (*GCSBackend, return nil, err } + if ctx == nil { + ctx = context.Background() + } + + lcmCache := &LCMCache{lcm: make(map[uint32]xdr.LedgerCloseMeta)} + + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(ctx) + + // Need at least 1 reader + parallelReaders := fileConfig.parallelReaders + if parallelReaders == 0 { + parallelReaders = 1 + } + cloudStorageBackend := &GCSBackend{ lcmDataStore: lcmDataStore, storageURL: fileConfig.StorageURL, fileSuffix: fileConfig.FileSuffix, ledgersPerFile: fileConfig.LedgersPerFile, filesPerPartition: fileConfig.FilesPerPartition, + cancel: cancel, + lcmCache: lcmCache, + parallelReaders: fileConfig.parallelReaders, + context: ctx, } return cloudStorageBackend, nil @@ -54,6 +102,9 @@ func NewGCSBackend(ctx context.Context, fileConfig LCMFileConfig) (*GCSBackend, // GetLatestLedgerSequence returns the most recent ledger sequence number in the cloud storage bucket. func (gcsb *GCSBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { + gcsb.gcsBackendLock.RLock() + defer gcsb.gcsBackendLock.RUnlock() + // Get the latest parition directory from the bucket directories, err := gcsb.lcmDataStore.ListDirectoryNames(ctx) if err != nil { @@ -81,75 +132,103 @@ func (gcsb *GCSBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, er // GetLedger returns the LedgerCloseMeta for the specified ledger sequence number func (gcsb *GCSBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { - var ledgerCloseMetaBatch xdr.LedgerCloseMetaBatch + gcsb.gcsBackendLock.RLock() + defer gcsb.gcsBackendLock.RUnlock() - objectKey, err := datastore.GetObjectKeyFromSequenceNumber(sequence, gcsb.ledgersPerFile, gcsb.filesPerPartition, gcsb.fileSuffix) - if err != nil { - return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed to get object key for ledger %d", sequence) + if gcsb.closed { + return xdr.LedgerCloseMeta{}, errors.New("gcsBackend is closed") } - reader, err := gcsb.lcmDataStore.GetFile(ctx, objectKey) - if err != nil { - return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed getting file: %s", objectKey) + if gcsb.prepared == nil { + return xdr.LedgerCloseMeta{}, errors.New("session is not prepared, call PrepareRange first") } - defer reader.Close() + // Block until the requested sequence is available + for { + select { + case <-ctx.Done(): + return xdr.LedgerCloseMeta{}, ctx.Err() + default: + lcm, ok := gcsb.lcmCache.lcm[sequence] + if !ok { + continue + } + // Delete to free space for unbounded mode lcm retrieval + delete(gcsb.lcmCache.lcm, sequence) + return lcm, nil + } + } +} - gzipReader, err := gzip.NewReader(reader) - if err != nil { - return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed getting file: %s", objectKey) +// PrepareRange checks if the starting and ending (if bounded) ledgers exist. +func (gcsb *GCSBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { + if alreadyPrepared, err := gcsb.startPreparingRange(ctx, ledgerRange); err != nil { + return errors.Wrap(err, "error starting prepare range") + } else if alreadyPrepared { + return nil } - defer gzipReader.Close() + gcsb.prepared = &ledgerRange - objectBytes, err := io.ReadAll(gzipReader) - if err != nil { - return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed reading file: %s", objectKey) - } + return nil +} - err = ledgerCloseMetaBatch.UnmarshalBinary(objectBytes) - if err != nil { - return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "failed unmarshalling file: %s", objectKey) - } +// IsPrepared returns true if a given ledgerRange is prepared. +func (gcsb *GCSBackend) IsPrepared(ctx context.Context, ledgerRange Range) (bool, error) { + gcsb.gcsBackendLock.RLock() + defer gcsb.gcsBackendLock.RUnlock() - startSequence := uint32(ledgerCloseMetaBatch.StartSequence) - if startSequence > sequence { - return xdr.LedgerCloseMeta{}, errors.Errorf("start sequence: %d; greater than sequence to get: %d", startSequence, sequence) + return gcsb.isPrepared(ledgerRange), nil +} + +func (gcsb *GCSBackend) isPrepared(ledgerRange Range) bool { + if gcsb.closed { + return false } - ledgerCloseMetasIndex := sequence - startSequence - ledgerCloseMeta := ledgerCloseMetaBatch.LedgerCloseMetas[ledgerCloseMetasIndex] + // lastLedger is only set when ledgerRange is bounded + lastLedger := uint32(0) + if gcsb.lastLedger != nil { + lastLedger = *gcsb.lastLedger + } - return ledgerCloseMeta, nil -} + if gcsb.prepared == nil { + return false + } -// PrepareRange checks if the starting and ending (if bounded) ledgers exist. -func (gcsb *GCSBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { - _, err := gcsb.GetLedger(ctx, ledgerRange.from) - if err != nil { - return errors.Wrapf(err, "error getting ledger %d", ledgerRange.from) + // Unbounded mode only checks for the starting ledger + if lastLedger == 0 { + _, ok := gcsb.lcmCache.lcm[ledgerRange.from] + return ok } + // From now on: lastLedger != 0 so current range is bounded if ledgerRange.bounded { - _, err := gcsb.GetLedger(ctx, ledgerRange.to) - if err != nil { - return errors.Wrapf(err, "error getting ending ledger %d", ledgerRange.to) - } + _, ok := gcsb.lcmCache.lcm[ledgerRange.from] + return ok && lastLedger >= ledgerRange.to } - return nil -} - -// IsPrepared is a no-op for GCSBackend. -func (gcsb *GCSBackend) IsPrepared(ctx context.Context, ledgerRange Range) (bool, error) { - return true, nil + // Requested range is unbounded but current one is bounded + return false } -// Close is a no-op for GCSBackend. +// Close closes existing GCSBackend process, streaming sessions and removes all +// temporary files. Note, once a GCSBackend instance is closed it can no longer be used and +// all subsequent calls to PrepareRange(), GetLedger(), etc will fail. +// Close is thread-safe and can be called from another go routine. func (gcsb *GCSBackend) Close() error { + gcsb.gcsBackendLock.RLock() + defer gcsb.gcsBackendLock.RUnlock() + + gcsb.closed = true + + // after the GCSBackend context is canceled all subsequent calls to PrepareRange() will fail + gcsb.cancel() + return nil } +// GetLatestDirectory returns the latest directory from an array of directories func (gcsb *GCSBackend) GetLatestDirectory(directories []string) (string, error) { var latestDirectory string largestDirectoryLedger := 0 @@ -177,6 +256,7 @@ func (gcsb *GCSBackend) GetLatestDirectory(directories []string) (string, error) return latestDirectory, nil } +// GetLatestFileNameLedgerSequence returns the lastest ledger sequence in a directory func (gcsb *GCSBackend) GetLatestFileNameLedgerSequence(fileNames []string, directory string) (uint32, error) { latestLedgerSequence := uint32(0) @@ -195,3 +275,145 @@ func (gcsb *GCSBackend) GetLatestFileNameLedgerSequence(fileNames []string, dire return latestLedgerSequence, nil } + +// startPreparingRange prepares the ledger range +// Bounded ranges will load the full range to lcmCache +// Unbounded ranges will continuously read new LCM to lcmCache +func (gcsb *GCSBackend) startPreparingRange(ctx context.Context, ledgerRange Range) (bool, error) { + gcsb.gcsBackendLock.Lock() + defer gcsb.gcsBackendLock.Unlock() + + if gcsb.isPrepared(ledgerRange) { + return true, nil + } + + // Set the starting ledger + gcsb.nextLedger = ledgerRange.from + + if ledgerRange.bounded { + gcsb.getGCSObjectsParallel(ctx, ledgerRange) + return false, nil + } + + // If unbounded, continously get new ledgers + go gcsb.getNewLedgerObjects(ctx) + + return false, nil +} + +// getGCSObjectsParallel loads the LCM from the ledgerRange to lcmCache in parallel +func (gcsb *GCSBackend) getGCSObjectsParallel(ctx context.Context, ledgerRange Range) { + var wg sync.WaitGroup + sem := make(chan struct{}, gcsb.parallelReaders) + + interval := (ledgerRange.to - ledgerRange.from) / gcsb.parallelReaders + + for i := uint32(0); i < gcsb.parallelReaders; i++ { + wg.Add(1) + sem <- struct{}{} // Acquire a slot in the semaphore + + // Get the subrange of ledgers to process + from := ledgerRange.from + (i * interval) + to := ledgerRange.from + ((i + 1) * interval) - 1 + // The last reader should run to the final ledger + if i+1 == gcsb.parallelReaders { + to = ledgerRange.to + } + subLedgerRange := BoundedRange(from, to) + + wg.Add(1) + sem <- struct{}{} + go gcsb.getGCSObjects(ctx, subLedgerRange, &wg, sem) + } + + wg.Wait() + gcsb.lastLedger = &ledgerRange.to +} + +// getGCSObjects loads the LCM to lcmCache +func (gcsb *GCSBackend) getGCSObjects(ctx context.Context, ledgerRange Range, wg *sync.WaitGroup, sem chan struct{}) { + defer wg.Done() + defer func() { <-sem }() + + select { + case <-ctx.Done(): // Check if the context was cancelled + return + default: + for i := ledgerRange.from; i <= ledgerRange.to; i++ { + lcm := gcsb.getLedgerGCSObject(i) + + // Store lcm in-memory + gcsb.lcmCache.mu.Lock() + gcsb.lcmCache.lcm[i] = lcm + gcsb.lcmCache.mu.Unlock() + } + } +} + +// getLedgerGCSObject gets the LCM for a given ledger sequence +func (gcsb *GCSBackend) getLedgerGCSObject(sequence uint32) xdr.LedgerCloseMeta { + var ledgerCloseMetaBatch xdr.LedgerCloseMetaBatch + + objectKey, err := datastore.GetObjectKeyFromSequenceNumber(sequence, gcsb.ledgersPerFile, gcsb.filesPerPartition, gcsb.fileSuffix) + if err != nil { + log.Fatalf("failed to get object key for ledger %d; %s", sequence, err) + } + + reader, err := gcsb.lcmDataStore.GetFile(context.Background(), objectKey) + if err != nil { + log.Fatalf("failed getting file: %s; %s", objectKey, err) + } + + defer reader.Close() + + gzipReader, err := gzip.NewReader(reader) + if err != nil { + log.Fatalf("failed getting file: %s; %s", objectKey, err) + } + + defer gzipReader.Close() + + objectBytes, err := io.ReadAll(gzipReader) + if err != nil { + log.Fatalf("failed reading file: %s; %s", objectKey, err) + } + + err = ledgerCloseMetaBatch.UnmarshalBinary(objectBytes) + if err != nil { + log.Fatalf("failed unmarshalling file: %s; %s", objectKey, err) + } + + startSequence := uint32(ledgerCloseMetaBatch.StartSequence) + if startSequence > sequence { + log.Fatalf("start sequence: %d; greater than sequence to get: %d; %s", startSequence, sequence, err) + } + + ledgerCloseMetasIndex := sequence - startSequence + ledgerCloseMeta := ledgerCloseMetaBatch.LedgerCloseMetas[ledgerCloseMetasIndex] + + return ledgerCloseMeta +} + +// getNewLedgerObjects polls GCS and buffers new LCM +func (gcsb *GCSBackend) getNewLedgerObjects(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return // Exit if the context is canceled + case <-time.After(1): + // Buffer lcmCache if LCMs exist + for len(gcsb.lcmCache.lcm) < 1000 { + // Check if LCM exists; otherwise wait and poll again + objectKey, _ := datastore.GetObjectKeyFromSequenceNumber(gcsb.nextLedger, gcsb.ledgersPerFile, gcsb.filesPerPartition, gcsb.fileSuffix) + exists, _ := gcsb.lcmDataStore.Exists(context.Background(), objectKey) + if !exists { + break + } + // Get LCM and add to lcmCache + lcm := gcsb.getLedgerGCSObject(gcsb.nextLedger) + gcsb.lcmCache.lcm[gcsb.nextLedger] = lcm + gcsb.nextLedger += 1 + } + } + } +} diff --git a/ingest/ledgerbackend/gcs_backend_test.go b/ingest/ledgerbackend/gcs_backend_test.go index 71d3e23f12..c605523c0d 100644 --- a/ingest/ledgerbackend/gcs_backend_test.go +++ b/ingest/ledgerbackend/gcs_backend_test.go @@ -1,34 +1,52 @@ package ledgerbackend import ( + "context" "testing" + "github.com/stellar/go/xdr" "github.com/stretchr/testify/assert" ) func MockGCSBackend() GCSBackend { + lcmCache := &LCMCache{lcm: make(map[uint32]xdr.LedgerCloseMeta)} return GCSBackend{ fileSuffix: ".xdr.gz", + lcmCache: lcmCache, } } +func TestGCSBackendGetLedger(t *testing.T) { + gcsb := MockGCSBackend() + gcsb.lcmCache.lcm[1] = xdr.LedgerCloseMeta{V: 0} + gcsb.lcmCache.lcm[2] = xdr.LedgerCloseMeta{V: 1} + ledgerRange := BoundedRange(1, 2) + gcsb.prepared = &ledgerRange + ctx := context.Background() + + lcm, err := gcsb.GetLedger(ctx, 1) + + assert.NoError(t, err) + assert.Equal(t, xdr.LedgerCloseMeta{V: 0}, lcm) +} + func TestGetLatestFileNameLedgerSequence(t *testing.T) { - csb := MockGCSBackend() + gcsb := MockGCSBackend() directory := "ledgers/pubnet/21-30" filenames := []string{ "ledgers/pubnet/21-30/21.xdr.gz", "ledgers/pubnet/21-30/22.xdr.gz", "ledgers/pubnet/21-30/23.xdr.gz", } - latestLedgerSequence, _ := csb.GetLatestFileNameLedgerSequence(filenames, directory) + latestLedgerSequence, _ := gcsb.GetLatestFileNameLedgerSequence(filenames, directory) assert.Equal(t, uint32(23), latestLedgerSequence) } func TestGetLatestDirectory(t *testing.T) { - csb := MockGCSBackend() + gcsb := MockGCSBackend() directories := []string{"ledgers/pubnet/1-10", "ledgers/pubnet/11-20", "ledgers/pubnet/21-30"} - latestDirectory, _ := csb.GetLatestDirectory(directories) + latestDirectory, _ := gcsb.GetLatestDirectory(directories) assert.Equal(t, "ledgers/pubnet/21-30", latestDirectory) } From 9f7f9c2494daf71e2144d371eaa482d50a924fc6 Mon Sep 17 00:00:00 2001 From: tamirms Date: Wed, 24 Apr 2024 08:15:30 +0100 Subject: [PATCH 123/234] exp/services/ledgerexporter: Add prometheus metrics for ledger exporter (#5265) --- exp/services/ledgerexporter/internal/app.go | 65 +++++-- .../ledgerexporter/internal/config.go | 5 + .../ledgerexporter/internal/datastore.go | 5 +- .../ledgerexporter/internal/exportmanager.go | 60 +++--- .../internal/exportmanager_test.go | 62 ++++-- .../ledgerexporter/internal/gcs_datastore.go | 21 +- .../ledgerexporter/internal/mock_datastore.go | 4 +- exp/services/ledgerexporter/internal/queue.go | 58 ++++++ .../ledgerexporter/internal/queue_test.go | 63 ++++++ .../ledgerexporter/internal/uploader.go | 127 +++++++++--- .../ledgerexporter/internal/uploader_test.go | 184 +++++++++++++++--- exp/services/ledgerexporter/internal/utils.go | 1 + 12 files changed, 522 insertions(+), 133 deletions(-) create mode 100644 exp/services/ledgerexporter/internal/queue.go create mode 100644 exp/services/ledgerexporter/internal/queue_test.go diff --git a/exp/services/ledgerexporter/internal/app.go b/exp/services/ledgerexporter/internal/app.go index fb4a5f788b..8239b9c72e 100644 --- a/exp/services/ledgerexporter/internal/app.go +++ b/exp/services/ledgerexporter/internal/app.go @@ -10,8 +10,13 @@ import ( "syscall" "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/stellar/go/ingest/ledgerbackend" _ "github.com/stellar/go/network" + supporthttp "github.com/stellar/go/support/http" "github.com/stellar/go/support/log" ) @@ -20,11 +25,12 @@ var ( ) type App struct { - config Config - ledgerBackend ledgerbackend.LedgerBackend - dataStore DataStore - exportManager ExportManager - uploader Uploader + config Config + ledgerBackend ledgerbackend.LedgerBackend + dataStore DataStore + exportManager *ExportManager + uploader Uploader + prometheusRegistry *prometheus.Registry } func NewApp() *App { @@ -34,15 +40,25 @@ func NewApp() *App { err := config.LoadConfig() logFatalIf(err, "Could not load configuration") - app := &App{config: config} + app := &App{config: config, prometheusRegistry: prometheus.NewRegistry()} + app.prometheusRegistry.MustRegister( + collectors.NewProcessCollector(collectors.ProcessCollectorOpts{Namespace: "ledger_exporter"}), + collectors.NewGoCollector(), + ) return app } func (a *App) init(ctx context.Context) { - a.dataStore = mustNewDataStore(ctx, &a.config) - a.ledgerBackend = mustNewLedgerBackend(ctx, a.config) - a.exportManager = NewExportManager(a.config.ExporterConfig, a.ledgerBackend) - a.uploader = NewUploader(a.dataStore, a.exportManager.GetMetaArchiveChannel()) + a.dataStore = mustNewDataStore(ctx, a.config) + a.ledgerBackend = mustNewLedgerBackend(ctx, a.config, a.prometheusRegistry) + // TODO: make number of upload workers configurable instead of hard coding it to 1 + queue := NewUploadQueue(1, a.prometheusRegistry) + a.exportManager = NewExportManager(a.config.ExporterConfig, a.ledgerBackend, queue, a.prometheusRegistry) + a.uploader = NewUploader( + a.dataStore, + queue, + a.prometheusRegistry, + ) } func (a *App) close() { @@ -54,6 +70,24 @@ func (a *App) close() { } } +func (a *App) serveAdmin() { + if a.config.AdminPort == 0 { + return + } + + mux := supporthttp.NewMux(logger) + mux.Handle("/metrics", promhttp.HandlerFor(a.prometheusRegistry, promhttp.HandlerOpts{})) + + addr := fmt.Sprintf(":%d", a.config.AdminPort) + supporthttp.Run(supporthttp.Config{ + ListenAddr: addr, + Handler: mux, + OnStarting: func() { + logger.Infof("Starting admin port server on %s", addr) + }, + }) +} + func (a *App) Run() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -84,6 +118,8 @@ func (a *App) Run() { } }() + go a.serveAdmin() + // Handle OS signals to gracefully terminate the service sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) @@ -97,7 +133,7 @@ func (a *App) Run() { logger.Info("Shutting down ledger-exporter") } -func mustNewDataStore(ctx context.Context, config *Config) DataStore { +func mustNewDataStore(ctx context.Context, config Config) DataStore { dataStore, err := NewDataStore(ctx, fmt.Sprintf("%s/%s", config.DestinationURL, config.Network)) logFatalIf(err, "Could not connect to destination data store") return dataStore @@ -105,12 +141,15 @@ func mustNewDataStore(ctx context.Context, config *Config) DataStore { // mustNewLedgerBackend Creates and initializes captive core ledger backend // Currently, only supports captive-core as ledger backend -func mustNewLedgerBackend(ctx context.Context, config Config) ledgerbackend.LedgerBackend { +func mustNewLedgerBackend(ctx context.Context, config Config, prometheusRegistry *prometheus.Registry) ledgerbackend.LedgerBackend { captiveConfig := config.GenerateCaptiveCoreConfig() + var backend ledgerbackend.LedgerBackend + var err error // Create a new captive core backend - backend, err := ledgerbackend.NewCaptive(captiveConfig) + backend, err = ledgerbackend.NewCaptive(captiveConfig) logFatalIf(err, "Failed to create captive-core instance") + backend = ledgerbackend.WithMetrics(backend, prometheusRegistry, "ledger_exporter") var ledgerRange ledgerbackend.Range if config.EndLedger == 0 { diff --git a/exp/services/ledgerexporter/internal/config.go b/exp/services/ledgerexporter/internal/config.go index 640841b1d9..17e76a5d68 100644 --- a/exp/services/ledgerexporter/internal/config.go +++ b/exp/services/ledgerexporter/internal/config.go @@ -10,6 +10,7 @@ import ( "github.com/stellar/go/network" "github.com/pelletier/go-toml" + "github.com/stellar/go/support/errors" "github.com/stellar/go/support/ordered" ) @@ -25,6 +26,8 @@ type StellarCoreConfig struct { } type Config struct { + AdminPort int `toml:"admin_port"` + Network string `toml:"network"` DestinationURL string `toml:"destination_url"` ExporterConfig ExporterConfig `toml:"exporter_config"` @@ -41,6 +44,7 @@ func (config *Config) LoadConfig() error { startLedger := flag.Uint("start", 0, "Starting ledger") endLedger := flag.Uint("end", 0, "Ending ledger (inclusive)") startFromLastNLedger := flag.Uint("from-last", 0, "Start streaming from last N ledgers") + adminPort := flag.Int("admin-port", 0, "Admin HTTP port for prometheus metrics") configFilePath := flag.String("config-file", "config.toml", "Path to the TOML config file") flag.Parse() @@ -48,6 +52,7 @@ func (config *Config) LoadConfig() error { config.StartLedger = uint32(*startLedger) config.EndLedger = uint32(*endLedger) config.StartFromLastLedgers = uint32(*startFromLastNLedger) + config.AdminPort = *adminPort // Load config TOML file cfg, err := toml.LoadFile(*configFilePath) diff --git a/exp/services/ledgerexporter/internal/datastore.go b/exp/services/ledgerexporter/internal/datastore.go index 0367e9008e..a53d97465a 100644 --- a/exp/services/ledgerexporter/internal/datastore.go +++ b/exp/services/ledgerexporter/internal/datastore.go @@ -6,16 +6,17 @@ import ( "strings" "cloud.google.com/go/storage" + "google.golang.org/api/option" + "github.com/stellar/go/support/errors" "github.com/stellar/go/support/url" - "google.golang.org/api/option" ) // DataStore defines an interface for interacting with data storage type DataStore interface { GetFile(ctx context.Context, path string) (io.ReadCloser, error) PutFile(ctx context.Context, path string, in io.WriterTo) error - PutFileIfNotExists(ctx context.Context, path string, in io.WriterTo) error + PutFileIfNotExists(ctx context.Context, path string, in io.WriterTo) (bool, error) Exists(ctx context.Context, path string) (bool, error) Size(ctx context.Context, path string) (int64, error) Close() error diff --git a/exp/services/ledgerexporter/internal/exportmanager.go b/exp/services/ledgerexporter/internal/exportmanager.go index de322aa30c..a918a16e4e 100644 --- a/exp/services/ledgerexporter/internal/exportmanager.go +++ b/exp/services/ledgerexporter/internal/exportmanager.go @@ -2,8 +2,11 @@ package ledgerexporter import ( "context" + "strconv" "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/xdr" ) @@ -14,35 +17,32 @@ type ExporterConfig struct { } // ExportManager manages the creation and handling of export objects. -type ExportManager interface { - GetMetaArchiveChannel() chan *LedgerMetaArchive - Run(ctx context.Context, startLedger uint32, endLedger uint32) error - AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta xdr.LedgerCloseMeta) error -} - -type exportManager struct { +type ExportManager struct { config ExporterConfig ledgerBackend ledgerbackend.LedgerBackend currentMetaArchive *LedgerMetaArchive - metaArchiveCh chan *LedgerMetaArchive + queue UploadQueue + latestLedgerMetric *prometheus.GaugeVec } // NewExportManager creates a new ExportManager with the provided configuration. -func NewExportManager(config ExporterConfig, backend ledgerbackend.LedgerBackend) ExportManager { - return &exportManager{ - config: config, - ledgerBackend: backend, - metaArchiveCh: make(chan *LedgerMetaArchive, 1), - } -} +func NewExportManager(config ExporterConfig, backend ledgerbackend.LedgerBackend, queue UploadQueue, prometheusRegistry *prometheus.Registry) *ExportManager { + latestLedgerMetric := prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: "ledger_exporter", Subsystem: "export_manager", Name: "latest_ledger", + Help: "sequence number of the latest ledger consumed by the export manager", + }, []string{"start_ledger", "end_ledger"}) + prometheusRegistry.MustRegister(latestLedgerMetric) -// GetMetaArchiveChannel returns a channel that receives LedgerMetaArchive objects. -func (e *exportManager) GetMetaArchiveChannel() chan *LedgerMetaArchive { - return e.metaArchiveCh + return &ExportManager{ + config: config, + ledgerBackend: backend, + queue: queue, + latestLedgerMetric: latestLedgerMetric, + } } // AddLedgerCloseMeta adds ledger metadata to the current export object -func (e *exportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta xdr.LedgerCloseMeta) error { +func (e *ExportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta xdr.LedgerCloseMeta) error { ledgerSeq := ledgerCloseMeta.LedgerSequence() // Determine the object key for the given ledger sequence @@ -67,19 +67,16 @@ func (e *exportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta e.currentMetaArchive = NewLedgerMetaArchive(objectKey, ledgerSeq, endSeq) } - err = e.currentMetaArchive.AddLedger(ledgerCloseMeta) - if err != nil { + if err = e.currentMetaArchive.AddLedger(ledgerCloseMeta); err != nil { return errors.Wrapf(err, "failed to add ledger %d", ledgerSeq) } if ledgerSeq >= e.currentMetaArchive.GetEndLedgerSequence() { // Current archive is full, send it for upload - select { - case e.metaArchiveCh <- e.currentMetaArchive: - e.currentMetaArchive = nil - case <-ctx.Done(): - return ctx.Err() + if err = e.queue.Enqueue(ctx, e.currentMetaArchive); err != nil { + return err } + e.currentMetaArchive = nil } return nil } @@ -88,10 +85,12 @@ func (e *exportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta // from the backend, and processes the corresponding ledger close metadata. // The process continues until the ending ledger number is reached or a cancellation // signal is received. -func (e *exportManager) Run(ctx context.Context, startLedger, endLedger uint32) error { - - // Close the object channel - defer close(e.metaArchiveCh) +func (e *ExportManager) Run(ctx context.Context, startLedger, endLedger uint32) error { + defer e.queue.Close() + labels := prometheus.Labels{ + "start_ledger": strconv.FormatUint(uint64(startLedger), 10), + "end_ledger": strconv.FormatUint(uint64(endLedger), 10), + } for nextLedger := startLedger; endLedger < 1 || nextLedger <= endLedger; nextLedger++ { select { @@ -103,6 +102,7 @@ func (e *exportManager) Run(ctx context.Context, startLedger, endLedger uint32) if err != nil { return errors.Wrapf(err, "failed to retrieve ledger %d from the ledger backend", nextLedger) } + e.latestLedgerMetric.With(labels).Set(float64(nextLedger)) err = e.AddLedgerCloseMeta(ctx, ledgerCloseMeta) if err != nil { return errors.Wrapf(err, "failed to add ledgerCloseMeta for ledger %d", nextLedger) diff --git a/exp/services/ledgerexporter/internal/exportmanager_test.go b/exp/services/ledgerexporter/internal/exportmanager_test.go index f6f330ec08..b082a8512e 100644 --- a/exp/services/ledgerexporter/internal/exportmanager_test.go +++ b/exp/services/ledgerexporter/internal/exportmanager_test.go @@ -6,12 +6,14 @@ import ( "testing" "time" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/mock" - "github.com/stellar/go/ingest/ledgerbackend" - "github.com/stellar/go/support/collections/set" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + + "github.com/stellar/go/ingest/ledgerbackend" + "github.com/stellar/go/support/collections/set" ) func TestExporterSuite(t *testing.T) { @@ -36,7 +38,9 @@ func (s *ExportManagerSuite) TearDownTest() { func (s *ExportManagerSuite) TestRun() { config := ExporterConfig{LedgersPerFile: 64, FilesPerPartition: 10} - exporter := NewExportManager(config, &s.mockBackend) + registry := prometheus.NewRegistry() + queue := NewUploadQueue(1, registry) + exporter := NewExportManager(config, &s.mockBackend, queue, registry) start := uint32(0) end := uint32(255) @@ -53,7 +57,12 @@ func (s *ExportManagerSuite) TestRun() { wg.Add(1) go func() { defer wg.Done() - for v := range exporter.GetMetaArchiveChannel() { + for { + v, ok, err := queue.Dequeue(s.ctx) + s.Assert().NoError(err) + if !ok { + break + } actualKeys.Add(v.objectKey) } }() @@ -64,11 +73,23 @@ func (s *ExportManagerSuite) TestRun() { wg.Wait() require.Equal(s.T(), expectedKeys, actualKeys) + require.Equal( + s.T(), + float64(255), + getMetricValue(exporter.latestLedgerMetric.With( + prometheus.Labels{ + "start_ledger": "0", + "end_ledger": "255", + }), + ).GetGauge().GetValue(), + ) } func (s *ExportManagerSuite) TestRunContextCancel() { config := ExporterConfig{LedgersPerFile: 1, FilesPerPartition: 1} - exporter := NewExportManager(config, &s.mockBackend) + registry := prometheus.NewRegistry() + queue := NewUploadQueue(1, registry) + exporter := NewExportManager(config, &s.mockBackend, queue, registry) ctx, cancel := context.WithCancel(context.Background()) s.mockBackend.On("GetLedger", mock.Anything, mock.Anything). @@ -80,9 +101,10 @@ func (s *ExportManagerSuite) TestRunContextCancel() { }() go func() { - ch := exporter.GetMetaArchiveChannel() for i := 0; i < 127; i++ { - <-ch + _, ok, err := queue.Dequeue(s.ctx) + s.Assert().NoError(err) + s.Assert().True(ok) } }() @@ -93,7 +115,9 @@ func (s *ExportManagerSuite) TestRunContextCancel() { func (s *ExportManagerSuite) TestRunWithCanceledContext() { config := ExporterConfig{LedgersPerFile: 1, FilesPerPartition: 10} - exporter := NewExportManager(config, &s.mockBackend) + registry := prometheus.NewRegistry() + queue := NewUploadQueue(1, registry) + exporter := NewExportManager(config, &s.mockBackend, queue, registry) ctx, cancel := context.WithCancel(context.Background()) cancel() err := exporter.Run(ctx, 1, 10) @@ -102,8 +126,9 @@ func (s *ExportManagerSuite) TestRunWithCanceledContext() { func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { config := ExporterConfig{LedgersPerFile: 1, FilesPerPartition: 10} - exporter := NewExportManager(config, &s.mockBackend) - objectCh := exporter.GetMetaArchiveChannel() + registry := prometheus.NewRegistry() + queue := NewUploadQueue(1, registry) + exporter := NewExportManager(config, &s.mockBackend, queue, registry) expectedkeys := set.NewSet[string](10) actualKeys := set.NewSet[string](10) @@ -111,7 +136,12 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { wg.Add(1) go func() { defer wg.Done() - for v := range objectCh { + for { + v, ok, err := queue.Dequeue(s.ctx) + s.Assert().NoError(err) + if !ok { + break + } actualKeys.Add(v.objectKey) } }() @@ -126,14 +156,16 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { expectedkeys.Add(key) } - close(objectCh) + queue.Close() wg.Wait() require.Equal(s.T(), expectedkeys, actualKeys) } func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { config := ExporterConfig{LedgersPerFile: 1, FilesPerPartition: 10} - exporter := NewExportManager(config, &s.mockBackend) + registry := prometheus.NewRegistry() + queue := NewUploadQueue(1, registry) + exporter := NewExportManager(config, &s.mockBackend, queue, registry) ctx, cancel := context.WithCancel(context.Background()) go func() { @@ -148,7 +180,9 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { func (s *ExportManagerSuite) TestAddLedgerCloseMetaKeyMismatch() { config := ExporterConfig{LedgersPerFile: 10, FilesPerPartition: 1} - exporter := NewExportManager(config, &s.mockBackend) + registry := prometheus.NewRegistry() + queue := NewUploadQueue(1, registry) + exporter := NewExportManager(config, &s.mockBackend, queue, registry) require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(16))) require.EqualError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(21)), diff --git a/exp/services/ledgerexporter/internal/gcs_datastore.go b/exp/services/ledgerexporter/internal/gcs_datastore.go index 4fa1287e94..927130fe6a 100644 --- a/exp/services/ledgerexporter/internal/gcs_datastore.go +++ b/exp/services/ledgerexporter/internal/gcs_datastore.go @@ -10,6 +10,7 @@ import ( "google.golang.org/api/googleapi" "cloud.google.com/go/storage" + "github.com/stellar/go/support/errors" ) @@ -21,7 +22,7 @@ type GCSDataStore struct { } // GetFile retrieves a file from the GCS bucket. -func (b *GCSDataStore) GetFile(ctx context.Context, filePath string) (io.ReadCloser, error) { +func (b GCSDataStore) GetFile(ctx context.Context, filePath string) (io.ReadCloser, error) { filePath = path.Join(b.prefix, filePath) r, err := b.bucket.Object(filePath).NewReader(ctx) if err != nil { @@ -35,26 +36,26 @@ func (b *GCSDataStore) GetFile(ctx context.Context, filePath string) (io.ReadClo } // PutFileIfNotExists uploads a file to GCS only if it doesn't already exist. -func (b *GCSDataStore) PutFileIfNotExists(ctx context.Context, filePath string, in io.WriterTo) error { +func (b GCSDataStore) PutFileIfNotExists(ctx context.Context, filePath string, in io.WriterTo) (bool, error) { err := b.putFile(ctx, filePath, in, &storage.Conditions{DoesNotExist: true}) if err != nil { if gcsError, ok := err.(*googleapi.Error); ok { switch gcsError.Code { case http.StatusPreconditionFailed: logger.Infof("Precondition failed: %s already exists in the bucket", filePath) - return nil // Treat as success + return false, nil // Treat as success default: logger.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) } } - return errors.Wrapf(err, "error uploading file: %s", filePath) + return false, errors.Wrapf(err, "error uploading file: %s", filePath) } logger.Infof("File uploaded successfully: %s", filePath) - return nil + return true, nil } // PutFile uploads a file to GCS -func (b *GCSDataStore) PutFile(ctx context.Context, filePath string, in io.WriterTo) error { +func (b GCSDataStore) PutFile(ctx context.Context, filePath string, in io.WriterTo) error { err := b.putFile(ctx, filePath, in, nil) // No conditions for regular PutFile if err != nil { @@ -68,7 +69,7 @@ func (b *GCSDataStore) PutFile(ctx context.Context, filePath string, in io.Write } // Size retrieves the size of a file in the GCS bucket. -func (b *GCSDataStore) Size(ctx context.Context, pth string) (int64, error) { +func (b GCSDataStore) Size(ctx context.Context, pth string) (int64, error) { pth = path.Join(b.prefix, pth) attrs, err := b.bucket.Object(pth).Attrs(ctx) if err == storage.ErrObjectNotExist { @@ -81,17 +82,17 @@ func (b *GCSDataStore) Size(ctx context.Context, pth string) (int64, error) { } // Exists checks if a file exists in the GCS bucket. -func (b *GCSDataStore) Exists(ctx context.Context, pth string) (bool, error) { +func (b GCSDataStore) Exists(ctx context.Context, pth string) (bool, error) { _, err := b.Size(ctx, pth) return err == nil, err } // Close closes the GCS client connection. -func (b *GCSDataStore) Close() error { +func (b GCSDataStore) Close() error { return b.client.Close() } -func (b *GCSDataStore) putFile(ctx context.Context, filePath string, in io.WriterTo, conditions *storage.Conditions) error { +func (b GCSDataStore) putFile(ctx context.Context, filePath string, in io.WriterTo, conditions *storage.Conditions) error { filePath = path.Join(b.prefix, filePath) o := b.bucket.Object(filePath) if conditions != nil { diff --git a/exp/services/ledgerexporter/internal/mock_datastore.go b/exp/services/ledgerexporter/internal/mock_datastore.go index 7675a87461..705df45a26 100644 --- a/exp/services/ledgerexporter/internal/mock_datastore.go +++ b/exp/services/ledgerexporter/internal/mock_datastore.go @@ -32,9 +32,9 @@ func (m *MockDataStore) PutFile(ctx context.Context, path string, in io.WriterTo return args.Error(0) } -func (m *MockDataStore) PutFileIfNotExists(ctx context.Context, path string, in io.WriterTo) error { +func (m *MockDataStore) PutFileIfNotExists(ctx context.Context, path string, in io.WriterTo) (bool, error) { args := m.Called(ctx, path, in) - return args.Error(0) + return args.Bool(0), args.Error(1) } func (m *MockDataStore) Close() error { diff --git a/exp/services/ledgerexporter/internal/queue.go b/exp/services/ledgerexporter/internal/queue.go new file mode 100644 index 0000000000..372ccb0056 --- /dev/null +++ b/exp/services/ledgerexporter/internal/queue.go @@ -0,0 +1,58 @@ +package ledgerexporter + +import ( + "context" + + "github.com/prometheus/client_golang/prometheus" +) + +// UploadQueue is a queue of LedgerMetaArchive objects which are scheduled for upload +type UploadQueue struct { + metaArchiveCh chan *LedgerMetaArchive + queueLengthMetric prometheus.Gauge +} + +// NewUploadQueue constructs a new UploadQueue +func NewUploadQueue(size int, prometheusRegistry *prometheus.Registry) UploadQueue { + queueLengthMetric := prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "ledger_exporter", + Subsystem: "upload_queue", + Name: "length", + Help: "The number of objects queued for upload", + }) + prometheusRegistry.MustRegister(queueLengthMetric) + return UploadQueue{ + metaArchiveCh: make(chan *LedgerMetaArchive, size), + queueLengthMetric: queueLengthMetric, + } +} + +// Enqueue will add an upload task to the queue. Enqueue may block if the queue is full. +func (u UploadQueue) Enqueue(ctx context.Context, archive *LedgerMetaArchive) error { + u.queueLengthMetric.Inc() + select { + case u.metaArchiveCh <- archive: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +// Dequeue will pop a task off the queue. Dequeue may block if the queue is empty. +func (u UploadQueue) Dequeue(ctx context.Context) (*LedgerMetaArchive, bool, error) { + select { + case <-ctx.Done(): + return nil, false, ctx.Err() + + case metaObject, ok := <-u.metaArchiveCh: + if ok { + u.queueLengthMetric.Dec() + } + return metaObject, ok, nil + } +} + +// Close will close the queue. +func (u UploadQueue) Close() { + close(u.metaArchiveCh) +} diff --git a/exp/services/ledgerexporter/internal/queue_test.go b/exp/services/ledgerexporter/internal/queue_test.go new file mode 100644 index 0000000000..1d001765ce --- /dev/null +++ b/exp/services/ledgerexporter/internal/queue_test.go @@ -0,0 +1,63 @@ +package ledgerexporter + +import ( + "context" + "testing" + + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" + "github.com/stretchr/testify/require" +) + +func TestQueueContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + queue := NewUploadQueue(0, prometheus.NewRegistry()) + cancel() + + require.ErrorIs(t, queue.Enqueue(ctx, nil), context.Canceled) + _, _, err := queue.Dequeue(ctx) + require.ErrorIs(t, err, context.Canceled) +} + +func getMetricValue(metric prometheus.Metric) *dto.Metric { + value := &dto.Metric{} + err := metric.Write(value) + if err != nil { + panic(err) + } + return value +} + +func TestQueue(t *testing.T) { + queue := NewUploadQueue(3, prometheus.NewRegistry()) + + require.NoError(t, queue.Enqueue(context.Background(), NewLedgerMetaArchive("test", 1, 1))) + require.NoError(t, queue.Enqueue(context.Background(), NewLedgerMetaArchive("test", 2, 2))) + require.NoError(t, queue.Enqueue(context.Background(), NewLedgerMetaArchive("test", 3, 3))) + + require.Equal(t, float64(3), getMetricValue(queue.queueLengthMetric).GetGauge().GetValue()) + queue.Close() + + l, ok, err := queue.Dequeue(context.Background()) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, float64(2), getMetricValue(queue.queueLengthMetric).GetGauge().GetValue()) + require.Equal(t, uint32(1), l.GetStartLedgerSequence()) + + l, ok, err = queue.Dequeue(context.Background()) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, float64(1), getMetricValue(queue.queueLengthMetric).GetGauge().GetValue()) + require.Equal(t, uint32(2), l.GetStartLedgerSequence()) + + l, ok, err = queue.Dequeue(context.Background()) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, float64(0), getMetricValue(queue.queueLengthMetric).GetGauge().GetValue()) + require.Equal(t, uint32(3), l.GetStartLedgerSequence()) + + l, ok, err = queue.Dequeue(context.Background()) + require.NoError(t, err) + require.False(t, false) + require.Nil(t, l) +} diff --git a/exp/services/ledgerexporter/internal/uploader.go b/exp/services/ledgerexporter/internal/uploader.go index 633db4ead7..04da703fd6 100644 --- a/exp/services/ledgerexporter/internal/uploader.go +++ b/exp/services/ledgerexporter/internal/uploader.go @@ -2,39 +2,108 @@ package ledgerexporter import ( "context" + "io" + "strconv" "time" "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" ) // Uploader is responsible for uploading data to a storage destination. -type Uploader interface { - Run(ctx context.Context) error - Upload(ctx context.Context, metaArchive *LedgerMetaArchive) error +type Uploader struct { + dataStore DataStore + queue UploadQueue + uploadDurationMetric *prometheus.SummaryVec + objectSizeMetrics *prometheus.SummaryVec } -type uploader struct { - dataStore DataStore - metaArchiveCh chan *LedgerMetaArchive +// NewUploader constructs a new Uploader instance +func NewUploader( + destination DataStore, + queue UploadQueue, + prometheusRegistry *prometheus.Registry, +) Uploader { + uploadDurationMetric := prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Namespace: "ledger_exporter", Subsystem: "uploader", Name: "put_duration_seconds", + Help: "duration for uploading a ledger batch, sliding window = 10m", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"already_exists", "ledgers"}, + ) + objectSizeMetrics := prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Namespace: "ledger_exporter", Subsystem: "uploader", Name: "object_size_bytes", + Help: "size of a ledger batch in bytes, sliding window = 10m", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"ledgers", "already_exists", "compression"}, + ) + prometheusRegistry.MustRegister(uploadDurationMetric, objectSizeMetrics) + return Uploader{ + dataStore: destination, + queue: queue, + uploadDurationMetric: uploadDurationMetric, + objectSizeMetrics: objectSizeMetrics, + } } -func NewUploader(destination DataStore, metaArchiveCh chan *LedgerMetaArchive) Uploader { - return &uploader{ - dataStore: destination, - metaArchiveCh: metaArchiveCh, - } +type writerRecorder struct { + io.Writer + count *int64 +} + +func (r writerRecorder) Write(p []byte) (int, error) { + total, err := r.Writer.Write(p) + *r.count += int64(total) + return total, err +} + +type writerToRecorder struct { + io.WriterTo + totalCompressed int64 + totalUncompressed int64 +} + +func (r *writerToRecorder) WriteTo(w io.Writer) (int64, error) { + uncompressedCount, err := r.WriterTo.WriteTo(writerRecorder{ + Writer: w, + count: &r.totalCompressed, + }) + r.totalUncompressed += uncompressedCount + return uncompressedCount, err } // Upload uploads the serialized binary data of ledger TxMeta to the specified destination. -// TODO: Add retry logic. -func (u *uploader) Upload(ctx context.Context, metaArchive *LedgerMetaArchive) error { +func (u Uploader) Upload(ctx context.Context, metaArchive *LedgerMetaArchive) error { logger.Infof("Uploading: %s", metaArchive.GetObjectKey()) + startTime := time.Now() + numLedgers := strconv.FormatUint(uint64(metaArchive.GetLedgerCount()), 10) - err := u.dataStore.PutFileIfNotExists(ctx, metaArchive.GetObjectKey(), - &XDRGzipEncoder{XdrPayload: &metaArchive.data}) + writerTo := &writerToRecorder{ + WriterTo: &XDRGzipEncoder{XdrPayload: &metaArchive.data}, + } + ok, err := u.dataStore.PutFileIfNotExists(ctx, metaArchive.GetObjectKey(), writerTo) if err != nil { return errors.Wrapf(err, "error uploading %s", metaArchive.GetObjectKey()) } + alreadyExists := strconv.FormatBool(!ok) + + u.uploadDurationMetric.With(prometheus.Labels{ + "ledgers": numLedgers, + "already_exists": alreadyExists, + }).Observe(time.Since(startTime).Seconds()) + u.objectSizeMetrics.With(prometheus.Labels{ + "compression": "none", + "ledgers": numLedgers, + "already_exists": alreadyExists, + }).Observe(float64(writerTo.totalUncompressed)) + u.objectSizeMetrics.With(prometheus.Labels{ + "compression": "gzip", + "ledgers": numLedgers, + "already_exists": alreadyExists, + }).Observe(float64(writerTo.totalCompressed)) return nil } @@ -42,7 +111,7 @@ func (u *uploader) Upload(ctx context.Context, metaArchive *LedgerMetaArchive) e var uploaderShutdownWaitTime = 10 * time.Second // Run starts the uploader, continuously listening for LedgerMetaArchive objects to upload. -func (u *uploader) Run(ctx context.Context) error { +func (u Uploader) Run(ctx context.Context) error { uploadCtx, cancel := context.WithCancel(context.Background()) go func() { <-ctx.Done() @@ -54,21 +123,19 @@ func (u *uploader) Run(ctx context.Context) error { }() for { - select { - case <-uploadCtx.Done(): - return uploadCtx.Err() + metaObject, ok, err := u.queue.Dequeue(uploadCtx) + if err != nil { + return err + } + if !ok { + logger.Info("Meta archive channel closed, stopping uploader") + return nil + } - case metaObject, ok := <-u.metaArchiveCh: - if !ok { - logger.Info("Meta archive channel closed, stopping uploader") - return nil - } - //Upload the received LedgerMetaArchive. - err := u.Upload(uploadCtx, metaObject) - if err != nil { - return err - } - logger.Infof("Uploaded %s successfully", metaObject.objectKey) + // Upload the received LedgerMetaArchive. + if err = u.Upload(uploadCtx, metaObject); err != nil { + return err } + logger.Infof("Uploaded %s successfully", metaObject.objectKey) } } diff --git a/exp/services/ledgerexporter/internal/uploader_test.go b/exp/services/ledgerexporter/internal/uploader_test.go index c2a0fb96ab..935b4445e8 100644 --- a/exp/services/ledgerexporter/internal/uploader_test.go +++ b/exp/services/ledgerexporter/internal/uploader_test.go @@ -5,13 +5,16 @@ import ( "context" "fmt" "io" + "strconv" "testing" "time" - "github.com/stellar/go/support/errors" + "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + + "github.com/stellar/go/support/errors" ) func TestUploaderSuite(t *testing.T) { @@ -31,97 +34,214 @@ func (s *UploaderSuite) SetupTest() { } func (s *UploaderSuite) TestUpload() { + s.testUpload(false) + s.testUpload(true) +} + +func (s *UploaderSuite) testUpload(putOkReturnVal bool) { key, start, end := "test-1-100", uint32(1), uint32(100) archive := NewLedgerMetaArchive(key, start, end) for i := start; i <= end; i++ { _ = archive.AddLedger(createLedgerCloseMeta(i)) } - var capturedWriterTo io.WriterTo + var capturedBuf bytes.Buffer var capturedKey string s.mockDataStore.On("PutFileIfNotExists", mock.Anything, key, mock.Anything). Run(func(args mock.Arguments) { capturedKey = args.Get(1).(string) - capturedWriterTo = args.Get(2).(io.WriterTo) - }).Return(nil).Once() + _, err := args.Get(2).(io.WriterTo).WriteTo(&capturedBuf) + require.NoError(s.T(), err) + }).Return(putOkReturnVal, nil).Once() - dataUploader := uploader{dataStore: &s.mockDataStore} + registry := prometheus.NewRegistry() + queue := NewUploadQueue(1, registry) + dataUploader := NewUploader(&s.mockDataStore, queue, registry) require.NoError(s.T(), dataUploader.Upload(context.Background(), archive)) - var capturedBuf bytes.Buffer - _, err := capturedWriterTo.WriteTo(&capturedBuf) - require.NoError(s.T(), err) - + expectedCompressedLength := capturedBuf.Len() var decodedArchive LedgerMetaArchive decoder := &XDRGzipDecoder{XdrPayload: &decodedArchive.data} - _, err = decoder.ReadFrom(&capturedBuf) + _, err := decoder.ReadFrom(&capturedBuf) require.NoError(s.T(), err) // require that the decoded data matches the original test data require.Equal(s.T(), key, capturedKey) require.Equal(s.T(), archive.data, decodedArchive.data) + + alreadyExists := !putOkReturnVal + metric, err := dataUploader.uploadDurationMetric.MetricVec.GetMetricWith(prometheus.Labels{ + "ledgers": "100", + "already_exists": strconv.FormatBool(alreadyExists), + }) + require.NoError(s.T(), err) + require.Equal( + s.T(), + uint64(1), + getMetricValue(metric).GetSummary().GetSampleCount(), + ) + require.Positive(s.T(), getMetricValue(metric).GetSummary().GetSampleSum()) + metric, err = dataUploader.uploadDurationMetric.MetricVec.GetMetricWith(prometheus.Labels{ + "ledgers": "100", + "already_exists": strconv.FormatBool(!alreadyExists), + }) + require.NoError(s.T(), err) + require.Equal( + s.T(), + uint64(0), + getMetricValue(metric).GetSummary().GetSampleCount(), + ) + + metric, err = dataUploader.objectSizeMetrics.MetricVec.GetMetricWith(prometheus.Labels{ + "ledgers": "100", + "compression": "gzip", + "already_exists": strconv.FormatBool(alreadyExists), + }) + require.NoError(s.T(), err) + require.Equal( + s.T(), + uint64(1), + getMetricValue(metric).GetSummary().GetSampleCount(), + ) + require.Equal( + s.T(), + float64(expectedCompressedLength), + getMetricValue(metric).GetSummary().GetSampleSum(), + ) + metric, err = dataUploader.objectSizeMetrics.MetricVec.GetMetricWith(prometheus.Labels{ + "ledgers": "100", + "compression": "gzip", + "already_exists": strconv.FormatBool(!alreadyExists), + }) + require.NoError(s.T(), err) + require.Equal( + s.T(), + uint64(0), + getMetricValue(metric).GetSummary().GetSampleCount(), + ) + + metric, err = dataUploader.objectSizeMetrics.MetricVec.GetMetricWith(prometheus.Labels{ + "ledgers": "100", + "compression": "none", + "already_exists": strconv.FormatBool(alreadyExists), + }) + require.NoError(s.T(), err) + require.Equal( + s.T(), + uint64(1), + getMetricValue(metric).GetSummary().GetSampleCount(), + ) + uncompressedPayload, err := decodedArchive.data.MarshalBinary() + require.NoError(s.T(), err) + require.Equal( + s.T(), + float64(len(uncompressedPayload)), + getMetricValue(metric).GetSummary().GetSampleSum(), + ) + metric, err = dataUploader.objectSizeMetrics.MetricVec.GetMetricWith(prometheus.Labels{ + "ledgers": "100", + "compression": "none", + "already_exists": strconv.FormatBool(!alreadyExists), + }) + require.NoError(s.T(), err) + require.Equal( + s.T(), + uint64(0), + getMetricValue(metric).GetSummary().GetSampleCount(), + ) } func (s *UploaderSuite) TestUploadPutError() { + s.testUploadPutError(true) + s.testUploadPutError(false) +} + +func (s *UploaderSuite) testUploadPutError(putOkReturnVal bool) { key, start, end := "test-1-100", uint32(1), uint32(100) archive := NewLedgerMetaArchive(key, start, end) s.mockDataStore.On("PutFileIfNotExists", context.Background(), key, - mock.Anything).Return(errors.New("error in PutFileIfNotExists")) + mock.Anything).Return(putOkReturnVal, errors.New("error in PutFileIfNotExists")) - dataUploader := uploader{dataStore: &s.mockDataStore} + registry := prometheus.NewRegistry() + queue := NewUploadQueue(1, registry) + dataUploader := NewUploader(&s.mockDataStore, queue, registry) err := dataUploader.Upload(context.Background(), archive) require.Equal(s.T(), fmt.Sprintf("error uploading %s: error in PutFileIfNotExists", key), err.Error()) + + for _, alreadyExists := range []string{"true", "false"} { + metric, err := dataUploader.uploadDurationMetric.MetricVec.GetMetricWith(prometheus.Labels{ + "ledgers": "100", + "already_exists": alreadyExists, + }) + require.NoError(s.T(), err) + require.Equal( + s.T(), + uint64(0), + getMetricValue(metric).GetSummary().GetSampleCount(), + ) + + for _, compression := range []string{"gzip", "none"} { + metric, err = dataUploader.objectSizeMetrics.MetricVec.GetMetricWith(prometheus.Labels{ + "ledgers": "100", + "compression": compression, + "already_exists": alreadyExists, + }) + require.NoError(s.T(), err) + require.Equal( + s.T(), + uint64(0), + getMetricValue(metric).GetSummary().GetSampleCount(), + ) + } + } } func (s *UploaderSuite) TestRunChannelClose() { s.mockDataStore.On("PutFileIfNotExists", mock.Anything, - mock.Anything, mock.Anything).Return(nil) + mock.Anything, mock.Anything).Return(true, nil) - objectCh := make(chan *LedgerMetaArchive, 1) + registry := prometheus.NewRegistry() + queue := NewUploadQueue(1, registry) go func() { key, start, end := "test", uint32(1), uint32(100) for i := start; i <= end; i++ { - objectCh <- NewLedgerMetaArchive(key, i, i) + s.Assert().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive(key, i, i))) } <-time.After(time.Second * 2) - close(objectCh) + queue.Close() }() - dataUploader := uploader{dataStore: &s.mockDataStore, metaArchiveCh: objectCh} + dataUploader := NewUploader(&s.mockDataStore, queue, registry) require.NoError(s.T(), dataUploader.Run(context.Background())) } func (s *UploaderSuite) TestRunContextCancel() { - objectCh := make(chan *LedgerMetaArchive, 1) - s.mockDataStore.On("PutFileIfNotExists", mock.Anything, mock.Anything, mock.Anything).Return(nil) + s.mockDataStore.On("PutFileIfNotExists", mock.Anything, mock.Anything, mock.Anything).Return(true, nil) ctx, cancel := context.WithCancel(context.Background()) + registry := prometheus.NewRegistry() + queue := NewUploadQueue(1, registry) - go func() { - for { - objectCh <- NewLedgerMetaArchive("test", 1, 1) - } - }() + s.Assert().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive("test", 1, 1))) go func() { <-time.After(time.Second * 2) cancel() }() - dataUploader := uploader{dataStore: &s.mockDataStore, metaArchiveCh: objectCh} - err := dataUploader.Run(ctx) - - require.EqualError(s.T(), err, "context canceled") + dataUploader := NewUploader(&s.mockDataStore, queue, registry) + require.EqualError(s.T(), dataUploader.Run(ctx), "context canceled") } func (s *UploaderSuite) TestRunUploadError() { - objectCh := make(chan *LedgerMetaArchive, 10) - objectCh <- NewLedgerMetaArchive("test", 1, 1) + registry := prometheus.NewRegistry() + queue := NewUploadQueue(1, registry) + s.Assert().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive("test", 1, 1))) s.mockDataStore.On("PutFileIfNotExists", mock.Anything, "test", - mock.Anything).Return(errors.New("Put error")) + mock.Anything).Return(false, errors.New("Put error")) - dataUploader := uploader{dataStore: &s.mockDataStore, metaArchiveCh: objectCh} + dataUploader := NewUploader(&s.mockDataStore, queue, registry) err := dataUploader.Run(context.Background()) require.Equal(s.T(), "error uploading test: Put error", err.Error()) } diff --git a/exp/services/ledgerexporter/internal/utils.go b/exp/services/ledgerexporter/internal/utils.go index d1bc8e20d1..8b556c5814 100644 --- a/exp/services/ledgerexporter/internal/utils.go +++ b/exp/services/ledgerexporter/internal/utils.go @@ -6,6 +6,7 @@ import ( "io" xdr3 "github.com/stellar/go-xdr/xdr3" + "github.com/stellar/go/historyarchive" "github.com/stellar/go/support/errors" "github.com/stellar/go/support/storage" From a9ac66b704a7d3f0a192e040bb0183cb81b5f2f9 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Wed, 24 Apr 2024 11:49:55 -0400 Subject: [PATCH 124/234] Fix merge issues --- exp/services/ledgerexporter/internal/app.go | 12 ++++++------ exp/services/ledgerexporter/internal/uploader.go | 4 ++-- .../ledgerexporter/internal/uploader_test.go | 4 +--- support/datastore/gcs_datastore.go | 4 ++-- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/exp/services/ledgerexporter/internal/app.go b/exp/services/ledgerexporter/internal/app.go index f086f5a6a6..7e3961dfb6 100644 --- a/exp/services/ledgerexporter/internal/app.go +++ b/exp/services/ledgerexporter/internal/app.go @@ -26,11 +26,11 @@ var ( ) type App struct { - config Config - ledgerBackend ledgerbackend.LedgerBackend - dataStore datastore.DataStore - exportManager ExportManager - uploader Uploader + config Config + ledgerBackend ledgerbackend.LedgerBackend + dataStore datastore.DataStore + exportManager *ExportManager + uploader Uploader prometheusRegistry *prometheus.Registry } @@ -134,7 +134,7 @@ func (a *App) Run() { logger.Info("Shutting down ledger-exporter") } -func mustNewDataStore(ctx context.Context, config *Config) datastore.DataStore { +func mustNewDataStore(ctx context.Context, config Config) datastore.DataStore { dataStore, err := datastore.NewDataStore(ctx, fmt.Sprintf("%s/%s", config.DestinationURL, config.Network)) logFatalIf(err, "Could not connect to destination data store") return dataStore diff --git a/exp/services/ledgerexporter/internal/uploader.go b/exp/services/ledgerexporter/internal/uploader.go index 918953b640..f7db44abd2 100644 --- a/exp/services/ledgerexporter/internal/uploader.go +++ b/exp/services/ledgerexporter/internal/uploader.go @@ -7,8 +7,8 @@ import ( "time" "github.com/pkg/errors" - "github.com/stellar/go/support/datastore" "github.com/prometheus/client_golang/prometheus" + "github.com/stellar/go/support/datastore" ) // Uploader is responsible for uploading data to a storage destination. @@ -21,7 +21,7 @@ type Uploader struct { // NewUploader constructs a new Uploader instance func NewUploader( - destination DataStore, + destination datastore.DataStore, queue UploadQueue, prometheusRegistry *prometheus.Registry, ) Uploader { diff --git a/exp/services/ledgerexporter/internal/uploader_test.go b/exp/services/ledgerexporter/internal/uploader_test.go index 00db940809..6c4dddade4 100644 --- a/exp/services/ledgerexporter/internal/uploader_test.go +++ b/exp/services/ledgerexporter/internal/uploader_test.go @@ -9,14 +9,12 @@ import ( "testing" "time" + "github.com/prometheus/client_golang/prometheus" "github.com/stellar/go/support/datastore" "github.com/stellar/go/support/errors" - "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - - "github.com/stellar/go/support/errors" ) func TestUploaderSuite(t *testing.T) { diff --git a/support/datastore/gcs_datastore.go b/support/datastore/gcs_datastore.go index 2281be8f82..7a2418e8c7 100644 --- a/support/datastore/gcs_datastore.go +++ b/support/datastore/gcs_datastore.go @@ -45,7 +45,7 @@ func (b GCSDataStore) PutFileIfNotExists(ctx context.Context, filePath string, i if gcsError, ok := err.(*googleapi.Error); ok { switch gcsError.Code { case http.StatusPreconditionFailed: - logger.Infof("Precondition failed: %s already exists in the bucket", filePath) + log.Infof("Precondition failed: %s already exists in the bucket", filePath) return false, nil // Treat as success default: log.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) @@ -53,7 +53,7 @@ func (b GCSDataStore) PutFileIfNotExists(ctx context.Context, filePath string, i } return false, errors.Wrapf(err, "error uploading file: %s", filePath) } - logger.Infof("File uploaded successfully: %s", filePath) + log.Infof("File uploaded successfully: %s", filePath) return true, nil } From 4fa8e69a8be41372199015f481c0e6aa659d3747 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Wed, 24 Apr 2024 17:54:18 +0200 Subject: [PATCH 125/234] Bump XDR definitions (#5289) --- Makefile | 2 +- gxdr/xdr_generated.go | 48 +------------- xdr/Stellar-contract-spec.x | 8 --- xdr/xdr_commit_generated.txt | 2 +- xdr/xdr_generated.go | 120 +---------------------------------- 5 files changed, 4 insertions(+), 176 deletions(-) diff --git a/Makefile b/Makefile index fe2021d39a..13e51985ad 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ XDRS = $(DOWNLOADABLE_XDRS) xdr/Stellar-lighthorizon.x \ XDRGEN_COMMIT=e2cac557162d99b12ae73b846cf3d5bfe16636de -XDR_COMMIT=2ba4049554bb0564950e6d9213e01a60fc190f54 +XDR_COMMIT=1a04392432dacc0092caaeae22a600ea1af3c6a5 .PHONY: xdr xdr-clean xdr-update diff --git a/gxdr/xdr_generated.go b/gxdr/xdr_generated.go index 214d405e80..e7c8f3ed58 100644 --- a/gxdr/xdr_generated.go +++ b/gxdr/xdr_generated.go @@ -3804,7 +3804,6 @@ const ( SC_SPEC_TYPE_MAP SCSpecType = 1004 SC_SPEC_TYPE_TUPLE SCSpecType = 1005 SC_SPEC_TYPE_BYTES_N SCSpecType = 1006 - SC_SPEC_TYPE_HASH SCSpecType = 1007 // User defined types. SC_SPEC_TYPE_UDT SCSpecType = 2000 ) @@ -3835,10 +3834,6 @@ type SCSpecTypeBytesN struct { N Uint32 } -type SCSpectTypeHash struct { - N Uint32 -} - type SCSpecTypeUDT struct { Name string // bound 60 } @@ -3859,8 +3854,6 @@ type SCSpecTypeDef struct { // Tuple() *SCSpecTypeTuple // SC_SPEC_TYPE_BYTES_N: // BytesN() *SCSpecTypeBytesN - // SC_SPEC_TYPE_HASH: - // Hash() *SCSpectTypeHash // SC_SPEC_TYPE_UDT: // Udt() *SCSpecTypeUDT Type SCSpecType @@ -25631,7 +25624,6 @@ var _XdrNames_SCSpecType = map[int32]string{ int32(SC_SPEC_TYPE_MAP): "SC_SPEC_TYPE_MAP", int32(SC_SPEC_TYPE_TUPLE): "SC_SPEC_TYPE_TUPLE", int32(SC_SPEC_TYPE_BYTES_N): "SC_SPEC_TYPE_BYTES_N", - int32(SC_SPEC_TYPE_HASH): "SC_SPEC_TYPE_HASH", int32(SC_SPEC_TYPE_UDT): "SC_SPEC_TYPE_UDT", } var _XdrValues_SCSpecType = map[string]int32{ @@ -25659,7 +25651,6 @@ var _XdrValues_SCSpecType = map[string]int32{ "SC_SPEC_TYPE_MAP": int32(SC_SPEC_TYPE_MAP), "SC_SPEC_TYPE_TUPLE": int32(SC_SPEC_TYPE_TUPLE), "SC_SPEC_TYPE_BYTES_N": int32(SC_SPEC_TYPE_BYTES_N), - "SC_SPEC_TYPE_HASH": int32(SC_SPEC_TYPE_HASH), "SC_SPEC_TYPE_UDT": int32(SC_SPEC_TYPE_UDT), } @@ -25852,20 +25843,6 @@ func (v *SCSpecTypeBytesN) XdrRecurse(x XDR, name string) { } func XDR_SCSpecTypeBytesN(v *SCSpecTypeBytesN) *SCSpecTypeBytesN { return v } -type XdrType_SCSpectTypeHash = *SCSpectTypeHash - -func (v *SCSpectTypeHash) XdrPointer() interface{} { return v } -func (SCSpectTypeHash) XdrTypeName() string { return "SCSpectTypeHash" } -func (v SCSpectTypeHash) XdrValue() interface{} { return v } -func (v *SCSpectTypeHash) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -func (v *SCSpectTypeHash) XdrRecurse(x XDR, name string) { - if name != "" { - name = x.Sprintf("%s.", name) - } - x.Marshal(x.Sprintf("%sn", name), XDR_Uint32(&v.N)) -} -func XDR_SCSpectTypeHash(v *SCSpectTypeHash) *SCSpectTypeHash { return v } - type XdrType_SCSpecTypeUDT = *SCSpecTypeUDT func (v *SCSpecTypeUDT) XdrPointer() interface{} { return v } @@ -25905,7 +25882,6 @@ var _XdrTags_SCSpecTypeDef = map[int32]bool{ XdrToI32(SC_SPEC_TYPE_MAP): true, XdrToI32(SC_SPEC_TYPE_TUPLE): true, XdrToI32(SC_SPEC_TYPE_BYTES_N): true, - XdrToI32(SC_SPEC_TYPE_HASH): true, XdrToI32(SC_SPEC_TYPE_UDT): true, } @@ -26002,21 +25978,6 @@ func (u *SCSpecTypeDef) BytesN() *SCSpecTypeBytesN { return nil } } -func (u *SCSpecTypeDef) Hash() *SCSpectTypeHash { - switch u.Type { - case SC_SPEC_TYPE_HASH: - if v, ok := u._u.(*SCSpectTypeHash); ok { - return v - } else { - var zero SCSpectTypeHash - u._u = &zero - return &zero - } - default: - XdrPanic("SCSpecTypeDef.Hash accessed when Type == %v", u.Type) - return nil - } -} func (u *SCSpecTypeDef) Udt() *SCSpecTypeUDT { switch u.Type { case SC_SPEC_TYPE_UDT: @@ -26034,7 +25995,7 @@ func (u *SCSpecTypeDef) Udt() *SCSpecTypeUDT { } func (u SCSpecTypeDef) XdrValid() bool { switch u.Type { - case SC_SPEC_TYPE_VAL, SC_SPEC_TYPE_BOOL, SC_SPEC_TYPE_VOID, SC_SPEC_TYPE_ERROR, SC_SPEC_TYPE_U32, SC_SPEC_TYPE_I32, SC_SPEC_TYPE_U64, SC_SPEC_TYPE_I64, SC_SPEC_TYPE_TIMEPOINT, SC_SPEC_TYPE_DURATION, SC_SPEC_TYPE_U128, SC_SPEC_TYPE_I128, SC_SPEC_TYPE_U256, SC_SPEC_TYPE_I256, SC_SPEC_TYPE_BYTES, SC_SPEC_TYPE_STRING, SC_SPEC_TYPE_SYMBOL, SC_SPEC_TYPE_ADDRESS, SC_SPEC_TYPE_OPTION, SC_SPEC_TYPE_RESULT, SC_SPEC_TYPE_VEC, SC_SPEC_TYPE_MAP, SC_SPEC_TYPE_TUPLE, SC_SPEC_TYPE_BYTES_N, SC_SPEC_TYPE_HASH, SC_SPEC_TYPE_UDT: + case SC_SPEC_TYPE_VAL, SC_SPEC_TYPE_BOOL, SC_SPEC_TYPE_VOID, SC_SPEC_TYPE_ERROR, SC_SPEC_TYPE_U32, SC_SPEC_TYPE_I32, SC_SPEC_TYPE_U64, SC_SPEC_TYPE_I64, SC_SPEC_TYPE_TIMEPOINT, SC_SPEC_TYPE_DURATION, SC_SPEC_TYPE_U128, SC_SPEC_TYPE_I128, SC_SPEC_TYPE_U256, SC_SPEC_TYPE_I256, SC_SPEC_TYPE_BYTES, SC_SPEC_TYPE_STRING, SC_SPEC_TYPE_SYMBOL, SC_SPEC_TYPE_ADDRESS, SC_SPEC_TYPE_OPTION, SC_SPEC_TYPE_RESULT, SC_SPEC_TYPE_VEC, SC_SPEC_TYPE_MAP, SC_SPEC_TYPE_TUPLE, SC_SPEC_TYPE_BYTES_N, SC_SPEC_TYPE_UDT: return true } return false @@ -26061,8 +26022,6 @@ func (u *SCSpecTypeDef) XdrUnionBody() XdrType { return XDR_SCSpecTypeTuple(u.Tuple()) case SC_SPEC_TYPE_BYTES_N: return XDR_SCSpecTypeBytesN(u.BytesN()) - case SC_SPEC_TYPE_HASH: - return XDR_SCSpectTypeHash(u.Hash()) case SC_SPEC_TYPE_UDT: return XDR_SCSpecTypeUDT(u.Udt()) } @@ -26084,8 +26043,6 @@ func (u *SCSpecTypeDef) XdrUnionBodyName() string { return "Tuple" case SC_SPEC_TYPE_BYTES_N: return "BytesN" - case SC_SPEC_TYPE_HASH: - return "Hash" case SC_SPEC_TYPE_UDT: return "Udt" } @@ -26124,9 +26081,6 @@ func (u *SCSpecTypeDef) XdrRecurse(x XDR, name string) { case SC_SPEC_TYPE_BYTES_N: x.Marshal(x.Sprintf("%sbytesN", name), XDR_SCSpecTypeBytesN(u.BytesN())) return - case SC_SPEC_TYPE_HASH: - x.Marshal(x.Sprintf("%shash", name), XDR_SCSpectTypeHash(u.Hash())) - return case SC_SPEC_TYPE_UDT: x.Marshal(x.Sprintf("%sudt", name), XDR_SCSpecTypeUDT(u.Udt())) return diff --git a/xdr/Stellar-contract-spec.x b/xdr/Stellar-contract-spec.x index 5d3e029951..6988a63385 100644 --- a/xdr/Stellar-contract-spec.x +++ b/xdr/Stellar-contract-spec.x @@ -42,7 +42,6 @@ enum SCSpecType SC_SPEC_TYPE_MAP = 1004, SC_SPEC_TYPE_TUPLE = 1005, SC_SPEC_TYPE_BYTES_N = 1006, - SC_SPEC_TYPE_HASH = 1007, // User defined types. SC_SPEC_TYPE_UDT = 2000 @@ -80,11 +79,6 @@ struct SCSpecTypeBytesN uint32 n; }; -struct SCSpectTypeHash -{ - uint32 n; -}; - struct SCSpecTypeUDT { string name<60>; @@ -123,8 +117,6 @@ case SC_SPEC_TYPE_TUPLE: SCSpecTypeTuple tuple; case SC_SPEC_TYPE_BYTES_N: SCSpecTypeBytesN bytesN; -case SC_SPEC_TYPE_HASH: - SCSpectTypeHash hash; case SC_SPEC_TYPE_UDT: SCSpecTypeUDT udt; }; diff --git a/xdr/xdr_commit_generated.txt b/xdr/xdr_commit_generated.txt index 8c75a203d4..52f66572f6 100644 --- a/xdr/xdr_commit_generated.txt +++ b/xdr/xdr_commit_generated.txt @@ -1 +1 @@ -2ba4049554bb0564950e6d9213e01a60fc190f54 \ No newline at end of file +1a04392432dacc0092caaeae22a600ea1af3c6a5 \ No newline at end of file diff --git a/xdr/xdr_generated.go b/xdr/xdr_generated.go index 49f8c691a3..b336714562 100644 --- a/xdr/xdr_generated.go +++ b/xdr/xdr_generated.go @@ -37,7 +37,7 @@ var XdrFilesSHA256 = map[string]string{ "xdr/Stellar-contract-config-setting.x": "393369678663cb0f9471a0b69e2a9cfa3ac93c4415fa40cec166e9a231ecbe0d", "xdr/Stellar-contract-env-meta.x": "928a30de814ee589bc1d2aadd8dd81c39f71b7e6f430f56974505ccb1f49654b", "xdr/Stellar-contract-meta.x": "f01532c11ca044e19d9f9f16fe373e9af64835da473be556b9a807ee3319ae0d", - "xdr/Stellar-contract-spec.x": "8d7f6bdd82c3e529cd8c6f035202ca0e7677cc05e4727492a165dfdc51a9cb3e", + "xdr/Stellar-contract-spec.x": "c7ffa21d2e91afb8e666b33524d307955426ff553a486d670c29217ed9888d49", "xdr/Stellar-contract.x": "7f665e4103e146a88fcdabce879aaaacd3bf9283feb194cc47ff986264c1e315", "xdr/Stellar-exporter.x": "a00c83d02e8c8382e06f79a191f1fb5abd097a4bbcab8481c67467e3270e0529", "xdr/Stellar-internal.x": "227835866c1b2122d1eaf28839ba85ea7289d1cb681dda4ca619c2da3d71fe00", @@ -48095,7 +48095,6 @@ const ScSpecDocLimit = 1024 // SC_SPEC_TYPE_MAP = 1004, // SC_SPEC_TYPE_TUPLE = 1005, // SC_SPEC_TYPE_BYTES_N = 1006, -// SC_SPEC_TYPE_HASH = 1007, // // // User defined types. // SC_SPEC_TYPE_UDT = 2000 @@ -48127,7 +48126,6 @@ const ( ScSpecTypeScSpecTypeMap ScSpecType = 1004 ScSpecTypeScSpecTypeTuple ScSpecType = 1005 ScSpecTypeScSpecTypeBytesN ScSpecType = 1006 - ScSpecTypeScSpecTypeHash ScSpecType = 1007 ScSpecTypeScSpecTypeUdt ScSpecType = 2000 ) @@ -48156,7 +48154,6 @@ var scSpecTypeMap = map[int32]string{ 1004: "ScSpecTypeScSpecTypeMap", 1005: "ScSpecTypeScSpecTypeTuple", 1006: "ScSpecTypeScSpecTypeBytesN", - 1007: "ScSpecTypeScSpecTypeHash", 2000: "ScSpecTypeScSpecTypeUdt", } @@ -48662,71 +48659,6 @@ func (s ScSpecTypeBytesN) xdrType() {} var _ xdrType = (*ScSpecTypeBytesN)(nil) -// ScSpectTypeHash is an XDR Struct defines as: -// -// struct SCSpectTypeHash -// { -// uint32 n; -// }; -type ScSpectTypeHash struct { - N Uint32 -} - -// EncodeTo encodes this value using the Encoder. -func (s *ScSpectTypeHash) EncodeTo(e *xdr.Encoder) error { - var err error - if err = s.N.EncodeTo(e); err != nil { - return err - } - return nil -} - -var _ decoderFrom = (*ScSpectTypeHash)(nil) - -// DecodeFrom decodes this value using the Decoder. -func (s *ScSpectTypeHash) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { - if maxDepth == 0 { - return 0, fmt.Errorf("decoding ScSpectTypeHash: %w", ErrMaxDecodingDepthReached) - } - maxDepth -= 1 - var err error - var n, nTmp int - nTmp, err = s.N.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding Uint32: %w", err) - } - return n, nil -} - -// MarshalBinary implements encoding.BinaryMarshaler. -func (s ScSpectTypeHash) MarshalBinary() ([]byte, error) { - b := bytes.Buffer{} - e := xdr.NewEncoder(&b) - err := s.EncodeTo(e) - return b.Bytes(), err -} - -// UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *ScSpectTypeHash) UnmarshalBinary(inp []byte) error { - r := bytes.NewReader(inp) - o := xdr.DefaultDecodeOptions - o.MaxInputLen = len(inp) - d := xdr.NewDecoderWithOptions(r, o) - _, err := s.DecodeFrom(d, o.MaxDepth) - return err -} - -var ( - _ encoding.BinaryMarshaler = (*ScSpectTypeHash)(nil) - _ encoding.BinaryUnmarshaler = (*ScSpectTypeHash)(nil) -) - -// xdrType signals that this type represents XDR values defined by this package. -func (s ScSpectTypeHash) xdrType() {} - -var _ xdrType = (*ScSpectTypeHash)(nil) - // ScSpecTypeUdt is an XDR Struct defines as: // // struct SCSpecTypeUDT @@ -48827,8 +48759,6 @@ var _ xdrType = (*ScSpecTypeUdt)(nil) // SCSpecTypeTuple tuple; // case SC_SPEC_TYPE_BYTES_N: // SCSpecTypeBytesN bytesN; -// case SC_SPEC_TYPE_HASH: -// SCSpectTypeHash hash; // case SC_SPEC_TYPE_UDT: // SCSpecTypeUDT udt; // }; @@ -48840,7 +48770,6 @@ type ScSpecTypeDef struct { Map *ScSpecTypeMap Tuple *ScSpecTypeTuple BytesN *ScSpecTypeBytesN - Hash *ScSpectTypeHash Udt *ScSpecTypeUdt } @@ -48902,8 +48831,6 @@ func (u ScSpecTypeDef) ArmForSwitch(sw int32) (string, bool) { return "Tuple", true case ScSpecTypeScSpecTypeBytesN: return "BytesN", true - case ScSpecTypeScSpecTypeHash: - return "Hash", true case ScSpecTypeScSpecTypeUdt: return "Udt", true } @@ -48992,13 +48919,6 @@ func NewScSpecTypeDef(aType ScSpecType, value interface{}) (result ScSpecTypeDef return } result.BytesN = &tv - case ScSpecTypeScSpecTypeHash: - tv, ok := value.(ScSpectTypeHash) - if !ok { - err = errors.New("invalid value, must be ScSpectTypeHash") - return - } - result.Hash = &tv case ScSpecTypeScSpecTypeUdt: tv, ok := value.(ScSpecTypeUdt) if !ok { @@ -49160,31 +49080,6 @@ func (u ScSpecTypeDef) GetBytesN() (result ScSpecTypeBytesN, ok bool) { return } -// MustHash retrieves the Hash value from the union, -// panicing if the value is not set. -func (u ScSpecTypeDef) MustHash() ScSpectTypeHash { - val, ok := u.GetHash() - - if !ok { - panic("arm Hash is not set") - } - - return val -} - -// GetHash retrieves the Hash value from the union, -// returning ok if the union's switch indicated the value is valid. -func (u ScSpecTypeDef) GetHash() (result ScSpectTypeHash, ok bool) { - armName, _ := u.ArmForSwitch(int32(u.Type)) - - if armName == "Hash" { - result = *u.Hash - ok = true - } - - return -} - // MustUdt retrieves the Udt value from the union, // panicing if the value is not set. func (u ScSpecTypeDef) MustUdt() ScSpecTypeUdt { @@ -49301,11 +49196,6 @@ func (u ScSpecTypeDef) EncodeTo(e *xdr.Encoder) error { return err } return nil - case ScSpecTypeScSpecTypeHash: - if err = (*u.Hash).EncodeTo(e); err != nil { - return err - } - return nil case ScSpecTypeScSpecTypeUdt: if err = (*u.Udt).EncodeTo(e); err != nil { return err @@ -49433,14 +49323,6 @@ func (u *ScSpecTypeDef) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { return n, fmt.Errorf("decoding ScSpecTypeBytesN: %w", err) } return n, nil - case ScSpecTypeScSpecTypeHash: - u.Hash = new(ScSpectTypeHash) - nTmp, err = (*u.Hash).DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding ScSpectTypeHash: %w", err) - } - return n, nil case ScSpecTypeScSpecTypeUdt: u.Udt = new(ScSpecTypeUdt) nTmp, err = (*u.Udt).DecodeFrom(d, maxDepth) From dd9cdb3c0bf18d797a3f0d8264b6f87cf9edebd7 Mon Sep 17 00:00:00 2001 From: Urvi Date: Wed, 24 Apr 2024 12:59:09 -0700 Subject: [PATCH 126/234] Update changelog to highlight protocol 21 support --- services/horizon/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 244a8c15c2..1d44eb5ec0 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -7,6 +7,8 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ## 2.30.0 +**This release adds support for Protocol 21** + ### Added - Bump XDR for [protocol 21](https://github.com/stellar/stellar-xdr/releases/tag/v21.0) From a387ffb88fd58cd7889f951ce766ec7bf105d2fb Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Fri, 26 Apr 2024 09:43:03 -0400 Subject: [PATCH 127/234] services/horizon + horizonclient: Add new async transaction submission endpoint (#5188) * Add new txsub endpoint - 1 * Add new txsub endpoint - 2 * Add new txsub endpoint - 3 * Update Status to TxStatus * Add unittests for new endpoint * Create submit_transaction_async_test.go * Fix goimports * Rearrange code and remove duplicate code * Add metrics - 1 * Add metrics - 2 * Add metrics - 3 * Fix failing unittest * Add new endpoint to go sdk + integration test * Small changes - 1 * Add openAPI taml * Address review changes - 1 * Remove private methods from interface * Use common metrics client for legacy and async txsub * Fix submitter test * Update submit_transaction_async.go * Fix failing test * Update txsub_async_oapi.yaml * Update submitter.go * Interface method change * Remove duplicate code * Add test for GET /transactions-async * Encapsulation - 1 * Change endpoint naming * Pass interface instead of client * Remove ClientInterface * Remove HTTP Status from submission response * Add logging statements * Fix failing integration tests * Fix failing tests - 1 * Add back deleted files * Remove circular import * Group metrics into submission duration * Group metrics into submission duration - 2 * Remove logging statements where not needed * Change to internal server error * Use request context logger * Use interface method for setting http status * Remove not needed metrics * Remove version * add error in extras * Resolve merge conflicts * Add TODO for problem response * Adding and removing logging statements * Move interface to async handler file * change httpstatus interface definition * Add deleted files back * Revert friendbot change * Add test for getting pending tx * Fix failing test * remove metrics struct and make vars private * pass only rawTx string * Move mock to test file * Make core client private * Remove UpdateTxSubMetrics func * Change http status for DISABLE_TX_SUB * Fix failing unittest * Revert submitter changes * Fix failing submitter_test * Revert import changes * Revert import changes - 2 * Revert import changes - 3 * Remove integration test function * Update main.go --- .gitignore | 1 + clients/horizonclient/client.go | 112 +++++++-- clients/horizonclient/internal.go | 5 +- clients/horizonclient/main.go | 5 + clients/horizonclient/mocks.go | 30 +++ clients/stellarcore/metrics_client.go | 70 ++++++ protocols/horizon/main.go | 29 +++ .../internal/actions/submit_transaction.go | 13 +- .../actions/submit_transaction_async.go | 138 +++++++++++ .../actions/submit_transaction_async_test.go | 218 ++++++++++++++++++ .../actions/submit_transaction_test.go | 2 +- services/horizon/internal/app.go | 1 + services/horizon/internal/httpx/handler.go | 11 +- services/horizon/internal/httpx/router.go | 23 ++ .../httpx/static/txsub_async_oapi.yaml | 168 ++++++++++++++ services/horizon/internal/init.go | 2 +- .../internal/integration/txsub_async_test.go | 146 ++++++++++++ .../internal/test/integration/integration.go | 15 ++ services/horizon/internal/txsub/submitter.go | 14 +- .../horizon/internal/txsub/submitter_test.go | 46 ++-- services/horizon/internal/txsub/system.go | 56 +---- .../horizon/internal/txsub/system_test.go | 2 - 22 files changed, 1001 insertions(+), 106 deletions(-) create mode 100644 clients/stellarcore/metrics_client.go create mode 100644 services/horizon/internal/actions/submit_transaction_async.go create mode 100644 services/horizon/internal/actions/submit_transaction_async_test.go create mode 100644 services/horizon/internal/httpx/static/txsub_async_oapi.yaml create mode 100644 services/horizon/internal/integration/txsub_async_test.go diff --git a/.gitignore b/.gitignore index e4760a698c..c505323c66 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /services/horizon/captive-core /services/horizon/horizon /services/horizon/stellar-horizon +/bucket-cache .vscode .idea debug diff --git a/clients/horizonclient/client.go b/clients/horizonclient/client.go index 0b51237bdd..0c580de771 100644 --- a/clients/horizonclient/client.go +++ b/clients/horizonclient/client.go @@ -440,6 +440,44 @@ func (c *Client) OperationDetail(id string) (ops operations.Operation, err error return ops, nil } +// validateFeeBumpTx checks if the inner transaction has a memo or not and converts the transaction object to +// base64 string. +func (c *Client) validateFeeBumpTx(transaction *txnbuild.FeeBumpTransaction, opts SubmitTxOpts) (string, error) { + var err error + if inner := transaction.InnerTransaction(); !opts.SkipMemoRequiredCheck && inner.Memo() == nil { + err = c.checkMemoRequired(inner) + if err != nil { + return "", err + } + } + + txeBase64, err := transaction.Base64() + if err != nil { + err = errors.Wrap(err, "Unable to convert transaction object to base64 string") + return "", err + } + return txeBase64, nil +} + +// validateTx checks if the transaction has a memo or not and converts the transaction object to +// base64 string. +func (c *Client) validateTx(transaction *txnbuild.Transaction, opts SubmitTxOpts) (string, error) { + var err error + if !opts.SkipMemoRequiredCheck && transaction.Memo() == nil { + err = c.checkMemoRequired(transaction) + if err != nil { + return "", err + } + } + + txeBase64, err := transaction.Base64() + if err != nil { + err = errors.Wrap(err, "Unable to convert transaction object to base64 string") + return "", err + } + return txeBase64, nil +} + // SubmitTransactionXDR submits a transaction represented as a base64 XDR string to the network. err can be either error object or horizon.Error object. // See https://developers.stellar.org/api/resources/transactions/post/ func (c *Client) SubmitTransactionXDR(transactionXdr string) (tx hProtocol.Transaction, @@ -469,16 +507,8 @@ func (c *Client) SubmitFeeBumpTransaction(transaction *txnbuild.FeeBumpTransacti func (c *Client) SubmitFeeBumpTransactionWithOptions(transaction *txnbuild.FeeBumpTransaction, opts SubmitTxOpts) (tx hProtocol.Transaction, err error) { // only check if memo is required if skip is false and the inner transaction // doesn't have a memo. - if inner := transaction.InnerTransaction(); !opts.SkipMemoRequiredCheck && inner.Memo() == nil { - err = c.checkMemoRequired(inner) - if err != nil { - return - } - } - - txeBase64, err := transaction.Base64() + txeBase64, err := c.validateFeeBumpTx(transaction, opts) if err != nil { - err = errors.Wrap(err, "Unable to convert transaction object to base64 string") return } @@ -505,20 +535,68 @@ func (c *Client) SubmitTransaction(transaction *txnbuild.Transaction) (tx hProto func (c *Client) SubmitTransactionWithOptions(transaction *txnbuild.Transaction, opts SubmitTxOpts) (tx hProtocol.Transaction, err error) { // only check if memo is required if skip is false and the transaction // doesn't have a memo. - if !opts.SkipMemoRequiredCheck && transaction.Memo() == nil { - err = c.checkMemoRequired(transaction) - if err != nil { - return - } + txeBase64, err := c.validateTx(transaction, opts) + if err != nil { + return } - txeBase64, err := transaction.Base64() + return c.SubmitTransactionXDR(txeBase64) +} + +// AsyncSubmitTransactionXDR submits a base64 XDR transaction using the transactions_async endpoint. err can be either error object or horizon.Error object. +func (c *Client) AsyncSubmitTransactionXDR(transactionXdr string) (txResp hProtocol.AsyncTransactionSubmissionResponse, + err error) { + request := submitRequest{endpoint: "transactions_async", transactionXdr: transactionXdr} + err = c.sendRequest(request, &txResp) + return +} + +// AsyncSubmitFeeBumpTransaction submits an async fee bump transaction to the network. err can be either an +// error object or a horizon.Error object. +// +// This function will always check if the destination account requires a memo in the transaction as +// defined in SEP0029: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0029.md +// +// If you want to skip this check, use SubmitTransactionWithOptions. +func (c *Client) AsyncSubmitFeeBumpTransaction(transaction *txnbuild.FeeBumpTransaction) (txResp hProtocol.AsyncTransactionSubmissionResponse, err error) { + return c.AsyncSubmitFeeBumpTransactionWithOptions(transaction, SubmitTxOpts{}) +} + +// AsyncSubmitFeeBumpTransactionWithOptions submits an async fee bump transaction to the network, allowing +// you to pass SubmitTxOpts. err can be either an error object or a horizon.Error object. +func (c *Client) AsyncSubmitFeeBumpTransactionWithOptions(transaction *txnbuild.FeeBumpTransaction, opts SubmitTxOpts) (txResp hProtocol.AsyncTransactionSubmissionResponse, err error) { + // only check if memo is required if skip is false and the inner transaction + // doesn't have a memo. + txeBase64, err := c.validateFeeBumpTx(transaction, opts) if err != nil { - err = errors.Wrap(err, "Unable to convert transaction object to base64 string") return } - return c.SubmitTransactionXDR(txeBase64) + return c.AsyncSubmitTransactionXDR(txeBase64) +} + +// AsyncSubmitTransaction submits an async transaction to the network. err can be either an +// error object or a horizon.Error object. +// +// This function will always check if the destination account requires a memo in the transaction as +// defined in SEP0029: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0029.md +// +// If you want to skip this check, use SubmitTransactionWithOptions. +func (c *Client) AsyncSubmitTransaction(transaction *txnbuild.Transaction) (txResp hProtocol.AsyncTransactionSubmissionResponse, err error) { + return c.AsyncSubmitTransactionWithOptions(transaction, SubmitTxOpts{}) +} + +// AsyncSubmitTransactionWithOptions submits an async transaction to the network, allowing +// you to pass SubmitTxOpts. err can be either an error object or a horizon.Error object. +func (c *Client) AsyncSubmitTransactionWithOptions(transaction *txnbuild.Transaction, opts SubmitTxOpts) (txResp hProtocol.AsyncTransactionSubmissionResponse, err error) { + // only check if memo is required if skip is false and the transaction + // doesn't have a memo. + txeBase64, err := c.validateTx(transaction, opts) + if err != nil { + return + } + + return c.AsyncSubmitTransactionXDR(txeBase64) } // Transactions returns stellar transactions (https://developers.stellar.org/api/resources/transactions/list/) diff --git a/clients/horizonclient/internal.go b/clients/horizonclient/internal.go index 123788caa9..9dc6052263 100644 --- a/clients/horizonclient/internal.go +++ b/clients/horizonclient/internal.go @@ -27,7 +27,10 @@ func decodeResponse(resp *http.Response, object interface{}, horizonUrl string, } setCurrentServerTime(u.Hostname(), resp.Header["Date"], clock) - if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { + // While this part of code assumes that any error < 200 or error >= 300 is a Horizon problem, it is not + // true for the response from /transactions_async endpoint which does give these codes for certain responses + // from core. + if !(resp.StatusCode >= 200 && resp.StatusCode < 300) && (resp.Request == nil || resp.Request.URL == nil || resp.Request.URL.Path != "/transactions_async") { horizonError := &Error{ Response: resp, } diff --git a/clients/horizonclient/main.go b/clients/horizonclient/main.go index c7fa40f03c..a7bf923ac9 100644 --- a/clients/horizonclient/main.go +++ b/clients/horizonclient/main.go @@ -194,6 +194,11 @@ type ClientInterface interface { SubmitTransactionWithOptions(transaction *txnbuild.Transaction, opts SubmitTxOpts) (hProtocol.Transaction, error) SubmitFeeBumpTransaction(transaction *txnbuild.FeeBumpTransaction) (hProtocol.Transaction, error) SubmitTransaction(transaction *txnbuild.Transaction) (hProtocol.Transaction, error) + AsyncSubmitTransactionXDR(transactionXdr string) (hProtocol.AsyncTransactionSubmissionResponse, error) + AsyncSubmitFeeBumpTransactionWithOptions(transaction *txnbuild.FeeBumpTransaction, opts SubmitTxOpts) (hProtocol.AsyncTransactionSubmissionResponse, error) + AsyncSubmitTransactionWithOptions(transaction *txnbuild.Transaction, opts SubmitTxOpts) (hProtocol.AsyncTransactionSubmissionResponse, error) + AsyncSubmitFeeBumpTransaction(transaction *txnbuild.FeeBumpTransaction) (hProtocol.AsyncTransactionSubmissionResponse, error) + AsyncSubmitTransaction(transaction *txnbuild.Transaction) (hProtocol.AsyncTransactionSubmissionResponse, error) Transactions(request TransactionRequest) (hProtocol.TransactionsPage, error) TransactionDetail(txHash string) (hProtocol.Transaction, error) OrderBook(request OrderBookRequest) (hProtocol.OrderBookSummary, error) diff --git a/clients/horizonclient/mocks.go b/clients/horizonclient/mocks.go index fbf6fe5b66..92c766dd54 100644 --- a/clients/horizonclient/mocks.go +++ b/clients/horizonclient/mocks.go @@ -121,6 +121,36 @@ func (m *MockClient) SubmitTransactionWithOptions(transaction *txnbuild.Transact return a.Get(0).(hProtocol.Transaction), a.Error(1) } +// AsyncSubmitTransactionXDR is a mocking method +func (m *MockClient) AsyncSubmitTransactionXDR(transactionXdr string) (hProtocol.AsyncTransactionSubmissionResponse, error) { + a := m.Called(transactionXdr) + return a.Get(0).(hProtocol.AsyncTransactionSubmissionResponse), a.Error(1) +} + +// AsyncSubmitFeeBumpTransaction is a mocking method +func (m *MockClient) AsyncSubmitFeeBumpTransaction(transaction *txnbuild.FeeBumpTransaction) (hProtocol.AsyncTransactionSubmissionResponse, error) { + a := m.Called(transaction) + return a.Get(0).(hProtocol.AsyncTransactionSubmissionResponse), a.Error(1) +} + +// AsyncSubmitTransaction is a mocking method +func (m *MockClient) AsyncSubmitTransaction(transaction *txnbuild.Transaction) (hProtocol.AsyncTransactionSubmissionResponse, error) { + a := m.Called(transaction) + return a.Get(0).(hProtocol.AsyncTransactionSubmissionResponse), a.Error(1) +} + +// AsyncSubmitFeeBumpTransactionWithOptions is a mocking method +func (m *MockClient) AsyncSubmitFeeBumpTransactionWithOptions(transaction *txnbuild.FeeBumpTransaction, opts SubmitTxOpts) (hProtocol.AsyncTransactionSubmissionResponse, error) { + a := m.Called(transaction, opts) + return a.Get(0).(hProtocol.AsyncTransactionSubmissionResponse), a.Error(1) +} + +// AsyncSubmitTransactionWithOptions is a mocking method +func (m *MockClient) AsyncSubmitTransactionWithOptions(transaction *txnbuild.Transaction, opts SubmitTxOpts) (hProtocol.AsyncTransactionSubmissionResponse, error) { + a := m.Called(transaction, opts) + return a.Get(0).(hProtocol.AsyncTransactionSubmissionResponse), a.Error(1) +} + // Transactions is a mocking method func (m *MockClient) Transactions(request TransactionRequest) (hProtocol.TransactionsPage, error) { a := m.Called(request) diff --git a/clients/stellarcore/metrics_client.go b/clients/stellarcore/metrics_client.go new file mode 100644 index 0000000000..2353905af7 --- /dev/null +++ b/clients/stellarcore/metrics_client.go @@ -0,0 +1,70 @@ +package stellarcore + +import ( + "context" + "time" + + "github.com/prometheus/client_golang/prometheus" + + proto "github.com/stellar/go/protocols/stellarcore" + "github.com/stellar/go/xdr" +) + +var envelopeTypeToLabel = map[xdr.EnvelopeType]string{ + xdr.EnvelopeTypeEnvelopeTypeTxV0: "v0", + xdr.EnvelopeTypeEnvelopeTypeTx: "v1", + xdr.EnvelopeTypeEnvelopeTypeTxFeeBump: "fee_bump", +} + +type ClientWithMetrics struct { + coreClient Client + + // submissionDuration exposes timing metrics about the rate and latency of + // submissions to stellar-core + submissionDuration *prometheus.SummaryVec +} + +func (c ClientWithMetrics) SubmitTx(ctx context.Context, rawTx string) (*proto.TXResponse, error) { + var envelope xdr.TransactionEnvelope + err := xdr.SafeUnmarshalBase64(rawTx, &envelope) + if err != nil { + return &proto.TXResponse{}, err + } + + startTime := time.Now() + response, err := c.coreClient.SubmitTransaction(ctx, rawTx) + duration := time.Since(startTime).Seconds() + + label := prometheus.Labels{} + if err != nil { + label["status"] = "request_error" + } else if response.IsException() { + label["status"] = "exception" + } else { + label["status"] = response.Status + } + + label["envelope_type"] = envelopeTypeToLabel[envelope.Type] + c.submissionDuration.With(label).Observe(duration) + + return response, err +} + +func NewClientWithMetrics(client Client, registry *prometheus.Registry, prometheusSubsystem string) ClientWithMetrics { + submissionDuration := prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Namespace: "horizon", + Subsystem: prometheusSubsystem, + Name: "submission_duration_seconds", + Help: "submission durations to Stellar-Core, sliding window = 10m", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, []string{"status", "envelope_type"}) + + registry.MustRegister( + submissionDuration, + ) + + return ClientWithMetrics{ + coreClient: client, + submissionDuration: submissionDuration, + } +} diff --git a/protocols/horizon/main.go b/protocols/horizon/main.go index bdec98ba0b..dd9cc7814f 100644 --- a/protocols/horizon/main.go +++ b/protocols/horizon/main.go @@ -8,10 +8,12 @@ import ( "fmt" "math" "math/big" + "net/http" "strconv" "time" "github.com/stellar/go/protocols/horizon/base" + proto "github.com/stellar/go/protocols/stellarcore" "github.com/stellar/go/strkey" "github.com/stellar/go/support/errors" "github.com/stellar/go/support/render/hal" @@ -29,6 +31,13 @@ var KeyTypeNames = map[strkey.VersionByte]string{ strkey.VersionByteSignedPayload: "ed25519_signed_payload", } +var coreStatusToHTTPStatus = map[string]int{ + proto.TXStatusPending: http.StatusCreated, + proto.TXStatusDuplicate: http.StatusConflict, + proto.TXStatusTryAgainLater: http.StatusServiceUnavailable, + proto.TXStatusError: http.StatusBadRequest, +} + // Account is the summary of an account type Account struct { Links struct { @@ -567,6 +576,26 @@ type InnerTransaction struct { MaxFee int64 `json:"max_fee,string"` } +// AsyncTransactionSubmissionResponse represents the response returned by Horizon +// when using the transaction-async endpoint. +type AsyncTransactionSubmissionResponse struct { + // ErrorResultXDR is present only if Status is equal to proto.TXStatusError. + // ErrorResultXDR is a TransactionResult xdr string which contains details on why + // the transaction could not be accepted by stellar-core. + ErrorResultXDR string `json:"errorResultXdr,omitempty"` + // TxStatus represents the status of the transaction submission returned by stellar-core. + // It can be one of: proto.TXStatusPending, proto.TXStatusDuplicate, + // proto.TXStatusTryAgainLater, or proto.TXStatusError. + TxStatus string `json:"tx_status"` + // Hash is a hash of the transaction which can be used to look up whether + // the transaction was included in the ledger. + Hash string `json:"hash"` +} + +func (response AsyncTransactionSubmissionResponse) GetStatus() int { + return coreStatusToHTTPStatus[response.TxStatus] +} + // MarshalJSON implements a custom marshaler for Transaction. // The memo field should be omitted if and only if the // memo_type is "none". diff --git a/services/horizon/internal/actions/submit_transaction.go b/services/horizon/internal/actions/submit_transaction.go index 314caf32a5..00049a9172 100644 --- a/services/horizon/internal/actions/submit_transaction.go +++ b/services/horizon/internal/actions/submit_transaction.go @@ -7,12 +7,13 @@ import ( "net/http" "github.com/stellar/go/network" + "github.com/stellar/go/support/errors" + "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" - "github.com/stellar/go/support/errors" "github.com/stellar/go/support/render/hal" "github.com/stellar/go/support/render/problem" "github.com/stellar/go/xdr" @@ -37,7 +38,7 @@ type envelopeInfo struct { parsed xdr.TransactionEnvelope } -func (handler SubmitTransactionHandler) extractEnvelopeInfo(raw string, passphrase string) (envelopeInfo, error) { +func extractEnvelopeInfo(raw string, passphrase string) (envelopeInfo, error) { result := envelopeInfo{raw: raw} err := xdr.SafeUnmarshalBase64(raw, &result.parsed) if err != nil { @@ -60,7 +61,7 @@ func (handler SubmitTransactionHandler) extractEnvelopeInfo(raw string, passphra return result, nil } -func (handler SubmitTransactionHandler) validateBodyType(r *http.Request) error { +func validateBodyType(r *http.Request) error { c := r.Header.Get("Content-Type") if c == "" { return nil @@ -139,7 +140,7 @@ func (handler SubmitTransactionHandler) response(r *http.Request, info envelopeI } func (handler SubmitTransactionHandler) GetResource(w HeaderWriter, r *http.Request) (interface{}, error) { - if err := handler.validateBodyType(r); err != nil { + if err := validateBodyType(r); err != nil { return nil, err } @@ -147,7 +148,7 @@ func (handler SubmitTransactionHandler) GetResource(w HeaderWriter, r *http.Requ return nil, &problem.P{ Type: "transaction_submission_disabled", Title: "Transaction Submission Disabled", - Status: http.StatusMethodNotAllowed, + Status: http.StatusForbidden, Detail: "Transaction submission has been disabled for Horizon. " + "To enable it again, remove env variable DISABLE_TX_SUB.", Extras: map[string]interface{}{}, @@ -159,7 +160,7 @@ func (handler SubmitTransactionHandler) GetResource(w HeaderWriter, r *http.Requ return nil, err } - info, err := handler.extractEnvelopeInfo(raw, handler.NetworkPassphrase) + info, err := extractEnvelopeInfo(raw, handler.NetworkPassphrase) if err != nil { return nil, &problem.P{ Type: "transaction_malformed", diff --git a/services/horizon/internal/actions/submit_transaction_async.go b/services/horizon/internal/actions/submit_transaction_async.go new file mode 100644 index 0000000000..0cce31b5f6 --- /dev/null +++ b/services/horizon/internal/actions/submit_transaction_async.go @@ -0,0 +1,138 @@ +package actions + +import ( + "context" + "net/http" + + "github.com/stellar/go/protocols/horizon" + proto "github.com/stellar/go/protocols/stellarcore" + hProblem "github.com/stellar/go/services/horizon/internal/render/problem" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/log" + "github.com/stellar/go/support/render/problem" +) + +type coreClient interface { + SubmitTx(ctx context.Context, rawTx string) (resp *proto.TXResponse, err error) +} + +type AsyncSubmitTransactionHandler struct { + NetworkPassphrase string + DisableTxSub bool + ClientWithMetrics coreClient + CoreStateGetter +} + +func (handler AsyncSubmitTransactionHandler) GetResource(_ HeaderWriter, r *http.Request) (interface{}, error) { + // TODO: Move the problem responses to a separate file as constants or a function. + logger := log.Ctx(r.Context()) + + if err := validateBodyType(r); err != nil { + return nil, err + } + + raw, err := getString(r, "tx") + if err != nil { + return nil, err + } + + if handler.DisableTxSub { + return nil, &problem.P{ + Type: "transaction_submission_disabled", + Title: "Transaction Submission Disabled", + Status: http.StatusForbidden, + Detail: "Transaction submission has been disabled for Horizon. " + + "To enable it again, remove env variable DISABLE_TX_SUB.", + Extras: map[string]interface{}{ + "envelope_xdr": raw, + }, + } + } + + info, err := extractEnvelopeInfo(raw, handler.NetworkPassphrase) + if err != nil { + return nil, &problem.P{ + Type: "transaction_malformed", + Title: "Transaction Malformed", + Status: http.StatusBadRequest, + Detail: "Horizon could not decode the transaction envelope in this " + + "request. A transaction should be an XDR TransactionEnvelope struct " + + "encoded using base64. The envelope read from this request is " + + "echoed in the `extras.envelope_xdr` field of this response for your " + + "convenience.", + Extras: map[string]interface{}{ + "envelope_xdr": raw, + "error": err, + }, + } + } + + coreState := handler.GetCoreState() + if !coreState.Synced { + return nil, hProblem.StaleHistory + } + + resp, err := handler.ClientWithMetrics.SubmitTx(r.Context(), raw) + if err != nil { + return nil, &problem.P{ + Type: "transaction_submission_failed", + Title: "Transaction Submission Failed", + Status: http.StatusInternalServerError, + Detail: "Could not submit transaction to stellar-core. " + + "The `extras.error` 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-submission-async/transaction_submission_failed", + Extras: map[string]interface{}{ + "envelope_xdr": raw, + "error": err, + }, + } + } + + if resp.IsException() { + logger.WithField("envelope_xdr", raw).WithError(errors.Errorf(resp.Exception)).Error("Transaction submission exception from stellar-core") + return nil, &problem.P{ + Type: "transaction_submission_exception", + Title: "Transaction Submission Exception", + Status: http.StatusInternalServerError, + Detail: "Received exception from stellar-core." + + "The `extras.error` 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-submission-async/transaction_submission_exception", + Extras: map[string]interface{}{ + "envelope_xdr": raw, + "error": resp.Exception, + }, + } + } + + switch resp.Status { + case proto.TXStatusError, proto.TXStatusPending, proto.TXStatusDuplicate, proto.TXStatusTryAgainLater: + response := horizon.AsyncTransactionSubmissionResponse{ + TxStatus: resp.Status, + Hash: info.hash, + } + + if resp.Status == proto.TXStatusError { + response.ErrorResultXDR = resp.Error + } + + return response, nil + default: + logger.WithField("envelope_xdr", raw).WithError(errors.Errorf(resp.Error)).Error("Received invalid submission status from stellar-core") + return nil, &problem.P{ + Type: "transaction_submission_invalid_status", + Title: "Transaction Submission Invalid Status", + Status: http.StatusInternalServerError, + Detail: "Received invalid status from stellar-core." + + "The `extras.error` 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-submission-async/transaction_submission_invalid_status", + Extras: map[string]interface{}{ + "envelope_xdr": raw, + "error": resp.Error, + }, + } + } + +} diff --git a/services/horizon/internal/actions/submit_transaction_async_test.go b/services/horizon/internal/actions/submit_transaction_async_test.go new file mode 100644 index 0000000000..258012bc3c --- /dev/null +++ b/services/horizon/internal/actions/submit_transaction_async_test.go @@ -0,0 +1,218 @@ +package actions + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/mock" + + "github.com/stellar/go/protocols/horizon" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/network" + proto "github.com/stellar/go/protocols/stellarcore" + "github.com/stellar/go/services/horizon/internal/corestate" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/render/problem" +) + +const ( + TxXDR = "AAAAAAGUcmKO5465JxTSLQOQljwk2SfqAJmZSG6JH6wtqpwhAAABLAAAAAAAAAABAAAAAAAAAAEAAAALaGVsbG8gd29ybGQAAAAAAwAAAAAAAAAAAAAAABbxCy3mLg3hiTqX4VUEEp60pFOrJNxYM1JtxXTwXhY2AAAAAAvrwgAAAAAAAAAAAQAAAAAW8Qst5i4N4Yk6l+FVBBKetKRTqyTcWDNSbcV08F4WNgAAAAAN4Lazj4x61AAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABLaqcIQAAAEBKwqWy3TaOxoGnfm9eUjfTRBvPf34dvDA0Nf+B8z4zBob90UXtuCqmQqwMCyH+okOI3c05br3khkH0yP4kCwcE" + TxHash = "3389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c8889" +) + +type MockClientWithMetrics struct { + mock.Mock +} + +// SubmitTx mocks the SubmitTransaction method +func (m *MockClientWithMetrics) SubmitTx(ctx context.Context, rawTx string) (*proto.TXResponse, error) { + args := m.Called(ctx, rawTx) + return args.Get(0).(*proto.TXResponse), args.Error(1) +} + +func createRequest() *http.Request { + form := url.Values{} + form.Set("tx", TxXDR) + + request, _ := http.NewRequest( + "POST", + "http://localhost:8000/transactions_async", + strings.NewReader(form.Encode()), + ) + request.Header.Add("Content-Type", "application/x-www-form-urlencoded") + return request +} + +func TestAsyncSubmitTransactionHandler_DisabledTxSub(t *testing.T) { + handler := AsyncSubmitTransactionHandler{ + DisableTxSub: true, + } + + request := createRequest() + w := httptest.NewRecorder() + + _, err := handler.GetResource(w, request) + assert.NotNil(t, err) + assert.IsType(t, &problem.P{}, err) + p := err.(*problem.P) + assert.Equal(t, "transaction_submission_disabled", p.Type) + assert.Equal(t, http.StatusForbidden, p.Status) +} + +func TestAsyncSubmitTransactionHandler_MalformedTransaction(t *testing.T) { + handler := AsyncSubmitTransactionHandler{} + + request := createRequest() + w := httptest.NewRecorder() + + _, err := handler.GetResource(w, request) + assert.NotNil(t, err) + assert.IsType(t, &problem.P{}, err) + p := err.(*problem.P) + assert.Equal(t, "transaction_malformed", p.Type) + assert.Equal(t, http.StatusBadRequest, p.Status) +} + +func TestAsyncSubmitTransactionHandler_CoreNotSynced(t *testing.T) { + coreStateGetter := new(coreStateGetterMock) + coreStateGetter.On("GetCoreState").Return(corestate.State{Synced: false}) + handler := AsyncSubmitTransactionHandler{ + CoreStateGetter: coreStateGetter, + NetworkPassphrase: network.PublicNetworkPassphrase, + } + + request := createRequest() + w := httptest.NewRecorder() + + _, err := handler.GetResource(w, request) + assert.NotNil(t, err) + assert.IsType(t, problem.P{}, err) + p := err.(problem.P) + assert.Equal(t, "stale_history", p.Type) + assert.Equal(t, http.StatusServiceUnavailable, p.Status) +} + +func TestAsyncSubmitTransactionHandler_TransactionSubmissionFailed(t *testing.T) { + coreStateGetter := new(coreStateGetterMock) + coreStateGetter.On("GetCoreState").Return(corestate.State{Synced: true}) + + MockClientWithMetrics := &MockClientWithMetrics{} + MockClientWithMetrics.On("SubmitTx", context.Background(), TxXDR).Return(&proto.TXResponse{}, errors.Errorf("submission error")) + + handler := AsyncSubmitTransactionHandler{ + CoreStateGetter: coreStateGetter, + NetworkPassphrase: network.PublicNetworkPassphrase, + ClientWithMetrics: MockClientWithMetrics, + } + + request := createRequest() + w := httptest.NewRecorder() + + _, err := handler.GetResource(w, request) + assert.NotNil(t, err) + assert.IsType(t, &problem.P{}, err) + p := err.(*problem.P) + assert.Equal(t, "transaction_submission_failed", p.Type) + assert.Equal(t, http.StatusInternalServerError, p.Status) +} + +func TestAsyncSubmitTransactionHandler_TransactionSubmissionException(t *testing.T) { + coreStateGetter := new(coreStateGetterMock) + coreStateGetter.On("GetCoreState").Return(corestate.State{Synced: true}) + + MockClientWithMetrics := &MockClientWithMetrics{} + MockClientWithMetrics.On("SubmitTx", context.Background(), TxXDR).Return(&proto.TXResponse{ + Exception: "some-exception", + }, nil) + + handler := AsyncSubmitTransactionHandler{ + CoreStateGetter: coreStateGetter, + NetworkPassphrase: network.PublicNetworkPassphrase, + ClientWithMetrics: MockClientWithMetrics, + } + + request := createRequest() + w := httptest.NewRecorder() + + _, err := handler.GetResource(w, request) + assert.NotNil(t, err) + assert.IsType(t, &problem.P{}, err) + p := err.(*problem.P) + assert.Equal(t, "transaction_submission_exception", p.Type) + assert.Equal(t, http.StatusInternalServerError, p.Status) +} + +func TestAsyncSubmitTransactionHandler_TransactionStatusResponse(t *testing.T) { + coreStateGetter := new(coreStateGetterMock) + coreStateGetter.On("GetCoreState").Return(corestate.State{Synced: true}) + + successCases := []struct { + mockCoreResponse *proto.TXResponse + expectedResponse horizon.AsyncTransactionSubmissionResponse + }{ + { + mockCoreResponse: &proto.TXResponse{ + Exception: "", + Error: "test-error", + Status: proto.TXStatusError, + DiagnosticEvents: "test-diagnostic-events", + }, + expectedResponse: horizon.AsyncTransactionSubmissionResponse{ + ErrorResultXDR: "test-error", + TxStatus: proto.TXStatusError, + Hash: TxHash, + }, + }, + { + mockCoreResponse: &proto.TXResponse{ + Status: proto.TXStatusPending, + }, + expectedResponse: horizon.AsyncTransactionSubmissionResponse{ + TxStatus: proto.TXStatusPending, + Hash: TxHash, + }, + }, + { + mockCoreResponse: &proto.TXResponse{ + Status: proto.TXStatusDuplicate, + }, + expectedResponse: horizon.AsyncTransactionSubmissionResponse{ + TxStatus: proto.TXStatusDuplicate, + Hash: TxHash, + }, + }, + { + mockCoreResponse: &proto.TXResponse{ + Status: proto.TXStatusTryAgainLater, + }, + expectedResponse: horizon.AsyncTransactionSubmissionResponse{ + TxStatus: proto.TXStatusTryAgainLater, + Hash: TxHash, + }, + }, + } + + for _, testCase := range successCases { + MockClientWithMetrics := &MockClientWithMetrics{} + MockClientWithMetrics.On("SubmitTx", context.Background(), TxXDR).Return(testCase.mockCoreResponse, nil) + + handler := AsyncSubmitTransactionHandler{ + NetworkPassphrase: network.PublicNetworkPassphrase, + ClientWithMetrics: MockClientWithMetrics, + CoreStateGetter: coreStateGetter, + } + + request := createRequest() + w := httptest.NewRecorder() + + resp, err := handler.GetResource(w, request) + assert.NoError(t, err) + assert.Equal(t, resp, testCase.expectedResponse) + } +} diff --git a/services/horizon/internal/actions/submit_transaction_test.go b/services/horizon/internal/actions/submit_transaction_test.go index a15ce3bd94..eb1987bdea 100644 --- a/services/horizon/internal/actions/submit_transaction_test.go +++ b/services/horizon/internal/actions/submit_transaction_test.go @@ -178,7 +178,7 @@ func TestDisableTxSubFlagSubmission(t *testing.T) { var p = &problem.P{ Type: "transaction_submission_disabled", Title: "Transaction Submission Disabled", - Status: http.StatusMethodNotAllowed, + Status: http.StatusForbidden, Detail: "Transaction submission has been disabled for Horizon. " + "To enable it again, remove env variable DISABLE_TX_SUB.", Extras: map[string]interface{}{}, diff --git a/services/horizon/internal/app.go b/services/horizon/internal/app.go index 36fc2031e8..843232beba 100644 --- a/services/horizon/internal/app.go +++ b/services/horizon/internal/app.go @@ -554,6 +554,7 @@ func (a *App) init() error { HorizonVersion: a.horizonVersion, FriendbotURL: a.config.FriendbotURL, DisableTxSub: a.config.DisableTxSub, + StellarCoreURL: a.config.StellarCoreURL, HealthCheck: healthCheck{ session: a.historyQ.SessionInterface, ctx: a.ctx, diff --git a/services/horizon/internal/httpx/handler.go b/services/horizon/internal/httpx/handler.go index e17ecb987d..ade5742566 100644 --- a/services/horizon/internal/httpx/handler.go +++ b/services/horizon/internal/httpx/handler.go @@ -25,6 +25,10 @@ type objectAction interface { ) (interface{}, error) } +type HttpResponse interface { + GetStatus() int +} + type ObjectActionHandler struct { Action objectAction } @@ -41,8 +45,13 @@ func (handler ObjectActionHandler) ServeHTTP( return } - httpjson.Render( + statusCode := http.StatusOK + if httpResponse, ok := response.(HttpResponse); ok { + statusCode = httpResponse.GetStatus() + } + httpjson.RenderStatus( w, + statusCode, response, httpjson.HALJSON, ) diff --git a/services/horizon/internal/httpx/router.go b/services/horizon/internal/httpx/router.go index cd3b6821b0..ba5af85b51 100644 --- a/services/horizon/internal/httpx/router.go +++ b/services/horizon/internal/httpx/router.go @@ -8,6 +8,8 @@ import ( "net/url" "time" + "github.com/stellar/go/clients/stellarcore" + "github.com/go-chi/chi" chimiddleware "github.com/go-chi/chi/middleware" "github.com/prometheus/client_golang/prometheus" @@ -52,6 +54,7 @@ type RouterConfig struct { HealthCheck http.Handler DisableTxSub bool SkipTxMeta bool + StellarCoreURL string } type Router struct { @@ -344,6 +347,17 @@ func (r *Router) addRoutes(config *RouterConfig, rateLimiter *throttled.HTTPRate SkipTxMeta: config.SkipTxMeta, }}) + // Async Transaction submission API + r.Method(http.MethodPost, "/transactions_async", ObjectActionHandler{actions.AsyncSubmitTransactionHandler{ + NetworkPassphrase: config.NetworkPassphrase, + DisableTxSub: config.DisableTxSub, + CoreStateGetter: config.CoreGetter, + ClientWithMetrics: stellarcore.NewClientWithMetrics(stellarcore.Client{ + HTTP: http.DefaultClient, + URL: config.StellarCoreURL, + }, config.PrometheusRegistry, "async_txsub"), + }}) + // Network state related endpoints r.Method(http.MethodGet, "/fee_stats", ObjectActionHandler{actions.FeeStatsHandler{}}) @@ -371,6 +385,15 @@ func (r *Router) addRoutes(config *RouterConfig, rateLimiter *throttled.HTTPRate w.Header().Set("Content-Type", "application/openapi+yaml") w.Write(p) }) + r.Internal.Get("/transactions_async", func(w http.ResponseWriter, r *http.Request) { + p, err := staticFiles.ReadFile("static/txsub_async_oapi.yaml") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/openapi+yaml") + w.Write(p) + }) r.Internal.Get("/metrics", promhttp.HandlerFor(config.PrometheusRegistry, promhttp.HandlerOpts{}).ServeHTTP) r.Internal.Get("/debug/pprof/heap", pprof.Index) r.Internal.Get("/debug/pprof/profile", pprof.Profile) diff --git a/services/horizon/internal/httpx/static/txsub_async_oapi.yaml b/services/horizon/internal/httpx/static/txsub_async_oapi.yaml new file mode 100644 index 0000000000..f889cf4ec8 --- /dev/null +++ b/services/horizon/internal/httpx/static/txsub_async_oapi.yaml @@ -0,0 +1,168 @@ +openapi: 3.0.0 +info: + title: Stellar Horizon Async Transaction Submission + version: "1.0" +paths: + /transactions_async: + post: + summary: Asynchronously submit a transaction to the Stellar network. + tags: + - Transactions + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + tx: + type: string + description: A base64 transaction XDR string. + required: + - tx + responses: + '201': + description: Transaction has been received by core and is in pending status. + content: + application/json: + schema: + $ref: '#/components/schemas/AsyncTransactionSubmissionResponse' + example: + tx_status: "PENDING" + hash: "6cbb7f714bd08cea7c30cab7818a35c510cbbfc0a6aa06172a1e94146ecf0165" + + '400': + description: Transaction is malformed; transaction submission exception; transaction submission failed; invalid submission status from core; ERROR status from core. + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/AsyncTransactionSubmissionResponse' + - $ref: '#/components/schemas/Problem' + examples: + TransactionMalformedExample: + summary: Transaction Malformed + value: + type: "transaction_malformed" + title: "Transaction Malformed" + status: 400 + detail: "Horizon could not decode the transaction envelope in this request. A transaction should be an XDR TransactionEnvelope struct encoded using base64. The envelope read from this request is echoed in the `extras.envelope_xdr` field of this response for your convenience." + extras: + envelope_xdr: "" + ErrorStatusExample: + summary: ERROR Status from core + value: + errorResultXdr: "AAAAAAAAAGT////7AAAAAA==" + tx_status: "ERROR" + hash: "6cbb7f714bd08cea7c30cab7818a35c510cbbfc0a6aa06172a1e94146ecf0165" + '405': + description: Transaction submission has been disabled for Horizon. + content: + application/json: + schema: + $ref: '#/components/schemas/Problem' + example: + TransactionSubmissionDisabledExample: + summary: Transaction Submission Disabled + value: + type: "transaction_submission_disabled" + title: "Transaction Submission Disabled" + status: 403 + detail: "Transaction submission has been disabled for Horizon. To enable it again, remove env variable DISABLE_TX_SUB." + extras: + envelope_xdr: "" + '409': + description: Transaction is a duplicate of a previously submitted transaction. + content: + application/json: + schema: + $ref: '#/components/schemas/AsyncTransactionSubmissionResponse' + example: + errorResultXdr: "" + tx_status: "DUPLICATE" + hash: "6cbb7f714bd08cea7c30cab7818a35c510cbbfc0a6aa06172a1e94146ecf0165" + '500': + description: Transaction is a duplicate of a previously submitted transaction. + content: + application/json: + schema: + $ref: '#/components/schemas/Problem' + examples: + TransactionFailedExample: + summary: Transaction Submission Failed + value: + type: "transaction_submission_failed" + title: "Transaction Submission Failed" + status: 500 + detail: "Could not submit transaction to stellar-core. The `extras.error` 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-submission-async/transaction_submission_failed" + extras: + envelope_xdr: "" + error: "Error details here" + TransactionExceptionExample: + summary: Transaction Submission Exception + value: + type: "transaction_submission_exception" + title: "Transaction Submission Exception" + status: 500 + detail: "Received exception from stellar-core. The `extras.error` 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-submission-async/transaction_submission_exception" + extras: + envelope_xdr: "" + error: "Exception details here" + '503': + description: History DB is stale; core is unavailable for transaction submission. + content: + application/json: + schema: + $ref: '#/components/schemas/AsyncTransactionSubmissionResponse' + examples: + HistoryDBStaleExample: + summary: Historical DB Is Too Stale + value: + type: "stale_history" + title: "Historical DB Is Too Stale" + status: 503 + detail: "This horizon instance is configured to reject client requests when it can determine that the history database is lagging too far behind the connected instance of Stellar-Core or read replica. It's also possible that Stellar-Core is out of sync. Please try again later." + extras: + envelope_xdr: "" + TryAgainLaterExample: + summary: TRY_AGAIN_LATER Status from core + value: + tx_status: "TRY_AGAIN_LATER" + hash: "6cbb7f714bd08cea7c30cab7818a35c510cbbfc0a6aa06172a1e94146ecf0165" + + +components: + schemas: + AsyncTransactionSubmissionResponse: + type: object + properties: + errorResultXdr: + type: string + nullable: true + description: TransactionResult XDR string which is present only if the submission status from core is an ERROR. + tx_status: + type: string + enum: ["ERROR", "PENDING", "DUPLICATE", "TRY_AGAIN_LATER"] + description: Status of the transaction submission. + hash: + type: string + description: Hash of the transaction. + Problem: + type: object + properties: + type: + type: string + description: Identifies the problem type. + title: + type: string + description: A short, human-readable summary of the problem type. + status: + type: integer + description: The HTTP status code for this occurrence of the problem. + detail: + type: string + description: A human-readable explanation specific to this occurrence of the problem. + extras: + type: object + additionalProperties: true + description: Additional details that might help the client understand the error(s) that occurred. diff --git a/services/horizon/internal/init.go b/services/horizon/internal/init.go index 93580fed54..a221c00682 100644 --- a/services/horizon/internal/init.go +++ b/services/horizon/internal/init.go @@ -223,7 +223,7 @@ func initWebMetrics(app *App) { func initSubmissionSystem(app *App) { app.submitter = &txsub.System{ Pending: txsub.NewDefaultSubmissionList(), - Submitter: txsub.NewDefaultSubmitter(http.DefaultClient, app.config.StellarCoreURL), + Submitter: txsub.NewDefaultSubmitter(http.DefaultClient, app.config.StellarCoreURL, app.prometheusRegistry), DB: func(ctx context.Context) txsub.HorizonDB { return &history.Q{SessionInterface: app.HorizonSession()} }, diff --git a/services/horizon/internal/integration/txsub_async_test.go b/services/horizon/internal/integration/txsub_async_test.go new file mode 100644 index 0000000000..2d16e4d4ea --- /dev/null +++ b/services/horizon/internal/integration/txsub_async_test.go @@ -0,0 +1,146 @@ +package integration + +import ( + "io" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/services/horizon/internal/test/integration" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/txnbuild" +) + +func getTransaction(client *horizonclient.Client, hash string) error { + for i := 0; i < 60; i++ { + _, err := client.TransactionDetail(hash) + if err != nil { + time.Sleep(time.Second) + continue + } + + return nil + } + return errors.New("transaction not found") +} + +func TestAsyncTxSub_SuccessfulSubmission(t *testing.T) { + itest := integration.NewTest(t, integration.Config{}) + master := itest.Master() + masterAccount := itest.MasterAccount() + + txParams := txnbuild.TransactionParams{ + BaseFee: txnbuild.MinBaseFee, + SourceAccount: masterAccount, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + &txnbuild.Payment{ + Destination: master.Address(), + Amount: "10", + Asset: txnbuild.NativeAsset{}, + }, + }, + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewInfiniteTimeout(), + LedgerBounds: &txnbuild.LedgerBounds{MinLedger: 0, MaxLedger: 100}, + }, + } + + txResp, err := itest.AsyncSubmitTransaction(master, txParams) + assert.NoError(t, err) + assert.Equal(t, txResp, horizon.AsyncTransactionSubmissionResponse{ + TxStatus: "PENDING", + Hash: "6cbb7f714bd08cea7c30cab7818a35c510cbbfc0a6aa06172a1e94146ecf0165", + }) + + err = getTransaction(itest.Client(), txResp.Hash) + assert.NoError(t, err) +} + +func TestAsyncTxSub_SubmissionError(t *testing.T) { + itest := integration.NewTest(t, integration.Config{}) + master := itest.Master() + masterAccount := itest.MasterAccount() + + txParams := txnbuild.TransactionParams{ + BaseFee: txnbuild.MinBaseFee, + SourceAccount: masterAccount, + IncrementSequenceNum: false, + Operations: []txnbuild.Operation{ + &txnbuild.Payment{ + Destination: master.Address(), + Amount: "10", + Asset: txnbuild.NativeAsset{}, + }, + }, + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewInfiniteTimeout(), + LedgerBounds: &txnbuild.LedgerBounds{MinLedger: 0, MaxLedger: 100}, + }, + } + + txResp, err := itest.AsyncSubmitTransaction(master, txParams) + assert.NoError(t, err) + assert.Equal(t, txResp, horizon.AsyncTransactionSubmissionResponse{ + ErrorResultXDR: "AAAAAAAAAGT////7AAAAAA==", + TxStatus: "ERROR", + Hash: "0684df00f20efd5876f1b8d17bc6d3a68d8b85c06bb41e448815ecaa6307a251", + }) +} + +func TestAsyncTxSub_SubmissionTryAgainLater(t *testing.T) { + itest := integration.NewTest(t, integration.Config{}) + master := itest.Master() + masterAccount := itest.MasterAccount() + + txParams := txnbuild.TransactionParams{ + BaseFee: txnbuild.MinBaseFee, + SourceAccount: masterAccount, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + &txnbuild.Payment{ + Destination: master.Address(), + Amount: "10", + Asset: txnbuild.NativeAsset{}, + }, + }, + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewInfiniteTimeout(), + LedgerBounds: &txnbuild.LedgerBounds{MinLedger: 0, MaxLedger: 100}, + }, + } + + txResp, err := itest.AsyncSubmitTransaction(master, txParams) + assert.NoError(t, err) + assert.Equal(t, txResp, horizon.AsyncTransactionSubmissionResponse{ + ErrorResultXDR: "", + TxStatus: "PENDING", + Hash: "6cbb7f714bd08cea7c30cab7818a35c510cbbfc0a6aa06172a1e94146ecf0165", + }) + + txResp, err = itest.AsyncSubmitTransaction(master, txParams) + assert.NoError(t, err) + assert.Equal(t, txResp, horizon.AsyncTransactionSubmissionResponse{ + ErrorResultXDR: "", + TxStatus: "TRY_AGAIN_LATER", + Hash: "d5eb72a4c1832b89965850fff0bd9bba4b6ca102e7c89099dcaba5e7d7d2e049", + }) +} + +func TestAsyncTxSub_GetOpenAPISpecResponse(t *testing.T) { + itest := integration.NewTest(t, integration.Config{}) + res, err := http.Get(itest.AsyncTxSubOpenAPISpecURL()) + assert.NoError(t, err) + assert.Equal(t, res.StatusCode, 200) + + bytes, err := io.ReadAll(res.Body) + res.Body.Close() + assert.NoError(t, err) + + openAPISpec := string(bytes) + assert.Contains(t, openAPISpec, "openapi: 3.0.0") +} diff --git a/services/horizon/internal/test/integration/integration.go b/services/horizon/internal/test/integration/integration.go index a661c9edf8..d755d00252 100644 --- a/services/horizon/internal/test/integration/integration.go +++ b/services/horizon/internal/test/integration/integration.go @@ -911,6 +911,11 @@ func (i *Test) MetricsURL() string { return fmt.Sprintf("http://localhost:%d/metrics", i.AdminPort()) } +// AsyncTxSubOpenAPISpecURL returns the URL for getting the openAPI spec yaml for async-txsub endpoint. +func (i *Test) AsyncTxSubOpenAPISpecURL() string { + return fmt.Sprintf("http://localhost:%d/transactions_async", i.AdminPort()) +} + // Master returns a keypair of the network masterKey account. func (i *Test) Master() *keypair.Full { if i.masterKey != nil { @@ -1146,6 +1151,16 @@ func (i *Test) SubmitMultiSigTransaction( return i.Client().SubmitTransaction(tx) } +func (i *Test) AsyncSubmitTransaction( + signer *keypair.Full, txParams txnbuild.TransactionParams, +) (proto.AsyncTransactionSubmissionResponse, error) { + tx, err := i.CreateSignedTransaction([]*keypair.Full{signer}, txParams) + if err != nil { + return proto.AsyncTransactionSubmissionResponse{}, err + } + return i.Client().AsyncSubmitTransaction(tx) +} + func (i *Test) MustSubmitMultiSigTransaction( signers []*keypair.Full, txParams txnbuild.TransactionParams, ) proto.Transaction { diff --git a/services/horizon/internal/txsub/submitter.go b/services/horizon/internal/txsub/submitter.go index 27ce85c87a..694cc3b372 100644 --- a/services/horizon/internal/txsub/submitter.go +++ b/services/horizon/internal/txsub/submitter.go @@ -5,6 +5,8 @@ import ( "net/http" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/stellar/go/clients/stellarcore" proto "github.com/stellar/go/protocols/stellarcore" "github.com/stellar/go/support/errors" @@ -14,12 +16,12 @@ import ( // NewDefaultSubmitter returns a new, simple Submitter implementation // that submits directly to the stellar-core at `url` using the http client // `h`. -func NewDefaultSubmitter(h *http.Client, url string) Submitter { +func NewDefaultSubmitter(h *http.Client, url string, registry *prometheus.Registry) Submitter { return &submitter{ - StellarCore: &stellarcore.Client{ + StellarCore: stellarcore.NewClientWithMetrics(stellarcore.Client{ HTTP: h, URL: url, - }, + }, registry, "txsub"), Log: log.DefaultLogger.WithField("service", "txsub.submitter"), } } @@ -28,13 +30,13 @@ func NewDefaultSubmitter(h *http.Client, url string) Submitter { // submits directly to the configured stellar-core instance using the // configured http client. type submitter struct { - StellarCore *stellarcore.Client + StellarCore stellarcore.ClientWithMetrics Log *log.Entry } // Submit sends the provided envelope to stellar-core and parses the response into // a SubmissionResult -func (sub *submitter) Submit(ctx context.Context, env string) (result SubmissionResult) { +func (sub *submitter) Submit(ctx context.Context, rawTx string) (result SubmissionResult) { start := time.Now() defer func() { result.Duration = time.Since(start) @@ -44,7 +46,7 @@ func (sub *submitter) Submit(ctx context.Context, env string) (result Submission }).Info("Submitter result") }() - cresp, err := sub.StellarCore.SubmitTransaction(ctx, env) + cresp, err := sub.StellarCore.SubmitTx(ctx, rawTx) if err != nil { result.Err = errors.Wrap(err, "failed to submit") return diff --git a/services/horizon/internal/txsub/submitter_test.go b/services/horizon/internal/txsub/submitter_test.go index 4406f46fb8..5662930b5d 100644 --- a/services/horizon/internal/txsub/submitter_test.go +++ b/services/horizon/internal/txsub/submitter_test.go @@ -1,13 +1,19 @@ package txsub import ( - "github.com/stretchr/testify/assert" "net/http" "testing" + "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/assert" + "github.com/stellar/go/services/horizon/internal/test" ) +const ( + TxXDR = "AAAAAAGUcmKO5465JxTSLQOQljwk2SfqAJmZSG6JH6wtqpwhAAABLAAAAAAAAAABAAAAAAAAAAEAAAALaGVsbG8gd29ybGQAAAAAAwAAAAAAAAAAAAAAABbxCy3mLg3hiTqX4VUEEp60pFOrJNxYM1JtxXTwXhY2AAAAAAvrwgAAAAAAAAAAAQAAAAAW8Qst5i4N4Yk6l+FVBBKetKRTqyTcWDNSbcV08F4WNgAAAAAN4Lazj4x61AAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABLaqcIQAAAEBKwqWy3TaOxoGnfm9eUjfTRBvPf34dvDA0Nf+B8z4zBob90UXtuCqmQqwMCyH+okOI3c05br3khkH0yP4kCwcE" +) + func TestDefaultSubmitter(t *testing.T) { ctx := test.Context() // submits to the configured stellar-core instance correctly @@ -17,11 +23,11 @@ func TestDefaultSubmitter(t *testing.T) { }`) defer server.Close() - s := NewDefaultSubmitter(http.DefaultClient, server.URL) - sr := s.Submit(ctx, "hello") + s := NewDefaultSubmitter(http.DefaultClient, server.URL, prometheus.NewRegistry()) + sr := s.Submit(ctx, TxXDR) assert.Nil(t, sr.Err) assert.True(t, sr.Duration > 0) - assert.Equal(t, "hello", server.LastRequest.URL.Query().Get("blob")) + assert.Equal(t, TxXDR, server.LastRequest.URL.Query().Get("blob")) // Succeeds when stellar-core gives the DUPLICATE response. server = test.NewStaticMockServer(`{ @@ -30,41 +36,41 @@ func TestDefaultSubmitter(t *testing.T) { }`) defer server.Close() - s = NewDefaultSubmitter(http.DefaultClient, server.URL) - sr = s.Submit(ctx, "hello") + s = NewDefaultSubmitter(http.DefaultClient, server.URL, prometheus.NewRegistry()) + sr = s.Submit(ctx, TxXDR) assert.Nil(t, sr.Err) // Errors when the stellar-core url is empty - s = NewDefaultSubmitter(http.DefaultClient, "") - sr = s.Submit(ctx, "hello") + s = NewDefaultSubmitter(http.DefaultClient, "", prometheus.NewRegistry()) + sr = s.Submit(ctx, TxXDR) assert.NotNil(t, sr.Err) //errors when the stellar-core url is not parseable - s = NewDefaultSubmitter(http.DefaultClient, "http://Not a url") - sr = s.Submit(ctx, "hello") + s = NewDefaultSubmitter(http.DefaultClient, "http://Not a url", prometheus.NewRegistry()) + sr = s.Submit(ctx, TxXDR) assert.NotNil(t, sr.Err) // errors when the stellar-core url is not reachable - s = NewDefaultSubmitter(http.DefaultClient, "http://127.0.0.1:65535") - sr = s.Submit(ctx, "hello") + s = NewDefaultSubmitter(http.DefaultClient, "http://127.0.0.1:65535", prometheus.NewRegistry()) + sr = s.Submit(ctx, TxXDR) assert.NotNil(t, sr.Err) // errors when the stellar-core returns an unparseable response server = test.NewStaticMockServer(`{`) defer server.Close() - s = NewDefaultSubmitter(http.DefaultClient, server.URL) - sr = s.Submit(ctx, "hello") + s = NewDefaultSubmitter(http.DefaultClient, server.URL, prometheus.NewRegistry()) + sr = s.Submit(ctx, TxXDR) assert.NotNil(t, sr.Err) // errors when the stellar-core returns an exception response server = test.NewStaticMockServer(`{"exception": "Invalid XDR"}`) defer server.Close() - s = NewDefaultSubmitter(http.DefaultClient, server.URL) - sr = s.Submit(ctx, "hello") + s = NewDefaultSubmitter(http.DefaultClient, server.URL, prometheus.NewRegistry()) + sr = s.Submit(ctx, TxXDR) assert.NotNil(t, sr.Err) assert.Contains(t, sr.Err.Error(), "Invalid XDR") @@ -72,8 +78,8 @@ func TestDefaultSubmitter(t *testing.T) { server = test.NewStaticMockServer(`{"status": "NOTREAL"}`) defer server.Close() - s = NewDefaultSubmitter(http.DefaultClient, server.URL) - sr = s.Submit(ctx, "hello") + s = NewDefaultSubmitter(http.DefaultClient, server.URL, prometheus.NewRegistry()) + sr = s.Submit(ctx, TxXDR) assert.NotNil(t, sr.Err) assert.Contains(t, sr.Err.Error(), "NOTREAL") @@ -81,8 +87,8 @@ func TestDefaultSubmitter(t *testing.T) { server = test.NewStaticMockServer(`{"status": "ERROR", "error": "1234"}`) defer server.Close() - s = NewDefaultSubmitter(http.DefaultClient, server.URL) - sr = s.Submit(ctx, "hello") + s = NewDefaultSubmitter(http.DefaultClient, server.URL, prometheus.NewRegistry()) + sr = s.Submit(ctx, TxXDR) assert.IsType(t, &FailedTransactionError{}, sr.Err) ferr := sr.Err.(*FailedTransactionError) assert.Equal(t, "1234", ferr.ResultXDR) diff --git a/services/horizon/internal/txsub/system.go b/services/horizon/internal/txsub/system.go index 31038135f3..2232e61c8d 100644 --- a/services/horizon/internal/txsub/system.go +++ b/services/horizon/internal/txsub/system.go @@ -4,11 +4,13 @@ import ( "context" "database/sql" "fmt" - "github.com/stellar/go/services/horizon/internal/ledger" "sync" "time" + "github.com/stellar/go/services/horizon/internal/ledger" + "github.com/prometheus/client_golang/prometheus" + "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/support/log" "github.com/stellar/go/xdr" @@ -44,10 +46,6 @@ type System struct { LedgerState ledger.StateInterface Metrics struct { - // SubmissionDuration exposes timing metrics about the rate and latency of - // submissions to stellar-core - SubmissionDuration prometheus.Summary - // OpenSubmissionsGauge tracks the count of "open" submissions (i.e. // submissions whose transactions haven't been confirmed successful or failed OpenSubmissionsGauge prometheus.Gauge @@ -59,30 +57,14 @@ type System struct { // SuccessfulSubmissionsCounter tracks the rate of successful transactions that // have been submitted to this process SuccessfulSubmissionsCounter prometheus.Counter - - // V0TransactionsCounter tracks the rate of v0 transaction envelopes that - // have been submitted to this process - V0TransactionsCounter prometheus.Counter - - // V1TransactionsCounter tracks the rate of v1 transaction envelopes that - // have been submitted to this process - V1TransactionsCounter prometheus.Counter - - // FeeBumpTransactionsCounter tracks the rate of fee bump transaction envelopes that - // have been submitted to this process - FeeBumpTransactionsCounter prometheus.Counter } } // RegisterMetrics registers the prometheus metrics func (sys *System) RegisterMetrics(registry *prometheus.Registry) { - registry.MustRegister(sys.Metrics.SubmissionDuration) registry.MustRegister(sys.Metrics.OpenSubmissionsGauge) registry.MustRegister(sys.Metrics.FailedSubmissionsCounter) registry.MustRegister(sys.Metrics.SuccessfulSubmissionsCounter) - registry.MustRegister(sys.Metrics.V0TransactionsCounter) - registry.MustRegister(sys.Metrics.V1TransactionsCounter) - registry.MustRegister(sys.Metrics.FeeBumpTransactionsCounter) } // Submit submits the provided base64 encoded transaction envelope to the @@ -130,7 +112,6 @@ func (sys *System) Submit( } sr := sys.submitOnce(ctx, rawTx) - sys.updateTransactionTypeMetrics(envelope) if sr.Err != nil { // any error other than "txBAD_SEQ" is a failure @@ -222,12 +203,10 @@ func (sys *System) deriveTxSubError(ctx context.Context) error { // Submit submits the provided base64 encoded transaction envelope to the // network using this submission system. -func (sys *System) submitOnce(ctx context.Context, env string) SubmissionResult { +func (sys *System) submitOnce(ctx context.Context, rawTx string) SubmissionResult { // submit to stellar-core - sr := sys.Submitter.Submit(ctx, env) - sys.Metrics.SubmissionDuration.Observe(float64(sr.Duration.Seconds())) + sr := sys.Submitter.Submit(ctx, rawTx) - // if received or duplicate, add to the open submissions list if sr.Err == nil { sys.Metrics.SuccessfulSubmissionsCounter.Inc() } else { @@ -237,17 +216,6 @@ func (sys *System) submitOnce(ctx context.Context, env string) SubmissionResult return sr } -func (sys *System) updateTransactionTypeMetrics(envelope xdr.TransactionEnvelope) { - switch envelope.Type { - case xdr.EnvelopeTypeEnvelopeTypeTxV0: - sys.Metrics.V0TransactionsCounter.Inc() - case xdr.EnvelopeTypeEnvelopeTypeTx: - sys.Metrics.V1TransactionsCounter.Inc() - case xdr.EnvelopeTypeEnvelopeTypeTxFeeBump: - sys.Metrics.FeeBumpTransactionsCounter.Inc() - } -} - // setTickInProgress sets `tickInProgress` to `true` if it's // `false`. Returns `true` if `tickInProgress` has been switched // to `true` inside this method and `Tick()` should continue. @@ -360,11 +328,6 @@ func (sys *System) Init() { sys.initializer.Do(func() { sys.Log = log.DefaultLogger.WithField("service", "txsub.System") - sys.Metrics.SubmissionDuration = prometheus.NewSummary(prometheus.SummaryOpts{ - Namespace: "horizon", Subsystem: "txsub", Name: "submission_duration_seconds", - Help: "submission durations to Stellar-Core, sliding window = 10m", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }) sys.Metrics.FailedSubmissionsCounter = prometheus.NewCounter(prometheus.CounterOpts{ Namespace: "horizon", Subsystem: "txsub", Name: "failed", }) @@ -374,15 +337,6 @@ func (sys *System) Init() { sys.Metrics.OpenSubmissionsGauge = prometheus.NewGauge(prometheus.GaugeOpts{ Namespace: "horizon", Subsystem: "txsub", Name: "open", }) - sys.Metrics.V0TransactionsCounter = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: "horizon", Subsystem: "txsub", Name: "v0", - }) - sys.Metrics.V1TransactionsCounter = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: "horizon", Subsystem: "txsub", Name: "v1", - }) - sys.Metrics.FeeBumpTransactionsCounter = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: "horizon", Subsystem: "txsub", Name: "feebump", - }) sys.accountSeqPollInterval = time.Second diff --git a/services/horizon/internal/txsub/system_test.go b/services/horizon/internal/txsub/system_test.go index b4a36fb522..fbaa353c1b 100644 --- a/services/horizon/internal/txsub/system_test.go +++ b/services/horizon/internal/txsub/system_test.go @@ -250,7 +250,6 @@ func (suite *SystemTestSuite) TestSubmit_NotFoundError() { assert.True(suite.T(), suite.submitter.WasSubmittedTo) assert.Equal(suite.T(), float64(0), getMetricValue(suite.system.Metrics.SuccessfulSubmissionsCounter).GetCounter().GetValue()) assert.Equal(suite.T(), float64(1), getMetricValue(suite.system.Metrics.FailedSubmissionsCounter).GetCounter().GetValue()) - assert.Equal(suite.T(), uint64(1), getMetricValue(suite.system.Metrics.SubmissionDuration).GetSummary().GetSampleCount()) } // If the error is bad_seq and the result at the transaction's sequence number is for the same hash, return result. @@ -408,7 +407,6 @@ func (suite *SystemTestSuite) TestSubmit_OpenTransactionList() { assert.Equal(suite.T(), suite.successTx.Transaction.TransactionHash, pending[0]) assert.Equal(suite.T(), float64(1), getMetricValue(suite.system.Metrics.SuccessfulSubmissionsCounter).GetCounter().GetValue()) assert.Equal(suite.T(), float64(0), getMetricValue(suite.system.Metrics.FailedSubmissionsCounter).GetCounter().GetValue()) - assert.Equal(suite.T(), uint64(1), getMetricValue(suite.system.Metrics.SubmissionDuration).GetSummary().GetSampleCount()) } // Tick should be a no-op if there are no open submissions. From 53b431915aafcdb6c57196a189af250f3e12a871 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 26 Apr 2024 17:19:17 -0400 Subject: [PATCH 128/234] Update how backend buffers ledgers --- ingest/ledgerbackend/gcs_backend.go | 489 +++++++++++++---------- ingest/ledgerbackend/gcs_backend_test.go | 52 --- support/datastore/datastore.go | 5 +- support/datastore/gcs_datastore.go | 5 +- support/datastore/mock_datastore.go | 3 + support/priorityqueue/priorityqueue.go | 53 +++ 6 files changed, 341 insertions(+), 266 deletions(-) delete mode 100644 ingest/ledgerbackend/gcs_backend_test.go create mode 100644 support/priorityqueue/priorityqueue.go diff --git a/ingest/ledgerbackend/gcs_backend.go b/ingest/ledgerbackend/gcs_backend.go index 6252c9ff9b..947637ffb0 100644 --- a/ingest/ledgerbackend/gcs_backend.go +++ b/ingest/ledgerbackend/gcs_backend.go @@ -2,49 +2,49 @@ package ledgerbackend import ( "compress/gzip" + "container/heap" "context" "io" - "path" - "strconv" - "strings" "sync" "time" "github.com/pkg/errors" "github.com/stellar/go/support/datastore" "github.com/stellar/go/support/log" - "github.com/stellar/go/support/ordered" + "github.com/stellar/go/support/priorityqueue" "github.com/stellar/go/xdr" + "google.golang.org/api/googleapi" ) // Ensure GCSBackend implements LedgerBackend var _ LedgerBackend = (*GCSBackend)(nil) -type LCMCache struct { - mu sync.Mutex - lcm map[uint32]xdr.LedgerCloseMeta -} - type LCMFileConfig struct { StorageURL string FileSuffix string - LedgersPerFile uint32 - FilesPerPartition uint32 - parallelReaders uint32 + LedgersPerFile uint32 `default:"1"` + FilesPerPartition uint32 `default:"64000"` +} + +type BufferConfig struct { + BufferSize uint32 `default:"1000"` + NumWorkers uint32 `default:"5"` + RetryLimit uint32 `default:"3"` + RetryWait time.Duration `default:"5"` +} + +type GCSBackendConfig struct { + lcmFileConfig LCMFileConfig + bufferConfig BufferConfig } // GCSBackend is a ledger backend that reads from a cloud storage service. // The cloud storage service contains files generated from the ledgerExporter. type GCSBackend struct { - lcmDataStore datastore.DataStore - storageURL string - fileSuffix string - ledgersPerFile uint32 - filesPerPartition uint32 + config GCSBackendConfig // cancel is the CancelFunc for context which controls the lifetime of a GCSBackend instance. - // Once it is invoked GCSBackend will not be able to stream ledgers from GCSBackend or - // spawn new instances of Stellar Core. + // Once it is invoked GCSBackend will not be able to stream ledgers from GCSBackend. cancel context.CancelFunc // gcsBackendLock protects access to gcsBackendRunner. When the read lock @@ -52,56 +52,238 @@ type GCSBackend struct { // gcsBackendRunner can be updated. gcsBackendLock sync.RWMutex - // lcmCache keeps that ledger close meta in-memory. - lcmCache *LCMCache + // ledgerBuffer is the buffer for LedgerCloseMeta data read in parallel. + ledgerBuffer *LedgerBufferGCS - prepared *Range // non-nil if any range is prepared - closed bool // False until the core is closed - parallelReaders uint32 // Number of parallel GCS readers - context context.Context - nextLedger uint32 // next ledger expected, error w/ restart if not seen - lastLedger *uint32 // end of current segment if offline, nil if online + prepared *Range // non-nil if any range is prepared + closed bool // False until the core is closed } -// Return a new GCSBackend instance. -func NewGCSBackend(ctx context.Context, fileConfig LCMFileConfig) (*GCSBackend, error) { - lcmDataStore, err := datastore.NewDataStore(ctx, fileConfig.StorageURL) +type LedgerBufferGCS struct { + config GCSBackendConfig + lcmDataStore datastore.DataStore + taskQueue chan uint32 + ledgerQueue chan []byte + ledgerPriorityQueue priorityqueue.PriorityQueue + priorityQueueLock sync.Mutex + count uint32 + limit uint32 + currentLedger uint32 + nextTaskLedger uint32 + nextLedgerQueueLedger uint32 + cancel context.CancelFunc + bounded bool + ledgerRange *Range +} + +func NewLedgerBuffer(ctx context.Context, config GCSBackendConfig) (*LedgerBufferGCS, error) { + var cancel context.CancelFunc + + lcmDataStore, err := datastore.NewDataStore(ctx, config.lcmFileConfig.StorageURL) if err != nil { return nil, err } - if ctx == nil { - ctx = context.Background() + pq := make(priorityqueue.PriorityQueue, config.bufferConfig.BufferSize) + heap.Init(&pq) + + ledgerBuffer := &LedgerBufferGCS{ + lcmDataStore: lcmDataStore, + taskQueue: make(chan uint32, config.bufferConfig.BufferSize), + ledgerQueue: make(chan []byte, config.bufferConfig.BufferSize), + ledgerPriorityQueue: pq, + count: 0, + limit: config.bufferConfig.BufferSize, + cancel: cancel, } - lcmCache := &LCMCache{lcm: make(map[uint32]xdr.LedgerCloseMeta)} + // Workers to read LCM files + for i := uint32(0); i < config.bufferConfig.NumWorkers; i++ { + go ledgerBuffer.worker() + } + + // goroutine to correctly LCM files + go ledgerBuffer.reorderLedgers() + + return ledgerBuffer, nil +} + +func (lb *LedgerBufferGCS) pushTaskQueue() { + for lb.count <= lb.limit { + lb.taskQueue <- lb.nextTaskLedger + lb.nextTaskLedger++ + lb.count++ + } +} + +func (lb *LedgerBufferGCS) worker() { + for sequence := range lb.taskQueue { + retryCount := uint32(0) + for retryCount <= lb.config.bufferConfig.RetryLimit { + ledgerObject, err := lb.getLedgerGCSObject(sequence) + if err != nil { + if e, ok := err.(*googleapi.Error); ok { + // ledgerObject not found and unbounded + if e.Code == 404 && !lb.bounded { + continue + } + } + retryCount++ + time.Sleep(lb.config.bufferConfig.RetryWait * time.Second) + } + + // Add to priority queue and continue to next task + lb.priorityQueueLock.Lock() + item := &priorityqueue.Item{ + Value: ledgerObject, + Priority: int(sequence), + } + heap.Push(&lb.ledgerPriorityQueue, item) + lb.ledgerPriorityQueue.Update(item, item.Value, int(sequence)) + lb.priorityQueueLock.Unlock() + break + } + } +} + +func (lb *LedgerBufferGCS) getLedgerGCSObject(sequence uint32) ([]byte, error) { + var ledgerCloseMetaBatch xdr.LedgerCloseMetaBatch + + objectKey, err := datastore.GetObjectKeyFromSequenceNumber( + sequence, + lb.config.lcmFileConfig.LedgersPerFile, + lb.config.lcmFileConfig.FilesPerPartition, + lb.config.lcmFileConfig.FileSuffix) + if err != nil { + return nil, errors.Wrapf(err, "failed to get object key for ledger %d", sequence) + } + + reader, err := lb.lcmDataStore.GetFile(context.Background(), objectKey) + if err != nil { + return nil, errors.Wrapf(err, "failed getting file: %s", objectKey) + } + + defer reader.Close() + + // Read file and unzip + gzipReader, err := gzip.NewReader(reader) + if err != nil { + return nil, errors.Wrapf(err, "failed getting file: %s", objectKey) + } + + defer gzipReader.Close() + + objectBytes, err := io.ReadAll(gzipReader) + if err != nil { + return nil, errors.Wrapf(err, "failed reading file: %s", objectKey) + } + + // Turn binary into xdr + err = ledgerCloseMetaBatch.UnmarshalBinary(objectBytes) + if err != nil { + return nil, errors.Wrapf(err, "failed unmarshalling file: %s", objectKey) + } + + // Check if ledger sequence within the xdr.ledgerCloseMetaBatch + startSequence := uint32(ledgerCloseMetaBatch.StartSequence) + if startSequence > sequence { + return nil, errors.Wrapf(err, "start sequence: %d; greater than sequence to get: %d", startSequence, sequence) + } + + ledgerCloseMetasIndex := sequence - startSequence + ledgerCloseMeta := ledgerCloseMetaBatch.LedgerCloseMetas[ledgerCloseMetasIndex] + + // Turn lcm back to binary to save memory in buffer + lcmBinary, err := ledgerCloseMeta.MarshalBinary() + if err != nil { + return nil, errors.Wrapf(err, "failed marshalling lcm sequence: %d", sequence) + } + + return lcmBinary, nil +} + +func (lb *LedgerBufferGCS) reorderLedgers() { + lb.priorityQueueLock.Lock() + defer lb.priorityQueueLock.Unlock() + + // Nothing in priority queue + if lb.ledgerPriorityQueue.Len() < 0 { + return + } + + // Check if the nextLedger is the next item in the priority queue + for lb.currentLedger == uint32(lb.ledgerPriorityQueue[0].Priority) { + item := heap.Pop(&lb.ledgerPriorityQueue).(*priorityqueue.Item) + lb.ledgerQueue <- item.Value + lb.nextLedgerQueueLedger++ + } +} + +func (lb *LedgerBufferGCS) getFromLedgerQueue(ctx context.Context) ([]byte, error) { + for { + select { + case <-ctx.Done(): + log.Info("Stopping ExportManager due to context cancellation") + return nil, ctx.Err() + case lcmBinary := <-lb.ledgerQueue: + lb.currentLedger++ + // Decrement ledger buffer counter + lb.count-- + // Add next task to the TaskQueue + lb.pushTaskQueue() + + return lcmBinary, nil + } + } +} + +// Return a new GCSBackend instance. +func NewGCSBackend(ctx context.Context, config GCSBackendConfig) (*GCSBackend, error) { + // Check/set minimum config values + if config.lcmFileConfig.StorageURL == "" { + return nil, errors.New("fileConfig.storageURL is not set") + } + + if config.lcmFileConfig.FileSuffix == "" { + return nil, errors.New("fileConfig.FileSuffix is not set") + } + + if config.lcmFileConfig.LedgersPerFile == 0 { + config.lcmFileConfig.LedgersPerFile = 1 + } + + if config.lcmFileConfig.FilesPerPartition == 0 { + config.lcmFileConfig.FilesPerPartition = 1 + } + + // Check/set minimum config values + if config.bufferConfig.BufferSize == 0 { + config.bufferConfig.BufferSize = 1 + } + + if config.bufferConfig.NumWorkers == 0 { + config.bufferConfig.NumWorkers = 1 + } var cancel context.CancelFunc - ctx, cancel = context.WithCancel(ctx) - // Need at least 1 reader - parallelReaders := fileConfig.parallelReaders - if parallelReaders == 0 { - parallelReaders = 1 + ledgerBuffer, err := NewLedgerBuffer(ctx, config) + if err != nil { + return nil, err } - cloudStorageBackend := &GCSBackend{ - lcmDataStore: lcmDataStore, - storageURL: fileConfig.StorageURL, - fileSuffix: fileConfig.FileSuffix, - ledgersPerFile: fileConfig.LedgersPerFile, - filesPerPartition: fileConfig.FilesPerPartition, - cancel: cancel, - lcmCache: lcmCache, - parallelReaders: fileConfig.parallelReaders, - context: ctx, + gcsBackend := &GCSBackend{ + config: config, + cancel: cancel, + ledgerBuffer: ledgerBuffer, } - return cloudStorageBackend, nil + return gcsBackend, nil } // GetLatestLedgerSequence returns the most recent ledger sequence number in the cloud storage bucket. func (gcsb *GCSBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { + /* TODO: replace with binary search code gcsb.gcsBackendLock.RLock() defer gcsb.gcsBackendLock.RUnlock() @@ -128,6 +310,8 @@ func (gcsb *GCSBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, er } return latestLedgerSequence, nil + */ + return 0, nil } // GetLedger returns the LedgerCloseMeta for the specified ledger sequence number @@ -143,26 +327,27 @@ func (gcsb *GCSBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.Led return xdr.LedgerCloseMeta{}, errors.New("session is not prepared, call PrepareRange first") } - // Block until the requested sequence is available - for { - select { - case <-ctx.Done(): - return xdr.LedgerCloseMeta{}, ctx.Err() - default: - lcm, ok := gcsb.lcmCache.lcm[sequence] - if !ok { - continue - } - // Delete to free space for unbounded mode lcm retrieval - delete(gcsb.lcmCache.lcm, sequence) - return lcm, nil + var lcmBinary []byte + var err error + for gcsb.ledgerBuffer.currentLedger <= sequence { + lcmBinary, err = gcsb.ledgerBuffer.getFromLedgerQueue(ctx) + if err != nil { + return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "could not get ledger sequence binary: %d", sequence) } } + + var lcm xdr.LedgerCloseMeta + err = lcm.UnmarshalBinary(lcmBinary) + if err != nil { + return xdr.LedgerCloseMeta{}, err + } + + return lcm, nil } // PrepareRange checks if the starting and ending (if bounded) ledgers exist. func (gcsb *GCSBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { - if alreadyPrepared, err := gcsb.startPreparingRange(ctx, ledgerRange); err != nil { + if alreadyPrepared, err := gcsb.startPreparingRange(ledgerRange); err != nil { return errors.Wrap(err, "error starting prepare range") } else if alreadyPrepared { return nil @@ -186,34 +371,35 @@ func (gcsb *GCSBackend) isPrepared(ledgerRange Range) bool { return false } - // lastLedger is only set when ledgerRange is bounded - lastLedger := uint32(0) - if gcsb.lastLedger != nil { - lastLedger = *gcsb.lastLedger + if gcsb.prepared == nil { + return false } - if gcsb.prepared == nil { + if gcsb.ledgerBuffer.ledgerRange == nil { + return false + } + + if gcsb.ledgerBuffer.ledgerRange.from > ledgerRange.from { + return false + } + + if gcsb.ledgerBuffer.ledgerRange.bounded && !ledgerRange.bounded { return false } - // Unbounded mode only checks for the starting ledger - if lastLedger == 0 { - _, ok := gcsb.lcmCache.lcm[ledgerRange.from] - return ok + if !gcsb.ledgerBuffer.ledgerRange.bounded && !ledgerRange.bounded { + return true } - // From now on: lastLedger != 0 so current range is bounded - if ledgerRange.bounded { - _, ok := gcsb.lcmCache.lcm[ledgerRange.from] - return ok && lastLedger >= ledgerRange.to + if gcsb.ledgerBuffer.ledgerRange.to >= ledgerRange.to { + return true } - // Requested range is unbounded but current one is bounded return false } -// Close closes existing GCSBackend process, streaming sessions and removes all -// temporary files. Note, once a GCSBackend instance is closed it can no longer be used and +// Close closes existing GCSBackend processes. +// Note, once a GCSBackend instance is closed it can no longer be used and // all subsequent calls to PrepareRange(), GetLedger(), etc will fail. // Close is thread-safe and can be called from another go routine. func (gcsb *GCSBackend) Close() error { @@ -228,6 +414,8 @@ func (gcsb *GCSBackend) Close() error { return nil } +// TODO: remove when binary search is merged +/* // GetLatestDirectory returns the latest directory from an array of directories func (gcsb *GCSBackend) GetLatestDirectory(directories []string) (string, error) { var latestDirectory string @@ -263,7 +451,7 @@ func (gcsb *GCSBackend) GetLatestFileNameLedgerSequence(fileNames []string, dire for _, fileName := range fileNames { // fileName follows the format of "ledgers//-/." // Trim the file down to just the - fileNameTrimExt := strings.TrimSuffix(fileName, gcsb.fileSuffix) + fileNameTrimExt := strings.TrimSuffix(fileName, gcsb.lcmFileConfig.FileSuffix) fileNameTrimPath := strings.TrimPrefix(fileNameTrimExt, directory+"/") ledgerSequence, err := strconv.ParseUint(fileNameTrimPath, 10, 32) if err != nil { @@ -275,11 +463,10 @@ func (gcsb *GCSBackend) GetLatestFileNameLedgerSequence(fileNames []string, dire return latestLedgerSequence, nil } +*/ -// startPreparingRange prepares the ledger range -// Bounded ranges will load the full range to lcmCache -// Unbounded ranges will continuously read new LCM to lcmCache -func (gcsb *GCSBackend) startPreparingRange(ctx context.Context, ledgerRange Range) (bool, error) { +// startPreparingRange prepares the ledger range by setting the range in the ledgerBuffer +func (gcsb *GCSBackend) startPreparingRange(ledgerRange Range) (bool, error) { gcsb.gcsBackendLock.Lock() defer gcsb.gcsBackendLock.Unlock() @@ -287,133 +474,15 @@ func (gcsb *GCSBackend) startPreparingRange(ctx context.Context, ledgerRange Ran return true, nil } - // Set the starting ledger - gcsb.nextLedger = ledgerRange.from - - if ledgerRange.bounded { - gcsb.getGCSObjectsParallel(ctx, ledgerRange) - return false, nil - } + // Set the ledgerRange in ledgerBuffer + gcsb.ledgerBuffer.ledgerRange = &ledgerRange + gcsb.ledgerBuffer.currentLedger = ledgerRange.from + gcsb.ledgerBuffer.nextTaskLedger = ledgerRange.from + gcsb.ledgerBuffer.nextLedgerQueueLedger = ledgerRange.from + gcsb.ledgerBuffer.bounded = ledgerRange.bounded - // If unbounded, continously get new ledgers - go gcsb.getNewLedgerObjects(ctx) + // Start the ledgerBuffer + gcsb.ledgerBuffer.pushTaskQueue() return false, nil } - -// getGCSObjectsParallel loads the LCM from the ledgerRange to lcmCache in parallel -func (gcsb *GCSBackend) getGCSObjectsParallel(ctx context.Context, ledgerRange Range) { - var wg sync.WaitGroup - sem := make(chan struct{}, gcsb.parallelReaders) - - interval := (ledgerRange.to - ledgerRange.from) / gcsb.parallelReaders - - for i := uint32(0); i < gcsb.parallelReaders; i++ { - wg.Add(1) - sem <- struct{}{} // Acquire a slot in the semaphore - - // Get the subrange of ledgers to process - from := ledgerRange.from + (i * interval) - to := ledgerRange.from + ((i + 1) * interval) - 1 - // The last reader should run to the final ledger - if i+1 == gcsb.parallelReaders { - to = ledgerRange.to - } - subLedgerRange := BoundedRange(from, to) - - wg.Add(1) - sem <- struct{}{} - go gcsb.getGCSObjects(ctx, subLedgerRange, &wg, sem) - } - - wg.Wait() - gcsb.lastLedger = &ledgerRange.to -} - -// getGCSObjects loads the LCM to lcmCache -func (gcsb *GCSBackend) getGCSObjects(ctx context.Context, ledgerRange Range, wg *sync.WaitGroup, sem chan struct{}) { - defer wg.Done() - defer func() { <-sem }() - - select { - case <-ctx.Done(): // Check if the context was cancelled - return - default: - for i := ledgerRange.from; i <= ledgerRange.to; i++ { - lcm := gcsb.getLedgerGCSObject(i) - - // Store lcm in-memory - gcsb.lcmCache.mu.Lock() - gcsb.lcmCache.lcm[i] = lcm - gcsb.lcmCache.mu.Unlock() - } - } -} - -// getLedgerGCSObject gets the LCM for a given ledger sequence -func (gcsb *GCSBackend) getLedgerGCSObject(sequence uint32) xdr.LedgerCloseMeta { - var ledgerCloseMetaBatch xdr.LedgerCloseMetaBatch - - objectKey, err := datastore.GetObjectKeyFromSequenceNumber(sequence, gcsb.ledgersPerFile, gcsb.filesPerPartition, gcsb.fileSuffix) - if err != nil { - log.Fatalf("failed to get object key for ledger %d; %s", sequence, err) - } - - reader, err := gcsb.lcmDataStore.GetFile(context.Background(), objectKey) - if err != nil { - log.Fatalf("failed getting file: %s; %s", objectKey, err) - } - - defer reader.Close() - - gzipReader, err := gzip.NewReader(reader) - if err != nil { - log.Fatalf("failed getting file: %s; %s", objectKey, err) - } - - defer gzipReader.Close() - - objectBytes, err := io.ReadAll(gzipReader) - if err != nil { - log.Fatalf("failed reading file: %s; %s", objectKey, err) - } - - err = ledgerCloseMetaBatch.UnmarshalBinary(objectBytes) - if err != nil { - log.Fatalf("failed unmarshalling file: %s; %s", objectKey, err) - } - - startSequence := uint32(ledgerCloseMetaBatch.StartSequence) - if startSequence > sequence { - log.Fatalf("start sequence: %d; greater than sequence to get: %d; %s", startSequence, sequence, err) - } - - ledgerCloseMetasIndex := sequence - startSequence - ledgerCloseMeta := ledgerCloseMetaBatch.LedgerCloseMetas[ledgerCloseMetasIndex] - - return ledgerCloseMeta -} - -// getNewLedgerObjects polls GCS and buffers new LCM -func (gcsb *GCSBackend) getNewLedgerObjects(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return // Exit if the context is canceled - case <-time.After(1): - // Buffer lcmCache if LCMs exist - for len(gcsb.lcmCache.lcm) < 1000 { - // Check if LCM exists; otherwise wait and poll again - objectKey, _ := datastore.GetObjectKeyFromSequenceNumber(gcsb.nextLedger, gcsb.ledgersPerFile, gcsb.filesPerPartition, gcsb.fileSuffix) - exists, _ := gcsb.lcmDataStore.Exists(context.Background(), objectKey) - if !exists { - break - } - // Get LCM and add to lcmCache - lcm := gcsb.getLedgerGCSObject(gcsb.nextLedger) - gcsb.lcmCache.lcm[gcsb.nextLedger] = lcm - gcsb.nextLedger += 1 - } - } - } -} diff --git a/ingest/ledgerbackend/gcs_backend_test.go b/ingest/ledgerbackend/gcs_backend_test.go deleted file mode 100644 index c605523c0d..0000000000 --- a/ingest/ledgerbackend/gcs_backend_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package ledgerbackend - -import ( - "context" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/assert" -) - -func MockGCSBackend() GCSBackend { - lcmCache := &LCMCache{lcm: make(map[uint32]xdr.LedgerCloseMeta)} - return GCSBackend{ - fileSuffix: ".xdr.gz", - lcmCache: lcmCache, - } -} - -func TestGCSBackendGetLedger(t *testing.T) { - gcsb := MockGCSBackend() - gcsb.lcmCache.lcm[1] = xdr.LedgerCloseMeta{V: 0} - gcsb.lcmCache.lcm[2] = xdr.LedgerCloseMeta{V: 1} - ledgerRange := BoundedRange(1, 2) - gcsb.prepared = &ledgerRange - ctx := context.Background() - - lcm, err := gcsb.GetLedger(ctx, 1) - - assert.NoError(t, err) - assert.Equal(t, xdr.LedgerCloseMeta{V: 0}, lcm) -} - -func TestGetLatestFileNameLedgerSequence(t *testing.T) { - gcsb := MockGCSBackend() - directory := "ledgers/pubnet/21-30" - filenames := []string{ - "ledgers/pubnet/21-30/21.xdr.gz", - "ledgers/pubnet/21-30/22.xdr.gz", - "ledgers/pubnet/21-30/23.xdr.gz", - } - latestLedgerSequence, _ := gcsb.GetLatestFileNameLedgerSequence(filenames, directory) - - assert.Equal(t, uint32(23), latestLedgerSequence) -} - -func TestGetLatestDirectory(t *testing.T) { - gcsb := MockGCSBackend() - directories := []string{"ledgers/pubnet/1-10", "ledgers/pubnet/11-20", "ledgers/pubnet/21-30"} - latestDirectory, _ := gcsb.GetLatestDirectory(directories) - - assert.Equal(t, "ledgers/pubnet/21-30", latestDirectory) -} diff --git a/support/datastore/datastore.go b/support/datastore/datastore.go index 5d9db57fbb..33df3f635d 100644 --- a/support/datastore/datastore.go +++ b/support/datastore/datastore.go @@ -22,8 +22,9 @@ type DataStore interface { Exists(ctx context.Context, path string) (bool, error) Size(ctx context.Context, path string) (int64, error) Close() error - ListDirectoryNames(ctx context.Context) ([]string, error) - ListFileNames(ctx context.Context, path string) ([]string, error) + // TODO: Remove when binary search code is added + //ListDirectoryNames(ctx context.Context) ([]string, error) + //ListFileNames(ctx context.Context, path string) ([]string, error) } // NewDataStore creates a new DataStore based on the destination URL. diff --git a/support/datastore/gcs_datastore.go b/support/datastore/gcs_datastore.go index 7a2418e8c7..cc6332726d 100644 --- a/support/datastore/gcs_datastore.go +++ b/support/datastore/gcs_datastore.go @@ -6,10 +6,8 @@ import ( "net/http" "os" "path" - "strings" "google.golang.org/api/googleapi" - "google.golang.org/api/iterator" "cloud.google.com/go/storage" @@ -108,6 +106,8 @@ func (b GCSDataStore) putFile(ctx context.Context, filePath string, in io.Writer return w.Close() } +// TODO: Remove when binary search code is added +/* func (b *GCSDataStore) ListDirectoryNames(ctx context.Context) ([]string, error) { var directories []string @@ -147,3 +147,4 @@ func (b *GCSDataStore) ListFileNames(ctx context.Context, path string) ([]string return files, nil } +*/ diff --git a/support/datastore/mock_datastore.go b/support/datastore/mock_datastore.go index 80bec3af59..b6fb06bb5e 100644 --- a/support/datastore/mock_datastore.go +++ b/support/datastore/mock_datastore.go @@ -42,6 +42,8 @@ func (m *MockDataStore) Close() error { return args.Error(0) } +// TODO: Remove when binary search code is added +/* func (m *MockDataStore) ListDirectoryNames(ctx context.Context) ([]string, error) { args := m.Called(ctx) return args.Get(0).([]string), args.Error(0) @@ -51,3 +53,4 @@ func (m *MockDataStore) ListFileNames(ctx context.Context, path string) ([]strin args := m.Called(ctx, path) return args.Get(0).([]string), args.Error(0) } +*/ diff --git a/support/priorityqueue/priorityqueue.go b/support/priorityqueue/priorityqueue.go new file mode 100644 index 0000000000..43d5e2ee1d --- /dev/null +++ b/support/priorityqueue/priorityqueue.go @@ -0,0 +1,53 @@ +package priorityqueue + +import ( + "container/heap" +) + +// An Item is something we manage in a priority queue. +type Item struct { + Value []byte // Value of the item + Priority int // The priority of the item in the queue. + // The index is needed by update and is maintained by the heap.Interface methods. + Index int // The index of the item in the heap. +} + +// A PriorityQueue implements heap.Interface and holds Items. +type PriorityQueue []*Item + +func (pq PriorityQueue) Len() int { return len(pq) } + +func (pq PriorityQueue) Less(i, j int) bool { + // We want Pop to give us the highest, not lowest, priority so we use greater than here. + return pq[i].Priority > pq[j].Priority +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] + pq[i].Index = i + pq[j].Index = j +} + +func (pq *PriorityQueue) Push(x any) { + n := len(*pq) + item := x.(*Item) + item.Index = n + *pq = append(*pq, item) +} + +func (pq *PriorityQueue) Pop() any { + old := *pq + n := len(old) + item := old[n-1] + old[n-1] = nil // avoid memory leak + item.Index = -1 // for safety + *pq = old[0 : n-1] + return item +} + +// update modifies the priority and value of an Item in the queue. +func (pq *PriorityQueue) Update(item *Item, value []byte, priority int) { + item.Value = value + item.Priority = priority + heap.Fix(pq, item.Index) +} From 5f508299f378f793972373635e988ea6a2ce7b79 Mon Sep 17 00:00:00 2001 From: Marcelo Salloum dos Santos Date: Mon, 29 Apr 2024 08:34:35 -0700 Subject: [PATCH 129/234] services/regulated-assets-approval-server: bump the Go version in Dockerfile that it was missed in #5249 (#5294) ### What Bump the Go version in Dockerfile that it was missed in #5249 ### Why The toolchain command in go.mod was requiring a Go version newer than the one in the Dockerfile, which was causing docker-build [to fail](https://stellarfoundation.slack.com/archives/C018BLTP2AU/p1714004215758189). Toolchain command: https://github.com/stellar/go/blob/ee9bbbf03be79bbac4000ea97b874ed010757b1b/go.mod#L3-L5 Container version incompatible with that toolchain version: https://github.com/stellar/go/blob/ee9bbbf03be79bbac4000ea97b874ed010757b1b/services/regulated-assets-approval-server/docker/Dockerfile#L1 --- services/regulated-assets-approval-server/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/regulated-assets-approval-server/docker/Dockerfile b/services/regulated-assets-approval-server/docker/Dockerfile index c029189e50..8f1562db6d 100644 --- a/services/regulated-assets-approval-server/docker/Dockerfile +++ b/services/regulated-assets-approval-server/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.19.1 as build +FROM golang:1.22-bullseye as build ADD . /src/regulated-assets-approval-server WORKDIR /src/regulated-assets-approval-server From b45cbc2b3a0bc4f831a6873622336680936cdb05 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Mon, 29 Apr 2024 11:40:58 -0400 Subject: [PATCH 130/234] Updates --- ingest/ledgerbackend/gcs_backend.go | 66 +++++++++++++++-------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/ingest/ledgerbackend/gcs_backend.go b/ingest/ledgerbackend/gcs_backend.go index 947637ffb0..266fe697e5 100644 --- a/ingest/ledgerbackend/gcs_backend.go +++ b/ingest/ledgerbackend/gcs_backend.go @@ -34,8 +34,8 @@ type BufferConfig struct { } type GCSBackendConfig struct { - lcmFileConfig LCMFileConfig - bufferConfig BufferConfig + LcmFileConfig LCMFileConfig + BufferConfig BufferConfig } // GCSBackend is a ledger backend that reads from a cloud storage service. @@ -53,13 +53,13 @@ type GCSBackend struct { gcsBackendLock sync.RWMutex // ledgerBuffer is the buffer for LedgerCloseMeta data read in parallel. - ledgerBuffer *LedgerBufferGCS + ledgerBuffer *ledgerBufferGCS prepared *Range // non-nil if any range is prepared closed bool // False until the core is closed } -type LedgerBufferGCS struct { +type ledgerBufferGCS struct { config GCSBackendConfig lcmDataStore datastore.DataStore taskQueue chan uint32 @@ -76,29 +76,29 @@ type LedgerBufferGCS struct { ledgerRange *Range } -func NewLedgerBuffer(ctx context.Context, config GCSBackendConfig) (*LedgerBufferGCS, error) { +func NewLedgerBuffer(ctx context.Context, config GCSBackendConfig) (*ledgerBufferGCS, error) { var cancel context.CancelFunc - lcmDataStore, err := datastore.NewDataStore(ctx, config.lcmFileConfig.StorageURL) + lcmDataStore, err := datastore.NewDataStore(ctx, config.LcmFileConfig.StorageURL) if err != nil { return nil, err } - pq := make(priorityqueue.PriorityQueue, config.bufferConfig.BufferSize) + pq := make(priorityqueue.PriorityQueue, config.BufferConfig.BufferSize) heap.Init(&pq) - ledgerBuffer := &LedgerBufferGCS{ + ledgerBuffer := &ledgerBufferGCS{ lcmDataStore: lcmDataStore, - taskQueue: make(chan uint32, config.bufferConfig.BufferSize), - ledgerQueue: make(chan []byte, config.bufferConfig.BufferSize), + taskQueue: make(chan uint32, config.BufferConfig.BufferSize), + ledgerQueue: make(chan []byte, config.BufferConfig.BufferSize), ledgerPriorityQueue: pq, count: 0, - limit: config.bufferConfig.BufferSize, + limit: config.BufferConfig.BufferSize, cancel: cancel, } // Workers to read LCM files - for i := uint32(0); i < config.bufferConfig.NumWorkers; i++ { + for i := uint32(0); i < config.BufferConfig.NumWorkers; i++ { go ledgerBuffer.worker() } @@ -108,7 +108,7 @@ func NewLedgerBuffer(ctx context.Context, config GCSBackendConfig) (*LedgerBuffe return ledgerBuffer, nil } -func (lb *LedgerBufferGCS) pushTaskQueue() { +func (lb *ledgerBufferGCS) pushTaskQueue() { for lb.count <= lb.limit { lb.taskQueue <- lb.nextTaskLedger lb.nextTaskLedger++ @@ -116,20 +116,21 @@ func (lb *LedgerBufferGCS) pushTaskQueue() { } } -func (lb *LedgerBufferGCS) worker() { +func (lb *ledgerBufferGCS) worker() { for sequence := range lb.taskQueue { retryCount := uint32(0) - for retryCount <= lb.config.bufferConfig.RetryLimit { + for retryCount <= lb.config.BufferConfig.RetryLimit { ledgerObject, err := lb.getLedgerGCSObject(sequence) if err != nil { if e, ok := err.(*googleapi.Error); ok { // ledgerObject not found and unbounded if e.Code == 404 && !lb.bounded { + time.Sleep(lb.config.BufferConfig.RetryWait * time.Second) continue } } retryCount++ - time.Sleep(lb.config.bufferConfig.RetryWait * time.Second) + time.Sleep(lb.config.BufferConfig.RetryWait * time.Second) } // Add to priority queue and continue to next task @@ -143,17 +144,18 @@ func (lb *LedgerBufferGCS) worker() { lb.priorityQueueLock.Unlock() break } + // Add abort case for max retries } } -func (lb *LedgerBufferGCS) getLedgerGCSObject(sequence uint32) ([]byte, error) { +func (lb *ledgerBufferGCS) getLedgerGCSObject(sequence uint32) ([]byte, error) { var ledgerCloseMetaBatch xdr.LedgerCloseMetaBatch objectKey, err := datastore.GetObjectKeyFromSequenceNumber( sequence, - lb.config.lcmFileConfig.LedgersPerFile, - lb.config.lcmFileConfig.FilesPerPartition, - lb.config.lcmFileConfig.FileSuffix) + lb.config.LcmFileConfig.LedgersPerFile, + lb.config.LcmFileConfig.FilesPerPartition, + lb.config.LcmFileConfig.FileSuffix) if err != nil { return nil, errors.Wrapf(err, "failed to get object key for ledger %d", sequence) } @@ -202,7 +204,7 @@ func (lb *LedgerBufferGCS) getLedgerGCSObject(sequence uint32) ([]byte, error) { return lcmBinary, nil } -func (lb *LedgerBufferGCS) reorderLedgers() { +func (lb *ledgerBufferGCS) reorderLedgers() { lb.priorityQueueLock.Lock() defer lb.priorityQueueLock.Unlock() @@ -219,7 +221,7 @@ func (lb *LedgerBufferGCS) reorderLedgers() { } } -func (lb *LedgerBufferGCS) getFromLedgerQueue(ctx context.Context) ([]byte, error) { +func (lb *ledgerBufferGCS) getFromLedgerQueue(ctx context.Context) ([]byte, error) { for { select { case <-ctx.Done(): @@ -240,29 +242,29 @@ func (lb *LedgerBufferGCS) getFromLedgerQueue(ctx context.Context) ([]byte, erro // Return a new GCSBackend instance. func NewGCSBackend(ctx context.Context, config GCSBackendConfig) (*GCSBackend, error) { // Check/set minimum config values - if config.lcmFileConfig.StorageURL == "" { + if config.LcmFileConfig.StorageURL == "" { return nil, errors.New("fileConfig.storageURL is not set") } - if config.lcmFileConfig.FileSuffix == "" { + if config.LcmFileConfig.FileSuffix == "" { return nil, errors.New("fileConfig.FileSuffix is not set") } - if config.lcmFileConfig.LedgersPerFile == 0 { - config.lcmFileConfig.LedgersPerFile = 1 + if config.LcmFileConfig.LedgersPerFile == 0 { + config.LcmFileConfig.LedgersPerFile = 1 } - if config.lcmFileConfig.FilesPerPartition == 0 { - config.lcmFileConfig.FilesPerPartition = 1 + if config.LcmFileConfig.FilesPerPartition == 0 { + config.LcmFileConfig.FilesPerPartition = 1 } // Check/set minimum config values - if config.bufferConfig.BufferSize == 0 { - config.bufferConfig.BufferSize = 1 + if config.BufferConfig.BufferSize == 0 { + config.BufferConfig.BufferSize = 1 } - if config.bufferConfig.NumWorkers == 0 { - config.bufferConfig.NumWorkers = 1 + if config.BufferConfig.NumWorkers == 0 { + config.BufferConfig.NumWorkers = 1 } var cancel context.CancelFunc From 9808f37f9f76202083cc632001d8fc2bef7bdd69 Mon Sep 17 00:00:00 2001 From: shawn Date: Tue, 30 Apr 2024 14:20:00 -0700 Subject: [PATCH 131/234] /exp/services/ledgerexporter: resumable export, check data storage for optimal starting point (#5264) --- exp/services/ledgerexporter/Makefile | 19 +- exp/services/ledgerexporter/README.md | 17 +- exp/services/ledgerexporter/config.toml | 10 +- exp/services/ledgerexporter/docker/start | 11 +- exp/services/ledgerexporter/internal/app.go | 155 +++++++--- .../ledgerexporter/internal/app_test.go | 114 ++++++++ .../ledgerexporter/internal/config.go | 206 +++++++------ .../ledgerexporter/internal/config_test.go | 213 ++++++++++---- .../ledgerexporter/internal/datastore.go | 44 +-- .../ledgerexporter/internal/datastore_test.go | 13 + .../ledgerexporter/internal/encoder.go | 39 +++ .../{utils_test.go => encoder_test.go} | 38 --- .../ledgerexporter/internal/exportmanager.go | 26 +- .../internal/exportmanager_test.go | 91 ++++-- .../ledgerexporter/internal/gcs_datastore.go | 44 +++ .../internal/ledgerbatch_config.go | 49 ++++ .../internal/ledgerbatch_config_test.go | 36 +++ .../internal/{mock_datastore.go => mocks.go} | 15 +- .../ledgerexporter/internal/queue_test.go | 2 +- .../internal/resumablemanager.go | 129 +++++++++ .../internal/resumablemanager_test.go | 273 ++++++++++++++++++ .../internal/test/10perfile.toml | 4 + .../internal/test/15perfile.toml | 4 + .../internal/test/1perfile.toml | 4 + .../internal/test/64perfile.toml | 4 + .../internal/test/no_network.toml | 10 + .../ledgerexporter/internal/test/test.toml | 11 + .../internal/test/validate_start_end.toml | 4 + exp/services/ledgerexporter/internal/utils.go | 105 ------- exp/services/ledgerexporter/main.go | 21 +- go.mod | 40 +-- go.sum | 88 +++--- 32 files changed, 1350 insertions(+), 489 deletions(-) create mode 100644 exp/services/ledgerexporter/internal/app_test.go create mode 100644 exp/services/ledgerexporter/internal/datastore_test.go create mode 100644 exp/services/ledgerexporter/internal/encoder.go rename exp/services/ledgerexporter/internal/{utils_test.go => encoder_test.go} (56%) create mode 100644 exp/services/ledgerexporter/internal/ledgerbatch_config.go create mode 100644 exp/services/ledgerexporter/internal/ledgerbatch_config_test.go rename exp/services/ledgerexporter/internal/{mock_datastore.go => mocks.go} (70%) create mode 100644 exp/services/ledgerexporter/internal/resumablemanager.go create mode 100644 exp/services/ledgerexporter/internal/resumablemanager_test.go create mode 100644 exp/services/ledgerexporter/internal/test/10perfile.toml create mode 100644 exp/services/ledgerexporter/internal/test/15perfile.toml create mode 100644 exp/services/ledgerexporter/internal/test/1perfile.toml create mode 100644 exp/services/ledgerexporter/internal/test/64perfile.toml create mode 100644 exp/services/ledgerexporter/internal/test/no_network.toml create mode 100644 exp/services/ledgerexporter/internal/test/test.toml create mode 100644 exp/services/ledgerexporter/internal/test/validate_start_end.toml delete mode 100644 exp/services/ledgerexporter/internal/utils.go diff --git a/exp/services/ledgerexporter/Makefile b/exp/services/ledgerexporter/Makefile index e4d6f647e0..5fee16f9d1 100644 --- a/exp/services/ledgerexporter/Makefile +++ b/exp/services/ledgerexporter/Makefile @@ -14,9 +14,15 @@ $(if $(STELLAR_CORE_VERSION), --build-arg STELLAR_CORE_VERSION=$(STELLAR_CORE_VE -t $(DOCKER_IMAGE):$(VERSION) \ -t $(DOCKER_IMAGE):latest . -docker-test: +docker-clean: + $(SUDO) docker stop fake-gcs-server || true + $(SUDO) docker rm fake-gcs-server || true + $(SUDO) rm -rf ${PWD}/storage || true + $(SUDO) docker network rm test-network || true + +docker-test: docker-clean # Create temp storage dir - $(SUDO) mkdir -p ${PWD}/storage/exporter-test + $(SUDO) mkdir -p ${PWD}/storage/exporter-test # Create test network for docker $(SUDO) docker network create test-network @@ -28,16 +34,13 @@ docker-test: # Run the ledger-exporter $(SUDO) docker run --platform linux/amd64 -t --network test-network\ -e NETWORK=pubnet \ - -e ARCHIVE_TARGET=gcs://exporter-test \ + -e ARCHIVE_TARGET=exporter-test/test-subpath \ -e START=1000 \ -e END=2000 \ -e STORAGE_EMULATOR_HOST=http://fake-gcs-server:4443 \ $(DOCKER_IMAGE):$(VERSION) - - $(SUDO) docker stop fake-gcs-server - $(SUDO) docker rm fake-gcs-server - $(SUDO) rm -rf ${PWD}/storage - $(SUDO) docker network rm test-network + + $(MAKE) docker-clean docker-push: $(SUDO) docker push $(DOCKER_IMAGE):$(VERSION) diff --git a/exp/services/ledgerexporter/README.md b/exp/services/ledgerexporter/README.md index ad8a3ddeae..5d483f0858 100644 --- a/exp/services/ledgerexporter/README.md +++ b/exp/services/ledgerexporter/README.md @@ -33,17 +33,24 @@ Exports ledgers continuously starting from --start. In this mode, the end ledger ledgerexporter --start --config-file ``` +#### Resumability: +Exporting a ledger range can be optimized further by enabling resumability if the remote data store supports it. -Starts exporting from a specified number of ledgers before the latest ledger sequence number on the network. -```bash -ledgerexporter --from-last --config-file -``` +By default, resumability is disabled, `--resume false` + +When enabled, `--resume true`, ledgerexporter will search the remote data store within the requested range, looking for the oldest absent ledger sequence number within range. If abscence is detected, the export range is narrowed to `--start `. +This feature requires all ledgers to be present on the remote data store for some (possibly empty) prefix of the requested range and then absent for the (possibly empty) remainder. ### Configuration (toml): ```toml network = "testnet" # Options: `testnet` or `pubnet` -destination_url = "gcs://your-bucket-name" + +[datastore_config] +type = "GCS" + +[datastore_config.params] +destination_bucket_path = "your-bucket-name/" [exporter_config] ledgers_per_file = 64 diff --git a/exp/services/ledgerexporter/config.toml b/exp/services/ledgerexporter/config.toml index 30c4a4fafe..a56087427c 100644 --- a/exp/services/ledgerexporter/config.toml +++ b/exp/services/ledgerexporter/config.toml @@ -1,7 +1,15 @@ network = "testnet" -destination_url = "gcs://exporter-test/ledgers" + +[datastore_config] +type = "GCS" + +[datastore_config.params] +destination_bucket_path = "exporter-test/ledgers" [exporter_config] ledgers_per_file = 1 files_per_partition = 64000 +[stellar_core_config] + stellar_core_binary_path = "/usr/local/bin/stellar-core" + diff --git a/exp/services/ledgerexporter/docker/start b/exp/services/ledgerexporter/docker/start index 8cca164dcd..eca213de0c 100644 --- a/exp/services/ledgerexporter/docker/start +++ b/exp/services/ledgerexporter/docker/start @@ -18,7 +18,12 @@ files_per_partition="${FILES_PER_PARTITION:-64000}" # Generate TOML configuration cat < config.toml network = "${NETWORK}" -destination_url = "${ARCHIVE_TARGET}" + +[datastore_config] +type = "GCS" + +[datastore_config.params] +destination_bucket_path = "${ARCHIVE_TARGET}" [exporter_config] ledgers_per_file = $ledgers_per_file @@ -29,10 +34,6 @@ EOF if [[ -n "$START" || -n "$END" ]]; then echo "START: $START END: $END" /usr/bin/ledgerexporter --config-file config.toml --start $START --end $END -# Check if FROM_LAST variable is set -elif [[ -n "$FROM_LAST" ]]; then - echo "FROM_LAST: $FROM_LAST" - /usr/bin/ledgerexporter --config-file config.toml --from-last $FROM_LAST else echo "Error: No ledger range provided." exit 1 diff --git a/exp/services/ledgerexporter/internal/app.go b/exp/services/ledgerexporter/internal/app.go index 8239b9c72e..588164582c 100644 --- a/exp/services/ledgerexporter/internal/app.go +++ b/exp/services/ledgerexporter/internal/app.go @@ -13,6 +13,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/stellar/go/historyarchive" "github.com/stellar/go/ingest/ledgerbackend" _ "github.com/stellar/go/network" @@ -24,41 +25,118 @@ var ( logger = log.New().WithField("service", "ledger-exporter") ) +func NewDataAlreadyExportedError(Start uint32, End uint32) *DataAlreadyExportedError { + return &DataAlreadyExportedError{ + Start: Start, + End: End, + } +} + +type DataAlreadyExportedError struct { + Start uint32 + End uint32 +} + +func (m DataAlreadyExportedError) Error() string { + return fmt.Sprintf("For export ledger range start=%d, end=%d, the remote storage has all the data, there is no need to continue export", m.Start, m.End) +} + +func NewInvalidDataStoreError(LedgerSequence uint32, LedgersPerFile uint32) *InvalidDataStoreError { + return &InvalidDataStoreError{ + LedgerSequence: LedgerSequence, + LedgersPerFile: LedgersPerFile, + } +} + +type InvalidDataStoreError struct { + LedgerSequence uint32 + LedgersPerFile uint32 +} + +func (m InvalidDataStoreError) Error() string { + return fmt.Sprintf("The remote data store has inconsistent data, "+ + "a resumable starting ledger of %v was identified, "+ + "but that is not aligned to expected ledgers-per-file of %v. use '--resume false' to bypass", + m.LedgerSequence, m.LedgersPerFile) +} + type App struct { - config Config + config *Config ledgerBackend ledgerbackend.LedgerBackend dataStore DataStore exportManager *ExportManager uploader Uploader + flags Flags prometheusRegistry *prometheus.Registry } -func NewApp() *App { +func NewApp(flags Flags) *App { logger.SetLevel(log.DebugLevel) - - config := Config{} - err := config.LoadConfig() - logFatalIf(err, "Could not load configuration") - - app := &App{config: config, prometheusRegistry: prometheus.NewRegistry()} - app.prometheusRegistry.MustRegister( + registry := prometheus.NewRegistry() + registry.MustRegister( collectors.NewProcessCollector(collectors.ProcessCollectorOpts{Namespace: "ledger_exporter"}), collectors.NewGoCollector(), ) + app := &App{flags: flags, prometheusRegistry: registry} return app } -func (a *App) init(ctx context.Context) { - a.dataStore = mustNewDataStore(ctx, a.config) - a.ledgerBackend = mustNewLedgerBackend(ctx, a.config, a.prometheusRegistry) +func (a *App) init(ctx context.Context) error { + var err error + var archive historyarchive.ArchiveInterface + + if a.config, err = NewConfig(ctx, a.flags); err != nil { + return errors.Wrap(err, "Could not load configuration") + } + if archive, err = createHistoryArchiveFromNetworkName(ctx, a.config.Network); err != nil { + return err + } + a.config.ValidateAndSetLedgerRange(ctx, archive) + + if a.dataStore, err = NewDataStore(ctx, a.config.DataStoreConfig, a.config.Network); err != nil { + return errors.Wrap(err, "Could not connect to destination data store") + } + if a.config.Resume { + if err = a.applyResumability(ctx, + NewResumableManager(a.dataStore, a.config.Network, a.config.LedgerBatchConfig, archive)); err != nil { + return err + } + } + + logger.Infof("Final computed ledger range for backend retrieval and export, start=%d, end=%d", a.config.StartLedger, a.config.EndLedger) + + if a.ledgerBackend, err = newLedgerBackend(ctx, a.config, a.prometheusRegistry); err != nil { + return err + } + // TODO: make number of upload workers configurable instead of hard coding it to 1 queue := NewUploadQueue(1, a.prometheusRegistry) - a.exportManager = NewExportManager(a.config.ExporterConfig, a.ledgerBackend, queue, a.prometheusRegistry) - a.uploader = NewUploader( - a.dataStore, - queue, - a.prometheusRegistry, - ) + if a.exportManager, err = NewExportManager(a.config.LedgerBatchConfig, a.ledgerBackend, queue, a.prometheusRegistry); err != nil { + return err + } + a.uploader = NewUploader(a.dataStore, queue, a.prometheusRegistry) + + return nil +} + +func (a *App) applyResumability(ctx context.Context, resumableManager ResumableManager) error { + absentLedger, ok, err := resumableManager.FindStart(ctx, a.config.StartLedger, a.config.EndLedger) + if err != nil { + return err + } + if !ok { + return NewDataAlreadyExportedError(a.config.StartLedger, a.config.EndLedger) + } + + // TODO - evaluate a more robust validation of remote data for ledgers-per-file consistency + // this assumes ValidateAndSetLedgerRange() has conditioned the a.config.StartLedger to be at least > 1 + if absentLedger > 2 && absentLedger != a.config.LedgerBatchConfig.GetSequenceNumberStartBoundary(absentLedger) { + return NewInvalidDataStoreError(absentLedger, a.config.LedgerBatchConfig.LedgersPerFile) + } + logger.Infof("For export ledger range start=%d, end=%d, the remote storage has some of this data already, will resume at later start ledger of %d", a.config.StartLedger, a.config.EndLedger, absentLedger) + a.config.StartLedger = absentLedger + + return nil } func (a *App) close() { @@ -92,7 +170,15 @@ func (a *App) Run() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - a.init(ctx) + if err := a.init(ctx); err != nil { + var dataAlreadyExported DataAlreadyExportedError + if errors.As(err, &dataAlreadyExported) { + logger.Info(err.Error()) + logger.Info("Shutting down ledger-exporter") + return + } + logger.WithError(err).Fatal("Stopping ledger-exporter") + } defer a.close() var wg sync.WaitGroup @@ -133,22 +219,20 @@ func (a *App) Run() { logger.Info("Shutting down ledger-exporter") } -func mustNewDataStore(ctx context.Context, config Config) DataStore { - dataStore, err := NewDataStore(ctx, fmt.Sprintf("%s/%s", config.DestinationURL, config.Network)) - logFatalIf(err, "Could not connect to destination data store") - return dataStore -} - -// mustNewLedgerBackend Creates and initializes captive core ledger backend +// newLedgerBackend Creates and initializes captive core ledger backend // Currently, only supports captive-core as ledger backend -func mustNewLedgerBackend(ctx context.Context, config Config, prometheusRegistry *prometheus.Registry) ledgerbackend.LedgerBackend { - captiveConfig := config.GenerateCaptiveCoreConfig() +func newLedgerBackend(ctx context.Context, config *Config, prometheusRegistry *prometheus.Registry) (ledgerbackend.LedgerBackend, error) { + captiveConfig, err := config.GenerateCaptiveCoreConfig() + if err != nil { + return nil, err + } var backend ledgerbackend.LedgerBackend - var err error // Create a new captive core backend backend, err = ledgerbackend.NewCaptive(captiveConfig) - logFatalIf(err, "Failed to create captive-core instance") + if err != nil { + return nil, errors.Wrap(err, "Failed to create captive-core instance") + } backend = ledgerbackend.WithMetrics(backend, prometheusRegistry, "ledger_exporter") var ledgerRange ledgerbackend.Range @@ -158,13 +242,8 @@ func mustNewLedgerBackend(ctx context.Context, config Config, prometheusRegistry ledgerRange = ledgerbackend.BoundedRange(config.StartLedger, config.EndLedger) } - err = backend.PrepareRange(ctx, ledgerRange) - logFatalIf(err, "Could not prepare captive core ledger backend") - return backend -} - -func logFatalIf(err error, message string, args ...interface{}) { - if err != nil { - logger.WithError(err).Fatalf(message, args...) + if err = backend.PrepareRange(ctx, ledgerRange); err != nil { + return nil, errors.Wrap(err, "Could not prepare captive core ledger backend") } + return backend, nil } diff --git a/exp/services/ledgerexporter/internal/app_test.go b/exp/services/ledgerexporter/internal/app_test.go new file mode 100644 index 0000000000..fc63c3a387 --- /dev/null +++ b/exp/services/ledgerexporter/internal/app_test.go @@ -0,0 +1,114 @@ +package ledgerexporter + +import ( + "context" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +func TestApplyResumeHasStartError(t *testing.T) { + ctx := context.Background() + app := &App{} + app.config = &Config{StartLedger: 10, EndLedger: 19, Resume: true} + mockResumableManager := &MockResumableManager{} + mockResumableManager.On("FindStart", ctx, uint32(10), uint32(19)).Return(uint32(0), false, errors.New("start error")).Once() + + err := app.applyResumability(ctx, mockResumableManager) + require.ErrorContains(t, err, "start error") + mockResumableManager.AssertExpectations(t) +} + +func TestApplyResumeDatastoreComplete(t *testing.T) { + ctx := context.Background() + app := &App{} + app.config = &Config{StartLedger: 10, EndLedger: 19, Resume: true} + mockResumableManager := &MockResumableManager{} + mockResumableManager.On("FindStart", ctx, uint32(10), uint32(19)).Return(uint32(0), false, nil).Once() + + var alreadyExported *DataAlreadyExportedError + err := app.applyResumability(ctx, mockResumableManager) + require.ErrorAs(t, err, &alreadyExported) + mockResumableManager.AssertExpectations(t) +} + +func TestApplyResumeInvalidDataStoreLedgersPerFileBoundary(t *testing.T) { + ctx := context.Background() + app := &App{} + app.config = &Config{ + StartLedger: 3, + EndLedger: 9, + Resume: true, + LedgerBatchConfig: LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + } + mockResumableManager := &MockResumableManager{} + // simulate the datastore has inconsistent data, + // with last ledger not aligned to starting boundary + mockResumableManager.On("FindStart", ctx, uint32(3), uint32(9)).Return(uint32(6), true, nil).Once() + + var invalidStore *InvalidDataStoreError + err := app.applyResumability(ctx, mockResumableManager) + require.ErrorAs(t, err, &invalidStore) + mockResumableManager.AssertExpectations(t) +} + +func TestApplyResumeWithPartialRemoteDataPresent(t *testing.T) { + ctx := context.Background() + app := &App{} + app.config = &Config{ + StartLedger: 10, + EndLedger: 99, + Resume: true, + LedgerBatchConfig: LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + } + mockResumableManager := &MockResumableManager{} + // simulates a data store that had ledger files populated up to seq=49, so the first absent ledger would be 50 + mockResumableManager.On("FindStart", ctx, uint32(10), uint32(99)).Return(uint32(50), true, nil).Once() + + err := app.applyResumability(ctx, mockResumableManager) + require.NoError(t, err) + require.Equal(t, app.config.StartLedger, uint32(50)) + mockResumableManager.AssertExpectations(t) +} + +func TestApplyResumeWithNoRemoteDataPresent(t *testing.T) { + ctx := context.Background() + app := &App{} + app.config = &Config{ + StartLedger: 10, + EndLedger: 99, + Resume: true, + LedgerBatchConfig: LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + } + mockResumableManager := &MockResumableManager{} + // simulates a data store that had no data in the requested range + mockResumableManager.On("FindStart", ctx, uint32(10), uint32(99)).Return(uint32(2), true, nil).Once() + + err := app.applyResumability(ctx, mockResumableManager) + require.NoError(t, err) + require.Equal(t, app.config.StartLedger, uint32(2)) + mockResumableManager.AssertExpectations(t) +} + +func TestApplyResumeWithNoRemoteDataAndRequestFromGenesis(t *testing.T) { + // app will coerce config.StartLedger values less than 2 to a min of 2 before applying resumability FindStart + // app will validate the response from FindStart to ensure datastore is ledgers-per-file aligned + // config.StartLedger=2 is a special genesis case that shouldn't trigger ledgers-per-file validation error + ctx := context.Background() + app := &App{} + app.config = &Config{ + StartLedger: 2, + EndLedger: 99, + Resume: true, + LedgerBatchConfig: LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + } + mockResumableManager := &MockResumableManager{} + // simulates a data store that had no data in the requested range + mockResumableManager.On("FindStart", ctx, uint32(2), uint32(99)).Return(uint32(2), true, nil).Once() + + err := app.applyResumability(ctx, mockResumableManager) + require.NoError(t, err) + require.Equal(t, app.config.StartLedger, uint32(2)) + mockResumableManager.AssertExpectations(t) +} diff --git a/exp/services/ledgerexporter/internal/config.go b/exp/services/ledgerexporter/internal/config.go index 17e76a5d68..0d3cb87cb4 100644 --- a/exp/services/ledgerexporter/internal/config.go +++ b/exp/services/ledgerexporter/internal/config.go @@ -1,8 +1,8 @@ package ledgerexporter import ( + "context" _ "embed" - "flag" "os/exec" "github.com/stellar/go/historyarchive" @@ -13,11 +13,20 @@ import ( "github.com/stellar/go/support/errors" "github.com/stellar/go/support/ordered" + "github.com/stellar/go/support/storage" ) const Pubnet = "pubnet" const Testnet = "testnet" +type Flags struct { + StartLedger uint32 + EndLedger uint32 + ConfigFilePath string + Resume bool + AdminPort uint +} + type StellarCoreConfig struct { NetworkPassphrase string `toml:"network_passphrase"` HistoryArchiveUrls []string `toml:"history_archive_urls"` @@ -25,94 +34,97 @@ type StellarCoreConfig struct { CaptiveCoreTomlPath string `toml:"captive_core_toml_path"` } +type DataStoreConfig struct { + Type string `toml:"type"` + Params map[string]string `toml:"params"` +} + type Config struct { AdminPort int `toml:"admin_port"` Network string `toml:"network"` - DestinationURL string `toml:"destination_url"` - ExporterConfig ExporterConfig `toml:"exporter_config"` + DataStoreConfig DataStoreConfig `toml:"datastore_config"` + LedgerBatchConfig LedgerBatchConfig `toml:"exporter_config"` StellarCoreConfig StellarCoreConfig `toml:"stellar_core_config"` - //From command-line - StartLedger uint32 `toml:"start"` - EndLedger uint32 `toml:"end"` - StartFromLastLedgers uint32 `toml:"from-last"` + StartLedger uint32 + EndLedger uint32 + Resume bool } -func (config *Config) LoadConfig() error { - // Parse command-line options - startLedger := flag.Uint("start", 0, "Starting ledger") - endLedger := flag.Uint("end", 0, "Ending ledger (inclusive)") - startFromLastNLedger := flag.Uint("from-last", 0, "Start streaming from last N ledgers") - adminPort := flag.Int("admin-port", 0, "Admin HTTP port for prometheus metrics") - - configFilePath := flag.String("config-file", "config.toml", "Path to the TOML config file") - flag.Parse() - - config.StartLedger = uint32(*startLedger) - config.EndLedger = uint32(*endLedger) - config.StartFromLastLedgers = uint32(*startFromLastNLedger) - config.AdminPort = *adminPort - - // Load config TOML file - cfg, err := toml.LoadFile(*configFilePath) - if err != nil { - return err - } - - // Unmarshal TOML data into the Config struct - err = cfg.Unmarshal(config) - logFatalIf(err, "Error unmarshalling TOML config.") - logger.Infof("Config: %v", *config) - +func createHistoryArchiveFromNetworkName(ctx context.Context, networkName string) (historyarchive.ArchiveInterface, error) { var historyArchiveUrls []string - switch config.Network { + switch networkName { case Pubnet: historyArchiveUrls = network.PublicNetworkhistoryArchiveURLs case Testnet: historyArchiveUrls = network.TestNetworkhistoryArchiveURLs default: - logger.Fatalf("Invalid network %s", config.Network) + return nil, errors.Errorf("Invalid network name %s", networkName) } - // Retrieve the latest ledger sequence from history archives - latestNetworkLedger, err := getLatestLedgerSequenceFromHistoryArchives(historyArchiveUrls) - logFatalIf(err, "Failed to retrieve the latest ledger sequence from history archives.") + return historyarchive.NewArchivePool(historyArchiveUrls, historyarchive.ArchiveOptions{ + ConnectOptions: storage.ConnectOptions{ + UserAgent: "ledger-exporter", + Context: ctx, + }, + }) +} - // Validate config params - err = config.validateAndSetLedgerRange(latestNetworkLedger) - logFatalIf(err, "Error validating config params.") +func getLatestLedgerSequenceFromHistoryArchives(archive historyarchive.ArchiveInterface) (uint32, error) { + has, err := archive.GetRootHAS() + if err != nil { + logger.WithError(err).Warnf("Error getting root HAS from archives") + return 0, errors.Wrap(err, "failed to retrieve the latest ledger sequence from any history archive") + } - // Validate and build the appropriate range - // TODO: Make it configurable - config.adjustLedgerRange() + return has.CurrentLedger, nil +} - return nil +func getHistoryArchivesCheckPointFrequency() uint32 { + // this could evolve to use other sources for checkpoint freq + return historyarchive.DefaultCheckpointFrequency } -func (config *Config) validateAndSetLedgerRange(latestNetworkLedger uint32) error { - if config.StartFromLastLedgers > 0 && (config.StartLedger > 0 || config.EndLedger > 0) { - return errors.New("--from-last cannot be used with --start or --end") +// This will generate the config based on commandline flags and toml +// +// ctx - the caller context +// flags - command line flags +// +// return - *Config or an error if any range validation failed. +func NewConfig(ctx context.Context, flags Flags) (*Config, error) { + config := &Config{} + + config.StartLedger = uint32(flags.StartLedger) + config.EndLedger = uint32(flags.EndLedger) + config.Resume = flags.Resume + + logger.Infof("Requested ledger range start=%d, end=%d, resume=%v", config.StartLedger, config.EndLedger, config.Resume) + + var err error + if err = config.processToml(flags.ConfigFilePath); err != nil { + return nil, err } + logger.Infof("Config: %v", *config) - if config.StartFromLastLedgers > 0 { - if config.StartFromLastLedgers > latestNetworkLedger { - return errors.Errorf("--from-last %d exceeds latest network ledger %d", - config.StartLedger, latestNetworkLedger) - } - config.StartLedger = latestNetworkLedger - config.StartFromLastLedgers - logger.Infof("Setting start ledger to %d, calculated as latest ledger (%d) minus --from-last value (%d)", - config.StartLedger, latestNetworkLedger, config.StartFromLastLedgers) + return config, nil +} + +// Validates requested ledger range, and will automatically adjust it +// to be ledgers-per-file boundary aligned +func (config *Config) ValidateAndSetLedgerRange(ctx context.Context, archive historyarchive.ArchiveInterface) error { + latestNetworkLedger, err := getLatestLedgerSequenceFromHistoryArchives(archive) + + if err != nil { + return errors.Wrap(err, "Failed to retrieve the latest ledger sequence from history archives.") } + logger.Infof("Latest %v ledger sequence was detected as %d", config.Network, latestNetworkLedger) if config.StartLedger > latestNetworkLedger { return errors.Errorf("--start %d exceeds latest network ledger %d", config.StartLedger, latestNetworkLedger) } - // Ensure that the start ledger is at least 2. - config.StartLedger = ordered.Max(2, config.StartLedger) - if config.EndLedger != 0 { // Bounded mode if config.EndLedger < config.StartLedger { return errors.New("invalid --end value, must be >= --start") @@ -123,39 +135,19 @@ func (config *Config) validateAndSetLedgerRange(latestNetworkLedger uint32) erro } } + config.adjustLedgerRange() return nil } -func (config *Config) adjustLedgerRange() { - logger.Infof("Requested ledger range start=%d, end=%d", config.StartLedger, config.EndLedger) - - // Check if either the start or end ledger does not fall on the "LedgersPerFile" boundary - // and adjust the start and end ledger accordingly. - // Align the start ledger to the nearest "LedgersPerFile" boundary. - config.StartLedger = config.StartLedger / config.ExporterConfig.LedgersPerFile * config.ExporterConfig.LedgersPerFile - - // Ensure that the adjusted start ledger is at least 2. - config.StartLedger = ordered.Max(2, config.StartLedger) - - // Align the end ledger (for bounded cases) to the nearest "LedgersPerFile" boundary. - if config.EndLedger != 0 { - // Add an extra batch only if "LedgersPerFile" is greater than 1 and the end ledger doesn't fall on the boundary. - if config.ExporterConfig.LedgersPerFile > 1 && config.EndLedger%config.ExporterConfig.LedgersPerFile != 0 { - config.EndLedger = (config.EndLedger/config.ExporterConfig.LedgersPerFile + 1) * config.ExporterConfig.LedgersPerFile - } - } - - logger.Infof("Adjusted ledger range: start=%d, end=%d", config.StartLedger, config.EndLedger) -} - -func (config *Config) GenerateCaptiveCoreConfig() ledgerbackend.CaptiveCoreConfig { +func (config *Config) GenerateCaptiveCoreConfig() (ledgerbackend.CaptiveCoreConfig, error) { coreConfig := &config.StellarCoreConfig // Look for stellar-core binary in $PATH, if not supplied if coreConfig.StellarCoreBinaryPath == "" { var err error - coreConfig.StellarCoreBinaryPath, err = exec.LookPath("stellar-core") - logFatalIf(err, "Failed to find stellar-core binary") + if coreConfig.StellarCoreBinaryPath, err = exec.LookPath("stellar-core"); err != nil { + return ledgerbackend.CaptiveCoreConfig{}, errors.Wrap(err, "Failed to find stellar-core binary") + } } var captiveCoreConfig []byte @@ -182,16 +174,58 @@ func (config *Config) GenerateCaptiveCoreConfig() ledgerbackend.CaptiveCoreConfi } captiveCoreToml, err := ledgerbackend.NewCaptiveCoreTomlFromData(captiveCoreConfig, params) - logFatalIf(err, "Failed to create captive-core toml") + if err != nil { + return ledgerbackend.CaptiveCoreConfig{}, errors.Wrap(err, "Failed to create captive-core toml") + } return ledgerbackend.CaptiveCoreConfig{ BinaryPath: coreConfig.StellarCoreBinaryPath, NetworkPassphrase: params.NetworkPassphrase, HistoryArchiveURLs: params.HistoryArchiveURLs, - CheckpointFrequency: historyarchive.DefaultCheckpointFrequency, + CheckpointFrequency: getHistoryArchivesCheckPointFrequency(), Log: logger.WithField("subservice", "stellar-core"), Toml: captiveCoreToml, UserAgent: "ledger-exporter", UseDB: true, + }, nil +} + +func (config *Config) processToml(tomlPath string) error { + // Load config TOML file + cfg, err := toml.LoadFile(tomlPath) + if err != nil { + return err + } + + // Unmarshal TOML data into the Config struct + if err := cfg.Unmarshal(config); err != nil { + return errors.Wrap(err, "Error unmarshalling TOML config.") } + + // validate TOML data + if config.Network == "" { + return errors.New("Invalid TOML config, 'network' must be set, supported values are 'testnet' or 'pubnet'") + } + return nil +} + +func (config *Config) adjustLedgerRange() { + + // Check if either the start or end ledger does not fall on the "LedgersPerFile" boundary + // and adjust the start and end ledger accordingly. + // Align the start ledger to the nearest "LedgersPerFile" boundary. + config.StartLedger = config.LedgerBatchConfig.GetSequenceNumberStartBoundary(config.StartLedger) + + // Ensure that the adjusted start ledger is at least 2. + config.StartLedger = ordered.Max(2, config.StartLedger) + + // Align the end ledger (for bounded cases) to the nearest "LedgersPerFile" boundary. + if config.EndLedger != 0 { + // Add an extra batch only if "LedgersPerFile" is greater than 1 and the end ledger doesn't fall on the boundary. + if config.LedgerBatchConfig.LedgersPerFile > 1 && config.EndLedger%config.LedgerBatchConfig.LedgersPerFile != 0 { + config.EndLedger = (config.EndLedger/config.LedgerBatchConfig.LedgersPerFile + 1) * config.LedgerBatchConfig.LedgersPerFile + } + } + + logger.Infof("Computed effective export boundary ledger range: start=%d, end=%d", config.StartLedger, config.EndLedger) } diff --git a/exp/services/ledgerexporter/internal/config_test.go b/exp/services/ledgerexporter/internal/config_test.go index 6a320c7424..86f6cfb5b3 100644 --- a/exp/services/ledgerexporter/internal/config_test.go +++ b/exp/services/ledgerexporter/internal/config_test.go @@ -1,20 +1,57 @@ package ledgerexporter import ( + "context" "fmt" "testing" + "github.com/stellar/go/historyarchive" "github.com/stretchr/testify/require" ) +func TestNewConfigResumeEnabled(t *testing.T) { + ctx := context.Background() + + mockArchive := &historyarchive.MockArchive{} + mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: 5}, nil).Once() + + config, err := NewConfig(ctx, + Flags{StartLedger: 1, EndLedger: 2, ConfigFilePath: "test/test.toml", Resume: true}) + config.ValidateAndSetLedgerRange(ctx, mockArchive) + require.NoError(t, err) + require.Equal(t, config.DataStoreConfig.Type, "ABC") + require.Equal(t, config.LedgerBatchConfig.FilesPerPartition, uint32(1)) + require.Equal(t, config.LedgerBatchConfig.LedgersPerFile, uint32(3)) + require.True(t, config.Resume) + url, ok := config.DataStoreConfig.Params["destination_bucket_path"] + require.True(t, ok) + require.Equal(t, url, "your-bucket-name/subpath") +} + +func TestNewConfigResumeDisabled(t *testing.T) { + ctx := context.Background() + + mockArchive := &historyarchive.MockArchive{} + mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: 5}, nil).Once() + + // resume disabled by default + config, err := NewConfig(ctx, + Flags{StartLedger: 1, EndLedger: 2, ConfigFilePath: "test/test.toml"}) + require.NoError(t, err) + require.False(t, config.Resume) +} + +func TestInvalidTomlConfig(t *testing.T) { + ctx := context.Background() + + _, err := NewConfig(ctx, + Flags{StartLedger: 1, EndLedger: 2, ConfigFilePath: "test/no_network.toml", Resume: true}) + require.ErrorContains(t, err, "Invalid TOML config") +} + func TestValidateStartAndEndLedger(t *testing.T) { - const latestNetworkLedger = 20000 + const latestNetworkLedger = uint32(20000) - config := &Config{ - ExporterConfig: ExporterConfig{ - LedgersPerFile: 1, - }, - } tests := []struct { name string startLedger uint32 @@ -67,104 +104,166 @@ func TestValidateStartAndEndLedger(t *testing.T) { }, } + ctx := context.Background() + mockArchive := &historyarchive.MockArchive{} + mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: latestNetworkLedger}, nil).Times(len(tests)) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - config.StartLedger = tt.startLedger - config.EndLedger = tt.endLedger + config, err := NewConfig(ctx, + Flags{StartLedger: tt.startLedger, EndLedger: tt.endLedger, ConfigFilePath: "test/validate_start_end.toml"}) + require.NoError(t, err) + err = config.ValidateAndSetLedgerRange(ctx, mockArchive) if tt.errMsg != "" { - require.Equal(t, tt.errMsg, config.validateAndSetLedgerRange(latestNetworkLedger).Error()) + require.Equal(t, tt.errMsg, err.Error()) } else { - require.NoError(t, config.validateAndSetLedgerRange(latestNetworkLedger)) + require.NoError(t, err) } }) } } -func TestAdjustLedgerRangeBoundedMode(t *testing.T) { +func TestAdjustedLedgerRangeBoundedMode(t *testing.T) { tests := []struct { - name string - config *Config - expected *Config + name string + configFile string + start uint32 + end uint32 + expectedStart uint32 + expectedEnd uint32 }{ { - name: "Min start ledger 2", - config: &Config{StartLedger: 0, EndLedger: 10, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, - expected: &Config{StartLedger: 2, EndLedger: 10, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, + name: "Min start ledger 2", + configFile: "test/1perfile.toml", + start: 0, + end: 10, + expectedStart: 2, + expectedEnd: 10, }, { - name: "No change, 1 ledger per file", - config: &Config{StartLedger: 2, EndLedger: 2, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, - expected: &Config{StartLedger: 2, EndLedger: 2, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, + name: "No change, 1 ledger per file", + configFile: "test/1perfile.toml", + start: 2, + end: 2, + expectedStart: 2, + expectedEnd: 2, }, { - name: "Min start ledger2, round up end ledger, 10 ledgers per file", - config: &Config{StartLedger: 0, EndLedger: 1, ExporterConfig: ExporterConfig{LedgersPerFile: 10}}, - expected: &Config{StartLedger: 2, EndLedger: 10, ExporterConfig: ExporterConfig{LedgersPerFile: 10}}, + name: "Min start ledger2, round up end ledger, 10 ledgers per file", + configFile: "test/10perfile.toml", + start: 0, + end: 1, + expectedStart: 2, + expectedEnd: 10, }, { - name: "Round down start ledger and round up end ledger, 15 ledgers per file ", - config: &Config{StartLedger: 4, EndLedger: 10, ExporterConfig: ExporterConfig{LedgersPerFile: 15}}, - expected: &Config{StartLedger: 2, EndLedger: 15, ExporterConfig: ExporterConfig{LedgersPerFile: 15}}, + name: "Round down start ledger and round up end ledger, 15 ledgers per file ", + configFile: "test/15perfile.toml", + start: 4, + end: 10, + expectedStart: 2, + expectedEnd: 15, }, { - name: "Round down start ledger and round up end ledger, 64 ledgers per file ", - config: &Config{StartLedger: 400, EndLedger: 500, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, - expected: &Config{StartLedger: 384, EndLedger: 512, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, + name: "Round down start ledger and round up end ledger, 64 ledgers per file ", + configFile: "test/64perfile.toml", + start: 400, + end: 500, + expectedStart: 384, + expectedEnd: 512, }, { - name: "No change, 64 ledger per file", - config: &Config{StartLedger: 64, EndLedger: 128, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, - expected: &Config{StartLedger: 64, EndLedger: 128, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, + name: "No change, 64 ledger per file", + configFile: "test/64perfile.toml", + start: 64, + end: 128, + expectedStart: 64, + expectedEnd: 128, }, } + ctx := context.Background() + mockArchive := &historyarchive.MockArchive{} + mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: 500}, nil).Times(len(tests)) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.config.adjustLedgerRange() - require.EqualValues(t, tt.expected.StartLedger, tt.config.StartLedger) - require.EqualValues(t, tt.expected.EndLedger, tt.config.EndLedger) + config, err := NewConfig(ctx, + Flags{StartLedger: tt.start, EndLedger: tt.end, ConfigFilePath: tt.configFile}) + require.NoError(t, err) + err = config.ValidateAndSetLedgerRange(ctx, mockArchive) + require.NoError(t, err) + require.EqualValues(t, tt.expectedStart, config.StartLedger) + require.EqualValues(t, tt.expectedEnd, config.EndLedger) }) } } -func TestAdjustLedgerRangeUnBoundedMode(t *testing.T) { +func TestAdjustedLedgerRangeUnBoundedMode(t *testing.T) { tests := []struct { - name string - config *Config - expected *Config + name string + configFile string + start uint32 + end uint32 + expectedStart uint32 + expectedEnd uint32 }{ { - name: "Min start ledger 2", - config: &Config{StartLedger: 0, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, - expected: &Config{StartLedger: 2, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, + name: "Min start ledger 2", + configFile: "test/1perfile.toml", + start: 0, + end: 0, + expectedStart: 2, + expectedEnd: 0, }, { - name: "No change, 1 ledger per file", - config: &Config{StartLedger: 2, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, - expected: &Config{StartLedger: 2, ExporterConfig: ExporterConfig{LedgersPerFile: 1}}, + name: "No change, 1 ledger per file", + configFile: "test/1perfile.toml", + start: 2, + end: 0, + expectedStart: 2, + expectedEnd: 0, }, { - name: "Round down start ledger, 15 ledgers per file ", - config: &Config{StartLedger: 4, ExporterConfig: ExporterConfig{LedgersPerFile: 15}}, - expected: &Config{StartLedger: 2, ExporterConfig: ExporterConfig{LedgersPerFile: 15}}, + name: "Round down start ledger, 15 ledgers per file ", + configFile: "test/15perfile.toml", + start: 4, + end: 0, + expectedStart: 2, + expectedEnd: 0, }, { - name: "Round down start ledger, 64 ledgers per file ", - config: &Config{StartLedger: 400, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, - expected: &Config{StartLedger: 384, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, + name: "Round down start ledger, 64 ledgers per file ", + configFile: "test/64perfile.toml", + start: 400, + end: 0, + expectedStart: 384, + expectedEnd: 0, }, { - name: "No change, 64 ledger per file", - config: &Config{StartLedger: 64, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, - expected: &Config{StartLedger: 64, ExporterConfig: ExporterConfig{LedgersPerFile: 64}}, + name: "No change, 64 ledger per file", + configFile: "test/64perfile.toml", + start: 64, + end: 0, + expectedStart: 64, + expectedEnd: 0, }, } + ctx := context.Background() + + mockArchive := &historyarchive.MockArchive{} + mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: 500}, nil).Times(len(tests)) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tt.config.adjustLedgerRange() - require.EqualValues(t, int(tt.expected.StartLedger), int(tt.config.StartLedger)) - require.EqualValues(t, int(tt.expected.EndLedger), int(tt.config.EndLedger)) + config, err := NewConfig(ctx, + Flags{StartLedger: tt.start, EndLedger: tt.end, ConfigFilePath: tt.configFile}) + require.NoError(t, err) + err = config.ValidateAndSetLedgerRange(ctx, mockArchive) + require.NoError(t, err) + require.EqualValues(t, tt.expectedStart, config.StartLedger) + require.EqualValues(t, tt.expectedEnd, config.EndLedger) }) } } diff --git a/exp/services/ledgerexporter/internal/datastore.go b/exp/services/ledgerexporter/internal/datastore.go index a53d97465a..0529ba2290 100644 --- a/exp/services/ledgerexporter/internal/datastore.go +++ b/exp/services/ledgerexporter/internal/datastore.go @@ -3,13 +3,8 @@ package ledgerexporter import ( "context" "io" - "strings" - - "cloud.google.com/go/storage" - "google.golang.org/api/option" "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/url" ) // DataStore defines an interface for interacting with data storage @@ -22,37 +17,12 @@ type DataStore interface { Close() error } -// NewDataStore creates a new DataStore based on the destination URL. -// Currently, only accepts GCS URLs. -func NewDataStore(ctx context.Context, destinationURL string) (DataStore, error) { - parsed, err := url.Parse(destinationURL) - if err != nil { - return nil, err - } - - pth := parsed.Path - if parsed.Scheme != "gcs" { - return nil, errors.Errorf("Invalid destination URL %s. Expected GCS URL ", destinationURL) - } - - // Inside gcs, all paths start _without_ the leading / - pth = strings.TrimPrefix(pth, "/") - bucketName := parsed.Host - prefix := pth - - logger.Infof("creating GCS client for bucket: %s, prefix: %s", bucketName, prefix) - - var options []option.ClientOption - client, err := storage.NewClient(ctx, options...) - if err != nil { - return nil, err +// NewDataStore factory, it creates a new DataStore based on the config type +func NewDataStore(ctx context.Context, datastoreConfig DataStoreConfig, network string) (DataStore, error) { + switch datastoreConfig.Type { + case "GCS": + return NewGCSDataStore(ctx, datastoreConfig.Params, network) + default: + return nil, errors.Errorf("Invalid datastore type %v, not supported", datastoreConfig.Type) } - - // Check the bucket exists - bucket := client.Bucket(bucketName) - if _, err := bucket.Attrs(ctx); err != nil { - return nil, errors.Wrap(err, "failed to retrieve bucket attributes") - } - - return &GCSDataStore{client: client, bucket: bucket, prefix: prefix}, nil } diff --git a/exp/services/ledgerexporter/internal/datastore_test.go b/exp/services/ledgerexporter/internal/datastore_test.go new file mode 100644 index 0000000000..d3128a4cf3 --- /dev/null +++ b/exp/services/ledgerexporter/internal/datastore_test.go @@ -0,0 +1,13 @@ +package ledgerexporter + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestInvalidStore(t *testing.T) { + _, err := NewDataStore(context.Background(), DataStoreConfig{Type: "unknown"}, "test") + require.Error(t, err) +} diff --git a/exp/services/ledgerexporter/internal/encoder.go b/exp/services/ledgerexporter/internal/encoder.go new file mode 100644 index 0000000000..33909ace75 --- /dev/null +++ b/exp/services/ledgerexporter/internal/encoder.go @@ -0,0 +1,39 @@ +package ledgerexporter + +import ( + "compress/gzip" + "io" + + xdr3 "github.com/stellar/go-xdr/xdr3" +) + +type XDRGzipEncoder struct { + XdrPayload interface{} +} + +func (g *XDRGzipEncoder) WriteTo(w io.Writer) (int64, error) { + gw := gzip.NewWriter(w) + n, err := xdr3.Marshal(gw, g.XdrPayload) + if err != nil { + return int64(n), err + } + return int64(n), gw.Close() +} + +type XDRGzipDecoder struct { + XdrPayload interface{} +} + +func (d *XDRGzipDecoder) ReadFrom(r io.Reader) (int64, error) { + gr, err := gzip.NewReader(r) + if err != nil { + return 0, err + } + defer gr.Close() + + n, err := xdr3.Unmarshal(gr, d.XdrPayload) + if err != nil { + return int64(n), err + } + return int64(n), nil +} diff --git a/exp/services/ledgerexporter/internal/utils_test.go b/exp/services/ledgerexporter/internal/encoder_test.go similarity index 56% rename from exp/services/ledgerexporter/internal/utils_test.go rename to exp/services/ledgerexporter/internal/encoder_test.go index c11b500c21..2bf61bcee7 100644 --- a/exp/services/ledgerexporter/internal/utils_test.go +++ b/exp/services/ledgerexporter/internal/encoder_test.go @@ -2,50 +2,12 @@ package ledgerexporter import ( "bytes" - "fmt" "testing" "github.com/stellar/go/xdr" "github.com/stretchr/testify/require" ) -func TestGetObjectKeyFromSequenceNumber(t *testing.T) { - testCases := []struct { - filesPerPartition uint32 - ledgerSeq uint32 - ledgersPerFile uint32 - expectedKey string - expectedError bool - }{ - {0, 5, 1, "5.xdr.gz", false}, - {0, 5, 10, "0-9.xdr.gz", false}, - {2, 5, 0, "", true}, - {2, 10, 100, "0-199/0-99.xdr.gz", false}, - {2, 150, 50, "100-199/150-199.xdr.gz", false}, - {2, 300, 200, "0-399/200-399.xdr.gz", false}, - {2, 1, 1, "0-1/1.xdr.gz", false}, - {4, 10, 100, "0-399/0-99.xdr.gz", false}, - {4, 250, 50, "200-399/250-299.xdr.gz", false}, - {1, 300, 200, "200-399.xdr.gz", false}, - {1, 1, 1, "1.xdr.gz", false}, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("LedgerSeq-%d-LedgersPerFile-%d", tc.ledgerSeq, tc.ledgersPerFile), func(t *testing.T) { - config := ExporterConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile} - key, err := GetObjectKeyFromSequenceNumber(config, tc.ledgerSeq) - - if tc.expectedError { - require.Error(t, err) - require.Empty(t, key) - } else { - require.NoError(t, err) - require.Equal(t, tc.expectedKey, key) - } - }) - } -} - func createTestLedgerCloseMetaBatch(startSeq, endSeq uint32, count int) xdr.LedgerCloseMetaBatch { var ledgerCloseMetas []xdr.LedgerCloseMeta for i := 0; i < count; i++ { diff --git a/exp/services/ledgerexporter/internal/exportmanager.go b/exp/services/ledgerexporter/internal/exportmanager.go index a918a16e4e..51c3947506 100644 --- a/exp/services/ledgerexporter/internal/exportmanager.go +++ b/exp/services/ledgerexporter/internal/exportmanager.go @@ -11,14 +11,8 @@ import ( "github.com/stellar/go/xdr" ) -type ExporterConfig struct { - LedgersPerFile uint32 `toml:"ledgers_per_file"` - FilesPerPartition uint32 `toml:"files_per_partition"` -} - -// ExportManager manages the creation and handling of export objects. type ExportManager struct { - config ExporterConfig + config LedgerBatchConfig ledgerBackend ledgerbackend.LedgerBackend currentMetaArchive *LedgerMetaArchive queue UploadQueue @@ -26,7 +20,11 @@ type ExportManager struct { } // NewExportManager creates a new ExportManager with the provided configuration. -func NewExportManager(config ExporterConfig, backend ledgerbackend.LedgerBackend, queue UploadQueue, prometheusRegistry *prometheus.Registry) *ExportManager { +func NewExportManager(config LedgerBatchConfig, backend ledgerbackend.LedgerBackend, queue UploadQueue, prometheusRegistry *prometheus.Registry) (*ExportManager, error) { + if config.LedgersPerFile < 1 { + return nil, errors.Errorf("Invalid ledgers per file (%d): must be at least 1", config.LedgersPerFile) + } + latestLedgerMetric := prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: "ledger_exporter", Subsystem: "export_manager", Name: "latest_ledger", Help: "sequence number of the latest ledger consumed by the export manager", @@ -38,7 +36,7 @@ func NewExportManager(config ExporterConfig, backend ledgerbackend.LedgerBackend ledgerBackend: backend, queue: queue, latestLedgerMetric: latestLedgerMetric, - } + }, nil } // AddLedgerCloseMeta adds ledger metadata to the current export object @@ -46,10 +44,8 @@ func (e *ExportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta ledgerSeq := ledgerCloseMeta.LedgerSequence() // Determine the object key for the given ledger sequence - objectKey, err := GetObjectKeyFromSequenceNumber(e.config, ledgerSeq) - if err != nil { - return errors.Wrapf(err, "failed to get object key for ledger %d", ledgerSeq) - } + objectKey := e.config.GetObjectKeyFromSequenceNumber(ledgerSeq) + if e.currentMetaArchive != nil && e.currentMetaArchive.GetObjectKey() != objectKey { return errors.New("Current meta archive object key mismatch") } @@ -67,13 +63,13 @@ func (e *ExportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta e.currentMetaArchive = NewLedgerMetaArchive(objectKey, ledgerSeq, endSeq) } - if err = e.currentMetaArchive.AddLedger(ledgerCloseMeta); err != nil { + if err := e.currentMetaArchive.AddLedger(ledgerCloseMeta); err != nil { return errors.Wrapf(err, "failed to add ledger %d", ledgerSeq) } if ledgerSeq >= e.currentMetaArchive.GetEndLedgerSequence() { // Current archive is full, send it for upload - if err = e.queue.Enqueue(ctx, e.currentMetaArchive); err != nil { + if err := e.queue.Enqueue(ctx, e.currentMetaArchive); err != nil { return err } e.currentMetaArchive = nil diff --git a/exp/services/ledgerexporter/internal/exportmanager_test.go b/exp/services/ledgerexporter/internal/exportmanager_test.go index b082a8512e..5eaa38bdcb 100644 --- a/exp/services/ledgerexporter/internal/exportmanager_test.go +++ b/exp/services/ledgerexporter/internal/exportmanager_test.go @@ -2,6 +2,7 @@ package ledgerexporter import ( "context" + "fmt" "sync" "testing" "time" @@ -36,11 +37,20 @@ func (s *ExportManagerSuite) TearDownTest() { s.mockBackend.AssertExpectations(s.T()) } +func (s *ExportManagerSuite) TestInvalidExportConfig() { + config := LedgerBatchConfig{LedgersPerFile: 0, FilesPerPartition: 10} + registry := prometheus.NewRegistry() + queue := NewUploadQueue(1, registry) + _, err := NewExportManager(config, &s.mockBackend, queue, registry) + require.Error(s.T(), err) +} + func (s *ExportManagerSuite) TestRun() { - config := ExporterConfig{LedgersPerFile: 64, FilesPerPartition: 10} + config := LedgerBatchConfig{LedgersPerFile: 64, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - exporter := NewExportManager(config, &s.mockBackend, queue, registry) + exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) + require.NoError(s.T(), err) start := uint32(0) end := uint32(255) @@ -48,7 +58,7 @@ func (s *ExportManagerSuite) TestRun() { for i := start; i <= end; i++ { s.mockBackend.On("GetLedger", s.ctx, i). Return(createLedgerCloseMeta(i), nil) - key, _ := GetObjectKeyFromSequenceNumber(config, i) + key := config.GetObjectKeyFromSequenceNumber(i) expectedKeys.Add(key) } @@ -58,8 +68,8 @@ func (s *ExportManagerSuite) TestRun() { go func() { defer wg.Done() for { - v, ok, err := queue.Dequeue(s.ctx) - s.Assert().NoError(err) + v, ok, dqErr := queue.Dequeue(s.ctx) + s.Assert().NoError(dqErr) if !ok { break } @@ -67,7 +77,7 @@ func (s *ExportManagerSuite) TestRun() { } }() - err := exporter.Run(s.ctx, start, end) + err = exporter.Run(s.ctx, start, end) require.NoError(s.T(), err) wg.Wait() @@ -86,10 +96,11 @@ func (s *ExportManagerSuite) TestRun() { } func (s *ExportManagerSuite) TestRunContextCancel() { - config := ExporterConfig{LedgersPerFile: 1, FilesPerPartition: 1} + config := LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 1} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - exporter := NewExportManager(config, &s.mockBackend, queue, registry) + exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) + require.NoError(s.T(), err) ctx, cancel := context.WithCancel(context.Background()) s.mockBackend.On("GetLedger", mock.Anything, mock.Anything). @@ -102,33 +113,65 @@ func (s *ExportManagerSuite) TestRunContextCancel() { go func() { for i := 0; i < 127; i++ { - _, ok, err := queue.Dequeue(s.ctx) - s.Assert().NoError(err) + _, ok, dqErr := queue.Dequeue(s.ctx) + s.Assert().NoError(dqErr) s.Assert().True(ok) } }() - err := exporter.Run(ctx, 0, 255) + err = exporter.Run(ctx, 0, 255) require.EqualError(s.T(), err, "failed to add ledgerCloseMeta for ledger 128: context canceled") } func (s *ExportManagerSuite) TestRunWithCanceledContext() { - config := ExporterConfig{LedgersPerFile: 1, FilesPerPartition: 10} + config := LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - exporter := NewExportManager(config, &s.mockBackend, queue, registry) + exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) + require.NoError(s.T(), err) + ctx, cancel := context.WithCancel(context.Background()) cancel() - err := exporter.Run(ctx, 1, 10) + err = exporter.Run(ctx, 1, 10) require.EqualError(s.T(), err, "context canceled") } +func (s *ExportManagerSuite) TestGetObjectKeyFromSequenceNumber() { + testCases := []struct { + filesPerPartition uint32 + ledgerSeq uint32 + ledgersPerFile uint32 + expectedKey string + }{ + {0, 5, 1, "5.xdr.gz"}, + {0, 5, 10, "0-9.xdr.gz"}, + {2, 10, 100, "0-199/0-99.xdr.gz"}, + {2, 150, 50, "100-199/150-199.xdr.gz"}, + {2, 300, 200, "0-399/200-399.xdr.gz"}, + {2, 1, 1, "0-1/1.xdr.gz"}, + {4, 10, 100, "0-399/0-99.xdr.gz"}, + {4, 250, 50, "200-399/250-299.xdr.gz"}, + {1, 300, 200, "200-399.xdr.gz"}, + {1, 1, 1, "1.xdr.gz"}, + } + + for _, tc := range testCases { + s.T().Run(fmt.Sprintf("LedgerSeq-%d-LedgersPerFile-%d", tc.ledgerSeq, tc.ledgersPerFile), func(t *testing.T) { + config := LedgerBatchConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile} + key := config.GetObjectKeyFromSequenceNumber(tc.ledgerSeq) + require.Equal(t, tc.expectedKey, key) + }) + } +} + func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { - config := ExporterConfig{LedgersPerFile: 1, FilesPerPartition: 10} + config := LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - exporter := NewExportManager(config, &s.mockBackend, queue, registry) + exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) + require.NoError(s.T(), err) + expectedkeys := set.NewSet[string](10) actualKeys := set.NewSet[string](10) @@ -150,9 +193,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { end := uint32(255) for i := start; i <= end; i++ { require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(i))) - - key, err := GetObjectKeyFromSequenceNumber(config, i) - require.NoError(s.T(), err) + key := config.GetObjectKeyFromSequenceNumber(i) expectedkeys.Add(key) } @@ -162,10 +203,11 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { } func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { - config := ExporterConfig{LedgersPerFile: 1, FilesPerPartition: 10} + config := LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - exporter := NewExportManager(config, &s.mockBackend, queue, registry) + exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) + require.NoError(s.T(), err) ctx, cancel := context.WithCancel(context.Background()) go func() { @@ -174,15 +216,16 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { }() require.NoError(s.T(), exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(1))) - err := exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(2)) + err = exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(2)) require.EqualError(s.T(), err, "context canceled") } func (s *ExportManagerSuite) TestAddLedgerCloseMetaKeyMismatch() { - config := ExporterConfig{LedgersPerFile: 10, FilesPerPartition: 1} + config := LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 1} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - exporter := NewExportManager(config, &s.mockBackend, queue, registry) + exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) + require.NoError(s.T(), err) require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(16))) require.EqualError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(21)), diff --git a/exp/services/ledgerexporter/internal/gcs_datastore.go b/exp/services/ledgerexporter/internal/gcs_datastore.go index 927130fe6a..c68b7e7a5e 100644 --- a/exp/services/ledgerexporter/internal/gcs_datastore.go +++ b/exp/services/ledgerexporter/internal/gcs_datastore.go @@ -2,16 +2,20 @@ package ledgerexporter import ( "context" + "fmt" "io" "net/http" "os" "path" + "strings" "google.golang.org/api/googleapi" + "google.golang.org/api/option" "cloud.google.com/go/storage" "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/url" ) // GCSDataStore implements DataStore for GCS @@ -21,6 +25,41 @@ type GCSDataStore struct { prefix string } +func NewGCSDataStore(ctx context.Context, params map[string]string, network string) (DataStore, error) { + destinationBucketPath, ok := params["destination_bucket_path"] + if !ok { + return nil, errors.Errorf("Invalid GCS config, no destination_bucket_path") + } + + // append the gcs:// scheme to enable usage of the url package reliably to + // get parse bucket name which is first path segment as URL.Host + gcsBucketURL := fmt.Sprintf("gcs://%s/%s", destinationBucketPath, network) + parsed, err := url.Parse(gcsBucketURL) + if err != nil { + return nil, err + } + + // Inside gcs, all paths start _without_ the leading / + prefix := strings.TrimPrefix(parsed.Path, "/") + bucketName := parsed.Host + + logger.Infof("creating GCS client for bucket: %s, prefix: %s", bucketName, prefix) + + var options []option.ClientOption + client, err := storage.NewClient(ctx, options...) + if err != nil { + return nil, err + } + + // Check the bucket exists + bucket := client.Bucket(bucketName) + if _, err := bucket.Attrs(ctx); err != nil { + return nil, errors.Wrap(err, "failed to retrieve bucket attributes") + } + + return &GCSDataStore{client: client, bucket: bucket, prefix: prefix}, nil +} + // GetFile retrieves a file from the GCS bucket. func (b GCSDataStore) GetFile(ctx context.Context, filePath string) (io.ReadCloser, error) { filePath = path.Join(b.prefix, filePath) @@ -84,6 +123,11 @@ func (b GCSDataStore) Size(ctx context.Context, pth string) (int64, error) { // Exists checks if a file exists in the GCS bucket. func (b GCSDataStore) Exists(ctx context.Context, pth string) (bool, error) { _, err := b.Size(ctx, pth) + + if err == os.ErrNotExist { + return false, nil + } + return err == nil, err } diff --git a/exp/services/ledgerexporter/internal/ledgerbatch_config.go b/exp/services/ledgerexporter/internal/ledgerbatch_config.go new file mode 100644 index 0000000000..6ac3a3f36a --- /dev/null +++ b/exp/services/ledgerexporter/internal/ledgerbatch_config.go @@ -0,0 +1,49 @@ +package ledgerexporter + +import ( + "fmt" +) + +const ( + fileSuffix = ".xdr.gz" +) + +type LedgerBatchConfig struct { + LedgersPerFile uint32 `toml:"ledgers_per_file"` + FilesPerPartition uint32 `toml:"files_per_partition"` +} + +func (ec LedgerBatchConfig) GetSequenceNumberStartBoundary(ledgerSeq uint32) uint32 { + if ec.LedgersPerFile == 0 { + return 0 + } + return (ledgerSeq / ec.LedgersPerFile) * ec.LedgersPerFile +} + +func (ec LedgerBatchConfig) GetSequenceNumberEndBoundary(ledgerSeq uint32) uint32 { + return ec.GetSequenceNumberStartBoundary(ledgerSeq) + ec.LedgersPerFile - 1 +} + +// GetObjectKeyFromSequenceNumber generates the object key name from the ledger sequence number based on configuration. +func (ec LedgerBatchConfig) GetObjectKeyFromSequenceNumber(ledgerSeq uint32) string { + var objectKey string + + if ec.FilesPerPartition > 1 { + partitionSize := ec.LedgersPerFile * ec.FilesPerPartition + partitionStart := (ledgerSeq / partitionSize) * partitionSize + partitionEnd := partitionStart + partitionSize - 1 + objectKey = fmt.Sprintf("%d-%d/", partitionStart, partitionEnd) + } + + fileStart := ec.GetSequenceNumberStartBoundary(ledgerSeq) + fileEnd := ec.GetSequenceNumberEndBoundary(ledgerSeq) + objectKey += fmt.Sprintf("%d", fileStart) + + // Multiple ledgers per file + if fileStart != fileEnd { + objectKey += fmt.Sprintf("-%d", fileEnd) + } + objectKey += fileSuffix + + return objectKey +} diff --git a/exp/services/ledgerexporter/internal/ledgerbatch_config_test.go b/exp/services/ledgerexporter/internal/ledgerbatch_config_test.go new file mode 100644 index 0000000000..cad9249dc5 --- /dev/null +++ b/exp/services/ledgerexporter/internal/ledgerbatch_config_test.go @@ -0,0 +1,36 @@ +package ledgerexporter + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetObjectKeyFromSequenceNumber(t *testing.T) { + testCases := []struct { + filesPerPartition uint32 + ledgerSeq uint32 + ledgersPerFile uint32 + expectedKey string + }{ + {0, 5, 1, "5.xdr.gz"}, + {0, 5, 10, "0-9.xdr.gz"}, + {2, 10, 100, "0-199/0-99.xdr.gz"}, + {2, 150, 50, "100-199/150-199.xdr.gz"}, + {2, 300, 200, "0-399/200-399.xdr.gz"}, + {2, 1, 1, "0-1/1.xdr.gz"}, + {4, 10, 100, "0-399/0-99.xdr.gz"}, + {4, 250, 50, "200-399/250-299.xdr.gz"}, + {1, 300, 200, "200-399.xdr.gz"}, + {1, 1, 1, "1.xdr.gz"}, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("LedgerSeq-%d-LedgersPerFile-%d", tc.ledgerSeq, tc.ledgersPerFile), func(t *testing.T) { + config := LedgerBatchConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile} + key := config.GetObjectKeyFromSequenceNumber(tc.ledgerSeq) + require.Equal(t, tc.expectedKey, key) + }) + } +} diff --git a/exp/services/ledgerexporter/internal/mock_datastore.go b/exp/services/ledgerexporter/internal/mocks.go similarity index 70% rename from exp/services/ledgerexporter/internal/mock_datastore.go rename to exp/services/ledgerexporter/internal/mocks.go index 705df45a26..3f514a8f57 100644 --- a/exp/services/ledgerexporter/internal/mock_datastore.go +++ b/exp/services/ledgerexporter/internal/mocks.go @@ -34,10 +34,23 @@ func (m *MockDataStore) PutFile(ctx context.Context, path string, in io.WriterTo func (m *MockDataStore) PutFileIfNotExists(ctx context.Context, path string, in io.WriterTo) (bool, error) { args := m.Called(ctx, path, in) - return args.Bool(0), args.Error(1) + return args.Get(0).(bool), args.Error(1) } func (m *MockDataStore) Close() error { args := m.Called() return args.Error(0) } + +type MockResumableManager struct { + mock.Mock +} + +func (m *MockResumableManager) FindStart(ctx context.Context, start, end uint32) (absentLedger uint32, ok bool, err error) { + a := m.Called(ctx, start, end) + return a.Get(0).(uint32), a.Get(1).(bool), a.Error(2) +} + +// ensure that the MockClient implements ClientInterface +var _ DataStore = &MockDataStore{} +var _ ResumableManager = &MockResumableManager{} diff --git a/exp/services/ledgerexporter/internal/queue_test.go b/exp/services/ledgerexporter/internal/queue_test.go index 1d001765ce..a791d29eae 100644 --- a/exp/services/ledgerexporter/internal/queue_test.go +++ b/exp/services/ledgerexporter/internal/queue_test.go @@ -58,6 +58,6 @@ func TestQueue(t *testing.T) { l, ok, err = queue.Dequeue(context.Background()) require.NoError(t, err) - require.False(t, false) + require.False(t, ok) require.Nil(t, l) } diff --git a/exp/services/ledgerexporter/internal/resumablemanager.go b/exp/services/ledgerexporter/internal/resumablemanager.go new file mode 100644 index 0000000000..1d832f2ea4 --- /dev/null +++ b/exp/services/ledgerexporter/internal/resumablemanager.go @@ -0,0 +1,129 @@ +package ledgerexporter + +import ( + "context" + "sort" + + "github.com/pkg/errors" + "github.com/stellar/go/historyarchive" +) + +type ResumableManager interface { + // Given a requested ledger range, return the first absent ledger within the + // requested range of [start, end]. + // + // start - begin search inclusive from this ledger, must be greater than 0. + // end - stop search inclusive to this ledger. + // + // If start=0, invalid, error will be returned. + // + // If end=0, is provided as a convenience, to allow requesting an effectively + // dynamic end value for the range, which will be an approximation of the network's + // most recent checkpointed ledger + (2 * checkpoint_frequency). + // + // return: + // absentLedger - will be non-zero, the oldest ledger sequence between range of [start, end] + // which is not populated on data store. + // ok - if true, 'absentLedger' has a usable non-zero value, if false, there is no absent ledger in the requested range and 'absentLedger' is set to zero. + // err - the search was cancelled due to this unexpected error, 'absentLedger' and 'ok' return values should be ignored. + // + // When no error, the two return values will compose the following truth table: + // 1. datastore had no data in the requested range: absentLedger={start}, ok=true + // 2. datastore had partial data in the requested range: absentLedger={a value > start and <= end}, ok=true + // 3. datastore had all data in the requested range: absentLedger=0, ok=false + FindStart(ctx context.Context, start, end uint32) (absentLedger uint32, ok bool, err error) +} + +type resumableManagerService struct { + network string + ledgerBatchConfig LedgerBatchConfig + dataStore DataStore + archive historyarchive.ArchiveInterface +} + +func NewResumableManager(dataStore DataStore, + network string, + ledgerBatchConfig LedgerBatchConfig, + archive historyarchive.ArchiveInterface) ResumableManager { + return &resumableManagerService{ + ledgerBatchConfig: ledgerBatchConfig, + network: network, + dataStore: dataStore, + archive: archive, + } +} + +func (rm resumableManagerService) FindStart(ctx context.Context, start, end uint32) (absentLedger uint32, ok bool, err error) { + if start < 1 { + return 0, false, errors.New("Invalid start value, must be greater than zero") + } + + log := logger.WithField("start", start).WithField("end", end).WithField("network", rm.network) + + networkLatest := uint32(0) + if end < 1 { + var latestErr error + networkLatest, latestErr = getLatestLedgerSequenceFromHistoryArchives(rm.archive) + if latestErr != nil { + err := errors.Wrap(latestErr, "Resumability of requested export ledger range, was not able to get latest ledger from network") + return 0, false, err + } + networkLatest = networkLatest + (getHistoryArchivesCheckPointFrequency() * 2) + logger.Infof("Resumability computed effective latest network ledger including padding of checkpoint frequency to be %d + for network=%v", networkLatest, rm.network) + + if start > networkLatest { + // requested to start at a point beyond the latest network, resume not applicable. + return 0, false, errors.Errorf("Invalid start value of %v, it is greater than network's latest ledger of %v", start, networkLatest) + } + end = networkLatest + } + + rangeSize := max(int(end-start), 1) + var binarySearchError error + lowestAbsentIndex := sort.Search(rangeSize, binarySearchCallbackFn(&rm, ctx, start, end, &binarySearchError)) + if binarySearchError != nil { + return 0, false, binarySearchError + } + + if lowestAbsentIndex < int(rangeSize) { + nearestAbsentLedgerSequence := start + uint32(lowestAbsentIndex) + log.Infof("Resumability determined next absent object start key of %d for requested export ledger range", nearestAbsentLedgerSequence) + return nearestAbsentLedgerSequence, true, nil + } + + // unbounded, and datastore had up to latest network, return that as staring point. + if networkLatest > 0 { + return networkLatest, true, nil + } + + // data store had all ledgers for requested range, no resumability needed. + log.Infof("Resumability found no absent object keys in requested ledger range") + return 0, false, nil +} + +func binarySearchCallbackFn(rm *resumableManagerService, ctx context.Context, start, end uint32, binarySearchError *error) func(ledgerSequence int) bool { + lookupCache := map[string]bool{} + + return func(binarySearchIndex int) bool { + if *binarySearchError != nil { + // an error has already occured in a callback for the same binary search, exiting + return true + } + objectKeyMiddle := rm.ledgerBatchConfig.GetObjectKeyFromSequenceNumber(start + uint32(binarySearchIndex)) + + // there may be small occurrence of repeated queries on same object key once + // search narrows down to a range that fits within the ledgers per file + // worst case being 'log of ledgers_per_file' queries. + middleFoundOnStore, foundInCache := lookupCache[objectKeyMiddle] + if !foundInCache { + var datastoreErr error + middleFoundOnStore, datastoreErr = rm.dataStore.Exists(ctx, objectKeyMiddle) + if datastoreErr != nil { + *binarySearchError = errors.Wrapf(datastoreErr, "While searching datastore for resumability within export ledger range start=%d, end=%d, was not able to check if object key %v exists on data store", start, end, objectKeyMiddle) + return true + } + lookupCache[objectKeyMiddle] = middleFoundOnStore + } + return !middleFoundOnStore + } +} diff --git a/exp/services/ledgerexporter/internal/resumablemanager_test.go b/exp/services/ledgerexporter/internal/resumablemanager_test.go new file mode 100644 index 0000000000..189136932a --- /dev/null +++ b/exp/services/ledgerexporter/internal/resumablemanager_test.go @@ -0,0 +1,273 @@ +package ledgerexporter + +import ( + "context" + "testing" + + "github.com/pkg/errors" + "github.com/stellar/go/historyarchive" + "github.com/stretchr/testify/require" +) + +func TestResumability(t *testing.T) { + + tests := []struct { + name string + startLedger uint32 + endLedger uint32 + ledgerBatchConfig LedgerBatchConfig + absentLedger uint32 + findStartOk bool + networkName string + latestLedger uint32 + errorSnippet string + archiveError error + }{ + { + name: "archive error when resolving network latest", + startLedger: 4, + endLedger: 0, + absentLedger: 0, + findStartOk: false, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(1), + LedgersPerFile: uint32(10), + }, + networkName: "test", + errorSnippet: "archive error", + archiveError: errors.New("archive error"), + }, + { + name: "End ledger same as start, data store has it", + startLedger: 4, + endLedger: 4, + absentLedger: 0, + findStartOk: false, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(1), + LedgersPerFile: uint32(10), + }, + networkName: "test", + }, + { + name: "End ledger same as start, data store does not have it", + startLedger: 14, + endLedger: 14, + absentLedger: 14, + findStartOk: true, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(1), + LedgersPerFile: uint32(10), + }, + networkName: "test", + }, + { + name: "binary search encounters an error during datastore retrieval", + startLedger: 24, + endLedger: 24, + absentLedger: 0, + findStartOk: false, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(1), + LedgersPerFile: uint32(10), + }, + networkName: "test", + errorSnippet: "datastore error happened", + }, + { + name: "Data store is beyond boundary aligned start ledger", + startLedger: 20, + endLedger: 50, + absentLedger: 40, + findStartOk: true, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(1), + LedgersPerFile: uint32(10), + }, + networkName: "test", + }, + { + name: "Data store is beyond non boundary aligned start ledger", + startLedger: 55, + endLedger: 85, + absentLedger: 80, + findStartOk: true, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(1), + LedgersPerFile: uint32(10), + }, + networkName: "test", + }, + { + name: "Data store is beyond start and end ledger", + startLedger: 255, + endLedger: 275, + absentLedger: 0, + findStartOk: false, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(1), + LedgersPerFile: uint32(10), + }, + networkName: "test", + }, + { + name: "Data store is not beyond start ledger", + startLedger: 95, + endLedger: 125, + absentLedger: 95, + findStartOk: true, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(1), + LedgersPerFile: uint32(10), + }, + networkName: "test", + }, + { + name: "No start ledger provided", + startLedger: 0, + endLedger: 10, + absentLedger: 0, + findStartOk: false, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(1), + LedgersPerFile: uint32(10), + }, + networkName: "test", + errorSnippet: "Invalid start value", + }, + { + name: "No end ledger provided, data store not beyond start", + startLedger: 1145, + endLedger: 0, + absentLedger: 1145, + findStartOk: true, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(1), + LedgersPerFile: uint32(10), + }, + networkName: "test2", + latestLedger: uint32(2000), + }, + { + name: "No end ledger provided, data store is beyond start", + startLedger: 2145, + endLedger: 0, + absentLedger: 2250, + findStartOk: true, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(1), + LedgersPerFile: uint32(10), + }, + networkName: "test3", + latestLedger: uint32(3000), + }, + { + name: "No end ledger provided, data store is beyond start and archive network latest, and partially into checkpoint frequency padding", + startLedger: 3145, + endLedger: 0, + absentLedger: 4070, + findStartOk: true, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(1), + LedgersPerFile: uint32(10), + }, + networkName: "test4", + latestLedger: uint32(4000), + }, + { + name: "No end ledger provided, start is beyond archive network latest and checkpoint frequency padding", + startLedger: 5129, + endLedger: 0, + absentLedger: 0, + findStartOk: false, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(1), + LedgersPerFile: uint32(10), + }, + networkName: "test5", + latestLedger: uint32(5000), + errorSnippet: "Invalid start value of 5129, it is greater than network's latest ledger of 5128", + }, + } + + ctx := context.Background() + + mockDataStore := &MockDataStore{} + + //"End ledger same as start, data store has it" + mockDataStore.On("Exists", ctx, "0-9.xdr.gz").Return(true, nil).Once() + + //"End ledger same as start, data store does not have it" + mockDataStore.On("Exists", ctx, "10-19.xdr.gz").Return(false, nil).Once() + + //"binary search encounters an error during datastore retrieval", + mockDataStore.On("Exists", ctx, "20-29.xdr.gz").Return(false, errors.New("datastore error happened")).Once() + + //"Data store is beyond boundary aligned start ledger" + mockDataStore.On("Exists", ctx, "30-39.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "40-49.xdr.gz").Return(false, nil).Once() + + //"Data store is beyond non boundary aligned start ledger" + mockDataStore.On("Exists", ctx, "70-79.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "80-89.xdr.gz").Return(false, nil).Once() + + //"Data store is beyond start and end ledger" + mockDataStore.On("Exists", ctx, "260-269.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "270-279.xdr.gz").Return(true, nil).Once() + + //"Data store is not beyond start ledger" + mockDataStore.On("Exists", ctx, "110-119.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "100-109.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "90-99.xdr.gz").Return(false, nil).Once() + + //"No end ledger provided, data store not beyond start" uses latest from network="test2" + mockDataStore.On("Exists", ctx, "1630-1639.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "1390-1399.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "1260-1269.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "1200-1209.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "1160-1169.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "1170-1179.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "1150-1159.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "1140-1149.xdr.gz").Return(false, nil).Once() + + //"No end ledger provided, data store is beyond start" uses latest from network="test3" + mockDataStore.On("Exists", ctx, "2630-2639.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "2390-2399.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "2260-2269.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "2250-2259.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "2240-2249.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "2230-2239.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "2200-2209.xdr.gz").Return(true, nil).Once() + + //"No end ledger provided, data store is beyond start and archive network latest, and partially into checkpoint frequency padding" uses latest from network="test4" + mockDataStore.On("Exists", ctx, "3630-3639.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "3880-3889.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "4000-4009.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "4060-4069.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "4090-4099.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "4080-4089.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "4070-4079.xdr.gz").Return(false, nil).Once() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockArchive := &historyarchive.MockArchive{} + mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: tt.latestLedger}, tt.archiveError).Once() + + resumableManager := NewResumableManager(mockDataStore, tt.networkName, tt.ledgerBatchConfig, mockArchive) + absentLedger, ok, err := resumableManager.FindStart(ctx, tt.startLedger, tt.endLedger) + if tt.errorSnippet != "" { + require.ErrorContains(t, err, tt.errorSnippet) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.absentLedger, absentLedger) + require.Equal(t, tt.findStartOk, ok) + if tt.endLedger == 0 { + // archives are only expected to be called when end = 0 + mockArchive.AssertExpectations(t) + } + }) + } + + mockDataStore.AssertExpectations(t) +} diff --git a/exp/services/ledgerexporter/internal/test/10perfile.toml b/exp/services/ledgerexporter/internal/test/10perfile.toml new file mode 100644 index 0000000000..8629bf4b5b --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/10perfile.toml @@ -0,0 +1,4 @@ +network = "test" + +[exporter_config] +ledgers_per_file = 10 \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/15perfile.toml b/exp/services/ledgerexporter/internal/test/15perfile.toml new file mode 100644 index 0000000000..eb76bac4d8 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/15perfile.toml @@ -0,0 +1,4 @@ +network = "test" + +[exporter_config] +ledgers_per_file = 15 \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/1perfile.toml b/exp/services/ledgerexporter/internal/test/1perfile.toml new file mode 100644 index 0000000000..cadef50df8 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/1perfile.toml @@ -0,0 +1,4 @@ +network = "test" + +[exporter_config] +ledgers_per_file = 1 \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/64perfile.toml b/exp/services/ledgerexporter/internal/test/64perfile.toml new file mode 100644 index 0000000000..f4e30a71c0 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/64perfile.toml @@ -0,0 +1,4 @@ +network = "test" + +[exporter_config] +ledgers_per_file = 64 \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/no_network.toml b/exp/services/ledgerexporter/internal/test/no_network.toml new file mode 100644 index 0000000000..f5815b9b9d --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/no_network.toml @@ -0,0 +1,10 @@ + +[datastore_config] +type = "ABC" + +[datastore_config.params] +destination_bucket_path = "your-bucket-name/subpath" + +[exporter_config] +ledgers_per_file = 3 +files_per_partition = 1 \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/test.toml b/exp/services/ledgerexporter/internal/test/test.toml new file mode 100644 index 0000000000..49d62384d7 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/test.toml @@ -0,0 +1,11 @@ +network = "test" + +[datastore_config] +type = "ABC" + +[datastore_config.params] +destination_bucket_path = "your-bucket-name/subpath" + +[exporter_config] +ledgers_per_file = 3 +files_per_partition = 1 \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/validate_start_end.toml b/exp/services/ledgerexporter/internal/test/validate_start_end.toml new file mode 100644 index 0000000000..cadef50df8 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/validate_start_end.toml @@ -0,0 +1,4 @@ +network = "test" + +[exporter_config] +ledgers_per_file = 1 \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/utils.go b/exp/services/ledgerexporter/internal/utils.go deleted file mode 100644 index 8b556c5814..0000000000 --- a/exp/services/ledgerexporter/internal/utils.go +++ /dev/null @@ -1,105 +0,0 @@ -package ledgerexporter - -import ( - "compress/gzip" - "fmt" - "io" - - xdr3 "github.com/stellar/go-xdr/xdr3" - - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/storage" -) - -const ( - fileSuffix = ".xdr.gz" -) - -// GetObjectKeyFromSequenceNumber generates the file name from the ledger sequence number based on configuration. -func GetObjectKeyFromSequenceNumber(config ExporterConfig, ledgerSeq uint32) (string, error) { - var objectKey string - - if config.LedgersPerFile < 1 { - return "", errors.Errorf("Invalid ledgers per file (%d): must be at least 1", config.LedgersPerFile) - } - - if config.FilesPerPartition > 1 { - partitionSize := config.LedgersPerFile * config.FilesPerPartition - partitionStart := (ledgerSeq / partitionSize) * partitionSize - partitionEnd := partitionStart + partitionSize - 1 - objectKey = fmt.Sprintf("%d-%d/", partitionStart, partitionEnd) - } - - fileStart := (ledgerSeq / config.LedgersPerFile) * config.LedgersPerFile - fileEnd := fileStart + config.LedgersPerFile - 1 - objectKey += fmt.Sprintf("%d", fileStart) - - // Multiple ledgers per file - if fileStart != fileEnd { - objectKey += fmt.Sprintf("-%d", fileEnd) - } - objectKey += fileSuffix - - return objectKey, nil -} - -// getLatestLedgerSequenceFromHistoryArchives returns the most recent ledger sequence (checkpoint ledger) -// number present in the history archives. -func getLatestLedgerSequenceFromHistoryArchives(historyArchivesURLs []string) (uint32, error) { - for _, historyArchiveURL := range historyArchivesURLs { - ha, err := historyarchive.Connect( - historyArchiveURL, - historyarchive.ArchiveOptions{ - ConnectOptions: storage.ConnectOptions{ - UserAgent: "ledger-exporter", - }, - }, - ) - if err != nil { - logger.WithError(err).Warnf("Error connecting to history archive %s", historyArchiveURL) - continue // Skip to next archive - } - - has, err := ha.GetRootHAS() - if err != nil { - logger.WithError(err).Warnf("Error getting RootHAS for %s", historyArchiveURL) - continue // Skip to next archive - } - - return has.CurrentLedger, nil - } - - return 0, errors.New("failed to retrieve the latest ledger sequence from any history archive") -} - -type XDRGzipEncoder struct { - XdrPayload interface{} -} - -func (g *XDRGzipEncoder) WriteTo(w io.Writer) (int64, error) { - gw := gzip.NewWriter(w) - n, err := xdr3.Marshal(gw, g.XdrPayload) - if err != nil { - return int64(n), err - } - return int64(n), gw.Close() -} - -type XDRGzipDecoder struct { - XdrPayload interface{} -} - -func (d *XDRGzipDecoder) ReadFrom(r io.Reader) (int64, error) { - gr, err := gzip.NewReader(r) - if err != nil { - return 0, err - } - defer gr.Close() - - n, err := xdr3.Unmarshal(gr, d.XdrPayload) - if err != nil { - return int64(n), err - } - return int64(n), nil -} diff --git a/exp/services/ledgerexporter/main.go b/exp/services/ledgerexporter/main.go index f1a81e95ba..63e094980f 100644 --- a/exp/services/ledgerexporter/main.go +++ b/exp/services/ledgerexporter/main.go @@ -1,8 +1,25 @@ package main -import exporter "github.com/stellar/go/exp/services/ledgerexporter/internal" +import ( + "flag" + + exporter "github.com/stellar/go/exp/services/ledgerexporter/internal" +) func main() { - app := exporter.NewApp() + flags := exporter.Flags{} + startLedger := uint(0) + endLedger := uint(0) + flag.UintVar(&startLedger, "start", 0, "Starting ledger") + flag.UintVar(&endLedger, "end", 0, "Ending ledger (inclusive)") + flag.StringVar(&flags.ConfigFilePath, "config-file", "config.toml", "Path to the TOML config file") + flag.BoolVar(&flags.Resume, "resume", false, "Attempt to find a resumable starting point on remote data store") + flag.UintVar(&flags.AdminPort, "admin-port", 0, "Admin HTTP port for prometheus metrics") + + flag.Parse() + flags.StartLedger = uint32(startLedger) + flags.EndLedger = uint32(endLedger) + + app := exporter.NewApp(flags) app.Run() } diff --git a/go.mod b/go.mod index f45b68de3c..fb02c94507 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.1 require ( cloud.google.com/go/firestore v1.14.0 // indirect - cloud.google.com/go/storage v1.37.0 + cloud.google.com/go/storage v1.40.0 firebase.google.com/go v3.12.0+incompatible github.com/2opremio/pretty v0.2.2-0.20230601220618-e1d5758b2a95 github.com/BurntSushi/toml v1.3.2 @@ -22,7 +22,7 @@ require ( github.com/go-chi/chi v4.1.2+incompatible github.com/go-errors/errors v1.5.1 github.com/golang-jwt/jwt v3.2.2+incompatible - github.com/google/uuid v1.5.0 + github.com/google/uuid v1.6.0 github.com/gorilla/schema v1.2.0 github.com/graph-gophers/graphql-go v1.3.0 github.com/guregu/null v4.0.0+incompatible @@ -52,7 +52,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8 github.com/xdrpp/goxdr v0.1.1 - google.golang.org/api v0.157.0 + google.golang.org/api v0.170.0 gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0 gopkg.in/square/go-jose.v2 v2.4.1 gopkg.in/tylerb/graceful.v1 v1.2.15 @@ -64,10 +64,10 @@ require ( ) require ( - cloud.google.com/go/compute v1.23.3 // indirect + cloud.google.com/go/compute v1.24.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v1.1.5 // indirect - cloud.google.com/go/longrunning v0.5.4 // indirect + cloud.google.com/go/iam v1.1.7 // indirect + cloud.google.com/go/longrunning v0.5.5 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect @@ -75,7 +75,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gobuffalo/packd v1.0.2 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect @@ -90,23 +90,23 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.10.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect - go.opentelemetry.io/otel v1.21.0 // indirect - go.opentelemetry.io/otel/metric v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.21.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.13.0 // indirect golang.org/x/tools v0.14.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240122161410-6c6643bf1457 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 // indirect gopkg.in/djherbis/atime.v1 v1.0.0 // indirect gopkg.in/djherbis/stream.v1 v1.3.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) require ( - cloud.google.com/go v0.112.0 // indirect + cloud.google.com/go v0.112.1 // indirect github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/goreplay v1.3.2 @@ -114,10 +114,10 @@ require ( github.com/fatih/structs v1.0.0 // indirect github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5 // indirect - github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/googleapis/gax-go/v2 v2.12.3 // indirect github.com/hashicorp/golang-lru v1.0.2 github.com/imkira/go-interpol v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -150,14 +150,14 @@ require ( golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/net v0.23.0 // indirect - golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/oauth2 v0.18.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect - google.golang.org/grpc v1.60.1 // indirect + google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect + google.golang.org/grpc v1.62.1 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 2ea7e9a01c..6d8713de6d 100644 --- a/go.sum +++ b/go.sum @@ -17,26 +17,26 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= -cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= +cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= +cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= +cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw= cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ= -cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= -cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= -cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= -cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= +cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= +cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= +cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg= +cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -47,8 +47,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.37.0 h1:WI8CsaFO8Q9KjPVtsZ5Cmi0dXV25zMoX0FklT7c3Jm4= -cloud.google.com/go/storage v1.37.0/go.mod h1:i34TiT2IhiNDmcj65PqwCjcoUX7Z5pLzS8DEmoiFq1k= +cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw= +cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= firebase.google.com/go v3.12.0+incompatible h1:q70KCp/J0oOL8kJ8oV2j3646kV4TB8Y5IvxXC0WT1bo= firebase.google.com/go v3.12.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs= @@ -96,8 +96,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/jrpc2 v1.1.0 h1:SgpJf0v1rVCZx68+4APv6dgsTFsIHlpgFD1NlQAWA0A= github.com/creachadair/jrpc2 v1.1.0/go.mod h1:5jN7MKwsm8qvgfTsTzLX3JIfidsAkZ1c8DZSQmp+g38= @@ -121,8 +119,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= -github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU= github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -150,8 +146,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -197,8 +193,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -240,14 +236,14 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= -github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= +github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -478,18 +474,18 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= -go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= -go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= -go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= -go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= -go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -591,8 +587,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -756,8 +752,8 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.157.0 h1:ORAeqmbrrozeyw5NjnMxh7peHO0UzV4wWYSwZeCUb20= -google.golang.org/api v0.157.0/go.mod h1:+z4v4ufbZ1WEpld6yMGHyggs+PmAHiaLNj5ytP3N01g= +google.golang.org/api v0.170.0 h1:zMaruDePM88zxZBG+NG8+reALO2rfLhe/JShitLyT48= +google.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -803,12 +799,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg= -google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= -google.golang.org/genproto/googleapis/api v0.0.0-20240122161410-6c6643bf1457 h1:KHBtwE+eQc3+NxpjmRFlQ3pJQ2FNnhhgB9xOV8kyBuU= -google.golang.org/genproto/googleapis/api v0.0.0-20240122161410-6c6643bf1457/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= +google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c h1:kaI7oewGK5YnVwj+Y+EJBO/YN1ht8iTL9XkFHtVZLsc= +google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 h1:9IZDv+/GcI6u+a4jRFRLxQs0RUCfavGfoOgEW6jpkI0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -825,8 +821,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= -google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= +google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From fff01229a5af77dee170a37bf0c71b2ce8bb8474 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Tue, 30 Apr 2024 23:53:21 -0400 Subject: [PATCH 132/234] Update GetLatestLedgerSequence --- exp/services/ledgerexporter/internal/app.go | 8 +- .../ledgerexporter/internal/app_test.go | 21 ++-- .../ledgerexporter/internal/config.go | 53 ++------ .../ledgerexporter/internal/datastore_test.go | 13 -- .../ledgerexporter/internal/exportmanager.go | 10 +- .../internal/exportmanager_test.go | 21 ++-- exp/services/ledgerexporter/internal/utils.go | 76 ----------- ingest/ledgerbackend/gcs_backend.go | 118 ++++-------------- support/datastore/datastore.go | 64 +--------- support/datastore/datastore_test.go | 40 +----- support/datastore/gcs_datastore.go | 45 +------ .../datastore}/ledgerbatch_config.go | 2 +- .../datastore}/ledgerbatch_config_test.go | 2 +- support/datastore/mock_datastore.go | 71 ----------- .../internal => support/datastore}/mocks.go | 2 +- .../datastore}/resumablemanager_test.go | 2 +- .../datastore/resumeablemanager.go | 11 +- support/datastore/utils.go | 48 +++++++ support/priorityqueue/priorityqueue.go | 4 +- 19 files changed, 133 insertions(+), 478 deletions(-) delete mode 100644 exp/services/ledgerexporter/internal/datastore_test.go delete mode 100644 exp/services/ledgerexporter/internal/utils.go rename {exp/services/ledgerexporter/internal => support/datastore}/ledgerbatch_config.go (98%) rename {exp/services/ledgerexporter/internal => support/datastore}/ledgerbatch_config_test.go (97%) delete mode 100644 support/datastore/mock_datastore.go rename {exp/services/ledgerexporter/internal => support/datastore}/mocks.go (100%) rename {exp/services/ledgerexporter/internal => support/datastore}/resumablemanager_test.go (99%) rename exp/services/ledgerexporter/internal/resumablemanager.go => support/datastore/resumeablemanager.go (92%) create mode 100644 support/datastore/utils.go diff --git a/exp/services/ledgerexporter/internal/app.go b/exp/services/ledgerexporter/internal/app.go index 777792d8ff..e23cc7b746 100644 --- a/exp/services/ledgerexporter/internal/app.go +++ b/exp/services/ledgerexporter/internal/app.go @@ -89,17 +89,17 @@ func (a *App) init(ctx context.Context) error { if a.config, err = NewConfig(ctx, a.flags); err != nil { return errors.Wrap(err, "Could not load configuration") } - if archive, err = createHistoryArchiveFromNetworkName(ctx, a.config.Network); err != nil { + if archive, err = datastore.CreateHistoryArchiveFromNetworkName(ctx, a.config.Network); err != nil { return err } a.config.ValidateAndSetLedgerRange(ctx, archive) - if a.dataStore, err = NewDataStore(ctx, a.config.DataStoreConfig, a.config.Network); err != nil { + if a.dataStore, err = datastore.NewDataStore(ctx, a.config.DataStoreConfig, a.config.Network); err != nil { return errors.Wrap(err, "Could not connect to destination data store") } if a.config.Resume { if err = a.applyResumability(ctx, - NewResumableManager(a.dataStore, a.config.Network, a.config.LedgerBatchConfig, archive)); err != nil { + datastore.NewResumableManager(a.dataStore, a.config.Network, a.config.LedgerBatchConfig, archive)); err != nil { return err } } @@ -120,7 +120,7 @@ func (a *App) init(ctx context.Context) error { return nil } -func (a *App) applyResumability(ctx context.Context, resumableManager ResumableManager) error { +func (a *App) applyResumability(ctx context.Context, resumableManager datastore.ResumableManager) error { absentLedger, ok, err := resumableManager.FindStart(ctx, a.config.StartLedger, a.config.EndLedger) if err != nil { return err diff --git a/exp/services/ledgerexporter/internal/app_test.go b/exp/services/ledgerexporter/internal/app_test.go index fc63c3a387..2063fd21a2 100644 --- a/exp/services/ledgerexporter/internal/app_test.go +++ b/exp/services/ledgerexporter/internal/app_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/pkg/errors" + "github.com/stellar/go/support/datastore" "github.com/stretchr/testify/require" ) @@ -12,7 +13,7 @@ func TestApplyResumeHasStartError(t *testing.T) { ctx := context.Background() app := &App{} app.config = &Config{StartLedger: 10, EndLedger: 19, Resume: true} - mockResumableManager := &MockResumableManager{} + mockResumableManager := &datastore.MockResumableManager{} mockResumableManager.On("FindStart", ctx, uint32(10), uint32(19)).Return(uint32(0), false, errors.New("start error")).Once() err := app.applyResumability(ctx, mockResumableManager) @@ -24,7 +25,7 @@ func TestApplyResumeDatastoreComplete(t *testing.T) { ctx := context.Background() app := &App{} app.config = &Config{StartLedger: 10, EndLedger: 19, Resume: true} - mockResumableManager := &MockResumableManager{} + mockResumableManager := &datastore.MockResumableManager{} mockResumableManager.On("FindStart", ctx, uint32(10), uint32(19)).Return(uint32(0), false, nil).Once() var alreadyExported *DataAlreadyExportedError @@ -40,9 +41,9 @@ func TestApplyResumeInvalidDataStoreLedgersPerFileBoundary(t *testing.T) { StartLedger: 3, EndLedger: 9, Resume: true, - LedgerBatchConfig: LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, } - mockResumableManager := &MockResumableManager{} + mockResumableManager := &datastore.MockResumableManager{} // simulate the datastore has inconsistent data, // with last ledger not aligned to starting boundary mockResumableManager.On("FindStart", ctx, uint32(3), uint32(9)).Return(uint32(6), true, nil).Once() @@ -60,9 +61,9 @@ func TestApplyResumeWithPartialRemoteDataPresent(t *testing.T) { StartLedger: 10, EndLedger: 99, Resume: true, - LedgerBatchConfig: LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, } - mockResumableManager := &MockResumableManager{} + mockResumableManager := &datastore.MockResumableManager{} // simulates a data store that had ledger files populated up to seq=49, so the first absent ledger would be 50 mockResumableManager.On("FindStart", ctx, uint32(10), uint32(99)).Return(uint32(50), true, nil).Once() @@ -79,9 +80,9 @@ func TestApplyResumeWithNoRemoteDataPresent(t *testing.T) { StartLedger: 10, EndLedger: 99, Resume: true, - LedgerBatchConfig: LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, } - mockResumableManager := &MockResumableManager{} + mockResumableManager := &datastore.MockResumableManager{} // simulates a data store that had no data in the requested range mockResumableManager.On("FindStart", ctx, uint32(10), uint32(99)).Return(uint32(2), true, nil).Once() @@ -101,9 +102,9 @@ func TestApplyResumeWithNoRemoteDataAndRequestFromGenesis(t *testing.T) { StartLedger: 2, EndLedger: 99, Resume: true, - LedgerBatchConfig: LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, } - mockResumableManager := &MockResumableManager{} + mockResumableManager := &datastore.MockResumableManager{} // simulates a data store that had no data in the requested range mockResumableManager.On("FindStart", ctx, uint32(2), uint32(99)).Return(uint32(2), true, nil).Once() diff --git a/exp/services/ledgerexporter/internal/config.go b/exp/services/ledgerexporter/internal/config.go index 0d3cb87cb4..2f6589a720 100644 --- a/exp/services/ledgerexporter/internal/config.go +++ b/exp/services/ledgerexporter/internal/config.go @@ -11,9 +11,9 @@ import ( "github.com/pelletier/go-toml" + "github.com/stellar/go/support/datastore" "github.com/stellar/go/support/errors" "github.com/stellar/go/support/ordered" - "github.com/stellar/go/support/storage" ) const Pubnet = "pubnet" @@ -34,58 +34,19 @@ type StellarCoreConfig struct { CaptiveCoreTomlPath string `toml:"captive_core_toml_path"` } -type DataStoreConfig struct { - Type string `toml:"type"` - Params map[string]string `toml:"params"` -} - type Config struct { AdminPort int `toml:"admin_port"` - Network string `toml:"network"` - DataStoreConfig DataStoreConfig `toml:"datastore_config"` - LedgerBatchConfig LedgerBatchConfig `toml:"exporter_config"` - StellarCoreConfig StellarCoreConfig `toml:"stellar_core_config"` + Network string `toml:"network"` + DataStoreConfig datastore.DataStoreConfig `toml:"datastore_config"` + LedgerBatchConfig datastore.LedgerBatchConfig `toml:"exporter_config"` + StellarCoreConfig StellarCoreConfig `toml:"stellar_core_config"` StartLedger uint32 EndLedger uint32 Resume bool } -func createHistoryArchiveFromNetworkName(ctx context.Context, networkName string) (historyarchive.ArchiveInterface, error) { - var historyArchiveUrls []string - switch networkName { - case Pubnet: - historyArchiveUrls = network.PublicNetworkhistoryArchiveURLs - case Testnet: - historyArchiveUrls = network.TestNetworkhistoryArchiveURLs - default: - return nil, errors.Errorf("Invalid network name %s", networkName) - } - - return historyarchive.NewArchivePool(historyArchiveUrls, historyarchive.ArchiveOptions{ - ConnectOptions: storage.ConnectOptions{ - UserAgent: "ledger-exporter", - Context: ctx, - }, - }) -} - -func getLatestLedgerSequenceFromHistoryArchives(archive historyarchive.ArchiveInterface) (uint32, error) { - has, err := archive.GetRootHAS() - if err != nil { - logger.WithError(err).Warnf("Error getting root HAS from archives") - return 0, errors.Wrap(err, "failed to retrieve the latest ledger sequence from any history archive") - } - - return has.CurrentLedger, nil -} - -func getHistoryArchivesCheckPointFrequency() uint32 { - // this could evolve to use other sources for checkpoint freq - return historyarchive.DefaultCheckpointFrequency -} - // This will generate the config based on commandline flags and toml // // ctx - the caller context @@ -113,7 +74,7 @@ func NewConfig(ctx context.Context, flags Flags) (*Config, error) { // Validates requested ledger range, and will automatically adjust it // to be ledgers-per-file boundary aligned func (config *Config) ValidateAndSetLedgerRange(ctx context.Context, archive historyarchive.ArchiveInterface) error { - latestNetworkLedger, err := getLatestLedgerSequenceFromHistoryArchives(archive) + latestNetworkLedger, err := datastore.GetLatestLedgerSequenceFromHistoryArchives(archive) if err != nil { return errors.Wrap(err, "Failed to retrieve the latest ledger sequence from history archives.") @@ -182,7 +143,7 @@ func (config *Config) GenerateCaptiveCoreConfig() (ledgerbackend.CaptiveCoreConf BinaryPath: coreConfig.StellarCoreBinaryPath, NetworkPassphrase: params.NetworkPassphrase, HistoryArchiveURLs: params.HistoryArchiveURLs, - CheckpointFrequency: getHistoryArchivesCheckPointFrequency(), + CheckpointFrequency: datastore.GetHistoryArchivesCheckPointFrequency(), Log: logger.WithField("subservice", "stellar-core"), Toml: captiveCoreToml, UserAgent: "ledger-exporter", diff --git a/exp/services/ledgerexporter/internal/datastore_test.go b/exp/services/ledgerexporter/internal/datastore_test.go deleted file mode 100644 index d3128a4cf3..0000000000 --- a/exp/services/ledgerexporter/internal/datastore_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package ledgerexporter - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestInvalidStore(t *testing.T) { - _, err := NewDataStore(context.Background(), DataStoreConfig{Type: "unknown"}, "test") - require.Error(t, err) -} diff --git a/exp/services/ledgerexporter/internal/exportmanager.go b/exp/services/ledgerexporter/internal/exportmanager.go index a51ae9a28b..58af651c06 100644 --- a/exp/services/ledgerexporter/internal/exportmanager.go +++ b/exp/services/ledgerexporter/internal/exportmanager.go @@ -13,7 +13,7 @@ import ( ) type ExportManager struct { - config LedgerBatchConfig + config datastore.LedgerBatchConfig ledgerBackend ledgerbackend.LedgerBackend currentMetaArchive *LedgerMetaArchive queue UploadQueue @@ -21,7 +21,7 @@ type ExportManager struct { } // NewExportManager creates a new ExportManager with the provided configuration. -func NewExportManager(config LedgerBatchConfig, backend ledgerbackend.LedgerBackend, queue UploadQueue, prometheusRegistry *prometheus.Registry) (*ExportManager, error) { +func NewExportManager(config datastore.LedgerBatchConfig, backend ledgerbackend.LedgerBackend, queue UploadQueue, prometheusRegistry *prometheus.Registry) (*ExportManager, error) { if config.LedgersPerFile < 1 { return nil, errors.Errorf("Invalid ledgers per file (%d): must be at least 1", config.LedgersPerFile) } @@ -45,10 +45,8 @@ func (e *ExportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta ledgerSeq := ledgerCloseMeta.LedgerSequence() // Determine the object key for the given ledger sequence - objectKey, err := datastore.GetObjectKeyFromSequenceNumber(ledgerSeq, e.config.LedgersPerFile, e.config.FilesPerPartition, fileSuffix) - if err != nil { - return errors.Wrapf(err, "failed to get object key for ledger %d", ledgerSeq) - } + objectKey := e.config.GetObjectKeyFromSequenceNumber(ledgerSeq) + if e.currentMetaArchive != nil && e.currentMetaArchive.GetObjectKey() != objectKey { return errors.New("Current meta archive object key mismatch") } diff --git a/exp/services/ledgerexporter/internal/exportmanager_test.go b/exp/services/ledgerexporter/internal/exportmanager_test.go index 1e113bb545..0fb3011266 100644 --- a/exp/services/ledgerexporter/internal/exportmanager_test.go +++ b/exp/services/ledgerexporter/internal/exportmanager_test.go @@ -39,7 +39,7 @@ func (s *ExportManagerSuite) TearDownTest() { } func (s *ExportManagerSuite) TestInvalidExportConfig() { - config := LedgerBatchConfig{LedgersPerFile: 0, FilesPerPartition: 10} + config := datastore.LedgerBatchConfig{LedgersPerFile: 0, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) _, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -47,7 +47,7 @@ func (s *ExportManagerSuite) TestInvalidExportConfig() { } func (s *ExportManagerSuite) TestRun() { - config := LedgerBatchConfig{LedgersPerFile: 64, FilesPerPartition: 10} + config := datastore.LedgerBatchConfig{LedgersPerFile: 64, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -59,7 +59,7 @@ func (s *ExportManagerSuite) TestRun() { for i := start; i <= end; i++ { s.mockBackend.On("GetLedger", s.ctx, i). Return(createLedgerCloseMeta(i), nil) - key, _ := datastore.GetObjectKeyFromSequenceNumber(i, config.LedgersPerFile, config.FilesPerPartition, fileSuffix) + key := config.GetObjectKeyFromSequenceNumber(i) expectedKeys.Add(key) } @@ -97,7 +97,7 @@ func (s *ExportManagerSuite) TestRun() { } func (s *ExportManagerSuite) TestRunContextCancel() { - config := LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 1} + config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 1} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -126,7 +126,7 @@ func (s *ExportManagerSuite) TestRunContextCancel() { } func (s *ExportManagerSuite) TestRunWithCanceledContext() { - config := LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} + config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -159,7 +159,7 @@ func (s *ExportManagerSuite) TestGetObjectKeyFromSequenceNumber() { for _, tc := range testCases { s.T().Run(fmt.Sprintf("LedgerSeq-%d-LedgersPerFile-%d", tc.ledgerSeq, tc.ledgersPerFile), func(t *testing.T) { - config := LedgerBatchConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile} + config := datastore.LedgerBatchConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile} key := config.GetObjectKeyFromSequenceNumber(tc.ledgerSeq) require.Equal(t, tc.expectedKey, key) }) @@ -167,7 +167,7 @@ func (s *ExportManagerSuite) TestGetObjectKeyFromSequenceNumber() { } func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { - config := LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} + config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -195,8 +195,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { for i := start; i <= end; i++ { require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(i))) - key, err := datastore.GetObjectKeyFromSequenceNumber(i, config.LedgersPerFile, config.FilesPerPartition, fileSuffix) - require.NoError(s.T(), err) + key := config.GetObjectKeyFromSequenceNumber(i) expectedkeys.Add(key) } @@ -206,7 +205,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { } func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { - config := LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} + config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -224,7 +223,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { } func (s *ExportManagerSuite) TestAddLedgerCloseMetaKeyMismatch() { - config := LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 1} + config := datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 1} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) diff --git a/exp/services/ledgerexporter/internal/utils.go b/exp/services/ledgerexporter/internal/utils.go deleted file mode 100644 index b468eb8301..0000000000 --- a/exp/services/ledgerexporter/internal/utils.go +++ /dev/null @@ -1,76 +0,0 @@ -package ledgerexporter - -import ( - "compress/gzip" - "io" - - xdr3 "github.com/stellar/go-xdr/xdr3" - - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/storage" -) - -const ( - fileSuffix = ".xdr.gz" -) - -// getLatestLedgerSequenceFromHistoryArchives returns the most recent ledger sequence (checkpoint ledger) -// number present in the history archives. -func getLatestLedgerSequenceFromHistoryArchives(historyArchivesURLs []string) (uint32, error) { - for _, historyArchiveURL := range historyArchivesURLs { - ha, err := historyarchive.Connect( - historyArchiveURL, - historyarchive.ArchiveOptions{ - ConnectOptions: storage.ConnectOptions{ - UserAgent: "ledger-exporter", - }, - }, - ) - if err != nil { - logger.WithError(err).Warnf("Error connecting to history archive %s", historyArchiveURL) - continue // Skip to next archive - } - - has, err := ha.GetRootHAS() - if err != nil { - logger.WithError(err).Warnf("Error getting RootHAS for %s", historyArchiveURL) - continue // Skip to next archive - } - - return has.CurrentLedger, nil - } - - return 0, errors.New("failed to retrieve the latest ledger sequence from any history archive") -} - -type XDRGzipEncoder struct { - XdrPayload interface{} -} - -func (g *XDRGzipEncoder) WriteTo(w io.Writer) (int64, error) { - gw := gzip.NewWriter(w) - n, err := xdr3.Marshal(gw, g.XdrPayload) - if err != nil { - return int64(n), err - } - return int64(n), gw.Close() -} - -type XDRGzipDecoder struct { - XdrPayload interface{} -} - -func (d *XDRGzipDecoder) ReadFrom(r io.Reader) (int64, error) { - gr, err := gzip.NewReader(r) - if err != nil { - return 0, err - } - defer gr.Close() - - n, err := xdr3.Unmarshal(gr, d.XdrPayload) - if err != nil { - return int64(n), err - } - return int64(n), nil -} diff --git a/ingest/ledgerbackend/gcs_backend.go b/ingest/ledgerbackend/gcs_backend.go index 266fe697e5..401f448d18 100644 --- a/ingest/ledgerbackend/gcs_backend.go +++ b/ingest/ledgerbackend/gcs_backend.go @@ -9,6 +9,8 @@ import ( "time" "github.com/pkg/errors" + + "github.com/stellar/go/historyarchive" "github.com/stellar/go/support/datastore" "github.com/stellar/go/support/log" "github.com/stellar/go/support/priorityqueue" @@ -55,13 +57,16 @@ type GCSBackend struct { // ledgerBuffer is the buffer for LedgerCloseMeta data read in parallel. ledgerBuffer *ledgerBufferGCS - prepared *Range // non-nil if any range is prepared - closed bool // False until the core is closed + dataStore datastore.DataStore + ledgerBatchConfig datastore.LedgerBatchConfig + network string + prepared *Range // non-nil if any range is prepared + closed bool // False until the core is closed } type ledgerBufferGCS struct { config GCSBackendConfig - lcmDataStore datastore.DataStore + dataStore datastore.DataStore taskQueue chan uint32 ledgerQueue chan []byte ledgerPriorityQueue priorityqueue.PriorityQueue @@ -79,16 +84,16 @@ type ledgerBufferGCS struct { func NewLedgerBuffer(ctx context.Context, config GCSBackendConfig) (*ledgerBufferGCS, error) { var cancel context.CancelFunc - lcmDataStore, err := datastore.NewDataStore(ctx, config.LcmFileConfig.StorageURL) - if err != nil { - return nil, err - } + //lcmDataStore, err := datastore.NewDataStore(ctx, config.datastoreConfig, config.network) + //if err != nil { + // return nil, err + //} pq := make(priorityqueue.PriorityQueue, config.BufferConfig.BufferSize) heap.Init(&pq) ledgerBuffer := &ledgerBufferGCS{ - lcmDataStore: lcmDataStore, + //lcmDataStore: lcmDataStore, taskQueue: make(chan uint32, config.BufferConfig.BufferSize), ledgerQueue: make(chan []byte, config.BufferConfig.BufferSize), ledgerPriorityQueue: pq, @@ -151,16 +156,10 @@ func (lb *ledgerBufferGCS) worker() { func (lb *ledgerBufferGCS) getLedgerGCSObject(sequence uint32) ([]byte, error) { var ledgerCloseMetaBatch xdr.LedgerCloseMetaBatch - objectKey, err := datastore.GetObjectKeyFromSequenceNumber( - sequence, - lb.config.LcmFileConfig.LedgersPerFile, - lb.config.LcmFileConfig.FilesPerPartition, - lb.config.LcmFileConfig.FileSuffix) - if err != nil { - return nil, errors.Wrapf(err, "failed to get object key for ledger %d", sequence) - } + config := datastore.LedgerBatchConfig{} + objectKey := config.GetObjectKeyFromSequenceNumber(sequence) - reader, err := lb.lcmDataStore.GetFile(context.Background(), objectKey) + reader, err := lb.dataStore.GetFile(context.Background(), objectKey) if err != nil { return nil, errors.Wrapf(err, "failed getting file: %s", objectKey) } @@ -285,35 +284,23 @@ func NewGCSBackend(ctx context.Context, config GCSBackendConfig) (*GCSBackend, e // GetLatestLedgerSequence returns the most recent ledger sequence number in the cloud storage bucket. func (gcsb *GCSBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - /* TODO: replace with binary search code - gcsb.gcsBackendLock.RLock() - defer gcsb.gcsBackendLock.RUnlock() - - // Get the latest parition directory from the bucket - directories, err := gcsb.lcmDataStore.ListDirectoryNames(ctx) - if err != nil { - return 0, errors.Wrapf(err, "failed getting list of directory names") - } + var err error + var archive historyarchive.ArchiveInterface - latestDirectory, err := gcsb.GetLatestDirectory(directories) - if err != nil { - return 0, errors.Wrapf(err, "failed getting latest directory") + if archive, err = datastore.CreateHistoryArchiveFromNetworkName(ctx, gcsb.network); err != nil { + return 0, err } - - // Search through the latest partition to find the latest file which would be the latestLedgerSequence - fileNames, err := gcsb.lcmDataStore.ListFileNames(ctx, latestDirectory) + resumableManager := datastore.NewResumableManager(gcsb.dataStore, gcsb.network, gcsb.ledgerBatchConfig, archive) + absentLedger, ok, err := resumableManager.FindStart(ctx, 1, 0) if err != nil { - return 0, errors.Wrapf(err, "failed getting filenames in dir %s", latestDirectory) + return 0, err } - - latestLedgerSequence, err := gcsb.GetLatestFileNameLedgerSequence(fileNames, latestDirectory) - if err != nil { - return 0, errors.Wrapf(err, "failed converting filename to ledger sequence") + if !ok { + return 0, errors.New("findStart returned sequence beyond latest history archive ledger") } - return latestLedgerSequence, nil - */ - return 0, nil + // Subtract one to get the oldest existing ledger seq closest to genesis + return absentLedger - 1, nil } // GetLedger returns the LedgerCloseMeta for the specified ledger sequence number @@ -416,57 +403,6 @@ func (gcsb *GCSBackend) Close() error { return nil } -// TODO: remove when binary search is merged -/* -// GetLatestDirectory returns the latest directory from an array of directories -func (gcsb *GCSBackend) GetLatestDirectory(directories []string) (string, error) { - var latestDirectory string - largestDirectoryLedger := 0 - - for _, dir := range directories { - // dir follows the format of "ledgers//-" - // Need to split the dir string to retrieve the ledger value to get the latest directory - dirTruncSlash := strings.TrimSuffix(dir, "/") - _, dirName := path.Split(dirTruncSlash) - parts := strings.Split(dirName, "-") - - if len(parts) == 2 { - upper, err := strconv.Atoi(parts[1]) - if err != nil { - return "", errors.Wrapf(err, "failed getting latest directory %s", dir) - } - - if upper > largestDirectoryLedger { - latestDirectory = dir - largestDirectoryLedger = upper - } - } - } - - return latestDirectory, nil -} - -// GetLatestFileNameLedgerSequence returns the lastest ledger sequence in a directory -func (gcsb *GCSBackend) GetLatestFileNameLedgerSequence(fileNames []string, directory string) (uint32, error) { - latestLedgerSequence := uint32(0) - - for _, fileName := range fileNames { - // fileName follows the format of "ledgers//-/." - // Trim the file down to just the - fileNameTrimExt := strings.TrimSuffix(fileName, gcsb.lcmFileConfig.FileSuffix) - fileNameTrimPath := strings.TrimPrefix(fileNameTrimExt, directory+"/") - ledgerSequence, err := strconv.ParseUint(fileNameTrimPath, 10, 32) - if err != nil { - return uint32(0), errors.Wrapf(err, "failed converting filename to uint32 %s", fileName) - } - - latestLedgerSequence = ordered.Max(latestLedgerSequence, uint32(ledgerSequence)) - } - - return latestLedgerSequence, nil -} -*/ - // startPreparingRange prepares the ledger range by setting the range in the ledgerBuffer func (gcsb *GCSBackend) startPreparingRange(ledgerRange Range) (bool, error) { gcsb.gcsBackendLock.Lock() diff --git a/support/datastore/datastore.go b/support/datastore/datastore.go index ab5fae9ce9..0b48a81b80 100644 --- a/support/datastore/datastore.go +++ b/support/datastore/datastore.go @@ -2,16 +2,16 @@ package datastore import ( "context" - "fmt" "io" - "strings" - "github.com/stellar/go/pkg/mod/firebase.google.com/go@v3.12.0+incompatible/storage" "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/log" - "google.golang.org/api/option" ) +type DataStoreConfig struct { + Type string `toml:"type"` + Params map[string]string `toml:"params"` +} + // DataStore defines an interface for interacting with data storage type DataStore interface { GetFile(ctx context.Context, path string) (io.ReadCloser, error) @@ -33,58 +33,4 @@ func NewDataStore(ctx context.Context, datastoreConfig DataStoreConfig, network default: return nil, errors.Errorf("Invalid datastore type %v, not supported", datastoreConfig.Type) } - - pth := parsed.Path - if parsed.Scheme != "gcs" { - return nil, errors.Errorf("Invalid destination URL %s. Expected GCS URL ", destinationURL) - } - - // Inside gcs, all paths start _without_ the leading / - pth = strings.TrimPrefix(pth, "/") - bucketName := parsed.Host - prefix := pth - - log.Infof("creating GCS client for bucket: %s, prefix: %s", bucketName, prefix) - - var options []option.ClientOption - client, err := storage.NewClient(ctx, options...) - if err != nil { - return nil, err - } - - // Check the bucket exists - bucket := client.Bucket(bucketName) - if _, err := bucket.Attrs(ctx); err != nil { - return nil, errors.Wrap(err, "failed to retrieve bucket attributes") - } - - return &GCSDataStore{client: client, bucket: bucket, prefix: prefix}, nil -} - -// GetObjectKeyFromSequenceNumber generates the file name from the ledger sequence number. -func GetObjectKeyFromSequenceNumber(ledgerSeq uint32, ledgersPerFile uint32, filesPerPartition uint32, fileSuffix string) (string, error) { - var objectKey string - - if ledgersPerFile < 1 { - return "", errors.Errorf("Invalid ledgers per file (%d): must be at least 1", ledgersPerFile) - } - - if filesPerPartition > 1 { - partitionSize := ledgersPerFile * filesPerPartition - partitionStart := (ledgerSeq / partitionSize) * partitionSize - partitionEnd := partitionStart + partitionSize - 1 - objectKey = fmt.Sprintf("%d-%d/", partitionStart, partitionEnd) - } - - fileStart := (ledgerSeq / ledgersPerFile) * ledgersPerFile - fileEnd := fileStart + ledgersPerFile - 1 - objectKey += fmt.Sprintf("%d", fileStart) - - // Multiple ledgers per file - if fileStart != fileEnd { - objectKey += fmt.Sprintf("-%d", fileEnd) - } - objectKey += fileSuffix - - return objectKey, nil } diff --git a/support/datastore/datastore_test.go b/support/datastore/datastore_test.go index b6eaf7670e..12041729be 100644 --- a/support/datastore/datastore_test.go +++ b/support/datastore/datastore_test.go @@ -1,45 +1,13 @@ package datastore import ( - "fmt" + "context" "testing" "github.com/stretchr/testify/require" ) -func TestGetObjectKeyFromSequenceNumber(t *testing.T) { - testCases := []struct { - filesPerPartition uint32 - ledgerSeq uint32 - ledgersPerFile uint32 - fileSuffix string - expectedKey string - expectedError bool - }{ - {0, 5, 1, ".xdr.gz", "5.xdr.gz", false}, - {0, 5, 10, ".xdr.gz", "0-9.xdr.gz", false}, - {2, 5, 0, ".xdr.gz", "", true}, - {2, 10, 100, ".xdr.gz", "0-199/0-99.xdr.gz", false}, - {2, 150, 50, ".xdr.gz", "100-199/150-199.xdr.gz", false}, - {2, 300, 200, ".xdr.gz", "0-399/200-399.xdr.gz", false}, - {2, 1, 1, ".xdr.gz", "0-1/1.xdr.gz", false}, - {4, 10, 100, ".xdr.gz", "0-399/0-99.xdr.gz", false}, - {4, 250, 50, ".xdr.gz", "200-399/250-299.xdr.gz", false}, - {1, 300, 200, ".xdr.gz", "200-399.xdr.gz", false}, - {1, 1, 1, ".xdr.gz", "1.xdr.gz", false}, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("LedgerSeq-%d-LedgersPerFile-%d", tc.ledgerSeq, tc.ledgersPerFile), func(t *testing.T) { - key, err := GetObjectKeyFromSequenceNumber(tc.ledgerSeq, tc.ledgersPerFile, tc.filesPerPartition, tc.fileSuffix) - - if tc.expectedError { - require.Error(t, err) - require.Empty(t, key) - } else { - require.NoError(t, err) - require.Equal(t, tc.expectedKey, key) - } - }) - } +func TestInvalidStore(t *testing.T) { + _, err := NewDataStore(context.Background(), DataStoreConfig{Type: "unknown"}, "test") + require.Error(t, err) } diff --git a/support/datastore/gcs_datastore.go b/support/datastore/gcs_datastore.go index 1c1e2cc736..59637fc21c 100644 --- a/support/datastore/gcs_datastore.go +++ b/support/datastore/gcs_datastore.go @@ -44,7 +44,7 @@ func NewGCSDataStore(ctx context.Context, params map[string]string, network stri prefix := strings.TrimPrefix(parsed.Path, "/") bucketName := parsed.Host - logger.Infof("creating GCS client for bucket: %s, prefix: %s", bucketName, prefix) + log.Infof("creating GCS client for bucket: %s, prefix: %s", bucketName, prefix) var options []option.ClientOption client, err := storage.NewClient(ctx, options...) @@ -149,46 +149,3 @@ func (b GCSDataStore) putFile(ctx context.Context, filePath string, in io.Writer } return w.Close() } - -// TODO: Remove when binary search code is added -/* -func (b *GCSDataStore) ListDirectoryNames(ctx context.Context) ([]string, error) { - var directories []string - - o := b.bucket.Objects(ctx, &storage.Query{Prefix: b.prefix + "/", Delimiter: "/"}) - for { - attrs, err := o.Next() - if err == iterator.Done { - break - } - if err != nil { - return nil, err - } - if attrs.Prefix != "" { - directories = append(directories, strings.TrimSuffix(attrs.Prefix, "/")) - } - } - - return directories, nil -} - -func (b *GCSDataStore) ListFileNames(ctx context.Context, path string) ([]string, error) { - var files []string - - o := b.bucket.Objects(ctx, &storage.Query{Prefix: path + "/", Delimiter: "/"}) - for { - attrs, err := o.Next() - if err == iterator.Done { - break - } - if err != nil { - log.Fatal(err) - } - if attrs.Name != "" && !strings.HasSuffix(attrs.Name, "/") { - files = append(files, attrs.Name) - } - } - - return files, nil -} -*/ diff --git a/exp/services/ledgerexporter/internal/ledgerbatch_config.go b/support/datastore/ledgerbatch_config.go similarity index 98% rename from exp/services/ledgerexporter/internal/ledgerbatch_config.go rename to support/datastore/ledgerbatch_config.go index 6ac3a3f36a..17c42c4ea2 100644 --- a/exp/services/ledgerexporter/internal/ledgerbatch_config.go +++ b/support/datastore/ledgerbatch_config.go @@ -1,4 +1,4 @@ -package ledgerexporter +package datastore import ( "fmt" diff --git a/exp/services/ledgerexporter/internal/ledgerbatch_config_test.go b/support/datastore/ledgerbatch_config_test.go similarity index 97% rename from exp/services/ledgerexporter/internal/ledgerbatch_config_test.go rename to support/datastore/ledgerbatch_config_test.go index cad9249dc5..e45fee109e 100644 --- a/exp/services/ledgerexporter/internal/ledgerbatch_config_test.go +++ b/support/datastore/ledgerbatch_config_test.go @@ -1,4 +1,4 @@ -package ledgerexporter +package datastore import ( "fmt" diff --git a/support/datastore/mock_datastore.go b/support/datastore/mock_datastore.go deleted file mode 100644 index 0b723b2fa3..0000000000 --- a/support/datastore/mock_datastore.go +++ /dev/null @@ -1,71 +0,0 @@ -package datastore - -import ( - "context" - "io" - - "github.com/stretchr/testify/mock" -) - -// MockDataStore is a mock implementation for the Storage interface. -type MockDataStore struct { - mock.Mock -} - -func (m *MockDataStore) Exists(ctx context.Context, path string) (bool, error) { - args := m.Called(ctx, path) - return args.Bool(0), args.Error(1) -} - -func (m *MockDataStore) Size(ctx context.Context, path string) (int64, error) { - args := m.Called(ctx, path) - return args.Get(0).(int64), args.Error(1) -} - -func (m *MockDataStore) GetFile(ctx context.Context, path string) (io.ReadCloser, error) { - args := m.Called(ctx, path) - return args.Get(0).(io.ReadCloser), args.Error(1) -} - -func (m *MockDataStore) PutFile(ctx context.Context, path string, in io.WriterTo) error { - args := m.Called(ctx, path, in) - return args.Error(0) -} - -func (m *MockDataStore) PutFileIfNotExists(ctx context.Context, path string, in io.WriterTo) (bool, error) { - args := m.Called(ctx, path, in) - return args.Get(0).(bool), args.Error(1) -} - -func (m *MockDataStore) Close() error { - args := m.Called() - return args.Error(0) -} - -<<<<<<<< HEAD:support/datastore/mock_datastore.go -// TODO: Remove when binary search code is added -/* -func (m *MockDataStore) ListDirectoryNames(ctx context.Context) ([]string, error) { - args := m.Called(ctx) - return args.Get(0).([]string), args.Error(0) -} - -func (m *MockDataStore) ListFileNames(ctx context.Context, path string) ([]string, error) { - args := m.Called(ctx, path) - return args.Get(0).([]string), args.Error(0) -} -*/ -======== -type MockResumableManager struct { - mock.Mock -} - -func (m *MockResumableManager) FindStart(ctx context.Context, start, end uint32) (absentLedger uint32, ok bool, err error) { - a := m.Called(ctx, start, end) - return a.Get(0).(uint32), a.Get(1).(bool), a.Error(2) -} - -// ensure that the MockClient implements ClientInterface -var _ DataStore = &MockDataStore{} -var _ ResumableManager = &MockResumableManager{} ->>>>>>>> master:exp/services/ledgerexporter/internal/mocks.go diff --git a/exp/services/ledgerexporter/internal/mocks.go b/support/datastore/mocks.go similarity index 100% rename from exp/services/ledgerexporter/internal/mocks.go rename to support/datastore/mocks.go index a95dfdc15c..b77040d280 100644 --- a/exp/services/ledgerexporter/internal/mocks.go +++ b/support/datastore/mocks.go @@ -52,5 +52,5 @@ func (m *MockResumableManager) FindStart(ctx context.Context, start, end uint32) } // ensure that the MockClient implements ClientInterface -var _ DataStore = &MockDataStore{} var _ ResumableManager = &MockResumableManager{} +var _ DataStore = &MockDataStore{} diff --git a/exp/services/ledgerexporter/internal/resumablemanager_test.go b/support/datastore/resumablemanager_test.go similarity index 99% rename from exp/services/ledgerexporter/internal/resumablemanager_test.go rename to support/datastore/resumablemanager_test.go index 189136932a..15b02c3be4 100644 --- a/exp/services/ledgerexporter/internal/resumablemanager_test.go +++ b/support/datastore/resumablemanager_test.go @@ -1,4 +1,4 @@ -package ledgerexporter +package datastore import ( "context" diff --git a/exp/services/ledgerexporter/internal/resumablemanager.go b/support/datastore/resumeablemanager.go similarity index 92% rename from exp/services/ledgerexporter/internal/resumablemanager.go rename to support/datastore/resumeablemanager.go index 1d832f2ea4..ce0bd12717 100644 --- a/exp/services/ledgerexporter/internal/resumablemanager.go +++ b/support/datastore/resumeablemanager.go @@ -1,4 +1,4 @@ -package ledgerexporter +package datastore import ( "context" @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" "github.com/stellar/go/historyarchive" + "github.com/stellar/go/support/log" ) type ResumableManager interface { @@ -58,18 +59,18 @@ func (rm resumableManagerService) FindStart(ctx context.Context, start, end uint return 0, false, errors.New("Invalid start value, must be greater than zero") } - log := logger.WithField("start", start).WithField("end", end).WithField("network", rm.network) + log.WithField("start", start).WithField("end", end).WithField("network", rm.network) networkLatest := uint32(0) if end < 1 { var latestErr error - networkLatest, latestErr = getLatestLedgerSequenceFromHistoryArchives(rm.archive) + networkLatest, latestErr = GetLatestLedgerSequenceFromHistoryArchives(rm.archive) if latestErr != nil { err := errors.Wrap(latestErr, "Resumability of requested export ledger range, was not able to get latest ledger from network") return 0, false, err } - networkLatest = networkLatest + (getHistoryArchivesCheckPointFrequency() * 2) - logger.Infof("Resumability computed effective latest network ledger including padding of checkpoint frequency to be %d + for network=%v", networkLatest, rm.network) + networkLatest = networkLatest + (GetHistoryArchivesCheckPointFrequency() * 2) + log.Infof("Resumability computed effective latest network ledger including padding of checkpoint frequency to be %d + for network=%v", networkLatest, rm.network) if start > networkLatest { // requested to start at a point beyond the latest network, resume not applicable. diff --git a/support/datastore/utils.go b/support/datastore/utils.go new file mode 100644 index 0000000000..bc692fb77e --- /dev/null +++ b/support/datastore/utils.go @@ -0,0 +1,48 @@ +package datastore + +import ( + "context" + + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/network" + "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/log" + "github.com/stellar/go/support/storage" +) + +const Pubnet = "pubnet" +const Testnet = "testnet" + +func CreateHistoryArchiveFromNetworkName(ctx context.Context, networkName string) (historyarchive.ArchiveInterface, error) { + var historyArchiveUrls []string + switch networkName { + case Pubnet: + historyArchiveUrls = network.PublicNetworkhistoryArchiveURLs + case Testnet: + historyArchiveUrls = network.TestNetworkhistoryArchiveURLs + default: + return nil, errors.Errorf("Invalid network name %s", networkName) + } + + return historyarchive.NewArchivePool(historyArchiveUrls, historyarchive.ArchiveOptions{ + ConnectOptions: storage.ConnectOptions{ + UserAgent: "ledger-exporter", + Context: ctx, + }, + }) +} + +func GetLatestLedgerSequenceFromHistoryArchives(archive historyarchive.ArchiveInterface) (uint32, error) { + has, err := archive.GetRootHAS() + if err != nil { + log.Error("Error getting root HAS from archives", err) + return 0, errors.Wrap(err, "failed to retrieve the latest ledger sequence from any history archive") + } + + return has.CurrentLedger, nil +} + +func GetHistoryArchivesCheckPointFrequency() uint32 { + // this could evolve to use other sources for checkpoint freq + return historyarchive.DefaultCheckpointFrequency +} diff --git a/support/priorityqueue/priorityqueue.go b/support/priorityqueue/priorityqueue.go index 43d5e2ee1d..ae8c40ed35 100644 --- a/support/priorityqueue/priorityqueue.go +++ b/support/priorityqueue/priorityqueue.go @@ -18,8 +18,8 @@ type PriorityQueue []*Item func (pq PriorityQueue) Len() int { return len(pq) } func (pq PriorityQueue) Less(i, j int) bool { - // We want Pop to give us the highest, not lowest, priority so we use greater than here. - return pq[i].Priority > pq[j].Priority + // We want Pop to give us the lowest, not highest, priority so we use less than here. + return pq[i].Priority < pq[j].Priority } func (pq PriorityQueue) Swap(i, j int) { From ed8f702caa7efe14ffebdd0efa503dd867e89039 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Wed, 1 May 2024 00:54:22 -0400 Subject: [PATCH 133/234] WIP; add heap and addressing comments --- ingest/ledgerbackend/gcs_backend.go | 146 +++++++++++++------------ support/collections/heap/heap.go | 71 ++++++++++++ support/collections/heap/heap_test.go | 32 ++++++ support/priorityqueue/priorityqueue.go | 53 --------- 4 files changed, 179 insertions(+), 123 deletions(-) create mode 100644 support/collections/heap/heap.go create mode 100644 support/collections/heap/heap_test.go delete mode 100644 support/priorityqueue/priorityqueue.go diff --git a/ingest/ledgerbackend/gcs_backend.go b/ingest/ledgerbackend/gcs_backend.go index 401f448d18..a3ff09078b 100644 --- a/ingest/ledgerbackend/gcs_backend.go +++ b/ingest/ledgerbackend/gcs_backend.go @@ -2,7 +2,6 @@ package ledgerbackend import ( "compress/gzip" - "container/heap" "context" "io" "sync" @@ -11,39 +10,48 @@ import ( "github.com/pkg/errors" "github.com/stellar/go/historyarchive" + "github.com/stellar/go/support/collections/heap" "github.com/stellar/go/support/datastore" "github.com/stellar/go/support/log" - "github.com/stellar/go/support/priorityqueue" "github.com/stellar/go/xdr" + "google.golang.org/api/googleapi" ) // Ensure GCSBackend implements LedgerBackend var _ LedgerBackend = (*GCSBackend)(nil) +// An Item is something we manage in a priority queue. +type Item struct { + Value []byte // Value of the item + Priority int // The priority of the item in the queue. +} + type LCMFileConfig struct { StorageURL string FileSuffix string - LedgersPerFile uint32 `default:"1"` - FilesPerPartition uint32 `default:"64000"` + LedgersPerFile uint32 + FilesPerPartition uint32 } type BufferConfig struct { - BufferSize uint32 `default:"1000"` - NumWorkers uint32 `default:"5"` - RetryLimit uint32 `default:"3"` - RetryWait time.Duration `default:"5"` + BufferSize uint32 + NumWorkers uint32 + RetryLimit uint32 + RetryWait time.Duration } -type GCSBackendConfig struct { - LcmFileConfig LCMFileConfig - BufferConfig BufferConfig +type gcsBackendConfig struct { + lcmFileConfig LCMFileConfig + bufferConfig BufferConfig + dataStoreConfig datastore.DataStoreConfig + network string } // GCSBackend is a ledger backend that reads from a cloud storage service. // The cloud storage service contains files generated from the ledgerExporter. type GCSBackend struct { - config GCSBackendConfig + config gcsBackendConfig // cancel is the CancelFunc for context which controls the lifetime of a GCSBackend instance. // Once it is invoked GCSBackend will not be able to stream ledgers from GCSBackend. @@ -65,45 +73,45 @@ type GCSBackend struct { } type ledgerBufferGCS struct { - config GCSBackendConfig + config gcsBackendConfig dataStore datastore.DataStore taskQueue chan uint32 ledgerQueue chan []byte - ledgerPriorityQueue priorityqueue.PriorityQueue + ledgerPriorityQueue *heap.Heap[Item] priorityQueueLock sync.Mutex count uint32 limit uint32 + cancel context.CancelFunc currentLedger uint32 nextTaskLedger uint32 nextLedgerQueueLedger uint32 - cancel context.CancelFunc - bounded bool - ledgerRange *Range + ledgerRange Range } -func NewLedgerBuffer(ctx context.Context, config GCSBackendConfig) (*ledgerBufferGCS, error) { +func (gcsb *GCSBackend) NewLedgerBuffer(ctx context.Context, ledgerRange Range) (*ledgerBufferGCS, error) { var cancel context.CancelFunc - - //lcmDataStore, err := datastore.NewDataStore(ctx, config.datastoreConfig, config.network) - //if err != nil { - // return nil, err - //} - - pq := make(priorityqueue.PriorityQueue, config.BufferConfig.BufferSize) - heap.Init(&pq) + less := func(a, b Item) bool { + return a.Priority < b.Priority + } + pq := heap.New(less, int(gcsb.config.bufferConfig.BufferSize)) ledgerBuffer := &ledgerBufferGCS{ - //lcmDataStore: lcmDataStore, - taskQueue: make(chan uint32, config.BufferConfig.BufferSize), - ledgerQueue: make(chan []byte, config.BufferConfig.BufferSize), - ledgerPriorityQueue: pq, - count: 0, - limit: config.BufferConfig.BufferSize, - cancel: cancel, + config: gcsb.config, + dataStore: gcsb.dataStore, + taskQueue: make(chan uint32, gcsb.config.bufferConfig.BufferSize), + ledgerQueue: make(chan []byte, gcsb.config.bufferConfig.BufferSize), + ledgerPriorityQueue: pq, + count: 0, + limit: gcsb.config.bufferConfig.BufferSize, + cancel: cancel, + currentLedger: ledgerRange.from, + nextTaskLedger: ledgerRange.from, + nextLedgerQueueLedger: ledgerRange.from, + ledgerRange: ledgerRange, } // Workers to read LCM files - for i := uint32(0); i < config.BufferConfig.NumWorkers; i++ { + for i := uint32(0); i < gcsb.config.bufferConfig.NumWorkers; i++ { go ledgerBuffer.worker() } @@ -115,6 +123,10 @@ func NewLedgerBuffer(ctx context.Context, config GCSBackendConfig) (*ledgerBuffe func (lb *ledgerBufferGCS) pushTaskQueue() { for lb.count <= lb.limit { + // In bounded mode, don't queue past the end ledger + if lb.ledgerRange.to < lb.nextTaskLedger && lb.ledgerRange.bounded { + return + } lb.taskQueue <- lb.nextTaskLedger lb.nextTaskLedger++ lb.count++ @@ -124,28 +136,27 @@ func (lb *ledgerBufferGCS) pushTaskQueue() { func (lb *ledgerBufferGCS) worker() { for sequence := range lb.taskQueue { retryCount := uint32(0) - for retryCount <= lb.config.BufferConfig.RetryLimit { + for retryCount <= lb.config.bufferConfig.RetryLimit { ledgerObject, err := lb.getLedgerGCSObject(sequence) if err != nil { if e, ok := err.(*googleapi.Error); ok { // ledgerObject not found and unbounded - if e.Code == 404 && !lb.bounded { - time.Sleep(lb.config.BufferConfig.RetryWait * time.Second) + if e.Code == 404 && !lb.ledgerRange.bounded { + time.Sleep(lb.config.bufferConfig.RetryWait * time.Second) continue } } retryCount++ - time.Sleep(lb.config.BufferConfig.RetryWait * time.Second) + time.Sleep(lb.config.bufferConfig.RetryWait * time.Second) } // Add to priority queue and continue to next task lb.priorityQueueLock.Lock() - item := &priorityqueue.Item{ + item := Item{ Value: ledgerObject, Priority: int(sequence), } - heap.Push(&lb.ledgerPriorityQueue, item) - lb.ledgerPriorityQueue.Update(item, item.Value, int(sequence)) + lb.ledgerPriorityQueue.Push(item) lb.priorityQueueLock.Unlock() break } @@ -213,8 +224,8 @@ func (lb *ledgerBufferGCS) reorderLedgers() { } // Check if the nextLedger is the next item in the priority queue - for lb.currentLedger == uint32(lb.ledgerPriorityQueue[0].Priority) { - item := heap.Pop(&lb.ledgerPriorityQueue).(*priorityqueue.Item) + for lb.currentLedger == uint32(lb.ledgerPriorityQueue.Peek().Priority) { + item := lb.ledgerPriorityQueue.Pop() lb.ledgerQueue <- item.Value lb.nextLedgerQueueLedger++ } @@ -239,44 +250,44 @@ func (lb *ledgerBufferGCS) getFromLedgerQueue(ctx context.Context) ([]byte, erro } // Return a new GCSBackend instance. -func NewGCSBackend(ctx context.Context, config GCSBackendConfig) (*GCSBackend, error) { +func NewGCSBackend(ctx context.Context, config gcsBackendConfig) (*GCSBackend, error) { // Check/set minimum config values - if config.LcmFileConfig.StorageURL == "" { + if config.lcmFileConfig.StorageURL == "" { return nil, errors.New("fileConfig.storageURL is not set") } - if config.LcmFileConfig.FileSuffix == "" { + if config.lcmFileConfig.FileSuffix == "" { return nil, errors.New("fileConfig.FileSuffix is not set") } - if config.LcmFileConfig.LedgersPerFile == 0 { - config.LcmFileConfig.LedgersPerFile = 1 + if config.lcmFileConfig.LedgersPerFile == 0 { + config.lcmFileConfig.LedgersPerFile = 1 } - if config.LcmFileConfig.FilesPerPartition == 0 { - config.LcmFileConfig.FilesPerPartition = 1 + if config.lcmFileConfig.FilesPerPartition == 0 { + config.lcmFileConfig.FilesPerPartition = 1 } // Check/set minimum config values - if config.BufferConfig.BufferSize == 0 { - config.BufferConfig.BufferSize = 1 + if config.bufferConfig.BufferSize == 0 { + config.bufferConfig.BufferSize = 1 } - if config.BufferConfig.NumWorkers == 0 { - config.BufferConfig.NumWorkers = 1 + if config.bufferConfig.NumWorkers == 0 { + config.bufferConfig.NumWorkers = 1 } var cancel context.CancelFunc - ledgerBuffer, err := NewLedgerBuffer(ctx, config) + dataStore, err := datastore.NewDataStore(ctx, config.dataStoreConfig, config.network) if err != nil { return nil, err } gcsBackend := &GCSBackend{ - config: config, - cancel: cancel, - ledgerBuffer: ledgerBuffer, + config: config, + cancel: cancel, + dataStore: dataStore, } return gcsBackend, nil @@ -336,7 +347,7 @@ func (gcsb *GCSBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.Led // PrepareRange checks if the starting and ending (if bounded) ledgers exist. func (gcsb *GCSBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { - if alreadyPrepared, err := gcsb.startPreparingRange(ledgerRange); err != nil { + if alreadyPrepared, err := gcsb.startPreparingRange(ctx, ledgerRange); err != nil { return errors.Wrap(err, "error starting prepare range") } else if alreadyPrepared { return nil @@ -364,10 +375,6 @@ func (gcsb *GCSBackend) isPrepared(ledgerRange Range) bool { return false } - if gcsb.ledgerBuffer.ledgerRange == nil { - return false - } - if gcsb.ledgerBuffer.ledgerRange.from > ledgerRange.from { return false } @@ -404,7 +411,7 @@ func (gcsb *GCSBackend) Close() error { } // startPreparingRange prepares the ledger range by setting the range in the ledgerBuffer -func (gcsb *GCSBackend) startPreparingRange(ledgerRange Range) (bool, error) { +func (gcsb *GCSBackend) startPreparingRange(ctx context.Context, ledgerRange Range) (bool, error) { gcsb.gcsBackendLock.Lock() defer gcsb.gcsBackendLock.Unlock() @@ -412,12 +419,11 @@ func (gcsb *GCSBackend) startPreparingRange(ledgerRange Range) (bool, error) { return true, nil } - // Set the ledgerRange in ledgerBuffer - gcsb.ledgerBuffer.ledgerRange = &ledgerRange - gcsb.ledgerBuffer.currentLedger = ledgerRange.from - gcsb.ledgerBuffer.nextTaskLedger = ledgerRange.from - gcsb.ledgerBuffer.nextLedgerQueueLedger = ledgerRange.from - gcsb.ledgerBuffer.bounded = ledgerRange.bounded + var err error + gcsb.ledgerBuffer, err = gcsb.NewLedgerBuffer(ctx, ledgerRange) + if err != nil { + return false, err + } // Start the ledgerBuffer gcsb.ledgerBuffer.pushTaskQueue() diff --git a/support/collections/heap/heap.go b/support/collections/heap/heap.go new file mode 100644 index 0000000000..66d86ee493 --- /dev/null +++ b/support/collections/heap/heap.go @@ -0,0 +1,71 @@ +package heap + +import "container/heap" + +// A Heap is a min-heap backed by a slice. +type Heap[E any] struct { + s sliceHeap[E] +} + +// New constructs a new Heap with a comparison function. +func New[E any](less func(E, E) bool, capacity int) *Heap[E] { + return &Heap[E]{sliceHeap[E]{ + s: make([]E, 0, capacity), + less: less, + }} +} + +// Push pushes an element onto the heap. The complexity is O(log n) +// where n = h.Len(). +func (h *Heap[E]) Push(elem E) { + heap.Push(&h.s, elem) +} + +// Pop removes and returns the minimum element (according to the less function) +// from the heap. Pop panics if the heap is empty. +// The complexity is O(log n) where n = h.Len(). +func (h *Heap[E]) Pop() E { + return heap.Pop(&h.s).(E) +} + +// Peek returns the minimum element (according to the less function) in the heap. +// Peek panics if the heap is empty. +// The complexity is O(1). +func (h *Heap[E]) Peek() E { + return h.s.s[0] +} + +// Len returns the number of elements in the heap. +func (h *Heap[E]) Len() int { + return len(h.s.s) +} + +// sliceHeap just exists to use the existing heap.Interface as the +// implementation of Heap. +type sliceHeap[E any] struct { + s []E + less func(E, E) bool +} + +func (s *sliceHeap[E]) Len() int { return len(s.s) } + +func (s *sliceHeap[E]) Swap(i, j int) { + s.s[i], s.s[j] = s.s[j], s.s[i] +} + +func (s *sliceHeap[E]) Less(i, j int) bool { + return s.less(s.s[i], s.s[j]) +} + +func (s *sliceHeap[E]) Push(x interface{}) { + s.s = append(s.s, x.(E)) +} + +func (s *sliceHeap[E]) Pop() interface{} { + var zero E + e := s.s[len(s.s)-1] + // avoid memory leak by clearing out popped value in slice + s.s[len(s.s)-1] = zero + s.s = s.s[:len(s.s)-1] + return e +} diff --git a/support/collections/heap/heap_test.go b/support/collections/heap/heap_test.go new file mode 100644 index 0000000000..100619f2a5 --- /dev/null +++ b/support/collections/heap/heap_test.go @@ -0,0 +1,32 @@ +package heap + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHeapPushAndPop(t *testing.T) { + less := func(a, b int) bool { return a < b } + h := New(less, 0) + + h.Push(3) + h.Push(1) + h.Push(2) + + assert.Equal(t, 1, h.Pop()) + assert.Equal(t, 2, h.Pop()) + assert.Equal(t, 3, h.Pop()) +} + +func TestHeapPeek(t *testing.T) { + less := func(a, b int) bool { return a < b } + h := New(less, 0) + + h.Push(3) + h.Push(1) + h.Push(2) + + assert.Equal(t, 1, h.Peek()) + assert.Equal(t, 1, h.Pop()) +} diff --git a/support/priorityqueue/priorityqueue.go b/support/priorityqueue/priorityqueue.go deleted file mode 100644 index ae8c40ed35..0000000000 --- a/support/priorityqueue/priorityqueue.go +++ /dev/null @@ -1,53 +0,0 @@ -package priorityqueue - -import ( - "container/heap" -) - -// An Item is something we manage in a priority queue. -type Item struct { - Value []byte // Value of the item - Priority int // The priority of the item in the queue. - // The index is needed by update and is maintained by the heap.Interface methods. - Index int // The index of the item in the heap. -} - -// A PriorityQueue implements heap.Interface and holds Items. -type PriorityQueue []*Item - -func (pq PriorityQueue) Len() int { return len(pq) } - -func (pq PriorityQueue) Less(i, j int) bool { - // We want Pop to give us the lowest, not highest, priority so we use less than here. - return pq[i].Priority < pq[j].Priority -} - -func (pq PriorityQueue) Swap(i, j int) { - pq[i], pq[j] = pq[j], pq[i] - pq[i].Index = i - pq[j].Index = j -} - -func (pq *PriorityQueue) Push(x any) { - n := len(*pq) - item := x.(*Item) - item.Index = n - *pq = append(*pq, item) -} - -func (pq *PriorityQueue) Pop() any { - old := *pq - n := len(old) - item := old[n-1] - old[n-1] = nil // avoid memory leak - item.Index = -1 // for safety - *pq = old[0 : n-1] - return item -} - -// update modifies the priority and value of an Item in the queue. -func (pq *PriorityQueue) Update(item *Item, value []byte, priority int) { - item.Value = value - item.Priority = priority - heap.Fix(pq, item.Index) -} From af00f1c342c2b13253fa96bfb4db77bf01f37c30 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Wed, 1 May 2024 21:18:11 -0400 Subject: [PATCH 134/234] WIP --- exp/services/ledgerexporter/config.toml | 1 + .../ledgerexporter/internal/app_test.go | 8 +- .../ledgerexporter/internal/config_test.go | 1 + .../ledgerexporter/internal/exportmanager.go | 4 +- .../internal/exportmanager_test.go | 55 ++-- exp/services/ledgerexporter/internal/queue.go | 9 +- .../ledgerexporter/internal/queue_test.go | 7 +- .../internal/test/10perfile.toml | 3 +- .../internal/test/15perfile.toml | 3 +- .../internal/test/1perfile.toml | 3 +- .../internal/test/64perfile.toml | 3 +- .../internal/test/no_network.toml | 3 +- .../ledgerexporter/internal/test/test.toml | 3 +- .../internal/test/validate_start_end.toml | 3 +- .../ledgerexporter/internal/uploader.go | 16 +- .../ledgerexporter/internal/uploader_test.go | 32 +- ingest/ledgerbackend/gcs_backend.go | 288 +++++++++--------- ingest/ledgerbackend/toml.go | 2 +- support/collections/heap/heap_test.go | 17 ++ support/compressxdr/compress_xdr.go | 38 +++ .../compressxdr/compress_xdr_test.go | 4 +- .../compressxdr/gzip_compress_xdr.go | 18 +- support/datastore/datastore.go | 3 - .../{utils.go => history_archive.go} | 0 .../datastore}/ledger_meta_archive.go | 55 +++- .../datastore}/ledger_meta_archive_test.go | 2 +- support/datastore/ledgerbatch_config.go | 7 +- 27 files changed, 357 insertions(+), 231 deletions(-) create mode 100644 support/compressxdr/compress_xdr.go rename exp/services/ledgerexporter/internal/encoder_test.go => support/compressxdr/compress_xdr_test.go (92%) rename exp/services/ledgerexporter/internal/encoder.go => support/compressxdr/gzip_compress_xdr.go (68%) rename support/datastore/{utils.go => history_archive.go} (100%) rename {exp/services/ledgerexporter/internal => support/datastore}/ledger_meta_archive.go (50%) rename {exp/services/ledgerexporter/internal => support/datastore}/ledger_meta_archive_test.go (98%) diff --git a/exp/services/ledgerexporter/config.toml b/exp/services/ledgerexporter/config.toml index a56087427c..bc522ff6a8 100644 --- a/exp/services/ledgerexporter/config.toml +++ b/exp/services/ledgerexporter/config.toml @@ -9,6 +9,7 @@ destination_bucket_path = "exporter-test/ledgers" [exporter_config] ledgers_per_file = 1 files_per_partition = 64000 + file_suffix = ".xdr.gz" [stellar_core_config] stellar_core_binary_path = "/usr/local/bin/stellar-core" diff --git a/exp/services/ledgerexporter/internal/app_test.go b/exp/services/ledgerexporter/internal/app_test.go index 2063fd21a2..e2db0e28d9 100644 --- a/exp/services/ledgerexporter/internal/app_test.go +++ b/exp/services/ledgerexporter/internal/app_test.go @@ -41,7 +41,7 @@ func TestApplyResumeInvalidDataStoreLedgersPerFileBoundary(t *testing.T) { StartLedger: 3, EndLedger: 9, Resume: true, - LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50, FileSuffix: ".xdr.gz"}, } mockResumableManager := &datastore.MockResumableManager{} // simulate the datastore has inconsistent data, @@ -61,7 +61,7 @@ func TestApplyResumeWithPartialRemoteDataPresent(t *testing.T) { StartLedger: 10, EndLedger: 99, Resume: true, - LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50, FileSuffix: ".xdr.gz"}, } mockResumableManager := &datastore.MockResumableManager{} // simulates a data store that had ledger files populated up to seq=49, so the first absent ledger would be 50 @@ -80,7 +80,7 @@ func TestApplyResumeWithNoRemoteDataPresent(t *testing.T) { StartLedger: 10, EndLedger: 99, Resume: true, - LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50, FileSuffix: ".xdr.gz"}, } mockResumableManager := &datastore.MockResumableManager{} // simulates a data store that had no data in the requested range @@ -102,7 +102,7 @@ func TestApplyResumeWithNoRemoteDataAndRequestFromGenesis(t *testing.T) { StartLedger: 2, EndLedger: 99, Resume: true, - LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50, FileSuffix: ".xdr.gz"}, } mockResumableManager := &datastore.MockResumableManager{} // simulates a data store that had no data in the requested range diff --git a/exp/services/ledgerexporter/internal/config_test.go b/exp/services/ledgerexporter/internal/config_test.go index 86f6cfb5b3..036c121455 100644 --- a/exp/services/ledgerexporter/internal/config_test.go +++ b/exp/services/ledgerexporter/internal/config_test.go @@ -22,6 +22,7 @@ func TestNewConfigResumeEnabled(t *testing.T) { require.Equal(t, config.DataStoreConfig.Type, "ABC") require.Equal(t, config.LedgerBatchConfig.FilesPerPartition, uint32(1)) require.Equal(t, config.LedgerBatchConfig.LedgersPerFile, uint32(3)) + require.Equal(t, config.LedgerBatchConfig.FileSuffix, ".xdr.gz") require.True(t, config.Resume) url, ok := config.DataStoreConfig.Params["destination_bucket_path"] require.True(t, ok) diff --git a/exp/services/ledgerexporter/internal/exportmanager.go b/exp/services/ledgerexporter/internal/exportmanager.go index 58af651c06..5ee06b8cd6 100644 --- a/exp/services/ledgerexporter/internal/exportmanager.go +++ b/exp/services/ledgerexporter/internal/exportmanager.go @@ -15,7 +15,7 @@ import ( type ExportManager struct { config datastore.LedgerBatchConfig ledgerBackend ledgerbackend.LedgerBackend - currentMetaArchive *LedgerMetaArchive + currentMetaArchive *datastore.LedgerMetaArchive queue UploadQueue latestLedgerMetric *prometheus.GaugeVec } @@ -61,7 +61,7 @@ func (e *ExportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta } // Create a new LedgerMetaArchive and add it to the map. - e.currentMetaArchive = NewLedgerMetaArchive(objectKey, ledgerSeq, endSeq) + e.currentMetaArchive = datastore.NewLedgerMetaArchive(objectKey, ledgerSeq, endSeq) } if err := e.currentMetaArchive.AddLedger(ledgerCloseMeta); err != nil { diff --git a/exp/services/ledgerexporter/internal/exportmanager_test.go b/exp/services/ledgerexporter/internal/exportmanager_test.go index 0fb3011266..01e3595373 100644 --- a/exp/services/ledgerexporter/internal/exportmanager_test.go +++ b/exp/services/ledgerexporter/internal/exportmanager_test.go @@ -39,7 +39,7 @@ func (s *ExportManagerSuite) TearDownTest() { } func (s *ExportManagerSuite) TestInvalidExportConfig() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 0, FilesPerPartition: 10} + config := datastore.LedgerBatchConfig{LedgersPerFile: 0, FilesPerPartition: 10, FileSuffix: ".xdr.gz"} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) _, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -47,7 +47,7 @@ func (s *ExportManagerSuite) TestInvalidExportConfig() { } func (s *ExportManagerSuite) TestRun() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 64, FilesPerPartition: 10} + config := datastore.LedgerBatchConfig{LedgersPerFile: 64, FilesPerPartition: 10, FileSuffix: ".xdr.gz"} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -58,7 +58,7 @@ func (s *ExportManagerSuite) TestRun() { expectedKeys := set.NewSet[string](10) for i := start; i <= end; i++ { s.mockBackend.On("GetLedger", s.ctx, i). - Return(createLedgerCloseMeta(i), nil) + Return(datastore.CreateLedgerCloseMeta(i), nil) key := config.GetObjectKeyFromSequenceNumber(i) expectedKeys.Add(key) } @@ -74,7 +74,7 @@ func (s *ExportManagerSuite) TestRun() { if !ok { break } - actualKeys.Add(v.objectKey) + actualKeys.Add(v.ObjectKey) } }() @@ -97,7 +97,7 @@ func (s *ExportManagerSuite) TestRun() { } func (s *ExportManagerSuite) TestRunContextCancel() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 1} + config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 1, FileSuffix: ".xdr.gz"} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -105,7 +105,7 @@ func (s *ExportManagerSuite) TestRunContextCancel() { ctx, cancel := context.WithCancel(context.Background()) s.mockBackend.On("GetLedger", mock.Anything, mock.Anything). - Return(createLedgerCloseMeta(1), nil) + Return(datastore.CreateLedgerCloseMeta(1), nil) go func() { <-time.After(time.Second * 1) @@ -126,7 +126,7 @@ func (s *ExportManagerSuite) TestRunContextCancel() { } func (s *ExportManagerSuite) TestRunWithCanceledContext() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} + config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10, FileSuffix: ".xdr.gz"} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -143,23 +143,24 @@ func (s *ExportManagerSuite) TestGetObjectKeyFromSequenceNumber() { filesPerPartition uint32 ledgerSeq uint32 ledgersPerFile uint32 + fileSuffix string expectedKey string }{ - {0, 5, 1, "5.xdr.gz"}, - {0, 5, 10, "0-9.xdr.gz"}, - {2, 10, 100, "0-199/0-99.xdr.gz"}, - {2, 150, 50, "100-199/150-199.xdr.gz"}, - {2, 300, 200, "0-399/200-399.xdr.gz"}, - {2, 1, 1, "0-1/1.xdr.gz"}, - {4, 10, 100, "0-399/0-99.xdr.gz"}, - {4, 250, 50, "200-399/250-299.xdr.gz"}, - {1, 300, 200, "200-399.xdr.gz"}, - {1, 1, 1, "1.xdr.gz"}, + {0, 5, 1, ".xdr.gz", "5.xdr.gz"}, + {0, 5, 10, ".xdr.gz", "0-9.xdr.gz"}, + {2, 10, 100, ".xdr.gz", "0-199/0-99.xdr.gz"}, + {2, 150, 50, ".xdr.gz", "100-199/150-199.xdr.gz"}, + {2, 300, 200, ".xdr.gz", "0-399/200-399.xdr.gz"}, + {2, 1, 1, ".xdr.gz", "0-1/1.xdr.gz"}, + {4, 10, 100, ".xdr.gz", "0-399/0-99.xdr.gz"}, + {4, 250, 50, ".xdr.gz", "200-399/250-299.xdr.gz"}, + {1, 300, 200, ".xdr.gz", "200-399.xdr.gz"}, + {1, 1, 1, ".xdr.gz", "1.xdr.gz"}, } for _, tc := range testCases { s.T().Run(fmt.Sprintf("LedgerSeq-%d-LedgersPerFile-%d", tc.ledgerSeq, tc.ledgersPerFile), func(t *testing.T) { - config := datastore.LedgerBatchConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile} + config := datastore.LedgerBatchConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile, FileSuffix: tc.fileSuffix} key := config.GetObjectKeyFromSequenceNumber(tc.ledgerSeq) require.Equal(t, tc.expectedKey, key) }) @@ -167,7 +168,7 @@ func (s *ExportManagerSuite) TestGetObjectKeyFromSequenceNumber() { } func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} + config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10, FileSuffix: ".xdr.gz"} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -186,14 +187,14 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { if !ok { break } - actualKeys.Add(v.objectKey) + actualKeys.Add(v.ObjectKey) } }() start := uint32(0) end := uint32(255) for i := start; i <= end; i++ { - require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(i))) + require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), datastore.CreateLedgerCloseMeta(i))) key := config.GetObjectKeyFromSequenceNumber(i) expectedkeys.Add(key) @@ -205,7 +206,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { } func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} + config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10, FileSuffix: ".xdr.gz"} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -217,19 +218,19 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { cancel() }() - require.NoError(s.T(), exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(1))) - err = exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(2)) + require.NoError(s.T(), exporter.AddLedgerCloseMeta(ctx, datastore.CreateLedgerCloseMeta(1))) + err = exporter.AddLedgerCloseMeta(ctx, datastore.CreateLedgerCloseMeta(2)) require.EqualError(s.T(), err, "context canceled") } func (s *ExportManagerSuite) TestAddLedgerCloseMetaKeyMismatch() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 1} + config := datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 1, FileSuffix: ".xdr.gz"} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) require.NoError(s.T(), err) - require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(16))) - require.EqualError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(21)), + require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), datastore.CreateLedgerCloseMeta(16))) + require.EqualError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), datastore.CreateLedgerCloseMeta(21)), "Current meta archive object key mismatch") } diff --git a/exp/services/ledgerexporter/internal/queue.go b/exp/services/ledgerexporter/internal/queue.go index 372ccb0056..5b46b202ee 100644 --- a/exp/services/ledgerexporter/internal/queue.go +++ b/exp/services/ledgerexporter/internal/queue.go @@ -4,11 +4,12 @@ import ( "context" "github.com/prometheus/client_golang/prometheus" + "github.com/stellar/go/support/datastore" ) // UploadQueue is a queue of LedgerMetaArchive objects which are scheduled for upload type UploadQueue struct { - metaArchiveCh chan *LedgerMetaArchive + metaArchiveCh chan *datastore.LedgerMetaArchive queueLengthMetric prometheus.Gauge } @@ -22,13 +23,13 @@ func NewUploadQueue(size int, prometheusRegistry *prometheus.Registry) UploadQue }) prometheusRegistry.MustRegister(queueLengthMetric) return UploadQueue{ - metaArchiveCh: make(chan *LedgerMetaArchive, size), + metaArchiveCh: make(chan *datastore.LedgerMetaArchive, size), queueLengthMetric: queueLengthMetric, } } // Enqueue will add an upload task to the queue. Enqueue may block if the queue is full. -func (u UploadQueue) Enqueue(ctx context.Context, archive *LedgerMetaArchive) error { +func (u UploadQueue) Enqueue(ctx context.Context, archive *datastore.LedgerMetaArchive) error { u.queueLengthMetric.Inc() select { case u.metaArchiveCh <- archive: @@ -39,7 +40,7 @@ func (u UploadQueue) Enqueue(ctx context.Context, archive *LedgerMetaArchive) er } // Dequeue will pop a task off the queue. Dequeue may block if the queue is empty. -func (u UploadQueue) Dequeue(ctx context.Context) (*LedgerMetaArchive, bool, error) { +func (u UploadQueue) Dequeue(ctx context.Context) (*datastore.LedgerMetaArchive, bool, error) { select { case <-ctx.Done(): return nil, false, ctx.Err() diff --git a/exp/services/ledgerexporter/internal/queue_test.go b/exp/services/ledgerexporter/internal/queue_test.go index a791d29eae..4f5fdd263f 100644 --- a/exp/services/ledgerexporter/internal/queue_test.go +++ b/exp/services/ledgerexporter/internal/queue_test.go @@ -6,6 +6,7 @@ import ( "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" + "github.com/stellar/go/support/datastore" "github.com/stretchr/testify/require" ) @@ -31,9 +32,9 @@ func getMetricValue(metric prometheus.Metric) *dto.Metric { func TestQueue(t *testing.T) { queue := NewUploadQueue(3, prometheus.NewRegistry()) - require.NoError(t, queue.Enqueue(context.Background(), NewLedgerMetaArchive("test", 1, 1))) - require.NoError(t, queue.Enqueue(context.Background(), NewLedgerMetaArchive("test", 2, 2))) - require.NoError(t, queue.Enqueue(context.Background(), NewLedgerMetaArchive("test", 3, 3))) + require.NoError(t, queue.Enqueue(context.Background(), datastore.NewLedgerMetaArchive("test", 1, 1))) + require.NoError(t, queue.Enqueue(context.Background(), datastore.NewLedgerMetaArchive("test", 2, 2))) + require.NoError(t, queue.Enqueue(context.Background(), datastore.NewLedgerMetaArchive("test", 3, 3))) require.Equal(t, float64(3), getMetricValue(queue.queueLengthMetric).GetGauge().GetValue()) queue.Close() diff --git a/exp/services/ledgerexporter/internal/test/10perfile.toml b/exp/services/ledgerexporter/internal/test/10perfile.toml index 8629bf4b5b..9b96927804 100644 --- a/exp/services/ledgerexporter/internal/test/10perfile.toml +++ b/exp/services/ledgerexporter/internal/test/10perfile.toml @@ -1,4 +1,5 @@ network = "test" [exporter_config] -ledgers_per_file = 10 \ No newline at end of file +ledgers_per_file = 10 +file_suffix = ".xdr.gz" \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/15perfile.toml b/exp/services/ledgerexporter/internal/test/15perfile.toml index eb76bac4d8..94df87a2e2 100644 --- a/exp/services/ledgerexporter/internal/test/15perfile.toml +++ b/exp/services/ledgerexporter/internal/test/15perfile.toml @@ -1,4 +1,5 @@ network = "test" [exporter_config] -ledgers_per_file = 15 \ No newline at end of file +ledgers_per_file = 15 +file_suffix = ".xdr.gz" \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/1perfile.toml b/exp/services/ledgerexporter/internal/test/1perfile.toml index cadef50df8..a15dc9bf41 100644 --- a/exp/services/ledgerexporter/internal/test/1perfile.toml +++ b/exp/services/ledgerexporter/internal/test/1perfile.toml @@ -1,4 +1,5 @@ network = "test" [exporter_config] -ledgers_per_file = 1 \ No newline at end of file +ledgers_per_file = 1 +file_suffix = ".xdr.gz" \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/64perfile.toml b/exp/services/ledgerexporter/internal/test/64perfile.toml index f4e30a71c0..8e8d122fcc 100644 --- a/exp/services/ledgerexporter/internal/test/64perfile.toml +++ b/exp/services/ledgerexporter/internal/test/64perfile.toml @@ -1,4 +1,5 @@ network = "test" [exporter_config] -ledgers_per_file = 64 \ No newline at end of file +ledgers_per_file = 64 +file_suffix = ".xdr.gz" \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/no_network.toml b/exp/services/ledgerexporter/internal/test/no_network.toml index f5815b9b9d..1cb591cdd4 100644 --- a/exp/services/ledgerexporter/internal/test/no_network.toml +++ b/exp/services/ledgerexporter/internal/test/no_network.toml @@ -7,4 +7,5 @@ destination_bucket_path = "your-bucket-name/subpath" [exporter_config] ledgers_per_file = 3 -files_per_partition = 1 \ No newline at end of file +files_per_partition = 1 +file_suffix = ".xdr.gz" \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/test.toml b/exp/services/ledgerexporter/internal/test/test.toml index 49d62384d7..58b5fc6df6 100644 --- a/exp/services/ledgerexporter/internal/test/test.toml +++ b/exp/services/ledgerexporter/internal/test/test.toml @@ -8,4 +8,5 @@ destination_bucket_path = "your-bucket-name/subpath" [exporter_config] ledgers_per_file = 3 -files_per_partition = 1 \ No newline at end of file +files_per_partition = 1 +file_suffix = ".xdr.gz" \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/validate_start_end.toml b/exp/services/ledgerexporter/internal/test/validate_start_end.toml index cadef50df8..a15dc9bf41 100644 --- a/exp/services/ledgerexporter/internal/test/validate_start_end.toml +++ b/exp/services/ledgerexporter/internal/test/validate_start_end.toml @@ -1,4 +1,5 @@ network = "test" [exporter_config] -ledgers_per_file = 1 \ No newline at end of file +ledgers_per_file = 1 +file_suffix = ".xdr.gz" \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/uploader.go b/exp/services/ledgerexporter/internal/uploader.go index f7db44abd2..ca07965b85 100644 --- a/exp/services/ledgerexporter/internal/uploader.go +++ b/exp/services/ledgerexporter/internal/uploader.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" + "github.com/stellar/go/support/compressxdr" "github.com/stellar/go/support/datastore" ) @@ -77,13 +78,20 @@ func (r *writerToRecorder) WriteTo(w io.Writer) (int64, error) { } // Upload uploads the serialized binary data of ledger TxMeta to the specified destination. -func (u Uploader) Upload(ctx context.Context, metaArchive *LedgerMetaArchive) error { +func (u Uploader) Upload(ctx context.Context, metaArchive *datastore.LedgerMetaArchive) error { logger.Infof("Uploading: %s", metaArchive.GetObjectKey()) startTime := time.Now() numLedgers := strconv.FormatUint(uint64(metaArchive.GetLedgerCount()), 10) + // TODO: Add compression config and optimize best compression algorithm + // JIRA https://stellarorg.atlassian.net/browse/HUBBLE-368 + xdrEncoder, err := compressxdr.NewXDREncoder(compressxdr.GZIP, &metaArchive.Data) + if err != nil { + return err + } + writerTo := &writerToRecorder{ - WriterTo: &XDRGzipEncoder{XdrPayload: &metaArchive.data}, + WriterTo: xdrEncoder, } ok, err := u.dataStore.PutFileIfNotExists(ctx, metaArchive.GetObjectKey(), writerTo) if err != nil { @@ -101,7 +109,7 @@ func (u Uploader) Upload(ctx context.Context, metaArchive *LedgerMetaArchive) er "already_exists": alreadyExists, }).Observe(float64(writerTo.totalUncompressed)) u.objectSizeMetrics.With(prometheus.Labels{ - "compression": "gzip", + "compression": compressxdr.GZIP, "ledgers": numLedgers, "already_exists": alreadyExists, }).Observe(float64(writerTo.totalCompressed)) @@ -137,6 +145,6 @@ func (u Uploader) Run(ctx context.Context) error { if err = u.Upload(uploadCtx, metaObject); err != nil { return err } - logger.Infof("Uploaded %s successfully", metaObject.objectKey) + logger.Infof("Uploaded %s successfully", metaObject.ObjectKey) } } diff --git a/exp/services/ledgerexporter/internal/uploader_test.go b/exp/services/ledgerexporter/internal/uploader_test.go index 6c4dddade4..ee39245486 100644 --- a/exp/services/ledgerexporter/internal/uploader_test.go +++ b/exp/services/ledgerexporter/internal/uploader_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "github.com/stellar/go/support/compressxdr" "github.com/stellar/go/support/datastore" "github.com/stellar/go/support/errors" "github.com/stretchr/testify/mock" @@ -40,9 +41,9 @@ func (s *UploaderSuite) TestUpload() { func (s *UploaderSuite) testUpload(putOkReturnVal bool) { key, start, end := "test-1-100", uint32(1), uint32(100) - archive := NewLedgerMetaArchive(key, start, end) + archive := datastore.NewLedgerMetaArchive(key, start, end) for i := start; i <= end; i++ { - _ = archive.AddLedger(createLedgerCloseMeta(i)) + _ = archive.AddLedger(datastore.CreateLedgerCloseMeta(i)) } var capturedBuf bytes.Buffer @@ -60,14 +61,17 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { require.NoError(s.T(), dataUploader.Upload(context.Background(), archive)) expectedCompressedLength := capturedBuf.Len() - var decodedArchive LedgerMetaArchive - decoder := &XDRGzipDecoder{XdrPayload: &decodedArchive.data} - _, err := decoder.ReadFrom(&capturedBuf) + var decodedArchive datastore.LedgerMetaArchive + xdrDecoder, err := compressxdr.NewXDRDecoder(compressxdr.GZIP, &decodedArchive.Data) + require.NoError(s.T(), err) + + decoder := xdrDecoder + _, err = decoder.ReadFrom(&capturedBuf) require.NoError(s.T(), err) // require that the decoded data matches the original test data require.Equal(s.T(), key, capturedKey) - require.Equal(s.T(), archive.data, decodedArchive.data) + require.Equal(s.T(), archive.Data, decodedArchive.Data) alreadyExists := !putOkReturnVal metric, err := dataUploader.uploadDurationMetric.MetricVec.GetMetricWith(prometheus.Labels{ @@ -94,7 +98,7 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { metric, err = dataUploader.objectSizeMetrics.MetricVec.GetMetricWith(prometheus.Labels{ "ledgers": "100", - "compression": "gzip", + "compression": compressxdr.GZIP, "already_exists": strconv.FormatBool(alreadyExists), }) require.NoError(s.T(), err) @@ -110,7 +114,7 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { ) metric, err = dataUploader.objectSizeMetrics.MetricVec.GetMetricWith(prometheus.Labels{ "ledgers": "100", - "compression": "gzip", + "compression": compressxdr.GZIP, "already_exists": strconv.FormatBool(!alreadyExists), }) require.NoError(s.T(), err) @@ -131,7 +135,7 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { uint64(1), getMetricValue(metric).GetSummary().GetSampleCount(), ) - uncompressedPayload, err := decodedArchive.data.MarshalBinary() + uncompressedPayload, err := decodedArchive.Data.MarshalBinary() require.NoError(s.T(), err) require.Equal( s.T(), @@ -158,7 +162,7 @@ func (s *UploaderSuite) TestUploadPutError() { func (s *UploaderSuite) testUploadPutError(putOkReturnVal bool) { key, start, end := "test-1-100", uint32(1), uint32(100) - archive := NewLedgerMetaArchive(key, start, end) + archive := datastore.NewLedgerMetaArchive(key, start, end) s.mockDataStore.On("PutFileIfNotExists", context.Background(), key, mock.Anything).Return(putOkReturnVal, errors.New("error in PutFileIfNotExists")) @@ -181,7 +185,7 @@ func (s *UploaderSuite) testUploadPutError(putOkReturnVal bool) { getMetricValue(metric).GetSummary().GetSampleCount(), ) - for _, compression := range []string{"gzip", "none"} { + for _, compression := range []string{compressxdr.GZIP, "none"} { metric, err = dataUploader.objectSizeMetrics.MetricVec.GetMetricWith(prometheus.Labels{ "ledgers": "100", "compression": compression, @@ -206,7 +210,7 @@ func (s *UploaderSuite) TestRunChannelClose() { go func() { key, start, end := "test", uint32(1), uint32(100) for i := start; i <= end; i++ { - s.Assert().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive(key, i, i))) + s.Assert().NoError(queue.Enqueue(s.ctx, datastore.NewLedgerMetaArchive(key, i, i))) } <-time.After(time.Second * 2) queue.Close() @@ -222,7 +226,7 @@ func (s *UploaderSuite) TestRunContextCancel() { registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - s.Assert().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive("test", 1, 1))) + s.Assert().NoError(queue.Enqueue(s.ctx, datastore.NewLedgerMetaArchive("test", 1, 1))) go func() { <-time.After(time.Second * 2) @@ -237,7 +241,7 @@ func (s *UploaderSuite) TestRunUploadError() { registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - s.Assert().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive("test", 1, 1))) + s.Assert().NoError(queue.Enqueue(s.ctx, datastore.NewLedgerMetaArchive("test", 1, 1))) s.mockDataStore.On("PutFileIfNotExists", mock.Anything, "test", mock.Anything).Return(false, errors.New("Put error")) diff --git a/ingest/ledgerbackend/gcs_backend.go b/ingest/ledgerbackend/gcs_backend.go index a3ff09078b..7859949d98 100644 --- a/ingest/ledgerbackend/gcs_backend.go +++ b/ingest/ledgerbackend/gcs_backend.go @@ -1,9 +1,7 @@ package ledgerbackend import ( - "compress/gzip" "context" - "io" "sync" "time" @@ -11,6 +9,7 @@ import ( "github.com/stellar/go/historyarchive" "github.com/stellar/go/support/collections/heap" + "github.com/stellar/go/support/compressxdr" "github.com/stellar/go/support/datastore" "github.com/stellar/go/support/log" "github.com/stellar/go/xdr" @@ -21,17 +20,9 @@ import ( // Ensure GCSBackend implements LedgerBackend var _ LedgerBackend = (*GCSBackend)(nil) -// An Item is something we manage in a priority queue. -type Item struct { - Value []byte // Value of the item - Priority int // The priority of the item in the queue. -} - -type LCMFileConfig struct { - StorageURL string - FileSuffix string - LedgersPerFile uint32 - FilesPerPartition uint32 +type LedgerBatchObject struct { + Payload []byte + StartLedger int // Ledger sequence used as the priority for the priorityqueue. } type BufferConfig struct { @@ -42,10 +33,12 @@ type BufferConfig struct { } type gcsBackendConfig struct { - lcmFileConfig LCMFileConfig - bufferConfig BufferConfig - dataStoreConfig datastore.DataStoreConfig - network string + bufferConfig BufferConfig + dataStoreConfig datastore.DataStoreConfig + ledgerBatchConfig datastore.LedgerBatchConfig + storageUrl string + network string + compressionType string } // GCSBackend is a ledger backend that reads from a cloud storage service. @@ -53,9 +46,10 @@ type gcsBackendConfig struct { type GCSBackend struct { config gcsBackendConfig - // cancel is the CancelFunc for context which controls the lifetime of a GCSBackend instance. + context context.Context + // cancel is the CancelCauseFunc for context which controls the lifetime of a GCSBackend instance. // Once it is invoked GCSBackend will not be able to stream ledgers from GCSBackend. - cancel context.CancelFunc + cancel context.CancelCauseFunc // gcsBackendLock protects access to gcsBackendRunner. When the read lock // is acquired gcsBackendRunner can be accessed. When the write lock is acquired @@ -66,35 +60,44 @@ type GCSBackend struct { ledgerBuffer *ledgerBufferGCS dataStore datastore.DataStore - ledgerBatchConfig datastore.LedgerBatchConfig - network string prepared *Range // non-nil if any range is prepared closed bool // False until the core is closed + ledgerMetaArchive *datastore.LedgerMetaArchive + decoder compressxdr.XDRDecoder } type ledgerBufferGCS struct { - config gcsBackendConfig - dataStore datastore.DataStore - taskQueue chan uint32 - ledgerQueue chan []byte - ledgerPriorityQueue *heap.Heap[Item] - priorityQueueLock sync.Mutex - count uint32 - limit uint32 - cancel context.CancelFunc + config gcsBackendConfig + dataStore datastore.DataStore + taskQueue chan uint32 // buffer next gcs object read + ledgerQueue chan []byte // order corrected lcm batches + ledgerPriorityQueue *heap.Heap[LedgerBatchObject] + priorityQueueLock sync.Mutex + count uint32 // buffer count + limit uint32 // buffer max + done chan struct{} + + // keep track of the ledgers to be processed and the next ordering + // the ledgers should be buffered currentLedger uint32 nextTaskLedger uint32 nextLedgerQueueLedger uint32 ledgerRange Range + + // passed through from GCSBackend to control lifetime of ledgerBufferGCS instance + context context.Context + cancel context.CancelCauseFunc + decoder compressxdr.XDRDecoder } -func (gcsb *GCSBackend) NewLedgerBuffer(ctx context.Context, ledgerRange Range) (*ledgerBufferGCS, error) { - var cancel context.CancelFunc - less := func(a, b Item) bool { - return a.Priority < b.Priority +func (gcsb *GCSBackend) NewLedgerBuffer(ledgerRange Range) (*ledgerBufferGCS, error) { + less := func(a, b LedgerBatchObject) bool { + return a.StartLedger < b.StartLedger } pq := heap.New(less, int(gcsb.config.bufferConfig.BufferSize)) + done := make(chan struct{}) + ledgerBuffer := &ledgerBufferGCS{ config: gcsb.config, dataStore: gcsb.dataStore, @@ -103,11 +106,14 @@ func (gcsb *GCSBackend) NewLedgerBuffer(ctx context.Context, ledgerRange Range) ledgerPriorityQueue: pq, count: 0, limit: gcsb.config.bufferConfig.BufferSize, - cancel: cancel, + done: done, currentLedger: ledgerRange.from, nextTaskLedger: ledgerRange.from, nextLedgerQueueLedger: ledgerRange.from, ledgerRange: ledgerRange, + context: gcsb.context, + cancel: gcsb.cancel, + decoder: gcsb.decoder, } // Workers to read LCM files @@ -115,9 +121,6 @@ func (gcsb *GCSBackend) NewLedgerBuffer(ctx context.Context, ledgerRange Range) go ledgerBuffer.worker() } - // goroutine to correctly LCM files - go ledgerBuffer.reorderLedgers() - return ledgerBuffer, nil } @@ -128,47 +131,50 @@ func (lb *ledgerBufferGCS) pushTaskQueue() { return } lb.taskQueue <- lb.nextTaskLedger - lb.nextTaskLedger++ + lb.nextTaskLedger += lb.config.ledgerBatchConfig.LedgersPerFile lb.count++ } } func (lb *ledgerBufferGCS) worker() { - for sequence := range lb.taskQueue { - retryCount := uint32(0) - for retryCount <= lb.config.bufferConfig.RetryLimit { - ledgerObject, err := lb.getLedgerGCSObject(sequence) - if err != nil { - if e, ok := err.(*googleapi.Error); ok { - // ledgerObject not found and unbounded - if e.Code == 404 && !lb.ledgerRange.bounded { - time.Sleep(lb.config.bufferConfig.RetryWait * time.Second) - continue + for { + select { + case <-lb.done: + log.Error("abort: getFromLedgerQueue blocked") + return + case <-lb.context.Done(): + log.Error(lb.context.Err()) + return + case sequence := <-lb.taskQueue: + retryCount := uint32(0) + for retryCount <= lb.config.bufferConfig.RetryLimit { + ledgerObject, err := lb.getLedgerGCSObject(sequence) + if err != nil { + if e, ok := err.(*googleapi.Error); ok { + // ledgerObject not found and unbounded + if e.Code == 404 && !lb.ledgerRange.bounded { + time.Sleep(lb.config.bufferConfig.RetryWait * time.Second) + continue + } + } + if retryCount == lb.config.bufferConfig.RetryLimit { + err = errors.New("maximum retries exceeded for gcs object reads") + lb.cancel(err) } + retryCount++ + time.Sleep(lb.config.bufferConfig.RetryWait * time.Second) } - retryCount++ - time.Sleep(lb.config.bufferConfig.RetryWait * time.Second) - } - // Add to priority queue and continue to next task - lb.priorityQueueLock.Lock() - item := Item{ - Value: ledgerObject, - Priority: int(sequence), + // Add to priority queue and continue to next task + lb.storeObject(ledgerObject, sequence) + break } - lb.ledgerPriorityQueue.Push(item) - lb.priorityQueueLock.Unlock() - break } - // Add abort case for max retries } } func (lb *ledgerBufferGCS) getLedgerGCSObject(sequence uint32) ([]byte, error) { - var ledgerCloseMetaBatch xdr.LedgerCloseMetaBatch - - config := datastore.LedgerBatchConfig{} - objectKey := config.GetObjectKeyFromSequenceNumber(sequence) + objectKey := lb.config.ledgerBatchConfig.GetObjectKeyFromSequenceNumber(sequence) reader, err := lb.dataStore.GetFile(context.Background(), objectKey) if err != nil { @@ -177,66 +183,38 @@ func (lb *ledgerBufferGCS) getLedgerGCSObject(sequence uint32) ([]byte, error) { defer reader.Close() - // Read file and unzip - gzipReader, err := gzip.NewReader(reader) - if err != nil { - return nil, errors.Wrapf(err, "failed getting file: %s", objectKey) - } - - defer gzipReader.Close() - - objectBytes, err := io.ReadAll(gzipReader) - if err != nil { - return nil, errors.Wrapf(err, "failed reading file: %s", objectKey) - } - - // Turn binary into xdr - err = ledgerCloseMetaBatch.UnmarshalBinary(objectBytes) - if err != nil { - return nil, errors.Wrapf(err, "failed unmarshalling file: %s", objectKey) - } - - // Check if ledger sequence within the xdr.ledgerCloseMetaBatch - startSequence := uint32(ledgerCloseMetaBatch.StartSequence) - if startSequence > sequence { - return nil, errors.Wrapf(err, "start sequence: %d; greater than sequence to get: %d", startSequence, sequence) - } - - ledgerCloseMetasIndex := sequence - startSequence - ledgerCloseMeta := ledgerCloseMetaBatch.LedgerCloseMetas[ledgerCloseMetasIndex] - - // Turn lcm back to binary to save memory in buffer - lcmBinary, err := ledgerCloseMeta.MarshalBinary() + objectBytes, err := lb.decoder.Unzip(reader) if err != nil { - return nil, errors.Wrapf(err, "failed marshalling lcm sequence: %d", sequence) + return nil, errors.Wrapf(err, "failed unzipping file: %s", objectKey) } - return lcmBinary, nil + return objectBytes, nil } -func (lb *ledgerBufferGCS) reorderLedgers() { +func (lb *ledgerBufferGCS) storeObject(ledgerObject []byte, sequence uint32) { lb.priorityQueueLock.Lock() defer lb.priorityQueueLock.Unlock() - // Nothing in priority queue - if lb.ledgerPriorityQueue.Len() < 0 { - return - } + lb.ledgerPriorityQueue.Push(LedgerBatchObject{ + Payload: ledgerObject, + StartLedger: int(sequence), + }) // Check if the nextLedger is the next item in the priority queue - for lb.currentLedger == uint32(lb.ledgerPriorityQueue.Peek().Priority) { + for lb.ledgerPriorityQueue.Len() > 0 && lb.currentLedger == uint32(lb.ledgerPriorityQueue.Peek().StartLedger) { item := lb.ledgerPriorityQueue.Pop() - lb.ledgerQueue <- item.Value + lb.ledgerQueue <- item.Payload lb.nextLedgerQueueLedger++ } } -func (lb *ledgerBufferGCS) getFromLedgerQueue(ctx context.Context) ([]byte, error) { +func (lb *ledgerBufferGCS) getFromLedgerQueue() ([]byte, error) { for { select { - case <-ctx.Done(): - log.Info("Stopping ExportManager due to context cancellation") - return nil, ctx.Err() + case <-lb.context.Done(): + log.Info("Stopping getFromLedgerQueue due to context cancellation") + close(lb.done) + return nil, lb.context.Err() case lcmBinary := <-lb.ledgerQueue: lb.currentLedger++ // Decrement ledger buffer counter @@ -252,20 +230,20 @@ func (lb *ledgerBufferGCS) getFromLedgerQueue(ctx context.Context) ([]byte, erro // Return a new GCSBackend instance. func NewGCSBackend(ctx context.Context, config gcsBackendConfig) (*GCSBackend, error) { // Check/set minimum config values - if config.lcmFileConfig.StorageURL == "" { - return nil, errors.New("fileConfig.storageURL is not set") + if config.storageUrl == "" { + return nil, errors.New("storageURL is not set") } - if config.lcmFileConfig.FileSuffix == "" { - return nil, errors.New("fileConfig.FileSuffix is not set") + if config.ledgerBatchConfig.FileSuffix == "" { + return nil, errors.New("ledgerBatchConfig.FileSuffix is not set") } - if config.lcmFileConfig.LedgersPerFile == 0 { - config.lcmFileConfig.LedgersPerFile = 1 + if config.ledgerBatchConfig.LedgersPerFile == 0 { + config.ledgerBatchConfig.LedgersPerFile = 1 } - if config.lcmFileConfig.FilesPerPartition == 0 { - config.lcmFileConfig.FilesPerPartition = 1 + if config.ledgerBatchConfig.FilesPerPartition == 0 { + config.ledgerBatchConfig.FilesPerPartition = 1 } // Check/set minimum config values @@ -277,17 +255,25 @@ func NewGCSBackend(ctx context.Context, config gcsBackendConfig) (*GCSBackend, e config.bufferConfig.NumWorkers = 1 } - var cancel context.CancelFunc + ctx, cancel := context.WithCancelCause(ctx) dataStore, err := datastore.NewDataStore(ctx, config.dataStoreConfig, config.network) if err != nil { return nil, err } + ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) + decoder, err := compressxdr.NewXDRDecoder(config.compressionType, nil) + if err != nil { + return nil, err + } + gcsBackend := &GCSBackend{ - config: config, - cancel: cancel, - dataStore: dataStore, + config: config, + cancel: cancel, + dataStore: dataStore, + ledgerMetaArchive: ledgerMetaArchive, + decoder: decoder, } return gcsBackend, nil @@ -298,11 +284,13 @@ func (gcsb *GCSBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, er var err error var archive historyarchive.ArchiveInterface - if archive, err = datastore.CreateHistoryArchiveFromNetworkName(ctx, gcsb.network); err != nil { + if archive, err = datastore.CreateHistoryArchiveFromNetworkName(ctx, gcsb.config.network); err != nil { return 0, err } - resumableManager := datastore.NewResumableManager(gcsb.dataStore, gcsb.network, gcsb.ledgerBatchConfig, archive) - absentLedger, ok, err := resumableManager.FindStart(ctx, 1, 0) + + resumableManager := datastore.NewResumableManager(gcsb.dataStore, gcsb.config.network, gcsb.config.ledgerBatchConfig, archive) + // Start at 2 to skip the genesis ledger + absentLedger, ok, err := resumableManager.FindStart(ctx, 2, 0) if err != nil { return 0, err } @@ -314,6 +302,35 @@ func (gcsb *GCSBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, er return absentLedger - 1, nil } +// getSequenceInBatch checks if the requested sequence is in the cached batch. +// Otherwise will continuously load in the next LedgerCloseMetaBatch until found. +func (gcsb *GCSBackend) getSequenceInBatch(sequence uint32) error { + for { + // Sequence inside the current cached LedgerCloseMetaBatch + if sequence >= gcsb.ledgerMetaArchive.GetStartLedgerSequence() && sequence <= gcsb.ledgerMetaArchive.GetEndLedgerSequence() { + return nil + } + + // Sequence is before the current LedgerCloseMetaBatch + // Does not support retrieving LedgerCloseMeta before the current cached batch + if sequence < gcsb.ledgerMetaArchive.GetStartLedgerSequence() { + return errors.New("requested sequence preceeds current LedgerCloseMetaBatch") + } + + // Sequence is beyond the current LedgerCloseMetaBatch + lcmBatchBinary, err := gcsb.ledgerBuffer.getFromLedgerQueue() + if err != nil { + return errors.Wrap(err, "failed getting next ledger batch from queue") + } + + // Turn binary into xdr + err = gcsb.ledgerMetaArchive.Data.UnmarshalBinary(lcmBatchBinary) + if err != nil { + return errors.Wrap(err, "failed unmarshalling lcmBatchBinary") + } + } +} + // GetLedger returns the LedgerCloseMeta for the specified ledger sequence number func (gcsb *GCSBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { gcsb.gcsBackendLock.RLock() @@ -327,27 +344,22 @@ func (gcsb *GCSBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.Led return xdr.LedgerCloseMeta{}, errors.New("session is not prepared, call PrepareRange first") } - var lcmBinary []byte - var err error - for gcsb.ledgerBuffer.currentLedger <= sequence { - lcmBinary, err = gcsb.ledgerBuffer.getFromLedgerQueue(ctx) - if err != nil { - return xdr.LedgerCloseMeta{}, errors.Wrapf(err, "could not get ledger sequence binary: %d", sequence) - } + err := gcsb.getSequenceInBatch(sequence) + if err != nil { + return xdr.LedgerCloseMeta{}, err } - var lcm xdr.LedgerCloseMeta - err = lcm.UnmarshalBinary(lcmBinary) + ledgerCloseMeta, err := gcsb.ledgerMetaArchive.GetLedger(sequence) if err != nil { return xdr.LedgerCloseMeta{}, err } - return lcm, nil + return *ledgerCloseMeta, nil } // PrepareRange checks if the starting and ending (if bounded) ledgers exist. func (gcsb *GCSBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { - if alreadyPrepared, err := gcsb.startPreparingRange(ctx, ledgerRange); err != nil { + if alreadyPrepared, err := gcsb.startPreparingRange(ledgerRange); err != nil { return errors.Wrap(err, "error starting prepare range") } else if alreadyPrepared { return nil @@ -404,14 +416,14 @@ func (gcsb *GCSBackend) Close() error { gcsb.closed = true - // after the GCSBackend context is canceled all subsequent calls to PrepareRange() will fail - gcsb.cancel() + // after the GCSBackend context is Done all subsequent calls to PrepareRange() will fail + gcsb.context.Done() return nil } // startPreparingRange prepares the ledger range by setting the range in the ledgerBuffer -func (gcsb *GCSBackend) startPreparingRange(ctx context.Context, ledgerRange Range) (bool, error) { +func (gcsb *GCSBackend) startPreparingRange(ledgerRange Range) (bool, error) { gcsb.gcsBackendLock.Lock() defer gcsb.gcsBackendLock.Unlock() @@ -420,7 +432,7 @@ func (gcsb *GCSBackend) startPreparingRange(ctx context.Context, ledgerRange Ran } var err error - gcsb.ledgerBuffer, err = gcsb.NewLedgerBuffer(ctx, ledgerRange) + gcsb.ledgerBuffer, err = gcsb.NewLedgerBuffer(ledgerRange) if err != nil { return false, err } diff --git a/ingest/ledgerbackend/toml.go b/ingest/ledgerbackend/toml.go index bc3ab2247a..84f84bfe4a 100644 --- a/ingest/ledgerbackend/toml.go +++ b/ingest/ledgerbackend/toml.go @@ -486,7 +486,7 @@ func (c *CaptiveCoreToml) checkCoreVersion(coreBinaryPath string) coreVersion { re := regexp.MustCompile(`\D*(\d*)\.(\d*).*`) versionStr := re.FindStringSubmatch(versionRaw) - if err == nil && len(versionStr) == 3 { + if len(versionStr) == 3 { for i := 1; i < len(versionStr); i++ { val, err := strconv.Atoi((versionStr[i])) if err != nil { diff --git a/support/collections/heap/heap_test.go b/support/collections/heap/heap_test.go index 100619f2a5..77f702a619 100644 --- a/support/collections/heap/heap_test.go +++ b/support/collections/heap/heap_test.go @@ -13,7 +13,9 @@ func TestHeapPushAndPop(t *testing.T) { h.Push(3) h.Push(1) h.Push(2) + h.Push(1) + assert.Equal(t, 1, h.Pop()) assert.Equal(t, 1, h.Pop()) assert.Equal(t, 2, h.Pop()) assert.Equal(t, 3, h.Pop()) @@ -30,3 +32,18 @@ func TestHeapPeek(t *testing.T) { assert.Equal(t, 1, h.Peek()) assert.Equal(t, 1, h.Pop()) } + +func TestHeapLen(t *testing.T) { + less := func(a, b int) bool { return a < b } + h := New(less, 0) + + assert.Equal(t, 0, h.Len()) + h.Push(5) + assert.Equal(t, 1, h.Len()) + h.Push(6) + h.Push(7) + assert.Equal(t, 3, h.Len()) + + h.Pop() + assert.Equal(t, 2, h.Len()) +} diff --git a/support/compressxdr/compress_xdr.go b/support/compressxdr/compress_xdr.go new file mode 100644 index 0000000000..518d12d796 --- /dev/null +++ b/support/compressxdr/compress_xdr.go @@ -0,0 +1,38 @@ +package compressxdr + +import ( + "io" + + "github.com/stellar/go/support/errors" +) + +const ( + GZIP = "gzip" +) + +type XDREncoder interface { + WriteTo(w io.Writer) (int64, error) +} + +type XDRDecoder interface { + ReadFrom(r io.Reader) (int64, error) + Unzip(r io.ReadCloser) ([]byte, error) +} + +func NewXDREncoder(compressionType string, xdrPayload interface{}) (XDREncoder, error) { + switch compressionType { + case GZIP: + return &XDRGzipEncoder{XdrPayload: xdrPayload}, nil + default: + return nil, errors.Errorf("invalid compression type %s, not supported", compressionType) + } +} + +func NewXDRDecoder(compressionType string, xdrPayload interface{}) (XDRDecoder, error) { + switch compressionType { + case GZIP: + return &XDRGzipDecoder{XdrPayload: xdrPayload}, nil + default: + return nil, errors.Errorf("invalid compression type %s, not supported", compressionType) + } +} diff --git a/exp/services/ledgerexporter/internal/encoder_test.go b/support/compressxdr/compress_xdr_test.go similarity index 92% rename from exp/services/ledgerexporter/internal/encoder_test.go rename to support/compressxdr/compress_xdr_test.go index 2bf61bcee7..0ea76b5369 100644 --- a/exp/services/ledgerexporter/internal/encoder_test.go +++ b/support/compressxdr/compress_xdr_test.go @@ -1,4 +1,4 @@ -package ledgerexporter +package compressxdr import ( "bytes" @@ -11,7 +11,7 @@ import ( func createTestLedgerCloseMetaBatch(startSeq, endSeq uint32, count int) xdr.LedgerCloseMetaBatch { var ledgerCloseMetas []xdr.LedgerCloseMeta for i := 0; i < count; i++ { - ledgerCloseMetas = append(ledgerCloseMetas, createLedgerCloseMeta(startSeq+uint32(i))) + // ledgerCloseMetas = append(ledgerCloseMetas, createLedgerCloseMeta(startSeq+uint32(i))) } return xdr.LedgerCloseMetaBatch{ StartSequence: xdr.Uint32(startSeq), diff --git a/exp/services/ledgerexporter/internal/encoder.go b/support/compressxdr/gzip_compress_xdr.go similarity index 68% rename from exp/services/ledgerexporter/internal/encoder.go rename to support/compressxdr/gzip_compress_xdr.go index 33909ace75..74d08b95d6 100644 --- a/exp/services/ledgerexporter/internal/encoder.go +++ b/support/compressxdr/gzip_compress_xdr.go @@ -1,4 +1,4 @@ -package ledgerexporter +package compressxdr import ( "compress/gzip" @@ -37,3 +37,19 @@ func (d *XDRGzipDecoder) ReadFrom(r io.Reader) (int64, error) { } return int64(n), nil } + +func (d *XDRGzipDecoder) Unzip(r io.ReadCloser) ([]byte, error) { + gzipReader, err := gzip.NewReader(r) + if err != nil { + return nil, err + } + + defer gzipReader.Close() + + objectBytes, err := io.ReadAll(gzipReader) + if err != nil { + return nil, err + } + + return objectBytes, nil +} diff --git a/support/datastore/datastore.go b/support/datastore/datastore.go index 0b48a81b80..2a26aa1328 100644 --- a/support/datastore/datastore.go +++ b/support/datastore/datastore.go @@ -20,9 +20,6 @@ type DataStore interface { Exists(ctx context.Context, path string) (bool, error) Size(ctx context.Context, path string) (int64, error) Close() error - // TODO: Remove when binary search code is added - //ListDirectoryNames(ctx context.Context) ([]string, error) - //ListFileNames(ctx context.Context, path string) ([]string, error) } // NewDataStore factory, it creates a new DataStore based on the config type diff --git a/support/datastore/utils.go b/support/datastore/history_archive.go similarity index 100% rename from support/datastore/utils.go rename to support/datastore/history_archive.go diff --git a/exp/services/ledgerexporter/internal/ledger_meta_archive.go b/support/datastore/ledger_meta_archive.go similarity index 50% rename from exp/services/ledgerexporter/internal/ledger_meta_archive.go rename to support/datastore/ledger_meta_archive.go index 2a193f812c..631e5749ee 100644 --- a/exp/services/ledgerexporter/internal/ledger_meta_archive.go +++ b/support/datastore/ledger_meta_archive.go @@ -1,4 +1,4 @@ -package ledgerexporter +package datastore import ( "fmt" @@ -9,16 +9,16 @@ import ( // LedgerMetaArchive represents a file with metadata and binary data. type LedgerMetaArchive struct { // file name - objectKey string + ObjectKey string // Actual binary data - data xdr.LedgerCloseMetaBatch + Data xdr.LedgerCloseMetaBatch } // NewLedgerMetaArchive creates a new LedgerMetaArchive instance. func NewLedgerMetaArchive(key string, startSeq uint32, endSeq uint32) *LedgerMetaArchive { return &LedgerMetaArchive{ - objectKey: key, - data: xdr.LedgerCloseMetaBatch{ + ObjectKey: key, + Data: xdr.LedgerCloseMetaBatch{ StartSequence: xdr.Uint32(startSeq), EndSequence: xdr.Uint32(endSeq), }, @@ -27,39 +27,64 @@ func NewLedgerMetaArchive(key string, startSeq uint32, endSeq uint32) *LedgerMet // AddLedger adds a LedgerCloseMeta to the archive. func (f *LedgerMetaArchive) AddLedger(ledgerCloseMeta xdr.LedgerCloseMeta) error { - if ledgerCloseMeta.LedgerSequence() < uint32(f.data.StartSequence) || - ledgerCloseMeta.LedgerSequence() > uint32(f.data.EndSequence) { + if ledgerCloseMeta.LedgerSequence() < uint32(f.Data.StartSequence) || + ledgerCloseMeta.LedgerSequence() > uint32(f.Data.EndSequence) { return fmt.Errorf("ledger sequence %d is outside valid range [%d, %d]", - ledgerCloseMeta.LedgerSequence(), f.data.StartSequence, f.data.EndSequence) + ledgerCloseMeta.LedgerSequence(), f.Data.StartSequence, f.Data.EndSequence) } - if len(f.data.LedgerCloseMetas) > 0 { - lastSequence := f.data.LedgerCloseMetas[len(f.data.LedgerCloseMetas)-1].LedgerSequence() + if len(f.Data.LedgerCloseMetas) > 0 { + lastSequence := f.Data.LedgerCloseMetas[len(f.Data.LedgerCloseMetas)-1].LedgerSequence() if ledgerCloseMeta.LedgerSequence() != lastSequence+1 { return fmt.Errorf("ledgers must be added sequentially: expected sequence %d, got %d", lastSequence+1, ledgerCloseMeta.LedgerSequence()) } } - f.data.LedgerCloseMetas = append(f.data.LedgerCloseMetas, ledgerCloseMeta) + f.Data.LedgerCloseMetas = append(f.Data.LedgerCloseMetas, ledgerCloseMeta) return nil } // GetLedgerCount returns the number of ledgers currently in the archive. func (f *LedgerMetaArchive) GetLedgerCount() uint32 { - return uint32(len(f.data.LedgerCloseMetas)) + return uint32(len(f.Data.LedgerCloseMetas)) } // GetStartLedgerSequence returns the starting ledger sequence of the archive. func (f *LedgerMetaArchive) GetStartLedgerSequence() uint32 { - return uint32(f.data.StartSequence) + return uint32(f.Data.StartSequence) } // GetEndLedgerSequence returns the ending ledger sequence of the archive. func (f *LedgerMetaArchive) GetEndLedgerSequence() uint32 { - return uint32(f.data.EndSequence) + return uint32(f.Data.EndSequence) } // GetObjectKey returns the object key of the archive. func (f *LedgerMetaArchive) GetObjectKey() string { - return f.objectKey + return f.ObjectKey +} + +func (f *LedgerMetaArchive) GetLedger(sequence uint32) (*xdr.LedgerCloseMeta, error) { + if sequence < uint32(f.Data.StartSequence) || sequence > uint32(f.Data.EndSequence) { + return nil, fmt.Errorf("ledger sequence %d is outside valid range [%d, %d]", + sequence, f.Data.StartSequence, f.Data.EndSequence) + } + + ledgerIndex := sequence - f.GetStartLedgerSequence() + if ledgerIndex >= uint32(len(f.Data.LedgerCloseMetas)) { + return nil, fmt.Errorf("LedgerCloseMeta for sequence %d not found", sequence) + } + return &f.Data.LedgerCloseMetas[ledgerIndex], nil +} + +func CreateLedgerCloseMeta(ledgerSeq uint32) xdr.LedgerCloseMeta { + return xdr.LedgerCloseMeta{ + V0: &xdr.LedgerCloseMetaV0{ + LedgerHeader: xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + LedgerSeq: xdr.Uint32(ledgerSeq), + }, + }, + }, + } } diff --git a/exp/services/ledgerexporter/internal/ledger_meta_archive_test.go b/support/datastore/ledger_meta_archive_test.go similarity index 98% rename from exp/services/ledgerexporter/internal/ledger_meta_archive_test.go rename to support/datastore/ledger_meta_archive_test.go index 3403cbaafa..26eeadc313 100644 --- a/exp/services/ledgerexporter/internal/ledger_meta_archive_test.go +++ b/support/datastore/ledger_meta_archive_test.go @@ -1,4 +1,4 @@ -package ledgerexporter +package datastore import ( "fmt" diff --git a/support/datastore/ledgerbatch_config.go b/support/datastore/ledgerbatch_config.go index 17c42c4ea2..eca8bbe737 100644 --- a/support/datastore/ledgerbatch_config.go +++ b/support/datastore/ledgerbatch_config.go @@ -4,13 +4,10 @@ import ( "fmt" ) -const ( - fileSuffix = ".xdr.gz" -) - type LedgerBatchConfig struct { LedgersPerFile uint32 `toml:"ledgers_per_file"` FilesPerPartition uint32 `toml:"files_per_partition"` + FileSuffix string `toml:"file_suffix"` } func (ec LedgerBatchConfig) GetSequenceNumberStartBoundary(ledgerSeq uint32) uint32 { @@ -43,7 +40,7 @@ func (ec LedgerBatchConfig) GetObjectKeyFromSequenceNumber(ledgerSeq uint32) str if fileStart != fileEnd { objectKey += fmt.Sprintf("-%d", fileEnd) } - objectKey += fileSuffix + objectKey += ec.FileSuffix return objectKey } From fdd3637425b8d30d795c5125165ff5574f8d7c0a Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Thu, 2 May 2024 11:03:15 -0400 Subject: [PATCH 135/234] some unit tests --- ingest/ledgerbackend/gcs_backend.go | 5 + ingest/ledgerbackend/gcs_backend_test.go | 180 +++++++++++++++++++++++ support/compressxdr/compress_xdr.go | 2 +- support/compressxdr/compress_xdr_test.go | 38 ++++- support/compressxdr/gzip_compress_xdr.go | 2 +- 5 files changed, 218 insertions(+), 9 deletions(-) create mode 100644 ingest/ledgerbackend/gcs_backend_test.go diff --git a/ingest/ledgerbackend/gcs_backend.go b/ingest/ledgerbackend/gcs_backend.go index 7859949d98..0386524fdf 100644 --- a/ingest/ledgerbackend/gcs_backend.go +++ b/ingest/ledgerbackend/gcs_backend.go @@ -270,6 +270,7 @@ func NewGCSBackend(ctx context.Context, config gcsBackendConfig) (*GCSBackend, e gcsBackend := &GCSBackend{ config: config, + context: ctx, cancel: cancel, dataStore: dataStore, ledgerMetaArchive: ledgerMetaArchive, @@ -399,6 +400,10 @@ func (gcsb *GCSBackend) isPrepared(ledgerRange Range) bool { return true } + if !gcsb.ledgerBuffer.ledgerRange.bounded && ledgerRange.bounded { + return true + } + if gcsb.ledgerBuffer.ledgerRange.to >= ledgerRange.to { return true } diff --git a/ingest/ledgerbackend/gcs_backend_test.go b/ingest/ledgerbackend/gcs_backend_test.go new file mode 100644 index 0000000000..4432fd1d90 --- /dev/null +++ b/ingest/ledgerbackend/gcs_backend_test.go @@ -0,0 +1,180 @@ +package ledgerbackend + +import ( + "context" + "testing" + + "github.com/stellar/go/support/compressxdr" + "github.com/stellar/go/support/datastore" + "github.com/stretchr/testify/assert" +) + +func createGCSBackendConfigForTesting() gcsBackendConfig { + bufferConfig := BufferConfig{ + BufferSize: 1000, + NumWorkers: 5, + RetryLimit: 3, + RetryWait: 5, + } + + param := make(map[string]string) + param["destination_bucket_path"] = "testURL" + dataStoreConfig := datastore.DataStoreConfig{ + Type: "GCS", + Params: param, + } + + ledgerBatchConfig := datastore.LedgerBatchConfig{ + LedgersPerFile: 1, + FilesPerPartition: 64000, + FileSuffix: ".xdr.gz", + } + + return gcsBackendConfig{ + bufferConfig: bufferConfig, + dataStoreConfig: dataStoreConfig, + ledgerBatchConfig: ledgerBatchConfig, + storageUrl: "testURL", + network: "testnet", + compressionType: compressxdr.GZIP, + } +} + +func createGCSBackendForTesting() GCSBackend { + config := createGCSBackendConfigForTesting() + ctx := context.Background() + mockDataStore := new(datastore.MockDataStore) + ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) + decoder, _ := compressxdr.NewXDRDecoder(config.compressionType, nil) + + return GCSBackend{ + config: config, + context: ctx, + dataStore: mockDataStore, + ledgerMetaArchive: ledgerMetaArchive, + decoder: decoder, + } +} + +func createLedgerBufferGCSForTesting(ledgerRange Range) *ledgerBufferGCS { + gcsb := createGCSBackendForTesting() + ledgerBuffer, _ := gcsb.NewLedgerBuffer(ledgerRange) + return ledgerBuffer +} + +func TestGCSNewLedgerBuffer(t *testing.T) { + gcsb := createGCSBackendForTesting() + ledgerRange := BoundedRange(2, 3) + + ledgerBuffer, err := gcsb.NewLedgerBuffer(ledgerRange) + assert.NoError(t, err) + assert.Equal(t, uint32(2), ledgerBuffer.currentLedger) + assert.Equal(t, uint32(2), ledgerBuffer.nextTaskLedger) + assert.Equal(t, uint32(2), ledgerBuffer.nextLedgerQueueLedger) + assert.Equal(t, ledgerRange, ledgerBuffer.ledgerRange) +} + +func TestGCSPrepareRange(t *testing.T) { + gcsb := createGCSBackendForTesting() + ctx := context.Background() + ledgerRange := BoundedRange(2, 3) + gcsb.ledgerBuffer = createLedgerBufferGCSForTesting(ledgerRange) + + err := gcsb.PrepareRange(ctx, ledgerRange) + assert.NoError(t, err) + assert.NotNil(t, gcsb.prepared) +} + +func TestGCSPrepareRange_AlreadyPrepared(t *testing.T) { + gcsb := createGCSBackendForTesting() + ctx := context.Background() + ledgerRange := BoundedRange(2, 3) + gcsb.ledgerBuffer = createLedgerBufferGCSForTesting(ledgerRange) + gcsb.prepared = &ledgerRange + + err := gcsb.PrepareRange(ctx, ledgerRange) + assert.NoError(t, err) +} + +func TestGCSIsPrepared_Bounded(t *testing.T) { + gcsb := createGCSBackendForTesting() + ctx := context.Background() + ledgerRange := BoundedRange(3, 4) + gcsb.ledgerBuffer = createLedgerBufferGCSForTesting(ledgerRange) + gcsb.PrepareRange(ctx, ledgerRange) + + ok, err := gcsb.IsPrepared(ctx, ledgerRange) + assert.NoError(t, err) + assert.True(t, ok) + + ok, err = gcsb.IsPrepared(ctx, BoundedRange(2, 4)) + assert.NoError(t, err) + assert.False(t, ok) + + ok, err = gcsb.IsPrepared(ctx, UnboundedRange(3)) + assert.NoError(t, err) + assert.False(t, ok) + + ok, err = gcsb.IsPrepared(ctx, UnboundedRange(2)) + assert.NoError(t, err) + assert.False(t, ok) +} + +func TestGCSIsPrepared_Unbounded(t *testing.T) { + gcsb := createGCSBackendForTesting() + ctx := context.Background() + ledgerRange := UnboundedRange(3) + gcsb.ledgerBuffer = createLedgerBufferGCSForTesting(ledgerRange) + gcsb.PrepareRange(ctx, ledgerRange) + + ok, err := gcsb.IsPrepared(ctx, ledgerRange) + assert.NoError(t, err) + assert.True(t, ok) + + ok, err = gcsb.IsPrepared(ctx, BoundedRange(3, 4)) + assert.NoError(t, err) + assert.True(t, ok) + + ok, err = gcsb.IsPrepared(ctx, BoundedRange(2, 4)) + assert.NoError(t, err) + assert.False(t, ok) + + ok, err = gcsb.IsPrepared(ctx, UnboundedRange(4)) + assert.NoError(t, err) + assert.True(t, ok) + + ok, err = gcsb.IsPrepared(ctx, UnboundedRange(2)) + assert.NoError(t, err) + assert.False(t, ok) +} + +func TestGCSClose(t *testing.T) { + gcsb := createGCSBackendForTesting() + ctx := context.Background() + ledgerRange := UnboundedRange(3) + gcsb.ledgerBuffer = createLedgerBufferGCSForTesting(ledgerRange) + gcsb.PrepareRange(ctx, ledgerRange) + + err := gcsb.Close() + assert.NoError(t, err) + assert.Equal(t, true, gcsb.closed) +} + +func TestGCSGetLedger(t *testing.T) { + expectedLCM := datastore.CreateLedgerCloseMeta(3) + + gcsb := createGCSBackendForTesting() + ctx := context.Background() + ledgerRange := BoundedRange(3, 3) + gcsb.ledgerBuffer = createLedgerBufferGCSForTesting(ledgerRange) + gcsb.PrepareRange(ctx, ledgerRange) + + mockDataStore := new(datastore.MockDataStore) + gcsb.dataStore = mockDataStore + gcsb.ledgerBuffer.dataStore = mockDataStore + mockDataStore.On("GetFile", ctx, "0-63999/3.xdr.gz") + + lcm, err := gcsb.GetLedger(ctx, 2) + assert.NoError(t, err) + assert.Equal(t, expectedLCM, lcm) +} diff --git a/support/compressxdr/compress_xdr.go b/support/compressxdr/compress_xdr.go index 518d12d796..c43a1bca77 100644 --- a/support/compressxdr/compress_xdr.go +++ b/support/compressxdr/compress_xdr.go @@ -16,7 +16,7 @@ type XDREncoder interface { type XDRDecoder interface { ReadFrom(r io.Reader) (int64, error) - Unzip(r io.ReadCloser) ([]byte, error) + Unzip(r io.Reader) ([]byte, error) } func NewXDREncoder(compressionType string, xdrPayload interface{}) (XDREncoder, error) { diff --git a/support/compressxdr/compress_xdr_test.go b/support/compressxdr/compress_xdr_test.go index 0ea76b5369..bd9a6c0ba9 100644 --- a/support/compressxdr/compress_xdr_test.go +++ b/support/compressxdr/compress_xdr_test.go @@ -20,26 +20,27 @@ func createTestLedgerCloseMetaBatch(startSeq, endSeq uint32, count int) xdr.Ledg } } -func TestEncodeDecodeLedgerCloseMetaBatch(t *testing.T) { +func TestEncodeDecodeLedgerCloseMetaBatchGzip(t *testing.T) { testData := createTestLedgerCloseMetaBatch(1000, 1005, 6) // Encode the test data - var encoder XDRGzipEncoder - encoder.XdrPayload = testData + encoder, err := NewXDREncoder(GZIP, testData) + require.NoError(t, err) var buf bytes.Buffer - _, err := encoder.WriteTo(&buf) + _, err = encoder.WriteTo(&buf) require.NoError(t, err) // Decode the encoded data - var decoder XDRGzipDecoder - decoder.XdrPayload = &xdr.LedgerCloseMetaBatch{} + lcmBatch := xdr.LedgerCloseMetaBatch{} + decoder, err := NewXDRDecoder(GZIP, &lcmBatch) + require.NoError(t, err) _, err = decoder.ReadFrom(&buf) require.NoError(t, err) // Check if the decoded data matches the original test data - decodedData := decoder.XdrPayload.(*xdr.LedgerCloseMetaBatch) + decodedData := lcmBatch require.Equal(t, testData.StartSequence, decodedData.StartSequence) require.Equal(t, testData.EndSequence, decodedData.EndSequence) require.Equal(t, len(testData.LedgerCloseMetas), len(decodedData.LedgerCloseMetas)) @@ -47,3 +48,26 @@ func TestEncodeDecodeLedgerCloseMetaBatch(t *testing.T) { require.Equal(t, testData.LedgerCloseMetas[i], decodedData.LedgerCloseMetas[i]) } } + +func TestDecodeUnzipGzip(t *testing.T) { + testData := createTestLedgerCloseMetaBatch(1000, 1005, 6) + + // Encode the test data + encoder, err := NewXDREncoder(GZIP, testData) + require.NoError(t, err) + + var buf bytes.Buffer + _, err = encoder.WriteTo(&buf) + require.NoError(t, err) + + // Decode the encoded data + lcmBatch := xdr.LedgerCloseMetaBatch{} + decoder, err := NewXDRDecoder(GZIP, &lcmBatch) + require.NoError(t, err) + + binary, err := decoder.Unzip(&buf) + require.NoError(t, err) + + require.Equal(t, []uint8([]byte{0x0, 0x0, 0x3, 0xe8, 0x0, 0x0, 0x3, 0xed, 0x0, 0x0, 0x0, 0x0}), binary) + +} diff --git a/support/compressxdr/gzip_compress_xdr.go b/support/compressxdr/gzip_compress_xdr.go index 74d08b95d6..9171b87ffd 100644 --- a/support/compressxdr/gzip_compress_xdr.go +++ b/support/compressxdr/gzip_compress_xdr.go @@ -38,7 +38,7 @@ func (d *XDRGzipDecoder) ReadFrom(r io.Reader) (int64, error) { return int64(n), nil } -func (d *XDRGzipDecoder) Unzip(r io.ReadCloser) ([]byte, error) { +func (d *XDRGzipDecoder) Unzip(r io.Reader) ([]byte, error) { gzipReader, err := gzip.NewReader(r) if err != nil { return nil, err From 4b1b50d8026ea2b20d1d90d5552233f68b86f24d Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Thu, 2 May 2024 11:44:06 -0400 Subject: [PATCH 136/234] wip --- ingest/ledgerbackend/gcs_backend.go | 70 ++++++++++++------------ ingest/ledgerbackend/gcs_backend_test.go | 18 +++--- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/ingest/ledgerbackend/gcs_backend.go b/ingest/ledgerbackend/gcs_backend.go index 0386524fdf..10220e8233 100644 --- a/ingest/ledgerbackend/gcs_backend.go +++ b/ingest/ledgerbackend/gcs_backend.go @@ -32,19 +32,19 @@ type BufferConfig struct { RetryWait time.Duration } -type gcsBackendConfig struct { - bufferConfig BufferConfig - dataStoreConfig datastore.DataStoreConfig - ledgerBatchConfig datastore.LedgerBatchConfig - storageUrl string - network string - compressionType string +type GCSBackendConfig struct { + BufferConfig BufferConfig + DataStoreConfig datastore.DataStoreConfig + LedgerBatchConfig datastore.LedgerBatchConfig + StorageUrl string + Network string + CompressionType string } // GCSBackend is a ledger backend that reads from a cloud storage service. // The cloud storage service contains files generated from the ledgerExporter. type GCSBackend struct { - config gcsBackendConfig + config GCSBackendConfig context context.Context // cancel is the CancelCauseFunc for context which controls the lifetime of a GCSBackend instance. @@ -67,7 +67,7 @@ type GCSBackend struct { } type ledgerBufferGCS struct { - config gcsBackendConfig + config GCSBackendConfig dataStore datastore.DataStore taskQueue chan uint32 // buffer next gcs object read ledgerQueue chan []byte // order corrected lcm batches @@ -94,18 +94,18 @@ func (gcsb *GCSBackend) NewLedgerBuffer(ledgerRange Range) (*ledgerBufferGCS, er less := func(a, b LedgerBatchObject) bool { return a.StartLedger < b.StartLedger } - pq := heap.New(less, int(gcsb.config.bufferConfig.BufferSize)) + pq := heap.New(less, int(gcsb.config.BufferConfig.BufferSize)) done := make(chan struct{}) ledgerBuffer := &ledgerBufferGCS{ config: gcsb.config, dataStore: gcsb.dataStore, - taskQueue: make(chan uint32, gcsb.config.bufferConfig.BufferSize), - ledgerQueue: make(chan []byte, gcsb.config.bufferConfig.BufferSize), + taskQueue: make(chan uint32, gcsb.config.BufferConfig.BufferSize), + ledgerQueue: make(chan []byte, gcsb.config.BufferConfig.BufferSize), ledgerPriorityQueue: pq, count: 0, - limit: gcsb.config.bufferConfig.BufferSize, + limit: gcsb.config.BufferConfig.BufferSize, done: done, currentLedger: ledgerRange.from, nextTaskLedger: ledgerRange.from, @@ -117,7 +117,7 @@ func (gcsb *GCSBackend) NewLedgerBuffer(ledgerRange Range) (*ledgerBufferGCS, er } // Workers to read LCM files - for i := uint32(0); i < gcsb.config.bufferConfig.NumWorkers; i++ { + for i := uint32(0); i < gcsb.config.BufferConfig.NumWorkers; i++ { go ledgerBuffer.worker() } @@ -131,7 +131,7 @@ func (lb *ledgerBufferGCS) pushTaskQueue() { return } lb.taskQueue <- lb.nextTaskLedger - lb.nextTaskLedger += lb.config.ledgerBatchConfig.LedgersPerFile + lb.nextTaskLedger += lb.config.LedgerBatchConfig.LedgersPerFile lb.count++ } } @@ -147,22 +147,22 @@ func (lb *ledgerBufferGCS) worker() { return case sequence := <-lb.taskQueue: retryCount := uint32(0) - for retryCount <= lb.config.bufferConfig.RetryLimit { + for retryCount <= lb.config.BufferConfig.RetryLimit { ledgerObject, err := lb.getLedgerGCSObject(sequence) if err != nil { if e, ok := err.(*googleapi.Error); ok { // ledgerObject not found and unbounded if e.Code == 404 && !lb.ledgerRange.bounded { - time.Sleep(lb.config.bufferConfig.RetryWait * time.Second) + time.Sleep(lb.config.BufferConfig.RetryWait * time.Second) continue } } - if retryCount == lb.config.bufferConfig.RetryLimit { + if retryCount == lb.config.BufferConfig.RetryLimit { err = errors.New("maximum retries exceeded for gcs object reads") lb.cancel(err) } retryCount++ - time.Sleep(lb.config.bufferConfig.RetryWait * time.Second) + time.Sleep(lb.config.BufferConfig.RetryWait * time.Second) } // Add to priority queue and continue to next task @@ -174,7 +174,7 @@ func (lb *ledgerBufferGCS) worker() { } func (lb *ledgerBufferGCS) getLedgerGCSObject(sequence uint32) ([]byte, error) { - objectKey := lb.config.ledgerBatchConfig.GetObjectKeyFromSequenceNumber(sequence) + objectKey := lb.config.LedgerBatchConfig.GetObjectKeyFromSequenceNumber(sequence) reader, err := lb.dataStore.GetFile(context.Background(), objectKey) if err != nil { @@ -228,42 +228,42 @@ func (lb *ledgerBufferGCS) getFromLedgerQueue() ([]byte, error) { } // Return a new GCSBackend instance. -func NewGCSBackend(ctx context.Context, config gcsBackendConfig) (*GCSBackend, error) { +func NewGCSBackend(ctx context.Context, config GCSBackendConfig) (*GCSBackend, error) { // Check/set minimum config values - if config.storageUrl == "" { + if config.StorageUrl == "" { return nil, errors.New("storageURL is not set") } - if config.ledgerBatchConfig.FileSuffix == "" { + if config.LedgerBatchConfig.FileSuffix == "" { return nil, errors.New("ledgerBatchConfig.FileSuffix is not set") } - if config.ledgerBatchConfig.LedgersPerFile == 0 { - config.ledgerBatchConfig.LedgersPerFile = 1 + if config.LedgerBatchConfig.LedgersPerFile == 0 { + config.LedgerBatchConfig.LedgersPerFile = 1 } - if config.ledgerBatchConfig.FilesPerPartition == 0 { - config.ledgerBatchConfig.FilesPerPartition = 1 + if config.LedgerBatchConfig.FilesPerPartition == 0 { + config.LedgerBatchConfig.FilesPerPartition = 1 } // Check/set minimum config values - if config.bufferConfig.BufferSize == 0 { - config.bufferConfig.BufferSize = 1 + if config.BufferConfig.BufferSize == 0 { + config.BufferConfig.BufferSize = 1 } - if config.bufferConfig.NumWorkers == 0 { - config.bufferConfig.NumWorkers = 1 + if config.BufferConfig.NumWorkers == 0 { + config.BufferConfig.NumWorkers = 1 } ctx, cancel := context.WithCancelCause(ctx) - dataStore, err := datastore.NewDataStore(ctx, config.dataStoreConfig, config.network) + dataStore, err := datastore.NewDataStore(ctx, config.DataStoreConfig, config.Network) if err != nil { return nil, err } ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) - decoder, err := compressxdr.NewXDRDecoder(config.compressionType, nil) + decoder, err := compressxdr.NewXDRDecoder(config.CompressionType, nil) if err != nil { return nil, err } @@ -285,11 +285,11 @@ func (gcsb *GCSBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, er var err error var archive historyarchive.ArchiveInterface - if archive, err = datastore.CreateHistoryArchiveFromNetworkName(ctx, gcsb.config.network); err != nil { + if archive, err = datastore.CreateHistoryArchiveFromNetworkName(ctx, gcsb.config.Network); err != nil { return 0, err } - resumableManager := datastore.NewResumableManager(gcsb.dataStore, gcsb.config.network, gcsb.config.ledgerBatchConfig, archive) + resumableManager := datastore.NewResumableManager(gcsb.dataStore, gcsb.config.Network, gcsb.config.LedgerBatchConfig, archive) // Start at 2 to skip the genesis ledger absentLedger, ok, err := resumableManager.FindStart(ctx, 2, 0) if err != nil { diff --git a/ingest/ledgerbackend/gcs_backend_test.go b/ingest/ledgerbackend/gcs_backend_test.go index 4432fd1d90..c128e0547f 100644 --- a/ingest/ledgerbackend/gcs_backend_test.go +++ b/ingest/ledgerbackend/gcs_backend_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/assert" ) -func createGCSBackendConfigForTesting() gcsBackendConfig { +func createGCSBackendConfigForTesting() GCSBackendConfig { bufferConfig := BufferConfig{ BufferSize: 1000, NumWorkers: 5, @@ -30,13 +30,13 @@ func createGCSBackendConfigForTesting() gcsBackendConfig { FileSuffix: ".xdr.gz", } - return gcsBackendConfig{ - bufferConfig: bufferConfig, - dataStoreConfig: dataStoreConfig, - ledgerBatchConfig: ledgerBatchConfig, - storageUrl: "testURL", - network: "testnet", - compressionType: compressxdr.GZIP, + return GCSBackendConfig{ + BufferConfig: bufferConfig, + DataStoreConfig: dataStoreConfig, + LedgerBatchConfig: ledgerBatchConfig, + StorageUrl: "testURL", + Network: "testnet", + CompressionType: compressxdr.GZIP, } } @@ -45,7 +45,7 @@ func createGCSBackendForTesting() GCSBackend { ctx := context.Background() mockDataStore := new(datastore.MockDataStore) ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) - decoder, _ := compressxdr.NewXDRDecoder(config.compressionType, nil) + decoder, _ := compressxdr.NewXDRDecoder(config.CompressionType, nil) return GCSBackend{ config: config, From ae23275710d6a4f32696c13984f459af11d696a6 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 3 May 2024 01:57:50 -0400 Subject: [PATCH 137/234] Update unit tests --- ingest/ledgerbackend/gcs_backend.go | 83 ++++++++--- ingest/ledgerbackend/gcs_backend_test.go | 180 +++++++++++++++++++---- support/compressxdr/mocks.go | 23 +++ support/datastore/ledger_meta_archive.go | 6 + 4 files changed, 243 insertions(+), 49 deletions(-) create mode 100644 support/compressxdr/mocks.go diff --git a/ingest/ledgerbackend/gcs_backend.go b/ingest/ledgerbackend/gcs_backend.go index 10220e8233..61fa9c9f2e 100644 --- a/ingest/ledgerbackend/gcs_backend.go +++ b/ingest/ledgerbackend/gcs_backend.go @@ -36,9 +36,10 @@ type GCSBackendConfig struct { BufferConfig BufferConfig DataStoreConfig datastore.DataStoreConfig LedgerBatchConfig datastore.LedgerBatchConfig - StorageUrl string Network string CompressionType string + DataStore datastore.DataStore + ResumableManager datastore.ResumableManager } // GCSBackend is a ledger backend that reads from a cloud storage service. @@ -60,6 +61,7 @@ type GCSBackend struct { ledgerBuffer *ledgerBufferGCS dataStore datastore.DataStore + resumableManager datastore.ResumableManager prepared *Range // non-nil if any range is prepared closed bool // False until the core is closed ledgerMetaArchive *datastore.LedgerMetaArchive @@ -127,7 +129,7 @@ func (gcsb *GCSBackend) NewLedgerBuffer(ledgerRange Range) (*ledgerBufferGCS, er func (lb *ledgerBufferGCS) pushTaskQueue() { for lb.count <= lb.limit { // In bounded mode, don't queue past the end ledger - if lb.ledgerRange.to < lb.nextTaskLedger && lb.ledgerRange.bounded { + if lb.nextTaskLedger > lb.ledgerRange.to && lb.ledgerRange.bounded { return } lb.taskQueue <- lb.nextTaskLedger @@ -204,6 +206,7 @@ func (lb *ledgerBufferGCS) storeObject(ledgerObject []byte, sequence uint32) { for lb.ledgerPriorityQueue.Len() > 0 && lb.currentLedger == uint32(lb.ledgerPriorityQueue.Peek().StartLedger) { item := lb.ledgerPriorityQueue.Pop() lb.ledgerQueue <- item.Payload + lb.currentLedger++ lb.nextLedgerQueueLedger++ } } @@ -216,7 +219,6 @@ func (lb *ledgerBufferGCS) getFromLedgerQueue() ([]byte, error) { close(lb.done) return nil, lb.context.Err() case lcmBinary := <-lb.ledgerQueue: - lb.currentLedger++ // Decrement ledger buffer counter lb.count-- // Add next task to the TaskQueue @@ -229,13 +231,9 @@ func (lb *ledgerBufferGCS) getFromLedgerQueue() ([]byte, error) { // Return a new GCSBackend instance. func NewGCSBackend(ctx context.Context, config GCSBackendConfig) (*GCSBackend, error) { - // Check/set minimum config values - if config.StorageUrl == "" { - return nil, errors.New("storageURL is not set") - } - + // Check/set minimum config values for LedgerBatchConfig if config.LedgerBatchConfig.FileSuffix == "" { - return nil, errors.New("ledgerBatchConfig.FileSuffix is not set") + config.LedgerBatchConfig.FileSuffix = ".xdr.gz" } if config.LedgerBatchConfig.LedgersPerFile == 0 { @@ -243,23 +241,45 @@ func NewGCSBackend(ctx context.Context, config GCSBackendConfig) (*GCSBackend, e } if config.LedgerBatchConfig.FilesPerPartition == 0 { - config.LedgerBatchConfig.FilesPerPartition = 1 + config.LedgerBatchConfig.FilesPerPartition = 64000 } - // Check/set minimum config values + // Check/set minimum config values for BufferConfig if config.BufferConfig.BufferSize == 0 { - config.BufferConfig.BufferSize = 1 + config.BufferConfig.BufferSize = 1000 } if config.BufferConfig.NumWorkers == 0 { - config.BufferConfig.NumWorkers = 1 + config.BufferConfig.NumWorkers = 5 + } + + if config.BufferConfig.RetryLimit == 0 { + config.BufferConfig.RetryLimit = 3 + } + + if config.BufferConfig.RetryWait == 0 { + config.BufferConfig.RetryWait = 5 } ctx, cancel := context.WithCancelCause(ctx) - dataStore, err := datastore.NewDataStore(ctx, config.DataStoreConfig, config.Network) - if err != nil { - return nil, err + if config.DataStore == nil { + dataStore, err := datastore.NewDataStore(ctx, config.DataStoreConfig, config.Network) + if err != nil { + return nil, err + } + config.DataStore = dataStore + } + + if config.ResumableManager == nil { + var err error + var archive historyarchive.ArchiveInterface + + if archive, err = datastore.CreateHistoryArchiveFromNetworkName(ctx, config.Network); err != nil { + return nil, err + } + + config.ResumableManager = datastore.NewResumableManager(config.DataStore, config.Network, config.LedgerBatchConfig, archive) } ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) @@ -272,7 +292,8 @@ func NewGCSBackend(ctx context.Context, config GCSBackendConfig) (*GCSBackend, e config: config, context: ctx, cancel: cancel, - dataStore: dataStore, + dataStore: config.DataStore, + resumableManager: config.ResumableManager, ledgerMetaArchive: ledgerMetaArchive, decoder: decoder, } @@ -283,15 +304,13 @@ func NewGCSBackend(ctx context.Context, config GCSBackendConfig) (*GCSBackend, e // GetLatestLedgerSequence returns the most recent ledger sequence number in the cloud storage bucket. func (gcsb *GCSBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { var err error - var archive historyarchive.ArchiveInterface - if archive, err = datastore.CreateHistoryArchiveFromNetworkName(ctx, gcsb.config.Network); err != nil { - return 0, err + if gcsb.closed { + return 0, errors.New("gcsBackend is closed; cannot GetLatestLedgerSequence") } - resumableManager := datastore.NewResumableManager(gcsb.dataStore, gcsb.config.Network, gcsb.config.LedgerBatchConfig, archive) // Start at 2 to skip the genesis ledger - absentLedger, ok, err := resumableManager.FindStart(ctx, 2, 0) + absentLedger, ok, err := gcsb.resumableManager.FindStart(ctx, uint32(2), uint32(0)) if err != nil { return 0, err } @@ -338,13 +357,23 @@ func (gcsb *GCSBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.Led defer gcsb.gcsBackendLock.RUnlock() if gcsb.closed { - return xdr.LedgerCloseMeta{}, errors.New("gcsBackend is closed") + return xdr.LedgerCloseMeta{}, errors.New("gcsBackend is closed; cannot GetLedger") } if gcsb.prepared == nil { return xdr.LedgerCloseMeta{}, errors.New("session is not prepared, call PrepareRange first") } + if sequence < gcsb.ledgerBuffer.ledgerRange.from { + return xdr.LedgerCloseMeta{}, errors.New("requested sequence preceeds current LedgerRange") + } + + if gcsb.ledgerBuffer.ledgerRange.bounded { + if sequence > gcsb.ledgerBuffer.ledgerRange.to { + return xdr.LedgerCloseMeta{}, errors.New("requested sequence beyond current LedgerRange") + } + } + err := gcsb.getSequenceInBatch(sequence) if err != nil { return xdr.LedgerCloseMeta{}, err @@ -360,6 +389,10 @@ func (gcsb *GCSBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.Led // PrepareRange checks if the starting and ending (if bounded) ledgers exist. func (gcsb *GCSBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { + if gcsb.closed { + return errors.New("gcsBackend is closed; cannot PrepareRange") + } + if alreadyPrepared, err := gcsb.startPreparingRange(ledgerRange); err != nil { return errors.Wrap(err, "error starting prepare range") } else if alreadyPrepared { @@ -376,6 +409,10 @@ func (gcsb *GCSBackend) IsPrepared(ctx context.Context, ledgerRange Range) (bool gcsb.gcsBackendLock.RLock() defer gcsb.gcsBackendLock.RUnlock() + if gcsb.closed { + return false, errors.New("gcsBackend is closed; cannot IsPrepared") + } + return gcsb.isPrepared(ledgerRange), nil } diff --git a/ingest/ledgerbackend/gcs_backend_test.go b/ingest/ledgerbackend/gcs_backend_test.go index c128e0547f..fe6fc98322 100644 --- a/ingest/ledgerbackend/gcs_backend_test.go +++ b/ingest/ledgerbackend/gcs_backend_test.go @@ -1,20 +1,24 @@ package ledgerbackend import ( + "bytes" "context" + "io" "testing" + "time" "github.com/stellar/go/support/compressxdr" "github.com/stellar/go/support/datastore" + "github.com/stellar/go/xdr" "github.com/stretchr/testify/assert" ) func createGCSBackendConfigForTesting() GCSBackendConfig { bufferConfig := BufferConfig{ - BufferSize: 1000, + BufferSize: 100, NumWorkers: 5, RetryLimit: 3, - RetryWait: 5, + RetryWait: 1, } param := make(map[string]string) @@ -30,55 +34,186 @@ func createGCSBackendConfigForTesting() GCSBackendConfig { FileSuffix: ".xdr.gz", } + dataStore := new(datastore.MockDataStore) + + resumableManager := new(datastore.MockResumableManager) + return GCSBackendConfig{ BufferConfig: bufferConfig, DataStoreConfig: dataStoreConfig, LedgerBatchConfig: ledgerBatchConfig, - StorageUrl: "testURL", Network: "testnet", CompressionType: compressxdr.GZIP, + DataStore: dataStore, + ResumableManager: resumableManager, } } func createGCSBackendForTesting() GCSBackend { config := createGCSBackendConfigForTesting() ctx := context.Background() - mockDataStore := new(datastore.MockDataStore) ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) decoder, _ := compressxdr.NewXDRDecoder(config.CompressionType, nil) return GCSBackend{ config: config, context: ctx, - dataStore: mockDataStore, + dataStore: config.DataStore, + resumableManager: config.ResumableManager, ledgerMetaArchive: ledgerMetaArchive, decoder: decoder, } } -func createLedgerBufferGCSForTesting(ledgerRange Range) *ledgerBufferGCS { +func createGCSLedgerBufferForTesting(ledgerRange Range) *ledgerBufferGCS { gcsb := createGCSBackendForTesting() ledgerBuffer, _ := gcsb.NewLedgerBuffer(ledgerRange) return ledgerBuffer } +func createReadCloserForTesting() io.ReadCloser { + var capturedBuf []byte + reader := bytes.NewReader(capturedBuf) + return io.NopCloser(reader) +} + +func TestNewGCSBackend(t *testing.T) { + ctx := context.Background() + config := createGCSBackendConfigForTesting() + config.LedgerBatchConfig = datastore.LedgerBatchConfig{} + config.BufferConfig = BufferConfig{} + + gcsb, err := NewGCSBackend(ctx, config) + assert.NoError(t, err) + + assert.Equal(t, gcsb.dataStore, config.DataStore) + assert.Equal(t, gcsb.resumableManager, config.ResumableManager) + assert.Equal(t, ".xdr.gz", gcsb.config.LedgerBatchConfig.FileSuffix) + assert.Equal(t, uint32(1), gcsb.config.LedgerBatchConfig.LedgersPerFile) + assert.Equal(t, uint32(64000), gcsb.config.LedgerBatchConfig.FilesPerPartition) + assert.Equal(t, uint32(1000), gcsb.config.BufferConfig.BufferSize) + assert.Equal(t, uint32(5), gcsb.config.BufferConfig.NumWorkers) + assert.Equal(t, uint32(3), gcsb.config.BufferConfig.RetryLimit) + assert.Equal(t, time.Duration(5), gcsb.config.BufferConfig.RetryWait) +} + func TestGCSNewLedgerBuffer(t *testing.T) { gcsb := createGCSBackendForTesting() ledgerRange := BoundedRange(2, 3) ledgerBuffer, err := gcsb.NewLedgerBuffer(ledgerRange) assert.NoError(t, err) + assert.Equal(t, uint32(2), ledgerBuffer.currentLedger) assert.Equal(t, uint32(2), ledgerBuffer.nextTaskLedger) assert.Equal(t, uint32(2), ledgerBuffer.nextLedgerQueueLedger) assert.Equal(t, ledgerRange, ledgerBuffer.ledgerRange) } +func TestGCSGetLatestLedgerSequence(t *testing.T) { + ctx := context.Background() + gcsb := createGCSBackendForTesting() + resumableManager := new(datastore.MockResumableManager) + gcsb.resumableManager = resumableManager + + resumableManager.On("FindStart", ctx, uint32(2), uint32(0)).Return(uint32(6), true, nil) + + seq, err := gcsb.GetLatestLedgerSequence(ctx) + assert.NoError(t, err) + + assert.Equal(t, uint32(5), seq) +} + +func createLCMForTesting(start, end uint32) []xdr.LedgerCloseMeta { + var lcmArray []xdr.LedgerCloseMeta + for i := start; i <= end; i++ { + lcmArray = append(lcmArray, datastore.CreateLedgerCloseMeta(i)) + } + + return lcmArray +} + +func createLCMBatchBinaryForTesting(lcm xdr.LedgerCloseMeta, start uint32, end uint32) []byte { + lcmBatch := xdr.LedgerCloseMetaBatch{ + StartSequence: xdr.Uint32(start), + EndSequence: xdr.Uint32(end), + LedgerCloseMetas: []xdr.LedgerCloseMeta{ + lcm, + }, + } + lcmBatchBinary, _ := lcmBatch.MarshalBinary() + return lcmBatchBinary +} + +func TestGCSGetLedger(t *testing.T) { + startLedger := uint32(3) + endLedger := uint32(4) + lcmArray := createLCMForTesting(startLedger, endLedger) + + gcsb := createGCSBackendForTesting() + ctx := context.Background() + ledgerRange := BoundedRange(startLedger, endLedger) + gcsb.ledgerBuffer = createGCSLedgerBufferForTesting(ledgerRange) + gcsb.PrepareRange(ctx, ledgerRange) + + readCloser1 := createReadCloserForTesting() + readCloser2 := createReadCloserForTesting() + + mockDataStore := new(datastore.MockDataStore) + gcsb.dataStore = mockDataStore + gcsb.ledgerBuffer.dataStore = mockDataStore + mockDataStore.On("GetFile", ctx, "0-63999/3.xdr.gz").Return(readCloser1, nil) + mockDataStore.On("GetFile", ctx, "0-63999/4.xdr.gz").Return(readCloser2, nil) + + objectBytes1 := createLCMBatchBinaryForTesting(lcmArray[0], uint32(3), uint32(3)) + objectBytes2 := createLCMBatchBinaryForTesting(lcmArray[1], uint32(4), uint32(4)) + + mockDecoder := new(compressxdr.MockXDRDecoder) + gcsb.decoder = mockDecoder + gcsb.ledgerBuffer.decoder = mockDecoder + mockDecoder.On("Unzip", readCloser1).Return(objectBytes1, nil) + mockDecoder.On("Unzip", readCloser2).Return(objectBytes2, nil) + + lcm, err := gcsb.GetLedger(ctx, uint32(3)) + assert.NoError(t, err) + assert.Equal(t, lcmArray[0], lcm) + + lcm, err = gcsb.GetLedger(ctx, uint32(4)) + assert.NoError(t, err) + assert.Equal(t, lcmArray[1], lcm) +} + +func TestGCSGetLedger_NotPrepared(t *testing.T) { + gcsb := createGCSBackendForTesting() + ctx := context.Background() + + _, err := gcsb.GetLedger(ctx, uint32(3)) + assert.Error(t, err, "session is not prepared, call PrepareRange first") +} + +func TestGCSGetLedger_SequenceNotInBatch(t *testing.T) { + gcsb := createGCSBackendForTesting() + ctx := context.Background() + ledgerRange := BoundedRange(3, 5) + gcsb.ledgerBuffer = createGCSLedgerBufferForTesting(ledgerRange) + gcsb.PrepareRange(ctx, ledgerRange) + gcsb.ledgerMetaArchive = datastore.NewLedgerMetaArchive("", 4, 0) + + _, err := gcsb.GetLedger(ctx, uint32(2)) + assert.Error(t, err, "requested sequence preceeds current LedgerRange") + + _, err = gcsb.GetLedger(ctx, uint32(6)) + assert.Error(t, err, "requested sequence beyond current LedgerRange") + + _, err = gcsb.GetLedger(ctx, uint32(3)) + assert.Error(t, err, "requested sequence preceeds current LedgerCloseMetaBatch") +} + func TestGCSPrepareRange(t *testing.T) { gcsb := createGCSBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(2, 3) - gcsb.ledgerBuffer = createLedgerBufferGCSForTesting(ledgerRange) + gcsb.ledgerBuffer = createGCSLedgerBufferForTesting(ledgerRange) err := gcsb.PrepareRange(ctx, ledgerRange) assert.NoError(t, err) @@ -89,7 +224,7 @@ func TestGCSPrepareRange_AlreadyPrepared(t *testing.T) { gcsb := createGCSBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(2, 3) - gcsb.ledgerBuffer = createLedgerBufferGCSForTesting(ledgerRange) + gcsb.ledgerBuffer = createGCSLedgerBufferForTesting(ledgerRange) gcsb.prepared = &ledgerRange err := gcsb.PrepareRange(ctx, ledgerRange) @@ -100,7 +235,7 @@ func TestGCSIsPrepared_Bounded(t *testing.T) { gcsb := createGCSBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(3, 4) - gcsb.ledgerBuffer = createLedgerBufferGCSForTesting(ledgerRange) + gcsb.ledgerBuffer = createGCSLedgerBufferForTesting(ledgerRange) gcsb.PrepareRange(ctx, ledgerRange) ok, err := gcsb.IsPrepared(ctx, ledgerRange) @@ -124,7 +259,7 @@ func TestGCSIsPrepared_Unbounded(t *testing.T) { gcsb := createGCSBackendForTesting() ctx := context.Background() ledgerRange := UnboundedRange(3) - gcsb.ledgerBuffer = createLedgerBufferGCSForTesting(ledgerRange) + gcsb.ledgerBuffer = createGCSLedgerBufferForTesting(ledgerRange) gcsb.PrepareRange(ctx, ledgerRange) ok, err := gcsb.IsPrepared(ctx, ledgerRange) @@ -152,29 +287,22 @@ func TestGCSClose(t *testing.T) { gcsb := createGCSBackendForTesting() ctx := context.Background() ledgerRange := UnboundedRange(3) - gcsb.ledgerBuffer = createLedgerBufferGCSForTesting(ledgerRange) + gcsb.ledgerBuffer = createGCSLedgerBufferForTesting(ledgerRange) gcsb.PrepareRange(ctx, ledgerRange) err := gcsb.Close() assert.NoError(t, err) assert.Equal(t, true, gcsb.closed) -} -func TestGCSGetLedger(t *testing.T) { - expectedLCM := datastore.CreateLedgerCloseMeta(3) + _, err = gcsb.GetLatestLedgerSequence(ctx) + assert.Error(t, err, "gcsBackend is closed; cannot GetLatestLedgerSequence") - gcsb := createGCSBackendForTesting() - ctx := context.Background() - ledgerRange := BoundedRange(3, 3) - gcsb.ledgerBuffer = createLedgerBufferGCSForTesting(ledgerRange) - gcsb.PrepareRange(ctx, ledgerRange) + _, err = gcsb.GetLedger(ctx, 3) + assert.Error(t, err, "gcsBackend is closed; cannot GetLedger") - mockDataStore := new(datastore.MockDataStore) - gcsb.dataStore = mockDataStore - gcsb.ledgerBuffer.dataStore = mockDataStore - mockDataStore.On("GetFile", ctx, "0-63999/3.xdr.gz") + err = gcsb.PrepareRange(ctx, ledgerRange) + assert.Error(t, err, "gcsBackend is closed; cannot PrepareRange") - lcm, err := gcsb.GetLedger(ctx, 2) - assert.NoError(t, err) - assert.Equal(t, expectedLCM, lcm) + _, err = gcsb.IsPrepared(ctx, ledgerRange) + assert.Error(t, err, "gcsBackend is closed; cannot IsPrepared") } diff --git a/support/compressxdr/mocks.go b/support/compressxdr/mocks.go new file mode 100644 index 0000000000..2525ca4477 --- /dev/null +++ b/support/compressxdr/mocks.go @@ -0,0 +1,23 @@ +package compressxdr + +import ( + "io" + + "github.com/stretchr/testify/mock" +) + +type MockXDRDecoder struct { + mock.Mock +} + +func (m *MockXDRDecoder) ReadFrom(r io.Reader) (int64, error) { + args := m.Called(r) + return args.Get(0).(int64), args.Error(1) +} + +func (m *MockXDRDecoder) Unzip(r io.Reader) ([]byte, error) { + args := m.Called(r) + return args.Get(0).([]byte), args.Error(1) +} + +var _ XDRDecoder = &MockXDRDecoder{} diff --git a/support/datastore/ledger_meta_archive.go b/support/datastore/ledger_meta_archive.go index 631e5749ee..356b4754de 100644 --- a/support/datastore/ledger_meta_archive.go +++ b/support/datastore/ledger_meta_archive.go @@ -79,12 +79,18 @@ func (f *LedgerMetaArchive) GetLedger(sequence uint32) (*xdr.LedgerCloseMeta, er func CreateLedgerCloseMeta(ledgerSeq uint32) xdr.LedgerCloseMeta { return xdr.LedgerCloseMeta{ + V: int32(0), V0: &xdr.LedgerCloseMetaV0{ LedgerHeader: xdr.LedgerHeaderHistoryEntry{ Header: xdr.LedgerHeader{ LedgerSeq: xdr.Uint32(ledgerSeq), }, }, + TxSet: xdr.TransactionSet{}, + TxProcessing: nil, + UpgradesProcessing: nil, + ScpInfo: nil, }, + V1: nil, } } From ee64b81ebaa005685038dbb471b0c44862e770c5 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 3 May 2024 11:20:24 -0400 Subject: [PATCH 138/234] fixing unit tests --- ingest/ledgerbackend/gcs_backend_test.go | 15 +++++++------ support/datastore/ledgerbatch_config_test.go | 23 ++++++++++---------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/ingest/ledgerbackend/gcs_backend_test.go b/ingest/ledgerbackend/gcs_backend_test.go index fe6fc98322..624d7b017a 100644 --- a/ingest/ledgerbackend/gcs_backend_test.go +++ b/ingest/ledgerbackend/gcs_backend_test.go @@ -16,7 +16,7 @@ import ( func createGCSBackendConfigForTesting() GCSBackendConfig { bufferConfig := BufferConfig{ BufferSize: 100, - NumWorkers: 5, + NumWorkers: 1, RetryLimit: 3, RetryWait: 1, } @@ -147,32 +147,33 @@ func createLCMBatchBinaryForTesting(lcm xdr.LedgerCloseMeta, start uint32, end u func TestGCSGetLedger(t *testing.T) { startLedger := uint32(3) - endLedger := uint32(4) + endLedger := uint32(5) lcmArray := createLCMForTesting(startLedger, endLedger) gcsb := createGCSBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(startLedger, endLedger) - gcsb.ledgerBuffer = createGCSLedgerBufferForTesting(ledgerRange) - gcsb.PrepareRange(ctx, ledgerRange) - readCloser1 := createReadCloserForTesting() readCloser2 := createReadCloserForTesting() + readCloser3 := createReadCloserForTesting() mockDataStore := new(datastore.MockDataStore) gcsb.dataStore = mockDataStore - gcsb.ledgerBuffer.dataStore = mockDataStore mockDataStore.On("GetFile", ctx, "0-63999/3.xdr.gz").Return(readCloser1, nil) mockDataStore.On("GetFile", ctx, "0-63999/4.xdr.gz").Return(readCloser2, nil) + mockDataStore.On("GetFile", ctx, "0-63999/5.xdr.gz").Return(readCloser3, nil) objectBytes1 := createLCMBatchBinaryForTesting(lcmArray[0], uint32(3), uint32(3)) objectBytes2 := createLCMBatchBinaryForTesting(lcmArray[1], uint32(4), uint32(4)) + objectBytes3 := createLCMBatchBinaryForTesting(lcmArray[2], uint32(5), uint32(5)) mockDecoder := new(compressxdr.MockXDRDecoder) gcsb.decoder = mockDecoder - gcsb.ledgerBuffer.decoder = mockDecoder mockDecoder.On("Unzip", readCloser1).Return(objectBytes1, nil) mockDecoder.On("Unzip", readCloser2).Return(objectBytes2, nil) + mockDecoder.On("Unzip", readCloser3).Return(objectBytes3, nil) + + gcsb.PrepareRange(ctx, ledgerRange) lcm, err := gcsb.GetLedger(ctx, uint32(3)) assert.NoError(t, err) diff --git a/support/datastore/ledgerbatch_config_test.go b/support/datastore/ledgerbatch_config_test.go index e45fee109e..9b2e466677 100644 --- a/support/datastore/ledgerbatch_config_test.go +++ b/support/datastore/ledgerbatch_config_test.go @@ -12,23 +12,24 @@ func TestGetObjectKeyFromSequenceNumber(t *testing.T) { filesPerPartition uint32 ledgerSeq uint32 ledgersPerFile uint32 + fileSuffix string expectedKey string }{ - {0, 5, 1, "5.xdr.gz"}, - {0, 5, 10, "0-9.xdr.gz"}, - {2, 10, 100, "0-199/0-99.xdr.gz"}, - {2, 150, 50, "100-199/150-199.xdr.gz"}, - {2, 300, 200, "0-399/200-399.xdr.gz"}, - {2, 1, 1, "0-1/1.xdr.gz"}, - {4, 10, 100, "0-399/0-99.xdr.gz"}, - {4, 250, 50, "200-399/250-299.xdr.gz"}, - {1, 300, 200, "200-399.xdr.gz"}, - {1, 1, 1, "1.xdr.gz"}, + {0, 5, 1, ".xdr.gz", "5.xdr.gz"}, + {0, 5, 10, ".xdr.gz", "0-9.xdr.gz"}, + {2, 10, 100, ".xdr.gz", "0-199/0-99.xdr.gz"}, + {2, 150, 50, ".xdr.gz", "100-199/150-199.xdr.gz"}, + {2, 300, 200, ".xdr.gz", "0-399/200-399.xdr.gz"}, + {2, 1, 1, ".xdr.gz", "0-1/1.xdr.gz"}, + {4, 10, 100, ".xdr.gz", "0-399/0-99.xdr.gz"}, + {4, 250, 50, ".xdr.gz", "200-399/250-299.xdr.gz"}, + {1, 300, 200, ".xdr.gz", "200-399.xdr.gz"}, + {1, 1, 1, ".xdr.gz", "1.xdr.gz"}, } for _, tc := range testCases { t.Run(fmt.Sprintf("LedgerSeq-%d-LedgersPerFile-%d", tc.ledgerSeq, tc.ledgersPerFile), func(t *testing.T) { - config := LedgerBatchConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile} + config := LedgerBatchConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile, FileSuffix: tc.fileSuffix} key := config.GetObjectKeyFromSequenceNumber(tc.ledgerSeq) require.Equal(t, tc.expectedKey, key) }) From d53248260bb89c76351246b843259d4fb485a3f7 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 3 May 2024 13:14:37 -0400 Subject: [PATCH 139/234] Update unit tests --- ingest/ledgerbackend/gcs_backend.go | 2 +- ingest/ledgerbackend/gcs_backend_test.go | 128 +++++++++++++++-------- support/compressxdr/compress_xdr_test.go | 3 +- 3 files changed, 88 insertions(+), 45 deletions(-) diff --git a/ingest/ledgerbackend/gcs_backend.go b/ingest/ledgerbackend/gcs_backend.go index 61fa9c9f2e..aad5494c24 100644 --- a/ingest/ledgerbackend/gcs_backend.go +++ b/ingest/ledgerbackend/gcs_backend.go @@ -206,7 +206,7 @@ func (lb *ledgerBufferGCS) storeObject(ledgerObject []byte, sequence uint32) { for lb.ledgerPriorityQueue.Len() > 0 && lb.currentLedger == uint32(lb.ledgerPriorityQueue.Peek().StartLedger) { item := lb.ledgerPriorityQueue.Pop() lb.ledgerQueue <- item.Payload - lb.currentLedger++ + lb.currentLedger += lb.config.LedgerBatchConfig.LedgersPerFile lb.nextLedgerQueueLedger++ } } diff --git a/ingest/ledgerbackend/gcs_backend_test.go b/ingest/ledgerbackend/gcs_backend_test.go index 624d7b017a..814d2212be 100644 --- a/ingest/ledgerbackend/gcs_backend_test.go +++ b/ingest/ledgerbackend/gcs_backend_test.go @@ -16,7 +16,7 @@ import ( func createGCSBackendConfigForTesting() GCSBackendConfig { bufferConfig := BufferConfig{ BufferSize: 100, - NumWorkers: 1, + NumWorkers: 5, RetryLimit: 3, RetryWait: 1, } @@ -65,18 +65,6 @@ func createGCSBackendForTesting() GCSBackend { } } -func createGCSLedgerBufferForTesting(ledgerRange Range) *ledgerBufferGCS { - gcsb := createGCSBackendForTesting() - ledgerBuffer, _ := gcsb.NewLedgerBuffer(ledgerRange) - return ledgerBuffer -} - -func createReadCloserForTesting() io.ReadCloser { - var capturedBuf []byte - reader := bytes.NewReader(capturedBuf) - return io.NopCloser(reader) -} - func TestNewGCSBackend(t *testing.T) { ctx := context.Background() config := createGCSBackendConfigForTesting() @@ -133,29 +121,39 @@ func createLCMForTesting(start, end uint32) []xdr.LedgerCloseMeta { return lcmArray } -func createLCMBatchBinaryForTesting(lcm xdr.LedgerCloseMeta, start uint32, end uint32) []byte { - lcmBatch := xdr.LedgerCloseMetaBatch{ - StartSequence: xdr.Uint32(start), - EndSequence: xdr.Uint32(end), - LedgerCloseMetas: []xdr.LedgerCloseMeta{ - lcm, - }, +func createTestLedgerCloseMetaBatch(startSeq, endSeq uint32, count int) xdr.LedgerCloseMetaBatch { + var ledgerCloseMetas []xdr.LedgerCloseMeta + for i := 0; i < count; i++ { + ledgerCloseMetas = append(ledgerCloseMetas, datastore.CreateLedgerCloseMeta(startSeq+uint32(i))) + } + return xdr.LedgerCloseMetaBatch{ + StartSequence: xdr.Uint32(startSeq), + EndSequence: xdr.Uint32(endSeq), + LedgerCloseMetas: ledgerCloseMetas, } - lcmBatchBinary, _ := lcmBatch.MarshalBinary() - return lcmBatchBinary } -func TestGCSGetLedger(t *testing.T) { +func createLCMBatchReader(start, end uint32, count int) io.ReadCloser { + testData := createTestLedgerCloseMetaBatch(start, end, count) + encoder, _ := compressxdr.NewXDREncoder(compressxdr.GZIP, testData) + var buf bytes.Buffer + encoder.WriteTo(&buf) + capturedBuf := buf.Bytes() + reader1 := bytes.NewReader(capturedBuf) + return io.NopCloser(reader1) +} + +func TestGCSGetLedger_SingleLedgerPerFile(t *testing.T) { startLedger := uint32(3) endLedger := uint32(5) lcmArray := createLCMForTesting(startLedger, endLedger) - gcsb := createGCSBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(startLedger, endLedger) - readCloser1 := createReadCloserForTesting() - readCloser2 := createReadCloserForTesting() - readCloser3 := createReadCloserForTesting() + + readCloser1 := createLCMBatchReader(uint32(3), uint32(3), 1) + readCloser2 := createLCMBatchReader(uint32(4), uint32(4), 1) + readCloser3 := createLCMBatchReader(uint32(5), uint32(5), 1) mockDataStore := new(datastore.MockDataStore) gcsb.dataStore = mockDataStore @@ -163,25 +161,75 @@ func TestGCSGetLedger(t *testing.T) { mockDataStore.On("GetFile", ctx, "0-63999/4.xdr.gz").Return(readCloser2, nil) mockDataStore.On("GetFile", ctx, "0-63999/5.xdr.gz").Return(readCloser3, nil) - objectBytes1 := createLCMBatchBinaryForTesting(lcmArray[0], uint32(3), uint32(3)) - objectBytes2 := createLCMBatchBinaryForTesting(lcmArray[1], uint32(4), uint32(4)) - objectBytes3 := createLCMBatchBinaryForTesting(lcmArray[2], uint32(5), uint32(5)) + gcsb.PrepareRange(ctx, ledgerRange) + + lcm, err := gcsb.GetLedger(ctx, uint32(3)) + assert.NoError(t, err) + assert.Equal(t, lcmArray[0], lcm) + // Skip sequence 4; Test non consecutive GetLedger + lcm, err = gcsb.GetLedger(ctx, uint32(5)) + assert.NoError(t, err) + assert.Equal(t, lcmArray[2], lcm) +} + +func TestGCSGetLedger_MultipleLedgerPerFile(t *testing.T) { + startLedger := uint32(2) + endLedger := uint32(5) + lcmArray := createLCMForTesting(startLedger, endLedger) + gcsb := createGCSBackendForTesting() + ctx := context.Background() + gcsb.config.LedgerBatchConfig.LedgersPerFile = uint32(2) + ledgerRange := BoundedRange(startLedger, endLedger) - mockDecoder := new(compressxdr.MockXDRDecoder) - gcsb.decoder = mockDecoder - mockDecoder.On("Unzip", readCloser1).Return(objectBytes1, nil) - mockDecoder.On("Unzip", readCloser2).Return(objectBytes2, nil) - mockDecoder.On("Unzip", readCloser3).Return(objectBytes3, nil) + readCloser1 := createLCMBatchReader(uint32(2), uint32(3), 2) + readCloser2 := createLCMBatchReader(uint32(4), uint32(5), 2) + + mockDataStore := new(datastore.MockDataStore) + gcsb.dataStore = mockDataStore + mockDataStore.On("GetFile", ctx, "0-127999/2-3.xdr.gz").Return(readCloser1, nil) + mockDataStore.On("GetFile", ctx, "0-127999/4-5.xdr.gz").Return(readCloser2, nil) gcsb.PrepareRange(ctx, ledgerRange) - lcm, err := gcsb.GetLedger(ctx, uint32(3)) + lcm, err := gcsb.GetLedger(ctx, uint32(2)) assert.NoError(t, err) assert.Equal(t, lcmArray[0], lcm) - lcm, err = gcsb.GetLedger(ctx, uint32(4)) + lcm, err = gcsb.GetLedger(ctx, uint32(3)) assert.NoError(t, err) assert.Equal(t, lcmArray[1], lcm) + + lcm, err = gcsb.GetLedger(ctx, uint32(4)) + assert.NoError(t, err) + assert.Equal(t, lcmArray[2], lcm) +} + +func TestGCSGetLedger_ErrorPreceedingLedger(t *testing.T) { + startLedger := uint32(3) + endLedger := uint32(5) + lcmArray := createLCMForTesting(startLedger, endLedger) + gcsb := createGCSBackendForTesting() + ctx := context.Background() + ledgerRange := BoundedRange(startLedger, endLedger) + + readCloser1 := createLCMBatchReader(uint32(3), uint32(3), 1) + readCloser2 := createLCMBatchReader(uint32(4), uint32(4), 1) + readCloser3 := createLCMBatchReader(uint32(5), uint32(5), 1) + + mockDataStore := new(datastore.MockDataStore) + gcsb.dataStore = mockDataStore + mockDataStore.On("GetFile", ctx, "0-63999/3.xdr.gz").Return(readCloser1, nil) + mockDataStore.On("GetFile", ctx, "0-63999/4.xdr.gz").Return(readCloser2, nil) + mockDataStore.On("GetFile", ctx, "0-63999/5.xdr.gz").Return(readCloser3, nil) + + gcsb.PrepareRange(ctx, ledgerRange) + + lcm, err := gcsb.GetLedger(ctx, uint32(5)) + assert.NoError(t, err) + assert.Equal(t, lcmArray[2], lcm) + + _, err = gcsb.GetLedger(ctx, uint32(4)) + assert.Error(t, err, "requested sequence preceeds current LedgerCloseMetaBatch") } func TestGCSGetLedger_NotPrepared(t *testing.T) { @@ -196,7 +244,6 @@ func TestGCSGetLedger_SequenceNotInBatch(t *testing.T) { gcsb := createGCSBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(3, 5) - gcsb.ledgerBuffer = createGCSLedgerBufferForTesting(ledgerRange) gcsb.PrepareRange(ctx, ledgerRange) gcsb.ledgerMetaArchive = datastore.NewLedgerMetaArchive("", 4, 0) @@ -214,7 +261,6 @@ func TestGCSPrepareRange(t *testing.T) { gcsb := createGCSBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(2, 3) - gcsb.ledgerBuffer = createGCSLedgerBufferForTesting(ledgerRange) err := gcsb.PrepareRange(ctx, ledgerRange) assert.NoError(t, err) @@ -225,7 +271,6 @@ func TestGCSPrepareRange_AlreadyPrepared(t *testing.T) { gcsb := createGCSBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(2, 3) - gcsb.ledgerBuffer = createGCSLedgerBufferForTesting(ledgerRange) gcsb.prepared = &ledgerRange err := gcsb.PrepareRange(ctx, ledgerRange) @@ -236,7 +281,6 @@ func TestGCSIsPrepared_Bounded(t *testing.T) { gcsb := createGCSBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(3, 4) - gcsb.ledgerBuffer = createGCSLedgerBufferForTesting(ledgerRange) gcsb.PrepareRange(ctx, ledgerRange) ok, err := gcsb.IsPrepared(ctx, ledgerRange) @@ -260,7 +304,6 @@ func TestGCSIsPrepared_Unbounded(t *testing.T) { gcsb := createGCSBackendForTesting() ctx := context.Background() ledgerRange := UnboundedRange(3) - gcsb.ledgerBuffer = createGCSLedgerBufferForTesting(ledgerRange) gcsb.PrepareRange(ctx, ledgerRange) ok, err := gcsb.IsPrepared(ctx, ledgerRange) @@ -288,7 +331,6 @@ func TestGCSClose(t *testing.T) { gcsb := createGCSBackendForTesting() ctx := context.Background() ledgerRange := UnboundedRange(3) - gcsb.ledgerBuffer = createGCSLedgerBufferForTesting(ledgerRange) gcsb.PrepareRange(ctx, ledgerRange) err := gcsb.Close() diff --git a/support/compressxdr/compress_xdr_test.go b/support/compressxdr/compress_xdr_test.go index bd9a6c0ba9..d8973dca4e 100644 --- a/support/compressxdr/compress_xdr_test.go +++ b/support/compressxdr/compress_xdr_test.go @@ -4,6 +4,7 @@ import ( "bytes" "testing" + "github.com/stellar/go/support/datastore" "github.com/stellar/go/xdr" "github.com/stretchr/testify/require" ) @@ -11,7 +12,7 @@ import ( func createTestLedgerCloseMetaBatch(startSeq, endSeq uint32, count int) xdr.LedgerCloseMetaBatch { var ledgerCloseMetas []xdr.LedgerCloseMeta for i := 0; i < count; i++ { - // ledgerCloseMetas = append(ledgerCloseMetas, createLedgerCloseMeta(startSeq+uint32(i))) + ledgerCloseMetas = append(ledgerCloseMetas, datastore.CreateLedgerCloseMeta(startSeq+uint32(i))) } return xdr.LedgerCloseMetaBatch{ StartSequence: xdr.Uint32(startSeq), From 5eba32448df213aa1a4a26b723eb515b1883a84a Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 3 May 2024 13:31:50 -0400 Subject: [PATCH 140/234] Fix unit tests --- support/compressxdr/compress_xdr_test.go | 9 ++++----- support/datastore/resumablemanager_test.go | 13 +++++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/support/compressxdr/compress_xdr_test.go b/support/compressxdr/compress_xdr_test.go index d8973dca4e..da94ab25ae 100644 --- a/support/compressxdr/compress_xdr_test.go +++ b/support/compressxdr/compress_xdr_test.go @@ -4,7 +4,6 @@ import ( "bytes" "testing" - "github.com/stellar/go/support/datastore" "github.com/stellar/go/xdr" "github.com/stretchr/testify/require" ) @@ -12,7 +11,7 @@ import ( func createTestLedgerCloseMetaBatch(startSeq, endSeq uint32, count int) xdr.LedgerCloseMetaBatch { var ledgerCloseMetas []xdr.LedgerCloseMeta for i := 0; i < count; i++ { - ledgerCloseMetas = append(ledgerCloseMetas, datastore.CreateLedgerCloseMeta(startSeq+uint32(i))) + //ledgerCloseMetas = append(ledgerCloseMetas, datastore.CreateLedgerCloseMeta(startSeq+uint32(i))) } return xdr.LedgerCloseMetaBatch{ StartSequence: xdr.Uint32(startSeq), @@ -51,7 +50,8 @@ func TestEncodeDecodeLedgerCloseMetaBatchGzip(t *testing.T) { } func TestDecodeUnzipGzip(t *testing.T) { - testData := createTestLedgerCloseMetaBatch(1000, 1005, 6) + expectedBinary := []byte{0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0} + testData := createTestLedgerCloseMetaBatch(2, 2, 1) // Encode the test data encoder, err := NewXDREncoder(GZIP, testData) @@ -69,6 +69,5 @@ func TestDecodeUnzipGzip(t *testing.T) { binary, err := decoder.Unzip(&buf) require.NoError(t, err) - require.Equal(t, []uint8([]byte{0x0, 0x0, 0x3, 0xe8, 0x0, 0x0, 0x3, 0xed, 0x0, 0x0, 0x0, 0x0}), binary) - + require.Equal(t, expectedBinary, binary) } diff --git a/support/datastore/resumablemanager_test.go b/support/datastore/resumablemanager_test.go index 15b02c3be4..34279682ef 100644 --- a/support/datastore/resumablemanager_test.go +++ b/support/datastore/resumablemanager_test.go @@ -32,6 +32,7 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), + FileSuffix: ".xdr.gz", }, networkName: "test", errorSnippet: "archive error", @@ -46,6 +47,7 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), + FileSuffix: ".xdr.gz", }, networkName: "test", }, @@ -58,6 +60,7 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), + FileSuffix: ".xdr.gz", }, networkName: "test", }, @@ -70,6 +73,7 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), + FileSuffix: ".xdr.gz", }, networkName: "test", errorSnippet: "datastore error happened", @@ -83,6 +87,7 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), + FileSuffix: ".xdr.gz", }, networkName: "test", }, @@ -95,6 +100,7 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), + FileSuffix: ".xdr.gz", }, networkName: "test", }, @@ -107,6 +113,7 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), + FileSuffix: ".xdr.gz", }, networkName: "test", }, @@ -119,6 +126,7 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), + FileSuffix: ".xdr.gz", }, networkName: "test", }, @@ -131,6 +139,7 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), + FileSuffix: ".xdr.gz", }, networkName: "test", errorSnippet: "Invalid start value", @@ -144,6 +153,7 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), + FileSuffix: ".xdr.gz", }, networkName: "test2", latestLedger: uint32(2000), @@ -157,6 +167,7 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), + FileSuffix: ".xdr.gz", }, networkName: "test3", latestLedger: uint32(3000), @@ -170,6 +181,7 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), + FileSuffix: ".xdr.gz", }, networkName: "test4", latestLedger: uint32(4000), @@ -183,6 +195,7 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), + FileSuffix: ".xdr.gz", }, networkName: "test5", latestLedger: uint32(5000), From 5c54f57ea13550f7df764ef12fe414640bc20923 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 3 May 2024 15:49:14 -0400 Subject: [PATCH 141/234] Address comments --- ingest/ledgerbackend/gcs_backend.go | 235 ++++------------------ ingest/ledgerbackend/gcs_backend_test.go | 27 +-- ingest/ledgerbackend/gcs_ledger_buffer.go | 177 ++++++++++++++++ support/datastore/ledger_meta_archive.go | 8 +- 4 files changed, 229 insertions(+), 218 deletions(-) create mode 100644 ingest/ledgerbackend/gcs_ledger_buffer.go diff --git a/ingest/ledgerbackend/gcs_backend.go b/ingest/ledgerbackend/gcs_backend.go index aad5494c24..656fbce7fc 100644 --- a/ingest/ledgerbackend/gcs_backend.go +++ b/ingest/ledgerbackend/gcs_backend.go @@ -7,22 +7,26 @@ import ( "github.com/pkg/errors" - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/support/collections/heap" "github.com/stellar/go/support/compressxdr" "github.com/stellar/go/support/datastore" - "github.com/stellar/go/support/log" "github.com/stellar/go/xdr" - - "google.golang.org/api/googleapi" ) // Ensure GCSBackend implements LedgerBackend var _ LedgerBackend = (*GCSBackend)(nil) -type LedgerBatchObject struct { - Payload []byte - StartLedger int // Ledger sequence used as the priority for the priorityqueue. +// default config values +var defaultFileSuffix = ".xdr.gz" +var defaultLedgersPerFile = uint32(1) +var defaultFilesPerPartition = uint32(64000) +var defaultBufferSize = uint32(1000) +var defaultNumWorkers = uint32(5) +var defaultRetryLimit = uint32(3) +var defaultRetryWait = time.Duration(5) + +type ledgerBatchObject struct { + payload []byte + startLedger int // Ledger sequence used as the priority for the priorityqueue. } type BufferConfig struct { @@ -34,9 +38,7 @@ type BufferConfig struct { type GCSBackendConfig struct { BufferConfig BufferConfig - DataStoreConfig datastore.DataStoreConfig LedgerBatchConfig datastore.LedgerBatchConfig - Network string CompressionType string DataStore datastore.DataStore ResumableManager datastore.ResumableManager @@ -66,220 +68,49 @@ type GCSBackend struct { closed bool // False until the core is closed ledgerMetaArchive *datastore.LedgerMetaArchive decoder compressxdr.XDRDecoder -} - -type ledgerBufferGCS struct { - config GCSBackendConfig - dataStore datastore.DataStore - taskQueue chan uint32 // buffer next gcs object read - ledgerQueue chan []byte // order corrected lcm batches - ledgerPriorityQueue *heap.Heap[LedgerBatchObject] - priorityQueueLock sync.Mutex - count uint32 // buffer count - limit uint32 // buffer max - done chan struct{} - - // keep track of the ledgers to be processed and the next ordering - // the ledgers should be buffered - currentLedger uint32 - nextTaskLedger uint32 - nextLedgerQueueLedger uint32 - ledgerRange Range - - // passed through from GCSBackend to control lifetime of ledgerBufferGCS instance - context context.Context - cancel context.CancelCauseFunc - decoder compressxdr.XDRDecoder -} - -func (gcsb *GCSBackend) NewLedgerBuffer(ledgerRange Range) (*ledgerBufferGCS, error) { - less := func(a, b LedgerBatchObject) bool { - return a.StartLedger < b.StartLedger - } - pq := heap.New(less, int(gcsb.config.BufferConfig.BufferSize)) - - done := make(chan struct{}) - - ledgerBuffer := &ledgerBufferGCS{ - config: gcsb.config, - dataStore: gcsb.dataStore, - taskQueue: make(chan uint32, gcsb.config.BufferConfig.BufferSize), - ledgerQueue: make(chan []byte, gcsb.config.BufferConfig.BufferSize), - ledgerPriorityQueue: pq, - count: 0, - limit: gcsb.config.BufferConfig.BufferSize, - done: done, - currentLedger: ledgerRange.from, - nextTaskLedger: ledgerRange.from, - nextLedgerQueueLedger: ledgerRange.from, - ledgerRange: ledgerRange, - context: gcsb.context, - cancel: gcsb.cancel, - decoder: gcsb.decoder, - } - - // Workers to read LCM files - for i := uint32(0); i < gcsb.config.BufferConfig.NumWorkers; i++ { - go ledgerBuffer.worker() - } - - return ledgerBuffer, nil -} - -func (lb *ledgerBufferGCS) pushTaskQueue() { - for lb.count <= lb.limit { - // In bounded mode, don't queue past the end ledger - if lb.nextTaskLedger > lb.ledgerRange.to && lb.ledgerRange.bounded { - return - } - lb.taskQueue <- lb.nextTaskLedger - lb.nextTaskLedger += lb.config.LedgerBatchConfig.LedgersPerFile - lb.count++ - } -} - -func (lb *ledgerBufferGCS) worker() { - for { - select { - case <-lb.done: - log.Error("abort: getFromLedgerQueue blocked") - return - case <-lb.context.Done(): - log.Error(lb.context.Err()) - return - case sequence := <-lb.taskQueue: - retryCount := uint32(0) - for retryCount <= lb.config.BufferConfig.RetryLimit { - ledgerObject, err := lb.getLedgerGCSObject(sequence) - if err != nil { - if e, ok := err.(*googleapi.Error); ok { - // ledgerObject not found and unbounded - if e.Code == 404 && !lb.ledgerRange.bounded { - time.Sleep(lb.config.BufferConfig.RetryWait * time.Second) - continue - } - } - if retryCount == lb.config.BufferConfig.RetryLimit { - err = errors.New("maximum retries exceeded for gcs object reads") - lb.cancel(err) - } - retryCount++ - time.Sleep(lb.config.BufferConfig.RetryWait * time.Second) - } - - // Add to priority queue and continue to next task - lb.storeObject(ledgerObject, sequence) - break - } - } - } -} - -func (lb *ledgerBufferGCS) getLedgerGCSObject(sequence uint32) ([]byte, error) { - objectKey := lb.config.LedgerBatchConfig.GetObjectKeyFromSequenceNumber(sequence) - - reader, err := lb.dataStore.GetFile(context.Background(), objectKey) - if err != nil { - return nil, errors.Wrapf(err, "failed getting file: %s", objectKey) - } - - defer reader.Close() - - objectBytes, err := lb.decoder.Unzip(reader) - if err != nil { - return nil, errors.Wrapf(err, "failed unzipping file: %s", objectKey) - } - - return objectBytes, nil -} - -func (lb *ledgerBufferGCS) storeObject(ledgerObject []byte, sequence uint32) { - lb.priorityQueueLock.Lock() - defer lb.priorityQueueLock.Unlock() - - lb.ledgerPriorityQueue.Push(LedgerBatchObject{ - Payload: ledgerObject, - StartLedger: int(sequence), - }) - - // Check if the nextLedger is the next item in the priority queue - for lb.ledgerPriorityQueue.Len() > 0 && lb.currentLedger == uint32(lb.ledgerPriorityQueue.Peek().StartLedger) { - item := lb.ledgerPriorityQueue.Pop() - lb.ledgerQueue <- item.Payload - lb.currentLedger += lb.config.LedgerBatchConfig.LedgersPerFile - lb.nextLedgerQueueLedger++ - } -} - -func (lb *ledgerBufferGCS) getFromLedgerQueue() ([]byte, error) { - for { - select { - case <-lb.context.Done(): - log.Info("Stopping getFromLedgerQueue due to context cancellation") - close(lb.done) - return nil, lb.context.Err() - case lcmBinary := <-lb.ledgerQueue: - // Decrement ledger buffer counter - lb.count-- - // Add next task to the TaskQueue - lb.pushTaskQueue() - - return lcmBinary, nil - } - } + nextLedger uint32 } // Return a new GCSBackend instance. func NewGCSBackend(ctx context.Context, config GCSBackendConfig) (*GCSBackend, error) { // Check/set minimum config values for LedgerBatchConfig if config.LedgerBatchConfig.FileSuffix == "" { - config.LedgerBatchConfig.FileSuffix = ".xdr.gz" + config.LedgerBatchConfig.FileSuffix = defaultFileSuffix } if config.LedgerBatchConfig.LedgersPerFile == 0 { - config.LedgerBatchConfig.LedgersPerFile = 1 + config.LedgerBatchConfig.LedgersPerFile = defaultLedgersPerFile } if config.LedgerBatchConfig.FilesPerPartition == 0 { - config.LedgerBatchConfig.FilesPerPartition = 64000 + config.LedgerBatchConfig.FilesPerPartition = defaultFilesPerPartition } // Check/set minimum config values for BufferConfig if config.BufferConfig.BufferSize == 0 { - config.BufferConfig.BufferSize = 1000 + config.BufferConfig.BufferSize = defaultBufferSize } if config.BufferConfig.NumWorkers == 0 { - config.BufferConfig.NumWorkers = 5 + config.BufferConfig.NumWorkers = defaultNumWorkers } if config.BufferConfig.RetryLimit == 0 { - config.BufferConfig.RetryLimit = 3 + config.BufferConfig.RetryLimit = defaultRetryLimit } if config.BufferConfig.RetryWait == 0 { - config.BufferConfig.RetryWait = 5 + config.BufferConfig.RetryWait = defaultRetryWait } ctx, cancel := context.WithCancelCause(ctx) if config.DataStore == nil { - dataStore, err := datastore.NewDataStore(ctx, config.DataStoreConfig, config.Network) - if err != nil { - return nil, err - } - config.DataStore = dataStore + return nil, errors.New("no DataStore provided") } if config.ResumableManager == nil { - var err error - var archive historyarchive.ArchiveInterface - - if archive, err = datastore.CreateHistoryArchiveFromNetworkName(ctx, config.Network); err != nil { - return nil, err - } - - config.ResumableManager = datastore.NewResumableManager(config.DataStore, config.Network, config.LedgerBatchConfig, archive) + return nil, errors.New("no ResumableManager provided") } ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) @@ -351,6 +182,18 @@ func (gcsb *GCSBackend) getSequenceInBatch(sequence uint32) error { } } +// nextExpectedSequence returns nextLedger (if currently set) or start of +// prepared range. Otherwise it returns 0. +// This is done because `nextLedger` is 0 between the moment Stellar-Core is +// started and streaming the first ledger (in such case we return first ledger +// in requested range). +func (gcsb *GCSBackend) nextExpectedSequence() uint32 { + if gcsb.nextLedger == 0 && gcsb.prepared != nil { + return gcsb.prepared.from + } + return gcsb.nextLedger +} + // GetLedger returns the LedgerCloseMeta for the specified ledger sequence number func (gcsb *GCSBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { gcsb.gcsBackendLock.RLock() @@ -374,6 +217,10 @@ func (gcsb *GCSBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.Led } } + if gcsb.nextExpectedSequence() != sequence { + return xdr.LedgerCloseMeta{}, errors.New("requested sequence is not the next available ledger") + } + err := gcsb.getSequenceInBatch(sequence) if err != nil { return xdr.LedgerCloseMeta{}, err @@ -383,8 +230,9 @@ func (gcsb *GCSBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.Led if err != nil { return xdr.LedgerCloseMeta{}, err } + gcsb.nextLedger++ - return *ledgerCloseMeta, nil + return ledgerCloseMeta, nil } // PrepareRange checks if the starting and ending (if bounded) ledgers exist. @@ -474,13 +322,14 @@ func (gcsb *GCSBackend) startPreparingRange(ledgerRange Range) (bool, error) { } var err error - gcsb.ledgerBuffer, err = gcsb.NewLedgerBuffer(ledgerRange) + gcsb.ledgerBuffer, err = gcsb.newLedgerBuffer(ledgerRange) if err != nil { return false, err } // Start the ledgerBuffer gcsb.ledgerBuffer.pushTaskQueue() + gcsb.nextLedger = ledgerRange.from return false, nil } diff --git a/ingest/ledgerbackend/gcs_backend_test.go b/ingest/ledgerbackend/gcs_backend_test.go index 814d2212be..9c09ca38c9 100644 --- a/ingest/ledgerbackend/gcs_backend_test.go +++ b/ingest/ledgerbackend/gcs_backend_test.go @@ -23,10 +23,6 @@ func createGCSBackendConfigForTesting() GCSBackendConfig { param := make(map[string]string) param["destination_bucket_path"] = "testURL" - dataStoreConfig := datastore.DataStoreConfig{ - Type: "GCS", - Params: param, - } ledgerBatchConfig := datastore.LedgerBatchConfig{ LedgersPerFile: 1, @@ -40,9 +36,7 @@ func createGCSBackendConfigForTesting() GCSBackendConfig { return GCSBackendConfig{ BufferConfig: bufferConfig, - DataStoreConfig: dataStoreConfig, LedgerBatchConfig: ledgerBatchConfig, - Network: "testnet", CompressionType: compressxdr.GZIP, DataStore: dataStore, ResumableManager: resumableManager, @@ -89,12 +83,11 @@ func TestGCSNewLedgerBuffer(t *testing.T) { gcsb := createGCSBackendForTesting() ledgerRange := BoundedRange(2, 3) - ledgerBuffer, err := gcsb.NewLedgerBuffer(ledgerRange) + ledgerBuffer, err := gcsb.newLedgerBuffer(ledgerRange) assert.NoError(t, err) assert.Equal(t, uint32(2), ledgerBuffer.currentLedger) assert.Equal(t, uint32(2), ledgerBuffer.nextTaskLedger) - assert.Equal(t, uint32(2), ledgerBuffer.nextLedgerQueueLedger) assert.Equal(t, ledgerRange, ledgerBuffer.ledgerRange) } @@ -244,17 +237,14 @@ func TestGCSGetLedger_SequenceNotInBatch(t *testing.T) { gcsb := createGCSBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(3, 5) + gcsb.PrepareRange(ctx, ledgerRange) - gcsb.ledgerMetaArchive = datastore.NewLedgerMetaArchive("", 4, 0) _, err := gcsb.GetLedger(ctx, uint32(2)) assert.Error(t, err, "requested sequence preceeds current LedgerRange") _, err = gcsb.GetLedger(ctx, uint32(6)) assert.Error(t, err, "requested sequence beyond current LedgerRange") - - _, err = gcsb.GetLedger(ctx, uint32(3)) - assert.Error(t, err, "requested sequence preceeds current LedgerCloseMetaBatch") } func TestGCSPrepareRange(t *testing.T) { @@ -265,16 +255,11 @@ func TestGCSPrepareRange(t *testing.T) { err := gcsb.PrepareRange(ctx, ledgerRange) assert.NoError(t, err) assert.NotNil(t, gcsb.prepared) -} - -func TestGCSPrepareRange_AlreadyPrepared(t *testing.T) { - gcsb := createGCSBackendForTesting() - ctx := context.Background() - ledgerRange := BoundedRange(2, 3) - gcsb.prepared = &ledgerRange - err := gcsb.PrepareRange(ctx, ledgerRange) + // check alreadyPrepared + err = gcsb.PrepareRange(ctx, ledgerRange) assert.NoError(t, err) + assert.NotNil(t, gcsb.prepared) } func TestGCSIsPrepared_Bounded(t *testing.T) { @@ -330,7 +315,7 @@ func TestGCSIsPrepared_Unbounded(t *testing.T) { func TestGCSClose(t *testing.T) { gcsb := createGCSBackendForTesting() ctx := context.Background() - ledgerRange := UnboundedRange(3) + ledgerRange := BoundedRange(3, 5) gcsb.PrepareRange(ctx, ledgerRange) err := gcsb.Close() diff --git a/ingest/ledgerbackend/gcs_ledger_buffer.go b/ingest/ledgerbackend/gcs_ledger_buffer.go new file mode 100644 index 0000000000..c58c113bab --- /dev/null +++ b/ingest/ledgerbackend/gcs_ledger_buffer.go @@ -0,0 +1,177 @@ +package ledgerbackend + +import ( + "bytes" + "context" + "io" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/stellar/go/support/collections/heap" + "github.com/stellar/go/support/compressxdr" + "github.com/stellar/go/support/datastore" + "github.com/stellar/go/support/log" + "google.golang.org/api/googleapi" +) + +type ledgerBufferGCS struct { + config GCSBackendConfig + dataStore datastore.DataStore + taskQueue chan uint32 // buffer next gcs object read + ledgerQueue chan []byte // order corrected lcm batches + ledgerPriorityQueue *heap.Heap[ledgerBatchObject] + priorityQueueLock sync.Mutex + done chan struct{} + + // keep track of the ledgers to be processed and the next ordering + // the ledgers should be buffered + currentLedger uint32 + nextTaskLedger uint32 + ledgerRange Range + + // passed through from GCSBackend to control lifetime of ledgerBufferGCS instance + context context.Context + cancel context.CancelCauseFunc + decoder compressxdr.XDRDecoder +} + +func (gcsb *GCSBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBufferGCS, error) { + less := func(a, b ledgerBatchObject) bool { + return a.startLedger < b.startLedger + } + pq := heap.New(less, int(gcsb.config.BufferConfig.BufferSize)) + + done := make(chan struct{}) + + ledgerBuffer := &ledgerBufferGCS{ + config: gcsb.config, + dataStore: gcsb.dataStore, + taskQueue: make(chan uint32, gcsb.config.BufferConfig.BufferSize), + ledgerQueue: make(chan []byte, gcsb.config.BufferConfig.BufferSize), + ledgerPriorityQueue: pq, + done: done, + currentLedger: ledgerRange.from, + nextTaskLedger: ledgerRange.from, + ledgerRange: ledgerRange, + context: gcsb.context, + cancel: gcsb.cancel, + decoder: gcsb.decoder, + } + + // Workers to read LCM files + for i := uint32(0); i < gcsb.config.BufferConfig.NumWorkers; i++ { + go ledgerBuffer.worker() + } + + return ledgerBuffer, nil +} + +func (lb *ledgerBufferGCS) pushTaskQueue() { + for { + // In bounded mode, don't queue past the end ledger + if lb.nextTaskLedger > lb.ledgerRange.to && lb.ledgerRange.bounded { + return + } + select { + case lb.taskQueue <- lb.nextTaskLedger: + lb.nextTaskLedger += lb.config.LedgerBatchConfig.LedgersPerFile + default: + return + } + } +} + +func (lb *ledgerBufferGCS) worker() { + for { + select { + case <-lb.done: + log.Error("abort: getFromLedgerQueue blocked") + return + case <-lb.context.Done(): + log.Error(lb.context.Err()) + return + case sequence := <-lb.taskQueue: + retryCount := uint32(0) + for retryCount <= lb.config.BufferConfig.RetryLimit { + ledgerObject, err := lb.getLedgerGCSObject(sequence) + if err != nil { + if e, ok := err.(*googleapi.Error); ok { + // ledgerObject not found and unbounded + if e.Code == 404 && !lb.ledgerRange.bounded { + time.Sleep(lb.config.BufferConfig.RetryWait * time.Second) + continue + } + } + if retryCount == lb.config.BufferConfig.RetryLimit { + err = errors.New("maximum retries exceeded for gcs object reads") + lb.cancel(err) + } + retryCount++ + time.Sleep(lb.config.BufferConfig.RetryWait * time.Second) + } + + // Add to priority queue and continue to next task + lb.storeObject(ledgerObject, sequence) + break + } + } + } +} + +func (lb *ledgerBufferGCS) getLedgerGCSObject(sequence uint32) ([]byte, error) { + objectKey := lb.config.LedgerBatchConfig.GetObjectKeyFromSequenceNumber(sequence) + + reader, err := lb.dataStore.GetFile(context.Background(), objectKey) + if err != nil { + return nil, errors.Wrapf(err, "failed getting file: %s", objectKey) + } + + defer reader.Close() + + objectBytes, err := io.ReadAll(reader) + if err != nil { + return nil, errors.Wrapf(err, "failed reading file: %s", objectKey) + } + + return objectBytes, nil +} + +func (lb *ledgerBufferGCS) storeObject(ledgerObject []byte, sequence uint32) { + lb.priorityQueueLock.Lock() + defer lb.priorityQueueLock.Unlock() + + lb.ledgerPriorityQueue.Push(ledgerBatchObject{ + payload: ledgerObject, + startLedger: int(sequence), + }) + + // Check if the nextLedger is the next item in the priority queue + for lb.ledgerPriorityQueue.Len() > 0 && lb.currentLedger == uint32(lb.ledgerPriorityQueue.Peek().startLedger) { + item := lb.ledgerPriorityQueue.Pop() + lb.ledgerQueue <- item.payload + lb.currentLedger += lb.config.LedgerBatchConfig.LedgersPerFile + } +} + +func (lb *ledgerBufferGCS) getFromLedgerQueue() ([]byte, error) { + for { + select { + case <-lb.context.Done(): + log.Info("Stopping getFromLedgerQueue due to context cancellation") + close(lb.done) + return nil, lb.context.Err() + case compressedBinary := <-lb.ledgerQueue: + // Add next task to the TaskQueue + lb.pushTaskQueue() + + reader := bytes.NewReader(compressedBinary) + lcmBinary, err := lb.decoder.Unzip(reader) + if err != nil { + return nil, err + } + + return lcmBinary, nil + } + } +} diff --git a/support/datastore/ledger_meta_archive.go b/support/datastore/ledger_meta_archive.go index 356b4754de..7942a8bdab 100644 --- a/support/datastore/ledger_meta_archive.go +++ b/support/datastore/ledger_meta_archive.go @@ -64,17 +64,17 @@ func (f *LedgerMetaArchive) GetObjectKey() string { return f.ObjectKey } -func (f *LedgerMetaArchive) GetLedger(sequence uint32) (*xdr.LedgerCloseMeta, error) { +func (f *LedgerMetaArchive) GetLedger(sequence uint32) (xdr.LedgerCloseMeta, error) { if sequence < uint32(f.Data.StartSequence) || sequence > uint32(f.Data.EndSequence) { - return nil, fmt.Errorf("ledger sequence %d is outside valid range [%d, %d]", + return xdr.LedgerCloseMeta{}, fmt.Errorf("ledger sequence %d is outside valid range [%d, %d]", sequence, f.Data.StartSequence, f.Data.EndSequence) } ledgerIndex := sequence - f.GetStartLedgerSequence() if ledgerIndex >= uint32(len(f.Data.LedgerCloseMetas)) { - return nil, fmt.Errorf("LedgerCloseMeta for sequence %d not found", sequence) + return xdr.LedgerCloseMeta{}, fmt.Errorf("LedgerCloseMeta for sequence %d not found", sequence) } - return &f.Data.LedgerCloseMetas[ledgerIndex], nil + return f.Data.LedgerCloseMetas[ledgerIndex], nil } func CreateLedgerCloseMeta(ledgerSeq uint32) xdr.LedgerCloseMeta { From 41844d233a0d9aea6b96675dad275363798d1475 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 3 May 2024 17:34:20 -0400 Subject: [PATCH 142/234] Address comments --- ingest/ledgerbackend/cloud_storage_backend.go | 295 +++++++++++++++ ..._test.go => cloud_storage_backend_test.go} | 150 ++++---- ingest/ledgerbackend/gcs_backend.go | 335 ------------------ ...{gcs_ledger_buffer.go => ledger_buffer.go} | 79 +++-- 4 files changed, 411 insertions(+), 448 deletions(-) create mode 100644 ingest/ledgerbackend/cloud_storage_backend.go rename ingest/ledgerbackend/{gcs_backend_test.go => cloud_storage_backend_test.go} (66%) delete mode 100644 ingest/ledgerbackend/gcs_backend.go rename ingest/ledgerbackend/{gcs_ledger_buffer.go => ledger_buffer.go} (64%) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go new file mode 100644 index 0000000000..e0a61146c5 --- /dev/null +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -0,0 +1,295 @@ +package ledgerbackend + +import ( + "context" + "sync" + "time" + + "github.com/pkg/errors" + + "github.com/stellar/go/support/compressxdr" + "github.com/stellar/go/support/datastore" + "github.com/stellar/go/xdr" +) + +// Ensure CloudStorageBackend implements LedgerBackend +var _ LedgerBackend = (*CloudStorageBackend)(nil) + +type ledgerBatchObject struct { + payload []byte + startLedger int // Ledger sequence used as the priority for the priorityqueue. +} + +type BufferConfig struct { + BufferSize uint32 + NumWorkers uint32 + RetryLimit uint32 + RetryWait time.Duration +} + +type CloudStorageBackendConfig struct { + BufferConfig BufferConfig + LedgerBatchConfig datastore.LedgerBatchConfig + CompressionType string + DataStore datastore.DataStore + ResumableManager datastore.ResumableManager +} + +// CloudStorageBackend is a ledger backend that reads from a cloud storage service. +// The cloud storage service contains files generated from the ledgerExporter. +type CloudStorageBackend struct { + config CloudStorageBackendConfig + + context context.Context + // cancel is the CancelCauseFunc for context which controls the lifetime of a CloudStorageBackend instance. + // Once it is invoked CloudStorageBackend will not be able to stream ledgers from CloudStorageBackend. + cancel context.CancelCauseFunc + csBackendLock sync.RWMutex + + // ledgerBuffer is the buffer for LedgerCloseMeta data read in parallel. + ledgerBuffer *ledgerBuffer + + dataStore datastore.DataStore + resumableManager datastore.ResumableManager + prepared *Range // non-nil if any range is prepared + closed bool // False until the core is closed + ledgerMetaArchive *datastore.LedgerMetaArchive + decoder compressxdr.XDRDecoder + nextLedger uint32 +} + +// Return a new CloudStorageBackend instance. +func NewCloudStorageBackend(ctx context.Context, config CloudStorageBackendConfig) (*CloudStorageBackend, error) { + ctx, cancel := context.WithCancelCause(ctx) + + if config.DataStore == nil { + return nil, errors.New("no DataStore provided") + } + + if config.ResumableManager == nil { + return nil, errors.New("no ResumableManager provided") + } + + ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) + decoder, err := compressxdr.NewXDRDecoder(config.CompressionType, nil) + if err != nil { + return nil, err + } + + csBackend := &CloudStorageBackend{ + config: config, + context: ctx, + cancel: cancel, + dataStore: config.DataStore, + resumableManager: config.ResumableManager, + ledgerMetaArchive: ledgerMetaArchive, + decoder: decoder, + } + + return csBackend, nil +} + +// GetLatestLedgerSequence returns the most recent ledger sequence number in the cloud storage bucket. +func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { + if csb.closed { + return 0, errors.New("CloudStorageBackend is closed; cannot GetLatestLedgerSequence") + } + + if csb.prepared == nil { + return 0, errors.New("CloudStorageBackend must be prepared, call PrepareRange first") + } + + if csb.ledgerBuffer.nextTaskLedger == csb.ledgerBuffer.ledgerRange.from { + return 0, nil + } + + // Subtract 1 to get the latest ledger in buffer + return csb.ledgerBuffer.nextTaskLedger - 1, nil +} + +// getSequenceInBatch checks if the requested sequence is in the cached batch. +// Otherwise will continuously load in the next LedgerCloseMetaBatch until found. +func (csb *CloudStorageBackend) getSequenceInBatch(sequence uint32) error { + for { + // Sequence inside the current cached LedgerCloseMetaBatch + if sequence >= csb.ledgerMetaArchive.GetStartLedgerSequence() && sequence <= csb.ledgerMetaArchive.GetEndLedgerSequence() { + return nil + } + + // Sequence is before the current LedgerCloseMetaBatch + // Does not support retrieving LedgerCloseMeta before the current cached batch + if sequence < csb.ledgerMetaArchive.GetStartLedgerSequence() { + return errors.New("requested sequence preceeds current LedgerCloseMetaBatch") + } + + // Sequence is beyond the current LedgerCloseMetaBatch + lcmBatchBinary, err := csb.ledgerBuffer.getFromLedgerQueue() + if err != nil { + return errors.Wrap(err, "failed getting next ledger batch from queue") + } + + // Turn binary into xdr + err = csb.ledgerMetaArchive.Data.UnmarshalBinary(lcmBatchBinary) + if err != nil { + return errors.Wrap(err, "failed unmarshalling lcmBatchBinary") + } + } +} + +// nextExpectedSequence returns nextLedger (if currently set) or start of +// prepared range. Otherwise it returns 0. +// This is done because `nextLedger` is 0 between the moment Stellar-Core is +// started and streaming the first ledger (in such case we return first ledger +// in requested range). +func (csb *CloudStorageBackend) nextExpectedSequence() uint32 { + if csb.nextLedger == 0 && csb.prepared != nil { + return csb.prepared.from + } + return csb.nextLedger +} + +// GetLedger returns the LedgerCloseMeta for the specified ledger sequence number +func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { + csb.csBackendLock.RLock() + defer csb.csBackendLock.RUnlock() + + if csb.closed { + return xdr.LedgerCloseMeta{}, errors.New("CloudStorageBackend is closed; cannot GetLedger") + } + + if csb.prepared == nil { + return xdr.LedgerCloseMeta{}, errors.New("session is not prepared, call PrepareRange first") + } + + if sequence < csb.ledgerBuffer.ledgerRange.from { + return xdr.LedgerCloseMeta{}, errors.New("requested sequence preceeds current LedgerRange") + } + + if csb.ledgerBuffer.ledgerRange.bounded { + if sequence > csb.ledgerBuffer.ledgerRange.to { + return xdr.LedgerCloseMeta{}, errors.New("requested sequence beyond current LedgerRange") + } + } + + if csb.nextExpectedSequence() != sequence { + return xdr.LedgerCloseMeta{}, errors.New("requested sequence is not the next available ledger") + } + + err := csb.getSequenceInBatch(sequence) + if err != nil { + return xdr.LedgerCloseMeta{}, err + } + + ledgerCloseMeta, err := csb.ledgerMetaArchive.GetLedger(sequence) + if err != nil { + return xdr.LedgerCloseMeta{}, err + } + csb.nextLedger++ + + return ledgerCloseMeta, nil +} + +// PrepareRange checks if the starting and ending (if bounded) ledgers exist. +func (csb *CloudStorageBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { + if csb.closed { + return errors.New("CloudStorageBackend is closed; cannot PrepareRange") + } + + if alreadyPrepared, err := csb.startPreparingRange(ledgerRange); err != nil { + return errors.Wrap(err, "error starting prepare range") + } else if alreadyPrepared { + return nil + } + + csb.prepared = &ledgerRange + + return nil +} + +// IsPrepared returns true if a given ledgerRange is prepared. +func (csb *CloudStorageBackend) IsPrepared(ctx context.Context, ledgerRange Range) (bool, error) { + csb.csBackendLock.RLock() + defer csb.csBackendLock.RUnlock() + + if csb.closed { + return false, errors.New("CloudStorageBackend is closed; cannot IsPrepared") + } + + return csb.isPrepared(ledgerRange), nil +} + +func (csb *CloudStorageBackend) isPrepared(ledgerRange Range) bool { + if csb.closed { + return false + } + + if csb.prepared == nil { + return false + } + + if csb.ledgerBuffer.ledgerRange.from > ledgerRange.from { + return false + } + + if csb.ledgerBuffer.ledgerRange.bounded && !ledgerRange.bounded { + return false + } + + if !csb.ledgerBuffer.ledgerRange.bounded && !ledgerRange.bounded { + return true + } + + if !csb.ledgerBuffer.ledgerRange.bounded && ledgerRange.bounded { + return true + } + + if csb.ledgerBuffer.ledgerRange.to >= ledgerRange.to { + return true + } + + return false +} + +// Close closes existing CloudStorageBackend processes. +// Note, once a CloudStorageBackend instance is closed it can no longer be used and +// all subsequent calls to PrepareRange(), GetLedger(), etc will fail. +// Close is thread-safe and can be called from another go routine. +func (csb *CloudStorageBackend) Close() error { + csb.csBackendLock.RLock() + defer csb.csBackendLock.RUnlock() + + csb.closed = true + + // after the CloudStorageBackend context is Done all subsequent calls to PrepareRange() will fail + csb.context.Done() + + return nil +} + +// startPreparingRange prepares the ledger range by setting the range in the ledgerBuffer +func (csb *CloudStorageBackend) startPreparingRange(ledgerRange Range) (bool, error) { + csb.csBackendLock.Lock() + defer csb.csBackendLock.Unlock() + + if csb.isPrepared(ledgerRange) { + return true, nil + } + + var err error + csb.ledgerBuffer, err = csb.newLedgerBuffer(ledgerRange) + if err != nil { + return false, err + } + + // Start the ledgerBuffer + for i := 0; i <= int(csb.config.BufferConfig.BufferSize); i++ { + if csb.ledgerBuffer.nextTaskLedger > ledgerRange.to && ledgerRange.bounded { + break + } + csb.ledgerBuffer.pushTaskQueue() + } + + csb.nextLedger = ledgerRange.from + + return false, nil +} diff --git a/ingest/ledgerbackend/gcs_backend_test.go b/ingest/ledgerbackend/cloud_storage_backend_test.go similarity index 66% rename from ingest/ledgerbackend/gcs_backend_test.go rename to ingest/ledgerbackend/cloud_storage_backend_test.go index 9c09ca38c9..f597c71a17 100644 --- a/ingest/ledgerbackend/gcs_backend_test.go +++ b/ingest/ledgerbackend/cloud_storage_backend_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" ) -func createGCSBackendConfigForTesting() GCSBackendConfig { +func createCloudStorageBackendConfigForTesting() CloudStorageBackendConfig { bufferConfig := BufferConfig{ BufferSize: 100, NumWorkers: 5, @@ -34,7 +34,7 @@ func createGCSBackendConfigForTesting() GCSBackendConfig { resumableManager := new(datastore.MockResumableManager) - return GCSBackendConfig{ + return CloudStorageBackendConfig{ BufferConfig: bufferConfig, LedgerBatchConfig: ledgerBatchConfig, CompressionType: compressxdr.GZIP, @@ -43,13 +43,13 @@ func createGCSBackendConfigForTesting() GCSBackendConfig { } } -func createGCSBackendForTesting() GCSBackend { - config := createGCSBackendConfigForTesting() +func createCloudStorageBackendForTesting() CloudStorageBackend { + config := createCloudStorageBackendConfigForTesting() ctx := context.Background() ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) decoder, _ := compressxdr.NewXDRDecoder(config.CompressionType, nil) - return GCSBackend{ + return CloudStorageBackend{ config: config, context: ctx, dataStore: config.DataStore, @@ -59,31 +59,29 @@ func createGCSBackendForTesting() GCSBackend { } } -func TestNewGCSBackend(t *testing.T) { +func TestNewCloudStorageBackend(t *testing.T) { ctx := context.Background() - config := createGCSBackendConfigForTesting() - config.LedgerBatchConfig = datastore.LedgerBatchConfig{} - config.BufferConfig = BufferConfig{} + config := createCloudStorageBackendConfigForTesting() - gcsb, err := NewGCSBackend(ctx, config) + csb, err := NewCloudStorageBackend(ctx, config) assert.NoError(t, err) - assert.Equal(t, gcsb.dataStore, config.DataStore) - assert.Equal(t, gcsb.resumableManager, config.ResumableManager) - assert.Equal(t, ".xdr.gz", gcsb.config.LedgerBatchConfig.FileSuffix) - assert.Equal(t, uint32(1), gcsb.config.LedgerBatchConfig.LedgersPerFile) - assert.Equal(t, uint32(64000), gcsb.config.LedgerBatchConfig.FilesPerPartition) - assert.Equal(t, uint32(1000), gcsb.config.BufferConfig.BufferSize) - assert.Equal(t, uint32(5), gcsb.config.BufferConfig.NumWorkers) - assert.Equal(t, uint32(3), gcsb.config.BufferConfig.RetryLimit) - assert.Equal(t, time.Duration(5), gcsb.config.BufferConfig.RetryWait) + assert.Equal(t, csb.dataStore, config.DataStore) + assert.Equal(t, csb.resumableManager, config.ResumableManager) + assert.Equal(t, ".xdr.gz", csb.config.LedgerBatchConfig.FileSuffix) + assert.Equal(t, uint32(1), csb.config.LedgerBatchConfig.LedgersPerFile) + assert.Equal(t, uint32(64000), csb.config.LedgerBatchConfig.FilesPerPartition) + assert.Equal(t, uint32(100), csb.config.BufferConfig.BufferSize) + assert.Equal(t, uint32(5), csb.config.BufferConfig.NumWorkers) + assert.Equal(t, uint32(3), csb.config.BufferConfig.RetryLimit) + assert.Equal(t, time.Duration(1), csb.config.BufferConfig.RetryWait) } func TestGCSNewLedgerBuffer(t *testing.T) { - gcsb := createGCSBackendForTesting() + csb := createCloudStorageBackendForTesting() ledgerRange := BoundedRange(2, 3) - ledgerBuffer, err := gcsb.newLedgerBuffer(ledgerRange) + ledgerBuffer, err := csb.newLedgerBuffer(ledgerRange) assert.NoError(t, err) assert.Equal(t, uint32(2), ledgerBuffer.currentLedger) @@ -91,15 +89,15 @@ func TestGCSNewLedgerBuffer(t *testing.T) { assert.Equal(t, ledgerRange, ledgerBuffer.ledgerRange) } -func TestGCSGetLatestLedgerSequence(t *testing.T) { +func TestCloudStorageGetLatestLedgerSequence(t *testing.T) { ctx := context.Background() - gcsb := createGCSBackendForTesting() + csb := createCloudStorageBackendForTesting() resumableManager := new(datastore.MockResumableManager) - gcsb.resumableManager = resumableManager + csb.resumableManager = resumableManager resumableManager.On("FindStart", ctx, uint32(2), uint32(0)).Return(uint32(6), true, nil) - seq, err := gcsb.GetLatestLedgerSequence(ctx) + seq, err := csb.GetLatestLedgerSequence(ctx) assert.NoError(t, err) assert.Equal(t, uint32(5), seq) @@ -136,11 +134,11 @@ func createLCMBatchReader(start, end uint32, count int) io.ReadCloser { return io.NopCloser(reader1) } -func TestGCSGetLedger_SingleLedgerPerFile(t *testing.T) { +func TestCloudStorageGetLedger_SingleLedgerPerFile(t *testing.T) { startLedger := uint32(3) endLedger := uint32(5) lcmArray := createLCMForTesting(startLedger, endLedger) - gcsb := createGCSBackendForTesting() + csb := createCloudStorageBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(startLedger, endLedger) @@ -149,50 +147,50 @@ func TestGCSGetLedger_SingleLedgerPerFile(t *testing.T) { readCloser3 := createLCMBatchReader(uint32(5), uint32(5), 1) mockDataStore := new(datastore.MockDataStore) - gcsb.dataStore = mockDataStore + csb.dataStore = mockDataStore mockDataStore.On("GetFile", ctx, "0-63999/3.xdr.gz").Return(readCloser1, nil) mockDataStore.On("GetFile", ctx, "0-63999/4.xdr.gz").Return(readCloser2, nil) mockDataStore.On("GetFile", ctx, "0-63999/5.xdr.gz").Return(readCloser3, nil) - gcsb.PrepareRange(ctx, ledgerRange) + csb.PrepareRange(ctx, ledgerRange) - lcm, err := gcsb.GetLedger(ctx, uint32(3)) + lcm, err := csb.GetLedger(ctx, uint32(3)) assert.NoError(t, err) assert.Equal(t, lcmArray[0], lcm) // Skip sequence 4; Test non consecutive GetLedger - lcm, err = gcsb.GetLedger(ctx, uint32(5)) + lcm, err = csb.GetLedger(ctx, uint32(5)) assert.NoError(t, err) assert.Equal(t, lcmArray[2], lcm) } -func TestGCSGetLedger_MultipleLedgerPerFile(t *testing.T) { +func TestCloudStorageGetLedger_MultipleLedgerPerFile(t *testing.T) { startLedger := uint32(2) endLedger := uint32(5) lcmArray := createLCMForTesting(startLedger, endLedger) - gcsb := createGCSBackendForTesting() + csb := createCloudStorageBackendForTesting() ctx := context.Background() - gcsb.config.LedgerBatchConfig.LedgersPerFile = uint32(2) + csb.config.LedgerBatchConfig.LedgersPerFile = uint32(2) ledgerRange := BoundedRange(startLedger, endLedger) readCloser1 := createLCMBatchReader(uint32(2), uint32(3), 2) readCloser2 := createLCMBatchReader(uint32(4), uint32(5), 2) mockDataStore := new(datastore.MockDataStore) - gcsb.dataStore = mockDataStore + csb.dataStore = mockDataStore mockDataStore.On("GetFile", ctx, "0-127999/2-3.xdr.gz").Return(readCloser1, nil) mockDataStore.On("GetFile", ctx, "0-127999/4-5.xdr.gz").Return(readCloser2, nil) - gcsb.PrepareRange(ctx, ledgerRange) + csb.PrepareRange(ctx, ledgerRange) - lcm, err := gcsb.GetLedger(ctx, uint32(2)) + lcm, err := csb.GetLedger(ctx, uint32(2)) assert.NoError(t, err) assert.Equal(t, lcmArray[0], lcm) - lcm, err = gcsb.GetLedger(ctx, uint32(3)) + lcm, err = csb.GetLedger(ctx, uint32(3)) assert.NoError(t, err) assert.Equal(t, lcmArray[1], lcm) - lcm, err = gcsb.GetLedger(ctx, uint32(4)) + lcm, err = csb.GetLedger(ctx, uint32(4)) assert.NoError(t, err) assert.Equal(t, lcmArray[2], lcm) } @@ -201,7 +199,7 @@ func TestGCSGetLedger_ErrorPreceedingLedger(t *testing.T) { startLedger := uint32(3) endLedger := uint32(5) lcmArray := createLCMForTesting(startLedger, endLedger) - gcsb := createGCSBackendForTesting() + csb := createCloudStorageBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(startLedger, endLedger) @@ -210,127 +208,127 @@ func TestGCSGetLedger_ErrorPreceedingLedger(t *testing.T) { readCloser3 := createLCMBatchReader(uint32(5), uint32(5), 1) mockDataStore := new(datastore.MockDataStore) - gcsb.dataStore = mockDataStore + csb.dataStore = mockDataStore mockDataStore.On("GetFile", ctx, "0-63999/3.xdr.gz").Return(readCloser1, nil) mockDataStore.On("GetFile", ctx, "0-63999/4.xdr.gz").Return(readCloser2, nil) mockDataStore.On("GetFile", ctx, "0-63999/5.xdr.gz").Return(readCloser3, nil) - gcsb.PrepareRange(ctx, ledgerRange) + csb.PrepareRange(ctx, ledgerRange) - lcm, err := gcsb.GetLedger(ctx, uint32(5)) + lcm, err := csb.GetLedger(ctx, uint32(5)) assert.NoError(t, err) assert.Equal(t, lcmArray[2], lcm) - _, err = gcsb.GetLedger(ctx, uint32(4)) + _, err = csb.GetLedger(ctx, uint32(4)) assert.Error(t, err, "requested sequence preceeds current LedgerCloseMetaBatch") } func TestGCSGetLedger_NotPrepared(t *testing.T) { - gcsb := createGCSBackendForTesting() + csb := createCloudStorageBackendForTesting() ctx := context.Background() - _, err := gcsb.GetLedger(ctx, uint32(3)) + _, err := csb.GetLedger(ctx, uint32(3)) assert.Error(t, err, "session is not prepared, call PrepareRange first") } func TestGCSGetLedger_SequenceNotInBatch(t *testing.T) { - gcsb := createGCSBackendForTesting() + csb := createCloudStorageBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(3, 5) - gcsb.PrepareRange(ctx, ledgerRange) + csb.PrepareRange(ctx, ledgerRange) - _, err := gcsb.GetLedger(ctx, uint32(2)) + _, err := csb.GetLedger(ctx, uint32(2)) assert.Error(t, err, "requested sequence preceeds current LedgerRange") - _, err = gcsb.GetLedger(ctx, uint32(6)) + _, err = csb.GetLedger(ctx, uint32(6)) assert.Error(t, err, "requested sequence beyond current LedgerRange") } func TestGCSPrepareRange(t *testing.T) { - gcsb := createGCSBackendForTesting() + csb := createCloudStorageBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(2, 3) - err := gcsb.PrepareRange(ctx, ledgerRange) + err := csb.PrepareRange(ctx, ledgerRange) assert.NoError(t, err) - assert.NotNil(t, gcsb.prepared) + assert.NotNil(t, csb.prepared) // check alreadyPrepared - err = gcsb.PrepareRange(ctx, ledgerRange) + err = csb.PrepareRange(ctx, ledgerRange) assert.NoError(t, err) - assert.NotNil(t, gcsb.prepared) + assert.NotNil(t, csb.prepared) } func TestGCSIsPrepared_Bounded(t *testing.T) { - gcsb := createGCSBackendForTesting() + csb := createCloudStorageBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(3, 4) - gcsb.PrepareRange(ctx, ledgerRange) + csb.PrepareRange(ctx, ledgerRange) - ok, err := gcsb.IsPrepared(ctx, ledgerRange) + ok, err := csb.IsPrepared(ctx, ledgerRange) assert.NoError(t, err) assert.True(t, ok) - ok, err = gcsb.IsPrepared(ctx, BoundedRange(2, 4)) + ok, err = csb.IsPrepared(ctx, BoundedRange(2, 4)) assert.NoError(t, err) assert.False(t, ok) - ok, err = gcsb.IsPrepared(ctx, UnboundedRange(3)) + ok, err = csb.IsPrepared(ctx, UnboundedRange(3)) assert.NoError(t, err) assert.False(t, ok) - ok, err = gcsb.IsPrepared(ctx, UnboundedRange(2)) + ok, err = csb.IsPrepared(ctx, UnboundedRange(2)) assert.NoError(t, err) assert.False(t, ok) } func TestGCSIsPrepared_Unbounded(t *testing.T) { - gcsb := createGCSBackendForTesting() + csb := createCloudStorageBackendForTesting() ctx := context.Background() ledgerRange := UnboundedRange(3) - gcsb.PrepareRange(ctx, ledgerRange) + csb.PrepareRange(ctx, ledgerRange) - ok, err := gcsb.IsPrepared(ctx, ledgerRange) + ok, err := csb.IsPrepared(ctx, ledgerRange) assert.NoError(t, err) assert.True(t, ok) - ok, err = gcsb.IsPrepared(ctx, BoundedRange(3, 4)) + ok, err = csb.IsPrepared(ctx, BoundedRange(3, 4)) assert.NoError(t, err) assert.True(t, ok) - ok, err = gcsb.IsPrepared(ctx, BoundedRange(2, 4)) + ok, err = csb.IsPrepared(ctx, BoundedRange(2, 4)) assert.NoError(t, err) assert.False(t, ok) - ok, err = gcsb.IsPrepared(ctx, UnboundedRange(4)) + ok, err = csb.IsPrepared(ctx, UnboundedRange(4)) assert.NoError(t, err) assert.True(t, ok) - ok, err = gcsb.IsPrepared(ctx, UnboundedRange(2)) + ok, err = csb.IsPrepared(ctx, UnboundedRange(2)) assert.NoError(t, err) assert.False(t, ok) } func TestGCSClose(t *testing.T) { - gcsb := createGCSBackendForTesting() + csb := createCloudStorageBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(3, 5) - gcsb.PrepareRange(ctx, ledgerRange) + csb.PrepareRange(ctx, ledgerRange) - err := gcsb.Close() + err := csb.Close() assert.NoError(t, err) - assert.Equal(t, true, gcsb.closed) + assert.Equal(t, true, csb.closed) - _, err = gcsb.GetLatestLedgerSequence(ctx) + _, err = csb.GetLatestLedgerSequence(ctx) assert.Error(t, err, "gcsBackend is closed; cannot GetLatestLedgerSequence") - _, err = gcsb.GetLedger(ctx, 3) + _, err = csb.GetLedger(ctx, 3) assert.Error(t, err, "gcsBackend is closed; cannot GetLedger") - err = gcsb.PrepareRange(ctx, ledgerRange) + err = csb.PrepareRange(ctx, ledgerRange) assert.Error(t, err, "gcsBackend is closed; cannot PrepareRange") - _, err = gcsb.IsPrepared(ctx, ledgerRange) + _, err = csb.IsPrepared(ctx, ledgerRange) assert.Error(t, err, "gcsBackend is closed; cannot IsPrepared") } diff --git a/ingest/ledgerbackend/gcs_backend.go b/ingest/ledgerbackend/gcs_backend.go deleted file mode 100644 index 656fbce7fc..0000000000 --- a/ingest/ledgerbackend/gcs_backend.go +++ /dev/null @@ -1,335 +0,0 @@ -package ledgerbackend - -import ( - "context" - "sync" - "time" - - "github.com/pkg/errors" - - "github.com/stellar/go/support/compressxdr" - "github.com/stellar/go/support/datastore" - "github.com/stellar/go/xdr" -) - -// Ensure GCSBackend implements LedgerBackend -var _ LedgerBackend = (*GCSBackend)(nil) - -// default config values -var defaultFileSuffix = ".xdr.gz" -var defaultLedgersPerFile = uint32(1) -var defaultFilesPerPartition = uint32(64000) -var defaultBufferSize = uint32(1000) -var defaultNumWorkers = uint32(5) -var defaultRetryLimit = uint32(3) -var defaultRetryWait = time.Duration(5) - -type ledgerBatchObject struct { - payload []byte - startLedger int // Ledger sequence used as the priority for the priorityqueue. -} - -type BufferConfig struct { - BufferSize uint32 - NumWorkers uint32 - RetryLimit uint32 - RetryWait time.Duration -} - -type GCSBackendConfig struct { - BufferConfig BufferConfig - LedgerBatchConfig datastore.LedgerBatchConfig - CompressionType string - DataStore datastore.DataStore - ResumableManager datastore.ResumableManager -} - -// GCSBackend is a ledger backend that reads from a cloud storage service. -// The cloud storage service contains files generated from the ledgerExporter. -type GCSBackend struct { - config GCSBackendConfig - - context context.Context - // cancel is the CancelCauseFunc for context which controls the lifetime of a GCSBackend instance. - // Once it is invoked GCSBackend will not be able to stream ledgers from GCSBackend. - cancel context.CancelCauseFunc - - // gcsBackendLock protects access to gcsBackendRunner. When the read lock - // is acquired gcsBackendRunner can be accessed. When the write lock is acquired - // gcsBackendRunner can be updated. - gcsBackendLock sync.RWMutex - - // ledgerBuffer is the buffer for LedgerCloseMeta data read in parallel. - ledgerBuffer *ledgerBufferGCS - - dataStore datastore.DataStore - resumableManager datastore.ResumableManager - prepared *Range // non-nil if any range is prepared - closed bool // False until the core is closed - ledgerMetaArchive *datastore.LedgerMetaArchive - decoder compressxdr.XDRDecoder - nextLedger uint32 -} - -// Return a new GCSBackend instance. -func NewGCSBackend(ctx context.Context, config GCSBackendConfig) (*GCSBackend, error) { - // Check/set minimum config values for LedgerBatchConfig - if config.LedgerBatchConfig.FileSuffix == "" { - config.LedgerBatchConfig.FileSuffix = defaultFileSuffix - } - - if config.LedgerBatchConfig.LedgersPerFile == 0 { - config.LedgerBatchConfig.LedgersPerFile = defaultLedgersPerFile - } - - if config.LedgerBatchConfig.FilesPerPartition == 0 { - config.LedgerBatchConfig.FilesPerPartition = defaultFilesPerPartition - } - - // Check/set minimum config values for BufferConfig - if config.BufferConfig.BufferSize == 0 { - config.BufferConfig.BufferSize = defaultBufferSize - } - - if config.BufferConfig.NumWorkers == 0 { - config.BufferConfig.NumWorkers = defaultNumWorkers - } - - if config.BufferConfig.RetryLimit == 0 { - config.BufferConfig.RetryLimit = defaultRetryLimit - } - - if config.BufferConfig.RetryWait == 0 { - config.BufferConfig.RetryWait = defaultRetryWait - } - - ctx, cancel := context.WithCancelCause(ctx) - - if config.DataStore == nil { - return nil, errors.New("no DataStore provided") - } - - if config.ResumableManager == nil { - return nil, errors.New("no ResumableManager provided") - } - - ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) - decoder, err := compressxdr.NewXDRDecoder(config.CompressionType, nil) - if err != nil { - return nil, err - } - - gcsBackend := &GCSBackend{ - config: config, - context: ctx, - cancel: cancel, - dataStore: config.DataStore, - resumableManager: config.ResumableManager, - ledgerMetaArchive: ledgerMetaArchive, - decoder: decoder, - } - - return gcsBackend, nil -} - -// GetLatestLedgerSequence returns the most recent ledger sequence number in the cloud storage bucket. -func (gcsb *GCSBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - var err error - - if gcsb.closed { - return 0, errors.New("gcsBackend is closed; cannot GetLatestLedgerSequence") - } - - // Start at 2 to skip the genesis ledger - absentLedger, ok, err := gcsb.resumableManager.FindStart(ctx, uint32(2), uint32(0)) - if err != nil { - return 0, err - } - if !ok { - return 0, errors.New("findStart returned sequence beyond latest history archive ledger") - } - - // Subtract one to get the oldest existing ledger seq closest to genesis - return absentLedger - 1, nil -} - -// getSequenceInBatch checks if the requested sequence is in the cached batch. -// Otherwise will continuously load in the next LedgerCloseMetaBatch until found. -func (gcsb *GCSBackend) getSequenceInBatch(sequence uint32) error { - for { - // Sequence inside the current cached LedgerCloseMetaBatch - if sequence >= gcsb.ledgerMetaArchive.GetStartLedgerSequence() && sequence <= gcsb.ledgerMetaArchive.GetEndLedgerSequence() { - return nil - } - - // Sequence is before the current LedgerCloseMetaBatch - // Does not support retrieving LedgerCloseMeta before the current cached batch - if sequence < gcsb.ledgerMetaArchive.GetStartLedgerSequence() { - return errors.New("requested sequence preceeds current LedgerCloseMetaBatch") - } - - // Sequence is beyond the current LedgerCloseMetaBatch - lcmBatchBinary, err := gcsb.ledgerBuffer.getFromLedgerQueue() - if err != nil { - return errors.Wrap(err, "failed getting next ledger batch from queue") - } - - // Turn binary into xdr - err = gcsb.ledgerMetaArchive.Data.UnmarshalBinary(lcmBatchBinary) - if err != nil { - return errors.Wrap(err, "failed unmarshalling lcmBatchBinary") - } - } -} - -// nextExpectedSequence returns nextLedger (if currently set) or start of -// prepared range. Otherwise it returns 0. -// This is done because `nextLedger` is 0 between the moment Stellar-Core is -// started and streaming the first ledger (in such case we return first ledger -// in requested range). -func (gcsb *GCSBackend) nextExpectedSequence() uint32 { - if gcsb.nextLedger == 0 && gcsb.prepared != nil { - return gcsb.prepared.from - } - return gcsb.nextLedger -} - -// GetLedger returns the LedgerCloseMeta for the specified ledger sequence number -func (gcsb *GCSBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { - gcsb.gcsBackendLock.RLock() - defer gcsb.gcsBackendLock.RUnlock() - - if gcsb.closed { - return xdr.LedgerCloseMeta{}, errors.New("gcsBackend is closed; cannot GetLedger") - } - - if gcsb.prepared == nil { - return xdr.LedgerCloseMeta{}, errors.New("session is not prepared, call PrepareRange first") - } - - if sequence < gcsb.ledgerBuffer.ledgerRange.from { - return xdr.LedgerCloseMeta{}, errors.New("requested sequence preceeds current LedgerRange") - } - - if gcsb.ledgerBuffer.ledgerRange.bounded { - if sequence > gcsb.ledgerBuffer.ledgerRange.to { - return xdr.LedgerCloseMeta{}, errors.New("requested sequence beyond current LedgerRange") - } - } - - if gcsb.nextExpectedSequence() != sequence { - return xdr.LedgerCloseMeta{}, errors.New("requested sequence is not the next available ledger") - } - - err := gcsb.getSequenceInBatch(sequence) - if err != nil { - return xdr.LedgerCloseMeta{}, err - } - - ledgerCloseMeta, err := gcsb.ledgerMetaArchive.GetLedger(sequence) - if err != nil { - return xdr.LedgerCloseMeta{}, err - } - gcsb.nextLedger++ - - return ledgerCloseMeta, nil -} - -// PrepareRange checks if the starting and ending (if bounded) ledgers exist. -func (gcsb *GCSBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { - if gcsb.closed { - return errors.New("gcsBackend is closed; cannot PrepareRange") - } - - if alreadyPrepared, err := gcsb.startPreparingRange(ledgerRange); err != nil { - return errors.Wrap(err, "error starting prepare range") - } else if alreadyPrepared { - return nil - } - - gcsb.prepared = &ledgerRange - - return nil -} - -// IsPrepared returns true if a given ledgerRange is prepared. -func (gcsb *GCSBackend) IsPrepared(ctx context.Context, ledgerRange Range) (bool, error) { - gcsb.gcsBackendLock.RLock() - defer gcsb.gcsBackendLock.RUnlock() - - if gcsb.closed { - return false, errors.New("gcsBackend is closed; cannot IsPrepared") - } - - return gcsb.isPrepared(ledgerRange), nil -} - -func (gcsb *GCSBackend) isPrepared(ledgerRange Range) bool { - if gcsb.closed { - return false - } - - if gcsb.prepared == nil { - return false - } - - if gcsb.ledgerBuffer.ledgerRange.from > ledgerRange.from { - return false - } - - if gcsb.ledgerBuffer.ledgerRange.bounded && !ledgerRange.bounded { - return false - } - - if !gcsb.ledgerBuffer.ledgerRange.bounded && !ledgerRange.bounded { - return true - } - - if !gcsb.ledgerBuffer.ledgerRange.bounded && ledgerRange.bounded { - return true - } - - if gcsb.ledgerBuffer.ledgerRange.to >= ledgerRange.to { - return true - } - - return false -} - -// Close closes existing GCSBackend processes. -// Note, once a GCSBackend instance is closed it can no longer be used and -// all subsequent calls to PrepareRange(), GetLedger(), etc will fail. -// Close is thread-safe and can be called from another go routine. -func (gcsb *GCSBackend) Close() error { - gcsb.gcsBackendLock.RLock() - defer gcsb.gcsBackendLock.RUnlock() - - gcsb.closed = true - - // after the GCSBackend context is Done all subsequent calls to PrepareRange() will fail - gcsb.context.Done() - - return nil -} - -// startPreparingRange prepares the ledger range by setting the range in the ledgerBuffer -func (gcsb *GCSBackend) startPreparingRange(ledgerRange Range) (bool, error) { - gcsb.gcsBackendLock.Lock() - defer gcsb.gcsBackendLock.Unlock() - - if gcsb.isPrepared(ledgerRange) { - return true, nil - } - - var err error - gcsb.ledgerBuffer, err = gcsb.newLedgerBuffer(ledgerRange) - if err != nil { - return false, err - } - - // Start the ledgerBuffer - gcsb.ledgerBuffer.pushTaskQueue() - gcsb.nextLedger = ledgerRange.from - - return false, nil -} diff --git a/ingest/ledgerbackend/gcs_ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go similarity index 64% rename from ingest/ledgerbackend/gcs_ledger_buffer.go rename to ingest/ledgerbackend/ledger_buffer.go index c58c113bab..eb4d44bfee 100644 --- a/ingest/ledgerbackend/gcs_ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "io" + "os" "sync" "time" @@ -12,13 +13,12 @@ import ( "github.com/stellar/go/support/compressxdr" "github.com/stellar/go/support/datastore" "github.com/stellar/go/support/log" - "google.golang.org/api/googleapi" ) -type ledgerBufferGCS struct { - config GCSBackendConfig +type ledgerBuffer struct { + config CloudStorageBackendConfig dataStore datastore.DataStore - taskQueue chan uint32 // buffer next gcs object read + taskQueue chan uint32 // buffer next object read ledgerQueue chan []byte // order corrected lcm batches ledgerPriorityQueue *heap.Heap[ledgerBatchObject] priorityQueueLock sync.Mutex @@ -30,59 +30,53 @@ type ledgerBufferGCS struct { nextTaskLedger uint32 ledgerRange Range - // passed through from GCSBackend to control lifetime of ledgerBufferGCS instance + // passed through from CloudStorageBackend to control lifetime of ledgerBuffer instance context context.Context cancel context.CancelCauseFunc decoder compressxdr.XDRDecoder } -func (gcsb *GCSBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBufferGCS, error) { +func (csb *CloudStorageBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBuffer, error) { less := func(a, b ledgerBatchObject) bool { return a.startLedger < b.startLedger } - pq := heap.New(less, int(gcsb.config.BufferConfig.BufferSize)) + pq := heap.New(less, int(csb.config.BufferConfig.BufferSize)) done := make(chan struct{}) - ledgerBuffer := &ledgerBufferGCS{ - config: gcsb.config, - dataStore: gcsb.dataStore, - taskQueue: make(chan uint32, gcsb.config.BufferConfig.BufferSize), - ledgerQueue: make(chan []byte, gcsb.config.BufferConfig.BufferSize), + ledgerBuffer := &ledgerBuffer{ + config: csb.config, + dataStore: csb.dataStore, + taskQueue: make(chan uint32, csb.config.BufferConfig.BufferSize), + ledgerQueue: make(chan []byte, csb.config.BufferConfig.BufferSize), ledgerPriorityQueue: pq, done: done, currentLedger: ledgerRange.from, nextTaskLedger: ledgerRange.from, ledgerRange: ledgerRange, - context: gcsb.context, - cancel: gcsb.cancel, - decoder: gcsb.decoder, + context: csb.context, + cancel: csb.cancel, + decoder: csb.decoder, } // Workers to read LCM files - for i := uint32(0); i < gcsb.config.BufferConfig.NumWorkers; i++ { + for i := uint32(0); i < csb.config.BufferConfig.NumWorkers; i++ { go ledgerBuffer.worker() } return ledgerBuffer, nil } -func (lb *ledgerBufferGCS) pushTaskQueue() { - for { - // In bounded mode, don't queue past the end ledger - if lb.nextTaskLedger > lb.ledgerRange.to && lb.ledgerRange.bounded { - return - } - select { - case lb.taskQueue <- lb.nextTaskLedger: - lb.nextTaskLedger += lb.config.LedgerBatchConfig.LedgersPerFile - default: - return - } +func (lb *ledgerBuffer) pushTaskQueue() { + // In bounded mode, don't queue past the end ledger + if lb.nextTaskLedger > lb.ledgerRange.to && lb.ledgerRange.bounded { + return } + lb.taskQueue <- lb.nextTaskLedger + lb.nextTaskLedger += lb.config.LedgerBatchConfig.LedgersPerFile } -func (lb *ledgerBufferGCS) worker() { +func (lb *ledgerBuffer) worker() { for { select { case <-lb.done: @@ -94,18 +88,21 @@ func (lb *ledgerBufferGCS) worker() { case sequence := <-lb.taskQueue: retryCount := uint32(0) for retryCount <= lb.config.BufferConfig.RetryLimit { - ledgerObject, err := lb.getLedgerGCSObject(sequence) + ledgerObject, err := lb.getLedgerObject(sequence) if err != nil { - if e, ok := err.(*googleapi.Error); ok { + if err == os.ErrNotExist { // ledgerObject not found and unbounded - if e.Code == 404 && !lb.ledgerRange.bounded { + if !lb.ledgerRange.bounded { time.Sleep(lb.config.BufferConfig.RetryWait * time.Second) continue } + lb.cancel(err) + return } if retryCount == lb.config.BufferConfig.RetryLimit { - err = errors.New("maximum retries exceeded for gcs object reads") + err = errors.New("maximum retries exceeded for object reads") lb.cancel(err) + return } retryCount++ time.Sleep(lb.config.BufferConfig.RetryWait * time.Second) @@ -119,12 +116,20 @@ func (lb *ledgerBufferGCS) worker() { } } -func (lb *ledgerBufferGCS) getLedgerGCSObject(sequence uint32) ([]byte, error) { +func (lb *ledgerBuffer) getLedgerObject(sequence uint32) ([]byte, error) { objectKey := lb.config.LedgerBatchConfig.GetObjectKeyFromSequenceNumber(sequence) + ok, err := lb.dataStore.Exists(context.Background(), objectKey) + if err != nil { + return nil, err + } + if !ok { + return nil, os.ErrNotExist + } + reader, err := lb.dataStore.GetFile(context.Background(), objectKey) if err != nil { - return nil, errors.Wrapf(err, "failed getting file: %s", objectKey) + return nil, err } defer reader.Close() @@ -137,7 +142,7 @@ func (lb *ledgerBufferGCS) getLedgerGCSObject(sequence uint32) ([]byte, error) { return objectBytes, nil } -func (lb *ledgerBufferGCS) storeObject(ledgerObject []byte, sequence uint32) { +func (lb *ledgerBuffer) storeObject(ledgerObject []byte, sequence uint32) { lb.priorityQueueLock.Lock() defer lb.priorityQueueLock.Unlock() @@ -154,7 +159,7 @@ func (lb *ledgerBufferGCS) storeObject(ledgerObject []byte, sequence uint32) { } } -func (lb *ledgerBufferGCS) getFromLedgerQueue() ([]byte, error) { +func (lb *ledgerBuffer) getFromLedgerQueue() ([]byte, error) { for { select { case <-lb.context.Done(): From 899aeab3dbd98c41cc797a810539883e428e51d1 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 3 May 2024 17:41:55 -0400 Subject: [PATCH 143/234] address comments --- ingest/ledgerbackend/cloud_storage_backend.go | 25 +++------------- .../cloud_storage_backend_test.go | 20 +++++-------- ingest/ledgerbackend/ledger_buffer.go | 29 ++++++++++++++----- 3 files changed, 33 insertions(+), 41 deletions(-) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index e0a61146c5..be36c9f15e 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -15,24 +15,15 @@ import ( // Ensure CloudStorageBackend implements LedgerBackend var _ LedgerBackend = (*CloudStorageBackend)(nil) -type ledgerBatchObject struct { - payload []byte - startLedger int // Ledger sequence used as the priority for the priorityqueue. -} - -type BufferConfig struct { - BufferSize uint32 - NumWorkers uint32 - RetryLimit uint32 - RetryWait time.Duration -} - type CloudStorageBackendConfig struct { - BufferConfig BufferConfig LedgerBatchConfig datastore.LedgerBatchConfig CompressionType string DataStore datastore.DataStore ResumableManager datastore.ResumableManager + BufferSize uint32 + NumWorkers uint32 + RetryLimit uint32 + RetryWait time.Duration } // CloudStorageBackend is a ledger backend that reads from a cloud storage service. @@ -281,14 +272,6 @@ func (csb *CloudStorageBackend) startPreparingRange(ledgerRange Range) (bool, er return false, err } - // Start the ledgerBuffer - for i := 0; i <= int(csb.config.BufferConfig.BufferSize); i++ { - if csb.ledgerBuffer.nextTaskLedger > ledgerRange.to && ledgerRange.bounded { - break - } - csb.ledgerBuffer.pushTaskQueue() - } - csb.nextLedger = ledgerRange.from return false, nil diff --git a/ingest/ledgerbackend/cloud_storage_backend_test.go b/ingest/ledgerbackend/cloud_storage_backend_test.go index f597c71a17..521f7c1fb0 100644 --- a/ingest/ledgerbackend/cloud_storage_backend_test.go +++ b/ingest/ledgerbackend/cloud_storage_backend_test.go @@ -14,13 +14,6 @@ import ( ) func createCloudStorageBackendConfigForTesting() CloudStorageBackendConfig { - bufferConfig := BufferConfig{ - BufferSize: 100, - NumWorkers: 5, - RetryLimit: 3, - RetryWait: 1, - } - param := make(map[string]string) param["destination_bucket_path"] = "testURL" @@ -35,11 +28,14 @@ func createCloudStorageBackendConfigForTesting() CloudStorageBackendConfig { resumableManager := new(datastore.MockResumableManager) return CloudStorageBackendConfig{ - BufferConfig: bufferConfig, LedgerBatchConfig: ledgerBatchConfig, CompressionType: compressxdr.GZIP, DataStore: dataStore, ResumableManager: resumableManager, + BufferSize: 100, + NumWorkers: 5, + RetryLimit: 3, + RetryWait: 1, } } @@ -71,10 +67,10 @@ func TestNewCloudStorageBackend(t *testing.T) { assert.Equal(t, ".xdr.gz", csb.config.LedgerBatchConfig.FileSuffix) assert.Equal(t, uint32(1), csb.config.LedgerBatchConfig.LedgersPerFile) assert.Equal(t, uint32(64000), csb.config.LedgerBatchConfig.FilesPerPartition) - assert.Equal(t, uint32(100), csb.config.BufferConfig.BufferSize) - assert.Equal(t, uint32(5), csb.config.BufferConfig.NumWorkers) - assert.Equal(t, uint32(3), csb.config.BufferConfig.RetryLimit) - assert.Equal(t, time.Duration(1), csb.config.BufferConfig.RetryWait) + assert.Equal(t, uint32(100), csb.config.BufferSize) + assert.Equal(t, uint32(5), csb.config.NumWorkers) + assert.Equal(t, uint32(3), csb.config.RetryLimit) + assert.Equal(t, time.Duration(1), csb.config.RetryWait) } func TestGCSNewLedgerBuffer(t *testing.T) { diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index eb4d44bfee..d3ae7a87a4 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -15,6 +15,11 @@ import ( "github.com/stellar/go/support/log" ) +type ledgerBatchObject struct { + payload []byte + startLedger int // Ledger sequence used as the priority for the priorityqueue. +} + type ledgerBuffer struct { config CloudStorageBackendConfig dataStore datastore.DataStore @@ -40,15 +45,15 @@ func (csb *CloudStorageBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBuffe less := func(a, b ledgerBatchObject) bool { return a.startLedger < b.startLedger } - pq := heap.New(less, int(csb.config.BufferConfig.BufferSize)) + pq := heap.New(less, int(csb.config.BufferSize)) done := make(chan struct{}) ledgerBuffer := &ledgerBuffer{ config: csb.config, dataStore: csb.dataStore, - taskQueue: make(chan uint32, csb.config.BufferConfig.BufferSize), - ledgerQueue: make(chan []byte, csb.config.BufferConfig.BufferSize), + taskQueue: make(chan uint32, csb.config.BufferSize), + ledgerQueue: make(chan []byte, csb.config.BufferSize), ledgerPriorityQueue: pq, done: done, currentLedger: ledgerRange.from, @@ -60,10 +65,18 @@ func (csb *CloudStorageBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBuffe } // Workers to read LCM files - for i := uint32(0); i < csb.config.BufferConfig.NumWorkers; i++ { + for i := uint32(0); i < csb.config.NumWorkers; i++ { go ledgerBuffer.worker() } + // Start the ledgerBuffer + for i := 0; i <= int(csb.config.BufferSize); i++ { + if csb.ledgerBuffer.nextTaskLedger > ledgerRange.to && ledgerRange.bounded { + break + } + csb.ledgerBuffer.pushTaskQueue() + } + return ledgerBuffer, nil } @@ -87,25 +100,25 @@ func (lb *ledgerBuffer) worker() { return case sequence := <-lb.taskQueue: retryCount := uint32(0) - for retryCount <= lb.config.BufferConfig.RetryLimit { + for retryCount <= lb.config.RetryLimit { ledgerObject, err := lb.getLedgerObject(sequence) if err != nil { if err == os.ErrNotExist { // ledgerObject not found and unbounded if !lb.ledgerRange.bounded { - time.Sleep(lb.config.BufferConfig.RetryWait * time.Second) + time.Sleep(lb.config.RetryWait * time.Second) continue } lb.cancel(err) return } - if retryCount == lb.config.BufferConfig.RetryLimit { + if retryCount == lb.config.RetryLimit { err = errors.New("maximum retries exceeded for object reads") lb.cancel(err) return } retryCount++ - time.Sleep(lb.config.BufferConfig.RetryWait * time.Second) + time.Sleep(lb.config.RetryWait * time.Second) } // Add to priority queue and continue to next task From 68ffa21b1bb631bdc1ddef912c6b58924d1b052e Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 3 May 2024 18:03:48 -0400 Subject: [PATCH 144/234] fix not exist error --- ingest/ledgerbackend/ledger_buffer.go | 17 ++++++----------- support/datastore/gcs_datastore.go | 3 +++ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index d3ae7a87a4..66ee903922 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -3,6 +3,7 @@ package ledgerbackend import ( "bytes" "context" + "fmt" "io" "os" "sync" @@ -71,10 +72,10 @@ func (csb *CloudStorageBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBuffe // Start the ledgerBuffer for i := 0; i <= int(csb.config.BufferSize); i++ { - if csb.ledgerBuffer.nextTaskLedger > ledgerRange.to && ledgerRange.bounded { + if ledgerBuffer.nextTaskLedger > ledgerRange.to && ledgerRange.bounded { break } - csb.ledgerBuffer.pushTaskQueue() + ledgerBuffer.pushTaskQueue() } return ledgerBuffer, nil @@ -103,7 +104,9 @@ func (lb *ledgerBuffer) worker() { for retryCount <= lb.config.RetryLimit { ledgerObject, err := lb.getLedgerObject(sequence) if err != nil { - if err == os.ErrNotExist { + fmt.Print("here1\n") + if errors.Is(err, os.ErrNotExist) { + fmt.Print("here2\n") // ledgerObject not found and unbounded if !lb.ledgerRange.bounded { time.Sleep(lb.config.RetryWait * time.Second) @@ -132,14 +135,6 @@ func (lb *ledgerBuffer) worker() { func (lb *ledgerBuffer) getLedgerObject(sequence uint32) ([]byte, error) { objectKey := lb.config.LedgerBatchConfig.GetObjectKeyFromSequenceNumber(sequence) - ok, err := lb.dataStore.Exists(context.Background(), objectKey) - if err != nil { - return nil, err - } - if !ok { - return nil, os.ErrNotExist - } - reader, err := lb.dataStore.GetFile(context.Background(), objectKey) if err != nil { return nil, err diff --git a/support/datastore/gcs_datastore.go b/support/datastore/gcs_datastore.go index 59637fc21c..ca1c90abea 100644 --- a/support/datastore/gcs_datastore.go +++ b/support/datastore/gcs_datastore.go @@ -66,6 +66,9 @@ func (b GCSDataStore) GetFile(ctx context.Context, filePath string) (io.ReadClos filePath = path.Join(b.prefix, filePath) r, err := b.bucket.Object(filePath).NewReader(ctx) if err != nil { + if err == storage.ErrObjectNotExist { + return nil, os.ErrNotExist + } if gcsError, ok := err.(*googleapi.Error); ok { log.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) } From c1091213b4551699aee4c7a0ddb93be5c5a29e08 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Fri, 3 May 2024 18:06:32 -0400 Subject: [PATCH 145/234] address comments --- ingest/ledgerbackend/ledger_buffer.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index 66ee903922..212368239d 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -72,9 +72,6 @@ func (csb *CloudStorageBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBuffe // Start the ledgerBuffer for i := 0; i <= int(csb.config.BufferSize); i++ { - if ledgerBuffer.nextTaskLedger > ledgerRange.to && ledgerRange.bounded { - break - } ledgerBuffer.pushTaskQueue() } From 3b6cb9f4bcbd469f4b260510173db91e275db54e Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Mon, 6 May 2024 12:34:18 -0400 Subject: [PATCH 146/234] address comments --- ingest/ledgerbackend/cloud_storage_backend.go | 53 +++++++---- .../cloud_storage_backend_test.go | 36 +++++--- ingest/ledgerbackend/ledger_buffer.go | 87 +++++++++++++------ 3 files changed, 121 insertions(+), 55 deletions(-) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/cloud_storage_backend.go index be36c9f15e..459af887ef 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/cloud_storage_backend.go @@ -41,17 +41,23 @@ type CloudStorageBackend struct { ledgerBuffer *ledgerBuffer dataStore datastore.DataStore - resumableManager datastore.ResumableManager prepared *Range // non-nil if any range is prepared closed bool // False until the core is closed ledgerMetaArchive *datastore.LedgerMetaArchive decoder compressxdr.XDRDecoder nextLedger uint32 + lastLedger uint32 } // Return a new CloudStorageBackend instance. func NewCloudStorageBackend(ctx context.Context, config CloudStorageBackendConfig) (*CloudStorageBackend, error) { - ctx, cancel := context.WithCancelCause(ctx) + if config.BufferSize == 0 { + return nil, errors.New("buffer size must be > 0") + } + + if config.NumWorkers > config.BufferSize { + return nil, errors.New("number of workers must be <= BufferSize") + } if config.DataStore == nil { return nil, errors.New("no DataStore provided") @@ -61,6 +67,20 @@ func NewCloudStorageBackend(ctx context.Context, config CloudStorageBackendConfi return nil, errors.New("no ResumableManager provided") } + if config.LedgerBatchConfig.LedgersPerFile <= 0 { + return nil, errors.New("ledgersPerFile must be > 0") + } + + if config.LedgerBatchConfig.FileSuffix == "" { + return nil, errors.New("no file suffix provided in LedgerBatchConfig") + } + + if config.CompressionType == "" { + return nil, errors.New("no compression type provided in config") + } + + ctx, cancel := context.WithCancelCause(ctx) + ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) decoder, err := compressxdr.NewXDRDecoder(config.CompressionType, nil) if err != nil { @@ -72,7 +92,6 @@ func NewCloudStorageBackend(ctx context.Context, config CloudStorageBackendConfi context: ctx, cancel: cancel, dataStore: config.DataStore, - resumableManager: config.ResumableManager, ledgerMetaArchive: ledgerMetaArchive, decoder: decoder, } @@ -82,6 +101,9 @@ func NewCloudStorageBackend(ctx context.Context, config CloudStorageBackendConfi // GetLatestLedgerSequence returns the most recent ledger sequence number in the cloud storage bucket. func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { + csb.csBackendLock.RLock() + defer csb.csBackendLock.RUnlock() + if csb.closed { return 0, errors.New("CloudStorageBackend is closed; cannot GetLatestLedgerSequence") } @@ -90,17 +112,17 @@ func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (ui return 0, errors.New("CloudStorageBackend must be prepared, call PrepareRange first") } - if csb.ledgerBuffer.nextTaskLedger == csb.ledgerBuffer.ledgerRange.from { - return 0, nil + latestSeq, err := csb.ledgerBuffer.getLatestLedgerSequence() + if err != nil { + return 0, err } - // Subtract 1 to get the latest ledger in buffer - return csb.ledgerBuffer.nextTaskLedger - 1, nil + return latestSeq, nil } -// getSequenceInBatch checks if the requested sequence is in the cached batch. +// getBatchForSequence checks if the requested sequence is in the cached batch. // Otherwise will continuously load in the next LedgerCloseMetaBatch until found. -func (csb *CloudStorageBackend) getSequenceInBatch(sequence uint32) error { +func (csb *CloudStorageBackend) getBatchForSequence(sequence uint32) error { for { // Sequence inside the current cached LedgerCloseMetaBatch if sequence >= csb.ledgerMetaArchive.GetStartLedgerSequence() && sequence <= csb.ledgerMetaArchive.GetEndLedgerSequence() { @@ -162,11 +184,11 @@ func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) } } - if csb.nextExpectedSequence() != sequence { - return xdr.LedgerCloseMeta{}, errors.New("requested sequence is not the next available ledger") + if sequence > csb.nextExpectedSequence() { + return xdr.LedgerCloseMeta{}, errors.New("requested sequence is not the lastLedger nor the next available ledger") } - err := csb.getSequenceInBatch(sequence) + err := csb.getBatchForSequence(sequence) if err != nil { return xdr.LedgerCloseMeta{}, err } @@ -175,6 +197,7 @@ func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) if err != nil { return xdr.LedgerCloseMeta{}, err } + csb.lastLedger = csb.nextLedger csb.nextLedger++ return ledgerCloseMeta, nil @@ -182,6 +205,9 @@ func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) // PrepareRange checks if the starting and ending (if bounded) ledgers exist. func (csb *CloudStorageBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { + csb.csBackendLock.Lock() + defer csb.csBackendLock.Unlock() + if csb.closed { return errors.New("CloudStorageBackend is closed; cannot PrepareRange") } @@ -259,9 +285,6 @@ func (csb *CloudStorageBackend) Close() error { // startPreparingRange prepares the ledger range by setting the range in the ledgerBuffer func (csb *CloudStorageBackend) startPreparingRange(ledgerRange Range) (bool, error) { - csb.csBackendLock.Lock() - defer csb.csBackendLock.Unlock() - if csb.isPrepared(ledgerRange) { return true, nil } diff --git a/ingest/ledgerbackend/cloud_storage_backend_test.go b/ingest/ledgerbackend/cloud_storage_backend_test.go index 521f7c1fb0..6ebee7eadd 100644 --- a/ingest/ledgerbackend/cloud_storage_backend_test.go +++ b/ingest/ledgerbackend/cloud_storage_backend_test.go @@ -49,7 +49,6 @@ func createCloudStorageBackendForTesting() CloudStorageBackend { config: config, context: ctx, dataStore: config.DataStore, - resumableManager: config.ResumableManager, ledgerMetaArchive: ledgerMetaArchive, decoder: decoder, } @@ -63,7 +62,6 @@ func TestNewCloudStorageBackend(t *testing.T) { assert.NoError(t, err) assert.Equal(t, csb.dataStore, config.DataStore) - assert.Equal(t, csb.resumableManager, config.ResumableManager) assert.Equal(t, ".xdr.gz", csb.config.LedgerBatchConfig.FileSuffix) assert.Equal(t, uint32(1), csb.config.LedgerBatchConfig.LedgersPerFile) assert.Equal(t, uint32(64000), csb.config.LedgerBatchConfig.FilesPerPartition) @@ -81,22 +79,32 @@ func TestGCSNewLedgerBuffer(t *testing.T) { assert.NoError(t, err) assert.Equal(t, uint32(2), ledgerBuffer.currentLedger) - assert.Equal(t, uint32(2), ledgerBuffer.nextTaskLedger) + assert.Equal(t, uint32(4), ledgerBuffer.nextTaskLedger) assert.Equal(t, ledgerRange, ledgerBuffer.ledgerRange) } func TestCloudStorageGetLatestLedgerSequence(t *testing.T) { - ctx := context.Background() + startLedger := uint32(3) + endLedger := uint32(5) csb := createCloudStorageBackendForTesting() - resumableManager := new(datastore.MockResumableManager) - csb.resumableManager = resumableManager + ctx := context.Background() + ledgerRange := BoundedRange(startLedger, endLedger) - resumableManager.On("FindStart", ctx, uint32(2), uint32(0)).Return(uint32(6), true, nil) + readCloser1 := createLCMBatchReader(uint32(3), uint32(3), 1) + readCloser2 := createLCMBatchReader(uint32(4), uint32(4), 1) + readCloser3 := createLCMBatchReader(uint32(5), uint32(5), 1) + + mockDataStore := new(datastore.MockDataStore) + csb.dataStore = mockDataStore + mockDataStore.On("GetFile", ctx, "0-63999/3.xdr.gz").Return(readCloser1, nil) + mockDataStore.On("GetFile", ctx, "0-63999/4.xdr.gz").Return(readCloser2, nil) + mockDataStore.On("GetFile", ctx, "0-63999/5.xdr.gz").Return(readCloser3, nil) - seq, err := csb.GetLatestLedgerSequence(ctx) + csb.PrepareRange(ctx, ledgerRange) + latestSeq, err := csb.GetLatestLedgerSequence(ctx) assert.NoError(t, err) - assert.Equal(t, uint32(5), seq) + assert.Equal(t, uint32(5), latestSeq) } func createLCMForTesting(start, end uint32) []xdr.LedgerCloseMeta { @@ -153,7 +161,9 @@ func TestCloudStorageGetLedger_SingleLedgerPerFile(t *testing.T) { lcm, err := csb.GetLedger(ctx, uint32(3)) assert.NoError(t, err) assert.Equal(t, lcmArray[0], lcm) - // Skip sequence 4; Test non consecutive GetLedger + lcm, err = csb.GetLedger(ctx, uint32(4)) + assert.NoError(t, err) + assert.Equal(t, lcmArray[1], lcm) lcm, err = csb.GetLedger(ctx, uint32(5)) assert.NoError(t, err) assert.Equal(t, lcmArray[2], lcm) @@ -211,11 +221,11 @@ func TestGCSGetLedger_ErrorPreceedingLedger(t *testing.T) { csb.PrepareRange(ctx, ledgerRange) - lcm, err := csb.GetLedger(ctx, uint32(5)) + lcm, err := csb.GetLedger(ctx, uint32(3)) assert.NoError(t, err) - assert.Equal(t, lcmArray[2], lcm) + assert.Equal(t, lcmArray[0], lcm) - _, err = csb.GetLedger(ctx, uint32(4)) + _, err = csb.GetLedger(ctx, uint32(2)) assert.Error(t, err, "requested sequence preceeds current LedgerCloseMetaBatch") } diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index 212368239d..69e3e8e46c 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -3,7 +3,6 @@ package ledgerbackend import ( "bytes" "context" - "fmt" "io" "os" "sync" @@ -22,24 +21,30 @@ type ledgerBatchObject struct { } type ledgerBuffer struct { - config CloudStorageBackendConfig - dataStore datastore.DataStore - taskQueue chan uint32 // buffer next object read - ledgerQueue chan []byte // order corrected lcm batches - ledgerPriorityQueue *heap.Heap[ledgerBatchObject] + // passed through from CloudStorageBackend to control lifetime of ledgerBuffer instance + config CloudStorageBackendConfig + dataStore datastore.DataStore + context context.Context + cancel context.CancelCauseFunc + decoder compressxdr.XDRDecoder + + // The pipes and data structures below help establish the ledgerBuffer invariant which is + // the number of tasks (both pending and in-flight) + len(ledgerQueue) + ledgerPriorityQueue.Len() + // is always less than or equal to the config.BufferSize + taskQueue chan uint32 // Buffer next object read + ledgerQueue chan []byte // Order corrected lcm batches + ledgerPriorityQueue *heap.Heap[ledgerBatchObject] // Priority is set to the sequence number priorityQueueLock sync.Mutex - done chan struct{} + + // done is used to signal the closure of the ledgerBuffer and workers + done chan struct{} // keep track of the ledgers to be processed and the next ordering // the ledgers should be buffered - currentLedger uint32 - nextTaskLedger uint32 - ledgerRange Range - - // passed through from CloudStorageBackend to control lifetime of ledgerBuffer instance - context context.Context - cancel context.CancelCauseFunc - decoder compressxdr.XDRDecoder + currentLedger uint32 // The current ledger that should be popped from ledgerPriorityQueue + nextTaskLedger uint32 // The next task ledger that should be added to taskQueue + ledgerRange Range + currentLedgerLock sync.RWMutex } func (csb *CloudStorageBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBuffer, error) { @@ -65,12 +70,18 @@ func (csb *CloudStorageBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBuffe decoder: csb.decoder, } - // Workers to read LCM files + // Start workers to read LCM files for i := uint32(0); i < csb.config.NumWorkers; i++ { go ledgerBuffer.worker() } - // Start the ledgerBuffer + // Upon initialization, the ledgerBuffer invariant is maintained because + // we create csb.config.BufferSize tasks while the len(ledgerQueue) and ledgerPriorityQueue.Len() are 0. + // Effectively, this is len(taskQueue) + len(ledgerQueue) + ledgerPriorityQueue.Len() == csb.config.BufferSize + // which enforces a limit of max tasks (both pending and in-flight) to be equal to csb.config.BufferSize. + // Note: when a task is in-flight it is no longer in the taskQueue + // but for easier conceptualization, len(taskQueue) can be interpreted as both pending and in-flight tasks + // where we assume the workers are empty and not processing any tasks. for i := 0; i <= int(csb.config.BufferSize); i++ { ledgerBuffer.pushTaskQueue() } @@ -97,13 +108,10 @@ func (lb *ledgerBuffer) worker() { log.Error(lb.context.Err()) return case sequence := <-lb.taskQueue: - retryCount := uint32(0) - for retryCount <= lb.config.RetryLimit { - ledgerObject, err := lb.getLedgerObject(sequence) + for retryCount := uint32(0); retryCount <= lb.config.RetryLimit; { + ledgerObject, err := lb.downloadLedgerObject(sequence) if err != nil { - fmt.Print("here1\n") if errors.Is(err, os.ErrNotExist) { - fmt.Print("here2\n") // ledgerObject not found and unbounded if !lb.ledgerRange.bounded { time.Sleep(lb.config.RetryWait * time.Second) @@ -113,7 +121,7 @@ func (lb *ledgerBuffer) worker() { return } if retryCount == lb.config.RetryLimit { - err = errors.New("maximum retries exceeded for object reads") + err = errors.Wrap(err, "maximum retries exceeded for object reads") lb.cancel(err) return } @@ -121,7 +129,11 @@ func (lb *ledgerBuffer) worker() { time.Sleep(lb.config.RetryWait * time.Second) } - // Add to priority queue and continue to next task + // When we store an object we still maintain the ledger buffer invariant because + // at this point the current task is finished and we add 1 ledger object to the priority queue. + // Thus, the number of tasks decreases by 1 and the priority queue length increases by 1. + // This keeps the overall total the same (<= BufferSize). As long as the the ledger buffer invariant + // was maintained in the previous state, it is still maintained during this state transition. lb.storeObject(ledgerObject, sequence) break } @@ -129,7 +141,7 @@ func (lb *ledgerBuffer) worker() { } } -func (lb *ledgerBuffer) getLedgerObject(sequence uint32) ([]byte, error) { +func (lb *ledgerBuffer) downloadLedgerObject(sequence uint32) ([]byte, error) { objectKey := lb.config.LedgerBatchConfig.GetObjectKeyFromSequenceNumber(sequence) reader, err := lb.dataStore.GetFile(context.Background(), objectKey) @@ -151,12 +163,17 @@ func (lb *ledgerBuffer) storeObject(ledgerObject []byte, sequence uint32) { lb.priorityQueueLock.Lock() defer lb.priorityQueueLock.Unlock() + lb.currentLedgerLock.Lock() + defer lb.currentLedgerLock.Unlock() + lb.ledgerPriorityQueue.Push(ledgerBatchObject{ payload: ledgerObject, startLedger: int(sequence), }) - // Check if the nextLedger is the next item in the priority queue + // Check if the nextLedger is the next item in the ledgerPriorityQueue + // The ledgerBuffer invariant is maintained here because items are transferred from the ledgerPriorityQueue to the ledgerQueue. + // Thus the overall sum of ledgerPriorityQueue.Len() + len(lb.ledgerQueue) remains the same. for lb.ledgerPriorityQueue.Len() > 0 && lb.currentLedger == uint32(lb.ledgerPriorityQueue.Peek().startLedger) { item := lb.ledgerPriorityQueue.Pop() lb.ledgerQueue <- item.payload @@ -172,7 +189,11 @@ func (lb *ledgerBuffer) getFromLedgerQueue() ([]byte, error) { close(lb.done) return nil, lb.context.Err() case compressedBinary := <-lb.ledgerQueue: - // Add next task to the TaskQueue + // The ledger buffer invariant is maintained here because + // we create an extra task when consuming one item from the ledger queue. + // Thus len(ledgerQueue) decreases by 1 and the number of tasks increases by 1. + // The overall sum below remains the same: + // len(taskQueue) + len(ledgerQueue) + ledgerPriorityQueue.Len() == csb.config.BufferSize lb.pushTaskQueue() reader := bytes.NewReader(compressedBinary) @@ -185,3 +206,15 @@ func (lb *ledgerBuffer) getFromLedgerQueue() ([]byte, error) { } } } + +func (lb *ledgerBuffer) getLatestLedgerSequence() (uint32, error) { + lb.currentLedgerLock.Lock() + defer lb.currentLedgerLock.Unlock() + + if lb.currentLedger == lb.ledgerRange.from { + return 0, nil + } + + // Subtract 1 to get the latest ledger in buffer + return lb.currentLedger - 1, nil +} From 4f60a7f163af8906a260352e92f70f70df8fb2bc Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Mon, 6 May 2024 12:47:19 -0400 Subject: [PATCH 147/234] rename to BufferedStorageBackend --- ...backend.go => buffered_storage_backend.go} | 154 +++++++++--------- ...st.go => buffered_storage_backend_test.go} | 152 ++++++++--------- ingest/ledgerbackend/ledger_buffer.go | 34 ++-- 3 files changed, 170 insertions(+), 170 deletions(-) rename ingest/ledgerbackend/{cloud_storage_backend.go => buffered_storage_backend.go} (54%) rename ingest/ledgerbackend/{cloud_storage_backend_test.go => buffered_storage_backend_test.go} (67%) diff --git a/ingest/ledgerbackend/cloud_storage_backend.go b/ingest/ledgerbackend/buffered_storage_backend.go similarity index 54% rename from ingest/ledgerbackend/cloud_storage_backend.go rename to ingest/ledgerbackend/buffered_storage_backend.go index 459af887ef..c076c406bc 100644 --- a/ingest/ledgerbackend/cloud_storage_backend.go +++ b/ingest/ledgerbackend/buffered_storage_backend.go @@ -12,10 +12,10 @@ import ( "github.com/stellar/go/xdr" ) -// Ensure CloudStorageBackend implements LedgerBackend -var _ LedgerBackend = (*CloudStorageBackend)(nil) +// Ensure BufferedStorageBackend implements LedgerBackend +var _ LedgerBackend = (*BufferedStorageBackend)(nil) -type CloudStorageBackendConfig struct { +type BufferedStorageBackendConfig struct { LedgerBatchConfig datastore.LedgerBatchConfig CompressionType string DataStore datastore.DataStore @@ -26,16 +26,16 @@ type CloudStorageBackendConfig struct { RetryWait time.Duration } -// CloudStorageBackend is a ledger backend that reads from a cloud storage service. +// BufferedStorageBackend is a ledger backend that reads from a cloud storage service. // The cloud storage service contains files generated from the ledgerExporter. -type CloudStorageBackend struct { - config CloudStorageBackendConfig +type BufferedStorageBackend struct { + config BufferedStorageBackendConfig context context.Context - // cancel is the CancelCauseFunc for context which controls the lifetime of a CloudStorageBackend instance. - // Once it is invoked CloudStorageBackend will not be able to stream ledgers from CloudStorageBackend. + // cancel is the CancelCauseFunc for context which controls the lifetime of a BufferedStorageBackend instance. + // Once it is invoked BufferedStorageBackend will not be able to stream ledgers from BufferedStorageBackend. cancel context.CancelCauseFunc - csBackendLock sync.RWMutex + bsBackendLock sync.RWMutex // ledgerBuffer is the buffer for LedgerCloseMeta data read in parallel. ledgerBuffer *ledgerBuffer @@ -49,8 +49,8 @@ type CloudStorageBackend struct { lastLedger uint32 } -// Return a new CloudStorageBackend instance. -func NewCloudStorageBackend(ctx context.Context, config CloudStorageBackendConfig) (*CloudStorageBackend, error) { +// Return a new BufferedStorageBackend instance. +func NewBufferedStorageBackend(ctx context.Context, config BufferedStorageBackendConfig) (*BufferedStorageBackend, error) { if config.BufferSize == 0 { return nil, errors.New("buffer size must be > 0") } @@ -87,7 +87,7 @@ func NewCloudStorageBackend(ctx context.Context, config CloudStorageBackendConfi return nil, err } - csBackend := &CloudStorageBackend{ + bsBackend := &BufferedStorageBackend{ config: config, context: ctx, cancel: cancel, @@ -96,23 +96,23 @@ func NewCloudStorageBackend(ctx context.Context, config CloudStorageBackendConfi decoder: decoder, } - return csBackend, nil + return bsBackend, nil } // GetLatestLedgerSequence returns the most recent ledger sequence number in the cloud storage bucket. -func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - csb.csBackendLock.RLock() - defer csb.csBackendLock.RUnlock() +func (bsb *BufferedStorageBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { + bsb.bsBackendLock.RLock() + defer bsb.bsBackendLock.RUnlock() - if csb.closed { - return 0, errors.New("CloudStorageBackend is closed; cannot GetLatestLedgerSequence") + if bsb.closed { + return 0, errors.New("BufferedStorageBackend is closed; cannot GetLatestLedgerSequence") } - if csb.prepared == nil { - return 0, errors.New("CloudStorageBackend must be prepared, call PrepareRange first") + if bsb.prepared == nil { + return 0, errors.New("BufferedStorageBackend must be prepared, call PrepareRange first") } - latestSeq, err := csb.ledgerBuffer.getLatestLedgerSequence() + latestSeq, err := bsb.ledgerBuffer.getLatestLedgerSequence() if err != nil { return 0, err } @@ -122,27 +122,27 @@ func (csb *CloudStorageBackend) GetLatestLedgerSequence(ctx context.Context) (ui // getBatchForSequence checks if the requested sequence is in the cached batch. // Otherwise will continuously load in the next LedgerCloseMetaBatch until found. -func (csb *CloudStorageBackend) getBatchForSequence(sequence uint32) error { +func (bsb *BufferedStorageBackend) getBatchForSequence(sequence uint32) error { for { // Sequence inside the current cached LedgerCloseMetaBatch - if sequence >= csb.ledgerMetaArchive.GetStartLedgerSequence() && sequence <= csb.ledgerMetaArchive.GetEndLedgerSequence() { + if sequence >= bsb.ledgerMetaArchive.GetStartLedgerSequence() && sequence <= bsb.ledgerMetaArchive.GetEndLedgerSequence() { return nil } // Sequence is before the current LedgerCloseMetaBatch // Does not support retrieving LedgerCloseMeta before the current cached batch - if sequence < csb.ledgerMetaArchive.GetStartLedgerSequence() { + if sequence < bsb.ledgerMetaArchive.GetStartLedgerSequence() { return errors.New("requested sequence preceeds current LedgerCloseMetaBatch") } // Sequence is beyond the current LedgerCloseMetaBatch - lcmBatchBinary, err := csb.ledgerBuffer.getFromLedgerQueue() + lcmBatchBinary, err := bsb.ledgerBuffer.getFromLedgerQueue() if err != nil { return errors.Wrap(err, "failed getting next ledger batch from queue") } // Turn binary into xdr - err = csb.ledgerMetaArchive.Data.UnmarshalBinary(lcmBatchBinary) + err = bsb.ledgerMetaArchive.Data.UnmarshalBinary(lcmBatchBinary) if err != nil { return errors.Wrap(err, "failed unmarshalling lcmBatchBinary") } @@ -154,148 +154,148 @@ func (csb *CloudStorageBackend) getBatchForSequence(sequence uint32) error { // This is done because `nextLedger` is 0 between the moment Stellar-Core is // started and streaming the first ledger (in such case we return first ledger // in requested range). -func (csb *CloudStorageBackend) nextExpectedSequence() uint32 { - if csb.nextLedger == 0 && csb.prepared != nil { - return csb.prepared.from +func (bsb *BufferedStorageBackend) nextExpectedSequence() uint32 { + if bsb.nextLedger == 0 && bsb.prepared != nil { + return bsb.prepared.from } - return csb.nextLedger + return bsb.nextLedger } // GetLedger returns the LedgerCloseMeta for the specified ledger sequence number -func (csb *CloudStorageBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { - csb.csBackendLock.RLock() - defer csb.csBackendLock.RUnlock() +func (bsb *BufferedStorageBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { + bsb.bsBackendLock.RLock() + defer bsb.bsBackendLock.RUnlock() - if csb.closed { - return xdr.LedgerCloseMeta{}, errors.New("CloudStorageBackend is closed; cannot GetLedger") + if bsb.closed { + return xdr.LedgerCloseMeta{}, errors.New("BufferedStorageBackend is closed; cannot GetLedger") } - if csb.prepared == nil { + if bsb.prepared == nil { return xdr.LedgerCloseMeta{}, errors.New("session is not prepared, call PrepareRange first") } - if sequence < csb.ledgerBuffer.ledgerRange.from { + if sequence < bsb.ledgerBuffer.ledgerRange.from { return xdr.LedgerCloseMeta{}, errors.New("requested sequence preceeds current LedgerRange") } - if csb.ledgerBuffer.ledgerRange.bounded { - if sequence > csb.ledgerBuffer.ledgerRange.to { + if bsb.ledgerBuffer.ledgerRange.bounded { + if sequence > bsb.ledgerBuffer.ledgerRange.to { return xdr.LedgerCloseMeta{}, errors.New("requested sequence beyond current LedgerRange") } } - if sequence > csb.nextExpectedSequence() { + if sequence > bsb.nextExpectedSequence() { return xdr.LedgerCloseMeta{}, errors.New("requested sequence is not the lastLedger nor the next available ledger") } - err := csb.getBatchForSequence(sequence) + err := bsb.getBatchForSequence(sequence) if err != nil { return xdr.LedgerCloseMeta{}, err } - ledgerCloseMeta, err := csb.ledgerMetaArchive.GetLedger(sequence) + ledgerCloseMeta, err := bsb.ledgerMetaArchive.GetLedger(sequence) if err != nil { return xdr.LedgerCloseMeta{}, err } - csb.lastLedger = csb.nextLedger - csb.nextLedger++ + bsb.lastLedger = bsb.nextLedger + bsb.nextLedger++ return ledgerCloseMeta, nil } // PrepareRange checks if the starting and ending (if bounded) ledgers exist. -func (csb *CloudStorageBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { - csb.csBackendLock.Lock() - defer csb.csBackendLock.Unlock() +func (bsb *BufferedStorageBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { + bsb.bsBackendLock.Lock() + defer bsb.bsBackendLock.Unlock() - if csb.closed { - return errors.New("CloudStorageBackend is closed; cannot PrepareRange") + if bsb.closed { + return errors.New("BufferedStorageBackend is closed; cannot PrepareRange") } - if alreadyPrepared, err := csb.startPreparingRange(ledgerRange); err != nil { + if alreadyPrepared, err := bsb.startPreparingRange(ledgerRange); err != nil { return errors.Wrap(err, "error starting prepare range") } else if alreadyPrepared { return nil } - csb.prepared = &ledgerRange + bsb.prepared = &ledgerRange return nil } // IsPrepared returns true if a given ledgerRange is prepared. -func (csb *CloudStorageBackend) IsPrepared(ctx context.Context, ledgerRange Range) (bool, error) { - csb.csBackendLock.RLock() - defer csb.csBackendLock.RUnlock() +func (bsb *BufferedStorageBackend) IsPrepared(ctx context.Context, ledgerRange Range) (bool, error) { + bsb.bsBackendLock.RLock() + defer bsb.bsBackendLock.RUnlock() - if csb.closed { - return false, errors.New("CloudStorageBackend is closed; cannot IsPrepared") + if bsb.closed { + return false, errors.New("BufferedStorageBackend is closed; cannot IsPrepared") } - return csb.isPrepared(ledgerRange), nil + return bsb.isPrepared(ledgerRange), nil } -func (csb *CloudStorageBackend) isPrepared(ledgerRange Range) bool { - if csb.closed { +func (bsb *BufferedStorageBackend) isPrepared(ledgerRange Range) bool { + if bsb.closed { return false } - if csb.prepared == nil { + if bsb.prepared == nil { return false } - if csb.ledgerBuffer.ledgerRange.from > ledgerRange.from { + if bsb.ledgerBuffer.ledgerRange.from > ledgerRange.from { return false } - if csb.ledgerBuffer.ledgerRange.bounded && !ledgerRange.bounded { + if bsb.ledgerBuffer.ledgerRange.bounded && !ledgerRange.bounded { return false } - if !csb.ledgerBuffer.ledgerRange.bounded && !ledgerRange.bounded { + if !bsb.ledgerBuffer.ledgerRange.bounded && !ledgerRange.bounded { return true } - if !csb.ledgerBuffer.ledgerRange.bounded && ledgerRange.bounded { + if !bsb.ledgerBuffer.ledgerRange.bounded && ledgerRange.bounded { return true } - if csb.ledgerBuffer.ledgerRange.to >= ledgerRange.to { + if bsb.ledgerBuffer.ledgerRange.to >= ledgerRange.to { return true } return false } -// Close closes existing CloudStorageBackend processes. -// Note, once a CloudStorageBackend instance is closed it can no longer be used and +// Close closes existing BufferedStorageBackend processes. +// Note, once a BufferedStorageBackend instance is closed it can no longer be used and // all subsequent calls to PrepareRange(), GetLedger(), etc will fail. // Close is thread-safe and can be called from another go routine. -func (csb *CloudStorageBackend) Close() error { - csb.csBackendLock.RLock() - defer csb.csBackendLock.RUnlock() +func (bsb *BufferedStorageBackend) Close() error { + bsb.bsBackendLock.RLock() + defer bsb.bsBackendLock.RUnlock() - csb.closed = true + bsb.closed = true - // after the CloudStorageBackend context is Done all subsequent calls to PrepareRange() will fail - csb.context.Done() + // after the BufferedStorageBackend context is Done all subsequent calls to PrepareRange() will fail + bsb.context.Done() return nil } // startPreparingRange prepares the ledger range by setting the range in the ledgerBuffer -func (csb *CloudStorageBackend) startPreparingRange(ledgerRange Range) (bool, error) { - if csb.isPrepared(ledgerRange) { +func (bsb *BufferedStorageBackend) startPreparingRange(ledgerRange Range) (bool, error) { + if bsb.isPrepared(ledgerRange) { return true, nil } var err error - csb.ledgerBuffer, err = csb.newLedgerBuffer(ledgerRange) + bsb.ledgerBuffer, err = bsb.newLedgerBuffer(ledgerRange) if err != nil { return false, err } - csb.nextLedger = ledgerRange.from + bsb.nextLedger = ledgerRange.from return false, nil } diff --git a/ingest/ledgerbackend/cloud_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go similarity index 67% rename from ingest/ledgerbackend/cloud_storage_backend_test.go rename to ingest/ledgerbackend/buffered_storage_backend_test.go index 6ebee7eadd..8d3b95b175 100644 --- a/ingest/ledgerbackend/cloud_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/assert" ) -func createCloudStorageBackendConfigForTesting() CloudStorageBackendConfig { +func createBufferedStorageBackendConfigForTesting() BufferedStorageBackendConfig { param := make(map[string]string) param["destination_bucket_path"] = "testURL" @@ -27,7 +27,7 @@ func createCloudStorageBackendConfigForTesting() CloudStorageBackendConfig { resumableManager := new(datastore.MockResumableManager) - return CloudStorageBackendConfig{ + return BufferedStorageBackendConfig{ LedgerBatchConfig: ledgerBatchConfig, CompressionType: compressxdr.GZIP, DataStore: dataStore, @@ -39,13 +39,13 @@ func createCloudStorageBackendConfigForTesting() CloudStorageBackendConfig { } } -func createCloudStorageBackendForTesting() CloudStorageBackend { - config := createCloudStorageBackendConfigForTesting() +func createBufferedStorageBackendForTesting() BufferedStorageBackend { + config := createBufferedStorageBackendConfigForTesting() ctx := context.Background() ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) decoder, _ := compressxdr.NewXDRDecoder(config.CompressionType, nil) - return CloudStorageBackend{ + return BufferedStorageBackend{ config: config, context: ctx, dataStore: config.DataStore, @@ -54,28 +54,28 @@ func createCloudStorageBackendForTesting() CloudStorageBackend { } } -func TestNewCloudStorageBackend(t *testing.T) { +func TestNewBufferedStorageBackend(t *testing.T) { ctx := context.Background() - config := createCloudStorageBackendConfigForTesting() + config := createBufferedStorageBackendConfigForTesting() - csb, err := NewCloudStorageBackend(ctx, config) + bsb, err := NewBufferedStorageBackend(ctx, config) assert.NoError(t, err) - assert.Equal(t, csb.dataStore, config.DataStore) - assert.Equal(t, ".xdr.gz", csb.config.LedgerBatchConfig.FileSuffix) - assert.Equal(t, uint32(1), csb.config.LedgerBatchConfig.LedgersPerFile) - assert.Equal(t, uint32(64000), csb.config.LedgerBatchConfig.FilesPerPartition) - assert.Equal(t, uint32(100), csb.config.BufferSize) - assert.Equal(t, uint32(5), csb.config.NumWorkers) - assert.Equal(t, uint32(3), csb.config.RetryLimit) - assert.Equal(t, time.Duration(1), csb.config.RetryWait) + assert.Equal(t, bsb.dataStore, config.DataStore) + assert.Equal(t, ".xdr.gz", bsb.config.LedgerBatchConfig.FileSuffix) + assert.Equal(t, uint32(1), bsb.config.LedgerBatchConfig.LedgersPerFile) + assert.Equal(t, uint32(64000), bsb.config.LedgerBatchConfig.FilesPerPartition) + assert.Equal(t, uint32(100), bsb.config.BufferSize) + assert.Equal(t, uint32(5), bsb.config.NumWorkers) + assert.Equal(t, uint32(3), bsb.config.RetryLimit) + assert.Equal(t, time.Duration(1), bsb.config.RetryWait) } func TestGCSNewLedgerBuffer(t *testing.T) { - csb := createCloudStorageBackendForTesting() + bsb := createBufferedStorageBackendForTesting() ledgerRange := BoundedRange(2, 3) - ledgerBuffer, err := csb.newLedgerBuffer(ledgerRange) + ledgerBuffer, err := bsb.newLedgerBuffer(ledgerRange) assert.NoError(t, err) assert.Equal(t, uint32(2), ledgerBuffer.currentLedger) @@ -86,7 +86,7 @@ func TestGCSNewLedgerBuffer(t *testing.T) { func TestCloudStorageGetLatestLedgerSequence(t *testing.T) { startLedger := uint32(3) endLedger := uint32(5) - csb := createCloudStorageBackendForTesting() + bsb := createBufferedStorageBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(startLedger, endLedger) @@ -95,13 +95,13 @@ func TestCloudStorageGetLatestLedgerSequence(t *testing.T) { readCloser3 := createLCMBatchReader(uint32(5), uint32(5), 1) mockDataStore := new(datastore.MockDataStore) - csb.dataStore = mockDataStore + bsb.dataStore = mockDataStore mockDataStore.On("GetFile", ctx, "0-63999/3.xdr.gz").Return(readCloser1, nil) mockDataStore.On("GetFile", ctx, "0-63999/4.xdr.gz").Return(readCloser2, nil) mockDataStore.On("GetFile", ctx, "0-63999/5.xdr.gz").Return(readCloser3, nil) - csb.PrepareRange(ctx, ledgerRange) - latestSeq, err := csb.GetLatestLedgerSequence(ctx) + bsb.PrepareRange(ctx, ledgerRange) + latestSeq, err := bsb.GetLatestLedgerSequence(ctx) assert.NoError(t, err) assert.Equal(t, uint32(5), latestSeq) @@ -142,7 +142,7 @@ func TestCloudStorageGetLedger_SingleLedgerPerFile(t *testing.T) { startLedger := uint32(3) endLedger := uint32(5) lcmArray := createLCMForTesting(startLedger, endLedger) - csb := createCloudStorageBackendForTesting() + bsb := createBufferedStorageBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(startLedger, endLedger) @@ -151,20 +151,20 @@ func TestCloudStorageGetLedger_SingleLedgerPerFile(t *testing.T) { readCloser3 := createLCMBatchReader(uint32(5), uint32(5), 1) mockDataStore := new(datastore.MockDataStore) - csb.dataStore = mockDataStore + bsb.dataStore = mockDataStore mockDataStore.On("GetFile", ctx, "0-63999/3.xdr.gz").Return(readCloser1, nil) mockDataStore.On("GetFile", ctx, "0-63999/4.xdr.gz").Return(readCloser2, nil) mockDataStore.On("GetFile", ctx, "0-63999/5.xdr.gz").Return(readCloser3, nil) - csb.PrepareRange(ctx, ledgerRange) + bsb.PrepareRange(ctx, ledgerRange) - lcm, err := csb.GetLedger(ctx, uint32(3)) + lcm, err := bsb.GetLedger(ctx, uint32(3)) assert.NoError(t, err) assert.Equal(t, lcmArray[0], lcm) - lcm, err = csb.GetLedger(ctx, uint32(4)) + lcm, err = bsb.GetLedger(ctx, uint32(4)) assert.NoError(t, err) assert.Equal(t, lcmArray[1], lcm) - lcm, err = csb.GetLedger(ctx, uint32(5)) + lcm, err = bsb.GetLedger(ctx, uint32(5)) assert.NoError(t, err) assert.Equal(t, lcmArray[2], lcm) } @@ -173,30 +173,30 @@ func TestCloudStorageGetLedger_MultipleLedgerPerFile(t *testing.T) { startLedger := uint32(2) endLedger := uint32(5) lcmArray := createLCMForTesting(startLedger, endLedger) - csb := createCloudStorageBackendForTesting() + bsb := createBufferedStorageBackendForTesting() ctx := context.Background() - csb.config.LedgerBatchConfig.LedgersPerFile = uint32(2) + bsb.config.LedgerBatchConfig.LedgersPerFile = uint32(2) ledgerRange := BoundedRange(startLedger, endLedger) readCloser1 := createLCMBatchReader(uint32(2), uint32(3), 2) readCloser2 := createLCMBatchReader(uint32(4), uint32(5), 2) mockDataStore := new(datastore.MockDataStore) - csb.dataStore = mockDataStore + bsb.dataStore = mockDataStore mockDataStore.On("GetFile", ctx, "0-127999/2-3.xdr.gz").Return(readCloser1, nil) mockDataStore.On("GetFile", ctx, "0-127999/4-5.xdr.gz").Return(readCloser2, nil) - csb.PrepareRange(ctx, ledgerRange) + bsb.PrepareRange(ctx, ledgerRange) - lcm, err := csb.GetLedger(ctx, uint32(2)) + lcm, err := bsb.GetLedger(ctx, uint32(2)) assert.NoError(t, err) assert.Equal(t, lcmArray[0], lcm) - lcm, err = csb.GetLedger(ctx, uint32(3)) + lcm, err = bsb.GetLedger(ctx, uint32(3)) assert.NoError(t, err) assert.Equal(t, lcmArray[1], lcm) - lcm, err = csb.GetLedger(ctx, uint32(4)) + lcm, err = bsb.GetLedger(ctx, uint32(4)) assert.NoError(t, err) assert.Equal(t, lcmArray[2], lcm) } @@ -205,7 +205,7 @@ func TestGCSGetLedger_ErrorPreceedingLedger(t *testing.T) { startLedger := uint32(3) endLedger := uint32(5) lcmArray := createLCMForTesting(startLedger, endLedger) - csb := createCloudStorageBackendForTesting() + bsb := createBufferedStorageBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(startLedger, endLedger) @@ -214,127 +214,127 @@ func TestGCSGetLedger_ErrorPreceedingLedger(t *testing.T) { readCloser3 := createLCMBatchReader(uint32(5), uint32(5), 1) mockDataStore := new(datastore.MockDataStore) - csb.dataStore = mockDataStore + bsb.dataStore = mockDataStore mockDataStore.On("GetFile", ctx, "0-63999/3.xdr.gz").Return(readCloser1, nil) mockDataStore.On("GetFile", ctx, "0-63999/4.xdr.gz").Return(readCloser2, nil) mockDataStore.On("GetFile", ctx, "0-63999/5.xdr.gz").Return(readCloser3, nil) - csb.PrepareRange(ctx, ledgerRange) + bsb.PrepareRange(ctx, ledgerRange) - lcm, err := csb.GetLedger(ctx, uint32(3)) + lcm, err := bsb.GetLedger(ctx, uint32(3)) assert.NoError(t, err) assert.Equal(t, lcmArray[0], lcm) - _, err = csb.GetLedger(ctx, uint32(2)) + _, err = bsb.GetLedger(ctx, uint32(2)) assert.Error(t, err, "requested sequence preceeds current LedgerCloseMetaBatch") } func TestGCSGetLedger_NotPrepared(t *testing.T) { - csb := createCloudStorageBackendForTesting() + bsb := createBufferedStorageBackendForTesting() ctx := context.Background() - _, err := csb.GetLedger(ctx, uint32(3)) + _, err := bsb.GetLedger(ctx, uint32(3)) assert.Error(t, err, "session is not prepared, call PrepareRange first") } func TestGCSGetLedger_SequenceNotInBatch(t *testing.T) { - csb := createCloudStorageBackendForTesting() + bsb := createBufferedStorageBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(3, 5) - csb.PrepareRange(ctx, ledgerRange) + bsb.PrepareRange(ctx, ledgerRange) - _, err := csb.GetLedger(ctx, uint32(2)) + _, err := bsb.GetLedger(ctx, uint32(2)) assert.Error(t, err, "requested sequence preceeds current LedgerRange") - _, err = csb.GetLedger(ctx, uint32(6)) + _, err = bsb.GetLedger(ctx, uint32(6)) assert.Error(t, err, "requested sequence beyond current LedgerRange") } func TestGCSPrepareRange(t *testing.T) { - csb := createCloudStorageBackendForTesting() + bsb := createBufferedStorageBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(2, 3) - err := csb.PrepareRange(ctx, ledgerRange) + err := bsb.PrepareRange(ctx, ledgerRange) assert.NoError(t, err) - assert.NotNil(t, csb.prepared) + assert.NotNil(t, bsb.prepared) // check alreadyPrepared - err = csb.PrepareRange(ctx, ledgerRange) + err = bsb.PrepareRange(ctx, ledgerRange) assert.NoError(t, err) - assert.NotNil(t, csb.prepared) + assert.NotNil(t, bsb.prepared) } func TestGCSIsPrepared_Bounded(t *testing.T) { - csb := createCloudStorageBackendForTesting() + bsb := createBufferedStorageBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(3, 4) - csb.PrepareRange(ctx, ledgerRange) + bsb.PrepareRange(ctx, ledgerRange) - ok, err := csb.IsPrepared(ctx, ledgerRange) + ok, err := bsb.IsPrepared(ctx, ledgerRange) assert.NoError(t, err) assert.True(t, ok) - ok, err = csb.IsPrepared(ctx, BoundedRange(2, 4)) + ok, err = bsb.IsPrepared(ctx, BoundedRange(2, 4)) assert.NoError(t, err) assert.False(t, ok) - ok, err = csb.IsPrepared(ctx, UnboundedRange(3)) + ok, err = bsb.IsPrepared(ctx, UnboundedRange(3)) assert.NoError(t, err) assert.False(t, ok) - ok, err = csb.IsPrepared(ctx, UnboundedRange(2)) + ok, err = bsb.IsPrepared(ctx, UnboundedRange(2)) assert.NoError(t, err) assert.False(t, ok) } func TestGCSIsPrepared_Unbounded(t *testing.T) { - csb := createCloudStorageBackendForTesting() + bsb := createBufferedStorageBackendForTesting() ctx := context.Background() ledgerRange := UnboundedRange(3) - csb.PrepareRange(ctx, ledgerRange) + bsb.PrepareRange(ctx, ledgerRange) - ok, err := csb.IsPrepared(ctx, ledgerRange) + ok, err := bsb.IsPrepared(ctx, ledgerRange) assert.NoError(t, err) assert.True(t, ok) - ok, err = csb.IsPrepared(ctx, BoundedRange(3, 4)) + ok, err = bsb.IsPrepared(ctx, BoundedRange(3, 4)) assert.NoError(t, err) assert.True(t, ok) - ok, err = csb.IsPrepared(ctx, BoundedRange(2, 4)) + ok, err = bsb.IsPrepared(ctx, BoundedRange(2, 4)) assert.NoError(t, err) assert.False(t, ok) - ok, err = csb.IsPrepared(ctx, UnboundedRange(4)) + ok, err = bsb.IsPrepared(ctx, UnboundedRange(4)) assert.NoError(t, err) assert.True(t, ok) - ok, err = csb.IsPrepared(ctx, UnboundedRange(2)) + ok, err = bsb.IsPrepared(ctx, UnboundedRange(2)) assert.NoError(t, err) assert.False(t, ok) } func TestGCSClose(t *testing.T) { - csb := createCloudStorageBackendForTesting() + bsb := createBufferedStorageBackendForTesting() ctx := context.Background() ledgerRange := BoundedRange(3, 5) - csb.PrepareRange(ctx, ledgerRange) + bsb.PrepareRange(ctx, ledgerRange) - err := csb.Close() + err := bsb.Close() assert.NoError(t, err) - assert.Equal(t, true, csb.closed) + assert.Equal(t, true, bsb.closed) - _, err = csb.GetLatestLedgerSequence(ctx) - assert.Error(t, err, "gcsBackend is closed; cannot GetLatestLedgerSequence") + _, err = bsb.GetLatestLedgerSequence(ctx) + assert.Error(t, err, "gbsbackend is closed; cannot GetLatestLedgerSequence") - _, err = csb.GetLedger(ctx, 3) - assert.Error(t, err, "gcsBackend is closed; cannot GetLedger") + _, err = bsb.GetLedger(ctx, 3) + assert.Error(t, err, "gbsbackend is closed; cannot GetLedger") - err = csb.PrepareRange(ctx, ledgerRange) - assert.Error(t, err, "gcsBackend is closed; cannot PrepareRange") + err = bsb.PrepareRange(ctx, ledgerRange) + assert.Error(t, err, "gbsbackend is closed; cannot PrepareRange") - _, err = csb.IsPrepared(ctx, ledgerRange) - assert.Error(t, err, "gcsBackend is closed; cannot IsPrepared") + _, err = bsb.IsPrepared(ctx, ledgerRange) + assert.Error(t, err, "gbsbackend is closed; cannot IsPrepared") } diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index 69e3e8e46c..ab1891be03 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -21,8 +21,8 @@ type ledgerBatchObject struct { } type ledgerBuffer struct { - // passed through from CloudStorageBackend to control lifetime of ledgerBuffer instance - config CloudStorageBackendConfig + // passed through from BufferedStorageBackend to control lifetime of ledgerBuffer instance + config BufferedStorageBackendConfig dataStore datastore.DataStore context context.Context cancel context.CancelCauseFunc @@ -47,42 +47,42 @@ type ledgerBuffer struct { currentLedgerLock sync.RWMutex } -func (csb *CloudStorageBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBuffer, error) { +func (bsb *BufferedStorageBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBuffer, error) { less := func(a, b ledgerBatchObject) bool { return a.startLedger < b.startLedger } - pq := heap.New(less, int(csb.config.BufferSize)) + pq := heap.New(less, int(bsb.config.BufferSize)) done := make(chan struct{}) ledgerBuffer := &ledgerBuffer{ - config: csb.config, - dataStore: csb.dataStore, - taskQueue: make(chan uint32, csb.config.BufferSize), - ledgerQueue: make(chan []byte, csb.config.BufferSize), + config: bsb.config, + dataStore: bsb.dataStore, + taskQueue: make(chan uint32, bsb.config.BufferSize), + ledgerQueue: make(chan []byte, bsb.config.BufferSize), ledgerPriorityQueue: pq, done: done, currentLedger: ledgerRange.from, nextTaskLedger: ledgerRange.from, ledgerRange: ledgerRange, - context: csb.context, - cancel: csb.cancel, - decoder: csb.decoder, + context: bsb.context, + cancel: bsb.cancel, + decoder: bsb.decoder, } // Start workers to read LCM files - for i := uint32(0); i < csb.config.NumWorkers; i++ { + for i := uint32(0); i < bsb.config.NumWorkers; i++ { go ledgerBuffer.worker() } // Upon initialization, the ledgerBuffer invariant is maintained because - // we create csb.config.BufferSize tasks while the len(ledgerQueue) and ledgerPriorityQueue.Len() are 0. - // Effectively, this is len(taskQueue) + len(ledgerQueue) + ledgerPriorityQueue.Len() == csb.config.BufferSize - // which enforces a limit of max tasks (both pending and in-flight) to be equal to csb.config.BufferSize. + // we create bsb.config.BufferSize tasks while the len(ledgerQueue) and ledgerPriorityQueue.Len() are 0. + // Effectively, this is len(taskQueue) + len(ledgerQueue) + ledgerPriorityQueue.Len() == bsb.config.BufferSize + // which enforces a limit of max tasks (both pending and in-flight) to be equal to bsb.config.BufferSize. // Note: when a task is in-flight it is no longer in the taskQueue // but for easier conceptualization, len(taskQueue) can be interpreted as both pending and in-flight tasks // where we assume the workers are empty and not processing any tasks. - for i := 0; i <= int(csb.config.BufferSize); i++ { + for i := 0; i <= int(bsb.config.BufferSize); i++ { ledgerBuffer.pushTaskQueue() } @@ -193,7 +193,7 @@ func (lb *ledgerBuffer) getFromLedgerQueue() ([]byte, error) { // we create an extra task when consuming one item from the ledger queue. // Thus len(ledgerQueue) decreases by 1 and the number of tasks increases by 1. // The overall sum below remains the same: - // len(taskQueue) + len(ledgerQueue) + ledgerPriorityQueue.Len() == csb.config.BufferSize + // len(taskQueue) + len(ledgerQueue) + ledgerPriorityQueue.Len() == bsb.config.BufferSize lb.pushTaskQueue() reader := bytes.NewReader(compressedBinary) From eca6d6505919c6c79afab216b61f92f99bbed326 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Mon, 6 May 2024 12:51:29 -0400 Subject: [PATCH 148/234] Add comment header --- ingest/ledgerbackend/buffered_storage_backend.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ingest/ledgerbackend/buffered_storage_backend.go b/ingest/ledgerbackend/buffered_storage_backend.go index c076c406bc..b57cc2e713 100644 --- a/ingest/ledgerbackend/buffered_storage_backend.go +++ b/ingest/ledgerbackend/buffered_storage_backend.go @@ -1,3 +1,6 @@ +// BufferedStorageBackend is a ledger backend that provides buffered access over a given DataStore. +// The DataStore must contain files generated from a LedgerExporter. + package ledgerbackend import ( From a460a9abb62eb2f9b5ce6e8194574e6d913a8828 Mon Sep 17 00:00:00 2001 From: Kanwalpreet Dhindsa Date: Mon, 6 May 2024 10:16:05 -0700 Subject: [PATCH 149/234] updated codeql config (#5297) --- .github/workflows/codeql-analysis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9fbf83bbd7..b3e66579e6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -24,15 +24,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v3 From 2a4e073b0874620378a346cfea02154f3e1483c3 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Mon, 6 May 2024 13:34:15 -0400 Subject: [PATCH 150/234] Remove ResumableManager --- ingest/ledgerbackend/buffered_storage_backend.go | 5 ----- ingest/ledgerbackend/buffered_storage_backend_test.go | 3 --- 2 files changed, 8 deletions(-) diff --git a/ingest/ledgerbackend/buffered_storage_backend.go b/ingest/ledgerbackend/buffered_storage_backend.go index b57cc2e713..d79822a817 100644 --- a/ingest/ledgerbackend/buffered_storage_backend.go +++ b/ingest/ledgerbackend/buffered_storage_backend.go @@ -22,7 +22,6 @@ type BufferedStorageBackendConfig struct { LedgerBatchConfig datastore.LedgerBatchConfig CompressionType string DataStore datastore.DataStore - ResumableManager datastore.ResumableManager BufferSize uint32 NumWorkers uint32 RetryLimit uint32 @@ -66,10 +65,6 @@ func NewBufferedStorageBackend(ctx context.Context, config BufferedStorageBacken return nil, errors.New("no DataStore provided") } - if config.ResumableManager == nil { - return nil, errors.New("no ResumableManager provided") - } - if config.LedgerBatchConfig.LedgersPerFile <= 0 { return nil, errors.New("ledgersPerFile must be > 0") } diff --git a/ingest/ledgerbackend/buffered_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go index 8d3b95b175..5b264f2893 100644 --- a/ingest/ledgerbackend/buffered_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -25,13 +25,10 @@ func createBufferedStorageBackendConfigForTesting() BufferedStorageBackendConfig dataStore := new(datastore.MockDataStore) - resumableManager := new(datastore.MockResumableManager) - return BufferedStorageBackendConfig{ LedgerBatchConfig: ledgerBatchConfig, CompressionType: compressxdr.GZIP, DataStore: dataStore, - ResumableManager: resumableManager, BufferSize: 100, NumWorkers: 5, RetryLimit: 3, From 3ada03bdc368a2cb366c0f73b9ea45321783da09 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Mon, 6 May 2024 13:48:02 -0400 Subject: [PATCH 151/234] Update error message --- ingest/ledgerbackend/buffered_storage_backend.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ingest/ledgerbackend/buffered_storage_backend.go b/ingest/ledgerbackend/buffered_storage_backend.go index d79822a817..620a22d515 100644 --- a/ingest/ledgerbackend/buffered_storage_backend.go +++ b/ingest/ledgerbackend/buffered_storage_backend.go @@ -5,6 +5,7 @@ package ledgerbackend import ( "context" + "fmt" "sync" "time" @@ -183,7 +184,7 @@ func (bsb *BufferedStorageBackend) GetLedger(ctx context.Context, sequence uint3 } if sequence > bsb.nextExpectedSequence() { - return xdr.LedgerCloseMeta{}, errors.New("requested sequence is not the lastLedger nor the next available ledger") + return xdr.LedgerCloseMeta{}, fmt.Errorf("requested sequence is not the lastLedger (%d) nor the next available ledger (%d)", bsb.lastLedger, bsb.nextLedger) } err := bsb.getBatchForSequence(sequence) From 333bbaa6661cb0ddae590b9e2b365c43bb3d78c4 Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Mon, 6 May 2024 16:39:09 -0400 Subject: [PATCH 152/234] Update context in ledger buffer --- .../ledgerbackend/buffered_storage_backend.go | 29 +- .../buffered_storage_backend_test.go | 286 ++++++++++++------ ingest/ledgerbackend/ledger_buffer.go | 49 ++- 3 files changed, 222 insertions(+), 142 deletions(-) diff --git a/ingest/ledgerbackend/buffered_storage_backend.go b/ingest/ledgerbackend/buffered_storage_backend.go index 620a22d515..726e4a2d4d 100644 --- a/ingest/ledgerbackend/buffered_storage_backend.go +++ b/ingest/ledgerbackend/buffered_storage_backend.go @@ -29,22 +29,18 @@ type BufferedStorageBackendConfig struct { RetryWait time.Duration } -// BufferedStorageBackend is a ledger backend that reads from a cloud storage service. -// The cloud storage service contains files generated from the ledgerExporter. +// BufferedStorageBackend is a ledger backend that reads from a storage service. +// The storage service contains files generated from the ledgerExporter. type BufferedStorageBackend struct { config BufferedStorageBackendConfig - context context.Context - // cancel is the CancelCauseFunc for context which controls the lifetime of a BufferedStorageBackend instance. - // Once it is invoked BufferedStorageBackend will not be able to stream ledgers from BufferedStorageBackend. - cancel context.CancelCauseFunc bsBackendLock sync.RWMutex // ledgerBuffer is the buffer for LedgerCloseMeta data read in parallel. ledgerBuffer *ledgerBuffer dataStore datastore.DataStore - prepared *Range // non-nil if any range is prepared + prepared *Range // Non-nil if any range is prepared closed bool // False until the core is closed ledgerMetaArchive *datastore.LedgerMetaArchive decoder compressxdr.XDRDecoder @@ -78,8 +74,6 @@ func NewBufferedStorageBackend(ctx context.Context, config BufferedStorageBacken return nil, errors.New("no compression type provided in config") } - ctx, cancel := context.WithCancelCause(ctx) - ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) decoder, err := compressxdr.NewXDRDecoder(config.CompressionType, nil) if err != nil { @@ -88,8 +82,6 @@ func NewBufferedStorageBackend(ctx context.Context, config BufferedStorageBacken bsBackend := &BufferedStorageBackend{ config: config, - context: ctx, - cancel: cancel, dataStore: config.DataStore, ledgerMetaArchive: ledgerMetaArchive, decoder: decoder, @@ -98,7 +90,7 @@ func NewBufferedStorageBackend(ctx context.Context, config BufferedStorageBacken return bsBackend, nil } -// GetLatestLedgerSequence returns the most recent ledger sequence number in the cloud storage bucket. +// GetLatestLedgerSequence returns the most recent ledger sequence number available in the buffer. func (bsb *BufferedStorageBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { bsb.bsBackendLock.RLock() defer bsb.bsBackendLock.RUnlock() @@ -121,7 +113,7 @@ func (bsb *BufferedStorageBackend) GetLatestLedgerSequence(ctx context.Context) // getBatchForSequence checks if the requested sequence is in the cached batch. // Otherwise will continuously load in the next LedgerCloseMetaBatch until found. -func (bsb *BufferedStorageBackend) getBatchForSequence(sequence uint32) error { +func (bsb *BufferedStorageBackend) getBatchForSequence(ctx context.Context, sequence uint32) error { for { // Sequence inside the current cached LedgerCloseMetaBatch if sequence >= bsb.ledgerMetaArchive.GetStartLedgerSequence() && sequence <= bsb.ledgerMetaArchive.GetEndLedgerSequence() { @@ -135,7 +127,7 @@ func (bsb *BufferedStorageBackend) getBatchForSequence(sequence uint32) error { } // Sequence is beyond the current LedgerCloseMetaBatch - lcmBatchBinary, err := bsb.ledgerBuffer.getFromLedgerQueue() + lcmBatchBinary, err := bsb.ledgerBuffer.getFromLedgerQueue(ctx) if err != nil { return errors.Wrap(err, "failed getting next ledger batch from queue") } @@ -187,7 +179,7 @@ func (bsb *BufferedStorageBackend) GetLedger(ctx context.Context, sequence uint3 return xdr.LedgerCloseMeta{}, fmt.Errorf("requested sequence is not the lastLedger (%d) nor the next available ledger (%d)", bsb.lastLedger, bsb.nextLedger) } - err := bsb.getBatchForSequence(sequence) + err := bsb.getBatchForSequence(ctx, sequence) if err != nil { return xdr.LedgerCloseMeta{}, err } @@ -274,10 +266,11 @@ func (bsb *BufferedStorageBackend) Close() error { bsb.bsBackendLock.RLock() defer bsb.bsBackendLock.RUnlock() - bsb.closed = true + if bsb.ledgerBuffer != nil { + bsb.ledgerBuffer.close() + } - // after the BufferedStorageBackend context is Done all subsequent calls to PrepareRange() will fail - bsb.context.Done() + bsb.closed = true return nil } diff --git a/ingest/ledgerbackend/buffered_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go index 5b264f2893..243f42ea3c 100644 --- a/ingest/ledgerbackend/buffered_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -3,6 +3,7 @@ package ledgerbackend import ( "bytes" "context" + "fmt" "io" "testing" "time" @@ -11,8 +12,12 @@ import ( "github.com/stellar/go/support/datastore" "github.com/stellar/go/xdr" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) +var partitionSize = uint32(64000) +var ledgerPerFileCount = uint32(1) + func createBufferedStorageBackendConfigForTesting() BufferedStorageBackendConfig { param := make(map[string]string) param["destination_bucket_path"] = "testURL" @@ -38,19 +43,68 @@ func createBufferedStorageBackendConfigForTesting() BufferedStorageBackendConfig func createBufferedStorageBackendForTesting() BufferedStorageBackend { config := createBufferedStorageBackendConfigForTesting() - ctx := context.Background() ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) decoder, _ := compressxdr.NewXDRDecoder(config.CompressionType, nil) return BufferedStorageBackend{ config: config, - context: ctx, dataStore: config.DataStore, ledgerMetaArchive: ledgerMetaArchive, decoder: decoder, } } +func createMockdataStore(start, end, partitionSize, count uint32) *datastore.MockDataStore { + mockDataStore := new(datastore.MockDataStore) + partition := count*partitionSize - 1 + for i := start; i <= end; i = i + count { + var objectName string + var readCloser io.ReadCloser + if count > 1 { + endFileSeq := i + count - 1 + readCloser = createLCMBatchReader(i, endFileSeq, count) + objectName = fmt.Sprintf("0-%d/%d-%d.xdr.gz", partition, i, endFileSeq) + } else { + readCloser = createLCMBatchReader(i, i, count) + objectName = fmt.Sprintf("0-%d/%d.xdr.gz", partition, i) + } + mockDataStore.On("GetFile", mock.Anything, objectName).Return(readCloser, nil) + } + + return mockDataStore +} + +func createLCMForTesting(start, end uint32) []xdr.LedgerCloseMeta { + var lcmArray []xdr.LedgerCloseMeta + for i := start; i <= end; i++ { + lcmArray = append(lcmArray, datastore.CreateLedgerCloseMeta(i)) + } + + return lcmArray +} + +func createTestLedgerCloseMetaBatch(startSeq, endSeq, count uint32) xdr.LedgerCloseMetaBatch { + var ledgerCloseMetas []xdr.LedgerCloseMeta + for i := uint32(0); i < count; i++ { + ledgerCloseMetas = append(ledgerCloseMetas, datastore.CreateLedgerCloseMeta(startSeq+uint32(i))) + } + return xdr.LedgerCloseMetaBatch{ + StartSequence: xdr.Uint32(startSeq), + EndSequence: xdr.Uint32(endSeq), + LedgerCloseMetas: ledgerCloseMetas, + } +} + +func createLCMBatchReader(start, end, count uint32) io.ReadCloser { + testData := createTestLedgerCloseMetaBatch(start, end, count) + encoder, _ := compressxdr.NewXDREncoder(compressxdr.GZIP, testData) + var buf bytes.Buffer + encoder.WriteTo(&buf) + capturedBuf := buf.Bytes() + reader := bytes.NewReader(capturedBuf) + return io.NopCloser(reader) +} + func TestNewBufferedStorageBackend(t *testing.T) { ctx := context.Background() config := createBufferedStorageBackendConfigForTesting() @@ -68,7 +122,7 @@ func TestNewBufferedStorageBackend(t *testing.T) { assert.Equal(t, time.Duration(1), bsb.config.RetryWait) } -func TestGCSNewLedgerBuffer(t *testing.T) { +func TestNewLedgerBuffer(t *testing.T) { bsb := createBufferedStorageBackendForTesting() ledgerRange := BoundedRange(2, 3) @@ -80,80 +134,39 @@ func TestGCSNewLedgerBuffer(t *testing.T) { assert.Equal(t, ledgerRange, ledgerBuffer.ledgerRange) } -func TestCloudStorageGetLatestLedgerSequence(t *testing.T) { +func TestBSBGetLatestLedgerSequence(t *testing.T) { startLedger := uint32(3) endLedger := uint32(5) - bsb := createBufferedStorageBackendForTesting() ctx := context.Background() + bsb := createBufferedStorageBackendForTesting() ledgerRange := BoundedRange(startLedger, endLedger) - - readCloser1 := createLCMBatchReader(uint32(3), uint32(3), 1) - readCloser2 := createLCMBatchReader(uint32(4), uint32(4), 1) - readCloser3 := createLCMBatchReader(uint32(5), uint32(5), 1) - - mockDataStore := new(datastore.MockDataStore) + mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore - mockDataStore.On("GetFile", ctx, "0-63999/3.xdr.gz").Return(readCloser1, nil) - mockDataStore.On("GetFile", ctx, "0-63999/4.xdr.gz").Return(readCloser2, nil) - mockDataStore.On("GetFile", ctx, "0-63999/5.xdr.gz").Return(readCloser3, nil) - bsb.PrepareRange(ctx, ledgerRange) + assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) + assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) == 3 }, time.Second*5, time.Millisecond*50) + latestSeq, err := bsb.GetLatestLedgerSequence(ctx) assert.NoError(t, err) assert.Equal(t, uint32(5), latestSeq) -} - -func createLCMForTesting(start, end uint32) []xdr.LedgerCloseMeta { - var lcmArray []xdr.LedgerCloseMeta - for i := start; i <= end; i++ { - lcmArray = append(lcmArray, datastore.CreateLedgerCloseMeta(i)) - } - return lcmArray + mockDataStore.AssertExpectations(t) } -func createTestLedgerCloseMetaBatch(startSeq, endSeq uint32, count int) xdr.LedgerCloseMetaBatch { - var ledgerCloseMetas []xdr.LedgerCloseMeta - for i := 0; i < count; i++ { - ledgerCloseMetas = append(ledgerCloseMetas, datastore.CreateLedgerCloseMeta(startSeq+uint32(i))) - } - return xdr.LedgerCloseMetaBatch{ - StartSequence: xdr.Uint32(startSeq), - EndSequence: xdr.Uint32(endSeq), - LedgerCloseMetas: ledgerCloseMetas, - } -} - -func createLCMBatchReader(start, end uint32, count int) io.ReadCloser { - testData := createTestLedgerCloseMetaBatch(start, end, count) - encoder, _ := compressxdr.NewXDREncoder(compressxdr.GZIP, testData) - var buf bytes.Buffer - encoder.WriteTo(&buf) - capturedBuf := buf.Bytes() - reader1 := bytes.NewReader(capturedBuf) - return io.NopCloser(reader1) -} - -func TestCloudStorageGetLedger_SingleLedgerPerFile(t *testing.T) { +func TestBSBGetLedger_SingleLedgerPerFile(t *testing.T) { startLedger := uint32(3) endLedger := uint32(5) + ctx := context.Background() lcmArray := createLCMForTesting(startLedger, endLedger) bsb := createBufferedStorageBackendForTesting() - ctx := context.Background() ledgerRange := BoundedRange(startLedger, endLedger) - readCloser1 := createLCMBatchReader(uint32(3), uint32(3), 1) - readCloser2 := createLCMBatchReader(uint32(4), uint32(4), 1) - readCloser3 := createLCMBatchReader(uint32(5), uint32(5), 1) - - mockDataStore := new(datastore.MockDataStore) + mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore - mockDataStore.On("GetFile", ctx, "0-63999/3.xdr.gz").Return(readCloser1, nil) - mockDataStore.On("GetFile", ctx, "0-63999/4.xdr.gz").Return(readCloser2, nil) - mockDataStore.On("GetFile", ctx, "0-63999/5.xdr.gz").Return(readCloser3, nil) - bsb.PrepareRange(ctx, ledgerRange) + assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) + assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) == 3 }, time.Second*5, time.Millisecond*50) lcm, err := bsb.GetLedger(ctx, uint32(3)) assert.NoError(t, err) @@ -164,6 +177,8 @@ func TestCloudStorageGetLedger_SingleLedgerPerFile(t *testing.T) { lcm, err = bsb.GetLedger(ctx, uint32(5)) assert.NoError(t, err) assert.Equal(t, lcmArray[2], lcm) + + mockDataStore.AssertExpectations(t) } func TestCloudStorageGetLedger_MultipleLedgerPerFile(t *testing.T) { @@ -175,48 +190,38 @@ func TestCloudStorageGetLedger_MultipleLedgerPerFile(t *testing.T) { bsb.config.LedgerBatchConfig.LedgersPerFile = uint32(2) ledgerRange := BoundedRange(startLedger, endLedger) - readCloser1 := createLCMBatchReader(uint32(2), uint32(3), 2) - readCloser2 := createLCMBatchReader(uint32(4), uint32(5), 2) - - mockDataStore := new(datastore.MockDataStore) + mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, 2) bsb.dataStore = mockDataStore - mockDataStore.On("GetFile", ctx, "0-127999/2-3.xdr.gz").Return(readCloser1, nil) - mockDataStore.On("GetFile", ctx, "0-127999/4-5.xdr.gz").Return(readCloser2, nil) - bsb.PrepareRange(ctx, ledgerRange) + assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) + assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) == 2 }, time.Second*5, time.Millisecond*50) lcm, err := bsb.GetLedger(ctx, uint32(2)) assert.NoError(t, err) assert.Equal(t, lcmArray[0], lcm) - lcm, err = bsb.GetLedger(ctx, uint32(3)) assert.NoError(t, err) assert.Equal(t, lcmArray[1], lcm) - lcm, err = bsb.GetLedger(ctx, uint32(4)) assert.NoError(t, err) assert.Equal(t, lcmArray[2], lcm) + + mockDataStore.AssertExpectations(t) } -func TestGCSGetLedger_ErrorPreceedingLedger(t *testing.T) { +func TestBSBGetLedger_ErrorPreceedingLedger(t *testing.T) { startLedger := uint32(3) endLedger := uint32(5) + ctx := context.Background() lcmArray := createLCMForTesting(startLedger, endLedger) bsb := createBufferedStorageBackendForTesting() - ctx := context.Background() ledgerRange := BoundedRange(startLedger, endLedger) - readCloser1 := createLCMBatchReader(uint32(3), uint32(3), 1) - readCloser2 := createLCMBatchReader(uint32(4), uint32(4), 1) - readCloser3 := createLCMBatchReader(uint32(5), uint32(5), 1) - - mockDataStore := new(datastore.MockDataStore) + mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore - mockDataStore.On("GetFile", ctx, "0-63999/3.xdr.gz").Return(readCloser1, nil) - mockDataStore.On("GetFile", ctx, "0-63999/4.xdr.gz").Return(readCloser2, nil) - mockDataStore.On("GetFile", ctx, "0-63999/5.xdr.gz").Return(readCloser3, nil) - bsb.PrepareRange(ctx, ledgerRange) + assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) + assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) == 3 }, time.Second*5, time.Millisecond*50) lcm, err := bsb.GetLedger(ctx, uint32(3)) assert.NoError(t, err) @@ -224,9 +229,11 @@ func TestGCSGetLedger_ErrorPreceedingLedger(t *testing.T) { _, err = bsb.GetLedger(ctx, uint32(2)) assert.Error(t, err, "requested sequence preceeds current LedgerCloseMetaBatch") + + mockDataStore.AssertExpectations(t) } -func TestGCSGetLedger_NotPrepared(t *testing.T) { +func TestBSBGetLedger_NotPrepared(t *testing.T) { bsb := createBufferedStorageBackendForTesting() ctx := context.Background() @@ -234,12 +241,18 @@ func TestGCSGetLedger_NotPrepared(t *testing.T) { assert.Error(t, err, "session is not prepared, call PrepareRange first") } -func TestGCSGetLedger_SequenceNotInBatch(t *testing.T) { - bsb := createBufferedStorageBackendForTesting() +func TestBSBGetLedger_SequenceNotInBatch(t *testing.T) { + startLedger := uint32(3) + endLedger := uint32(5) ctx := context.Background() - ledgerRange := BoundedRange(3, 5) + bsb := createBufferedStorageBackendForTesting() + ledgerRange := BoundedRange(startLedger, endLedger) - bsb.PrepareRange(ctx, ledgerRange) + mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + bsb.dataStore = mockDataStore + + assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) + assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) == 3 }, time.Second*5, time.Millisecond*50) _, err := bsb.GetLedger(ctx, uint32(2)) assert.Error(t, err, "requested sequence preceeds current LedgerRange") @@ -248,26 +261,39 @@ func TestGCSGetLedger_SequenceNotInBatch(t *testing.T) { assert.Error(t, err, "requested sequence beyond current LedgerRange") } -func TestGCSPrepareRange(t *testing.T) { - bsb := createBufferedStorageBackendForTesting() +func TestBSBPrepareRange(t *testing.T) { + startLedger := uint32(2) + endLedger := uint32(3) ctx := context.Background() - ledgerRange := BoundedRange(2, 3) + bsb := createBufferedStorageBackendForTesting() + ledgerRange := BoundedRange(startLedger, endLedger) + + mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + bsb.dataStore = mockDataStore + + assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) + assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) == 2 }, time.Second*5, time.Millisecond*50) - err := bsb.PrepareRange(ctx, ledgerRange) - assert.NoError(t, err) assert.NotNil(t, bsb.prepared) // check alreadyPrepared - err = bsb.PrepareRange(ctx, ledgerRange) + err := bsb.PrepareRange(ctx, ledgerRange) assert.NoError(t, err) assert.NotNil(t, bsb.prepared) } -func TestGCSIsPrepared_Bounded(t *testing.T) { - bsb := createBufferedStorageBackendForTesting() +func TestBSBIsPrepared_Bounded(t *testing.T) { + startLedger := uint32(3) + endLedger := uint32(5) ctx := context.Background() - ledgerRange := BoundedRange(3, 4) - bsb.PrepareRange(ctx, ledgerRange) + bsb := createBufferedStorageBackendForTesting() + ledgerRange := BoundedRange(startLedger, endLedger) + + mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + bsb.dataStore = mockDataStore + + assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) + assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) == 3 }, time.Second*5, time.Millisecond*50) ok, err := bsb.IsPrepared(ctx, ledgerRange) assert.NoError(t, err) @@ -286,11 +312,18 @@ func TestGCSIsPrepared_Bounded(t *testing.T) { assert.False(t, ok) } -func TestGCSIsPrepared_Unbounded(t *testing.T) { - bsb := createBufferedStorageBackendForTesting() +func TestBSBIsPrepared_Unbounded(t *testing.T) { ctx := context.Background() + bsb := createBufferedStorageBackendForTesting() ledgerRange := UnboundedRange(3) - bsb.PrepareRange(ctx, ledgerRange) + + readCloser := createLCMBatchReader(3, 3, 1) + mockDataStore := new(datastore.MockDataStore) + mockDataStore.On("GetFile", mock.Anything, mock.Anything).Return(readCloser, nil) + bsb.dataStore = mockDataStore + + assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) + assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) > 3 }, time.Second*5, time.Millisecond*50) ok, err := bsb.IsPrepared(ctx, ledgerRange) assert.NoError(t, err) @@ -313,11 +346,18 @@ func TestGCSIsPrepared_Unbounded(t *testing.T) { assert.False(t, ok) } -func TestGCSClose(t *testing.T) { - bsb := createBufferedStorageBackendForTesting() +func TestBSBClose(t *testing.T) { + startLedger := uint32(2) + endLedger := uint32(3) ctx := context.Background() - ledgerRange := BoundedRange(3, 5) - bsb.PrepareRange(ctx, ledgerRange) + bsb := createBufferedStorageBackendForTesting() + ledgerRange := BoundedRange(startLedger, endLedger) + + mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + bsb.dataStore = mockDataStore + + assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) + assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) == 2 }, time.Second*5, time.Millisecond*50) err := bsb.Close() assert.NoError(t, err) @@ -334,4 +374,54 @@ func TestGCSClose(t *testing.T) { _, err = bsb.IsPrepared(ctx, ledgerRange) assert.Error(t, err, "gbsbackend is closed; cannot IsPrepared") + + mockDataStore.AssertExpectations(t) +} + +func TestLedgerBufferInvariant(t *testing.T) { + startLedger := uint32(3) + endLedger := uint32(6) + ctx := context.Background() + lcmArray := createLCMForTesting(startLedger, endLedger) + bsb := createBufferedStorageBackendForTesting() + bsb.config.NumWorkers = 2 + bsb.config.BufferSize = 2 + ledgerRange := BoundedRange(startLedger, endLedger) + + mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + bsb.dataStore = mockDataStore + + assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) + assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) == 2 }, time.Second*5, time.Millisecond*50) + + // Buffer should have hit the BufferSize limit + assert.Equal(t, 2, len(bsb.ledgerBuffer.ledgerQueue)) + + lcm, err := bsb.GetLedger(ctx, uint32(3)) + assert.NoError(t, err) + assert.Equal(t, lcmArray[0], lcm) + lcm, err = bsb.GetLedger(ctx, uint32(4)) + assert.NoError(t, err) + assert.Equal(t, lcmArray[1], lcm) + + // Buffer should fill up with remaining ledgers + assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) == 2 }, time.Second*5, time.Millisecond*50) + assert.Equal(t, 2, len(bsb.ledgerBuffer.ledgerQueue)) + + lcm, err = bsb.GetLedger(ctx, uint32(5)) + assert.NoError(t, err) + assert.Equal(t, lcmArray[2], lcm) + + // Buffer should only have the final ledger + assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) == 1 }, time.Second*5, time.Millisecond*50) + assert.Equal(t, 1, len(bsb.ledgerBuffer.ledgerQueue)) + + lcm, err = bsb.GetLedger(ctx, uint32(6)) + assert.NoError(t, err) + assert.Equal(t, lcmArray[3], lcm) + + // Buffer should be empty + assert.Equal(t, 0, len(bsb.ledgerBuffer.ledgerQueue)) + + mockDataStore.AssertExpectations(t) } diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index ab1891be03..c3d3e13782 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -12,7 +12,6 @@ import ( "github.com/stellar/go/support/collections/heap" "github.com/stellar/go/support/compressxdr" "github.com/stellar/go/support/datastore" - "github.com/stellar/go/support/log" ) type ledgerBatchObject struct { @@ -21,13 +20,15 @@ type ledgerBatchObject struct { } type ledgerBuffer struct { - // passed through from BufferedStorageBackend to control lifetime of ledgerBuffer instance + // Passed through from BufferedStorageBackend to control lifetime of ledgerBuffer instance config BufferedStorageBackendConfig dataStore datastore.DataStore - context context.Context - cancel context.CancelCauseFunc decoder compressxdr.XDRDecoder + // context used to cancel workers within the ledgerBuffer + context context.Context + cancel context.CancelCauseFunc + // The pipes and data structures below help establish the ledgerBuffer invariant which is // the number of tasks (both pending and in-flight) + len(ledgerQueue) + ledgerPriorityQueue.Len() // is always less than or equal to the config.BufferSize @@ -36,10 +37,7 @@ type ledgerBuffer struct { ledgerPriorityQueue *heap.Heap[ledgerBatchObject] // Priority is set to the sequence number priorityQueueLock sync.Mutex - // done is used to signal the closure of the ledgerBuffer and workers - done chan struct{} - - // keep track of the ledgers to be processed and the next ordering + // Keep track of the ledgers to be processed and the next ordering // the ledgers should be buffered currentLedger uint32 // The current ledger that should be popped from ledgerPriorityQueue nextTaskLedger uint32 // The next task ledger that should be added to taskQueue @@ -48,31 +46,30 @@ type ledgerBuffer struct { } func (bsb *BufferedStorageBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBuffer, error) { + ctx, cancel := context.WithCancelCause(context.Background()) + less := func(a, b ledgerBatchObject) bool { return a.startLedger < b.startLedger } pq := heap.New(less, int(bsb.config.BufferSize)) - done := make(chan struct{}) - ledgerBuffer := &ledgerBuffer{ config: bsb.config, dataStore: bsb.dataStore, taskQueue: make(chan uint32, bsb.config.BufferSize), ledgerQueue: make(chan []byte, bsb.config.BufferSize), ledgerPriorityQueue: pq, - done: done, currentLedger: ledgerRange.from, nextTaskLedger: ledgerRange.from, ledgerRange: ledgerRange, - context: bsb.context, - cancel: bsb.cancel, + context: ctx, + cancel: cancel, decoder: bsb.decoder, } // Start workers to read LCM files for i := uint32(0); i < bsb.config.NumWorkers; i++ { - go ledgerBuffer.worker() + go ledgerBuffer.worker(ctx) } // Upon initialization, the ledgerBuffer invariant is maintained because @@ -98,18 +95,14 @@ func (lb *ledgerBuffer) pushTaskQueue() { lb.nextTaskLedger += lb.config.LedgerBatchConfig.LedgersPerFile } -func (lb *ledgerBuffer) worker() { +func (lb *ledgerBuffer) worker(ctx context.Context) { for { select { - case <-lb.done: - log.Error("abort: getFromLedgerQueue blocked") - return - case <-lb.context.Done(): - log.Error(lb.context.Err()) + case <-ctx.Done(): return case sequence := <-lb.taskQueue: for retryCount := uint32(0); retryCount <= lb.config.RetryLimit; { - ledgerObject, err := lb.downloadLedgerObject(sequence) + ledgerObject, err := lb.downloadLedgerObject(lb.context, sequence) if err != nil { if errors.Is(err, os.ErrNotExist) { // ledgerObject not found and unbounded @@ -141,10 +134,10 @@ func (lb *ledgerBuffer) worker() { } } -func (lb *ledgerBuffer) downloadLedgerObject(sequence uint32) ([]byte, error) { +func (lb *ledgerBuffer) downloadLedgerObject(ctx context.Context, sequence uint32) ([]byte, error) { objectKey := lb.config.LedgerBatchConfig.GetObjectKeyFromSequenceNumber(sequence) - reader, err := lb.dataStore.GetFile(context.Background(), objectKey) + reader, err := lb.dataStore.GetFile(ctx, objectKey) if err != nil { return nil, err } @@ -181,13 +174,13 @@ func (lb *ledgerBuffer) storeObject(ledgerObject []byte, sequence uint32) { } } -func (lb *ledgerBuffer) getFromLedgerQueue() ([]byte, error) { +func (lb *ledgerBuffer) getFromLedgerQueue(ctx context.Context) ([]byte, error) { for { select { case <-lb.context.Done(): - log.Info("Stopping getFromLedgerQueue due to context cancellation") - close(lb.done) return nil, lb.context.Err() + case <-ctx.Done(): + return nil, ctx.Err() case compressedBinary := <-lb.ledgerQueue: // The ledger buffer invariant is maintained here because // we create an extra task when consuming one item from the ledger queue. @@ -218,3 +211,7 @@ func (lb *ledgerBuffer) getLatestLedgerSequence() (uint32, error) { // Subtract 1 to get the latest ledger in buffer return lb.currentLedger - 1, nil } + +func (lb *ledgerBuffer) close() { + lb.cancel(context.Canceled) +} From e1e83c934180ee47dd83c224361a8e05ef6d8b7a Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Mon, 6 May 2024 16:42:42 -0400 Subject: [PATCH 153/234] add test cast --- ingest/ledgerbackend/buffered_storage_backend.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ingest/ledgerbackend/buffered_storage_backend.go b/ingest/ledgerbackend/buffered_storage_backend.go index 726e4a2d4d..ea929d502d 100644 --- a/ingest/ledgerbackend/buffered_storage_backend.go +++ b/ingest/ledgerbackend/buffered_storage_backend.go @@ -5,7 +5,6 @@ package ledgerbackend import ( "context" - "fmt" "sync" "time" @@ -175,8 +174,12 @@ func (bsb *BufferedStorageBackend) GetLedger(ctx context.Context, sequence uint3 } } + if sequence < bsb.lastLedger { + return xdr.LedgerCloseMeta{}, errors.New("requested sequence preceeds the lastLedger") + } + if sequence > bsb.nextExpectedSequence() { - return xdr.LedgerCloseMeta{}, fmt.Errorf("requested sequence is not the lastLedger (%d) nor the next available ledger (%d)", bsb.lastLedger, bsb.nextLedger) + return xdr.LedgerCloseMeta{}, errors.New("requested sequence is not the lastLedger nor the next available ledger") } err := bsb.getBatchForSequence(ctx, sequence) From 34b81ae429fae22955b87903d3ea198a105a3d63 Mon Sep 17 00:00:00 2001 From: chowbao Date: Mon, 6 May 2024 18:05:07 -0400 Subject: [PATCH 154/234] Update ingest/ledgerbackend/ledger_buffer.go Co-authored-by: tamirms --- ingest/ledgerbackend/ledger_buffer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index c3d3e13782..559e69cc0f 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -102,7 +102,7 @@ func (lb *ledgerBuffer) worker(ctx context.Context) { return case sequence := <-lb.taskQueue: for retryCount := uint32(0); retryCount <= lb.config.RetryLimit; { - ledgerObject, err := lb.downloadLedgerObject(lb.context, sequence) + ledgerObject, err := lb.downloadLedgerObject(ctx, sequence) if err != nil { if errors.Is(err, os.ErrNotExist) { // ledgerObject not found and unbounded From d7b47044ef43723b15dea0e6a45942a0fe357157 Mon Sep 17 00:00:00 2001 From: chowbao Date: Mon, 6 May 2024 18:07:39 -0400 Subject: [PATCH 155/234] Update ingest/ledgerbackend/ledger_buffer.go Co-authored-by: tamirms --- ingest/ledgerbackend/ledger_buffer.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index 559e69cc0f..734a5bd9b2 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -74,8 +74,8 @@ func (bsb *BufferedStorageBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBu // Upon initialization, the ledgerBuffer invariant is maintained because // we create bsb.config.BufferSize tasks while the len(ledgerQueue) and ledgerPriorityQueue.Len() are 0. - // Effectively, this is len(taskQueue) + len(ledgerQueue) + ledgerPriorityQueue.Len() == bsb.config.BufferSize - // which enforces a limit of max tasks (both pending and in-flight) to be equal to bsb.config.BufferSize. + // Effectively, this is len(taskQueue) + len(ledgerQueue) + ledgerPriorityQueue.Len() <= bsb.config.BufferSize + // which enforces a limit of max tasks (both pending and in-flight) to be less than or equal to bsb.config.BufferSize. // Note: when a task is in-flight it is no longer in the taskQueue // but for easier conceptualization, len(taskQueue) can be interpreted as both pending and in-flight tasks // where we assume the workers are empty and not processing any tasks. From acd57df7f9fc7350e3ebbb722e8695fe31c05315 Mon Sep 17 00:00:00 2001 From: chowbao Date: Mon, 6 May 2024 18:07:49 -0400 Subject: [PATCH 156/234] Update ingest/ledgerbackend/ledger_buffer.go Co-authored-by: tamirms --- ingest/ledgerbackend/ledger_buffer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index 734a5bd9b2..36a493566b 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -186,7 +186,7 @@ func (lb *ledgerBuffer) getFromLedgerQueue(ctx context.Context) ([]byte, error) // we create an extra task when consuming one item from the ledger queue. // Thus len(ledgerQueue) decreases by 1 and the number of tasks increases by 1. // The overall sum below remains the same: - // len(taskQueue) + len(ledgerQueue) + ledgerPriorityQueue.Len() == bsb.config.BufferSize + // len(taskQueue) + len(ledgerQueue) + ledgerPriorityQueue.Len() <= bsb.config.BufferSize lb.pushTaskQueue() reader := bytes.NewReader(compressedBinary) From 9348f2eeaf3c336f21348cbc5528366769b5100c Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Mon, 6 May 2024 18:17:20 -0400 Subject: [PATCH 157/234] updates --- .../ledgerbackend/buffered_storage_backend.go | 40 +++++++++---------- .../buffered_storage_backend_test.go | 12 +++++- ingest/ledgerbackend/ledger_buffer.go | 15 ++++++- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/ingest/ledgerbackend/buffered_storage_backend.go b/ingest/ledgerbackend/buffered_storage_backend.go index ea929d502d..fdfd3e50ad 100644 --- a/ingest/ledgerbackend/buffered_storage_backend.go +++ b/ingest/ledgerbackend/buffered_storage_backend.go @@ -113,30 +113,30 @@ func (bsb *BufferedStorageBackend) GetLatestLedgerSequence(ctx context.Context) // getBatchForSequence checks if the requested sequence is in the cached batch. // Otherwise will continuously load in the next LedgerCloseMetaBatch until found. func (bsb *BufferedStorageBackend) getBatchForSequence(ctx context.Context, sequence uint32) error { - for { - // Sequence inside the current cached LedgerCloseMetaBatch - if sequence >= bsb.ledgerMetaArchive.GetStartLedgerSequence() && sequence <= bsb.ledgerMetaArchive.GetEndLedgerSequence() { - return nil - } + // Sequence inside the current cached LedgerCloseMetaBatch + if sequence >= bsb.ledgerMetaArchive.GetStartLedgerSequence() && sequence <= bsb.ledgerMetaArchive.GetEndLedgerSequence() { + return nil + } - // Sequence is before the current LedgerCloseMetaBatch - // Does not support retrieving LedgerCloseMeta before the current cached batch - if sequence < bsb.ledgerMetaArchive.GetStartLedgerSequence() { - return errors.New("requested sequence preceeds current LedgerCloseMetaBatch") - } + // Sequence is before the current LedgerCloseMetaBatch + // Does not support retrieving LedgerCloseMeta before the current cached batch + if sequence < bsb.ledgerMetaArchive.GetStartLedgerSequence() { + return errors.New("requested sequence preceeds current LedgerCloseMetaBatch") + } - // Sequence is beyond the current LedgerCloseMetaBatch - lcmBatchBinary, err := bsb.ledgerBuffer.getFromLedgerQueue(ctx) - if err != nil { - return errors.Wrap(err, "failed getting next ledger batch from queue") - } + // Sequence is beyond the current LedgerCloseMetaBatch + lcmBatchBinary, err := bsb.ledgerBuffer.getFromLedgerQueue(ctx) + if err != nil { + return errors.Wrap(err, "failed getting next ledger batch from queue") + } - // Turn binary into xdr - err = bsb.ledgerMetaArchive.Data.UnmarshalBinary(lcmBatchBinary) - if err != nil { - return errors.Wrap(err, "failed unmarshalling lcmBatchBinary") - } + // Turn binary into xdr + err = bsb.ledgerMetaArchive.Data.UnmarshalBinary(lcmBatchBinary) + if err != nil { + return errors.Wrap(err, "failed unmarshalling lcmBatchBinary") } + + return nil } // nextExpectedSequence returns nextLedger (if currently set) or start of diff --git a/ingest/ledgerbackend/buffered_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go index 243f42ea3c..2bb1ad942b 100644 --- a/ingest/ledgerbackend/buffered_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -123,15 +123,23 @@ func TestNewBufferedStorageBackend(t *testing.T) { } func TestNewLedgerBuffer(t *testing.T) { + startLedger := uint32(2) + endLedger := uint32(3) bsb := createBufferedStorageBackendForTesting() - ledgerRange := BoundedRange(2, 3) + ledgerRange := BoundedRange(startLedger, endLedger) + mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + bsb.dataStore = mockDataStore ledgerBuffer, err := bsb.newLedgerBuffer(ledgerRange) + assert.Eventually(t, func() bool { return len(ledgerBuffer.ledgerQueue) == 2 }, time.Second*5, time.Millisecond*50) assert.NoError(t, err) - assert.Equal(t, uint32(2), ledgerBuffer.currentLedger) + // values should be the ledger following ledgerRange.to + assert.Equal(t, uint32(4), ledgerBuffer.currentLedger) assert.Equal(t, uint32(4), ledgerBuffer.nextTaskLedger) assert.Equal(t, ledgerRange, ledgerBuffer.ledgerRange) + + mockDataStore.AssertExpectations(t) } func TestBSBGetLatestLedgerSequence(t *testing.T) { diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index 36a493566b..6462b3063a 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -95,6 +95,17 @@ func (lb *ledgerBuffer) pushTaskQueue() { lb.nextTaskLedger += lb.config.LedgerBatchConfig.LedgersPerFile } +func (lb *ledgerBuffer) sleepWithContext(ctx context.Context, d time.Duration) { + timer := time.NewTimer(d) + select { + case <-ctx.Done(): + if !timer.Stop() { + <-timer.C + } + case <-timer.C: + } +} + func (lb *ledgerBuffer) worker(ctx context.Context) { for { select { @@ -107,7 +118,7 @@ func (lb *ledgerBuffer) worker(ctx context.Context) { if errors.Is(err, os.ErrNotExist) { // ledgerObject not found and unbounded if !lb.ledgerRange.bounded { - time.Sleep(lb.config.RetryWait * time.Second) + lb.sleepWithContext(ctx, lb.config.RetryWait*time.Second) continue } lb.cancel(err) @@ -119,7 +130,7 @@ func (lb *ledgerBuffer) worker(ctx context.Context) { return } retryCount++ - time.Sleep(lb.config.RetryWait * time.Second) + lb.sleepWithContext(ctx, lb.config.RetryWait*time.Second) } // When we store an object we still maintain the ledger buffer invariant because From 17245593049d06d0a4986e6fe936f76b4406754a Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Mon, 6 May 2024 18:51:05 -0400 Subject: [PATCH 158/234] Update unbounded test --- .../ledgerbackend/buffered_storage_backend_test.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ingest/ledgerbackend/buffered_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go index 2bb1ad942b..3f97f48128 100644 --- a/ingest/ledgerbackend/buffered_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -132,6 +132,8 @@ func TestNewLedgerBuffer(t *testing.T) { ledgerBuffer, err := bsb.newLedgerBuffer(ledgerRange) assert.Eventually(t, func() bool { return len(ledgerBuffer.ledgerQueue) == 2 }, time.Second*5, time.Millisecond*50) + assert.Eventually(t, func() bool { return len(ledgerBuffer.taskQueue) == 0 }, time.Second*5, time.Millisecond*50) + assert.Eventually(t, func() bool { return ledgerBuffer.ledgerPriorityQueue.Len() == 0 }, time.Second*5, time.Millisecond*50) assert.NoError(t, err) // values should be the ledger following ledgerRange.to @@ -321,17 +323,18 @@ func TestBSBIsPrepared_Bounded(t *testing.T) { } func TestBSBIsPrepared_Unbounded(t *testing.T) { + startLedger := uint32(3) + endLedger := uint32(8) ctx := context.Background() bsb := createBufferedStorageBackendForTesting() + bsb.config.NumWorkers = 2 + bsb.config.BufferSize = 5 ledgerRange := UnboundedRange(3) - - readCloser := createLCMBatchReader(3, 3, 1) - mockDataStore := new(datastore.MockDataStore) - mockDataStore.On("GetFile", mock.Anything, mock.Anything).Return(readCloser, nil) + mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) - assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) > 3 }, time.Second*5, time.Millisecond*50) + assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) == 5 }, time.Second*5, time.Millisecond*50) ok, err := bsb.IsPrepared(ctx, ledgerRange) assert.NoError(t, err) From 0311b45f09d2ba04e750edde87ef911b9c0a22ee Mon Sep 17 00:00:00 2001 From: Simon Chow Date: Mon, 6 May 2024 19:11:37 -0400 Subject: [PATCH 159/234] Fix tests --- .../buffered_storage_backend_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ingest/ledgerbackend/buffered_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go index 3f97f48128..7acd81912b 100644 --- a/ingest/ledgerbackend/buffered_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -123,22 +123,22 @@ func TestNewBufferedStorageBackend(t *testing.T) { } func TestNewLedgerBuffer(t *testing.T) { - startLedger := uint32(2) - endLedger := uint32(3) + startLedger := uint32(3) + endLedger := uint32(7) bsb := createBufferedStorageBackendForTesting() + bsb.config.NumWorkers = 2 + bsb.config.BufferSize = 5 ledgerRange := BoundedRange(startLedger, endLedger) mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore ledgerBuffer, err := bsb.newLedgerBuffer(ledgerRange) - assert.Eventually(t, func() bool { return len(ledgerBuffer.ledgerQueue) == 2 }, time.Second*5, time.Millisecond*50) - assert.Eventually(t, func() bool { return len(ledgerBuffer.taskQueue) == 0 }, time.Second*5, time.Millisecond*50) - assert.Eventually(t, func() bool { return ledgerBuffer.ledgerPriorityQueue.Len() == 0 }, time.Second*5, time.Millisecond*50) + assert.Eventually(t, func() bool { return len(ledgerBuffer.ledgerQueue) == 5 }, time.Second*5, time.Millisecond*50) assert.NoError(t, err) - // values should be the ledger following ledgerRange.to - assert.Equal(t, uint32(4), ledgerBuffer.currentLedger) - assert.Equal(t, uint32(4), ledgerBuffer.nextTaskLedger) + latestSeq, err := ledgerBuffer.getLatestLedgerSequence() + assert.NoError(t, err) + assert.Equal(t, uint32(7), latestSeq) assert.Equal(t, ledgerRange, ledgerBuffer.ledgerRange) mockDataStore.AssertExpectations(t) From 046c3efffba0078cfad2046ca57ab2645ba30384 Mon Sep 17 00:00:00 2001 From: tamirms Date: Tue, 7 May 2024 11:20:32 +0100 Subject: [PATCH 160/234] Add tests on ledger buffer termination logic --- .../ledgerbackend/buffered_storage_backend.go | 2 +- .../buffered_storage_backend_test.go | 181 ++++++++++++++---- ingest/ledgerbackend/ledger_buffer.go | 36 +++- 3 files changed, 173 insertions(+), 46 deletions(-) diff --git a/ingest/ledgerbackend/buffered_storage_backend.go b/ingest/ledgerbackend/buffered_storage_backend.go index fdfd3e50ad..b28e68b63c 100644 --- a/ingest/ledgerbackend/buffered_storage_backend.go +++ b/ingest/ledgerbackend/buffered_storage_backend.go @@ -47,7 +47,7 @@ type BufferedStorageBackend struct { lastLedger uint32 } -// Return a new BufferedStorageBackend instance. +// NewBufferedStorageBackend returns a new BufferedStorageBackend instance. func NewBufferedStorageBackend(ctx context.Context, config BufferedStorageBackendConfig) (*BufferedStorageBackend, error) { if config.BufferSize == 0 { return nil, errors.New("buffer size must be > 0") diff --git a/ingest/ledgerbackend/buffered_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go index 7acd81912b..6576050867 100644 --- a/ingest/ledgerbackend/buffered_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -5,14 +5,17 @@ import ( "context" "fmt" "io" + "os" + "sync/atomic" "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stellar/go/support/compressxdr" "github.com/stellar/go/support/datastore" "github.com/stellar/go/xdr" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" ) var partitionSize = uint32(64000) @@ -37,7 +40,7 @@ func createBufferedStorageBackendConfigForTesting() BufferedStorageBackendConfig BufferSize: 100, NumWorkers: 5, RetryLimit: 3, - RetryWait: 1, + RetryWait: time.Microsecond, } } @@ -54,7 +57,7 @@ func createBufferedStorageBackendForTesting() BufferedStorageBackend { } } -func createMockdataStore(start, end, partitionSize, count uint32) *datastore.MockDataStore { +func createMockdataStore(t *testing.T, start, end, partitionSize, count uint32) *datastore.MockDataStore { mockDataStore := new(datastore.MockDataStore) partition := count*partitionSize - 1 for i := start; i <= end; i = i + count { @@ -71,6 +74,10 @@ func createMockdataStore(start, end, partitionSize, count uint32) *datastore.Moc mockDataStore.On("GetFile", mock.Anything, objectName).Return(readCloser, nil) } + t.Cleanup(func() { + mockDataStore.AssertExpectations(t) + }) + return mockDataStore } @@ -119,7 +126,7 @@ func TestNewBufferedStorageBackend(t *testing.T) { assert.Equal(t, uint32(100), bsb.config.BufferSize) assert.Equal(t, uint32(5), bsb.config.NumWorkers) assert.Equal(t, uint32(3), bsb.config.RetryLimit) - assert.Equal(t, time.Duration(1), bsb.config.RetryWait) + assert.Equal(t, time.Microsecond, bsb.config.RetryWait) } func TestNewLedgerBuffer(t *testing.T) { @@ -129,7 +136,7 @@ func TestNewLedgerBuffer(t *testing.T) { bsb.config.NumWorkers = 2 bsb.config.BufferSize = 5 ledgerRange := BoundedRange(startLedger, endLedger) - mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + mockDataStore := createMockdataStore(t, startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore ledgerBuffer, err := bsb.newLedgerBuffer(ledgerRange) @@ -140,8 +147,6 @@ func TestNewLedgerBuffer(t *testing.T) { assert.NoError(t, err) assert.Equal(t, uint32(7), latestSeq) assert.Equal(t, ledgerRange, ledgerBuffer.ledgerRange) - - mockDataStore.AssertExpectations(t) } func TestBSBGetLatestLedgerSequence(t *testing.T) { @@ -150,7 +155,7 @@ func TestBSBGetLatestLedgerSequence(t *testing.T) { ctx := context.Background() bsb := createBufferedStorageBackendForTesting() ledgerRange := BoundedRange(startLedger, endLedger) - mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + mockDataStore := createMockdataStore(t, startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) @@ -160,8 +165,6 @@ func TestBSBGetLatestLedgerSequence(t *testing.T) { assert.NoError(t, err) assert.Equal(t, uint32(5), latestSeq) - - mockDataStore.AssertExpectations(t) } func TestBSBGetLedger_SingleLedgerPerFile(t *testing.T) { @@ -172,7 +175,7 @@ func TestBSBGetLedger_SingleLedgerPerFile(t *testing.T) { bsb := createBufferedStorageBackendForTesting() ledgerRange := BoundedRange(startLedger, endLedger) - mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + mockDataStore := createMockdataStore(t, startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) @@ -187,8 +190,6 @@ func TestBSBGetLedger_SingleLedgerPerFile(t *testing.T) { lcm, err = bsb.GetLedger(ctx, uint32(5)) assert.NoError(t, err) assert.Equal(t, lcmArray[2], lcm) - - mockDataStore.AssertExpectations(t) } func TestCloudStorageGetLedger_MultipleLedgerPerFile(t *testing.T) { @@ -200,7 +201,7 @@ func TestCloudStorageGetLedger_MultipleLedgerPerFile(t *testing.T) { bsb.config.LedgerBatchConfig.LedgersPerFile = uint32(2) ledgerRange := BoundedRange(startLedger, endLedger) - mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, 2) + mockDataStore := createMockdataStore(t, startLedger, endLedger, partitionSize, 2) bsb.dataStore = mockDataStore assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) @@ -215,8 +216,6 @@ func TestCloudStorageGetLedger_MultipleLedgerPerFile(t *testing.T) { lcm, err = bsb.GetLedger(ctx, uint32(4)) assert.NoError(t, err) assert.Equal(t, lcmArray[2], lcm) - - mockDataStore.AssertExpectations(t) } func TestBSBGetLedger_ErrorPreceedingLedger(t *testing.T) { @@ -227,7 +226,7 @@ func TestBSBGetLedger_ErrorPreceedingLedger(t *testing.T) { bsb := createBufferedStorageBackendForTesting() ledgerRange := BoundedRange(startLedger, endLedger) - mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + mockDataStore := createMockdataStore(t, startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) @@ -238,9 +237,7 @@ func TestBSBGetLedger_ErrorPreceedingLedger(t *testing.T) { assert.Equal(t, lcmArray[0], lcm) _, err = bsb.GetLedger(ctx, uint32(2)) - assert.Error(t, err, "requested sequence preceeds current LedgerCloseMetaBatch") - - mockDataStore.AssertExpectations(t) + assert.EqualError(t, err, "requested sequence preceeds current LedgerRange") } func TestBSBGetLedger_NotPrepared(t *testing.T) { @@ -248,7 +245,7 @@ func TestBSBGetLedger_NotPrepared(t *testing.T) { ctx := context.Background() _, err := bsb.GetLedger(ctx, uint32(3)) - assert.Error(t, err, "session is not prepared, call PrepareRange first") + assert.EqualError(t, err, "session is not prepared, call PrepareRange first") } func TestBSBGetLedger_SequenceNotInBatch(t *testing.T) { @@ -258,17 +255,17 @@ func TestBSBGetLedger_SequenceNotInBatch(t *testing.T) { bsb := createBufferedStorageBackendForTesting() ledgerRange := BoundedRange(startLedger, endLedger) - mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + mockDataStore := createMockdataStore(t, startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) == 3 }, time.Second*5, time.Millisecond*50) _, err := bsb.GetLedger(ctx, uint32(2)) - assert.Error(t, err, "requested sequence preceeds current LedgerRange") + assert.EqualError(t, err, "requested sequence preceeds current LedgerRange") _, err = bsb.GetLedger(ctx, uint32(6)) - assert.Error(t, err, "requested sequence beyond current LedgerRange") + assert.EqualError(t, err, "requested sequence beyond current LedgerRange") } func TestBSBPrepareRange(t *testing.T) { @@ -278,7 +275,7 @@ func TestBSBPrepareRange(t *testing.T) { bsb := createBufferedStorageBackendForTesting() ledgerRange := BoundedRange(startLedger, endLedger) - mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + mockDataStore := createMockdataStore(t, startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) @@ -299,7 +296,7 @@ func TestBSBIsPrepared_Bounded(t *testing.T) { bsb := createBufferedStorageBackendForTesting() ledgerRange := BoundedRange(startLedger, endLedger) - mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + mockDataStore := createMockdataStore(t, startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) @@ -330,7 +327,7 @@ func TestBSBIsPrepared_Unbounded(t *testing.T) { bsb.config.NumWorkers = 2 bsb.config.BufferSize = 5 ledgerRange := UnboundedRange(3) - mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + mockDataStore := createMockdataStore(t, startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) @@ -364,7 +361,7 @@ func TestBSBClose(t *testing.T) { bsb := createBufferedStorageBackendForTesting() ledgerRange := BoundedRange(startLedger, endLedger) - mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + mockDataStore := createMockdataStore(t, startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) @@ -375,18 +372,16 @@ func TestBSBClose(t *testing.T) { assert.Equal(t, true, bsb.closed) _, err = bsb.GetLatestLedgerSequence(ctx) - assert.Error(t, err, "gbsbackend is closed; cannot GetLatestLedgerSequence") + assert.EqualError(t, err, "BufferedStorageBackend is closed; cannot GetLatestLedgerSequence") _, err = bsb.GetLedger(ctx, 3) - assert.Error(t, err, "gbsbackend is closed; cannot GetLedger") + assert.EqualError(t, err, "BufferedStorageBackend is closed; cannot GetLedger") err = bsb.PrepareRange(ctx, ledgerRange) - assert.Error(t, err, "gbsbackend is closed; cannot PrepareRange") + assert.EqualError(t, err, "BufferedStorageBackend is closed; cannot PrepareRange") _, err = bsb.IsPrepared(ctx, ledgerRange) - assert.Error(t, err, "gbsbackend is closed; cannot IsPrepared") - - mockDataStore.AssertExpectations(t) + assert.EqualError(t, err, "BufferedStorageBackend is closed; cannot IsPrepared") } func TestLedgerBufferInvariant(t *testing.T) { @@ -399,7 +394,7 @@ func TestLedgerBufferInvariant(t *testing.T) { bsb.config.BufferSize = 2 ledgerRange := BoundedRange(startLedger, endLedger) - mockDataStore := createMockdataStore(startLedger, endLedger, partitionSize, ledgerPerFileCount) + mockDataStore := createMockdataStore(t, startLedger, endLedger, partitionSize, ledgerPerFileCount) bsb.dataStore = mockDataStore assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) @@ -433,6 +428,118 @@ func TestLedgerBufferInvariant(t *testing.T) { // Buffer should be empty assert.Equal(t, 0, len(bsb.ledgerBuffer.ledgerQueue)) +} + +func TestLedgerBufferClose(t *testing.T) { + ctx := context.Background() + bsb := createBufferedStorageBackendForTesting() + bsb.config.NumWorkers = 1 + bsb.config.BufferSize = 5 + ledgerRange := UnboundedRange(3) + + mockDataStore := new(datastore.MockDataStore) + partition := ledgerPerFileCount*partitionSize - 1 + + objectName := fmt.Sprintf("0-%d/%d.xdr.gz", partition, 3) + mockDataStore.On("GetFile", mock.Anything, objectName).Return(io.NopCloser(&bytes.Buffer{}), context.Canceled).Run(func(args mock.Arguments) { + go bsb.ledgerBuffer.close() + }).Once() + t.Cleanup(func() { + mockDataStore.AssertExpectations(t) + }) + + bsb.dataStore = mockDataStore + + assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) + + bsb.ledgerBuffer.wg.Wait() + + _, err := bsb.GetLedger(ctx, 3) + assert.EqualError(t, err, "failed getting next ledger batch from queue: context canceled") +} + +func TestLedgerBufferBoundedObjectNotFound(t *testing.T) { + ctx := context.Background() + bsb := createBufferedStorageBackendForTesting() + bsb.config.NumWorkers = 1 + bsb.config.BufferSize = 5 + ledgerRange := BoundedRange(3, 5) + + mockDataStore := new(datastore.MockDataStore) + partition := ledgerPerFileCount*partitionSize - 1 + + objectName := fmt.Sprintf("0-%d/%d.xdr.gz", partition, 3) + mockDataStore.On("GetFile", mock.Anything, objectName).Return(io.NopCloser(&bytes.Buffer{}), os.ErrNotExist).Once() + t.Cleanup(func() { + mockDataStore.AssertExpectations(t) + }) + + bsb.dataStore = mockDataStore + + assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) + + bsb.ledgerBuffer.wg.Wait() + + _, err := bsb.GetLedger(ctx, 3) + assert.EqualError(t, err, "failed getting next ledger batch from queue: ledger object containing sequence 3 is missing: file does not exist") +} + +func TestLedgerBufferUnboundedObjectNotFound(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + bsb := createBufferedStorageBackendForTesting() + bsb.config.NumWorkers = 1 + bsb.config.BufferSize = 5 + ledgerRange := UnboundedRange(3) + + mockDataStore := new(datastore.MockDataStore) + partition := ledgerPerFileCount*partitionSize - 1 + + objectName := fmt.Sprintf("0-%d/%d.xdr.gz", partition, 3) + iteration := &atomic.Int32{} + cancelAfter := int32(bsb.config.RetryLimit) + 2 + mockDataStore.On("GetFile", mock.Anything, objectName).Return(io.NopCloser(&bytes.Buffer{}), os.ErrNotExist).Run(func(args mock.Arguments) { + if iteration.Load() >= cancelAfter { + cancel() + } + iteration.Add(1) + }) + t.Cleanup(func() { + mockDataStore.AssertExpectations(t) + }) + + bsb.dataStore = mockDataStore + + assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) + + _, err := bsb.GetLedger(ctx, 3) + assert.EqualError(t, err, "failed getting next ledger batch from queue: context canceled") + assert.GreaterOrEqual(t, iteration.Load(), cancelAfter) + assert.NoError(t, bsb.Close()) +} + +func TestLedgerBufferRetryLimit(t *testing.T) { + bsb := createBufferedStorageBackendForTesting() + bsb.config.NumWorkers = 1 + bsb.config.BufferSize = 5 + ledgerRange := UnboundedRange(3) + + mockDataStore := new(datastore.MockDataStore) + partition := ledgerPerFileCount*partitionSize - 1 + + objectName := fmt.Sprintf("0-%d/%d.xdr.gz", partition, 3) + mockDataStore.On("GetFile", mock.Anything, objectName). + Return(io.NopCloser(&bytes.Buffer{}), fmt.Errorf("transient error")). + Times(int(bsb.config.RetryLimit) + 1) + t.Cleanup(func() { + mockDataStore.AssertExpectations(t) + }) + + bsb.dataStore = mockDataStore + + assert.NoError(t, bsb.PrepareRange(context.Background(), ledgerRange)) + + bsb.ledgerBuffer.wg.Wait() - mockDataStore.AssertExpectations(t) + _, err := bsb.GetLedger(context.Background(), 3) + assert.EqualError(t, err, "failed getting next ledger batch from queue: maximum retries exceeded for object reads: transient error") } diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index 6462b3063a..21e14bb10f 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -9,6 +9,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/stellar/go/support/collections/heap" "github.com/stellar/go/support/compressxdr" "github.com/stellar/go/support/datastore" @@ -29,6 +30,8 @@ type ledgerBuffer struct { context context.Context cancel context.CancelCauseFunc + wg sync.WaitGroup + // The pipes and data structures below help establish the ledgerBuffer invariant which is // the number of tasks (both pending and in-flight) + len(ledgerQueue) + ledgerPriorityQueue.Len() // is always less than or equal to the config.BufferSize @@ -68,6 +71,7 @@ func (bsb *BufferedStorageBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBu } // Start workers to read LCM files + ledgerBuffer.wg.Add(int(bsb.config.NumWorkers)) for i := uint32(0); i < bsb.config.NumWorkers; i++ { go ledgerBuffer.worker(ctx) } @@ -95,42 +99,56 @@ func (lb *ledgerBuffer) pushTaskQueue() { lb.nextTaskLedger += lb.config.LedgerBatchConfig.LedgersPerFile } -func (lb *ledgerBuffer) sleepWithContext(ctx context.Context, d time.Duration) { +// sleepWithContext returns true upon sleeping without interruption from the context +func (lb *ledgerBuffer) sleepWithContext(ctx context.Context, d time.Duration) bool { timer := time.NewTimer(d) select { case <-ctx.Done(): if !timer.Stop() { <-timer.C } + return false case <-timer.C: } + return true } func (lb *ledgerBuffer) worker(ctx context.Context) { + defer lb.wg.Done() + for { select { case <-ctx.Done(): return case sequence := <-lb.taskQueue: - for retryCount := uint32(0); retryCount <= lb.config.RetryLimit; { + for attempt := uint32(0); attempt <= lb.config.RetryLimit; { ledgerObject, err := lb.downloadLedgerObject(ctx, sequence) if err != nil { if errors.Is(err, os.ErrNotExist) { // ledgerObject not found and unbounded if !lb.ledgerRange.bounded { - lb.sleepWithContext(ctx, lb.config.RetryWait*time.Second) + if !lb.sleepWithContext(ctx, lb.config.RetryWait) { + return + } continue } - lb.cancel(err) + lb.cancel(errors.Wrapf(err, "ledger object containing sequence %v is missing", sequence)) return } - if retryCount == lb.config.RetryLimit { + // don't bother retrying if we've received the signal to shut down + if errors.Is(err, context.Canceled) { + return + } + if attempt == lb.config.RetryLimit { err = errors.Wrap(err, "maximum retries exceeded for object reads") lb.cancel(err) return } - retryCount++ - lb.sleepWithContext(ctx, lb.config.RetryWait*time.Second) + attempt++ + if !lb.sleepWithContext(ctx, lb.config.RetryWait) { + return + } + continue } // When we store an object we still maintain the ledger buffer invariant because @@ -189,7 +207,7 @@ func (lb *ledgerBuffer) getFromLedgerQueue(ctx context.Context) ([]byte, error) for { select { case <-lb.context.Done(): - return nil, lb.context.Err() + return nil, context.Cause(lb.context) case <-ctx.Done(): return nil, ctx.Err() case compressedBinary := <-lb.ledgerQueue: @@ -225,4 +243,6 @@ func (lb *ledgerBuffer) getLatestLedgerSequence() (uint32, error) { func (lb *ledgerBuffer) close() { lb.cancel(context.Canceled) + // wait for all workers to finish terminating + lb.wg.Wait() } From 010e25464da254a9bd8ba56137f1a03a6b0a2ca5 Mon Sep 17 00:00:00 2001 From: tamirms Date: Tue, 7 May 2024 11:24:14 +0100 Subject: [PATCH 161/234] make error message more descriptive --- ingest/ledgerbackend/buffered_storage_backend_test.go | 2 +- ingest/ledgerbackend/ledger_buffer.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ingest/ledgerbackend/buffered_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go index 6576050867..af17bcfdff 100644 --- a/ingest/ledgerbackend/buffered_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -541,5 +541,5 @@ func TestLedgerBufferRetryLimit(t *testing.T) { bsb.ledgerBuffer.wg.Wait() _, err := bsb.GetLedger(context.Background(), 3) - assert.EqualError(t, err, "failed getting next ledger batch from queue: maximum retries exceeded for object reads: transient error") + assert.EqualError(t, err, "failed getting next ledger batch from queue: maximum retries exceeded for downloading object containing sequence 3: transient error") } diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index 21e14bb10f..a26aab8ba2 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -140,7 +140,7 @@ func (lb *ledgerBuffer) worker(ctx context.Context) { return } if attempt == lb.config.RetryLimit { - err = errors.Wrap(err, "maximum retries exceeded for object reads") + err = errors.Wrapf(err, "maximum retries exceeded for downloading object containing sequence %v", sequence) lb.cancel(err) return } From 241c802981b71c36c0be1d069b087477424f4338 Mon Sep 17 00:00:00 2001 From: tamirms Date: Tue, 7 May 2024 11:31:02 +0100 Subject: [PATCH 162/234] fix data race in TestLedgerBufferClose --- ingest/ledgerbackend/buffered_storage_backend_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ingest/ledgerbackend/buffered_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go index af17bcfdff..f132bc9419 100644 --- a/ingest/ledgerbackend/buffered_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -441,9 +441,12 @@ func TestLedgerBufferClose(t *testing.T) { partition := ledgerPerFileCount*partitionSize - 1 objectName := fmt.Sprintf("0-%d/%d.xdr.gz", partition, 3) + afterPrepareRange := make(chan struct{}) mockDataStore.On("GetFile", mock.Anything, objectName).Return(io.NopCloser(&bytes.Buffer{}), context.Canceled).Run(func(args mock.Arguments) { + <-afterPrepareRange go bsb.ledgerBuffer.close() }).Once() + t.Cleanup(func() { mockDataStore.AssertExpectations(t) }) @@ -451,6 +454,7 @@ func TestLedgerBufferClose(t *testing.T) { bsb.dataStore = mockDataStore assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) + close(afterPrepareRange) bsb.ledgerBuffer.wg.Wait() From 33f7d2d91ee0cb5d010e2bda0e936095f807e1f5 Mon Sep 17 00:00:00 2001 From: tamirms Date: Tue, 7 May 2024 11:41:12 +0100 Subject: [PATCH 163/234] configure STELLAR_CORE_VERSION for ledger-exporter CI job --- .github/workflows/horizon.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index 5ec7c934a5..ddf0da7f11 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -154,6 +154,8 @@ jobs: ledger-exporter: name: Test and push the Ledger Exporter images runs-on: ubuntu-latest + env: + STELLAR_CORE_VERSION: 21.0.0-1812.rc1.a10329cca.focal steps: - uses: actions/checkout@v3 with: From 18dee5c955a6ae4b39aa4769e185a684864063ad Mon Sep 17 00:00:00 2001 From: chowbao Date: Thu, 9 May 2024 15:55:13 -0400 Subject: [PATCH 164/234] ledgerexporter: Add lexicographical ordering token to filenames (#5305) * ledgerexporter: Add lexicographical ordering token to filenames * Add test * Update support/datastore/ledgerbatch_config_test.go Co-authored-by: tamirms * add zero and max uint32 edge case --------- Co-authored-by: tamirms --- .../internal/exportmanager_test.go | 36 +--------- .../buffered_storage_backend_test.go | 13 ++-- support/datastore/ledgerbatch_config.go | 6 +- support/datastore/ledgerbatch_config_test.go | 67 +++++++++++++++--- support/datastore/resumablemanager_test.go | 68 +++++++++---------- 5 files changed, 105 insertions(+), 85 deletions(-) diff --git a/exp/services/ledgerexporter/internal/exportmanager_test.go b/exp/services/ledgerexporter/internal/exportmanager_test.go index 01e3595373..41a5e6acf8 100644 --- a/exp/services/ledgerexporter/internal/exportmanager_test.go +++ b/exp/services/ledgerexporter/internal/exportmanager_test.go @@ -2,7 +2,6 @@ package ledgerexporter import ( "context" - "fmt" "sync" "testing" "time" @@ -138,35 +137,6 @@ func (s *ExportManagerSuite) TestRunWithCanceledContext() { require.EqualError(s.T(), err, "context canceled") } -func (s *ExportManagerSuite) TestGetObjectKeyFromSequenceNumber() { - testCases := []struct { - filesPerPartition uint32 - ledgerSeq uint32 - ledgersPerFile uint32 - fileSuffix string - expectedKey string - }{ - {0, 5, 1, ".xdr.gz", "5.xdr.gz"}, - {0, 5, 10, ".xdr.gz", "0-9.xdr.gz"}, - {2, 10, 100, ".xdr.gz", "0-199/0-99.xdr.gz"}, - {2, 150, 50, ".xdr.gz", "100-199/150-199.xdr.gz"}, - {2, 300, 200, ".xdr.gz", "0-399/200-399.xdr.gz"}, - {2, 1, 1, ".xdr.gz", "0-1/1.xdr.gz"}, - {4, 10, 100, ".xdr.gz", "0-399/0-99.xdr.gz"}, - {4, 250, 50, ".xdr.gz", "200-399/250-299.xdr.gz"}, - {1, 300, 200, ".xdr.gz", "200-399.xdr.gz"}, - {1, 1, 1, ".xdr.gz", "1.xdr.gz"}, - } - - for _, tc := range testCases { - s.T().Run(fmt.Sprintf("LedgerSeq-%d-LedgersPerFile-%d", tc.ledgerSeq, tc.ledgersPerFile), func(t *testing.T) { - config := datastore.LedgerBatchConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile, FileSuffix: tc.fileSuffix} - key := config.GetObjectKeyFromSequenceNumber(tc.ledgerSeq) - require.Equal(t, tc.expectedKey, key) - }) - } -} - func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10, FileSuffix: ".xdr.gz"} registry := prometheus.NewRegistry() @@ -174,7 +144,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) require.NoError(s.T(), err) - expectedkeys := set.NewSet[string](10) + expectedKeys := set.NewSet[string](10) actualKeys := set.NewSet[string](10) wg := sync.WaitGroup{} @@ -197,12 +167,12 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), datastore.CreateLedgerCloseMeta(i))) key := config.GetObjectKeyFromSequenceNumber(i) - expectedkeys.Add(key) + expectedKeys.Add(key) } queue.Close() wg.Wait() - require.Equal(s.T(), expectedkeys, actualKeys) + require.Equal(s.T(), expectedKeys, actualKeys) } func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { diff --git a/ingest/ledgerbackend/buffered_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go index f132bc9419..eb2e355765 100644 --- a/ingest/ledgerbackend/buffered_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io" + "math" "os" "sync/atomic" "testing" @@ -66,10 +67,10 @@ func createMockdataStore(t *testing.T, start, end, partitionSize, count uint32) if count > 1 { endFileSeq := i + count - 1 readCloser = createLCMBatchReader(i, endFileSeq, count) - objectName = fmt.Sprintf("0-%d/%d-%d.xdr.gz", partition, i, endFileSeq) + objectName = fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d-%d.xdr.gz", partition, math.MaxUint32-i, i, endFileSeq) } else { readCloser = createLCMBatchReader(i, i, count) - objectName = fmt.Sprintf("0-%d/%d.xdr.gz", partition, i) + objectName = fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.gz", partition, math.MaxUint32-i, i) } mockDataStore.On("GetFile", mock.Anything, objectName).Return(readCloser, nil) } @@ -440,7 +441,7 @@ func TestLedgerBufferClose(t *testing.T) { mockDataStore := new(datastore.MockDataStore) partition := ledgerPerFileCount*partitionSize - 1 - objectName := fmt.Sprintf("0-%d/%d.xdr.gz", partition, 3) + objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.gz", partition, math.MaxUint32-3, 3) afterPrepareRange := make(chan struct{}) mockDataStore.On("GetFile", mock.Anything, objectName).Return(io.NopCloser(&bytes.Buffer{}), context.Canceled).Run(func(args mock.Arguments) { <-afterPrepareRange @@ -472,7 +473,7 @@ func TestLedgerBufferBoundedObjectNotFound(t *testing.T) { mockDataStore := new(datastore.MockDataStore) partition := ledgerPerFileCount*partitionSize - 1 - objectName := fmt.Sprintf("0-%d/%d.xdr.gz", partition, 3) + objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.gz", partition, math.MaxUint32-3, 3) mockDataStore.On("GetFile", mock.Anything, objectName).Return(io.NopCloser(&bytes.Buffer{}), os.ErrNotExist).Once() t.Cleanup(func() { mockDataStore.AssertExpectations(t) @@ -498,7 +499,7 @@ func TestLedgerBufferUnboundedObjectNotFound(t *testing.T) { mockDataStore := new(datastore.MockDataStore) partition := ledgerPerFileCount*partitionSize - 1 - objectName := fmt.Sprintf("0-%d/%d.xdr.gz", partition, 3) + objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.gz", partition, math.MaxUint32-3, 3) iteration := &atomic.Int32{} cancelAfter := int32(bsb.config.RetryLimit) + 2 mockDataStore.On("GetFile", mock.Anything, objectName).Return(io.NopCloser(&bytes.Buffer{}), os.ErrNotExist).Run(func(args mock.Arguments) { @@ -530,7 +531,7 @@ func TestLedgerBufferRetryLimit(t *testing.T) { mockDataStore := new(datastore.MockDataStore) partition := ledgerPerFileCount*partitionSize - 1 - objectName := fmt.Sprintf("0-%d/%d.xdr.gz", partition, 3) + objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.gz", partition, math.MaxUint32-3, 3) mockDataStore.On("GetFile", mock.Anything, objectName). Return(io.NopCloser(&bytes.Buffer{}), fmt.Errorf("transient error")). Times(int(bsb.config.RetryLimit) + 1) diff --git a/support/datastore/ledgerbatch_config.go b/support/datastore/ledgerbatch_config.go index eca8bbe737..cd3a0f45bf 100644 --- a/support/datastore/ledgerbatch_config.go +++ b/support/datastore/ledgerbatch_config.go @@ -2,6 +2,7 @@ package datastore import ( "fmt" + "math" ) type LedgerBatchConfig struct { @@ -29,12 +30,13 @@ func (ec LedgerBatchConfig) GetObjectKeyFromSequenceNumber(ledgerSeq uint32) str partitionSize := ec.LedgersPerFile * ec.FilesPerPartition partitionStart := (ledgerSeq / partitionSize) * partitionSize partitionEnd := partitionStart + partitionSize - 1 - objectKey = fmt.Sprintf("%d-%d/", partitionStart, partitionEnd) + + objectKey = fmt.Sprintf("%08X--%d-%d/", math.MaxUint32-partitionStart, partitionStart, partitionEnd) } fileStart := ec.GetSequenceNumberStartBoundary(ledgerSeq) fileEnd := ec.GetSequenceNumberEndBoundary(ledgerSeq) - objectKey += fmt.Sprintf("%d", fileStart) + objectKey += fmt.Sprintf("%08X--%d", math.MaxUint32-fileStart, fileStart) // Multiple ledgers per file if fileStart != fileEnd { diff --git a/support/datastore/ledgerbatch_config_test.go b/support/datastore/ledgerbatch_config_test.go index 9b2e466677..255b5ed070 100644 --- a/support/datastore/ledgerbatch_config_test.go +++ b/support/datastore/ledgerbatch_config_test.go @@ -2,6 +2,9 @@ package datastore import ( "fmt" + "math" + "math/rand" + "sort" "testing" "github.com/stretchr/testify/require" @@ -15,16 +18,16 @@ func TestGetObjectKeyFromSequenceNumber(t *testing.T) { fileSuffix string expectedKey string }{ - {0, 5, 1, ".xdr.gz", "5.xdr.gz"}, - {0, 5, 10, ".xdr.gz", "0-9.xdr.gz"}, - {2, 10, 100, ".xdr.gz", "0-199/0-99.xdr.gz"}, - {2, 150, 50, ".xdr.gz", "100-199/150-199.xdr.gz"}, - {2, 300, 200, ".xdr.gz", "0-399/200-399.xdr.gz"}, - {2, 1, 1, ".xdr.gz", "0-1/1.xdr.gz"}, - {4, 10, 100, ".xdr.gz", "0-399/0-99.xdr.gz"}, - {4, 250, 50, ".xdr.gz", "200-399/250-299.xdr.gz"}, - {1, 300, 200, ".xdr.gz", "200-399.xdr.gz"}, - {1, 1, 1, ".xdr.gz", "1.xdr.gz"}, + {0, 5, 1, ".xdr.gz", "FFFFFFFA--5.xdr.gz"}, + {0, 5, 10, ".xdr.gz", "FFFFFFFF--0-9.xdr.gz"}, + {2, 10, 100, ".xdr.gz", "FFFFFFFF--0-199/FFFFFFFF--0-99.xdr.gz"}, + {2, 150, 50, ".xdr.gz", "FFFFFF9B--100-199/FFFFFF69--150-199.xdr.gz"}, + {2, 300, 200, ".xdr.gz", "FFFFFFFF--0-399/FFFFFF37--200-399.xdr.gz"}, + {2, 1, 1, ".xdr.gz", "FFFFFFFF--0-1/FFFFFFFE--1.xdr.gz"}, + {4, 10, 100, ".xdr.gz", "FFFFFFFF--0-399/FFFFFFFF--0-99.xdr.gz"}, + {4, 250, 50, ".xdr.gz", "FFFFFF37--200-399/FFFFFF05--250-299.xdr.gz"}, + {1, 300, 200, ".xdr.gz", "FFFFFF37--200-399.xdr.gz"}, + {1, 1, 1, ".xdr.gz", "FFFFFFFE--1.xdr.gz"}, } for _, tc := range testCases { @@ -35,3 +38,47 @@ func TestGetObjectKeyFromSequenceNumber(t *testing.T) { }) } } + +func TestGetObjectKeyFromSequenceNumber_ObjectKeyDescOrder(t *testing.T) { + config := LedgerBatchConfig{ + LedgersPerFile: 1, + FilesPerPartition: 10, + FileSuffix: ".xdr.gz", + } + sequenceCount := 10000 + sequenceMap := make(map[uint32]string) + keys := make([]uint32, len(sequenceMap)) + count := 0 + + // Add 0 and MaxUint32 as edge cases + sequenceMap[0] = config.GetObjectKeyFromSequenceNumber(0) + keys = append(keys, 0) + sequenceMap[math.MaxUint32] = config.GetObjectKeyFromSequenceNumber(math.MaxUint32) + keys = append(keys, math.MaxUint32) + + for { + if count >= sequenceCount { + break + } + randSequence := rand.Uint32() + if _, ok := sequenceMap[randSequence]; ok { + continue + } + sequenceMap[randSequence] = config.GetObjectKeyFromSequenceNumber(randSequence) + keys = append(keys, randSequence) + count++ + } + + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + prev := sequenceMap[keys[0]] + for i := 1; i < sequenceCount; i++ { + curr := sequenceMap[keys[i]] + if prev <= curr { + t.Error("sequences not in lexicographic order") + } + prev = curr + } +} diff --git a/support/datastore/resumablemanager_test.go b/support/datastore/resumablemanager_test.go index 34279682ef..726854164f 100644 --- a/support/datastore/resumablemanager_test.go +++ b/support/datastore/resumablemanager_test.go @@ -208,58 +208,58 @@ func TestResumability(t *testing.T) { mockDataStore := &MockDataStore{} //"End ledger same as start, data store has it" - mockDataStore.On("Exists", ctx, "0-9.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFFF--0-9.xdr.gz").Return(true, nil).Once() //"End ledger same as start, data store does not have it" - mockDataStore.On("Exists", ctx, "10-19.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFF5--10-19.xdr.gz").Return(false, nil).Once() //"binary search encounters an error during datastore retrieval", - mockDataStore.On("Exists", ctx, "20-29.xdr.gz").Return(false, errors.New("datastore error happened")).Once() + mockDataStore.On("Exists", ctx, "FFFFFFEB--20-29.xdr.gz").Return(false, errors.New("datastore error happened")).Once() //"Data store is beyond boundary aligned start ledger" - mockDataStore.On("Exists", ctx, "30-39.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "40-49.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFE1--30-39.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFD7--40-49.xdr.gz").Return(false, nil).Once() //"Data store is beyond non boundary aligned start ledger" - mockDataStore.On("Exists", ctx, "70-79.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "80-89.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFB9--70-79.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFAF--80-89.xdr.gz").Return(false, nil).Once() //"Data store is beyond start and end ledger" - mockDataStore.On("Exists", ctx, "260-269.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "270-279.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFEFB--260-269.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFEF1--270-279.xdr.gz").Return(true, nil).Once() //"Data store is not beyond start ledger" - mockDataStore.On("Exists", ctx, "110-119.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "100-109.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "90-99.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFF91--110-119.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFF9B--100-109.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFA5--90-99.xdr.gz").Return(false, nil).Once() //"No end ledger provided, data store not beyond start" uses latest from network="test2" - mockDataStore.On("Exists", ctx, "1630-1639.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "1390-1399.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "1260-1269.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "1200-1209.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "1160-1169.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "1170-1179.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "1150-1159.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "1140-1149.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF9A1--1630-1639.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFA91--1390-1399.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB13--1260-1269.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB4F--1200-1209.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB77--1160-1169.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB6D--1170-1179.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB81--1150-1159.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB8B--1140-1149.xdr.gz").Return(false, nil).Once() //"No end ledger provided, data store is beyond start" uses latest from network="test3" - mockDataStore.On("Exists", ctx, "2630-2639.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "2390-2399.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "2260-2269.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "2250-2259.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "2240-2249.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "2230-2239.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "2200-2209.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF5B9--2630-2639.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF6A9--2390-2399.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF72B--2260-2269.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF735--2250-2259.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF73F--2240-2249.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF749--2230-2239.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF767--2200-2209.xdr.gz").Return(true, nil).Once() //"No end ledger provided, data store is beyond start and archive network latest, and partially into checkpoint frequency padding" uses latest from network="test4" - mockDataStore.On("Exists", ctx, "3630-3639.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "3880-3889.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "4000-4009.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "4060-4069.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "4090-4099.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "4080-4089.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "4070-4079.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF1D1--3630-3639.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF0D7--3880-3889.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF05F--4000-4009.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF023--4060-4069.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF005--4090-4099.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF00F--4080-4089.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF019--4070-4079.xdr.gz").Return(false, nil).Once() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 64cb1875d3f8403369e19e914b13dc176ba8e8f2 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Thu, 9 May 2024 14:52:36 -0700 Subject: [PATCH 165/234] services/horizon: clean up duplicate captive-core default config files (#5293) --- .../internal/configs/captive-core-pubnet.cfg | 195 ------------------ .../internal/configs/captive-core-testnet.cfg | 28 --- .../internal/docs/GUIDE_FOR_DEVELOPERS.md | 2 +- services/horizon/internal/flags.go | 10 +- services/horizon/internal/flags_test.go | 4 +- 5 files changed, 5 insertions(+), 234 deletions(-) delete mode 100644 services/horizon/internal/configs/captive-core-pubnet.cfg delete mode 100644 services/horizon/internal/configs/captive-core-testnet.cfg diff --git a/services/horizon/internal/configs/captive-core-pubnet.cfg b/services/horizon/internal/configs/captive-core-pubnet.cfg deleted file mode 100644 index 6e61aa2603..0000000000 --- a/services/horizon/internal/configs/captive-core-pubnet.cfg +++ /dev/null @@ -1,195 +0,0 @@ -# WARNING! Do not use this config in production. Quorum sets should -# be carefully selected manually. -NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" -FAILURE_SAFETY=1 -HTTP_PORT=11626 -PEER_PORT=11725 - -[[HOME_DOMAINS]] -HOME_DOMAIN="publicnode.org" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="lobstr.co" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="www.franklintempleton.com" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="satoshipay.io" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="whalestack.com" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="www.stellar.org" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="stellar.blockdaemon.com" -QUALITY="HIGH" - -[[VALIDATORS]] -NAME="Boötes" -PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" -ADDRESS="bootes.publicnode.org:11625" -HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" -HOME_DOMAIN="publicnode.org" - -[[VALIDATORS]] -NAME="Lyra by BP Ventures" -PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" -ADDRESS="lyra.publicnode.org:11625" -HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" -HOME_DOMAIN="publicnode.org" - -[[VALIDATORS]] -NAME="Hercules by OG Technologies" -PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" -ADDRESS="hercules.publicnode.org:11625" -HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" -HOME_DOMAIN="publicnode.org" - -[[VALIDATORS]] -NAME="LOBSTR 3 (North America)" -PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" -ADDRESS="v3.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v3.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="LOBSTR 1 (Europe)" -PUBLIC_KEY="GCFONE23AB7Y6C5YZOMKUKGETPIAJA4QOYLS5VNS4JHBGKRZCPYHDLW7" -ADDRESS="v1.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v1.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="LOBSTR 2 (Europe)" -PUBLIC_KEY="GCB2VSADESRV2DDTIVTFLBDI562K6KE3KMKILBHUHUWFXCUBHGQDI7VL" -ADDRESS="v2.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v2.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="LOBSTR 4 (Asia)" -PUBLIC_KEY="GA7TEPCBDQKI7JQLQ34ZURRMK44DVYCIGVXQQWNSWAEQR6KB4FMCBT7J" -ADDRESS="v4.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v4.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="LOBSTR 5 (India)" -PUBLIC_KEY="GA5STBMV6QDXFDGD62MEHLLHZTPDI77U3PFOD2SELU5RJDHQWBR5NNK7" -ADDRESS="v5.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v5.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="FT SCV 2" -PUBLIC_KEY="GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" -ADDRESS="stellar2.franklintempleton.com:11625" -HISTORY="curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" -HOME_DOMAIN="www.franklintempleton.com" - -[[VALIDATORS]] -NAME="FT SCV 3" -PUBLIC_KEY="GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" -ADDRESS="stellar3.franklintempleton.com:11625" -HISTORY="curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" -HOME_DOMAIN="www.franklintempleton.com" - -[[VALIDATORS]] -NAME="FT SCV 1" -PUBLIC_KEY="GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" -ADDRESS="stellar1.franklintempleton.com:11625" -HISTORY="curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" -HOME_DOMAIN="www.franklintempleton.com" - -[[VALIDATORS]] -NAME="SatoshiPay Frankfurt" -PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" -ADDRESS="stellar-de-fra.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" -HOME_DOMAIN="satoshipay.io" - -[[VALIDATORS]] -NAME="SatoshiPay Singapore" -PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" -ADDRESS="stellar-sg-sin.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" -HOME_DOMAIN="satoshipay.io" - -[[VALIDATORS]] -NAME="SatoshiPay Iowa" -PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" -ADDRESS="stellar-us-iowa.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" -HOME_DOMAIN="satoshipay.io" - -[[VALIDATORS]] -NAME="Whalestack (Germany)" -PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" -ADDRESS="germany.stellar.whalestack.com:11625" -HISTORY="curl -sf https://germany.stellar.whalestack.com/history/{0} -o {1}" -HOME_DOMAIN="whalestack.com" - -[[VALIDATORS]] -NAME="Whalestack (Hong Kong)" -PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" -ADDRESS="hongkong.stellar.whalestack.com:11625" -HISTORY="curl -sf https://hongkong.stellar.whalestack.com/history/{0} -o {1}" -HOME_DOMAIN="whalestack.com" - -[[VALIDATORS]] -NAME="Whalestack (Finland)" -PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" -ADDRESS="finland.stellar.whalestack.com:11625" -HISTORY="curl -sf https://finland.stellar.whalestack.com/history/{0} -o {1}" -HOME_DOMAIN="whalestack.com" - -[[VALIDATORS]] -NAME="SDF 2" -PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" -ADDRESS="core-live-b.stellar.org:11625" -HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" -HOME_DOMAIN="www.stellar.org" - -[[VALIDATORS]] -NAME="SDF 1" -PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" -ADDRESS="core-live-a.stellar.org:11625" -HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" -HOME_DOMAIN="www.stellar.org" - -[[VALIDATORS]] -NAME="SDF 3" -PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" -ADDRESS="core-live-c.stellar.org:11625" -HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" -HOME_DOMAIN="www.stellar.org" - -[[VALIDATORS]] -NAME="Blockdaemon Validator 3" -PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" -ADDRESS="stellar-full-validator3.bdnodes.net:11625" -HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" -HOME_DOMAIN="stellar.blockdaemon.com" - -[[VALIDATORS]] -NAME="Blockdaemon Validator 2" -PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" -ADDRESS="stellar-full-validator2.bdnodes.net:11625" -HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" -HOME_DOMAIN="stellar.blockdaemon.com" - -[[VALIDATORS]] -NAME="Blockdaemon Validator 1" -PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" -ADDRESS="stellar-full-validator1.bdnodes.net:11625" -HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" -HOME_DOMAIN="stellar.blockdaemon.com" \ No newline at end of file diff --git a/services/horizon/internal/configs/captive-core-testnet.cfg b/services/horizon/internal/configs/captive-core-testnet.cfg deleted file mode 100644 index 9abeecc8f5..0000000000 --- a/services/horizon/internal/configs/captive-core-testnet.cfg +++ /dev/null @@ -1,28 +0,0 @@ -NETWORK_PASSPHRASE="Test SDF Network ; September 2015" -UNSAFE_QUORUM=true -FAILURE_SAFETY=1 - -[[HOME_DOMAINS]] -HOME_DOMAIN="testnet.stellar.org" -QUALITY="HIGH" - -[[VALIDATORS]] -NAME="sdf_testnet_1" -HOME_DOMAIN="testnet.stellar.org" -PUBLIC_KEY="GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" -ADDRESS="core-testnet1.stellar.org" -HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_001/{0} -o {1}" - -[[VALIDATORS]] -NAME="sdf_testnet_2" -HOME_DOMAIN="testnet.stellar.org" -PUBLIC_KEY="GCUCJTIYXSOXKBSNFGNFWW5MUQ54HKRPGJUTQFJ5RQXZXNOLNXYDHRAP" -ADDRESS="core-testnet2.stellar.org" -HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_002/{0} -o {1}" - -[[VALIDATORS]] -NAME="sdf_testnet_3" -HOME_DOMAIN="testnet.stellar.org" -PUBLIC_KEY="GC2V2EFSXN6SQTWVYA5EPJPBWWIMSD2XQNKUOHGEKB535AQE2I6IXV2Z" -ADDRESS="core-testnet3.stellar.org" -HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_003/{0} -o {1}" \ No newline at end of file diff --git a/services/horizon/internal/docs/GUIDE_FOR_DEVELOPERS.md b/services/horizon/internal/docs/GUIDE_FOR_DEVELOPERS.md index 8236551215..b076dc4dd4 100644 --- a/services/horizon/internal/docs/GUIDE_FOR_DEVELOPERS.md +++ b/services/horizon/internal/docs/GUIDE_FOR_DEVELOPERS.md @@ -94,7 +94,7 @@ Add a debug configuration in your IDE to attach a debugger to the local Horizon "program": "${workspaceRoot}/services/horizon/main.go", "env": { "DATABASE_URL": "postgres://postgres@localhost:5432/horizon?sslmode=disable", - "CAPTIVE_CORE_CONFIG_APPEND_PATH": "./services/horizon/internal/configs/captive-core-testnet.cfg", + "CAPTIVE_CORE_CONFIG_APPEND_PATH": "./ingest/ledgerbackend/configs/captive-core-testnet.cfg", "HISTORY_ARCHIVE_URLS": "https://history.stellar.org/prd/core-testnet/core_testnet_001,https://history.stellar.org/prd/core-testnet/core_testnet_002", "NETWORK_PASSPHRASE": "Test SDF Network ; September 2015", "PER_HOUR_RATE_LIMIT": "0" diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index 5489b57d50..8c6b122dcb 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -835,20 +835,14 @@ type networkConfig struct { } var ( - //go:embed configs/captive-core-pubnet.cfg - PubnetDefaultConfig []byte - - //go:embed configs/captive-core-testnet.cfg - TestnetDefaultConfig []byte - PubnetConf = networkConfig{ - defaultConfig: PubnetDefaultConfig, + defaultConfig: ledgerbackend.PubnetDefaultConfig, HistoryArchiveURLs: network.PublicNetworkhistoryArchiveURLs, NetworkPassphrase: network.PublicNetworkPassphrase, } TestnetConf = networkConfig{ - defaultConfig: TestnetDefaultConfig, + defaultConfig: ledgerbackend.TestnetDefaultConfig, HistoryArchiveURLs: network.TestNetworkhistoryArchiveURLs, NetworkPassphrase: network.TestNetworkPassphrase, } diff --git a/services/horizon/internal/flags_test.go b/services/horizon/internal/flags_test.go index 3da39bc7a5..a30ea3a404 100644 --- a/services/horizon/internal/flags_test.go +++ b/services/horizon/internal/flags_test.go @@ -118,7 +118,7 @@ func Test_createCaptiveCoreConfig(t *testing.T) { config: Config{ NetworkPassphrase: PubnetConf.NetworkPassphrase, HistoryArchiveURLs: PubnetConf.HistoryArchiveURLs, - CaptiveCoreConfigPath: "configs/captive-core-pubnet.cfg", + CaptiveCoreConfigPath: "../../../ingest/ledgerbackend/configs/captive-core-pubnet.cfg", CaptiveCoreBinaryPath: "/path/to/captive-core/binary", }, networkPassphrase: PubnetConf.NetworkPassphrase, @@ -171,7 +171,7 @@ func Test_createCaptiveCoreConfig(t *testing.T) { config: Config{ NetworkPassphrase: PubnetConf.NetworkPassphrase, HistoryArchiveURLs: PubnetConf.HistoryArchiveURLs, - CaptiveCoreConfigPath: "configs/captive-core-testnet.cfg", + CaptiveCoreConfigPath: "../../../ingest/ledgerbackend/configs/captive-core-testnet.cfg", CaptiveCoreBinaryPath: "/path/to/captive-core/binary", }, errStr: fmt.Sprintf("invalid captive core toml file: invalid captive core toml: "+ From c65b5b7408145c5c3ae6b0652db17b57480e354f Mon Sep 17 00:00:00 2001 From: urvisavla Date: Thu, 9 May 2024 17:21:52 -0700 Subject: [PATCH 166/234] services/horizon: Add integration test for claimable balance sponsorship change (#5308) --- .../internal/integration/sponsorship_test.go | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/services/horizon/internal/integration/sponsorship_test.go b/services/horizon/internal/integration/sponsorship_test.go index fac8c499e7..57e4958122 100644 --- a/services/horizon/internal/integration/sponsorship_test.go +++ b/services/horizon/internal/integration/sponsorship_test.go @@ -739,6 +739,63 @@ func TestSponsorships(t *testing.T) { tt.Equalf(needed, actual, "effect %s not found enough", expectedType) } }) + + t.Run("ClaimableBalanceTransferSponsorship", func(t *testing.T) { + keys, accounts := itest.CreateAccounts(3, "150") + sponsorPair1, sponsor1 := keys[0], accounts[0] + sponsoreePair, _ := keys[1], accounts[1] + sponsorPair2, sponsor2 := keys[2], accounts[2] + + // Create a claimable balance + op := &txnbuild.CreateClaimableBalance{ + SourceAccount: sponsor1.GetAccountID(), + Destinations: []txnbuild.Claimant{ + txnbuild.NewClaimant(sponsorPair1.Address(), nil), + txnbuild.NewClaimant(sponsoreePair.Address(), nil), + }, + Amount: "10", + Asset: txnbuild.NativeAsset{}, + } + + _, err := itest.SubmitOperations(sponsor1, sponsorPair1, op) + tt.NoError(err) + + balances, err := client.ClaimableBalances(sdk.ClaimableBalanceRequest{}) + tt.NoError(err) + + // Verify the sponsor + claims := balances.Embedded.Records + tt.Len(claims, 1) + balance := claims[0] + tt.Equal(sponsorPair1.Address(), balance.Sponsor) + + // Transfer sponsorship + ops := []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SourceAccount: sponsor2.GetAccountID(), + SponsoredID: sponsorPair1.Address(), + }, + &txnbuild.RevokeSponsorship{ + SponsorshipType: txnbuild.RevokeSponsorshipTypeClaimableBalance, + SourceAccount: sponsor1.GetAccountID(), + ClaimableBalance: &claims[0].BalanceID, + }, + &txnbuild.EndSponsoringFutureReserves{SourceAccount: sponsor1.GetAccountID()}, + } + + _, err = itest.SubmitMultiSigOperations(sponsor2, []*keypair.Full{sponsorPair1, sponsorPair2}, ops...) + tt.NoError(err) + + balances, err = client.ClaimableBalances(sdk.ClaimableBalanceRequest{}) + tt.NoError(err) + + // Verify sponsorship transfer + claims = balances.Embedded.Records + tt.Len(claims, 1) + balance = claims[0] + tt.Equal(sponsorPair2.Address(), balance.Sponsor) + + }) } // Sandwiches a set of operations between a Begin/End reserve sponsorship. From 38d28bb233f6e5ee89170fc1d9014f5860fab63a Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 9 May 2024 22:23:54 -0700 Subject: [PATCH 167/234] services/horizon/ingest: added 'horizon_ingest_errors_total' metric key (#5302) --- go.mod | 4 +- go.sum | 8 +- services/horizon/internal/ingest/fsm.go | 26 +++++ .../ingest/ingest_history_range_state_test.go | 1 + services/horizon/internal/ingest/main.go | 20 ++++ services/horizon/internal/ingest/main_test.go | 104 +++++++++++++++++- 6 files changed, 155 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index fb02c94507..bb6a691f40 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( github.com/spf13/viper v1.17.0 github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 github.com/stellar/throttled v2.2.3-0.20190823235211-89d75816f59d+incompatible - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8 github.com/xdrpp/goxdr v0.1.1 google.golang.org/api v0.170.0 @@ -136,7 +136,7 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/sergi/go-diff v0.0.0-20161205080420-83532ca1c1ca // indirect github.com/spf13/cast v1.5.1 // indirect - github.com/stretchr/objx v0.5.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.34.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20151027082146-e0fe6f683076 // indirect diff --git a/go.sum b/go.sum index 6d8713de6d..6f4c2d37b2 100644 --- a/go.sum +++ b/go.sum @@ -419,8 +419,8 @@ github.com/stellar/throttled v2.2.3-0.20190823235211-89d75816f59d+incompatible/g github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= -github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -428,9 +428,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8 h1:g3yQGZK+G6dfF/mw/SOwsTMzUVkpT4hB8pHxpbTXkKw= diff --git a/services/horizon/internal/ingest/fsm.go b/services/horizon/internal/ingest/fsm.go index 5dce974e35..1b51518391 100644 --- a/services/horizon/internal/ingest/fsm.go +++ b/services/horizon/internal/ingest/fsm.go @@ -46,6 +46,32 @@ const ( ReingestHistoryRange ) +// provide a name represention for a state +func (state State) Name() string { + switch state { + case Start: + return "start" + case Stop: + return "stop" + case Build: + return "build" + case Resume: + return "resume" + case WaitForCheckpoint: + return "waitforcheckpoint" + case StressTest: + return "stresstest" + case VerifyRange: + return "verifyrange" + case HistoryRange: + return "historyrange" + case ReingestHistoryRange: + return "reingesthistoryrange" + default: + return "none" + } +} + type stateMachineNode interface { run(*system) (transition, error) String() string diff --git a/services/horizon/internal/ingest/ingest_history_range_state_test.go b/services/horizon/internal/ingest/ingest_history_range_state_test.go index cf248a6a7d..cf89ce4ab2 100644 --- a/services/horizon/internal/ingest/ingest_history_range_state_test.go +++ b/services/horizon/internal/ingest/ingest_history_range_state_test.go @@ -291,6 +291,7 @@ func (s *ReingestHistoryRangeStateTestSuite) SetupTest() { } s.historyQ.On("GetTx").Return(nil).Once() s.ledgerBackend.On("PrepareRange", s.ctx, ledgerbackend.BoundedRange(100, 200)).Return(nil).Once() + s.system.initMetrics() } func (s *ReingestHistoryRangeStateTestSuite) TearDownTest() { diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index d88cc3a3ce..7e109c391a 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -173,6 +173,10 @@ type Metrics struct { // ArchiveRequestCounter counts how many http requests are sent to history server HistoryArchiveStatsCounter *prometheus.CounterVec + + // IngestionErrorCounter counts the number of times the live/forward ingestion state machine + // encounters an error condition. + IngestionErrorCounter *prometheus.CounterVec } type System interface { @@ -443,6 +447,16 @@ func (s *system) initMetrics() { }, []string{"source", "type"}, ) + + s.metrics.IngestionErrorCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "horizon", Subsystem: "ingest", Name: "errors_total", + Help: "Counters of the number of times the live/forward ingestion state machine encountered an error. " + + "'current_state' label has the name of the state where the error occurred. " + + "'next_state' label has the name of the next state requested from the current_state.", + }, + []string{"current_state", "next_state"}, + ) } func (s *system) GetCurrentState() State { @@ -471,6 +485,7 @@ func (s *system) RegisterMetrics(registry *prometheus.Registry) { registry.MustRegister(s.metrics.LoadersStatsSummary) registry.MustRegister(s.metrics.StateVerifyLedgerEntriesCount) registry.MustRegister(s.metrics.HistoryArchiveStatsCounter) + registry.MustRegister(s.metrics.IngestionErrorCounter) s.ledgerBackend = ledgerbackend.WithMetrics(s.ledgerBackend, registry, "horizon") } @@ -643,6 +658,11 @@ func (s *system) runStateMachine(cur stateMachineNode) error { // so we log these errors using the info log level logger.Info("Error in ingestion state machine") } else { + s.Metrics().IngestionErrorCounter. + With(prometheus.Labels{ + "current_state": cur.GetState().Name(), + "next_state": next.node.GetState().Name(), + }).Inc() logger.Error("Error in ingestion state machine") } } diff --git a/services/horizon/internal/ingest/main_test.go b/services/horizon/internal/ingest/main_test.go index 6f5d89bd6a..3c7c587aa2 100644 --- a/services/horizon/internal/ingest/main_test.go +++ b/services/horizon/internal/ingest/main_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "database/sql" + "sync" "testing" "time" @@ -113,10 +114,13 @@ func TestStateMachineRunReturnsUnexpectedTransaction(t *testing.T) { historyQ: historyQ, ctx: context.Background(), } + reg := setupMetrics(system) historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() - assert.PanicsWithValue(t, "unexpected transaction", func() { + defer func() { + assertErrorRestartMetrics(reg, "", "", 0, t) + }() system.Run() }) } @@ -127,12 +131,17 @@ func TestStateMachineTransition(t *testing.T) { historyQ: historyQ, ctx: context.Background(), } + reg := setupMetrics(system) historyQ.On("GetTx").Return(nil).Once() historyQ.On("Begin", mock.Anything).Return(errors.New("my error")).Once() historyQ.On("GetTx").Return(&sqlx.Tx{}).Once() assert.PanicsWithValue(t, "unexpected transaction", func() { + defer func() { + // the test triggers error in the first start state exec, so metric is added + assertErrorRestartMetrics(reg, "start", "start", 1, t) + }() system.Run() }) } @@ -144,12 +153,14 @@ func TestContextCancel(t *testing.T) { historyQ: historyQ, ctx: ctx, } + reg := setupMetrics(system) historyQ.On("GetTx").Return(nil).Once() - historyQ.On("Begin", mock.AnythingOfType("*context.cancelCtx")).Return(errors.New("my error")).Once() + historyQ.On("Begin", mock.AnythingOfType("*context.cancelCtx")).Return(context.Canceled).Once() cancel() assert.NoError(t, system.runStateMachine(startState{})) + assertErrorRestartMetrics(reg, "", "", 0, t) } // TestStateMachineRunReturnsErrorWhenNextStateIsShutdownWithError checks if the @@ -162,12 +173,61 @@ func TestStateMachineRunReturnsErrorWhenNextStateIsShutdownWithError(t *testing. ctx: context.Background(), historyQ: historyQ, } + reg := setupMetrics(system) historyQ.On("GetTx").Return(nil).Once() err := system.runStateMachine(verifyRangeState{}) assert.Error(t, err) assert.EqualError(t, err, "invalid range: [0, 0]") + assertErrorRestartMetrics(reg, "verifyrange", "stop", 1, t) +} + +func TestStateMachineRestartEmitsMetric(t *testing.T) { + historyQ := &mockDBQ{} + ledgerBackend := &mockLedgerBackend{} + ctx, cancel := context.WithCancel(context.Background()) + var wg sync.WaitGroup + defer func() { + cancel() + wg.Wait() + }() + + system := &system{ + ctx: ctx, + historyQ: historyQ, + ledgerBackend: ledgerBackend, + } + + ledgerBackend.On("IsPrepared", system.ctx, ledgerbackend.UnboundedRange(101)).Return(true, nil) + ledgerBackend.On("GetLedger", system.ctx, uint32(101)).Return(xdr.LedgerCloseMeta{ + V0: &xdr.LedgerCloseMetaV0{ + LedgerHeader: xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + LedgerSeq: 101, + LedgerVersion: xdr.Uint32(MaxSupportedProtocolVersion), + BucketListHash: xdr.Hash{1, 2, 3}, + }, + }, + }, + }, nil) + + reg := setupMetrics(system) + + historyQ.On("GetTx").Return(nil) + historyQ.On("Begin", system.ctx).Return(errors.New("stop state machine")) + + wg.Add(1) + go func() { + defer wg.Done() + system.runStateMachine(resumeState{latestSuccessfullyProcessedLedger: 100}) + }() + + assert.EventuallyWithT(t, func(c *assert.CollectT) { + // this checks every 50ms up to 10s total, for at least 3 fsm retries based on a db Begin error + // this condition should be met as the fsm retries every second. + assertErrorRestartMetrics(reg, "resume", "resume", 3, c) + }, 10*time.Second, 50*time.Millisecond, "horizon_ingest_errors_total metric was not incremented on a fsm error") } func TestMaybeVerifyStateGetExpStateInvalidError(t *testing.T) { @@ -248,6 +308,7 @@ func TestCurrentStateRaceCondition(t *testing.T) { historyQ: historyQ, ctx: context.Background(), } + reg := setupMetrics(s) historyQ.On("GetTx").Return(nil) historyQ.On("Begin", s.ctx).Return(nil) @@ -280,6 +341,45 @@ loop: } close(getCh) <-doneCh + assertErrorRestartMetrics(reg, "", "", 0, t) +} + +func setupMetrics(system *system) *prometheus.Registry { + registry := prometheus.NewRegistry() + system.initMetrics() + registry.Register(system.Metrics().IngestionErrorCounter) + return registry +} + +func assertErrorRestartMetrics(reg *prometheus.Registry, assertCurrentState string, assertNextState string, assertRestartCount float64, t assert.TestingT) { + assert := assert.New(t) + metrics, err := reg.Gather() + assert.NoError(err) + + for _, metricFamily := range metrics { + if metricFamily.GetName() == "horizon_ingest_errors_total" { + assert.Len(metricFamily.GetMetric(), 1) + assert.Equal(metricFamily.GetMetric()[0].GetCounter().GetValue(), assertRestartCount) + var metricCurrentState = "" + var metricNextState = "" + for _, label := range metricFamily.GetMetric()[0].GetLabel() { + if label.GetName() == "current_state" { + metricCurrentState = label.GetValue() + } + if label.GetName() == "next_state" { + metricNextState = label.GetValue() + } + } + + assert.Equal(metricCurrentState, assertCurrentState) + assert.Equal(metricNextState, assertNextState) + return + } + } + + if assertRestartCount > 0.0 { + assert.Fail("horizon_ingest_errors_total metrics were not correct") + } } type mockDBQ struct { From a4e5a3fc6664f7cf6b26fb3196b366910563636f Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 10 May 2024 11:13:10 +0100 Subject: [PATCH 168/234] ingest/ledgerbackend: Improve thread-safety of stellarCoreRunner.close() (#5307) --- ingest/ledgerbackend/captive_core_backend.go | 6 -- .../captive_core_backend_test.go | 2 - ingest/ledgerbackend/mock_cmd_test.go | 2 +- ingest/ledgerbackend/stellar_core_runner.go | 88 +++++++++---------- .../ledgerbackend/stellar_core_runner_test.go | 62 ++++++++++++- 5 files changed, 101 insertions(+), 59 deletions(-) diff --git a/ingest/ledgerbackend/captive_core_backend.go b/ingest/ledgerbackend/captive_core_backend.go index 50e933bb6a..25b22d01d5 100644 --- a/ingest/ledgerbackend/captive_core_backend.go +++ b/ingest/ledgerbackend/captive_core_backend.go @@ -452,12 +452,6 @@ func (c *CaptiveStellarCore) startPreparingRange(ctx context.Context, ledgerRang if err := c.stellarCoreRunner.close(); err != nil { return false, errors.Wrap(err, "error closing existing session") } - - // Make sure Stellar-Core is terminated before starting a new instance. - processExited, _ := c.stellarCoreRunner.getProcessExitError() - if !processExited { - return false, errors.New("the previous Stellar-Core instance is still running") - } } var err error diff --git a/ingest/ledgerbackend/captive_core_backend_test.go b/ingest/ledgerbackend/captive_core_backend_test.go index 5178fd97a1..a367f560f1 100644 --- a/ingest/ledgerbackend/captive_core_backend_test.go +++ b/ingest/ledgerbackend/captive_core_backend_test.go @@ -302,8 +302,6 @@ func TestCaptivePrepareRangeCloseNotFullyTerminated(t *testing.T) { mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) mockRunner.On("context").Return(ctx) mockRunner.On("close").Return(nil) - mockRunner.On("getProcessExitError").Return(true, nil) - mockRunner.On("getProcessExitError").Return(false, nil) mockArchive := &historyarchive.MockArchive{} mockArchive. diff --git a/ingest/ledgerbackend/mock_cmd_test.go b/ingest/ledgerbackend/mock_cmd_test.go index a1d280421a..a28b6c8d01 100644 --- a/ingest/ledgerbackend/mock_cmd_test.go +++ b/ingest/ledgerbackend/mock_cmd_test.go @@ -70,7 +70,7 @@ func simpleCommandMock() *mockCmd { cmdMock.On("getStdout").Return(writer) cmdMock.On("setStderr", mock.Anything) cmdMock.On("getStderr").Return(writer) - cmdMock.On("getProcess").Return(&os.Process{}) + cmdMock.On("getProcess").Return(&os.Process{}).Maybe() cmdMock.On("setExtraFiles", mock.Anything) cmdMock.On("Start").Return(nil) return cmdMock diff --git a/ingest/ledgerbackend/stellar_core_runner.go b/ingest/ledgerbackend/stellar_core_runner.go index 1c2c09c4a6..7f883b69c5 100644 --- a/ingest/ledgerbackend/stellar_core_runner.go +++ b/ingest/ledgerbackend/stellar_core_runner.go @@ -17,6 +17,7 @@ import ( "time" "github.com/pkg/errors" + "github.com/stellar/go/protocols/stellarcore" "github.com/stellar/go/support/log" ) @@ -65,6 +66,7 @@ type stellarCoreRunner struct { systemCaller systemCaller lock sync.Mutex + closeOnce sync.Once processExited bool processExitError error @@ -285,9 +287,6 @@ func (r *stellarCoreRunner) catchup(from, to uint32) error { r.lock.Lock() defer r.lock.Unlock() - r.mode = stellarCoreRunnerModeOffline - r.storagePath = r.getFullStoragePath() - // check if we have already been closed if r.ctx.Err() != nil { return r.ctx.Err() @@ -297,6 +296,9 @@ func (r *stellarCoreRunner) catchup(from, to uint32) error { return errors.New("runner already started") } + r.mode = stellarCoreRunnerModeOffline + r.storagePath = r.getFullStoragePath() + rangeArg := fmt.Sprintf("%d/%d", to, to-from+1) params := []string{"catchup", rangeArg, "--metadata-output-stream", r.getPipeName()} @@ -350,9 +352,6 @@ func (r *stellarCoreRunner) runFrom(from uint32, hash string) error { r.lock.Lock() defer r.lock.Unlock() - r.mode = stellarCoreRunnerModeOnline - r.storagePath = r.getFullStoragePath() - // check if we have already been closed if r.ctx.Err() != nil { return r.ctx.Err() @@ -362,6 +361,9 @@ func (r *stellarCoreRunner) runFrom(from uint32, hash string) error { return errors.New("runner already started") } + r.mode = stellarCoreRunnerModeOnline + r.storagePath = r.getFullStoragePath() + var err error if r.useDB { @@ -546,53 +548,45 @@ func (r *stellarCoreRunner) getProcessExitError() (bool, error) { // the necessary cleanup on the resources associated with the captive core process // close is both thread safe and idempotent func (r *stellarCoreRunner) close() error { - r.lock.Lock() - started := r.started - storagePath := r.storagePath - - r.storagePath = "" - - // check if we have already closed - if storagePath == "" { + var closeError error + r.closeOnce.Do(func() { + r.lock.Lock() + // we cancel the context while holding the lock in order to guarantee that + // this captive core instance cannot start once the lock is released. + // catchup() and runFrom() can only execute while holding the lock and if + // the context is canceled both catchup() and runFrom() will abort early + // without performing any side effects (e.g. state mutations). + r.cancel() r.lock.Unlock() - return nil - } - if !started { - // Update processExited if handleExit that updates it not even started - // (error before command run). - r.processExited = true - } - - r.cancel() - r.lock.Unlock() + // only reap captive core sub process and related go routines if we've started + // otherwise, just cleanup the temp dir + if r.started { + // wait for the stellar core process to terminate + r.wg.Wait() - // only reap captive core sub process and related go routines if we've started - // otherwise, just cleanup the temp dir - if started { - // wait for the stellar core process to terminate - r.wg.Wait() + // drain meta pipe channel to make sure the ledger buffer goroutine exits + for range r.getMetaPipe() { - // drain meta pipe channel to make sure the ledger buffer goroutine exits - for range r.getMetaPipe() { + } + // now it's safe to close the pipe reader + // because the ledger buffer is no longer reading from it + r.pipe.Reader.Close() } - // now it's safe to close the pipe reader - // because the ledger buffer is no longer reading from it - r.pipe.Reader.Close() - } - - if r.mode != 0 && (runtime.GOOS == "windows" || - (r.processExitError != nil && r.processExitError != context.Canceled) || - r.mode == stellarCoreRunnerModeOffline) { - // It's impossible to send SIGINT on Windows so buckets can become - // corrupted. If we can't reuse it, then remove it. - // We also remove the storage path if there was an error terminating the - // process (files can be corrupted). - // We remove all files when reingesting to save disk space. - return r.systemCaller.removeAll(storagePath) - } + if r.mode != 0 && (runtime.GOOS == "windows" || + (r.processExitError != nil && r.processExitError != context.Canceled) || + r.mode == stellarCoreRunnerModeOffline) { + // It's impossible to send SIGINT on Windows so buckets can become + // corrupted. If we can't reuse it, then remove it. + // We also remove the storage path if there was an error terminating the + // process (files can be corrupted). + // We remove all files when reingesting to save disk space. + closeError = r.systemCaller.removeAll(r.storagePath) + return + } + }) - return nil + return closeError } diff --git a/ingest/ledgerbackend/stellar_core_runner_test.go b/ingest/ledgerbackend/stellar_core_runner_test.go index 60871922b7..00cb29137b 100644 --- a/ingest/ledgerbackend/stellar_core_runner_test.go +++ b/ingest/ledgerbackend/stellar_core_runner_test.go @@ -3,6 +3,7 @@ package ledgerbackend import ( "context" "encoding/json" + "sync" "testing" "time" @@ -46,7 +47,7 @@ func TestCloseOffline(t *testing.T) { "fd:3", "--in-memory", ).Return(cmdMock) - scMock.On("removeAll", mock.Anything).Return(nil) + scMock.On("removeAll", mock.Anything).Return(nil).Once() runner.systemCaller = scMock assert.NoError(t, runner.catchup(100, 200)) @@ -133,7 +134,7 @@ func TestCloseOnlineWithError(t *testing.T) { "--metadata-output-stream", "fd:3", ).Return(cmdMock) - scMock.On("removeAll", mock.Anything).Return(nil) + scMock.On("removeAll", mock.Anything).Return(nil).Once() runner.systemCaller = scMock assert.NoError(t, runner.runFrom(100, "hash")) @@ -149,6 +150,61 @@ func TestCloseOnlineWithError(t *testing.T) { assert.NoError(t, runner.close()) } +func TestCloseConcurrency(t *testing.T) { + captiveCoreToml, err := NewCaptiveCoreToml(CaptiveCoreTomlParams{}) + assert.NoError(t, err) + + captiveCoreToml.AddExamplePubnetValidators() + + runner := newStellarCoreRunner(CaptiveCoreConfig{ + BinaryPath: "/usr/bin/stellar-core", + HistoryArchiveURLs: []string{"http://localhost"}, + Log: log.New(), + Context: context.Background(), + Toml: captiveCoreToml, + StoragePath: "/tmp/captive-core", + }) + + cmdMock := simpleCommandMock() + cmdMock.On("Wait").Return(errors.New("wait error")).WaitUntil(time.After(time.Millisecond * 300)) + defer cmdMock.AssertExpectations(t) + + // Replace system calls with a mock + scMock := &mockSystemCaller{} + defer scMock.AssertExpectations(t) + scMock.On("stat", mock.Anything).Return(isDirImpl(true), nil) + scMock.On("writeFile", mock.Anything, mock.Anything, mock.Anything).Return(nil) + scMock.On("command", + "/usr/bin/stellar-core", + "--conf", + mock.Anything, + "--console", + "catchup", + "200/101", + "--metadata-output-stream", + "fd:3", + "--in-memory", + ).Return(cmdMock) + scMock.On("removeAll", mock.Anything).Return(nil).Once() + runner.systemCaller = scMock + + assert.NoError(t, runner.catchup(100, 200)) + + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + assert.NoError(t, runner.close()) + exited, err := runner.getProcessExitError() + assert.True(t, exited) + assert.Error(t, err) + }() + } + + wg.Wait() +} + func TestRunFromUseDBLedgersMatch(t *testing.T) { captiveCoreToml, err := NewCaptiveCoreToml(CaptiveCoreTomlParams{}) assert.NoError(t, err) @@ -300,7 +356,7 @@ func TestRunFromUseDBLedgersInFront(t *testing.T) { scMock := &mockSystemCaller{} defer scMock.AssertExpectations(t) // Storage dir is removed because ledgers do not match - scMock.On("removeAll", mock.Anything).Return(nil) + scMock.On("removeAll", mock.Anything).Return(nil).Once() scMock.On("stat", mock.Anything).Return(isDirImpl(true), nil) scMock.On("writeFile", mock.Anything, mock.Anything, mock.Anything).Return(nil) scMock.On("command", From 57486f0192dbf714b5b97fabd31f8b218894f7dd Mon Sep 17 00:00:00 2001 From: chowbao Date: Fri, 10 May 2024 11:49:41 -0400 Subject: [PATCH 169/234] Add new captive core flags to ledgerbackend toml (#5309) --- ingest/ledgerbackend/toml.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ingest/ledgerbackend/toml.go b/ingest/ledgerbackend/toml.go index 84f84bfe4a..f156d5b888 100644 --- a/ingest/ledgerbackend/toml.go +++ b/ingest/ledgerbackend/toml.go @@ -104,6 +104,8 @@ type captiveCoreTomlValues struct { TestingMinimumPersistentEntryLifetime *uint `toml:"TESTING_MINIMUM_PERSISTENT_ENTRY_LIFETIME,omitempty"` TestingSorobanHighLimitOverride *bool `toml:"TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE,omitempty"` EnableDiagnosticsForTxSubmission *bool `toml:"ENABLE_DIAGNOSTICS_FOR_TX_SUBMISSION,omitempty"` + EnableEmitSorobanTransactionMetaExtV1 *bool `toml:"EMIT_SOROBAN_TRANSACTION_META_EXT_V1,omitempty"` + EnableEmitLedgerCloseMetaExtV1 *bool `toml:"EMIT_LEDGER_CLOSE_META_EXT_V1,omitempty"` } // QuorumSetIsConfigured returns true if there is a quorum set defined in the configuration. From fbe76313513d81246e4ecd58a13c29db7c2b56ec Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 10 May 2024 19:37:01 +0100 Subject: [PATCH 170/234] support/datastore: Add CRC32C validation when exporting and consuming tx meta files (#5310) --- support/datastore/gcs_datastore.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/support/datastore/gcs_datastore.go b/support/datastore/gcs_datastore.go index ca1c90abea..c8c22fd57e 100644 --- a/support/datastore/gcs_datastore.go +++ b/support/datastore/gcs_datastore.go @@ -1,8 +1,10 @@ package datastore import ( + "bytes" "context" "fmt" + "hash/crc32" "io" "net/http" "os" @@ -64,7 +66,13 @@ func NewGCSDataStore(ctx context.Context, params map[string]string, network stri // GetFile retrieves a file from the GCS bucket. func (b GCSDataStore) GetFile(ctx context.Context, filePath string) (io.ReadCloser, error) { filePath = path.Join(b.prefix, filePath) - r, err := b.bucket.Object(filePath).NewReader(ctx) + // setting ReadCompressed(true) will avoid transcoding of compressed files by including + // an "Accept-Encoding: gzip" header in the request: + // https://github.com/googleapis/google-cloud-go/blob/main/storage/http_client.go#L1307-L1309 + // https://cloud.google.com/storage/docs/transcoding#decompressive_transcoding + // This will ensure that the reader performs CRC validation upon finishing the download: + // https://pkg.go.dev/cloud.google.com/go/storage#Reader + r, err := b.bucket.Object(filePath).ReadCompressed(true).NewReader(ctx) if err != nil { if err == storage.ErrObjectNotExist { return nil, os.ErrNotExist @@ -147,7 +155,14 @@ func (b GCSDataStore) putFile(ctx context.Context, filePath string, in io.Writer o = o.If(*conditions) } w := o.NewWriter(ctx) - if _, err := in.WriteTo(w); err != nil { + buf := &bytes.Buffer{} + if _, err := in.WriteTo(buf); err != nil { + return errors.Wrapf(err, "failed to write file: %s", filePath) + } + w.SendCRC32C = true + // we must set CRC32C before invoking w.Write() for the first time + w.CRC32C = crc32.Checksum(buf.Bytes(), crc32.MakeTable(crc32.Castagnoli)) + if _, err := in.WriteTo(buf); err != nil { return errors.Wrapf(err, "failed to put file: %s", filePath) } return w.Close() From 0f6abb0432536c6eb64bfb637cb1cf987b4d1f6f Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Fri, 10 May 2024 15:46:01 -0400 Subject: [PATCH 171/234] Ingestion filtering should use OR logic for rules rather than AND (#5303) * Refactor ingestion filtering rules for OR rather than AND * Fix failing tests * Rename variable * rename func name to IsEnabled * Refactor to remove interface dependency * Add extra assertion to asset_test * Add changelog entry --- services/horizon/CHANGELOG.md | 7 +- .../internal/ingest/filters/account.go | 30 +++++---- .../internal/ingest/filters/account_test.go | 12 ++-- .../horizon/internal/ingest/filters/asset.go | 28 ++++---- .../internal/ingest/filters/asset_test.go | 21 ++++-- .../internal/ingest/group_processors.go | 26 ++++--- .../internal/ingest/processors/main.go | 4 +- .../integration/ingestion_filtering_test.go | 67 ++++++++++++++++++- 8 files changed, 147 insertions(+), 48 deletions(-) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 1d44eb5ec0..7a89844062 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -4,7 +4,12 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased - + +### Breaking Changes + +- Change ingestion filtering logic to store transactions if any filter matches on it. ([5303](https://github.com/stellar/go/pull/5303)) + - The previous behaviour was to store a tx only if both asset and account filters match together. So even if a tx matched an account filter but failed to match an asset filter, it would not be stored by Horizon. + ## 2.30.0 **This release adds support for Protocol 21** diff --git a/services/horizon/internal/ingest/filters/account.go b/services/horizon/internal/ingest/filters/account.go index 08def6a2b7..1601314c75 100644 --- a/services/horizon/internal/ingest/filters/account.go +++ b/services/horizon/internal/ingest/filters/account.go @@ -26,41 +26,45 @@ func NewAccountFilter() AccountFilter { } } -func (filter *accountFilter) Name() string { +func (f *accountFilter) Name() string { return "filters.accountFilter" } -func (filter *accountFilter) RefreshAccountFilter(filterConfig *history.AccountFilterConfig) error { +func (f *accountFilter) RefreshAccountFilter(filterConfig *history.AccountFilterConfig) error { // only need to re-initialize the filter config state(rules) if its cached version(in memory) // is older than the incoming config version based on lastModified epoch timestamp - if filterConfig.LastModified > filter.lastModified { + if filterConfig.LastModified > f.lastModified { logger.Infof("New Account Filter config detected, reloading new config %v ", *filterConfig) - filter.enabled = filterConfig.Enabled - filter.whitelistedAccountsSet = listToSet(filterConfig.Whitelist) - filter.lastModified = filterConfig.LastModified + f.enabled = filterConfig.Enabled + f.whitelistedAccountsSet = listToSet(filterConfig.Whitelist) + f.lastModified = filterConfig.LastModified } return nil } -func (f *accountFilter) FilterTransaction(ctx context.Context, transaction ingest.LedgerTransaction) (bool, error) { - // filtering is disabled if the whitelist is empty for now, as that is the only filter rule - if len(f.whitelistedAccountsSet) == 0 || !f.enabled { - return true, nil +func (f *accountFilter) FilterTransaction(ctx context.Context, transaction ingest.LedgerTransaction) (bool, bool, error) { + if !f.isEnabled() { + return false, true, nil } participants, err := processors.ParticipantsForTransaction(0, transaction) if err != nil { - return false, err + return true, false, err } // NOTE: this assumes that the participant list has a small memory footprint // otherwise, we should be doing the filtering on the DB side for _, p := range participants { if f.whitelistedAccountsSet.Contains(p.Address()) { - return true, nil + return true, true, nil } } - return false, nil + return true, false, nil +} + +func (f accountFilter) isEnabled() bool { + // filtering is disabled if the whitelist is empty for now, as that is the only filter rule + return len(f.whitelistedAccountsSet) >= 1 && f.enabled } diff --git a/services/horizon/internal/ingest/filters/account_test.go b/services/horizon/internal/ingest/filters/account_test.go index 1831a6a6e5..17f290a460 100644 --- a/services/horizon/internal/ingest/filters/account_test.go +++ b/services/horizon/internal/ingest/filters/account_test.go @@ -26,11 +26,12 @@ func TestAccountFilterAllowsWhenMatch(t *testing.T) { err := filter.RefreshAccountFilter(filterConfig) tt.NoError(err) - result, err := filter.FilterTransaction(ctx, getAccountTestTx(t, + isEnabled, result, err := filter.FilterTransaction(ctx, getAccountTestTx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL", "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H")) tt.NoError(err) + tt.Equal(isEnabled, true) tt.Equal(result, true) } @@ -47,13 +48,14 @@ func TestAccountFilterAllowsWhenDisabled(t *testing.T) { err := filter.RefreshAccountFilter(filterConfig) tt.NoError(err) - result, err := filter.FilterTransaction(ctx, getAccountTestTx(t, + isEnabled, result, err := filter.FilterTransaction(ctx, getAccountTestTx(t, "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H")) tt.NoError(err) // there is no match on filter rule, but since filter is disabled, it should allow all + tt.Equal(isEnabled, false) tt.Equal(result, true) } @@ -70,11 +72,12 @@ func TestAccountFilterAllowsWhenEmptyWhitelist(t *testing.T) { err := filter.RefreshAccountFilter(filterConfig) tt.NoError(err) - result, err := filter.FilterTransaction(ctx, getAccountTestTx(t, + isEnabled, result, err := filter.FilterTransaction(ctx, getAccountTestTx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL", "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H")) tt.NoError(err) + tt.Equal(isEnabled, false) tt.Equal(result, true) } @@ -92,11 +95,12 @@ func TestAccountFilterDoesNotAllowWhenNoMatch(t *testing.T) { err := filter.RefreshAccountFilter(filterConfig) tt.NoError(err) - result, err := filter.FilterTransaction(ctx, getAccountTestTx(t, + isEnabled, result, err := filter.FilterTransaction(ctx, getAccountTestTx(t, "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H")) tt.NoError(err) + tt.Equal(isEnabled, true) tt.Equal(result, false) } diff --git a/services/horizon/internal/ingest/filters/asset.go b/services/horizon/internal/ingest/filters/asset.go index 0a3a4ca6c7..4aa6ebc9b3 100644 --- a/services/horizon/internal/ingest/filters/asset.go +++ b/services/horizon/internal/ingest/filters/asset.go @@ -34,27 +34,26 @@ func NewAssetFilter() AssetFilter { } } -func (filter *assetFilter) Name() string { +func (f *assetFilter) Name() string { return "filters.assetFilter" } -func (filter *assetFilter) RefreshAssetFilter(filterConfig *history.AssetFilterConfig) error { +func (f *assetFilter) RefreshAssetFilter(filterConfig *history.AssetFilterConfig) error { // only need to re-initialize the filter config state(rules) if it's cached version(in memory) // is older than the incoming config version based on lastModified epoch timestamp - if filterConfig.LastModified > filter.lastModified { + if filterConfig.LastModified > f.lastModified { logger.Infof("New Asset Filter config detected, reloading new config %v ", *filterConfig) - filter.enabled = filterConfig.Enabled - filter.canonicalAssetsLookup = listToSet(filterConfig.Whitelist) - filter.lastModified = filterConfig.LastModified + f.enabled = filterConfig.Enabled + f.canonicalAssetsLookup = listToSet(filterConfig.Whitelist) + f.lastModified = filterConfig.LastModified } return nil } -func (f *assetFilter) FilterTransaction(ctx context.Context, transaction ingest.LedgerTransaction) (bool, error) { - // filtering is disabled if the whitelist is empty for now as that is the only filter rule - if len(f.canonicalAssetsLookup) < 1 || !f.enabled { - return true, nil +func (f *assetFilter) FilterTransaction(ctx context.Context, transaction ingest.LedgerTransaction) (bool, bool, error) { + if !f.isEnabled() { + return false, true, nil } var operations []xdr.Operation @@ -68,11 +67,11 @@ func (f *assetFilter) FilterTransaction(ctx context.Context, transaction ingest. } if f.filterOperationsMatchedOnRules(operations) { - return true, nil + return true, true, nil } logger.Debugf("No match, dropped tx with seq %v ", transaction.Envelope.SeqNum()) - return false, nil + return true, false, nil } func (f assetFilter) filterOperationsMatchedOnRules(operations []xdr.Operation) bool { @@ -144,3 +143,8 @@ func listToSet(list []string) set.Set[string] { } return set } + +func (f assetFilter) isEnabled() bool { + // filtering is disabled if the whitelist is empty for now as that is the only filter rule + return len(f.canonicalAssetsLookup) >= 1 && f.enabled +} diff --git a/services/horizon/internal/ingest/filters/asset_test.go b/services/horizon/internal/ingest/filters/asset_test.go index 3da23bc440..e0deed7771 100644 --- a/services/horizon/internal/ingest/filters/asset_test.go +++ b/services/horizon/internal/ingest/filters/asset_test.go @@ -25,12 +25,14 @@ func TestAssetFilterAllowsOnMatch(t *testing.T) { err := filter.RefreshAssetFilter(filterConfig) tt.NoError(err) - result, err := filter.FilterTransaction(ctx, getAssetTestV1Tx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL")) + isEnabled, result, err := filter.FilterTransaction(ctx, getAssetTestV1Tx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL")) tt.NoError(err) + tt.Equal(isEnabled, true) tt.Equal(result, true) - result, err = filter.FilterTransaction(ctx, getAssetTestV0Tx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL")) + isEnabled, result, err = filter.FilterTransaction(ctx, getAssetTestV0Tx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL")) tt.NoError(err) + tt.Equal(isEnabled, true) tt.Equal(result, true) } @@ -47,12 +49,14 @@ func TestAssetFilterAllowsWhenEmptyWhitelist(t *testing.T) { err := filter.RefreshAssetFilter(filterConfig) tt.NoError(err) - result, err := filter.FilterTransaction(ctx, getAssetTestV1Tx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL")) + isEnabled, result, err := filter.FilterTransaction(ctx, getAssetTestV1Tx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL")) tt.NoError(err) + tt.Equal(isEnabled, false) tt.Equal(result, true) - result, err = filter.FilterTransaction(ctx, getAssetTestV0Tx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL")) + isEnabled, result, err = filter.FilterTransaction(ctx, getAssetTestV0Tx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL")) tt.NoError(err) + tt.Equal(isEnabled, false) tt.Equal(result, true) } @@ -69,9 +73,10 @@ func TestAssetFilterAllowsWhenDisabled(t *testing.T) { err := filter.RefreshAssetFilter(filterConfig) tt.NoError(err) - result, err := filter.FilterTransaction(ctx, getAssetTestV1Tx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL")) + isEnabled, result, err := filter.FilterTransaction(ctx, getAssetTestV1Tx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL")) tt.NoError(err) // there was no match on filter rules, but since filter was disabled also, it should allow all + tt.Equal(isEnabled, false) tt.Equal(result, true) } @@ -89,12 +94,14 @@ func TestAssetFilterDoesNotAllowV1WhenNoMatch(t *testing.T) { err := filter.RefreshAssetFilter(filterConfig) tt.NoError(err) - result, err := filter.FilterTransaction(ctx, getAssetTestV1Tx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL")) + isEnabled, result, err := filter.FilterTransaction(ctx, getAssetTestV1Tx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL")) tt.NoError(err) + tt.Equal(isEnabled, true) tt.Equal(result, false) - result, err = filter.FilterTransaction(ctx, getAssetTestV0Tx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL")) + isEnabled, result, err = filter.FilterTransaction(ctx, getAssetTestV0Tx(t, "GD6WNNTW664WH7FXC5RUMUTF7P5QSURC2IT36VOQEEGFZ4UWUEQGECAL")) tt.NoError(err) + tt.Equal(isEnabled, true) tt.Equal(result, false) } diff --git a/services/horizon/internal/ingest/group_processors.go b/services/horizon/internal/ingest/group_processors.go index 00f8adde8a..4a6c2e69af 100644 --- a/services/horizon/internal/ingest/group_processors.go +++ b/services/horizon/internal/ingest/group_processors.go @@ -178,21 +178,31 @@ func (g *groupTransactionFilterers) Name() string { return "groupTransactionFilterers" } -func (g *groupTransactionFilterers) FilterTransaction(ctx context.Context, tx ingest.LedgerTransaction) (bool, error) { +func (g *groupTransactionFilterers) FilterTransaction(ctx context.Context, tx ingest.LedgerTransaction) (bool, bool, error) { + filtersEnabled := false + for _, f := range g.filterers { startTime := time.Now() - include, err := f.FilterTransaction(ctx, tx) + filterEnabled, include, err := f.FilterTransaction(ctx, tx) + if !filterEnabled { + continue + } + + filtersEnabled = true if err != nil { - return false, errors.Wrapf(err, "error in %T.FilterTransaction", f) + return true, false, errors.Wrapf(err, "error in %T.FilterTransaction", f) } g.AddRunDuration(f.Name(), startTime) - if !include { - // filter out, we can return early - g.droppedTransactions++ - return false, nil + if include { + return true, true, nil } } - return true, nil + + if filtersEnabled { + g.droppedTransactions++ + return true, false, nil + } + return false, true, nil } func (g *groupTransactionFilterers) ResetStats() { diff --git a/services/horizon/internal/ingest/processors/main.go b/services/horizon/internal/ingest/processors/main.go index 2b6c1cc8fb..5db09d9ef0 100644 --- a/services/horizon/internal/ingest/processors/main.go +++ b/services/horizon/internal/ingest/processors/main.go @@ -28,7 +28,7 @@ type LedgerTransactionProcessor interface { type LedgerTransactionFilterer interface { Name() string - FilterTransaction(ctx context.Context, transaction ingest.LedgerTransaction) (bool, error) + FilterTransaction(ctx context.Context, transaction ingest.LedgerTransaction) (bool, bool, error) } func StreamLedgerTransactions( @@ -47,7 +47,7 @@ func StreamLedgerTransactions( if err != nil { return errors.Wrap(err, "could not read transaction") } - include, err := txFilterer.FilterTransaction(ctx, tx) + _, include, err := txFilterer.FilterTransaction(ctx, tx) if err != nil { return errors.Wrapf( err, diff --git a/services/horizon/internal/integration/ingestion_filtering_test.go b/services/horizon/internal/integration/ingestion_filtering_test.go index 6d0bcee69e..f35e67cada 100644 --- a/services/horizon/internal/integration/ingestion_filtering_test.go +++ b/services/horizon/internal/integration/ingestion_filtering_test.go @@ -5,12 +5,13 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stellar/go/clients/horizonclient" hProtocol "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/services/horizon/internal/ingest/filters" "github.com/stellar/go/services/horizon/internal/test/integration" "github.com/stellar/go/txnbuild" - "github.com/stretchr/testify/assert" ) func TestFilteringWithNoFilters(t *testing.T) { @@ -168,3 +169,67 @@ func TestFilteringAssetWhiteList(t *testing.T) { _, err = itest.Client().TransactionDetail(txResp.Hash) tt.NoError(err) } + +func TestFilteringAssetAndAccountFilters(t *testing.T) { + tt := assert.New(t) + const adminPort uint16 = 6000 + itest := integration.NewTest(t, integration.Config{ + HorizonIngestParameters: map[string]string{ + "admin-port": strconv.Itoa(int(adminPort)), + }, + }) + + fullKeys, accounts := itest.CreateAccounts(2, "10000") + whitelistedAccount := accounts[0] + whitelistedAccountKey := fullKeys[0] + nonWhitelistedAccount := accounts[1] + nonWhitelistedAccountKey := fullKeys[1] + enabled := true + + whitelistedAsset := txnbuild.CreditAsset{Code: "PTS", Issuer: itest.Master().Address()} + nonWhitelistedAsset := txnbuild.CreditAsset{Code: "SEK", Issuer: nonWhitelistedAccountKey.Address()} + itest.MustEstablishTrustline(whitelistedAccountKey, whitelistedAccount, nonWhitelistedAsset) + + // Setup whitelisted account and asset rule, force refresh of filter configs to be quick + filters.SetFilterConfigCheckIntervalSeconds(1) + + expectedAccountFilter := hProtocol.AccountFilterConfig{ + Whitelist: []string{whitelistedAccount.GetAccountID()}, + Enabled: &enabled, + } + err := itest.AdminClient().SetIngestionAccountFilter(expectedAccountFilter) + tt.NoError(err) + accountFilter, err := itest.AdminClient().GetIngestionAccountFilter() + tt.NoError(err) + tt.ElementsMatch(expectedAccountFilter.Whitelist, accountFilter.Whitelist) + tt.Equal(expectedAccountFilter.Enabled, accountFilter.Enabled) + + asset, err := whitelistedAsset.ToXDR() + tt.NoError(err) + expectedAssetFilter := hProtocol.AssetFilterConfig{ + Whitelist: []string{asset.StringCanonical()}, + Enabled: &enabled, + } + err = itest.AdminClient().SetIngestionAssetFilter(expectedAssetFilter) + tt.NoError(err) + assetFilter, err := itest.AdminClient().GetIngestionAssetFilter() + tt.NoError(err) + + tt.ElementsMatch(expectedAssetFilter.Whitelist, assetFilter.Whitelist) + tt.Equal(expectedAssetFilter.Enabled, assetFilter.Enabled) + + // Ensure the latest filter configs are reloaded by the ingestion state machine processor + time.Sleep(filters.GetFilterConfigCheckIntervalSeconds() * time.Second) + + // Use a non-whitelisted account to submit a non-whitelisted asset to a whitelisted account. + // The transaction should be stored. + txResp := itest.MustSubmitOperations(nonWhitelistedAccount, nonWhitelistedAccountKey, + &txnbuild.Payment{ + Destination: whitelistedAccount.GetAccountID(), + Amount: "10", + Asset: nonWhitelistedAsset, + }, + ) + _, err = itest.Client().TransactionDetail(txResp.Hash) + tt.NoError(err) +} From 79f44c65cb44c539f46cde0fa314936c0758d3d0 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Fri, 10 May 2024 14:33:28 -0700 Subject: [PATCH 172/234] exp/services/ledgerexporter: Change default compression algorithm from gzip to zstd (#5306) --------- Co-authored-by: tamirms --- exp/services/ledgerexporter/config.toml | 1 - .../ledgerexporter/internal/app_test.go | 8 +- .../ledgerexporter/internal/config_test.go | 1 - .../internal/exportmanager_test.go | 14 ++-- .../ledgerexporter/internal/uploader.go | 9 +-- .../ledgerexporter/internal/uploader_test.go | 11 ++- go.mod | 2 +- .../ledgerbackend/buffered_storage_backend.go | 26 +----- .../buffered_storage_backend_test.go | 19 ++--- ingest/ledgerbackend/ledger_buffer.go | 18 ++--- support/compressxdr/compress_xdr.go | 56 ++++++++----- support/compressxdr/compress_xdr_test.go | 33 +------- support/compressxdr/compressor.go | 38 +++++++++ support/compressxdr/gzip_compress_xdr.go | 55 ------------- support/compressxdr/mocks.go | 2 - support/datastore/ledgerbatch_config.go | 5 +- support/datastore/ledgerbatch_config_test.go | 24 +++--- support/datastore/resumablemanager_test.go | 81 ++++++++----------- 18 files changed, 162 insertions(+), 241 deletions(-) create mode 100644 support/compressxdr/compressor.go delete mode 100644 support/compressxdr/gzip_compress_xdr.go diff --git a/exp/services/ledgerexporter/config.toml b/exp/services/ledgerexporter/config.toml index bc522ff6a8..a56087427c 100644 --- a/exp/services/ledgerexporter/config.toml +++ b/exp/services/ledgerexporter/config.toml @@ -9,7 +9,6 @@ destination_bucket_path = "exporter-test/ledgers" [exporter_config] ledgers_per_file = 1 files_per_partition = 64000 - file_suffix = ".xdr.gz" [stellar_core_config] stellar_core_binary_path = "/usr/local/bin/stellar-core" diff --git a/exp/services/ledgerexporter/internal/app_test.go b/exp/services/ledgerexporter/internal/app_test.go index e2db0e28d9..2063fd21a2 100644 --- a/exp/services/ledgerexporter/internal/app_test.go +++ b/exp/services/ledgerexporter/internal/app_test.go @@ -41,7 +41,7 @@ func TestApplyResumeInvalidDataStoreLedgersPerFileBoundary(t *testing.T) { StartLedger: 3, EndLedger: 9, Resume: true, - LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50, FileSuffix: ".xdr.gz"}, + LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, } mockResumableManager := &datastore.MockResumableManager{} // simulate the datastore has inconsistent data, @@ -61,7 +61,7 @@ func TestApplyResumeWithPartialRemoteDataPresent(t *testing.T) { StartLedger: 10, EndLedger: 99, Resume: true, - LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50, FileSuffix: ".xdr.gz"}, + LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, } mockResumableManager := &datastore.MockResumableManager{} // simulates a data store that had ledger files populated up to seq=49, so the first absent ledger would be 50 @@ -80,7 +80,7 @@ func TestApplyResumeWithNoRemoteDataPresent(t *testing.T) { StartLedger: 10, EndLedger: 99, Resume: true, - LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50, FileSuffix: ".xdr.gz"}, + LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, } mockResumableManager := &datastore.MockResumableManager{} // simulates a data store that had no data in the requested range @@ -102,7 +102,7 @@ func TestApplyResumeWithNoRemoteDataAndRequestFromGenesis(t *testing.T) { StartLedger: 2, EndLedger: 99, Resume: true, - LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50, FileSuffix: ".xdr.gz"}, + LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, } mockResumableManager := &datastore.MockResumableManager{} // simulates a data store that had no data in the requested range diff --git a/exp/services/ledgerexporter/internal/config_test.go b/exp/services/ledgerexporter/internal/config_test.go index 036c121455..86f6cfb5b3 100644 --- a/exp/services/ledgerexporter/internal/config_test.go +++ b/exp/services/ledgerexporter/internal/config_test.go @@ -22,7 +22,6 @@ func TestNewConfigResumeEnabled(t *testing.T) { require.Equal(t, config.DataStoreConfig.Type, "ABC") require.Equal(t, config.LedgerBatchConfig.FilesPerPartition, uint32(1)) require.Equal(t, config.LedgerBatchConfig.LedgersPerFile, uint32(3)) - require.Equal(t, config.LedgerBatchConfig.FileSuffix, ".xdr.gz") require.True(t, config.Resume) url, ok := config.DataStoreConfig.Params["destination_bucket_path"] require.True(t, ok) diff --git a/exp/services/ledgerexporter/internal/exportmanager_test.go b/exp/services/ledgerexporter/internal/exportmanager_test.go index 41a5e6acf8..f3b302b739 100644 --- a/exp/services/ledgerexporter/internal/exportmanager_test.go +++ b/exp/services/ledgerexporter/internal/exportmanager_test.go @@ -38,7 +38,7 @@ func (s *ExportManagerSuite) TearDownTest() { } func (s *ExportManagerSuite) TestInvalidExportConfig() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 0, FilesPerPartition: 10, FileSuffix: ".xdr.gz"} + config := datastore.LedgerBatchConfig{LedgersPerFile: 0, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) _, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -46,7 +46,7 @@ func (s *ExportManagerSuite) TestInvalidExportConfig() { } func (s *ExportManagerSuite) TestRun() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 64, FilesPerPartition: 10, FileSuffix: ".xdr.gz"} + config := datastore.LedgerBatchConfig{LedgersPerFile: 64, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -96,7 +96,7 @@ func (s *ExportManagerSuite) TestRun() { } func (s *ExportManagerSuite) TestRunContextCancel() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 1, FileSuffix: ".xdr.gz"} + config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 1} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -125,7 +125,7 @@ func (s *ExportManagerSuite) TestRunContextCancel() { } func (s *ExportManagerSuite) TestRunWithCanceledContext() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10, FileSuffix: ".xdr.gz"} + config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -138,7 +138,7 @@ func (s *ExportManagerSuite) TestRunWithCanceledContext() { } func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10, FileSuffix: ".xdr.gz"} + config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -176,7 +176,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { } func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10, FileSuffix: ".xdr.gz"} + config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -194,7 +194,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { } func (s *ExportManagerSuite) TestAddLedgerCloseMetaKeyMismatch() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 1, FileSuffix: ".xdr.gz"} + config := datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 1} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) diff --git a/exp/services/ledgerexporter/internal/uploader.go b/exp/services/ledgerexporter/internal/uploader.go index ca07965b85..c1f80d6416 100644 --- a/exp/services/ledgerexporter/internal/uploader.go +++ b/exp/services/ledgerexporter/internal/uploader.go @@ -83,12 +83,7 @@ func (u Uploader) Upload(ctx context.Context, metaArchive *datastore.LedgerMetaA startTime := time.Now() numLedgers := strconv.FormatUint(uint64(metaArchive.GetLedgerCount()), 10) - // TODO: Add compression config and optimize best compression algorithm - // JIRA https://stellarorg.atlassian.net/browse/HUBBLE-368 - xdrEncoder, err := compressxdr.NewXDREncoder(compressxdr.GZIP, &metaArchive.Data) - if err != nil { - return err - } + xdrEncoder := compressxdr.NewXDREncoder(compressxdr.DefaultCompressor, &metaArchive.Data) writerTo := &writerToRecorder{ WriterTo: xdrEncoder, @@ -109,7 +104,7 @@ func (u Uploader) Upload(ctx context.Context, metaArchive *datastore.LedgerMetaA "already_exists": alreadyExists, }).Observe(float64(writerTo.totalUncompressed)) u.objectSizeMetrics.With(prometheus.Labels{ - "compression": compressxdr.GZIP, + "compression": xdrEncoder.Compressor.Name(), "ledgers": numLedgers, "already_exists": alreadyExists, }).Observe(float64(writerTo.totalCompressed)) diff --git a/exp/services/ledgerexporter/internal/uploader_test.go b/exp/services/ledgerexporter/internal/uploader_test.go index ee39245486..da0cc6d2ad 100644 --- a/exp/services/ledgerexporter/internal/uploader_test.go +++ b/exp/services/ledgerexporter/internal/uploader_test.go @@ -62,11 +62,10 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { expectedCompressedLength := capturedBuf.Len() var decodedArchive datastore.LedgerMetaArchive - xdrDecoder, err := compressxdr.NewXDRDecoder(compressxdr.GZIP, &decodedArchive.Data) - require.NoError(s.T(), err) + xdrDecoder := compressxdr.NewXDRDecoder(compressxdr.DefaultCompressor, &decodedArchive.Data) decoder := xdrDecoder - _, err = decoder.ReadFrom(&capturedBuf) + _, err := decoder.ReadFrom(&capturedBuf) require.NoError(s.T(), err) // require that the decoded data matches the original test data @@ -98,7 +97,7 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { metric, err = dataUploader.objectSizeMetrics.MetricVec.GetMetricWith(prometheus.Labels{ "ledgers": "100", - "compression": compressxdr.GZIP, + "compression": decoder.Compressor.Name(), "already_exists": strconv.FormatBool(alreadyExists), }) require.NoError(s.T(), err) @@ -114,7 +113,7 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { ) metric, err = dataUploader.objectSizeMetrics.MetricVec.GetMetricWith(prometheus.Labels{ "ledgers": "100", - "compression": compressxdr.GZIP, + "compression": decoder.Compressor.Name(), "already_exists": strconv.FormatBool(!alreadyExists), }) require.NoError(s.T(), err) @@ -185,7 +184,7 @@ func (s *UploaderSuite) testUploadPutError(putOkReturnVal bool) { getMetricValue(metric).GetSummary().GetSampleCount(), ) - for _, compression := range []string{compressxdr.GZIP, "none"} { + for _, compression := range []string{compressxdr.DefaultCompressor.Name(), "none"} { metric, err = dataUploader.objectSizeMetrics.MetricVec.GetMetricWith(prometheus.Labels{ "ledgers": "100", "compression": compression, diff --git a/go.mod b/go.mod index bb6a691f40..4f2e0fd865 100644 --- a/go.mod +++ b/go.mod @@ -122,7 +122,7 @@ require ( github.com/imkira/go-interpol v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/klauspost/compress v1.17.0 // indirect + github.com/klauspost/compress v1.17.0 github.com/kr/text v0.2.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect diff --git a/ingest/ledgerbackend/buffered_storage_backend.go b/ingest/ledgerbackend/buffered_storage_backend.go index b28e68b63c..c3c1007e87 100644 --- a/ingest/ledgerbackend/buffered_storage_backend.go +++ b/ingest/ledgerbackend/buffered_storage_backend.go @@ -10,7 +10,6 @@ import ( "github.com/pkg/errors" - "github.com/stellar/go/support/compressxdr" "github.com/stellar/go/support/datastore" "github.com/stellar/go/xdr" ) @@ -20,7 +19,6 @@ var _ LedgerBackend = (*BufferedStorageBackend)(nil) type BufferedStorageBackendConfig struct { LedgerBatchConfig datastore.LedgerBatchConfig - CompressionType string DataStore datastore.DataStore BufferSize uint32 NumWorkers uint32 @@ -42,7 +40,6 @@ type BufferedStorageBackend struct { prepared *Range // Non-nil if any range is prepared closed bool // False until the core is closed ledgerMetaArchive *datastore.LedgerMetaArchive - decoder compressxdr.XDRDecoder nextLedger uint32 lastLedger uint32 } @@ -65,25 +62,12 @@ func NewBufferedStorageBackend(ctx context.Context, config BufferedStorageBacken return nil, errors.New("ledgersPerFile must be > 0") } - if config.LedgerBatchConfig.FileSuffix == "" { - return nil, errors.New("no file suffix provided in LedgerBatchConfig") - } - - if config.CompressionType == "" { - return nil, errors.New("no compression type provided in config") - } - ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) - decoder, err := compressxdr.NewXDRDecoder(config.CompressionType, nil) - if err != nil { - return nil, err - } bsBackend := &BufferedStorageBackend{ config: config, dataStore: config.DataStore, ledgerMetaArchive: ledgerMetaArchive, - decoder: decoder, } return bsBackend, nil @@ -125,17 +109,11 @@ func (bsb *BufferedStorageBackend) getBatchForSequence(ctx context.Context, sequ } // Sequence is beyond the current LedgerCloseMetaBatch - lcmBatchBinary, err := bsb.ledgerBuffer.getFromLedgerQueue(ctx) + var err error + bsb.ledgerMetaArchive.Data, err = bsb.ledgerBuffer.getFromLedgerQueue(ctx) if err != nil { return errors.Wrap(err, "failed getting next ledger batch from queue") } - - // Turn binary into xdr - err = bsb.ledgerMetaArchive.Data.UnmarshalBinary(lcmBatchBinary) - if err != nil { - return errors.Wrap(err, "failed unmarshalling lcmBatchBinary") - } - return nil } diff --git a/ingest/ledgerbackend/buffered_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go index eb2e355765..063e234c31 100644 --- a/ingest/ledgerbackend/buffered_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -29,14 +29,12 @@ func createBufferedStorageBackendConfigForTesting() BufferedStorageBackendConfig ledgerBatchConfig := datastore.LedgerBatchConfig{ LedgersPerFile: 1, FilesPerPartition: 64000, - FileSuffix: ".xdr.gz", } dataStore := new(datastore.MockDataStore) return BufferedStorageBackendConfig{ LedgerBatchConfig: ledgerBatchConfig, - CompressionType: compressxdr.GZIP, DataStore: dataStore, BufferSize: 100, NumWorkers: 5, @@ -48,13 +46,11 @@ func createBufferedStorageBackendConfigForTesting() BufferedStorageBackendConfig func createBufferedStorageBackendForTesting() BufferedStorageBackend { config := createBufferedStorageBackendConfigForTesting() ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) - decoder, _ := compressxdr.NewXDRDecoder(config.CompressionType, nil) return BufferedStorageBackend{ config: config, dataStore: config.DataStore, ledgerMetaArchive: ledgerMetaArchive, - decoder: decoder, } } @@ -67,10 +63,10 @@ func createMockdataStore(t *testing.T, start, end, partitionSize, count uint32) if count > 1 { endFileSeq := i + count - 1 readCloser = createLCMBatchReader(i, endFileSeq, count) - objectName = fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d-%d.xdr.gz", partition, math.MaxUint32-i, i, endFileSeq) + objectName = fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d-%d.xdr.zstd", partition, math.MaxUint32-i, i, endFileSeq) } else { readCloser = createLCMBatchReader(i, i, count) - objectName = fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.gz", partition, math.MaxUint32-i, i) + objectName = fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.zstd", partition, math.MaxUint32-i, i) } mockDataStore.On("GetFile", mock.Anything, objectName).Return(readCloser, nil) } @@ -105,7 +101,7 @@ func createTestLedgerCloseMetaBatch(startSeq, endSeq, count uint32) xdr.LedgerCl func createLCMBatchReader(start, end, count uint32) io.ReadCloser { testData := createTestLedgerCloseMetaBatch(start, end, count) - encoder, _ := compressxdr.NewXDREncoder(compressxdr.GZIP, testData) + encoder := compressxdr.NewXDREncoder(compressxdr.DefaultCompressor, testData) var buf bytes.Buffer encoder.WriteTo(&buf) capturedBuf := buf.Bytes() @@ -121,7 +117,6 @@ func TestNewBufferedStorageBackend(t *testing.T) { assert.NoError(t, err) assert.Equal(t, bsb.dataStore, config.DataStore) - assert.Equal(t, ".xdr.gz", bsb.config.LedgerBatchConfig.FileSuffix) assert.Equal(t, uint32(1), bsb.config.LedgerBatchConfig.LedgersPerFile) assert.Equal(t, uint32(64000), bsb.config.LedgerBatchConfig.FilesPerPartition) assert.Equal(t, uint32(100), bsb.config.BufferSize) @@ -441,7 +436,7 @@ func TestLedgerBufferClose(t *testing.T) { mockDataStore := new(datastore.MockDataStore) partition := ledgerPerFileCount*partitionSize - 1 - objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.gz", partition, math.MaxUint32-3, 3) + objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.zstd", partition, math.MaxUint32-3, 3) afterPrepareRange := make(chan struct{}) mockDataStore.On("GetFile", mock.Anything, objectName).Return(io.NopCloser(&bytes.Buffer{}), context.Canceled).Run(func(args mock.Arguments) { <-afterPrepareRange @@ -473,7 +468,7 @@ func TestLedgerBufferBoundedObjectNotFound(t *testing.T) { mockDataStore := new(datastore.MockDataStore) partition := ledgerPerFileCount*partitionSize - 1 - objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.gz", partition, math.MaxUint32-3, 3) + objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.zstd", partition, math.MaxUint32-3, 3) mockDataStore.On("GetFile", mock.Anything, objectName).Return(io.NopCloser(&bytes.Buffer{}), os.ErrNotExist).Once() t.Cleanup(func() { mockDataStore.AssertExpectations(t) @@ -499,7 +494,7 @@ func TestLedgerBufferUnboundedObjectNotFound(t *testing.T) { mockDataStore := new(datastore.MockDataStore) partition := ledgerPerFileCount*partitionSize - 1 - objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.gz", partition, math.MaxUint32-3, 3) + objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.zstd", partition, math.MaxUint32-3, 3) iteration := &atomic.Int32{} cancelAfter := int32(bsb.config.RetryLimit) + 2 mockDataStore.On("GetFile", mock.Anything, objectName).Return(io.NopCloser(&bytes.Buffer{}), os.ErrNotExist).Run(func(args mock.Arguments) { @@ -531,7 +526,7 @@ func TestLedgerBufferRetryLimit(t *testing.T) { mockDataStore := new(datastore.MockDataStore) partition := ledgerPerFileCount*partitionSize - 1 - objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.gz", partition, math.MaxUint32-3, 3) + objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.zstd", partition, math.MaxUint32-3, 3) mockDataStore.On("GetFile", mock.Anything, objectName). Return(io.NopCloser(&bytes.Buffer{}), fmt.Errorf("transient error")). Times(int(bsb.config.RetryLimit) + 1) diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index a26aab8ba2..5b2ec57ffc 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -13,6 +13,7 @@ import ( "github.com/stellar/go/support/collections/heap" "github.com/stellar/go/support/compressxdr" "github.com/stellar/go/support/datastore" + "github.com/stellar/go/xdr" ) type ledgerBatchObject struct { @@ -24,7 +25,6 @@ type ledgerBuffer struct { // Passed through from BufferedStorageBackend to control lifetime of ledgerBuffer instance config BufferedStorageBackendConfig dataStore datastore.DataStore - decoder compressxdr.XDRDecoder // context used to cancel workers within the ledgerBuffer context context.Context @@ -67,7 +67,6 @@ func (bsb *BufferedStorageBackend) newLedgerBuffer(ledgerRange Range) (*ledgerBu ledgerRange: ledgerRange, context: ctx, cancel: cancel, - decoder: bsb.decoder, } // Start workers to read LCM files @@ -203,13 +202,13 @@ func (lb *ledgerBuffer) storeObject(ledgerObject []byte, sequence uint32) { } } -func (lb *ledgerBuffer) getFromLedgerQueue(ctx context.Context) ([]byte, error) { +func (lb *ledgerBuffer) getFromLedgerQueue(ctx context.Context) (xdr.LedgerCloseMetaBatch, error) { for { select { case <-lb.context.Done(): - return nil, context.Cause(lb.context) + return xdr.LedgerCloseMetaBatch{}, context.Cause(lb.context) case <-ctx.Done(): - return nil, ctx.Err() + return xdr.LedgerCloseMetaBatch{}, ctx.Err() case compressedBinary := <-lb.ledgerQueue: // The ledger buffer invariant is maintained here because // we create an extra task when consuming one item from the ledger queue. @@ -218,13 +217,14 @@ func (lb *ledgerBuffer) getFromLedgerQueue(ctx context.Context) ([]byte, error) // len(taskQueue) + len(ledgerQueue) + ledgerPriorityQueue.Len() <= bsb.config.BufferSize lb.pushTaskQueue() - reader := bytes.NewReader(compressedBinary) - lcmBinary, err := lb.decoder.Unzip(reader) + lcmBatch := xdr.LedgerCloseMetaBatch{} + decoder := compressxdr.NewXDRDecoder(compressxdr.DefaultCompressor, &lcmBatch) + _, err := decoder.ReadFrom(bytes.NewReader(compressedBinary)) if err != nil { - return nil, err + return xdr.LedgerCloseMetaBatch{}, err } - return lcmBinary, nil + return lcmBatch, nil } } } diff --git a/support/compressxdr/compress_xdr.go b/support/compressxdr/compress_xdr.go index c43a1bca77..514251f971 100644 --- a/support/compressxdr/compress_xdr.go +++ b/support/compressxdr/compress_xdr.go @@ -3,36 +3,50 @@ package compressxdr import ( "io" - "github.com/stellar/go/support/errors" + xdr3 "github.com/stellar/go-xdr/xdr3" ) -const ( - GZIP = "gzip" -) +func NewXDREncoder(compressor Compressor, xdrPayload interface{}) XDREncoder { + return XDREncoder{Compressor: compressor, XdrPayload: xdrPayload} +} + +func NewXDRDecoder(compressor Compressor, xdrPayload interface{}) XDRDecoder { + return XDRDecoder{Compressor: compressor, XdrPayload: xdrPayload} -type XDREncoder interface { - WriteTo(w io.Writer) (int64, error) } -type XDRDecoder interface { - ReadFrom(r io.Reader) (int64, error) - Unzip(r io.Reader) ([]byte, error) +// XDREncoder combines compression with XDR encoding +type XDREncoder struct { + Compressor Compressor + XdrPayload interface{} } -func NewXDREncoder(compressionType string, xdrPayload interface{}) (XDREncoder, error) { - switch compressionType { - case GZIP: - return &XDRGzipEncoder{XdrPayload: xdrPayload}, nil - default: - return nil, errors.Errorf("invalid compression type %s, not supported", compressionType) +// WriteTo writes the XDR compressed encoded data +func (e XDREncoder) WriteTo(w io.Writer) (int64, error) { + zw, err := e.Compressor.NewWriter(w) + if err != nil { + return 0, err } + defer zw.Close() + + n, err := xdr3.Marshal(zw, e.XdrPayload) + return int64(n), err } -func NewXDRDecoder(compressionType string, xdrPayload interface{}) (XDRDecoder, error) { - switch compressionType { - case GZIP: - return &XDRGzipDecoder{XdrPayload: xdrPayload}, nil - default: - return nil, errors.Errorf("invalid compression type %s, not supported", compressionType) +// XDRDecoder combines decompression with XDR decoding +type XDRDecoder struct { + Compressor Compressor + XdrPayload interface{} +} + +// ReadFrom reads XDR compressed encoded data +func (d XDRDecoder) ReadFrom(r io.Reader) (int64, error) { + zr, err := d.Compressor.NewReader(r) + if err != nil { + return 0, err } + defer zr.Close() + + n, err := xdr3.Unmarshal(zr, d.XdrPayload) + return int64(n), err } diff --git a/support/compressxdr/compress_xdr_test.go b/support/compressxdr/compress_xdr_test.go index da94ab25ae..82aa7e186c 100644 --- a/support/compressxdr/compress_xdr_test.go +++ b/support/compressxdr/compress_xdr_test.go @@ -20,21 +20,19 @@ func createTestLedgerCloseMetaBatch(startSeq, endSeq uint32, count int) xdr.Ledg } } -func TestEncodeDecodeLedgerCloseMetaBatchGzip(t *testing.T) { +func TestEncodeDecodeLedgerCloseMetaBatch(t *testing.T) { testData := createTestLedgerCloseMetaBatch(1000, 1005, 6) // Encode the test data - encoder, err := NewXDREncoder(GZIP, testData) - require.NoError(t, err) + encoder := NewXDREncoder(DefaultCompressor, testData) var buf bytes.Buffer - _, err = encoder.WriteTo(&buf) + _, err := encoder.WriteTo(&buf) require.NoError(t, err) // Decode the encoded data lcmBatch := xdr.LedgerCloseMetaBatch{} - decoder, err := NewXDRDecoder(GZIP, &lcmBatch) - require.NoError(t, err) + decoder := NewXDRDecoder(DefaultCompressor, &lcmBatch) _, err = decoder.ReadFrom(&buf) require.NoError(t, err) @@ -48,26 +46,3 @@ func TestEncodeDecodeLedgerCloseMetaBatchGzip(t *testing.T) { require.Equal(t, testData.LedgerCloseMetas[i], decodedData.LedgerCloseMetas[i]) } } - -func TestDecodeUnzipGzip(t *testing.T) { - expectedBinary := []byte{0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0} - testData := createTestLedgerCloseMetaBatch(2, 2, 1) - - // Encode the test data - encoder, err := NewXDREncoder(GZIP, testData) - require.NoError(t, err) - - var buf bytes.Buffer - _, err = encoder.WriteTo(&buf) - require.NoError(t, err) - - // Decode the encoded data - lcmBatch := xdr.LedgerCloseMetaBatch{} - decoder, err := NewXDRDecoder(GZIP, &lcmBatch) - require.NoError(t, err) - - binary, err := decoder.Unzip(&buf) - require.NoError(t, err) - - require.Equal(t, expectedBinary, binary) -} diff --git a/support/compressxdr/compressor.go b/support/compressxdr/compressor.go new file mode 100644 index 0000000000..946e1b89f0 --- /dev/null +++ b/support/compressxdr/compressor.go @@ -0,0 +1,38 @@ +package compressxdr + +import ( + "io" + + "github.com/klauspost/compress/zstd" +) + +var DefaultCompressor = &ZstdCompressor{} + +// Compressor represents a compression algorithm. +type Compressor interface { + NewWriter(w io.Writer) (io.WriteCloser, error) + NewReader(r io.Reader) (io.ReadCloser, error) + Name() string +} + +// ZstdCompressor is an implementation of the Compressor interface for Zstd compression. +type ZstdCompressor struct{} + +// GetName returns the name of the compression algorithm. +func (z ZstdCompressor) Name() string { + return "zstd" +} + +// NewWriter creates a new Zstd writer. +func (z ZstdCompressor) NewWriter(w io.Writer) (io.WriteCloser, error) { + return zstd.NewWriter(w) +} + +// NewReader creates a new Zstd reader. +func (z ZstdCompressor) NewReader(r io.Reader) (io.ReadCloser, error) { + zr, err := zstd.NewReader(r) + if err != nil { + return nil, err + } + return zr.IOReadCloser(), err +} diff --git a/support/compressxdr/gzip_compress_xdr.go b/support/compressxdr/gzip_compress_xdr.go deleted file mode 100644 index 9171b87ffd..0000000000 --- a/support/compressxdr/gzip_compress_xdr.go +++ /dev/null @@ -1,55 +0,0 @@ -package compressxdr - -import ( - "compress/gzip" - "io" - - xdr3 "github.com/stellar/go-xdr/xdr3" -) - -type XDRGzipEncoder struct { - XdrPayload interface{} -} - -func (g *XDRGzipEncoder) WriteTo(w io.Writer) (int64, error) { - gw := gzip.NewWriter(w) - n, err := xdr3.Marshal(gw, g.XdrPayload) - if err != nil { - return int64(n), err - } - return int64(n), gw.Close() -} - -type XDRGzipDecoder struct { - XdrPayload interface{} -} - -func (d *XDRGzipDecoder) ReadFrom(r io.Reader) (int64, error) { - gr, err := gzip.NewReader(r) - if err != nil { - return 0, err - } - defer gr.Close() - - n, err := xdr3.Unmarshal(gr, d.XdrPayload) - if err != nil { - return int64(n), err - } - return int64(n), nil -} - -func (d *XDRGzipDecoder) Unzip(r io.Reader) ([]byte, error) { - gzipReader, err := gzip.NewReader(r) - if err != nil { - return nil, err - } - - defer gzipReader.Close() - - objectBytes, err := io.ReadAll(gzipReader) - if err != nil { - return nil, err - } - - return objectBytes, nil -} diff --git a/support/compressxdr/mocks.go b/support/compressxdr/mocks.go index 2525ca4477..c6d6909f43 100644 --- a/support/compressxdr/mocks.go +++ b/support/compressxdr/mocks.go @@ -19,5 +19,3 @@ func (m *MockXDRDecoder) Unzip(r io.Reader) ([]byte, error) { args := m.Called(r) return args.Get(0).([]byte), args.Error(1) } - -var _ XDRDecoder = &MockXDRDecoder{} diff --git a/support/datastore/ledgerbatch_config.go b/support/datastore/ledgerbatch_config.go index cd3a0f45bf..4ebac0d110 100644 --- a/support/datastore/ledgerbatch_config.go +++ b/support/datastore/ledgerbatch_config.go @@ -3,12 +3,13 @@ package datastore import ( "fmt" "math" + + "github.com/stellar/go/support/compressxdr" ) type LedgerBatchConfig struct { LedgersPerFile uint32 `toml:"ledgers_per_file"` FilesPerPartition uint32 `toml:"files_per_partition"` - FileSuffix string `toml:"file_suffix"` } func (ec LedgerBatchConfig) GetSequenceNumberStartBoundary(ledgerSeq uint32) uint32 { @@ -42,7 +43,7 @@ func (ec LedgerBatchConfig) GetObjectKeyFromSequenceNumber(ledgerSeq uint32) str if fileStart != fileEnd { objectKey += fmt.Sprintf("-%d", fileEnd) } - objectKey += ec.FileSuffix + objectKey += fmt.Sprintf(".xdr.%s", compressxdr.DefaultCompressor.Name()) return objectKey } diff --git a/support/datastore/ledgerbatch_config_test.go b/support/datastore/ledgerbatch_config_test.go index 255b5ed070..bd79dd265d 100644 --- a/support/datastore/ledgerbatch_config_test.go +++ b/support/datastore/ledgerbatch_config_test.go @@ -15,24 +15,23 @@ func TestGetObjectKeyFromSequenceNumber(t *testing.T) { filesPerPartition uint32 ledgerSeq uint32 ledgersPerFile uint32 - fileSuffix string expectedKey string }{ - {0, 5, 1, ".xdr.gz", "FFFFFFFA--5.xdr.gz"}, - {0, 5, 10, ".xdr.gz", "FFFFFFFF--0-9.xdr.gz"}, - {2, 10, 100, ".xdr.gz", "FFFFFFFF--0-199/FFFFFFFF--0-99.xdr.gz"}, - {2, 150, 50, ".xdr.gz", "FFFFFF9B--100-199/FFFFFF69--150-199.xdr.gz"}, - {2, 300, 200, ".xdr.gz", "FFFFFFFF--0-399/FFFFFF37--200-399.xdr.gz"}, - {2, 1, 1, ".xdr.gz", "FFFFFFFF--0-1/FFFFFFFE--1.xdr.gz"}, - {4, 10, 100, ".xdr.gz", "FFFFFFFF--0-399/FFFFFFFF--0-99.xdr.gz"}, - {4, 250, 50, ".xdr.gz", "FFFFFF37--200-399/FFFFFF05--250-299.xdr.gz"}, - {1, 300, 200, ".xdr.gz", "FFFFFF37--200-399.xdr.gz"}, - {1, 1, 1, ".xdr.gz", "FFFFFFFE--1.xdr.gz"}, + {0, 5, 1, "FFFFFFFA--5.xdr.zstd"}, + {0, 5, 10, "FFFFFFFF--0-9.xdr.zstd"}, + {2, 10, 100, "FFFFFFFF--0-199/FFFFFFFF--0-99.xdr.zstd"}, + {2, 150, 50, "FFFFFF9B--100-199/FFFFFF69--150-199.xdr.zstd"}, + {2, 300, 200, "FFFFFFFF--0-399/FFFFFF37--200-399.xdr.zstd"}, + {2, 1, 1, "FFFFFFFF--0-1/FFFFFFFE--1.xdr.zstd"}, + {4, 10, 100, "FFFFFFFF--0-399/FFFFFFFF--0-99.xdr.zstd"}, + {4, 250, 50, "FFFFFF37--200-399/FFFFFF05--250-299.xdr.zstd"}, + {1, 300, 200, "FFFFFF37--200-399.xdr.zstd"}, + {1, 1, 1, "FFFFFFFE--1.xdr.zstd"}, } for _, tc := range testCases { t.Run(fmt.Sprintf("LedgerSeq-%d-LedgersPerFile-%d", tc.ledgerSeq, tc.ledgersPerFile), func(t *testing.T) { - config := LedgerBatchConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile, FileSuffix: tc.fileSuffix} + config := LedgerBatchConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile} key := config.GetObjectKeyFromSequenceNumber(tc.ledgerSeq) require.Equal(t, tc.expectedKey, key) }) @@ -43,7 +42,6 @@ func TestGetObjectKeyFromSequenceNumber_ObjectKeyDescOrder(t *testing.T) { config := LedgerBatchConfig{ LedgersPerFile: 1, FilesPerPartition: 10, - FileSuffix: ".xdr.gz", } sequenceCount := 10000 sequenceMap := make(map[uint32]string) diff --git a/support/datastore/resumablemanager_test.go b/support/datastore/resumablemanager_test.go index 726854164f..05416658a8 100644 --- a/support/datastore/resumablemanager_test.go +++ b/support/datastore/resumablemanager_test.go @@ -32,7 +32,6 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), - FileSuffix: ".xdr.gz", }, networkName: "test", errorSnippet: "archive error", @@ -47,7 +46,6 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), - FileSuffix: ".xdr.gz", }, networkName: "test", }, @@ -60,7 +58,6 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), - FileSuffix: ".xdr.gz", }, networkName: "test", }, @@ -73,7 +70,6 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), - FileSuffix: ".xdr.gz", }, networkName: "test", errorSnippet: "datastore error happened", @@ -87,7 +83,6 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), - FileSuffix: ".xdr.gz", }, networkName: "test", }, @@ -100,7 +95,6 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), - FileSuffix: ".xdr.gz", }, networkName: "test", }, @@ -113,7 +107,6 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), - FileSuffix: ".xdr.gz", }, networkName: "test", }, @@ -126,7 +119,6 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), - FileSuffix: ".xdr.gz", }, networkName: "test", }, @@ -139,7 +131,6 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), - FileSuffix: ".xdr.gz", }, networkName: "test", errorSnippet: "Invalid start value", @@ -153,7 +144,6 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), - FileSuffix: ".xdr.gz", }, networkName: "test2", latestLedger: uint32(2000), @@ -167,7 +157,6 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), - FileSuffix: ".xdr.gz", }, networkName: "test3", latestLedger: uint32(3000), @@ -181,7 +170,6 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), - FileSuffix: ".xdr.gz", }, networkName: "test4", latestLedger: uint32(4000), @@ -195,7 +183,6 @@ func TestResumability(t *testing.T) { ledgerBatchConfig: LedgerBatchConfig{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), - FileSuffix: ".xdr.gz", }, networkName: "test5", latestLedger: uint32(5000), @@ -208,58 +195,58 @@ func TestResumability(t *testing.T) { mockDataStore := &MockDataStore{} //"End ledger same as start, data store has it" - mockDataStore.On("Exists", ctx, "FFFFFFFF--0-9.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFFF--0-9.xdr.zstd").Return(true, nil).Once() //"End ledger same as start, data store does not have it" - mockDataStore.On("Exists", ctx, "FFFFFFF5--10-19.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFF5--10-19.xdr.zstd").Return(false, nil).Once() //"binary search encounters an error during datastore retrieval", - mockDataStore.On("Exists", ctx, "FFFFFFEB--20-29.xdr.gz").Return(false, errors.New("datastore error happened")).Once() + mockDataStore.On("Exists", ctx, "FFFFFFEB--20-29.xdr.zstd").Return(false, errors.New("datastore error happened")).Once() //"Data store is beyond boundary aligned start ledger" - mockDataStore.On("Exists", ctx, "FFFFFFE1--30-39.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFFD7--40-49.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFE1--30-39.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFD7--40-49.xdr.zstd").Return(false, nil).Once() //"Data store is beyond non boundary aligned start ledger" - mockDataStore.On("Exists", ctx, "FFFFFFB9--70-79.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFFAF--80-89.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFB9--70-79.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFAF--80-89.xdr.zstd").Return(false, nil).Once() //"Data store is beyond start and end ledger" - mockDataStore.On("Exists", ctx, "FFFFFEFB--260-269.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFEF1--270-279.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFEFB--260-269.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFEF1--270-279.xdr.zstd").Return(true, nil).Once() //"Data store is not beyond start ledger" - mockDataStore.On("Exists", ctx, "FFFFFF91--110-119.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFF9B--100-109.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFFA5--90-99.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFF91--110-119.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFF9B--100-109.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFA5--90-99.xdr.zstd").Return(false, nil).Once() //"No end ledger provided, data store not beyond start" uses latest from network="test2" - mockDataStore.On("Exists", ctx, "FFFFF9A1--1630-1639.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFA91--1390-1399.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFB13--1260-1269.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFB4F--1200-1209.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFB77--1160-1169.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFB6D--1170-1179.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFB81--1150-1159.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFB8B--1140-1149.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF9A1--1630-1639.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFA91--1390-1399.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB13--1260-1269.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB4F--1200-1209.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB77--1160-1169.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB6D--1170-1179.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB81--1150-1159.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB8B--1140-1149.xdr.zstd").Return(false, nil).Once() //"No end ledger provided, data store is beyond start" uses latest from network="test3" - mockDataStore.On("Exists", ctx, "FFFFF5B9--2630-2639.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF6A9--2390-2399.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF72B--2260-2269.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF735--2250-2259.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF73F--2240-2249.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF749--2230-2239.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF767--2200-2209.xdr.gz").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF5B9--2630-2639.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF6A9--2390-2399.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF72B--2260-2269.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF735--2250-2259.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF73F--2240-2249.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF749--2230-2239.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF767--2200-2209.xdr.zstd").Return(true, nil).Once() //"No end ledger provided, data store is beyond start and archive network latest, and partially into checkpoint frequency padding" uses latest from network="test4" - mockDataStore.On("Exists", ctx, "FFFFF1D1--3630-3639.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF0D7--3880-3889.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF05F--4000-4009.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF023--4060-4069.xdr.gz").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF005--4090-4099.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF00F--4080-4089.xdr.gz").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF019--4070-4079.xdr.gz").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF1D1--3630-3639.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF0D7--3880-3889.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF05F--4000-4009.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF023--4060-4069.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF005--4090-4099.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF00F--4080-4089.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF019--4070-4079.xdr.zstd").Return(false, nil).Once() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { From 90e24d8e6f23e20b8ea22b32d498c1716c2a54d2 Mon Sep 17 00:00:00 2001 From: tamirms Date: Wed, 15 May 2024 06:01:33 +0100 Subject: [PATCH 173/234] Add tests for GCS datastore (#5313) --- go.mod | 45 +++--- go.sum | 111 +++++++++----- support/datastore/datastore.go | 6 +- support/datastore/gcs_datastore.go | 51 +++---- support/datastore/gcs_test.go | 235 +++++++++++++++++++++++++++++ 5 files changed, 361 insertions(+), 87 deletions(-) create mode 100644 support/datastore/gcs_test.go diff --git a/go.mod b/go.mod index 4f2e0fd865..b1edd86933 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22 toolchain go1.22.1 require ( - cloud.google.com/go/firestore v1.14.0 // indirect + cloud.google.com/go/firestore v1.15.0 // indirect cloud.google.com/go/storage v1.40.0 firebase.google.com/go v3.12.0+incompatible github.com/2opremio/pretty v0.2.2-0.20230601220618-e1d5758b2a95 @@ -52,7 +52,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8 github.com/xdrpp/goxdr v0.1.1 - google.golang.org/api v0.170.0 + google.golang.org/api v0.177.0 gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0 gopkg.in/square/go-jose.v2 v2.4.1 gopkg.in/tylerb/graceful.v1 v1.2.15 @@ -60,14 +60,17 @@ require ( require ( github.com/cenkalti/backoff/v4 v4.2.1 - golang.org/x/sync v0.6.0 + github.com/fsouza/fake-gcs-server v1.49.0 + golang.org/x/sync v0.7.0 ) require ( - cloud.google.com/go/compute v1.24.0 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v1.1.7 // indirect - cloud.google.com/go/longrunning v0.5.5 // indirect + cloud.google.com/go/auth v0.3.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/iam v1.1.8 // indirect + cloud.google.com/go/longrunning v0.5.6 // indirect + cloud.google.com/go/pubsub v1.37.0 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect @@ -79,11 +82,15 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/gobuffalo/packd v1.0.2 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect + github.com/google/renameio/v2 v2.0.0 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/gorilla/handlers v1.5.2 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/nxadm/tail v1.4.8 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pkg/xattr v0.4.9 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -98,15 +105,15 @@ require ( go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.13.0 // indirect golang.org/x/tools v0.14.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect gopkg.in/djherbis/atime.v1 v1.0.0 // indirect gopkg.in/djherbis/stream.v1 v1.3.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) require ( - cloud.google.com/go v0.112.1 // indirect + cloud.google.com/go v0.112.2 // indirect github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/goreplay v1.3.2 @@ -122,7 +129,7 @@ require ( github.com/imkira/go-interpol v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/klauspost/compress v1.17.0 + github.com/klauspost/compress v1.17.6 github.com/kr/text v0.2.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect @@ -147,18 +154,18 @@ require ( github.com/yudai/golcs v0.0.0-20150405163532-d1c525dea8ce // indirect github.com/yudai/pp v2.0.1+incompatible // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.21.0 // indirect + golang.org/x/crypto v0.22.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d - golang.org/x/net v0.23.0 // indirect - golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/oauth2 v0.20.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect - google.golang.org/grpc v1.62.1 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect + google.golang.org/grpc v1.63.2 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6f4c2d37b2..ab95f599dd 100644 --- a/go.sum +++ b/go.sum @@ -17,30 +17,36 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= -cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= +cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw= +cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= +cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs= +cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= +cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= -cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.14.0 h1:8aLcKnMPoldYU3YHgu4t2exrKhLQkqaXAGqT0ljrFVw= -cloud.google.com/go/firestore v1.14.0/go.mod h1:96MVaHLsEhbvkBEdZgfN+AS/GIkco1LRpH9Xp9YZfzQ= -cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= -cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= -cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg= -cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s= +cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8= +cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= +cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= +cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= +cloud.google.com/go/kms v1.15.8 h1:szIeDCowID8th2i8XE4uRev5PMxQFqW+JjwYxL9h6xs= +cloud.google.com/go/kms v1.15.8/go.mod h1:WoUHcDjD9pluCg7pNds131awnH429QGvRM3N/4MyoVs= +cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE= +cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.37.0 h1:0uEEfaB1VIJzabPpwpZf44zWAKAme3zwKKxHk7vJQxQ= +cloud.google.com/go/pubsub v1.37.0/go.mod h1:YQOQr1uiUM092EXwKs56OPT650nwnawc+8/IjoUeGzQ= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= @@ -108,6 +114,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/djherbis/fscache v0.10.1 h1:hDv+RGyvD+UDKyRYuLoVNbuRTnf2SrA2K3VyR1br9lk= github.com/djherbis/fscache v0.10.1/go.mod h1:yyPYtkNnnPXsW+81lAcQS6yab3G2CRfnPLotBvtbf0c= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= @@ -132,6 +140,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsouza/fake-gcs-server v1.49.0 h1:4x1RxKuqoqhZrXogtj5nInQnIjQylxld43tKrkPHnmE= +github.com/fsouza/fake-gcs-server v1.49.0/go.mod h1:FJYZxdHQk2nGxrczFjLbDv8h6SnYXxSxcnM14eeespA= github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955 h1:gmtGRvSexPU4B1T/yYo0sLOKzER1YT+b4kPxPpm0Ty4= github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955/go.mod h1:vmp8DIyckQMXOPl0AQVHt+7n5h7Gb7hS6CUydiV8QeA= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= @@ -162,6 +172,8 @@ github.com/gobuffalo/packd v1.0.2 h1:Yg523YqnOxGIWCp69W12yYBKsoChwI7mtu6ceM9Bwfw github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8= github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -233,6 +245,8 @@ github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg= +github.com/google/renameio/v2 v2.0.0/go.mod h1:BtmJXm5YlszgC+TD4HOEEUFgkJP3nLxehU6hfe7jRt4= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -247,6 +261,10 @@ github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBH github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= @@ -291,8 +309,10 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= -github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= +github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -329,6 +349,10 @@ github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOj github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.70 h1:1u9NtMgfK1U42kUxcsl5v0yj6TEOPR497OAQxpJnn2g= +github.com/minio/minio-go/v7 v7.0.70/go.mod h1:4yBA8v80xGA30cfM3fz0DKYMXunWl/AV/6tWEs9ryzo= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -359,6 +383,8 @@ github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= +github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -381,6 +407,8 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rubenv/sql-migrate v1.5.2 h1:bMDqOnrJVV/6JQgQ/MxOpU+AdO8uzYYA/TxFUBzFtS0= github.com/rubenv/sql-migrate v1.5.2/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -466,6 +494,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.einride.tech/aip v0.66.0 h1:XfV+NQX6L7EOYK11yoHHFtndeaWh3KbD9/cN/6iWEt8= +go.einride.tech/aip v0.66.0/go.mod h1:qAhMsfT7plxBX+Oy7Huol6YUvZ0ZzdUz26yZsQwfl1M= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -482,8 +512,8 @@ go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= -go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -498,8 +528,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -576,8 +606,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -587,8 +617,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= +golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= +golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -600,8 +630,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -646,18 +676,19 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -752,8 +783,8 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.170.0 h1:zMaruDePM88zxZBG+NG8+reALO2rfLhe/JShitLyT48= -google.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8= +google.golang.org/api v0.177.0 h1:8a0p/BbPa65GlqGWtUKxot4p0TV8OGOfyTjtmkXNXmk= +google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -799,12 +830,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= -google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= -google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c h1:kaI7oewGK5YnVwj+Y+EJBO/YN1ht8iTL9XkFHtVZLsc= -google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2 h1:9IZDv+/GcI6u+a4jRFRLxQs0RUCfavGfoOgEW6jpkI0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240311132316-a219d84964c2/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs= +google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw= +google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw= +google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 h1:DTJM0R8LECCgFeUwApvcEJHz85HLagW8uRENYxHh1ww= +google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6/go.mod h1:10yRODfgim2/T8csjQsMPgZOMvtytXKTDRzH6HRGzRw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -821,8 +852,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= -google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -835,8 +866,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/support/datastore/datastore.go b/support/datastore/datastore.go index 2a26aa1328..5592edf6fb 100644 --- a/support/datastore/datastore.go +++ b/support/datastore/datastore.go @@ -26,7 +26,11 @@ type DataStore interface { func NewDataStore(ctx context.Context, datastoreConfig DataStoreConfig, network string) (DataStore, error) { switch datastoreConfig.Type { case "GCS": - return NewGCSDataStore(ctx, datastoreConfig.Params, network) + destinationBucketPath, ok := datastoreConfig.Params["destination_bucket_path"] + if !ok { + return nil, errors.Errorf("Invalid GCS config, no destination_bucket_path") + } + return NewGCSDataStore(ctx, destinationBucketPath, network) default: return nil, errors.Errorf("Invalid datastore type %v, not supported", datastoreConfig.Type) } diff --git a/support/datastore/gcs_datastore.go b/support/datastore/gcs_datastore.go index c8c22fd57e..aaf7a55ac4 100644 --- a/support/datastore/gcs_datastore.go +++ b/support/datastore/gcs_datastore.go @@ -3,6 +3,7 @@ package datastore import ( "bytes" "context" + "errors" "fmt" "hash/crc32" "io" @@ -11,12 +12,9 @@ import ( "path" "strings" - "google.golang.org/api/googleapi" - "google.golang.org/api/option" - "cloud.google.com/go/storage" + "google.golang.org/api/googleapi" - "github.com/stellar/go/support/errors" "github.com/stellar/go/support/log" "github.com/stellar/go/support/url" ) @@ -28,15 +26,19 @@ type GCSDataStore struct { prefix string } -func NewGCSDataStore(ctx context.Context, params map[string]string, network string) (DataStore, error) { - destinationBucketPath, ok := params["destination_bucket_path"] - if !ok { - return nil, errors.Errorf("Invalid GCS config, no destination_bucket_path") +func NewGCSDataStore(ctx context.Context, bucketPath string, network string) (DataStore, error) { + client, err := storage.NewClient(ctx) + if err != nil { + return nil, err } + return FromGCSClient(ctx, client, bucketPath, network) +} + +func FromGCSClient(ctx context.Context, client *storage.Client, bucketPath string, network string) (DataStore, error) { // append the gcs:// scheme to enable usage of the url package reliably to // get parse bucket name which is first path segment as URL.Host - gcsBucketURL := fmt.Sprintf("gcs://%s/%s", destinationBucketPath, network) + gcsBucketURL := fmt.Sprintf("gcs://%s/%s", bucketPath, network) parsed, err := url.Parse(gcsBucketURL) if err != nil { return nil, err @@ -47,17 +49,10 @@ func NewGCSDataStore(ctx context.Context, params map[string]string, network stri bucketName := parsed.Host log.Infof("creating GCS client for bucket: %s, prefix: %s", bucketName, prefix) - - var options []option.ClientOption - client, err := storage.NewClient(ctx, options...) - if err != nil { - return nil, err - } - // Check the bucket exists bucket := client.Bucket(bucketName) if _, err := bucket.Attrs(ctx); err != nil { - return nil, errors.Wrap(err, "failed to retrieve bucket attributes") + return nil, fmt.Errorf("failed to retrieve bucket attributes: %w", err) } return &GCSDataStore{client: client, bucket: bucket, prefix: prefix}, nil @@ -74,13 +69,13 @@ func (b GCSDataStore) GetFile(ctx context.Context, filePath string) (io.ReadClos // https://pkg.go.dev/cloud.google.com/go/storage#Reader r, err := b.bucket.Object(filePath).ReadCompressed(true).NewReader(ctx) if err != nil { - if err == storage.ErrObjectNotExist { + if errors.Is(err, storage.ErrObjectNotExist) { return nil, os.ErrNotExist } if gcsError, ok := err.(*googleapi.Error); ok { log.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) } - return nil, errors.Wrapf(err, "error retrieving file: %s", filePath) + return nil, fmt.Errorf("error retrieving file %s: %w", filePath, err) } log.Infof("File retrieved successfully: %s", filePath) return r, nil @@ -99,7 +94,7 @@ func (b GCSDataStore) PutFileIfNotExists(ctx context.Context, filePath string, i log.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) } } - return false, errors.Wrapf(err, "error uploading file: %s", filePath) + return false, fmt.Errorf("error uploading file %s: %w", filePath, err) } log.Infof("File uploaded successfully: %s", filePath) return true, nil @@ -113,7 +108,7 @@ func (b GCSDataStore) PutFile(ctx context.Context, filePath string, in io.Writer if gcsError, ok := err.(*googleapi.Error); ok { log.Errorf("GCS error: %s %s", gcsError.Message, gcsError.Body) } - return errors.Wrapf(err, "error uploading file: %v", filePath) + return fmt.Errorf("error uploading file %s: %w", filePath, err) } log.Infof("File uploaded successfully: %s", filePath) return nil @@ -123,7 +118,7 @@ func (b GCSDataStore) PutFile(ctx context.Context, filePath string, in io.Writer func (b GCSDataStore) Size(ctx context.Context, pth string) (int64, error) { pth = path.Join(b.prefix, pth) attrs, err := b.bucket.Object(pth).Attrs(ctx) - if err == storage.ErrObjectNotExist { + if errors.Is(err, storage.ErrObjectNotExist) { err = os.ErrNotExist } if err != nil { @@ -136,7 +131,7 @@ func (b GCSDataStore) Size(ctx context.Context, pth string) (int64, error) { func (b GCSDataStore) Exists(ctx context.Context, pth string) (bool, error) { _, err := b.Size(ctx, pth) - if err == os.ErrNotExist { + if errors.Is(err, os.ErrNotExist) { return false, nil } @@ -154,16 +149,18 @@ func (b GCSDataStore) putFile(ctx context.Context, filePath string, in io.Writer if conditions != nil { o = o.If(*conditions) } - w := o.NewWriter(ctx) buf := &bytes.Buffer{} if _, err := in.WriteTo(buf); err != nil { - return errors.Wrapf(err, "failed to write file: %s", filePath) + return fmt.Errorf("failed to write file %s: %w", filePath, err) } + + w := o.NewWriter(ctx) w.SendCRC32C = true // we must set CRC32C before invoking w.Write() for the first time w.CRC32C = crc32.Checksum(buf.Bytes(), crc32.MakeTable(crc32.Castagnoli)) - if _, err := in.WriteTo(buf); err != nil { - return errors.Wrapf(err, "failed to put file: %s", filePath) + if _, err := io.Copy(w, buf); err != nil { + return fmt.Errorf("failed to put file %s: %w", filePath, err) + } return w.Close() } diff --git a/support/datastore/gcs_test.go b/support/datastore/gcs_test.go new file mode 100644 index 0000000000..9fe23bf2c8 --- /dev/null +++ b/support/datastore/gcs_test.go @@ -0,0 +1,235 @@ +package datastore + +import ( + "bytes" + "compress/gzip" + "context" + "io" + "os" + "testing" + + "github.com/fsouza/fake-gcs-server/fakestorage" + "github.com/stretchr/testify/require" +) + +func TestGCSExists(t *testing.T) { + server := fakestorage.NewServer([]fakestorage.Object{ + { + ObjectAttrs: fakestorage.ObjectAttrs{ + BucketName: "test-bucket", + Name: "objects/testnet/file.txt", + }, + Content: []byte("inside the file"), + }, + }) + defer server.Stop() + + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, store.Close()) + }) + + exists, err := store.Exists(context.Background(), "file.txt") + require.NoError(t, err) + require.True(t, exists) + + exists, err = store.Exists(context.Background(), "missing-file.txt") + require.NoError(t, err) + require.False(t, exists) +} + +func TestGCSSize(t *testing.T) { + content := []byte("inside the file") + server := fakestorage.NewServer([]fakestorage.Object{ + { + ObjectAttrs: fakestorage.ObjectAttrs{ + BucketName: "test-bucket", + Name: "objects/testnet/file.txt", + }, + Content: content, + }, + }) + defer server.Stop() + + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, store.Close()) + }) + + size, err := store.Size(context.Background(), "file.txt") + require.NoError(t, err) + require.Equal(t, int64(len(content)), size) + + _, err = store.Size(context.Background(), "missing-file.txt") + require.ErrorIs(t, err, os.ErrNotExist) +} + +type writerToRecorder struct { + io.WriterTo + total int64 +} + +func (r *writerToRecorder) WriteTo(w io.Writer) (int64, error) { + count, err := r.WriterTo.WriteTo(w) + r.total += count + return count, err +} + +func TestGCSPutFile(t *testing.T) { + server := fakestorage.NewServer([]fakestorage.Object{}) + defer server.Stop() + server.CreateBucketWithOpts(fakestorage.CreateBucketOpts{ + Name: "test-bucket", + VersioningEnabled: false, + DefaultEventBasedHold: false, + }) + + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, store.Close()) + }) + + content := []byte("inside the file") + writerTo := &writerToRecorder{ + WriterTo: bytes.NewReader(content), + } + err = store.PutFile(context.Background(), "file.txt", writerTo) + require.NoError(t, err) + require.Equal(t, int64(len(content)), writerTo.total) + + reader, err := store.GetFile(context.Background(), "file.txt") + require.NoError(t, err) + requireReaderContentEquals(t, reader, content) + + otherContent := []byte("other text") + writerTo = &writerToRecorder{ + WriterTo: bytes.NewReader(otherContent), + } + err = store.PutFile(context.Background(), "file.txt", writerTo) + require.NoError(t, err) + require.Equal(t, int64(len(otherContent)), writerTo.total) + + reader, err = store.GetFile(context.Background(), "file.txt") + require.NoError(t, err) + requireReaderContentEquals(t, reader, otherContent) +} + +func TestGCSPutFileIfNotExists(t *testing.T) { + existingContent := []byte("inside the file") + server := fakestorage.NewServer([]fakestorage.Object{ + { + ObjectAttrs: fakestorage.ObjectAttrs{ + BucketName: "test-bucket", + Name: "objects/testnet/file.txt", + }, + Content: existingContent, + }, + }) + defer server.Stop() + + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, store.Close()) + }) + + newContent := []byte("overwrite the file") + writerTo := &writerToRecorder{ + WriterTo: bytes.NewReader(newContent), + } + ok, err := store.PutFileIfNotExists(context.Background(), "file.txt", writerTo) + require.NoError(t, err) + require.False(t, ok) + require.Equal(t, int64(len(newContent)), writerTo.total) + + reader, err := store.GetFile(context.Background(), "file.txt") + require.NoError(t, err) + requireReaderContentEquals(t, reader, existingContent) + + writerTo = &writerToRecorder{ + WriterTo: bytes.NewReader(newContent), + } + ok, err = store.PutFileIfNotExists(context.Background(), "other-file.txt", writerTo) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, int64(len(newContent)), writerTo.total) + + reader, err = store.GetFile(context.Background(), "other-file.txt") + require.NoError(t, err) + requireReaderContentEquals(t, reader, newContent) +} + +func TestGCSGetNonExistentFile(t *testing.T) { + existingContent := []byte("inside the file") + server := fakestorage.NewServer([]fakestorage.Object{ + { + ObjectAttrs: fakestorage.ObjectAttrs{ + BucketName: "test-bucket", + Name: "objects/testnet/file.txt", + }, + Content: existingContent, + }, + }) + defer server.Stop() + + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, store.Close()) + }) + + _, err = store.GetFile(context.Background(), "other-file.txt") + require.ErrorIs(t, err, os.ErrNotExist) +} + +func TestGCSGetFileValidatesCRC32C(t *testing.T) { + // test on a gzipped file so we can verify ReadCompressed() + // was called correctly + var buf bytes.Buffer + zw := gzip.NewWriter(&buf) + zw.Name = "file.gz" + if _, err := zw.Write([]byte("gzipped object data")); err != nil { + t.Fatalf("creating gzip: %v", err) + } + if err := zw.Close(); err != nil { + t.Fatalf("closing gzip writer: %v", err) + } + + server := fakestorage.NewServer([]fakestorage.Object{ + { + ObjectAttrs: fakestorage.ObjectAttrs{ + // set a CRC32C value which doesn't actually match the file contents + Crc32c: "mw/l0Q==", + BucketName: "test-bucket", + Name: "objects/testnet/file.gz", + ContentEncoding: "gzip", + ContentType: "text/plain", + }, + Content: buf.Bytes(), + }, + }) + defer server.Stop() + + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, store.Close()) + }) + + reader, err := store.GetFile(context.Background(), "file.gz") + require.NoError(t, err) + buf.Reset() + _, err = io.Copy(&buf, reader) + require.EqualError(t, err, "storage: bad CRC on read: got 985946173, want 2601510353") +} + +func requireReaderContentEquals(t *testing.T, reader io.ReadCloser, expected []byte) { + var buf bytes.Buffer + _, err := io.Copy(&buf, reader) + require.NoError(t, err) + require.NoError(t, reader.Close()) + require.Equal(t, expected, buf.Bytes()) +} From 8f8237aadc9b35eadc337788e0f2b71a52643969 Mon Sep 17 00:00:00 2001 From: tamirms Date: Wed, 15 May 2024 06:19:41 +0100 Subject: [PATCH 174/234] Gracefully shutdown ledger exporter admin server (#5314) --- exp/services/ledgerexporter/internal/app.go | 105 ++++++++++++-------- 1 file changed, 65 insertions(+), 40 deletions(-) diff --git a/exp/services/ledgerexporter/internal/app.go b/exp/services/ledgerexporter/internal/app.go index e23cc7b746..b90c7a33c3 100644 --- a/exp/services/ledgerexporter/internal/app.go +++ b/exp/services/ledgerexporter/internal/app.go @@ -2,26 +2,31 @@ package ledgerexporter import ( "context" - _ "embed" "fmt" + "net/http" "os" "os/signal" "sync" "syscall" + "time" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/stellar/go/historyarchive" + "github.com/stellar/go/historyarchive" "github.com/stellar/go/ingest/ledgerbackend" - _ "github.com/stellar/go/network" "github.com/stellar/go/support/datastore" supporthttp "github.com/stellar/go/support/http" "github.com/stellar/go/support/log" ) +const ( + adminServerReadTimeout = 5 * time.Second + adminServerShutdownTimeout = time.Second * 5 +) + var ( logger = log.New().WithField("service", "ledger-exporter") ) @@ -62,23 +67,18 @@ func (m InvalidDataStoreError) Error() string { } type App struct { - config *Config - ledgerBackend ledgerbackend.LedgerBackend - dataStore datastore.DataStore - exportManager *ExportManager - uploader Uploader - flags Flags - prometheusRegistry *prometheus.Registry + config *Config + ledgerBackend ledgerbackend.LedgerBackend + dataStore datastore.DataStore + exportManager *ExportManager + uploader Uploader + flags Flags + adminServer *http.Server } func NewApp(flags Flags) *App { logger.SetLevel(log.DebugLevel) - registry := prometheus.NewRegistry() - registry.MustRegister( - collectors.NewProcessCollector(collectors.ProcessCollectorOpts{Namespace: "ledger_exporter"}), - collectors.NewGoCollector(), - ) - app := &App{flags: flags, prometheusRegistry: registry} + app := &App{flags: flags} return app } @@ -86,6 +86,12 @@ func (a *App) init(ctx context.Context) error { var err error var archive historyarchive.ArchiveInterface + registry := prometheus.NewRegistry() + registry.MustRegister( + collectors.NewProcessCollector(collectors.ProcessCollectorOpts{Namespace: "ledger_exporter"}), + collectors.NewGoCollector(), + ) + if a.config, err = NewConfig(ctx, a.flags); err != nil { return errors.Wrap(err, "Could not load configuration") } @@ -106,17 +112,20 @@ func (a *App) init(ctx context.Context) error { logger.Infof("Final computed ledger range for backend retrieval and export, start=%d, end=%d", a.config.StartLedger, a.config.EndLedger) - if a.ledgerBackend, err = newLedgerBackend(ctx, a.config, a.prometheusRegistry); err != nil { + if a.ledgerBackend, err = newLedgerBackend(ctx, a.config, registry); err != nil { return err } - // TODO: make number of upload workers configurable instead of hard coding it to 1 - queue := NewUploadQueue(1, a.prometheusRegistry) - if a.exportManager, err = NewExportManager(a.config.LedgerBatchConfig, a.ledgerBackend, queue, a.prometheusRegistry); err != nil { + // TODO: make queue size configurable instead of hard coding it to 1 + queue := NewUploadQueue(1, registry) + if a.exportManager, err = NewExportManager(a.config.LedgerBatchConfig, a.ledgerBackend, queue, registry); err != nil { return err } - a.uploader = NewUploader(a.dataStore, queue, a.prometheusRegistry) + a.uploader = NewUploader(a.dataStore, queue, registry) + if a.config.AdminPort != 0 { + a.adminServer = newAdminServer(a.config.AdminPort, registry) + } return nil } @@ -149,22 +158,15 @@ func (a *App) close() { } } -func (a *App) serveAdmin() { - if a.config.AdminPort == 0 { - return - } - +func newAdminServer(adminPort int, prometheusRegistry *prometheus.Registry) *http.Server { mux := supporthttp.NewMux(logger) - mux.Handle("/metrics", promhttp.HandlerFor(a.prometheusRegistry, promhttp.HandlerOpts{})) - - addr := fmt.Sprintf(":%d", a.config.AdminPort) - supporthttp.Run(supporthttp.Config{ - ListenAddr: addr, - Handler: mux, - OnStarting: func() { - logger.Infof("Starting admin port server on %s", addr) - }, - }) + mux.Handle("/metrics", promhttp.HandlerFor(prometheusRegistry, promhttp.HandlerOpts{})) + adminAddr := fmt.Sprintf(":%d", adminPort) + return &http.Server{ + Addr: adminAddr, + Handler: mux, + ReadTimeout: adminServerReadTimeout, + } } func (a *App) Run() { @@ -205,19 +207,42 @@ func (a *App) Run() { } }() - go a.serveAdmin() + if a.adminServer != nil { + // no need to include this goroutine in the wait group + // because a.adminServer.Shutdown() is called below and + // that will block until a.adminServer has finished + // shutting down + go func() { + logger.Infof("Starting admin server on port %v", a.config.AdminPort) + if err := a.adminServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Warn(errors.Wrap(err, "error in internalServer.ListenAndServe()")) + } + }() + } // Handle OS signals to gracefully terminate the service sigCh := make(chan os.Signal, 1) + defer close(sigCh) signal.Notify(sigCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) go func() { - sig := <-sigCh - logger.Infof("Received termination signal: %v", sig) - cancel() + sig, ok := <-sigCh + if ok { + logger.Infof("Received termination signal: %v", sig) + cancel() + } }() wg.Wait() logger.Info("Shutting down ledger-exporter") + + if a.adminServer != nil { + serverShutdownCtx, serverShutdownCancel := context.WithTimeout(context.Background(), adminServerShutdownTimeout) + defer serverShutdownCancel() + + if err := a.adminServer.Shutdown(serverShutdownCtx); err != nil { + logger.WithError(err).Warn("error in internalServer.Shutdown") + } + } } // newLedgerBackend Creates and initializes captive core ledger backend From ae1019c78864f3801e0f1ee4c44d70c005fa17de Mon Sep 17 00:00:00 2001 From: urvisavla Date: Wed, 15 May 2024 11:12:50 -0700 Subject: [PATCH 175/234] exp/services/ledgerexporter: refactoring LedgerMetaArchive (#5311) --- .../ledgerexporter/internal/exportmanager.go | 10 +- .../internal/exportmanager_test.go | 33 +++- .../internal/ledger_meta_archive.go | 24 +++ exp/services/ledgerexporter/internal/queue.go | 9 +- .../ledgerexporter/internal/queue_test.go | 13 +- .../ledgerexporter/internal/uploader.go | 11 +- .../ledgerexporter/internal/uploader_test.go | 14 +- .../ledgerbackend/buffered_storage_backend.go | 29 ++- .../buffered_storage_backend_test.go | 28 ++- support/datastore/ledger_meta_archive.go | 96 --------- support/datastore/ledger_meta_archive_test.go | 84 -------- xdr/ledger_close_meta_batch.go | 41 ++++ xdr/ledger_close_meta_batch_test.go | 183 ++++++++++++++++++ 13 files changed, 337 insertions(+), 238 deletions(-) create mode 100644 exp/services/ledgerexporter/internal/ledger_meta_archive.go delete mode 100644 support/datastore/ledger_meta_archive.go delete mode 100644 support/datastore/ledger_meta_archive_test.go create mode 100644 xdr/ledger_close_meta_batch.go create mode 100644 xdr/ledger_close_meta_batch_test.go diff --git a/exp/services/ledgerexporter/internal/exportmanager.go b/exp/services/ledgerexporter/internal/exportmanager.go index 5ee06b8cd6..5e6c87bbf5 100644 --- a/exp/services/ledgerexporter/internal/exportmanager.go +++ b/exp/services/ledgerexporter/internal/exportmanager.go @@ -15,7 +15,7 @@ import ( type ExportManager struct { config datastore.LedgerBatchConfig ledgerBackend ledgerbackend.LedgerBackend - currentMetaArchive *datastore.LedgerMetaArchive + currentMetaArchive *LedgerMetaArchive queue UploadQueue latestLedgerMetric *prometheus.GaugeVec } @@ -47,7 +47,7 @@ func (e *ExportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta // Determine the object key for the given ledger sequence objectKey := e.config.GetObjectKeyFromSequenceNumber(ledgerSeq) - if e.currentMetaArchive != nil && e.currentMetaArchive.GetObjectKey() != objectKey { + if e.currentMetaArchive != nil && e.currentMetaArchive.ObjectKey != objectKey { return errors.New("Current meta archive object key mismatch") } if e.currentMetaArchive == nil { @@ -61,14 +61,14 @@ func (e *ExportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta } // Create a new LedgerMetaArchive and add it to the map. - e.currentMetaArchive = datastore.NewLedgerMetaArchive(objectKey, ledgerSeq, endSeq) + e.currentMetaArchive = NewLedgerMetaArchive(objectKey, ledgerSeq, endSeq) } - if err := e.currentMetaArchive.AddLedger(ledgerCloseMeta); err != nil { + if err := e.currentMetaArchive.Data.AddLedger(ledgerCloseMeta); err != nil { return errors.Wrapf(err, "failed to add ledger %d", ledgerSeq) } - if ledgerSeq >= e.currentMetaArchive.GetEndLedgerSequence() { + if ledgerSeq >= uint32(e.currentMetaArchive.Data.EndSequence) { // Current archive is full, send it for upload if err := e.queue.Enqueue(ctx, e.currentMetaArchive); err != nil { return err diff --git a/exp/services/ledgerexporter/internal/exportmanager_test.go b/exp/services/ledgerexporter/internal/exportmanager_test.go index f3b302b739..a84e20fb52 100644 --- a/exp/services/ledgerexporter/internal/exportmanager_test.go +++ b/exp/services/ledgerexporter/internal/exportmanager_test.go @@ -15,8 +15,27 @@ import ( "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/support/collections/set" "github.com/stellar/go/support/datastore" + "github.com/stellar/go/xdr" ) +func createLedgerCloseMeta(ledgerSeq uint32) xdr.LedgerCloseMeta { + return xdr.LedgerCloseMeta{ + V: int32(0), + V0: &xdr.LedgerCloseMetaV0{ + LedgerHeader: xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + LedgerSeq: xdr.Uint32(ledgerSeq), + }, + }, + TxSet: xdr.TransactionSet{}, + TxProcessing: nil, + UpgradesProcessing: nil, + ScpInfo: nil, + }, + V1: nil, + } +} + func TestExporterSuite(t *testing.T) { suite.Run(t, new(ExportManagerSuite)) } @@ -57,7 +76,7 @@ func (s *ExportManagerSuite) TestRun() { expectedKeys := set.NewSet[string](10) for i := start; i <= end; i++ { s.mockBackend.On("GetLedger", s.ctx, i). - Return(datastore.CreateLedgerCloseMeta(i), nil) + Return(createLedgerCloseMeta(i), nil) key := config.GetObjectKeyFromSequenceNumber(i) expectedKeys.Add(key) } @@ -104,7 +123,7 @@ func (s *ExportManagerSuite) TestRunContextCancel() { ctx, cancel := context.WithCancel(context.Background()) s.mockBackend.On("GetLedger", mock.Anything, mock.Anything). - Return(datastore.CreateLedgerCloseMeta(1), nil) + Return(createLedgerCloseMeta(1), nil) go func() { <-time.After(time.Second * 1) @@ -164,7 +183,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { start := uint32(0) end := uint32(255) for i := start; i <= end; i++ { - require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), datastore.CreateLedgerCloseMeta(i))) + require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(i))) key := config.GetObjectKeyFromSequenceNumber(i) expectedKeys.Add(key) @@ -188,8 +207,8 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { cancel() }() - require.NoError(s.T(), exporter.AddLedgerCloseMeta(ctx, datastore.CreateLedgerCloseMeta(1))) - err = exporter.AddLedgerCloseMeta(ctx, datastore.CreateLedgerCloseMeta(2)) + require.NoError(s.T(), exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(1))) + err = exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(2)) require.EqualError(s.T(), err, "context canceled") } @@ -200,7 +219,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMetaKeyMismatch() { exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) require.NoError(s.T(), err) - require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), datastore.CreateLedgerCloseMeta(16))) - require.EqualError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), datastore.CreateLedgerCloseMeta(21)), + require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(16))) + require.EqualError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(21)), "Current meta archive object key mismatch") } diff --git a/exp/services/ledgerexporter/internal/ledger_meta_archive.go b/exp/services/ledgerexporter/internal/ledger_meta_archive.go new file mode 100644 index 0000000000..5215e2353e --- /dev/null +++ b/exp/services/ledgerexporter/internal/ledger_meta_archive.go @@ -0,0 +1,24 @@ +package ledgerexporter + +import ( + "github.com/stellar/go/xdr" +) + +// LedgerMetaArchive represents a file with metadata and binary data. +type LedgerMetaArchive struct { + // file name + ObjectKey string + // Actual binary data + Data xdr.LedgerCloseMetaBatch +} + +// NewLedgerMetaArchive creates a new LedgerMetaArchive instance. +func NewLedgerMetaArchive(key string, startSeq uint32, endSeq uint32) *LedgerMetaArchive { + return &LedgerMetaArchive{ + ObjectKey: key, + Data: xdr.LedgerCloseMetaBatch{ + StartSequence: xdr.Uint32(startSeq), + EndSequence: xdr.Uint32(endSeq), + }, + } +} diff --git a/exp/services/ledgerexporter/internal/queue.go b/exp/services/ledgerexporter/internal/queue.go index 5b46b202ee..372ccb0056 100644 --- a/exp/services/ledgerexporter/internal/queue.go +++ b/exp/services/ledgerexporter/internal/queue.go @@ -4,12 +4,11 @@ import ( "context" "github.com/prometheus/client_golang/prometheus" - "github.com/stellar/go/support/datastore" ) // UploadQueue is a queue of LedgerMetaArchive objects which are scheduled for upload type UploadQueue struct { - metaArchiveCh chan *datastore.LedgerMetaArchive + metaArchiveCh chan *LedgerMetaArchive queueLengthMetric prometheus.Gauge } @@ -23,13 +22,13 @@ func NewUploadQueue(size int, prometheusRegistry *prometheus.Registry) UploadQue }) prometheusRegistry.MustRegister(queueLengthMetric) return UploadQueue{ - metaArchiveCh: make(chan *datastore.LedgerMetaArchive, size), + metaArchiveCh: make(chan *LedgerMetaArchive, size), queueLengthMetric: queueLengthMetric, } } // Enqueue will add an upload task to the queue. Enqueue may block if the queue is full. -func (u UploadQueue) Enqueue(ctx context.Context, archive *datastore.LedgerMetaArchive) error { +func (u UploadQueue) Enqueue(ctx context.Context, archive *LedgerMetaArchive) error { u.queueLengthMetric.Inc() select { case u.metaArchiveCh <- archive: @@ -40,7 +39,7 @@ func (u UploadQueue) Enqueue(ctx context.Context, archive *datastore.LedgerMetaA } // Dequeue will pop a task off the queue. Dequeue may block if the queue is empty. -func (u UploadQueue) Dequeue(ctx context.Context) (*datastore.LedgerMetaArchive, bool, error) { +func (u UploadQueue) Dequeue(ctx context.Context) (*LedgerMetaArchive, bool, error) { select { case <-ctx.Done(): return nil, false, ctx.Err() diff --git a/exp/services/ledgerexporter/internal/queue_test.go b/exp/services/ledgerexporter/internal/queue_test.go index 4f5fdd263f..0676f3594f 100644 --- a/exp/services/ledgerexporter/internal/queue_test.go +++ b/exp/services/ledgerexporter/internal/queue_test.go @@ -6,7 +6,6 @@ import ( "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" - "github.com/stellar/go/support/datastore" "github.com/stretchr/testify/require" ) @@ -32,9 +31,9 @@ func getMetricValue(metric prometheus.Metric) *dto.Metric { func TestQueue(t *testing.T) { queue := NewUploadQueue(3, prometheus.NewRegistry()) - require.NoError(t, queue.Enqueue(context.Background(), datastore.NewLedgerMetaArchive("test", 1, 1))) - require.NoError(t, queue.Enqueue(context.Background(), datastore.NewLedgerMetaArchive("test", 2, 2))) - require.NoError(t, queue.Enqueue(context.Background(), datastore.NewLedgerMetaArchive("test", 3, 3))) + require.NoError(t, queue.Enqueue(context.Background(), NewLedgerMetaArchive("test", 1, 1))) + require.NoError(t, queue.Enqueue(context.Background(), NewLedgerMetaArchive("test", 2, 2))) + require.NoError(t, queue.Enqueue(context.Background(), NewLedgerMetaArchive("test", 3, 3))) require.Equal(t, float64(3), getMetricValue(queue.queueLengthMetric).GetGauge().GetValue()) queue.Close() @@ -43,19 +42,19 @@ func TestQueue(t *testing.T) { require.NoError(t, err) require.True(t, ok) require.Equal(t, float64(2), getMetricValue(queue.queueLengthMetric).GetGauge().GetValue()) - require.Equal(t, uint32(1), l.GetStartLedgerSequence()) + require.Equal(t, uint32(1), uint32(l.Data.StartSequence)) l, ok, err = queue.Dequeue(context.Background()) require.NoError(t, err) require.True(t, ok) require.Equal(t, float64(1), getMetricValue(queue.queueLengthMetric).GetGauge().GetValue()) - require.Equal(t, uint32(2), l.GetStartLedgerSequence()) + require.Equal(t, uint32(2), uint32(l.Data.StartSequence)) l, ok, err = queue.Dequeue(context.Background()) require.NoError(t, err) require.True(t, ok) require.Equal(t, float64(0), getMetricValue(queue.queueLengthMetric).GetGauge().GetValue()) - require.Equal(t, uint32(3), l.GetStartLedgerSequence()) + require.Equal(t, uint32(3), uint32(l.Data.StartSequence)) l, ok, err = queue.Dequeue(context.Background()) require.NoError(t, err) diff --git a/exp/services/ledgerexporter/internal/uploader.go b/exp/services/ledgerexporter/internal/uploader.go index c1f80d6416..94344ee1ee 100644 --- a/exp/services/ledgerexporter/internal/uploader.go +++ b/exp/services/ledgerexporter/internal/uploader.go @@ -8,6 +8,7 @@ import ( "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" + "github.com/stellar/go/support/compressxdr" "github.com/stellar/go/support/datastore" ) @@ -78,19 +79,19 @@ func (r *writerToRecorder) WriteTo(w io.Writer) (int64, error) { } // Upload uploads the serialized binary data of ledger TxMeta to the specified destination. -func (u Uploader) Upload(ctx context.Context, metaArchive *datastore.LedgerMetaArchive) error { - logger.Infof("Uploading: %s", metaArchive.GetObjectKey()) +func (u Uploader) Upload(ctx context.Context, metaArchive *LedgerMetaArchive) error { + logger.Infof("Uploading: %s", metaArchive.ObjectKey) startTime := time.Now() - numLedgers := strconv.FormatUint(uint64(metaArchive.GetLedgerCount()), 10) + numLedgers := strconv.FormatUint(uint64(len(metaArchive.Data.LedgerCloseMetas)), 10) xdrEncoder := compressxdr.NewXDREncoder(compressxdr.DefaultCompressor, &metaArchive.Data) writerTo := &writerToRecorder{ WriterTo: xdrEncoder, } - ok, err := u.dataStore.PutFileIfNotExists(ctx, metaArchive.GetObjectKey(), writerTo) + ok, err := u.dataStore.PutFileIfNotExists(ctx, metaArchive.ObjectKey, writerTo) if err != nil { - return errors.Wrapf(err, "error uploading %s", metaArchive.GetObjectKey()) + return errors.Wrapf(err, "error uploading %s", metaArchive.ObjectKey) } alreadyExists := strconv.FormatBool(!ok) diff --git a/exp/services/ledgerexporter/internal/uploader_test.go b/exp/services/ledgerexporter/internal/uploader_test.go index da0cc6d2ad..b31397c183 100644 --- a/exp/services/ledgerexporter/internal/uploader_test.go +++ b/exp/services/ledgerexporter/internal/uploader_test.go @@ -41,9 +41,9 @@ func (s *UploaderSuite) TestUpload() { func (s *UploaderSuite) testUpload(putOkReturnVal bool) { key, start, end := "test-1-100", uint32(1), uint32(100) - archive := datastore.NewLedgerMetaArchive(key, start, end) + archive := NewLedgerMetaArchive(key, start, end) for i := start; i <= end; i++ { - _ = archive.AddLedger(datastore.CreateLedgerCloseMeta(i)) + _ = archive.Data.AddLedger(createLedgerCloseMeta(i)) } var capturedBuf bytes.Buffer @@ -61,7 +61,7 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { require.NoError(s.T(), dataUploader.Upload(context.Background(), archive)) expectedCompressedLength := capturedBuf.Len() - var decodedArchive datastore.LedgerMetaArchive + var decodedArchive LedgerMetaArchive xdrDecoder := compressxdr.NewXDRDecoder(compressxdr.DefaultCompressor, &decodedArchive.Data) decoder := xdrDecoder @@ -161,7 +161,7 @@ func (s *UploaderSuite) TestUploadPutError() { func (s *UploaderSuite) testUploadPutError(putOkReturnVal bool) { key, start, end := "test-1-100", uint32(1), uint32(100) - archive := datastore.NewLedgerMetaArchive(key, start, end) + archive := NewLedgerMetaArchive(key, start, end) s.mockDataStore.On("PutFileIfNotExists", context.Background(), key, mock.Anything).Return(putOkReturnVal, errors.New("error in PutFileIfNotExists")) @@ -209,7 +209,7 @@ func (s *UploaderSuite) TestRunChannelClose() { go func() { key, start, end := "test", uint32(1), uint32(100) for i := start; i <= end; i++ { - s.Assert().NoError(queue.Enqueue(s.ctx, datastore.NewLedgerMetaArchive(key, i, i))) + s.Assert().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive(key, i, i))) } <-time.After(time.Second * 2) queue.Close() @@ -225,7 +225,7 @@ func (s *UploaderSuite) TestRunContextCancel() { registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - s.Assert().NoError(queue.Enqueue(s.ctx, datastore.NewLedgerMetaArchive("test", 1, 1))) + s.Assert().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive("test", 1, 1))) go func() { <-time.After(time.Second * 2) @@ -240,7 +240,7 @@ func (s *UploaderSuite) TestRunUploadError() { registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - s.Assert().NoError(queue.Enqueue(s.ctx, datastore.NewLedgerMetaArchive("test", 1, 1))) + s.Assert().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive("test", 1, 1))) s.mockDataStore.On("PutFileIfNotExists", mock.Anything, "test", mock.Anything).Return(false, errors.New("Put error")) diff --git a/ingest/ledgerbackend/buffered_storage_backend.go b/ingest/ledgerbackend/buffered_storage_backend.go index c3c1007e87..41ff4a942e 100644 --- a/ingest/ledgerbackend/buffered_storage_backend.go +++ b/ingest/ledgerbackend/buffered_storage_backend.go @@ -36,12 +36,12 @@ type BufferedStorageBackend struct { // ledgerBuffer is the buffer for LedgerCloseMeta data read in parallel. ledgerBuffer *ledgerBuffer - dataStore datastore.DataStore - prepared *Range // Non-nil if any range is prepared - closed bool // False until the core is closed - ledgerMetaArchive *datastore.LedgerMetaArchive - nextLedger uint32 - lastLedger uint32 + dataStore datastore.DataStore + prepared *Range // Non-nil if any range is prepared + closed bool // False until the core is closed + lcmBatch xdr.LedgerCloseMetaBatch + nextLedger uint32 + lastLedger uint32 } // NewBufferedStorageBackend returns a new BufferedStorageBackend instance. @@ -62,12 +62,9 @@ func NewBufferedStorageBackend(ctx context.Context, config BufferedStorageBacken return nil, errors.New("ledgersPerFile must be > 0") } - ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) - bsBackend := &BufferedStorageBackend{ - config: config, - dataStore: config.DataStore, - ledgerMetaArchive: ledgerMetaArchive, + config: config, + dataStore: config.DataStore, } return bsBackend, nil @@ -98,19 +95,19 @@ func (bsb *BufferedStorageBackend) GetLatestLedgerSequence(ctx context.Context) // Otherwise will continuously load in the next LedgerCloseMetaBatch until found. func (bsb *BufferedStorageBackend) getBatchForSequence(ctx context.Context, sequence uint32) error { // Sequence inside the current cached LedgerCloseMetaBatch - if sequence >= bsb.ledgerMetaArchive.GetStartLedgerSequence() && sequence <= bsb.ledgerMetaArchive.GetEndLedgerSequence() { + if sequence >= uint32(bsb.lcmBatch.StartSequence) && sequence <= uint32(bsb.lcmBatch.EndSequence) { return nil } // Sequence is before the current LedgerCloseMetaBatch // Does not support retrieving LedgerCloseMeta before the current cached batch - if sequence < bsb.ledgerMetaArchive.GetStartLedgerSequence() { - return errors.New("requested sequence preceeds current LedgerCloseMetaBatch") + if sequence < uint32(bsb.lcmBatch.StartSequence) { + return errors.New("requested sequence precedes current LedgerCloseMetaBatch") } // Sequence is beyond the current LedgerCloseMetaBatch var err error - bsb.ledgerMetaArchive.Data, err = bsb.ledgerBuffer.getFromLedgerQueue(ctx) + bsb.lcmBatch, err = bsb.ledgerBuffer.getFromLedgerQueue(ctx) if err != nil { return errors.Wrap(err, "failed getting next ledger batch from queue") } @@ -165,7 +162,7 @@ func (bsb *BufferedStorageBackend) GetLedger(ctx context.Context, sequence uint3 return xdr.LedgerCloseMeta{}, err } - ledgerCloseMeta, err := bsb.ledgerMetaArchive.GetLedger(sequence) + ledgerCloseMeta, err := bsb.lcmBatch.GetLedger(sequence) if err != nil { return xdr.LedgerCloseMeta{}, err } diff --git a/ingest/ledgerbackend/buffered_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go index 063e234c31..1aca8306c8 100644 --- a/ingest/ledgerbackend/buffered_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -22,6 +22,24 @@ import ( var partitionSize = uint32(64000) var ledgerPerFileCount = uint32(1) +func createLedgerCloseMeta(ledgerSeq uint32) xdr.LedgerCloseMeta { + return xdr.LedgerCloseMeta{ + V: int32(0), + V0: &xdr.LedgerCloseMetaV0{ + LedgerHeader: xdr.LedgerHeaderHistoryEntry{ + Header: xdr.LedgerHeader{ + LedgerSeq: xdr.Uint32(ledgerSeq), + }, + }, + TxSet: xdr.TransactionSet{}, + TxProcessing: nil, + UpgradesProcessing: nil, + ScpInfo: nil, + }, + V1: nil, + } +} + func createBufferedStorageBackendConfigForTesting() BufferedStorageBackendConfig { param := make(map[string]string) param["destination_bucket_path"] = "testURL" @@ -45,12 +63,10 @@ func createBufferedStorageBackendConfigForTesting() BufferedStorageBackendConfig func createBufferedStorageBackendForTesting() BufferedStorageBackend { config := createBufferedStorageBackendConfigForTesting() - ledgerMetaArchive := datastore.NewLedgerMetaArchive("", 0, 0) return BufferedStorageBackend{ - config: config, - dataStore: config.DataStore, - ledgerMetaArchive: ledgerMetaArchive, + config: config, + dataStore: config.DataStore, } } @@ -81,7 +97,7 @@ func createMockdataStore(t *testing.T, start, end, partitionSize, count uint32) func createLCMForTesting(start, end uint32) []xdr.LedgerCloseMeta { var lcmArray []xdr.LedgerCloseMeta for i := start; i <= end; i++ { - lcmArray = append(lcmArray, datastore.CreateLedgerCloseMeta(i)) + lcmArray = append(lcmArray, createLedgerCloseMeta(i)) } return lcmArray @@ -90,7 +106,7 @@ func createLCMForTesting(start, end uint32) []xdr.LedgerCloseMeta { func createTestLedgerCloseMetaBatch(startSeq, endSeq, count uint32) xdr.LedgerCloseMetaBatch { var ledgerCloseMetas []xdr.LedgerCloseMeta for i := uint32(0); i < count; i++ { - ledgerCloseMetas = append(ledgerCloseMetas, datastore.CreateLedgerCloseMeta(startSeq+uint32(i))) + ledgerCloseMetas = append(ledgerCloseMetas, createLedgerCloseMeta(startSeq+uint32(i))) } return xdr.LedgerCloseMetaBatch{ StartSequence: xdr.Uint32(startSeq), diff --git a/support/datastore/ledger_meta_archive.go b/support/datastore/ledger_meta_archive.go deleted file mode 100644 index 7942a8bdab..0000000000 --- a/support/datastore/ledger_meta_archive.go +++ /dev/null @@ -1,96 +0,0 @@ -package datastore - -import ( - "fmt" - - "github.com/stellar/go/xdr" -) - -// LedgerMetaArchive represents a file with metadata and binary data. -type LedgerMetaArchive struct { - // file name - ObjectKey string - // Actual binary data - Data xdr.LedgerCloseMetaBatch -} - -// NewLedgerMetaArchive creates a new LedgerMetaArchive instance. -func NewLedgerMetaArchive(key string, startSeq uint32, endSeq uint32) *LedgerMetaArchive { - return &LedgerMetaArchive{ - ObjectKey: key, - Data: xdr.LedgerCloseMetaBatch{ - StartSequence: xdr.Uint32(startSeq), - EndSequence: xdr.Uint32(endSeq), - }, - } -} - -// AddLedger adds a LedgerCloseMeta to the archive. -func (f *LedgerMetaArchive) AddLedger(ledgerCloseMeta xdr.LedgerCloseMeta) error { - if ledgerCloseMeta.LedgerSequence() < uint32(f.Data.StartSequence) || - ledgerCloseMeta.LedgerSequence() > uint32(f.Data.EndSequence) { - return fmt.Errorf("ledger sequence %d is outside valid range [%d, %d]", - ledgerCloseMeta.LedgerSequence(), f.Data.StartSequence, f.Data.EndSequence) - } - - if len(f.Data.LedgerCloseMetas) > 0 { - lastSequence := f.Data.LedgerCloseMetas[len(f.Data.LedgerCloseMetas)-1].LedgerSequence() - if ledgerCloseMeta.LedgerSequence() != lastSequence+1 { - return fmt.Errorf("ledgers must be added sequentially: expected sequence %d, got %d", - lastSequence+1, ledgerCloseMeta.LedgerSequence()) - } - } - f.Data.LedgerCloseMetas = append(f.Data.LedgerCloseMetas, ledgerCloseMeta) - return nil -} - -// GetLedgerCount returns the number of ledgers currently in the archive. -func (f *LedgerMetaArchive) GetLedgerCount() uint32 { - return uint32(len(f.Data.LedgerCloseMetas)) -} - -// GetStartLedgerSequence returns the starting ledger sequence of the archive. -func (f *LedgerMetaArchive) GetStartLedgerSequence() uint32 { - return uint32(f.Data.StartSequence) -} - -// GetEndLedgerSequence returns the ending ledger sequence of the archive. -func (f *LedgerMetaArchive) GetEndLedgerSequence() uint32 { - return uint32(f.Data.EndSequence) -} - -// GetObjectKey returns the object key of the archive. -func (f *LedgerMetaArchive) GetObjectKey() string { - return f.ObjectKey -} - -func (f *LedgerMetaArchive) GetLedger(sequence uint32) (xdr.LedgerCloseMeta, error) { - if sequence < uint32(f.Data.StartSequence) || sequence > uint32(f.Data.EndSequence) { - return xdr.LedgerCloseMeta{}, fmt.Errorf("ledger sequence %d is outside valid range [%d, %d]", - sequence, f.Data.StartSequence, f.Data.EndSequence) - } - - ledgerIndex := sequence - f.GetStartLedgerSequence() - if ledgerIndex >= uint32(len(f.Data.LedgerCloseMetas)) { - return xdr.LedgerCloseMeta{}, fmt.Errorf("LedgerCloseMeta for sequence %d not found", sequence) - } - return f.Data.LedgerCloseMetas[ledgerIndex], nil -} - -func CreateLedgerCloseMeta(ledgerSeq uint32) xdr.LedgerCloseMeta { - return xdr.LedgerCloseMeta{ - V: int32(0), - V0: &xdr.LedgerCloseMetaV0{ - LedgerHeader: xdr.LedgerHeaderHistoryEntry{ - Header: xdr.LedgerHeader{ - LedgerSeq: xdr.Uint32(ledgerSeq), - }, - }, - TxSet: xdr.TransactionSet{}, - TxProcessing: nil, - UpgradesProcessing: nil, - ScpInfo: nil, - }, - V1: nil, - } -} diff --git a/support/datastore/ledger_meta_archive_test.go b/support/datastore/ledger_meta_archive_test.go deleted file mode 100644 index 26eeadc313..0000000000 --- a/support/datastore/ledger_meta_archive_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package datastore - -import ( - "fmt" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/require" -) - -func createLedgerCloseMeta(ledgerSeq uint32) xdr.LedgerCloseMeta { - return xdr.LedgerCloseMeta{ - V0: &xdr.LedgerCloseMetaV0{ - LedgerHeader: xdr.LedgerHeaderHistoryEntry{ - Header: xdr.LedgerHeader{ - LedgerSeq: xdr.Uint32(ledgerSeq), - }, - }, - }, - } -} - -func TestLedgerMetaArchive_AddLedgerValidRange(t *testing.T) { - - tests := []struct { - name string - startSeq uint32 - endSeq uint32 - seqNum uint32 - errMsg string - }{ - {startSeq: 10, endSeq: 100, seqNum: 10, errMsg: ""}, - {startSeq: 10, endSeq: 100, seqNum: 11, errMsg: ""}, - {startSeq: 10, endSeq: 100, seqNum: 99, errMsg: ""}, - {startSeq: 10, endSeq: 100, seqNum: 100, errMsg: ""}, - {startSeq: 10, endSeq: 100, seqNum: 9, errMsg: "ledger sequence 9 is outside valid range [10, 100]"}, - {startSeq: 10, endSeq: 100, seqNum: 101, errMsg: "ledger sequence 101 is outside valid range [10, 100]"}, - } - for _, tt := range tests { - t.Run(fmt.Sprintf("range [%d, %d]: Add seq %d", tt.startSeq, tt.endSeq, tt.seqNum), - func(t *testing.T) { - f := NewLedgerMetaArchive("", tt.startSeq, tt.endSeq) - err := f.AddLedger(createLedgerCloseMeta(tt.seqNum)) - if tt.errMsg != "" { - require.EqualError(t, err, tt.errMsg) - } else { - require.NoError(t, err) - } - }) - } -} -func TestLedgerMetaArchive_AddLedgerSequential(t *testing.T) { - var start, end uint32 = 1, 100 - f := NewLedgerMetaArchive("", start, end+100) - - // Add ledgers sequentially - for i := start; i <= end; i++ { - require.NoError(t, f.AddLedger(createLedgerCloseMeta(i))) - } - - // Test out of sequence - testCases := []struct { - ledgerSeq uint32 - expectedErrMsg string - }{ - { - end + 2, - fmt.Sprintf("ledgers must be added sequentially: expected sequence %d, got %d", end+1, end+2), - }, - { - end, - fmt.Sprintf("ledgers must be added sequentially: expected sequence %d, got %d", end+1, end), - }, - { - end - 1, - fmt.Sprintf("ledgers must be added sequentially: expected sequence %d, got %d", end+1, end-1), - }, - } - - for _, tc := range testCases { - err := f.AddLedger(createLedgerCloseMeta(tc.ledgerSeq)) - require.EqualError(t, err, tc.expectedErrMsg) - } -} diff --git a/xdr/ledger_close_meta_batch.go b/xdr/ledger_close_meta_batch.go new file mode 100644 index 0000000000..7fb212d85e --- /dev/null +++ b/xdr/ledger_close_meta_batch.go @@ -0,0 +1,41 @@ +package xdr + +import ( + "fmt" +) + +// GetLedger retrieves the LedgerCloseMeta for a given sequence number. +// It returns an error if LedgerCloseMeta for the sequence number is not found in the batch. +func (s *LedgerCloseMetaBatch) GetLedger(sequence uint32) (LedgerCloseMeta, error) { + + if sequence < uint32(s.StartSequence) || sequence > uint32(s.EndSequence) { + return LedgerCloseMeta{}, fmt.Errorf("ledger sequence %d is outside the "+ + "valid range of ledger sequences [%d, %d] this batch holds", + sequence, s.StartSequence, s.EndSequence) + } + + ledgerIndex := sequence - uint32(s.StartSequence) + if ledgerIndex >= uint32(len(s.LedgerCloseMetas)) { + return LedgerCloseMeta{}, fmt.Errorf("LedgerCloseMeta for sequence %d not found in the batch", sequence) + } + return s.LedgerCloseMetas[ledgerIndex], nil +} + +// AddLedger adds a LedgerCloseMeta to the batch. +func (s *LedgerCloseMetaBatch) AddLedger(ledgerCloseMeta LedgerCloseMeta) error { + if ledgerCloseMeta.LedgerSequence() < uint32(s.StartSequence) || + ledgerCloseMeta.LedgerSequence() > uint32(s.EndSequence) { + return fmt.Errorf("ledger sequence %d is outside valid range [%d, %d]", + ledgerCloseMeta.LedgerSequence(), s.StartSequence, s.EndSequence) + } + + if len(s.LedgerCloseMetas) > 0 { + lastSequence := s.LedgerCloseMetas[len(s.LedgerCloseMetas)-1].LedgerSequence() + if ledgerCloseMeta.LedgerSequence() != lastSequence+1 { + return fmt.Errorf("ledgers must be added sequentially: expected sequence %d, got %d", + lastSequence+1, ledgerCloseMeta.LedgerSequence()) + } + } + s.LedgerCloseMetas = append(s.LedgerCloseMetas, ledgerCloseMeta) + return nil +} diff --git a/xdr/ledger_close_meta_batch_test.go b/xdr/ledger_close_meta_batch_test.go new file mode 100644 index 0000000000..66197db5c1 --- /dev/null +++ b/xdr/ledger_close_meta_batch_test.go @@ -0,0 +1,183 @@ +package xdr + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLedgerMetaArchive_AddLedgerValidRange(t *testing.T) { + + tests := []struct { + name string + startSeq uint32 + endSeq uint32 + seqNum uint32 + errMsg string + }{ + {startSeq: 10, endSeq: 100, seqNum: 10, errMsg: ""}, + {startSeq: 10, endSeq: 100, seqNum: 11, errMsg: ""}, + {startSeq: 10, endSeq: 100, seqNum: 99, errMsg: ""}, + {startSeq: 10, endSeq: 100, seqNum: 100, errMsg: ""}, + {startSeq: 10, endSeq: 100, seqNum: 9, errMsg: "ledger sequence 9 is outside valid range [10, 100]"}, + {startSeq: 10, endSeq: 100, seqNum: 101, errMsg: "ledger sequence 101 is outside valid range [10, 100]"}, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("range [%d, %d]: Add seq %d", tt.startSeq, tt.endSeq, tt.seqNum), + func(t *testing.T) { + f := LedgerCloseMetaBatch{StartSequence: Uint32(tt.startSeq), EndSequence: Uint32(tt.endSeq)} + err := f.AddLedger(LedgerCloseMeta{ + V: int32(0), + V0: &LedgerCloseMetaV0{ + LedgerHeader: LedgerHeaderHistoryEntry{ + Header: LedgerHeader{ + LedgerSeq: Uint32(tt.seqNum), + }, + }, + TxSet: TransactionSet{}, + TxProcessing: nil, + UpgradesProcessing: nil, + ScpInfo: nil, + }, + V1: nil, + }) + if tt.errMsg != "" { + require.EqualError(t, err, tt.errMsg) + } else { + require.NoError(t, err) + } + }) + } +} +func TestLedgerMetaArchive_AddLedgerSequential(t *testing.T) { + var start, end uint32 = 1, 100 + f := LedgerCloseMetaBatch{StartSequence: Uint32(start), EndSequence: Uint32(end + 100)} + + // Add ledgers sequentially + for i := start; i <= end; i++ { + require.NoError(t, f.AddLedger(LedgerCloseMeta{ + V: int32(0), + V0: &LedgerCloseMetaV0{ + LedgerHeader: LedgerHeaderHistoryEntry{ + Header: LedgerHeader{ + LedgerSeq: Uint32(i), + }, + }, + TxSet: TransactionSet{}, + TxProcessing: nil, + UpgradesProcessing: nil, + ScpInfo: nil, + }, + V1: nil, + })) + } + + // Test out of sequence + testCases := []struct { + ledgerSeq uint32 + expectedErrMsg string + }{ + { + end + 2, + fmt.Sprintf("ledgers must be added sequentially: expected sequence %d, got %d", end+1, end+2), + }, + { + end, + fmt.Sprintf("ledgers must be added sequentially: expected sequence %d, got %d", end+1, end), + }, + { + end - 1, + fmt.Sprintf("ledgers must be added sequentially: expected sequence %d, got %d", end+1, end-1), + }, + } + + for _, tc := range testCases { + err := f.AddLedger(LedgerCloseMeta{ + V: int32(0), + V0: &LedgerCloseMetaV0{ + LedgerHeader: LedgerHeaderHistoryEntry{ + Header: LedgerHeader{ + LedgerSeq: Uint32(tc.ledgerSeq), + }, + }, + TxSet: TransactionSet{}, + TxProcessing: nil, + UpgradesProcessing: nil, + ScpInfo: nil, + }, + V1: nil, + }) + require.EqualError(t, err, tc.expectedErrMsg) + } +} + +func TestGetLedger(t *testing.T) { + var start, end uint32 = 121, 1300 + f := LedgerCloseMetaBatch{StartSequence: Uint32(start), EndSequence: Uint32(end)} + + for i := start; i <= end-10; i++ { + f.LedgerCloseMetas = append(f.LedgerCloseMetas, LedgerCloseMeta{ + V: int32(0), + V0: &LedgerCloseMetaV0{ + LedgerHeader: LedgerHeaderHistoryEntry{ + Header: LedgerHeader{ + LedgerSeq: Uint32(i), + }, + }, + TxSet: TransactionSet{}, + TxProcessing: nil, + UpgradesProcessing: nil, + ScpInfo: nil, + }, + V1: nil, + }) + } + + testCases := []struct { + name string + ledgerSeq uint32 + expectedErrMsg string + }{ + { + name: "LedgerSequenceInRange", + ledgerSeq: start, + expectedErrMsg: "", + }, + { + name: "LedgerSequenceInRange", + ledgerSeq: start + 10, + expectedErrMsg: "", + }, + { + name: "LedgerSequenceAboveRange", + ledgerSeq: end + 1, + expectedErrMsg: fmt.Sprintf("ledger sequence %d is outside the valid range "+ + "of ledger sequences [%d, %d] this batch holds", end+1, start, end), + }, + { + name: "LedgerSequenceBelowRange", + ledgerSeq: start - 1, + expectedErrMsg: fmt.Sprintf("ledger sequence %d is outside the valid range "+ + "of ledger sequences [%d, %d] this batch holds", start-1, start, end), + }, + { + name: "LedgerCloseMetaNotFound", + ledgerSeq: end - 5, + expectedErrMsg: fmt.Sprintf("LedgerCloseMeta for sequence %d not found in the batch", end-5), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + archive, err := f.GetLedger(tc.ledgerSeq) + if tc.expectedErrMsg != "" { + require.EqualError(t, err, tc.expectedErrMsg) + require.Equal(t, archive, LedgerCloseMeta{}) + } else { + require.NoError(t, err) + require.Equal(t, archive.LedgerSequence(), tc.ledgerSeq) + } + }) + } +} From 5a41b35abe54335d1e86d9a106410be79a786e7e Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Thu, 16 May 2024 16:28:49 +0200 Subject: [PATCH 176/234] ingest/ledgerbackend: tweak enforcing toml parameters and tests (#5317) * ingest/ledgerbackend: tweak enforcing toml parameters and tests * Enable `ENABLE_DIAGNOSTICS_FOR_TX_SUBMISSION` when enforcing soroban diagnostics (needed for soroban-rpc) * Add `EnforceSorobanTransactionMetaExtV1` toml parameter, so that `EMIT_SOROBAN_TRANSACTION_META_EXT_V1` is enabled by default (needed for soroban-rpcs new `getFeeStats` endpoint). See https://github.com/stellar/soroban-rpc/pull/172 * Mock core version and protocol in tests so that toml generation is more realistic * Appease go vet --- ...x-disable-diagnostic-events-and-metav1.cfg | 3 + .../appendix-disable-diagnostic-events.cfg | 1 - .../testdata/expected-offline-core.cfg | 2 + ...ffline-enforce-diag-events-and-metav1.cfg} | 2 + ...-with-no-http-port-diag-events-metav1.cfg} | 2 + ingest/ledgerbackend/toml.go | 47 +++++-- ingest/ledgerbackend/toml_test.go | 120 +++++++++--------- 7 files changed, 100 insertions(+), 77 deletions(-) create mode 100644 ingest/ledgerbackend/testdata/appendix-disable-diagnostic-events-and-metav1.cfg delete mode 100644 ingest/ledgerbackend/testdata/appendix-disable-diagnostic-events.cfg rename ingest/ledgerbackend/testdata/{expected-offline-enforce-diagnostic-events.cfg => expected-offline-enforce-diag-events-and-metav1.cfg} (82%) rename ingest/ledgerbackend/testdata/{expected-online-with-no-http-port-diag-events.cfg => expected-online-with-no-http-port-diag-events-metav1.cfg} (85%) diff --git a/ingest/ledgerbackend/testdata/appendix-disable-diagnostic-events-and-metav1.cfg b/ingest/ledgerbackend/testdata/appendix-disable-diagnostic-events-and-metav1.cfg new file mode 100644 index 0000000000..6d3b643426 --- /dev/null +++ b/ingest/ledgerbackend/testdata/appendix-disable-diagnostic-events-and-metav1.cfg @@ -0,0 +1,3 @@ +EMIT_SOROBAN_TRANSACTION_META_EXT_V1=false +ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=false +ENABLE_DIAGNOSTICS_FOR_TX_SUBMISSION=false \ No newline at end of file diff --git a/ingest/ledgerbackend/testdata/appendix-disable-diagnostic-events.cfg b/ingest/ledgerbackend/testdata/appendix-disable-diagnostic-events.cfg deleted file mode 100644 index 9fcb0f804b..0000000000 --- a/ingest/ledgerbackend/testdata/appendix-disable-diagnostic-events.cfg +++ /dev/null @@ -1 +0,0 @@ -ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=false diff --git a/ingest/ledgerbackend/testdata/expected-offline-core.cfg b/ingest/ledgerbackend/testdata/expected-offline-core.cfg index d6a80a628d..53838f6165 100644 --- a/ingest/ledgerbackend/testdata/expected-offline-core.cfg +++ b/ingest/ledgerbackend/testdata/expected-offline-core.cfg @@ -1,5 +1,7 @@ # Generated file, do not edit DATABASE = "sqlite3://stellar.db" +EXPERIMENTAL_BUCKETLIST_DB = true +EXPERIMENTAL_BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT = 12 FAILURE_SAFETY = 0 HTTP_PORT = 0 LOG_FILE_PATH = "" diff --git a/ingest/ledgerbackend/testdata/expected-offline-enforce-diagnostic-events.cfg b/ingest/ledgerbackend/testdata/expected-offline-enforce-diag-events-and-metav1.cfg similarity index 82% rename from ingest/ledgerbackend/testdata/expected-offline-enforce-diagnostic-events.cfg rename to ingest/ledgerbackend/testdata/expected-offline-enforce-diag-events-and-metav1.cfg index 48b6223377..8c66b4a5ad 100644 --- a/ingest/ledgerbackend/testdata/expected-offline-enforce-diagnostic-events.cfg +++ b/ingest/ledgerbackend/testdata/expected-offline-enforce-diag-events-and-metav1.cfg @@ -1,4 +1,6 @@ # Generated file, do not edit +EMIT_SOROBAN_TRANSACTION_META_EXT_V1 = true +ENABLE_DIAGNOSTICS_FOR_TX_SUBMISSION = true ENABLE_SOROBAN_DIAGNOSTIC_EVENTS = true FAILURE_SAFETY = 0 HTTP_PORT = 0 diff --git a/ingest/ledgerbackend/testdata/expected-online-with-no-http-port-diag-events.cfg b/ingest/ledgerbackend/testdata/expected-online-with-no-http-port-diag-events-metav1.cfg similarity index 85% rename from ingest/ledgerbackend/testdata/expected-online-with-no-http-port-diag-events.cfg rename to ingest/ledgerbackend/testdata/expected-online-with-no-http-port-diag-events-metav1.cfg index fa785894e5..f2227376a9 100644 --- a/ingest/ledgerbackend/testdata/expected-online-with-no-http-port-diag-events.cfg +++ b/ingest/ledgerbackend/testdata/expected-online-with-no-http-port-diag-events-metav1.cfg @@ -1,4 +1,6 @@ # Generated file, do not edit +EMIT_SOROBAN_TRANSACTION_META_EXT_V1 = true +ENABLE_DIAGNOSTICS_FOR_TX_SUBMISSION = true ENABLE_SOROBAN_DIAGNOSTIC_EVENTS = true FAILURE_SAFETY = -1 HTTP_PORT = 11626 diff --git a/ingest/ledgerbackend/toml.go b/ingest/ledgerbackend/toml.go index f156d5b888..a4f61a454d 100644 --- a/ingest/ledgerbackend/toml.go +++ b/ingest/ledgerbackend/toml.go @@ -342,8 +342,12 @@ type CaptiveCoreTomlParams struct { UseDB bool // the path to the core binary, used to introspect core at runtime, determine some toml capabilities CoreBinaryPath string - // Enforce EnableSorobanDiagnosticEvents when not disabled explicitly + // Enforce EnableSorobanDiagnosticEvents and EnableDiagnosticsForTxSubmission when not disabled explicitly EnforceSorobanDiagnosticEvents bool + // Enfore EnableSorobanTransactionMetaExtV1 when not disabled explicitly + EnforceSorobanTransactionMetaExtV1 bool + // used for testing + checkCoreVersion func(coreBinaryPath string) coreVersion } // NewCaptiveCoreTomlFromFile constructs a new CaptiveCoreToml instance by merging configuration @@ -470,7 +474,7 @@ func (c *coreVersion) IsProtocolVersionEqualOrAbove(protocolVer int) bool { return c.ledgerProtocolVersion >= protocolVer } -func (c *CaptiveCoreToml) checkCoreVersion(coreBinaryPath string) coreVersion { +func checkCoreVersion(coreBinaryPath string) coreVersion { if coreBinaryPath == "" { return coreVersion{} } @@ -529,10 +533,14 @@ func (c *CaptiveCoreToml) setDefaults(params CaptiveCoreTomlParams) { c.Database = "sqlite3://stellar.db" } - coreVersion := c.checkCoreVersion(params.CoreBinaryPath) + checkCoreVersionF := params.checkCoreVersion + if checkCoreVersionF == nil { + checkCoreVersionF = checkCoreVersion + } + currentCoreVersion := checkCoreVersionF(params.CoreBinaryPath) if def := c.tree.Has("EXPERIMENTAL_BUCKETLIST_DB"); !def && params.UseDB { // Supports version 19.6 and above - if coreVersion.IsEqualOrAbove(MinimalBucketListDBCoreSupportVersionMajor, MinimalBucketListDBCoreSupportVersionMinor) { + if currentCoreVersion.IsEqualOrAbove(MinimalBucketListDBCoreSupportVersionMajor, MinimalBucketListDBCoreSupportVersionMinor) { c.UseBucketListDB = true } } @@ -575,19 +583,30 @@ func (c *CaptiveCoreToml) setDefaults(params CaptiveCoreTomlParams) { } } - // starting version 20, we have dignostics events. - if params.EnforceSorobanDiagnosticEvents && coreVersion.IsProtocolVersionEqualOrAbove(MinimalSorobanProtocolSupport) { - if c.EnableSorobanDiagnosticEvents == nil { - // We are generating the file from scratch or the user didn't explicitly oppose to diagnostic events in the config file. - // Enforce it. - t := true - c.EnableSorobanDiagnosticEvents = &t + if params.EnforceSorobanDiagnosticEvents { + if currentCoreVersion.IsEqualOrAbove(20, 0) { + enforceOption(&c.EnableSorobanDiagnosticEvents) } - if !*c.EnableSorobanDiagnosticEvents { - // The user opposed to diagnostic events in the config file, but there is no need to pass on the option - c.EnableSorobanDiagnosticEvents = nil + if currentCoreVersion.IsEqualOrAbove(20, 1) { + enforceOption(&c.EnableDiagnosticsForTxSubmission) } } + if params.EnforceSorobanTransactionMetaExtV1 && currentCoreVersion.IsEqualOrAbove(20, 4) { + enforceOption(&c.EnableEmitSorobanTransactionMetaExtV1) + } +} + +func enforceOption(opt **bool) { + if *opt == nil { + // We are generating the file from scratch or the user didn't explicitly oppose the option. + // Enforce it. + t := true + *opt = &t + } + if !**opt { + // The user opposed the option, but there is no need to pass it on + *opt = nil + } } func (c *CaptiveCoreToml) validate(params CaptiveCoreTomlParams) error { diff --git a/ingest/ledgerbackend/toml_test.go b/ingest/ledgerbackend/toml_test.go index c5d40c77e3..fecf50c8d9 100644 --- a/ingest/ledgerbackend/toml_test.go +++ b/ingest/ledgerbackend/toml_test.go @@ -232,7 +232,7 @@ func checkTestingAboveProtocol19() bool { } func TestGenerateConfig(t *testing.T) { - testCases := []struct { + for _, testCase := range []struct { name string appendPath string mode stellarCoreRunnerMode @@ -242,6 +242,7 @@ func TestGenerateConfig(t *testing.T) { logPath *string useDB bool enforceSorobanDiagnosticEvents bool + enforceEmitMetaV1 bool }{ { name: "offline config with no appendix", @@ -315,67 +316,63 @@ func TestGenerateConfig(t *testing.T) { httpPort: newUint(6789), peerPort: newUint(12345), logPath: nil, - }} - if checkTestingAboveProtocol19() { - testCases = append(testCases, []struct { - name string - appendPath string - mode stellarCoreRunnerMode - expectedPath string - httpPort *uint - peerPort *uint - logPath *string - useDB bool - enforceSorobanDiagnosticEvents bool - }{ - { - name: "offline config with enforce diagnostic events", - mode: stellarCoreRunnerModeOffline, - expectedPath: filepath.Join("testdata", "expected-offline-enforce-diagnostic-events.cfg"), - logPath: nil, - enforceSorobanDiagnosticEvents: true, - }, - { - name: "offline config disabling enforced diagnostic events", - mode: stellarCoreRunnerModeOffline, - expectedPath: filepath.Join("testdata", "expected-offline-enforce-disabled-diagnostic-events.cfg"), - appendPath: filepath.Join("testdata", "appendix-disable-diagnostic-events.cfg"), - logPath: nil, - enforceSorobanDiagnosticEvents: true, - }, - { - name: "online config with enforce diagnostic events", - mode: stellarCoreRunnerModeOnline, - appendPath: filepath.Join("testdata", "sample-appendix.cfg"), - expectedPath: filepath.Join("testdata", "expected-online-with-no-http-port-diag-events.cfg"), - httpPort: nil, - peerPort: newUint(12345), - logPath: nil, - enforceSorobanDiagnosticEvents: true, - }, - { - name: "offline config with minimum persistent entry in appendix", - mode: stellarCoreRunnerModeOnline, - appendPath: filepath.Join("testdata", "appendix-with-minimum-persistent-entry.cfg"), - expectedPath: filepath.Join("testdata", "expected-online-with-appendix-minimum-persistent-entry.cfg"), - logPath: nil, - }, - }...) - } - - for _, testCase := range testCases { + }, + { + name: "offline config with enforce diagnostic events and metav1", + mode: stellarCoreRunnerModeOffline, + expectedPath: filepath.Join("testdata", "expected-offline-enforce-diag-events-and-metav1.cfg"), + logPath: nil, + enforceSorobanDiagnosticEvents: true, + enforceEmitMetaV1: true, + }, + { + name: "offline config disabling enforced diagnostic events and metav1", + mode: stellarCoreRunnerModeOffline, + expectedPath: filepath.Join("testdata", "expected-offline-enforce-disabled-diagnostic-events.cfg"), + appendPath: filepath.Join("testdata", "appendix-disable-diagnostic-events-and-metav1.cfg"), + logPath: nil, + enforceSorobanDiagnosticEvents: true, + enforceEmitMetaV1: true, + }, + { + name: "online config with enforce diagnostic events and meta v1", + mode: stellarCoreRunnerModeOnline, + appendPath: filepath.Join("testdata", "sample-appendix.cfg"), + expectedPath: filepath.Join("testdata", "expected-online-with-no-http-port-diag-events-metav1.cfg"), + httpPort: nil, + peerPort: newUint(12345), + logPath: nil, + enforceSorobanDiagnosticEvents: true, + enforceEmitMetaV1: true, + }, + { + name: "offline config with minimum persistent entry in appendix", + mode: stellarCoreRunnerModeOnline, + appendPath: filepath.Join("testdata", "appendix-with-minimum-persistent-entry.cfg"), + expectedPath: filepath.Join("testdata", "expected-online-with-appendix-minimum-persistent-entry.cfg"), + logPath: nil, + }, + } { t.Run(testCase.name, func(t *testing.T) { var err error var captiveCoreToml *CaptiveCoreToml params := CaptiveCoreTomlParams{ - NetworkPassphrase: "Public Global Stellar Network ; September 2015", - HistoryArchiveURLs: []string{"http://localhost:1170"}, - HTTPPort: testCase.httpPort, - PeerPort: testCase.peerPort, - LogPath: testCase.logPath, - Strict: false, - UseDB: testCase.useDB, - EnforceSorobanDiagnosticEvents: testCase.enforceSorobanDiagnosticEvents, + NetworkPassphrase: "Public Global Stellar Network ; September 2015", + HistoryArchiveURLs: []string{"http://localhost:1170"}, + HTTPPort: testCase.httpPort, + PeerPort: testCase.peerPort, + LogPath: testCase.logPath, + Strict: false, + UseDB: testCase.useDB, + EnforceSorobanDiagnosticEvents: testCase.enforceSorobanDiagnosticEvents, + EnforceSorobanTransactionMetaExtV1: testCase.enforceEmitMetaV1, + checkCoreVersion: func(coreBinaryPath string) coreVersion { + return coreVersion{ + major: 21, + minor: 0, + ledgerProtocolVersion: 21, + } + }, } if testCase.appendPath != "" { captiveCoreToml, err = NewCaptiveCoreTomlFromFile(testCase.appendPath, params) @@ -390,7 +387,7 @@ func TestGenerateConfig(t *testing.T) { expectedByte, err := ioutil.ReadFile(testCase.expectedPath) assert.NoError(t, err) - assert.Equal(t, string(configBytes), string(expectedByte)) + assert.Equal(t, string(expectedByte), string(configBytes)) }) } } @@ -507,7 +504,6 @@ func TestCheckCoreVersion(t *testing.T) { t.SkipNow() return } - var cctoml CaptiveCoreToml - version := cctoml.checkCoreVersion(coreBin) - require.True(t, version.IsEqualOrAbove(19, 0)) + version := checkCoreVersion(coreBin) + require.True(t, version.IsEqualOrAbove(20, 0)) } From 13899498e810364e58893cf5445c661eb88c71e9 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Thu, 16 May 2024 11:37:37 -0700 Subject: [PATCH 177/234] exp/services/ledgerexporter: Update docs with default compression zstd (#5318) --- exp/services/ledgerexporter/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/exp/services/ledgerexporter/README.md b/exp/services/ledgerexporter/README.md index 5d483f0858..967bd1dc9e 100644 --- a/exp/services/ledgerexporter/README.md +++ b/exp/services/ledgerexporter/README.md @@ -5,7 +5,7 @@ The Ledger Exporter is a tool designed to export ledger data from a Stellar netw Ledger Exporter currently uses captive-core as the ledger backend and GCS as the destination data store. # Exported Data Format -The tool allows for the export of multiple ledgers in a single exported file. The exported data is in XDR format and is compressed using gzip before being uploaded. +The tool allows for the export of multiple ledgers in a single exported file. The exported data is in XDR format and is compressed using zstd before being uploaded. ```go type LedgerCloseMetaBatch struct { @@ -68,13 +68,13 @@ files_per_partition = 10 - Files are further organized into partitions, with the number of files per partition set by `files_per_partition`. ### Filename Structure: -- Filenames indicate the ledger range they contain, e.g., `0-63.xdr.gz` holds ledgers 0 to 63. +- Filenames indicate the ledger range they contain, e.g., `0-63.xdr.zstd` holds ledgers 0 to 63. - Partition directories group files, e.g., `/0-639/` holds files for ledgers 0 to 639. #### Example: with `ledgers_per_file = 64` and `files_per_partition = 10`: - Partition names: `/0-639`, `/640-1279`, ... -- Filenames: `/0-639/0-63.xdr.gz`, `/0-639/64-127.xdr.gz`, ... +- Filenames: `/0-639/0-63.xdr.zstd`, `/0-639/64-127.xdr.zstd`, ... #### Special Cases: From 7d6f7e789ac62096260177aa822d33e24a9fc52a Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 17 May 2024 17:06:59 +0100 Subject: [PATCH 178/234] PrepareRange() in exporter manager goroutine (#5321) --- exp/services/ledgerexporter/internal/app.go | 14 +----- .../ledgerexporter/internal/exportmanager.go | 32 +++++++------ .../internal/exportmanager_test.go | 46 +++++++++++-------- 3 files changed, 46 insertions(+), 46 deletions(-) diff --git a/exp/services/ledgerexporter/internal/app.go b/exp/services/ledgerexporter/internal/app.go index b90c7a33c3..fe84022d65 100644 --- a/exp/services/ledgerexporter/internal/app.go +++ b/exp/services/ledgerexporter/internal/app.go @@ -112,7 +112,7 @@ func (a *App) init(ctx context.Context) error { logger.Infof("Final computed ledger range for backend retrieval and export, start=%d, end=%d", a.config.StartLedger, a.config.EndLedger) - if a.ledgerBackend, err = newLedgerBackend(ctx, a.config, registry); err != nil { + if a.ledgerBackend, err = newLedgerBackend(a.config, registry); err != nil { return err } @@ -247,7 +247,7 @@ func (a *App) Run() { // newLedgerBackend Creates and initializes captive core ledger backend // Currently, only supports captive-core as ledger backend -func newLedgerBackend(ctx context.Context, config *Config, prometheusRegistry *prometheus.Registry) (ledgerbackend.LedgerBackend, error) { +func newLedgerBackend(config *Config, prometheusRegistry *prometheus.Registry) (ledgerbackend.LedgerBackend, error) { captiveConfig, err := config.GenerateCaptiveCoreConfig() if err != nil { return nil, err @@ -261,15 +261,5 @@ func newLedgerBackend(ctx context.Context, config *Config, prometheusRegistry *p } backend = ledgerbackend.WithMetrics(backend, prometheusRegistry, "ledger_exporter") - var ledgerRange ledgerbackend.Range - if config.EndLedger == 0 { - ledgerRange = ledgerbackend.UnboundedRange(config.StartLedger) - } else { - ledgerRange = ledgerbackend.BoundedRange(config.StartLedger, config.EndLedger) - } - - if err = backend.PrepareRange(ctx, ledgerRange); err != nil { - return nil, errors.Wrap(err, "Could not prepare captive core ledger backend") - } return backend, nil } diff --git a/exp/services/ledgerexporter/internal/exportmanager.go b/exp/services/ledgerexporter/internal/exportmanager.go index 5e6c87bbf5..55f85b9c46 100644 --- a/exp/services/ledgerexporter/internal/exportmanager.go +++ b/exp/services/ledgerexporter/internal/exportmanager.go @@ -89,21 +89,25 @@ func (e *ExportManager) Run(ctx context.Context, startLedger, endLedger uint32) "end_ledger": strconv.FormatUint(uint64(endLedger), 10), } + var ledgerRange ledgerbackend.Range + if endLedger < 1 { + ledgerRange = ledgerbackend.UnboundedRange(startLedger) + } else { + ledgerRange = ledgerbackend.BoundedRange(startLedger, endLedger) + } + if err := e.ledgerBackend.PrepareRange(ctx, ledgerRange); err != nil { + return errors.Wrap(err, "Could not prepare captive core ledger backend") + } + for nextLedger := startLedger; endLedger < 1 || nextLedger <= endLedger; nextLedger++ { - select { - case <-ctx.Done(): - logger.Info("Stopping ExportManager due to context cancellation") - return ctx.Err() - default: - ledgerCloseMeta, err := e.ledgerBackend.GetLedger(ctx, nextLedger) - if err != nil { - return errors.Wrapf(err, "failed to retrieve ledger %d from the ledger backend", nextLedger) - } - e.latestLedgerMetric.With(labels).Set(float64(nextLedger)) - err = e.AddLedgerCloseMeta(ctx, ledgerCloseMeta) - if err != nil { - return errors.Wrapf(err, "failed to add ledgerCloseMeta for ledger %d", nextLedger) - } + ledgerCloseMeta, err := e.ledgerBackend.GetLedger(ctx, nextLedger) + if err != nil { + return errors.Wrapf(err, "failed to retrieve ledger %d from the ledger backend", nextLedger) + } + e.latestLedgerMetric.With(labels).Set(float64(nextLedger)) + err = e.AddLedgerCloseMeta(ctx, ledgerCloseMeta) + if err != nil { + return errors.Wrapf(err, "failed to add ledgerCloseMeta for ledger %d", nextLedger) } } logger.Infof("ExportManager successfully exported ledgers from %d to %d", startLedger, endLedger) diff --git a/exp/services/ledgerexporter/internal/exportmanager_test.go b/exp/services/ledgerexporter/internal/exportmanager_test.go index a84e20fb52..d99f88cd14 100644 --- a/exp/services/ledgerexporter/internal/exportmanager_test.go +++ b/exp/services/ledgerexporter/internal/exportmanager_test.go @@ -9,7 +9,6 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/stellar/go/ingest/ledgerbackend" @@ -61,7 +60,7 @@ func (s *ExportManagerSuite) TestInvalidExportConfig() { registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) _, err := NewExportManager(config, &s.mockBackend, queue, registry) - require.Error(s.T(), err) + s.Require().Error(err) } func (s *ExportManagerSuite) TestRun() { @@ -69,11 +68,12 @@ func (s *ExportManagerSuite) TestRun() { registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) - require.NoError(s.T(), err) + s.Require().NoError(err) start := uint32(0) end := uint32(255) expectedKeys := set.NewSet[string](10) + s.mockBackend.On("PrepareRange", s.ctx, ledgerbackend.BoundedRange(start, end)).Return(nil) for i := start; i <= end; i++ { s.mockBackend.On("GetLedger", s.ctx, i). Return(createLedgerCloseMeta(i), nil) @@ -97,13 +97,12 @@ func (s *ExportManagerSuite) TestRun() { }() err = exporter.Run(s.ctx, start, end) - require.NoError(s.T(), err) + s.Require().NoError(err) wg.Wait() - require.Equal(s.T(), expectedKeys, actualKeys) - require.Equal( - s.T(), + s.Require().Equal(expectedKeys, actualKeys) + s.Require().Equal( float64(255), getMetricValue(exporter.latestLedgerMetric.With( prometheus.Labels{ @@ -119,9 +118,10 @@ func (s *ExportManagerSuite) TestRunContextCancel() { registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) - require.NoError(s.T(), err) + s.Require().NoError(err) ctx, cancel := context.WithCancel(context.Background()) + s.mockBackend.On("PrepareRange", ctx, ledgerbackend.BoundedRange(0, 255)).Return(nil) s.mockBackend.On("GetLedger", mock.Anything, mock.Anything). Return(createLedgerCloseMeta(1), nil) @@ -139,7 +139,7 @@ func (s *ExportManagerSuite) TestRunContextCancel() { }() err = exporter.Run(ctx, 0, 255) - require.EqualError(s.T(), err, "failed to add ledgerCloseMeta for ledger 128: context canceled") + s.Require().EqualError(err, "failed to add ledgerCloseMeta for ledger 128: context canceled") } @@ -148,12 +148,18 @@ func (s *ExportManagerSuite) TestRunWithCanceledContext() { registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) - require.NoError(s.T(), err) + s.Require().NoError(err) ctx, cancel := context.WithCancel(context.Background()) cancel() + + s.mockBackend.On("PrepareRange", ctx, ledgerbackend.BoundedRange(1, 10)). + Return(context.Canceled).Run(func(args mock.Arguments) { + ctx := args.Get(0).(context.Context) + s.Require().ErrorIs(ctx.Err(), context.Canceled) + }) err = exporter.Run(ctx, 1, 10) - require.EqualError(s.T(), err, "context canceled") + s.Require().ErrorIs(err, context.Canceled) } func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { @@ -161,7 +167,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) - require.NoError(s.T(), err) + s.Require().NoError(err) expectedKeys := set.NewSet[string](10) actualKeys := set.NewSet[string](10) @@ -183,7 +189,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { start := uint32(0) end := uint32(255) for i := start; i <= end; i++ { - require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(i))) + s.Require().NoError(exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(i))) key := config.GetObjectKeyFromSequenceNumber(i) expectedKeys.Add(key) @@ -191,7 +197,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { queue.Close() wg.Wait() - require.Equal(s.T(), expectedKeys, actualKeys) + s.Require().Equal(expectedKeys, actualKeys) } func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { @@ -199,7 +205,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) - require.NoError(s.T(), err) + s.Require().NoError(err) ctx, cancel := context.WithCancel(context.Background()) go func() { @@ -207,9 +213,9 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { cancel() }() - require.NoError(s.T(), exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(1))) + s.Require().NoError(exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(1))) err = exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(2)) - require.EqualError(s.T(), err, "context canceled") + s.Require().EqualError(err, "context canceled") } func (s *ExportManagerSuite) TestAddLedgerCloseMetaKeyMismatch() { @@ -217,9 +223,9 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMetaKeyMismatch() { registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) - require.NoError(s.T(), err) + s.Require().NoError(err) - require.NoError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(16))) - require.EqualError(s.T(), exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(21)), + s.Require().NoError(exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(16))) + s.Require().EqualError(exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(21)), "Current meta archive object key mismatch") } From afd526d41b2d8acce71baad68ca93483fad12499 Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 17 May 2024 17:39:48 +0100 Subject: [PATCH 179/234] Add latest uploaded ledger metric (#5322) --- exp/services/ledgerexporter/internal/app.go | 14 +- .../ledgerexporter/internal/uploader.go | 45 ++++-- .../ledgerexporter/internal/uploader_test.go | 147 +++++++++++------- 3 files changed, 133 insertions(+), 73 deletions(-) diff --git a/exp/services/ledgerexporter/internal/app.go b/exp/services/ledgerexporter/internal/app.go index fe84022d65..e6dde872ce 100644 --- a/exp/services/ledgerexporter/internal/app.go +++ b/exp/services/ledgerexporter/internal/app.go @@ -25,6 +25,15 @@ import ( const ( adminServerReadTimeout = 5 * time.Second adminServerShutdownTimeout = time.Second * 5 + // TODO: make this timeout configurable + uploadShutdownTimeout = 10 * time.Second + // We expect the queue size to rarely exceed 1 or 2 because + // upload speeds are expected to be much faster than the rate at which + // captive core emits ledgers. However, configuring a higher capacity + // than our expectation is useful because if we observe a large queue + // size in our metrics that is an indication that uploads to the + // data store have degraded + uploadQueueCapacity = 128 ) var ( @@ -116,8 +125,7 @@ func (a *App) init(ctx context.Context) error { return err } - // TODO: make queue size configurable instead of hard coding it to 1 - queue := NewUploadQueue(1, registry) + queue := NewUploadQueue(uploadQueueCapacity, registry) if a.exportManager, err = NewExportManager(a.config.LedgerBatchConfig, a.ledgerBackend, queue, registry); err != nil { return err } @@ -190,7 +198,7 @@ func (a *App) Run() { go func() { defer wg.Done() - err := a.uploader.Run(ctx) + err := a.uploader.Run(ctx, uploadShutdownTimeout) if err != nil && !errors.Is(err, context.Canceled) { logger.WithError(err).Error("Error executing Uploader") cancel() diff --git a/exp/services/ledgerexporter/internal/uploader.go b/exp/services/ledgerexporter/internal/uploader.go index 94344ee1ee..78296cde88 100644 --- a/exp/services/ledgerexporter/internal/uploader.go +++ b/exp/services/ledgerexporter/internal/uploader.go @@ -19,6 +19,7 @@ type Uploader struct { queue UploadQueue uploadDurationMetric *prometheus.SummaryVec objectSizeMetrics *prometheus.SummaryVec + latestLedgerMetric prometheus.Gauge } // NewUploader constructs a new Uploader instance @@ -43,12 +44,17 @@ func NewUploader( }, []string{"ledgers", "already_exists", "compression"}, ) - prometheusRegistry.MustRegister(uploadDurationMetric, objectSizeMetrics) + latestLedgerMetric := prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "ledger_exporter", Subsystem: "uploader", Name: "latest_ledger", + Help: "sequence number of the latest ledger uploaded", + }) + prometheusRegistry.MustRegister(uploadDurationMetric, objectSizeMetrics, latestLedgerMetric) return Uploader{ dataStore: destination, queue: queue, uploadDurationMetric: uploadDurationMetric, objectSizeMetrics: objectSizeMetrics, + latestLedgerMetric: latestLedgerMetric, } } @@ -93,6 +99,8 @@ func (u Uploader) Upload(ctx context.Context, metaArchive *LedgerMetaArchive) er if err != nil { return errors.Wrapf(err, "error uploading %s", metaArchive.ObjectKey) } + + logger.Infof("Uploaded %s successfully", metaArchive.ObjectKey) alreadyExists := strconv.FormatBool(!ok) u.uploadDurationMetric.With(prometheus.Labels{ @@ -109,22 +117,36 @@ func (u Uploader) Upload(ctx context.Context, metaArchive *LedgerMetaArchive) er "ledgers": numLedgers, "already_exists": alreadyExists, }).Observe(float64(writerTo.totalCompressed)) + u.latestLedgerMetric.Set(float64(metaArchive.Data.EndSequence)) return nil } -// TODO: make it configurable -var uploaderShutdownWaitTime = 10 * time.Second - // Run starts the uploader, continuously listening for LedgerMetaArchive objects to upload. -func (u Uploader) Run(ctx context.Context) error { +func (u Uploader) Run(ctx context.Context, shutdownDelayTime time.Duration) error { uploadCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func() { - <-ctx.Done() - logger.Info("Context done, waiting for remaining uploads to complete...") - // wait for a few seconds to upload remaining objects from metaArchiveCh - <-time.After(uploaderShutdownWaitTime) - logger.Info("Timeout reached, canceling remaining uploads...") - cancel() + select { + case <-uploadCtx.Done(): + // if uploadCtx is cancelled that means we have exited Run() + // and therefore there are no remaining uploads + return + case <-ctx.Done(): + logger.Info("Received shutdown signal, waiting for remaining uploads to complete...") + } + + select { + case <-time.After(shutdownDelayTime): + // wait for some time to upload remaining objects from + // the upload queue + logger.Info("Timeout reached, canceling remaining uploads...") + cancel() + case <-uploadCtx.Done(): + // if uploadCtx is cancelled that means we have exited Run() + // and therefore there are no remaining uploads + return + } }() for { @@ -141,6 +163,5 @@ func (u Uploader) Run(ctx context.Context) error { if err = u.Upload(uploadCtx, metaObject); err != nil { return err } - logger.Infof("Uploaded %s successfully", metaObject.ObjectKey) } } diff --git a/exp/services/ledgerexporter/internal/uploader_test.go b/exp/services/ledgerexporter/internal/uploader_test.go index b31397c183..5fbf5e22d0 100644 --- a/exp/services/ledgerexporter/internal/uploader_test.go +++ b/exp/services/ledgerexporter/internal/uploader_test.go @@ -10,14 +10,16 @@ import ( "time" "github.com/prometheus/client_golang/prometheus" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "github.com/stellar/go/support/compressxdr" "github.com/stellar/go/support/datastore" "github.com/stellar/go/support/errors" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" ) +var testShutdownDelayTime = 300 * time.Millisecond + func TestUploaderSuite(t *testing.T) { suite.Run(t, new(UploaderSuite)) } @@ -34,6 +36,10 @@ func (s *UploaderSuite) SetupTest() { s.mockDataStore = datastore.MockDataStore{} } +func (s *UploaderSuite) TearDownTest() { + s.mockDataStore.AssertExpectations(s.T()) +} + func (s *UploaderSuite) TestUpload() { s.testUpload(false) s.testUpload(true) @@ -52,13 +58,13 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { Run(func(args mock.Arguments) { capturedKey = args.Get(1).(string) _, err := args.Get(2).(io.WriterTo).WriteTo(&capturedBuf) - require.NoError(s.T(), err) + s.Require().NoError(err) }).Return(putOkReturnVal, nil).Once() registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) dataUploader := NewUploader(&s.mockDataStore, queue, registry) - require.NoError(s.T(), dataUploader.Upload(context.Background(), archive)) + s.Require().NoError(dataUploader.Upload(context.Background(), archive)) expectedCompressedLength := capturedBuf.Len() var decodedArchive LedgerMetaArchive @@ -66,31 +72,29 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { decoder := xdrDecoder _, err := decoder.ReadFrom(&capturedBuf) - require.NoError(s.T(), err) + s.Require().NoError(err) // require that the decoded data matches the original test data - require.Equal(s.T(), key, capturedKey) - require.Equal(s.T(), archive.Data, decodedArchive.Data) + s.Require().Equal(key, capturedKey) + s.Require().Equal(archive.Data, decodedArchive.Data) alreadyExists := !putOkReturnVal metric, err := dataUploader.uploadDurationMetric.MetricVec.GetMetricWith(prometheus.Labels{ "ledgers": "100", "already_exists": strconv.FormatBool(alreadyExists), }) - require.NoError(s.T(), err) - require.Equal( - s.T(), + s.Require().NoError(err) + s.Require().Equal( uint64(1), getMetricValue(metric).GetSummary().GetSampleCount(), ) - require.Positive(s.T(), getMetricValue(metric).GetSummary().GetSampleSum()) + s.Require().Positive(getMetricValue(metric).GetSummary().GetSampleSum()) metric, err = dataUploader.uploadDurationMetric.MetricVec.GetMetricWith(prometheus.Labels{ "ledgers": "100", "already_exists": strconv.FormatBool(!alreadyExists), }) - require.NoError(s.T(), err) - require.Equal( - s.T(), + s.Require().NoError(err) + s.Require().Equal( uint64(0), getMetricValue(metric).GetSummary().GetSampleCount(), ) @@ -100,14 +104,12 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { "compression": decoder.Compressor.Name(), "already_exists": strconv.FormatBool(alreadyExists), }) - require.NoError(s.T(), err) - require.Equal( - s.T(), + s.Require().NoError(err) + s.Require().Equal( uint64(1), getMetricValue(metric).GetSummary().GetSampleCount(), ) - require.Equal( - s.T(), + s.Require().Equal( float64(expectedCompressedLength), getMetricValue(metric).GetSummary().GetSampleSum(), ) @@ -116,9 +118,8 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { "compression": decoder.Compressor.Name(), "already_exists": strconv.FormatBool(!alreadyExists), }) - require.NoError(s.T(), err) - require.Equal( - s.T(), + s.Require().NoError(err) + s.Require().Equal( uint64(0), getMetricValue(metric).GetSummary().GetSampleCount(), ) @@ -128,16 +129,14 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { "compression": "none", "already_exists": strconv.FormatBool(alreadyExists), }) - require.NoError(s.T(), err) - require.Equal( - s.T(), + s.Require().NoError(err) + s.Require().Equal( uint64(1), getMetricValue(metric).GetSummary().GetSampleCount(), ) uncompressedPayload, err := decodedArchive.Data.MarshalBinary() - require.NoError(s.T(), err) - require.Equal( - s.T(), + s.Require().NoError(err) + s.Require().Equal( float64(len(uncompressedPayload)), getMetricValue(metric).GetSummary().GetSampleSum(), ) @@ -146,12 +145,16 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { "compression": "none", "already_exists": strconv.FormatBool(!alreadyExists), }) - require.NoError(s.T(), err) - require.Equal( - s.T(), + s.Require().NoError(err) + s.Require().Equal( uint64(0), getMetricValue(metric).GetSummary().GetSampleCount(), ) + + s.Require().Equal( + float64(100), + getMetricValue(dataUploader.latestLedgerMetric).GetGauge().GetValue(), + ) } func (s *UploaderSuite) TestUploadPutError() { @@ -164,22 +167,21 @@ func (s *UploaderSuite) testUploadPutError(putOkReturnVal bool) { archive := NewLedgerMetaArchive(key, start, end) s.mockDataStore.On("PutFileIfNotExists", context.Background(), key, - mock.Anything).Return(putOkReturnVal, errors.New("error in PutFileIfNotExists")) + mock.Anything).Return(putOkReturnVal, errors.New("error in PutFileIfNotExists")).Once() registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) dataUploader := NewUploader(&s.mockDataStore, queue, registry) err := dataUploader.Upload(context.Background(), archive) - require.Equal(s.T(), fmt.Sprintf("error uploading %s: error in PutFileIfNotExists", key), err.Error()) + s.Require().Equal(fmt.Sprintf("error uploading %s: error in PutFileIfNotExists", key), err.Error()) for _, alreadyExists := range []string{"true", "false"} { metric, err := dataUploader.uploadDurationMetric.MetricVec.GetMetricWith(prometheus.Labels{ "ledgers": "100", "already_exists": alreadyExists, }) - require.NoError(s.T(), err) - require.Equal( - s.T(), + s.Require().NoError(err) + s.Require().Equal( uint64(0), getMetricValue(metric).GetSummary().GetSampleCount(), ) @@ -190,61 +192,90 @@ func (s *UploaderSuite) testUploadPutError(putOkReturnVal bool) { "compression": compression, "already_exists": alreadyExists, }) - require.NoError(s.T(), err) - require.Equal( - s.T(), + s.Require().NoError(err) + s.Require().Equal( uint64(0), getMetricValue(metric).GetSummary().GetSampleCount(), ) } + + s.Require().Equal( + float64(0), + getMetricValue(dataUploader.latestLedgerMetric).GetGauge().GetValue(), + ) } } -func (s *UploaderSuite) TestRunChannelClose() { - s.mockDataStore.On("PutFileIfNotExists", mock.Anything, - mock.Anything, mock.Anything).Return(true, nil) +func (s *UploaderSuite) TestRunUntilQueueClose() { + var prev *mock.Call + for i := 1; i <= 100; i++ { + key := fmt.Sprintf("test-%d", i) + cur := s.mockDataStore.On("PutFileIfNotExists", mock.Anything, + key, mock.Anything).Return(true, nil).Once() + if prev != nil { + cur.NotBefore(prev) + } + prev = cur + } registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) go func() { - key, start, end := "test", uint32(1), uint32(100) - for i := start; i <= end; i++ { - s.Assert().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive(key, i, i))) + for i := uint32(1); i <= uint32(100); i++ { + key := fmt.Sprintf("test-%d", i) + s.Require().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive(key, i, i))) } - <-time.After(time.Second * 2) queue.Close() }() dataUploader := NewUploader(&s.mockDataStore, queue, registry) - require.NoError(s.T(), dataUploader.Run(context.Background())) + s.Require().NoError(dataUploader.Run(context.Background(), testShutdownDelayTime)) + + s.Require().Equal( + float64(100), + getMetricValue(dataUploader.latestLedgerMetric).GetGauge().GetValue(), + ) } func (s *UploaderSuite) TestRunContextCancel() { - s.mockDataStore.On("PutFileIfNotExists", mock.Anything, mock.Anything, mock.Anything).Return(true, nil) ctx, cancel := context.WithCancel(context.Background()) registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - s.Assert().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive("test", 1, 1))) + first := s.mockDataStore.On("PutFileIfNotExists", mock.Anything, "test", mock.Anything). + Return(true, nil).Once().Run(func(args mock.Arguments) { + cancel() + }) + s.mockDataStore.On("PutFileIfNotExists", mock.Anything, "test1", mock.Anything). + Return(true, nil).Once().NotBefore(first).Run(func(args mock.Arguments) { + ctxArg := args.Get(0).(context.Context) + s.Require().NoError(ctxArg.Err()) + }) go func() { - <-time.After(time.Second * 2) - cancel() + s.Require().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive("test", 1, 1))) + s.Require().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive("test1", 2, 2))) }() dataUploader := NewUploader(&s.mockDataStore, queue, registry) - require.EqualError(s.T(), dataUploader.Run(ctx), "context canceled") + s.Require().EqualError(dataUploader.Run(ctx, testShutdownDelayTime), "context canceled") + s.Require().Equal( + float64(2), + getMetricValue(dataUploader.latestLedgerMetric).GetGauge().GetValue(), + ) } func (s *UploaderSuite) TestRunUploadError() { registry := prometheus.NewRegistry() - queue := NewUploadQueue(1, registry) + queue := NewUploadQueue(2, registry) + + s.Require().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive("test", 1, 1))) + s.Require().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive("test1", 2, 2))) - s.Assert().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive("test", 1, 1))) s.mockDataStore.On("PutFileIfNotExists", mock.Anything, "test", - mock.Anything).Return(false, errors.New("Put error")) + mock.Anything).Return(false, errors.New("Put error")).Once() dataUploader := NewUploader(&s.mockDataStore, queue, registry) - err := dataUploader.Run(context.Background()) - require.Equal(s.T(), "error uploading test: Put error", err.Error()) + err := dataUploader.Run(context.Background(), testShutdownDelayTime) + s.Require().Equal("error uploading test: Put error", err.Error()) } From e86ca9f0a73eb8bc81e98ec1e9a6fbcc2b4a887f Mon Sep 17 00:00:00 2001 From: tamirms Date: Thu, 30 May 2024 16:49:02 +0100 Subject: [PATCH 180/234] Fix adjustLedgerRange() (#5325) --- exp/services/ledgerexporter/internal/config.go | 5 +---- exp/services/ledgerexporter/internal/config_test.go | 11 ++++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/exp/services/ledgerexporter/internal/config.go b/exp/services/ledgerexporter/internal/config.go index 2f6589a720..bab184bdc7 100644 --- a/exp/services/ledgerexporter/internal/config.go +++ b/exp/services/ledgerexporter/internal/config.go @@ -182,10 +182,7 @@ func (config *Config) adjustLedgerRange() { // Align the end ledger (for bounded cases) to the nearest "LedgersPerFile" boundary. if config.EndLedger != 0 { - // Add an extra batch only if "LedgersPerFile" is greater than 1 and the end ledger doesn't fall on the boundary. - if config.LedgerBatchConfig.LedgersPerFile > 1 && config.EndLedger%config.LedgerBatchConfig.LedgersPerFile != 0 { - config.EndLedger = (config.EndLedger/config.LedgerBatchConfig.LedgersPerFile + 1) * config.LedgerBatchConfig.LedgersPerFile - } + config.EndLedger = config.LedgerBatchConfig.GetSequenceNumberEndBoundary(config.EndLedger) } logger.Infof("Computed effective export boundary ledger range: start=%d, end=%d", config.StartLedger, config.EndLedger) diff --git a/exp/services/ledgerexporter/internal/config_test.go b/exp/services/ledgerexporter/internal/config_test.go index 86f6cfb5b3..133165072f 100644 --- a/exp/services/ledgerexporter/internal/config_test.go +++ b/exp/services/ledgerexporter/internal/config_test.go @@ -5,8 +5,9 @@ import ( "fmt" "testing" - "github.com/stellar/go/historyarchive" "github.com/stretchr/testify/require" + + "github.com/stellar/go/historyarchive" ) func TestNewConfigResumeEnabled(t *testing.T) { @@ -154,7 +155,7 @@ func TestAdjustedLedgerRangeBoundedMode(t *testing.T) { start: 0, end: 1, expectedStart: 2, - expectedEnd: 10, + expectedEnd: 9, }, { name: "Round down start ledger and round up end ledger, 15 ledgers per file ", @@ -162,7 +163,7 @@ func TestAdjustedLedgerRangeBoundedMode(t *testing.T) { start: 4, end: 10, expectedStart: 2, - expectedEnd: 15, + expectedEnd: 14, }, { name: "Round down start ledger and round up end ledger, 64 ledgers per file ", @@ -170,7 +171,7 @@ func TestAdjustedLedgerRangeBoundedMode(t *testing.T) { start: 400, end: 500, expectedStart: 384, - expectedEnd: 512, + expectedEnd: 511, }, { name: "No change, 64 ledger per file", @@ -178,7 +179,7 @@ func TestAdjustedLedgerRangeBoundedMode(t *testing.T) { start: 64, end: 128, expectedStart: 64, - expectedEnd: 128, + expectedEnd: 191, }, } From 083b7bb11d58906e095a8ebb3b1126688aa3ef8b Mon Sep 17 00:00:00 2001 From: tamirms Date: Thu, 30 May 2024 17:00:56 +0100 Subject: [PATCH 181/234] support/datastore: Make resumability robust to unexpected overlaps in adjacent ranges (#5326) --- support/datastore/resumablemanager_test.go | 191 +++++++++++++-------- support/datastore/resumeablemanager.go | 24 +++ 2 files changed, 145 insertions(+), 70 deletions(-) diff --git a/support/datastore/resumablemanager_test.go b/support/datastore/resumablemanager_test.go index 05416658a8..2649a26033 100644 --- a/support/datastore/resumablemanager_test.go +++ b/support/datastore/resumablemanager_test.go @@ -5,12 +5,13 @@ import ( "testing" "github.com/pkg/errors" - "github.com/stellar/go/historyarchive" "github.com/stretchr/testify/require" + + "github.com/stellar/go/historyarchive" ) func TestResumability(t *testing.T) { - + ctx := context.Background() tests := []struct { name string startLedger uint32 @@ -22,6 +23,7 @@ func TestResumability(t *testing.T) { latestLedger uint32 errorSnippet string archiveError error + registerMockCalls func(*MockDataStore) }{ { name: "archive error when resolving network latest", @@ -33,9 +35,10 @@ func TestResumability(t *testing.T) { FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test", - errorSnippet: "archive error", - archiveError: errors.New("archive error"), + networkName: "test", + errorSnippet: "archive error", + archiveError: errors.New("archive error"), + registerMockCalls: func(store *MockDataStore) {}, }, { name: "End ledger same as start, data store has it", @@ -48,6 +51,9 @@ func TestResumability(t *testing.T) { LedgersPerFile: uint32(10), }, networkName: "test", + registerMockCalls: func(mockDataStore *MockDataStore) { + mockDataStore.On("Exists", ctx, "FFFFFFFF--0-9.xdr.zstd").Return(true, nil).Once() + }, }, { name: "End ledger same as start, data store does not have it", @@ -60,6 +66,55 @@ func TestResumability(t *testing.T) { LedgersPerFile: uint32(10), }, networkName: "test", + registerMockCalls: func(mockDataStore *MockDataStore) { + mockDataStore.On("Exists", ctx, "FFFFFFF5--10-19.xdr.zstd").Return(false, nil).Twice() + }, + }, + { + name: "start and end ledger are in same file, data store does not have it", + startLedger: 64, + endLedger: 68, + absentLedger: 64, + findStartOk: true, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(100), + LedgersPerFile: uint32(64), + }, + networkName: "test", + registerMockCalls: func(mockDataStore *MockDataStore) { + mockDataStore.On("Exists", ctx, "FFFFFFFF--0-6399/FFFFFFBF--64-127.xdr.zstd").Return(false, nil).Twice() + }, + }, + { + name: "start and end ledger are in same file, data store has it", + startLedger: 128, + endLedger: 130, + absentLedger: 0, + findStartOk: false, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(100), + LedgersPerFile: uint32(64), + }, + networkName: "test", + registerMockCalls: func(mockDataStore *MockDataStore) { + mockDataStore.On("Exists", ctx, "FFFFFFFF--0-6399/FFFFFF7F--128-191.xdr.zstd").Return(true, nil).Once() + }, + }, + { + name: "ledger range overlaps with a range which is already exported", + startLedger: 2, + endLedger: 127, + absentLedger: 2, + findStartOk: true, + ledgerBatchConfig: LedgerBatchConfig{ + FilesPerPartition: uint32(100), + LedgersPerFile: uint32(64), + }, + networkName: "test", + registerMockCalls: func(mockDataStore *MockDataStore) { + mockDataStore.On("Exists", ctx, "FFFFFFFF--0-6399/FFFFFFBF--64-127.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFFF--0-6399/FFFFFFFF--0-63.xdr.zstd").Return(false, nil).Once() + }, }, { name: "binary search encounters an error during datastore retrieval", @@ -73,6 +128,9 @@ func TestResumability(t *testing.T) { }, networkName: "test", errorSnippet: "datastore error happened", + registerMockCalls: func(mockDataStore *MockDataStore) { + mockDataStore.On("Exists", ctx, "FFFFFFEB--20-29.xdr.zstd").Return(false, errors.New("datastore error happened")).Once() + }, }, { name: "Data store is beyond boundary aligned start ledger", @@ -85,6 +143,11 @@ func TestResumability(t *testing.T) { LedgersPerFile: uint32(10), }, networkName: "test", + registerMockCalls: func(mockDataStore *MockDataStore) { + mockDataStore.On("Exists", ctx, "FFFFFFCD--50-59.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFE1--30-39.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFD7--40-49.xdr.zstd").Return(false, nil).Once() + }, }, { name: "Data store is beyond non boundary aligned start ledger", @@ -97,6 +160,10 @@ func TestResumability(t *testing.T) { LedgersPerFile: uint32(10), }, networkName: "test", + registerMockCalls: func(mockDataStore *MockDataStore) { + mockDataStore.On("Exists", ctx, "FFFFFFB9--70-79.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFAF--80-89.xdr.zstd").Return(false, nil).Twice() + }, }, { name: "Data store is beyond start and end ledger", @@ -109,6 +176,10 @@ func TestResumability(t *testing.T) { LedgersPerFile: uint32(10), }, networkName: "test", + registerMockCalls: func(mockDataStore *MockDataStore) { + mockDataStore.On("Exists", ctx, "FFFFFEFB--260-269.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFEF1--270-279.xdr.zstd").Return(true, nil).Once() + }, }, { name: "Data store is not beyond start ledger", @@ -121,6 +192,12 @@ func TestResumability(t *testing.T) { LedgersPerFile: uint32(10), }, networkName: "test", + registerMockCalls: func(mockDataStore *MockDataStore) { + mockDataStore.On("Exists", ctx, "FFFFFF87--120-129.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFF91--110-119.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFF9B--100-109.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFFA5--90-99.xdr.zstd").Return(false, nil).Once() + }, }, { name: "No start ledger provided", @@ -132,8 +209,9 @@ func TestResumability(t *testing.T) { FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test", - errorSnippet: "Invalid start value", + networkName: "test", + errorSnippet: "Invalid start value", + registerMockCalls: func(store *MockDataStore) {}, }, { name: "No end ledger provided, data store not beyond start", @@ -147,6 +225,16 @@ func TestResumability(t *testing.T) { }, networkName: "test2", latestLedger: uint32(2000), + registerMockCalls: func(mockDataStore *MockDataStore) { + mockDataStore.On("Exists", ctx, "FFFFF9A1--1630-1639.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFA91--1390-1399.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB13--1260-1269.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB4F--1200-1209.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB77--1160-1169.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB6D--1170-1179.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB81--1150-1159.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFFB8B--1140-1149.xdr.zstd").Return(false, nil).Once() + }, }, { name: "No end ledger provided, data store is beyond start", @@ -160,6 +248,15 @@ func TestResumability(t *testing.T) { }, networkName: "test3", latestLedger: uint32(3000), + registerMockCalls: func(mockDataStore *MockDataStore) { + mockDataStore.On("Exists", ctx, "FFFFF5B9--2630-2639.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF6A9--2390-2399.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF72B--2260-2269.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF735--2250-2259.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF73F--2240-2249.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF749--2230-2239.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF767--2200-2209.xdr.zstd").Return(true, nil).Once() + }, }, { name: "No end ledger provided, data store is beyond start and archive network latest, and partially into checkpoint frequency padding", @@ -173,6 +270,15 @@ func TestResumability(t *testing.T) { }, networkName: "test4", latestLedger: uint32(4000), + registerMockCalls: func(mockDataStore *MockDataStore) { + mockDataStore.On("Exists", ctx, "FFFFF1D1--3630-3639.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF0D7--3880-3889.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF05F--4000-4009.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF023--4060-4069.xdr.zstd").Return(true, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF005--4090-4099.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF00F--4080-4089.xdr.zstd").Return(false, nil).Once() + mockDataStore.On("Exists", ctx, "FFFFF019--4070-4079.xdr.zstd").Return(false, nil).Once() + }, }, { name: "No end ledger provided, start is beyond archive network latest and checkpoint frequency padding", @@ -184,75 +290,20 @@ func TestResumability(t *testing.T) { FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test5", - latestLedger: uint32(5000), - errorSnippet: "Invalid start value of 5129, it is greater than network's latest ledger of 5128", + networkName: "test5", + latestLedger: uint32(5000), + errorSnippet: "Invalid start value of 5129, it is greater than network's latest ledger of 5128", + registerMockCalls: func(store *MockDataStore) {}, }, } - - ctx := context.Background() - - mockDataStore := &MockDataStore{} - - //"End ledger same as start, data store has it" - mockDataStore.On("Exists", ctx, "FFFFFFFF--0-9.xdr.zstd").Return(true, nil).Once() - - //"End ledger same as start, data store does not have it" - mockDataStore.On("Exists", ctx, "FFFFFFF5--10-19.xdr.zstd").Return(false, nil).Once() - - //"binary search encounters an error during datastore retrieval", - mockDataStore.On("Exists", ctx, "FFFFFFEB--20-29.xdr.zstd").Return(false, errors.New("datastore error happened")).Once() - - //"Data store is beyond boundary aligned start ledger" - mockDataStore.On("Exists", ctx, "FFFFFFE1--30-39.xdr.zstd").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFFD7--40-49.xdr.zstd").Return(false, nil).Once() - - //"Data store is beyond non boundary aligned start ledger" - mockDataStore.On("Exists", ctx, "FFFFFFB9--70-79.xdr.zstd").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFFAF--80-89.xdr.zstd").Return(false, nil).Once() - - //"Data store is beyond start and end ledger" - mockDataStore.On("Exists", ctx, "FFFFFEFB--260-269.xdr.zstd").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFEF1--270-279.xdr.zstd").Return(true, nil).Once() - - //"Data store is not beyond start ledger" - mockDataStore.On("Exists", ctx, "FFFFFF91--110-119.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFF9B--100-109.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFFA5--90-99.xdr.zstd").Return(false, nil).Once() - - //"No end ledger provided, data store not beyond start" uses latest from network="test2" - mockDataStore.On("Exists", ctx, "FFFFF9A1--1630-1639.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFA91--1390-1399.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFB13--1260-1269.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFB4F--1200-1209.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFB77--1160-1169.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFB6D--1170-1179.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFB81--1150-1159.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFFB8B--1140-1149.xdr.zstd").Return(false, nil).Once() - - //"No end ledger provided, data store is beyond start" uses latest from network="test3" - mockDataStore.On("Exists", ctx, "FFFFF5B9--2630-2639.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF6A9--2390-2399.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF72B--2260-2269.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF735--2250-2259.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF73F--2240-2249.xdr.zstd").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF749--2230-2239.xdr.zstd").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF767--2200-2209.xdr.zstd").Return(true, nil).Once() - - //"No end ledger provided, data store is beyond start and archive network latest, and partially into checkpoint frequency padding" uses latest from network="test4" - mockDataStore.On("Exists", ctx, "FFFFF1D1--3630-3639.xdr.zstd").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF0D7--3880-3889.xdr.zstd").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF05F--4000-4009.xdr.zstd").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF023--4060-4069.xdr.zstd").Return(true, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF005--4090-4099.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF00F--4080-4089.xdr.zstd").Return(false, nil).Once() - mockDataStore.On("Exists", ctx, "FFFFF019--4070-4079.xdr.zstd").Return(false, nil).Once() - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockArchive := &historyarchive.MockArchive{} mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: tt.latestLedger}, tt.archiveError).Once() + mockDataStore := &MockDataStore{} + tt.registerMockCalls(mockDataStore) + resumableManager := NewResumableManager(mockDataStore, tt.networkName, tt.ledgerBatchConfig, mockArchive) absentLedger, ok, err := resumableManager.FindStart(ctx, tt.startLedger, tt.endLedger) if tt.errorSnippet != "" { @@ -266,8 +317,8 @@ func TestResumability(t *testing.T) { // archives are only expected to be called when end = 0 mockArchive.AssertExpectations(t) } + mockDataStore.AssertExpectations(t) }) } - mockDataStore.AssertExpectations(t) } diff --git a/support/datastore/resumeablemanager.go b/support/datastore/resumeablemanager.go index ce0bd12717..f552dcf106 100644 --- a/support/datastore/resumeablemanager.go +++ b/support/datastore/resumeablemanager.go @@ -5,6 +5,7 @@ import ( "sort" "github.com/pkg/errors" + "github.com/stellar/go/historyarchive" "github.com/stellar/go/support/log" ) @@ -77,6 +78,29 @@ func (rm resumableManagerService) FindStart(ctx context.Context, start, end uint return 0, false, errors.Errorf("Invalid start value of %v, it is greater than network's latest ledger of %v", start, networkLatest) } end = networkLatest + } else if end >= rm.ledgerBatchConfig.LedgersPerFile { + // Adjacent ranges may end up overlapping due to the clamping behavior in adjustLedgerRange() + // https://github.com/stellar/go/blob/fff01229a5af77dee170a37bf0c71b2ce8bb8474/exp/services/ledgerexporter/internal/config.go#L173-L192 + // For example, assuming 64 ledgers per file, [2, 100] and [101, 150] get adjusted to [2, 127] and [64, 191] + // If we export [64, 191] and then try to resume on [2, 127], the binary search logic will determine that + // [2, 127] is fully exported because the midpoint of [2, 127] is present. + // To fix this issue we query the end ledger and if it is present, we only do the binary search on the + // preceding sub range. This will allow resumability to work on adjacent ranges that end up overlapping + // due to adjustLedgerRange(). + // Note that if there is an overlap the size of the overlap will never be larger than the number of files + // per partition and that is why it is sufficient to only check if the end ledger is present. + exists, err := rm.dataStore.Exists(ctx, rm.ledgerBatchConfig.GetObjectKeyFromSequenceNumber(end)) + if err != nil { + return 0, false, err + } + if exists { + end -= rm.ledgerBatchConfig.LedgersPerFile + if start > end { + // data store had all ledgers for requested range, no resumability needed. + log.Infof("Resumability found no absent object keys in requested ledger range") + return 0, false, nil + } + } } rangeSize := max(int(end-start), 1) From 34b0e4cb895016ac472f3ad0896bd9bcf42047ed Mon Sep 17 00:00:00 2001 From: urvisavla Date: Mon, 3 Jun 2024 10:47:23 -0700 Subject: [PATCH 182/234] exp/services/ledgerexporter: Add metadata to exported files (#5324) * Add versioning in docker build * Remove unused docker buildargs. Improve error handling when parsing core version --------- Co-authored-by: tamirms --- exp/services/ledgerexporter/Makefile | 4 +- exp/services/ledgerexporter/docker/Dockerfile | 1 + exp/services/ledgerexporter/internal/app.go | 11 +- .../ledgerexporter/internal/config.go | 41 ++++- .../ledgerexporter/internal/config_test.go | 89 ++++++++-- .../ledgerexporter/internal/exportmanager.go | 37 ++-- .../internal/exportmanager_test.go | 32 ++-- .../internal/ledger_meta_archive.go | 37 +++- .../internal/ledger_meta_archive_test.go | 75 ++++++++ .../ledgerexporter/internal/uploader.go | 2 +- .../ledgerexporter/internal/uploader_test.go | 57 ++++++- support/datastore/datastore.go | 5 +- support/datastore/gcs_datastore.go | 23 ++- support/datastore/gcs_test.go | 160 +++++++++++++++++- support/datastore/metadata.go | 79 +++++++++ support/datastore/metadata_test.go | 89 ++++++++++ support/datastore/mocks.go | 13 +- 17 files changed, 659 insertions(+), 96 deletions(-) create mode 100644 exp/services/ledgerexporter/internal/ledger_meta_archive_test.go create mode 100644 support/datastore/metadata.go create mode 100644 support/datastore/metadata_test.go diff --git a/exp/services/ledgerexporter/Makefile b/exp/services/ledgerexporter/Makefile index 5fee16f9d1..10bf16e9dd 100644 --- a/exp/services/ledgerexporter/Makefile +++ b/exp/services/ledgerexporter/Makefile @@ -2,13 +2,13 @@ SUDO := $(shell docker version >/dev/null 2>&1 || echo "sudo") # https://github.com/opencontainers/image-spec/blob/master/annotations.md BUILD_DATE := $(shell date -u +%FT%TZ) -VERSION ?= $(shell git rev-parse --short HEAD) +VERSION ?= 1.0.0-$(shell git rev-parse --short HEAD) DOCKER_IMAGE := stellar/ledger-exporter docker-build: cd ../../../ && \ $(SUDO) docker build --platform linux/amd64 --pull --label org.opencontainers.image.created="$(BUILD_DATE)" \ - --build-arg VERSION=$(VERSION) \ + --build-arg GOFLAGS="-ldflags=-X=github.com/stellar/go/exp/services/ledgerexporter/internal.version=$(VERSION)" \ $(if $(STELLAR_CORE_VERSION), --build-arg STELLAR_CORE_VERSION=$(STELLAR_CORE_VERSION)) \ -f exp/services/ledgerexporter/docker/Dockerfile \ -t $(DOCKER_IMAGE):$(VERSION) \ diff --git a/exp/services/ledgerexporter/docker/Dockerfile b/exp/services/ledgerexporter/docker/Dockerfile index a862306718..59e57030f3 100644 --- a/exp/services/ledgerexporter/docker/Dockerfile +++ b/exp/services/ledgerexporter/docker/Dockerfile @@ -9,6 +9,7 @@ RUN go mod download COPY . ./ +ARG GOFLAGS RUN go install github.com/stellar/go/exp/services/ledgerexporter FROM ubuntu:22.04 diff --git a/exp/services/ledgerexporter/internal/app.go b/exp/services/ledgerexporter/internal/app.go index e6dde872ce..c6adcd41a0 100644 --- a/exp/services/ledgerexporter/internal/app.go +++ b/exp/services/ledgerexporter/internal/app.go @@ -37,7 +37,8 @@ const ( ) var ( - logger = log.New().WithField("service", "ledger-exporter") + logger = log.New().WithField("service", "ledger-exporter") + version = "develop" ) func NewDataAlreadyExportedError(Start uint32, End uint32) *DataAlreadyExportedError { @@ -95,13 +96,15 @@ func (a *App) init(ctx context.Context) error { var err error var archive historyarchive.ArchiveInterface + logger.Infof("Starting Ledger Exporter with version %s", version) + registry := prometheus.NewRegistry() registry.MustRegister( collectors.NewProcessCollector(collectors.ProcessCollectorOpts{Namespace: "ledger_exporter"}), collectors.NewGoCollector(), ) - if a.config, err = NewConfig(ctx, a.flags); err != nil { + if a.config, err = NewConfig(a.flags); err != nil { return errors.Wrap(err, "Could not load configuration") } if archive, err = datastore.CreateHistoryArchiveFromNetworkName(ctx, a.config.Network); err != nil { @@ -126,7 +129,7 @@ func (a *App) init(ctx context.Context) error { } queue := NewUploadQueue(uploadQueueCapacity, registry) - if a.exportManager, err = NewExportManager(a.config.LedgerBatchConfig, a.ledgerBackend, queue, registry); err != nil { + if a.exportManager, err = NewExportManager(a.config, a.ledgerBackend, queue, registry); err != nil { return err } a.uploader = NewUploader(a.dataStore, queue, registry) @@ -256,7 +259,7 @@ func (a *App) Run() { // newLedgerBackend Creates and initializes captive core ledger backend // Currently, only supports captive-core as ledger backend func newLedgerBackend(config *Config, prometheusRegistry *prometheus.Registry) (ledgerbackend.LedgerBackend, error) { - captiveConfig, err := config.GenerateCaptiveCoreConfig() + captiveConfig, err := config.generateCaptiveCoreConfig() if err != nil { return nil, err } diff --git a/exp/services/ledgerexporter/internal/config.go b/exp/services/ledgerexporter/internal/config.go index bab184bdc7..ef062867f4 100644 --- a/exp/services/ledgerexporter/internal/config.go +++ b/exp/services/ledgerexporter/internal/config.go @@ -3,7 +3,9 @@ package ledgerexporter import ( "context" _ "embed" + "fmt" "os/exec" + "strings" "github.com/stellar/go/historyarchive" "github.com/stellar/go/ingest/ledgerbackend" @@ -45,15 +47,16 @@ type Config struct { StartLedger uint32 EndLedger uint32 Resume bool + + CoreVersion string } // This will generate the config based on commandline flags and toml // -// ctx - the caller context // flags - command line flags // // return - *Config or an error if any range validation failed. -func NewConfig(ctx context.Context, flags Flags) (*Config, error) { +func NewConfig(flags Flags) (*Config, error) { config := &Config{} config.StartLedger = uint32(flags.StartLedger) @@ -100,17 +103,21 @@ func (config *Config) ValidateAndSetLedgerRange(ctx context.Context, archive his return nil } -func (config *Config) GenerateCaptiveCoreConfig() (ledgerbackend.CaptiveCoreConfig, error) { +func (config *Config) generateCaptiveCoreConfig() (ledgerbackend.CaptiveCoreConfig, error) { coreConfig := &config.StellarCoreConfig // Look for stellar-core binary in $PATH, if not supplied - if coreConfig.StellarCoreBinaryPath == "" { + if config.StellarCoreConfig.StellarCoreBinaryPath == "" { var err error - if coreConfig.StellarCoreBinaryPath, err = exec.LookPath("stellar-core"); err != nil { + if config.StellarCoreConfig.StellarCoreBinaryPath, err = exec.LookPath("stellar-core"); err != nil { return ledgerbackend.CaptiveCoreConfig{}, errors.Wrap(err, "Failed to find stellar-core binary") } } + if err := config.setCoreVersionInfo(); err != nil { + return ledgerbackend.CaptiveCoreConfig{}, fmt.Errorf("failed to set stellar-core version info: %w", err) + } + var captiveCoreConfig []byte // Default network config switch config.Network { @@ -151,6 +158,30 @@ func (config *Config) GenerateCaptiveCoreConfig() (ledgerbackend.CaptiveCoreConf }, nil } +// By default, it points to exec.Command, overridden for testing purpose +var execCommand = exec.Command + +// Executes the "stellar-core version" command and parses its output to extract +// the core version +// The output of the "version" command is expected to be a multi-line string where the +// first line is the core version in format "vX.Y.Z-*". +func (c *Config) setCoreVersionInfo() (err error) { + versionCmd := execCommand(c.StellarCoreConfig.StellarCoreBinaryPath, "version") + versionOutput, err := versionCmd.Output() + if err != nil { + return fmt.Errorf("failed to execute stellar-core version command: %w", err) + } + + // Split the output into lines + rows := strings.Split(string(versionOutput), "\n") + if len(rows) == 0 || len(rows[0]) == 0 { + return fmt.Errorf("stellar-core version not found") + } + c.CoreVersion = rows[0] + logger.Infof("stellar-core version: %s", c.CoreVersion) + return nil +} + func (config *Config) processToml(tomlPath string) error { // Load config TOML file cfg, err := toml.LoadFile(tomlPath) diff --git a/exp/services/ledgerexporter/internal/config_test.go b/exp/services/ledgerexporter/internal/config_test.go index 133165072f..a4ce76cfe5 100644 --- a/exp/services/ledgerexporter/internal/config_test.go +++ b/exp/services/ledgerexporter/internal/config_test.go @@ -3,11 +3,14 @@ package ledgerexporter import ( "context" "fmt" + "os" + "os/exec" "testing" "github.com/stretchr/testify/require" "github.com/stellar/go/historyarchive" + "github.com/stellar/go/support/errors" ) func TestNewConfigResumeEnabled(t *testing.T) { @@ -16,8 +19,7 @@ func TestNewConfigResumeEnabled(t *testing.T) { mockArchive := &historyarchive.MockArchive{} mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: 5}, nil).Once() - config, err := NewConfig(ctx, - Flags{StartLedger: 1, EndLedger: 2, ConfigFilePath: "test/test.toml", Resume: true}) + config, err := NewConfig(Flags{StartLedger: 1, EndLedger: 2, ConfigFilePath: "test/test.toml", Resume: true}) config.ValidateAndSetLedgerRange(ctx, mockArchive) require.NoError(t, err) require.Equal(t, config.DataStoreConfig.Type, "ABC") @@ -30,23 +32,19 @@ func TestNewConfigResumeEnabled(t *testing.T) { } func TestNewConfigResumeDisabled(t *testing.T) { - ctx := context.Background() mockArchive := &historyarchive.MockArchive{} mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: 5}, nil).Once() // resume disabled by default - config, err := NewConfig(ctx, - Flags{StartLedger: 1, EndLedger: 2, ConfigFilePath: "test/test.toml"}) + config, err := NewConfig(Flags{StartLedger: 1, EndLedger: 2, ConfigFilePath: "test/test.toml"}) require.NoError(t, err) require.False(t, config.Resume) } func TestInvalidTomlConfig(t *testing.T) { - ctx := context.Background() - _, err := NewConfig(ctx, - Flags{StartLedger: 1, EndLedger: 2, ConfigFilePath: "test/no_network.toml", Resume: true}) + _, err := NewConfig(Flags{StartLedger: 1, EndLedger: 2, ConfigFilePath: "test/no_network.toml", Resume: true}) require.ErrorContains(t, err, "Invalid TOML config") } @@ -111,8 +109,7 @@ func TestValidateStartAndEndLedger(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - config, err := NewConfig(ctx, - Flags{StartLedger: tt.startLedger, EndLedger: tt.endLedger, ConfigFilePath: "test/validate_start_end.toml"}) + config, err := NewConfig(Flags{StartLedger: tt.startLedger, EndLedger: tt.endLedger, ConfigFilePath: "test/validate_start_end.toml"}) require.NoError(t, err) err = config.ValidateAndSetLedgerRange(ctx, mockArchive) if tt.errMsg != "" { @@ -189,8 +186,7 @@ func TestAdjustedLedgerRangeBoundedMode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - config, err := NewConfig(ctx, - Flags{StartLedger: tt.start, EndLedger: tt.end, ConfigFilePath: tt.configFile}) + config, err := NewConfig(Flags{StartLedger: tt.start, EndLedger: tt.end, ConfigFilePath: tt.configFile}) require.NoError(t, err) err = config.ValidateAndSetLedgerRange(ctx, mockArchive) require.NoError(t, err) @@ -258,8 +254,7 @@ func TestAdjustedLedgerRangeUnBoundedMode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - config, err := NewConfig(ctx, - Flags{StartLedger: tt.start, EndLedger: tt.end, ConfigFilePath: tt.configFile}) + config, err := NewConfig(Flags{StartLedger: tt.start, EndLedger: tt.end, ConfigFilePath: tt.configFile}) require.NoError(t, err) err = config.ValidateAndSetLedgerRange(ctx, mockArchive) require.NoError(t, err) @@ -268,3 +263,69 @@ func TestAdjustedLedgerRangeUnBoundedMode(t *testing.T) { }) } } + +var cmdOut = "" + +func fakeExecCommand(command string, args ...string) *exec.Cmd { + cs := append([]string{"-test.run=TestExecCmdHelperProcess", "--", command}, args...) + cmd := exec.Command(os.Args[0], cs...) + cmd.Env = append(os.Environ(), "GO_EXEC_CMD_HELPER_PROCESS=1", "CMD_OUT="+cmdOut) + return cmd +} + +func TestExecCmdHelperProcess(t *testing.T) { + if os.Getenv("GO_EXEC_CMD_HELPER_PROCESS") != "1" { + return + } + fmt.Fprintf(os.Stdout, os.Getenv("CMD_OUT")) + os.Exit(0) +} + +func TestSetCoreVersionInfo(t *testing.T) { + tests := []struct { + name string + commandOutput string + expectedError error + expectedCoreVer string + }{ + { + name: "version found", + commandOutput: "v20.2.0-2-g6e73c0a88\n" + + "rust version: rustc 1.74.1 (a28077b28 2023-12-04)\n" + + "soroban-env-host: \n" + + " curr:\n" + + " package version: 20.2.0\n" + + " git version: 1bfc0f2a2ee134efc1e1b0d5270281d0cba61c2e\n" + + " ledger protocol version: 20\n" + + " pre-release version: 0\n" + + " rs-stellar-xdr:\n" + + " package version: 20.1.0\n" + + " git version: 8b9d623ef40423a8462442b86997155f2c04d3a1\n" + + " base XDR git version: b96148cd4acc372cc9af17b909ffe4b12c43ecb6\n", + expectedError: nil, + expectedCoreVer: "v20.2.0-2-g6e73c0a88", + }, + { + name: "core version not found", + commandOutput: "", + expectedError: errors.New("stellar-core version not found"), + expectedCoreVer: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config := Config{} + + cmdOut = tt.commandOutput + execCommand = fakeExecCommand + err := config.setCoreVersionInfo() + + if tt.expectedError != nil { + require.EqualError(t, err, tt.expectedError.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tt.expectedCoreVer, config.CoreVersion) + } + }) + } +} diff --git a/exp/services/ledgerexporter/internal/exportmanager.go b/exp/services/ledgerexporter/internal/exportmanager.go index 55f85b9c46..ae85c072d6 100644 --- a/exp/services/ledgerexporter/internal/exportmanager.go +++ b/exp/services/ledgerexporter/internal/exportmanager.go @@ -8,22 +8,21 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/stellar/go/ingest/ledgerbackend" - "github.com/stellar/go/support/datastore" "github.com/stellar/go/xdr" ) type ExportManager struct { - config datastore.LedgerBatchConfig + config *Config ledgerBackend ledgerbackend.LedgerBackend - currentMetaArchive *LedgerMetaArchive + currentMetaArchive *xdr.LedgerCloseMetaBatch queue UploadQueue latestLedgerMetric *prometheus.GaugeVec } // NewExportManager creates a new ExportManager with the provided configuration. -func NewExportManager(config datastore.LedgerBatchConfig, backend ledgerbackend.LedgerBackend, queue UploadQueue, prometheusRegistry *prometheus.Registry) (*ExportManager, error) { - if config.LedgersPerFile < 1 { - return nil, errors.Errorf("Invalid ledgers per file (%d): must be at least 1", config.LedgersPerFile) +func NewExportManager(config *Config, backend ledgerbackend.LedgerBackend, queue UploadQueue, prometheusRegistry *prometheus.Registry) (*ExportManager, error) { + if config.LedgerBatchConfig.LedgersPerFile < 1 { + return nil, errors.Errorf("Invalid ledgers per file (%d): must be at least 1", config.LedgerBatchConfig.LedgersPerFile) } latestLedgerMetric := prometheus.NewGaugeVec(prometheus.GaugeOpts{ @@ -45,32 +44,32 @@ func (e *ExportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta ledgerSeq := ledgerCloseMeta.LedgerSequence() // Determine the object key for the given ledger sequence - objectKey := e.config.GetObjectKeyFromSequenceNumber(ledgerSeq) + objectKey := e.config.LedgerBatchConfig.GetObjectKeyFromSequenceNumber(ledgerSeq) - if e.currentMetaArchive != nil && e.currentMetaArchive.ObjectKey != objectKey { - return errors.New("Current meta archive object key mismatch") - } if e.currentMetaArchive == nil { - endSeq := ledgerSeq + e.config.LedgersPerFile - 1 - if ledgerSeq < e.config.LedgersPerFile { + endSeq := ledgerSeq + e.config.LedgerBatchConfig.LedgersPerFile - 1 + if ledgerSeq < e.config.LedgerBatchConfig.LedgersPerFile { // Special case: Adjust the end ledger sequence for the first batch. // Since the start ledger is 2 instead of 0, we want to ensure that the end ledger sequence // does not exceed LedgersPerFile. // For example, if LedgersPerFile is 64, the file name for the first batch should be 0-63, not 2-66. - endSeq = e.config.LedgersPerFile - 1 + endSeq = e.config.LedgerBatchConfig.LedgersPerFile - 1 } - // Create a new LedgerMetaArchive and add it to the map. - e.currentMetaArchive = NewLedgerMetaArchive(objectKey, ledgerSeq, endSeq) + // Create a new LedgerCloseMetaBatch + e.currentMetaArchive = &xdr.LedgerCloseMetaBatch{StartSequence: xdr.Uint32(ledgerSeq), EndSequence: xdr.Uint32(endSeq)} } - if err := e.currentMetaArchive.Data.AddLedger(ledgerCloseMeta); err != nil { + if err := e.currentMetaArchive.AddLedger(ledgerCloseMeta); err != nil { return errors.Wrapf(err, "failed to add ledger %d", ledgerSeq) } - if ledgerSeq >= uint32(e.currentMetaArchive.Data.EndSequence) { - // Current archive is full, send it for upload - if err := e.queue.Enqueue(ctx, e.currentMetaArchive); err != nil { + if ledgerSeq >= uint32(e.currentMetaArchive.EndSequence) { + ledgerMetaArchive, err := NewLedgerMetaArchiveFromXDR(e.config, objectKey, *e.currentMetaArchive) + if err != nil { + return err + } + if err := e.queue.Enqueue(ctx, ledgerMetaArchive); err != nil { return err } e.currentMetaArchive = nil diff --git a/exp/services/ledgerexporter/internal/exportmanager_test.go b/exp/services/ledgerexporter/internal/exportmanager_test.go index d99f88cd14..b74af2e19e 100644 --- a/exp/services/ledgerexporter/internal/exportmanager_test.go +++ b/exp/services/ledgerexporter/internal/exportmanager_test.go @@ -23,7 +23,9 @@ func createLedgerCloseMeta(ledgerSeq uint32) xdr.LedgerCloseMeta { V0: &xdr.LedgerCloseMetaV0{ LedgerHeader: xdr.LedgerHeaderHistoryEntry{ Header: xdr.LedgerHeader{ - LedgerSeq: xdr.Uint32(ledgerSeq), + LedgerVersion: 21, + LedgerSeq: xdr.Uint32(ledgerSeq), + ScpValue: xdr.StellarValue{CloseTime: xdr.TimePoint(ledgerSeq * 100)}, }, }, TxSet: xdr.TransactionSet{}, @@ -56,7 +58,7 @@ func (s *ExportManagerSuite) TearDownTest() { } func (s *ExportManagerSuite) TestInvalidExportConfig() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 0, FilesPerPartition: 10} + config := &Config{LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 0, FilesPerPartition: 10}} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) _, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -64,7 +66,7 @@ func (s *ExportManagerSuite) TestInvalidExportConfig() { } func (s *ExportManagerSuite) TestRun() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 64, FilesPerPartition: 10} + config := &Config{LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 64, FilesPerPartition: 10}} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -77,7 +79,7 @@ func (s *ExportManagerSuite) TestRun() { for i := start; i <= end; i++ { s.mockBackend.On("GetLedger", s.ctx, i). Return(createLedgerCloseMeta(i), nil) - key := config.GetObjectKeyFromSequenceNumber(i) + key := config.LedgerBatchConfig.GetObjectKeyFromSequenceNumber(i) expectedKeys.Add(key) } @@ -114,7 +116,7 @@ func (s *ExportManagerSuite) TestRun() { } func (s *ExportManagerSuite) TestRunContextCancel() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 1} + config := &Config{LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 1}} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -144,7 +146,7 @@ func (s *ExportManagerSuite) TestRunContextCancel() { } func (s *ExportManagerSuite) TestRunWithCanceledContext() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} + config := &Config{LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10}} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -163,7 +165,7 @@ func (s *ExportManagerSuite) TestRunWithCanceledContext() { } func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} + config := &Config{LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10}} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -191,7 +193,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { for i := start; i <= end; i++ { s.Require().NoError(exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(i))) - key := config.GetObjectKeyFromSequenceNumber(i) + key := config.LedgerBatchConfig.GetObjectKeyFromSequenceNumber(i) expectedKeys.Add(key) } @@ -201,7 +203,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { } func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10} + config := &Config{LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10}} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) @@ -217,15 +219,3 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { err = exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(2)) s.Require().EqualError(err, "context canceled") } - -func (s *ExportManagerSuite) TestAddLedgerCloseMetaKeyMismatch() { - config := datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 1} - registry := prometheus.NewRegistry() - queue := NewUploadQueue(1, registry) - exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) - s.Require().NoError(err) - - s.Require().NoError(exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(16))) - s.Require().EqualError(exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(21)), - "Current meta archive object key mismatch") -} diff --git a/exp/services/ledgerexporter/internal/ledger_meta_archive.go b/exp/services/ledgerexporter/internal/ledger_meta_archive.go index 5215e2353e..583f6a3ec4 100644 --- a/exp/services/ledgerexporter/internal/ledger_meta_archive.go +++ b/exp/services/ledgerexporter/internal/ledger_meta_archive.go @@ -1,24 +1,43 @@ package ledgerexporter import ( + "github.com/stellar/go/support/compressxdr" + "github.com/stellar/go/support/datastore" "github.com/stellar/go/xdr" ) // LedgerMetaArchive represents a file with metadata and binary data. type LedgerMetaArchive struct { - // file name ObjectKey string - // Actual binary data - Data xdr.LedgerCloseMetaBatch + Data xdr.LedgerCloseMetaBatch + metaData datastore.MetaData } -// NewLedgerMetaArchive creates a new LedgerMetaArchive instance. -func NewLedgerMetaArchive(key string, startSeq uint32, endSeq uint32) *LedgerMetaArchive { +// NewLedgerMetaArchiveFromXDR creates a new LedgerMetaArchive instance. +func NewLedgerMetaArchiveFromXDR(config *Config, key string, data xdr.LedgerCloseMetaBatch) (*LedgerMetaArchive, error) { + startLedger, err := data.GetLedger(uint32(data.StartSequence)) + if err != nil { + return &LedgerMetaArchive{}, err + + } + endLedger, err := data.GetLedger(uint32(data.EndSequence)) + if err != nil { + return &LedgerMetaArchive{}, err + } + return &LedgerMetaArchive{ ObjectKey: key, - Data: xdr.LedgerCloseMetaBatch{ - StartSequence: xdr.Uint32(startSeq), - EndSequence: xdr.Uint32(endSeq), + Data: data, + metaData: datastore.MetaData{ + StartLedger: startLedger.LedgerSequence(), + EndLedger: endLedger.LedgerSequence(), + StartLedgerCloseTime: startLedger.LedgerCloseTime(), + EndLedgerCloseTime: endLedger.LedgerCloseTime(), + Network: config.Network, + CompressionType: compressxdr.DefaultCompressor.Name(), + ProtocolVersion: endLedger.ProtocolVersion(), + CoreVersion: config.CoreVersion, + Version: version, }, - } + }, nil } diff --git a/exp/services/ledgerexporter/internal/ledger_meta_archive_test.go b/exp/services/ledgerexporter/internal/ledger_meta_archive_test.go new file mode 100644 index 0000000000..3cf263d71b --- /dev/null +++ b/exp/services/ledgerexporter/internal/ledger_meta_archive_test.go @@ -0,0 +1,75 @@ +package ledgerexporter + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/stellar/go/support/datastore" + "github.com/stellar/go/xdr" +) + +func TestNewLedgerMetaArchiveFromXDR(t *testing.T) { + config := &Config{ + Network: "testnet", + CoreVersion: "v1.2.3", + } + data := xdr.LedgerCloseMetaBatch{ + StartSequence: 1234, + EndSequence: 1234, + LedgerCloseMetas: []xdr.LedgerCloseMeta{ + createLedgerCloseMeta(1234), + }, + } + + archive, err := NewLedgerMetaArchiveFromXDR(config, "key", data) + + require.NoError(t, err) + require.NotNil(t, archive) + + // Check if the metadata fields are correctly populated + expectedMetaData := datastore.MetaData{ + StartLedger: 1234, + EndLedger: 1234, + StartLedgerCloseTime: 1234 * 100, + EndLedgerCloseTime: 1234 * 100, + Network: "testnet", + CompressionType: "zstd", + ProtocolVersion: 21, + CoreVersion: "v1.2.3", + Version: "develop", + } + + require.Equal(t, expectedMetaData, archive.metaData) + + data = xdr.LedgerCloseMetaBatch{ + StartSequence: 1234, + EndSequence: 1237, + LedgerCloseMetas: []xdr.LedgerCloseMeta{ + createLedgerCloseMeta(1234), + createLedgerCloseMeta(1235), + createLedgerCloseMeta(1236), + createLedgerCloseMeta(1237), + }, + } + + archive, err = NewLedgerMetaArchiveFromXDR(config, "key", data) + + require.NoError(t, err) + require.NotNil(t, archive) + + // Check if the metadata fields are correctly populated + expectedMetaData = datastore.MetaData{ + StartLedger: 1234, + EndLedger: 1237, + StartLedgerCloseTime: 1234 * 100, + EndLedgerCloseTime: 1237 * 100, + Network: "testnet", + CompressionType: "zstd", + ProtocolVersion: 21, + CoreVersion: "v1.2.3", + Version: "develop", + } + + require.Equal(t, expectedMetaData, archive.metaData) +} diff --git a/exp/services/ledgerexporter/internal/uploader.go b/exp/services/ledgerexporter/internal/uploader.go index 78296cde88..6d35d2920f 100644 --- a/exp/services/ledgerexporter/internal/uploader.go +++ b/exp/services/ledgerexporter/internal/uploader.go @@ -95,7 +95,7 @@ func (u Uploader) Upload(ctx context.Context, metaArchive *LedgerMetaArchive) er writerTo := &writerToRecorder{ WriterTo: xdrEncoder, } - ok, err := u.dataStore.PutFileIfNotExists(ctx, metaArchive.ObjectKey, writerTo) + ok, err := u.dataStore.PutFileIfNotExists(ctx, metaArchive.ObjectKey, writerTo, metaArchive.metaData.ToMap()) if err != nil { return errors.Wrapf(err, "error uploading %s", metaArchive.ObjectKey) } diff --git a/exp/services/ledgerexporter/internal/uploader_test.go b/exp/services/ledgerexporter/internal/uploader_test.go index 5fbf5e22d0..bb3ca1e29a 100644 --- a/exp/services/ledgerexporter/internal/uploader_test.go +++ b/exp/services/ledgerexporter/internal/uploader_test.go @@ -16,6 +16,7 @@ import ( "github.com/stellar/go/support/compressxdr" "github.com/stellar/go/support/datastore" "github.com/stellar/go/support/errors" + "github.com/stellar/go/xdr" ) var testShutdownDelayTime = 300 * time.Millisecond @@ -45,6 +46,39 @@ func (s *UploaderSuite) TestUpload() { s.testUpload(true) } +func (s *UploaderSuite) TestUploadWithMetadata() { + key, start, end := "test-1-100", uint32(1), uint32(100) + archive := NewLedgerMetaArchive(key, start, end) + for i := start; i <= end; i++ { + _ = archive.Data.AddLedger(createLedgerCloseMeta(i)) + } + metadata := datastore.MetaData{ + StartLedger: start, + EndLedger: end, + StartLedgerCloseTime: 123456789, + EndLedgerCloseTime: 987654321, + ProtocolVersion: 3, + CoreVersion: "v1.2.3", + Network: "testnet", + CompressionType: "gzip", + Version: "1.0.0", + } + archive.metaData = metadata + var capturedBuf bytes.Buffer + s.mockDataStore.On("PutFileIfNotExists", mock.Anything, key, mock.Anything, metadata.ToMap()). + Run(func(args mock.Arguments) { + _ = args.Get(1).(string) + _, err := args.Get(2).(io.WriterTo).WriteTo(&capturedBuf) + s.Require().NoError(err) + }).Return(true, nil).Once() + + registry := prometheus.NewRegistry() + queue := NewUploadQueue(1, registry) + dataUploader := NewUploader(&s.mockDataStore, queue, registry) + s.Require().NoError(dataUploader.Upload(context.Background(), archive)) + +} + func (s *UploaderSuite) testUpload(putOkReturnVal bool) { key, start, end := "test-1-100", uint32(1), uint32(100) archive := NewLedgerMetaArchive(key, start, end) @@ -54,7 +88,7 @@ func (s *UploaderSuite) testUpload(putOkReturnVal bool) { var capturedBuf bytes.Buffer var capturedKey string - s.mockDataStore.On("PutFileIfNotExists", mock.Anything, key, mock.Anything). + s.mockDataStore.On("PutFileIfNotExists", mock.Anything, key, mock.Anything, datastore.MetaData{}.ToMap()). Run(func(args mock.Arguments) { capturedKey = args.Get(1).(string) _, err := args.Get(2).(io.WriterTo).WriteTo(&capturedBuf) @@ -167,7 +201,7 @@ func (s *UploaderSuite) testUploadPutError(putOkReturnVal bool) { archive := NewLedgerMetaArchive(key, start, end) s.mockDataStore.On("PutFileIfNotExists", context.Background(), key, - mock.Anything).Return(putOkReturnVal, errors.New("error in PutFileIfNotExists")).Once() + mock.Anything, datastore.MetaData{}.ToMap()).Return(putOkReturnVal, errors.New("error in PutFileIfNotExists")).Once() registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) @@ -211,7 +245,7 @@ func (s *UploaderSuite) TestRunUntilQueueClose() { for i := 1; i <= 100; i++ { key := fmt.Sprintf("test-%d", i) cur := s.mockDataStore.On("PutFileIfNotExists", mock.Anything, - key, mock.Anything).Return(true, nil).Once() + key, mock.Anything, mock.Anything).Return(true, nil).Once() if prev != nil { cur.NotBefore(prev) } @@ -242,11 +276,11 @@ func (s *UploaderSuite) TestRunContextCancel() { registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - first := s.mockDataStore.On("PutFileIfNotExists", mock.Anything, "test", mock.Anything). + first := s.mockDataStore.On("PutFileIfNotExists", mock.Anything, "test", mock.Anything, datastore.MetaData{}.ToMap()). Return(true, nil).Once().Run(func(args mock.Arguments) { cancel() }) - s.mockDataStore.On("PutFileIfNotExists", mock.Anything, "test1", mock.Anything). + s.mockDataStore.On("PutFileIfNotExists", mock.Anything, "test1", mock.Anything, datastore.MetaData{}.ToMap()). Return(true, nil).Once().NotBefore(first).Run(func(args mock.Arguments) { ctxArg := args.Get(0).(context.Context) s.Require().NoError(ctxArg.Err()) @@ -273,9 +307,20 @@ func (s *UploaderSuite) TestRunUploadError() { s.Require().NoError(queue.Enqueue(s.ctx, NewLedgerMetaArchive("test1", 2, 2))) s.mockDataStore.On("PutFileIfNotExists", mock.Anything, "test", - mock.Anything).Return(false, errors.New("Put error")).Once() + mock.Anything, mock.Anything).Return(false, errors.New("Put error")).Once() dataUploader := NewUploader(&s.mockDataStore, queue, registry) err := dataUploader.Run(context.Background(), testShutdownDelayTime) s.Require().Equal("error uploading test: Put error", err.Error()) } + +func NewLedgerMetaArchive(key string, startSeq uint32, endSeq uint32) *LedgerMetaArchive { + return &LedgerMetaArchive{ + ObjectKey: key, + Data: xdr.LedgerCloseMetaBatch{ + StartSequence: xdr.Uint32(startSeq), + EndSequence: xdr.Uint32(endSeq), + }, + metaData: datastore.MetaData{}, + } +} diff --git a/support/datastore/datastore.go b/support/datastore/datastore.go index 5592edf6fb..0a92857b5c 100644 --- a/support/datastore/datastore.go +++ b/support/datastore/datastore.go @@ -14,9 +14,10 @@ type DataStoreConfig struct { // DataStore defines an interface for interacting with data storage type DataStore interface { + GetFileMetadata(ctx context.Context, path string) (map[string]string, error) GetFile(ctx context.Context, path string) (io.ReadCloser, error) - PutFile(ctx context.Context, path string, in io.WriterTo) error - PutFileIfNotExists(ctx context.Context, path string, in io.WriterTo) (bool, error) + PutFile(ctx context.Context, path string, in io.WriterTo, metaData map[string]string) error + PutFileIfNotExists(ctx context.Context, path string, in io.WriterTo, metaData map[string]string) (bool, error) Exists(ctx context.Context, path string) (bool, error) Size(ctx context.Context, path string) (int64, error) Close() error diff --git a/support/datastore/gcs_datastore.go b/support/datastore/gcs_datastore.go index aaf7a55ac4..c6b22b0fca 100644 --- a/support/datastore/gcs_datastore.go +++ b/support/datastore/gcs_datastore.go @@ -58,6 +58,18 @@ func FromGCSClient(ctx context.Context, client *storage.Client, bucketPath strin return &GCSDataStore{client: client, bucket: bucket, prefix: prefix}, nil } +// GetFileMetadata retrieves the metadata for the specified file in the GCS bucket. +func (b GCSDataStore) GetFileMetadata(ctx context.Context, filePath string) (map[string]string, error) { + filePath = path.Join(b.prefix, filePath) + attrs, err := b.bucket.Object(filePath).Attrs(ctx) + if err != nil { + if errors.Is(err, storage.ErrObjectNotExist) { + return nil, os.ErrNotExist + } + } + return attrs.Metadata, nil +} + // GetFile retrieves a file from the GCS bucket. func (b GCSDataStore) GetFile(ctx context.Context, filePath string) (io.ReadCloser, error) { filePath = path.Join(b.prefix, filePath) @@ -82,8 +94,8 @@ func (b GCSDataStore) GetFile(ctx context.Context, filePath string) (io.ReadClos } // PutFileIfNotExists uploads a file to GCS only if it doesn't already exist. -func (b GCSDataStore) PutFileIfNotExists(ctx context.Context, filePath string, in io.WriterTo) (bool, error) { - err := b.putFile(ctx, filePath, in, &storage.Conditions{DoesNotExist: true}) +func (b GCSDataStore) PutFileIfNotExists(ctx context.Context, filePath string, in io.WriterTo, metaData map[string]string) (bool, error) { + err := b.putFile(ctx, filePath, in, &storage.Conditions{DoesNotExist: true}, metaData) if err != nil { if gcsError, ok := err.(*googleapi.Error); ok { switch gcsError.Code { @@ -101,8 +113,8 @@ func (b GCSDataStore) PutFileIfNotExists(ctx context.Context, filePath string, i } // PutFile uploads a file to GCS -func (b GCSDataStore) PutFile(ctx context.Context, filePath string, in io.WriterTo) error { - err := b.putFile(ctx, filePath, in, nil) // No conditions for regular PutFile +func (b GCSDataStore) PutFile(ctx context.Context, filePath string, in io.WriterTo, metaData map[string]string) error { + err := b.putFile(ctx, filePath, in, nil, metaData) // No conditions for regular PutFile if err != nil { if gcsError, ok := err.(*googleapi.Error); ok { @@ -143,7 +155,7 @@ func (b GCSDataStore) Close() error { return b.client.Close() } -func (b GCSDataStore) putFile(ctx context.Context, filePath string, in io.WriterTo, conditions *storage.Conditions) error { +func (b GCSDataStore) putFile(ctx context.Context, filePath string, in io.WriterTo, conditions *storage.Conditions, metaData map[string]string) error { filePath = path.Join(b.prefix, filePath) o := b.bucket.Object(filePath) if conditions != nil { @@ -155,6 +167,7 @@ func (b GCSDataStore) putFile(ctx context.Context, filePath string, in io.Writer } w := o.NewWriter(ctx) + w.Metadata = metaData w.SendCRC32C = true // we must set CRC32C before invoking w.Write() for the first time w.CRC32C = crc32.Checksum(buf.Bytes(), crc32.MakeTable(crc32.Castagnoli)) diff --git a/support/datastore/gcs_test.go b/support/datastore/gcs_test.go index 9fe23bf2c8..0ddf60ac99 100644 --- a/support/datastore/gcs_test.go +++ b/support/datastore/gcs_test.go @@ -96,7 +96,7 @@ func TestGCSPutFile(t *testing.T) { writerTo := &writerToRecorder{ WriterTo: bytes.NewReader(content), } - err = store.PutFile(context.Background(), "file.txt", writerTo) + err = store.PutFile(context.Background(), "file.txt", writerTo, nil) require.NoError(t, err) require.Equal(t, int64(len(content)), writerTo.total) @@ -104,17 +104,25 @@ func TestGCSPutFile(t *testing.T) { require.NoError(t, err) requireReaderContentEquals(t, reader, content) + metadata, err := store.GetFileMetadata(context.Background(), "file.txt") + require.NoError(t, err) + require.Equal(t, map[string]string(nil), metadata) + otherContent := []byte("other text") writerTo = &writerToRecorder{ WriterTo: bytes.NewReader(otherContent), } - err = store.PutFile(context.Background(), "file.txt", writerTo) + err = store.PutFile(context.Background(), "file.txt", writerTo, nil) require.NoError(t, err) require.Equal(t, int64(len(otherContent)), writerTo.total) reader, err = store.GetFile(context.Background(), "file.txt") require.NoError(t, err) requireReaderContentEquals(t, reader, otherContent) + + metadata, err = store.GetFileMetadata(context.Background(), "file.txt") + require.NoError(t, err) + require.Equal(t, map[string]string(nil), metadata) } func TestGCSPutFileIfNotExists(t *testing.T) { @@ -140,7 +148,7 @@ func TestGCSPutFileIfNotExists(t *testing.T) { writerTo := &writerToRecorder{ WriterTo: bytes.NewReader(newContent), } - ok, err := store.PutFileIfNotExists(context.Background(), "file.txt", writerTo) + ok, err := store.PutFileIfNotExists(context.Background(), "file.txt", writerTo, nil) require.NoError(t, err) require.False(t, ok) require.Equal(t, int64(len(newContent)), writerTo.total) @@ -149,10 +157,14 @@ func TestGCSPutFileIfNotExists(t *testing.T) { require.NoError(t, err) requireReaderContentEquals(t, reader, existingContent) + metadata, err := store.GetFileMetadata(context.Background(), "file.txt") + require.NoError(t, err) + require.Equal(t, map[string]string(nil), metadata) + writerTo = &writerToRecorder{ WriterTo: bytes.NewReader(newContent), } - ok, err = store.PutFileIfNotExists(context.Background(), "other-file.txt", writerTo) + ok, err = store.PutFileIfNotExists(context.Background(), "other-file.txt", writerTo, nil) require.NoError(t, err) require.True(t, ok) require.Equal(t, int64(len(newContent)), writerTo.total) @@ -160,6 +172,142 @@ func TestGCSPutFileIfNotExists(t *testing.T) { reader, err = store.GetFile(context.Background(), "other-file.txt") require.NoError(t, err) requireReaderContentEquals(t, reader, newContent) + + metadata, err = store.GetFileMetadata(context.Background(), "other-file.txt") + require.NoError(t, err) + require.Equal(t, map[string]string(nil), metadata) +} + +func TestGCSPutFileWithMetadata(t *testing.T) { + server := fakestorage.NewServer([]fakestorage.Object{}) + defer server.Stop() + server.CreateBucketWithOpts(fakestorage.CreateBucketOpts{ + Name: "test-bucket", + VersioningEnabled: false, + DefaultEventBasedHold: false, + }) + + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, store.Close()) + }) + + // Initial metadata + metadataObj := MetaData{StartLedger: 1234, + EndLedger: 1234, + StartLedgerCloseTime: 1234, + EndLedgerCloseTime: 1234, + Network: "testnet", + CompressionType: "zstd", + ProtocolVersion: 21, + CoreVersion: "v1.2.3", + Version: "1.0.0", + } + + content := []byte("inside the file") + writerTo := &writerToRecorder{ + WriterTo: bytes.NewReader(content), + } + err = store.PutFile(context.Background(), "file.txt", writerTo, metadataObj.ToMap()) + require.NoError(t, err) + require.Equal(t, int64(len(content)), writerTo.total) + + metadata, err := store.GetFileMetadata(context.Background(), "file.txt") + require.NoError(t, err) + require.Equal(t, metadataObj.ToMap(), metadata) + + // Update metadata + modifiedMetadataObj := MetaData{ + StartLedger: 5678, + EndLedger: 6789, + StartLedgerCloseTime: 1622547800, + EndLedgerCloseTime: 1622548900, + Network: "mainnet", + CompressionType: "gzip", + ProtocolVersion: 23, + CoreVersion: "v1.4.0", + Version: "2.0.0", + } + + otherContent := []byte("other text") + writerTo = &writerToRecorder{ + WriterTo: bytes.NewReader(otherContent), + } + err = store.PutFile(context.Background(), "file.txt", writerTo, modifiedMetadataObj.ToMap()) + require.NoError(t, err) + require.Equal(t, int64(len(otherContent)), writerTo.total) + + metadata, err = store.GetFileMetadata(context.Background(), "file.txt") + require.NoError(t, err) + require.Equal(t, modifiedMetadataObj.ToMap(), metadata) +} + +func TestGCSPutFileIfNotExistsWithMetadata(t *testing.T) { + existingContent := []byte("inside the file") + server := fakestorage.NewServer([]fakestorage.Object{ + { + ObjectAttrs: fakestorage.ObjectAttrs{ + BucketName: "test-bucket", + }, + Content: existingContent, + }, + }) + defer server.Stop() + + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, store.Close()) + }) + + metadataObj := MetaData{StartLedger: 1234, + EndLedger: 1234, + StartLedgerCloseTime: 1234, + EndLedgerCloseTime: 1234, + Network: "testnet", + CompressionType: "zstd", + ProtocolVersion: 21, + CoreVersion: "v1.2.3", + Version: "1.0.0", + } + + newContent := []byte("overwrite the file") + writerTo := &writerToRecorder{ + WriterTo: bytes.NewReader(newContent), + } + ok, err := store.PutFileIfNotExists(context.Background(), "file.txt", writerTo, metadataObj.ToMap()) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, int64(len(newContent)), writerTo.total) + + metadata, err := store.GetFileMetadata(context.Background(), "file.txt") + require.NoError(t, err) + require.Equal(t, metadataObj.ToMap(), metadata) + + modifiedMetadataObj := MetaData{ + StartLedger: 5678, + EndLedger: 6789, + StartLedgerCloseTime: 1622547800, + EndLedgerCloseTime: 1622548900, + Network: "mainnet", + CompressionType: "gzip", + ProtocolVersion: 23, + CoreVersion: "v1.4.0", + Version: "2.0.0", + } + + writerTo = &writerToRecorder{ + WriterTo: bytes.NewReader(newContent), + } + ok, err = store.PutFileIfNotExists(context.Background(), "file.txt", writerTo, modifiedMetadataObj.ToMap()) + require.NoError(t, err) + require.False(t, ok) + require.Equal(t, int64(len(newContent)), writerTo.total) + + metadata, err = store.GetFileMetadata(context.Background(), "file.txt") + require.NoError(t, err) + require.Equal(t, metadataObj.ToMap(), metadata) } func TestGCSGetNonExistentFile(t *testing.T) { @@ -183,6 +331,10 @@ func TestGCSGetNonExistentFile(t *testing.T) { _, err = store.GetFile(context.Background(), "other-file.txt") require.ErrorIs(t, err, os.ErrNotExist) + + metadata, err := store.GetFileMetadata(context.Background(), "other-file.txt") + require.ErrorIs(t, err, os.ErrNotExist) + require.Equal(t, map[string]string(nil), metadata) } func TestGCSGetFileValidatesCRC32C(t *testing.T) { diff --git a/support/datastore/metadata.go b/support/datastore/metadata.go new file mode 100644 index 0000000000..db734bbd4d --- /dev/null +++ b/support/datastore/metadata.go @@ -0,0 +1,79 @@ +package datastore + +import "strconv" + +type MetaData struct { + StartLedger uint32 + EndLedger uint32 + StartLedgerCloseTime int64 + EndLedgerCloseTime int64 + ProtocolVersion uint32 + CoreVersion string + Network string + CompressionType string + Version string +} + +func (m MetaData) ToMap() map[string]string { + return map[string]string{ + "start-ledger": strconv.FormatUint(uint64(m.StartLedger), 10), + "end-ledger": strconv.FormatUint(uint64(m.EndLedger), 10), + "start-ledger-close-time": strconv.FormatInt(m.StartLedgerCloseTime, 10), + "end-ledger-close-time": strconv.FormatInt(m.EndLedgerCloseTime, 10), + "protocol-version": strconv.FormatInt(int64(m.ProtocolVersion), 10), + "core-version": m.CoreVersion, + "network": m.Network, + "compression-type": m.CompressionType, + "version": m.Version, + } +} +func NewMetaDataFromMap(data map[string]string) (MetaData, error) { + var metaData MetaData + + if val, ok := data["start-ledger"]; ok { + startLedger, err := strconv.ParseUint(val, 10, 32) + if err != nil { + return metaData, err + } + metaData.StartLedger = uint32(startLedger) + } + + if val, ok := data["end-ledger"]; ok { + endLedger, err := strconv.ParseUint(val, 10, 32) + if err != nil { + return metaData, err + } + metaData.EndLedger = uint32(endLedger) + } + + if val, ok := data["start-ledger-close-time"]; ok { + startLedgerCloseTime, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return metaData, err + } + metaData.StartLedgerCloseTime = startLedgerCloseTime + } + + if val, ok := data["end-ledger-close-time"]; ok { + endLedgerCloseTime, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return metaData, err + } + metaData.EndLedgerCloseTime = endLedgerCloseTime + } + + if val, ok := data["protocol-version"]; ok { + protocolVersion, err := strconv.ParseInt(val, 10, 32) + if err != nil { + return metaData, err + } + metaData.ProtocolVersion = uint32(protocolVersion) + } + + metaData.CoreVersion = data["core-version"] + metaData.Network = data["network"] + metaData.CompressionType = data["compression-type"] + metaData.Version = data["version"] + + return metaData, nil +} diff --git a/support/datastore/metadata_test.go b/support/datastore/metadata_test.go new file mode 100644 index 0000000000..6ec6a176b8 --- /dev/null +++ b/support/datastore/metadata_test.go @@ -0,0 +1,89 @@ +package datastore + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMetaDataToMap(t *testing.T) { + + var tests = []struct { + name string + metaData MetaData + expected map[string]string + }{ + { + name: "testToMap", + metaData: MetaData{ + StartLedger: 100, + EndLedger: 200, + StartLedgerCloseTime: 123456789, + EndLedgerCloseTime: 987654321, + ProtocolVersion: 3, + CoreVersion: "v1.2.3", + Network: "testnet", + CompressionType: "gzip", + Version: "1.0.0", + }, + expected: map[string]string{ + "start-ledger": "100", + "end-ledger": "200", + "start-ledger-close-time": "123456789", + "end-ledger-close-time": "987654321", + "protocol-version": "3", + "core-version": "v1.2.3", + "network": "testnet", + "compression-type": "gzip", + "version": "1.0.0", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := MetaData{ + StartLedger: tt.metaData.StartLedger, + EndLedger: tt.metaData.EndLedger, + StartLedgerCloseTime: tt.metaData.StartLedgerCloseTime, + EndLedgerCloseTime: tt.metaData.EndLedgerCloseTime, + ProtocolVersion: tt.metaData.ProtocolVersion, + CoreVersion: tt.metaData.CoreVersion, + Network: tt.metaData.Network, + CompressionType: tt.metaData.CompressionType, + Version: tt.metaData.Version, + } + got := m.ToMap() + require.Equal(t, got, tt.expected) + }) + } +} + +func TestNewMetaDataFromMap(t *testing.T) { + data := map[string]string{ + "start-ledger": "100", + "end-ledger": "200", + "start-ledger-close-time": "123456789", + "end-ledger-close-time": "987654321", + "protocol-version": "3", + "core-version": "v1.2.3", + "network": "testnet", + "compression-type": "gzip", + "version": "1.0.0", + } + + expected := MetaData{ + StartLedger: 100, + EndLedger: 200, + StartLedgerCloseTime: 123456789, + EndLedgerCloseTime: 987654321, + ProtocolVersion: 3, + CoreVersion: "v1.2.3", + Network: "testnet", + CompressionType: "gzip", + Version: "1.0.0", + } + + got, err := NewMetaDataFromMap(data) + require.NoError(t, err) + require.Equal(t, got, expected) +} diff --git a/support/datastore/mocks.go b/support/datastore/mocks.go index b77040d280..96c15c1371 100644 --- a/support/datastore/mocks.go +++ b/support/datastore/mocks.go @@ -22,18 +22,23 @@ func (m *MockDataStore) Size(ctx context.Context, path string) (int64, error) { return args.Get(0).(int64), args.Error(1) } +func (m *MockDataStore) GetFileMetadata(ctx context.Context, path string) (map[string]string, error) { + args := m.Called(ctx, path) + return args.Get(0).(map[string]string), args.Error(1) +} + func (m *MockDataStore) GetFile(ctx context.Context, path string) (io.ReadCloser, error) { args := m.Called(ctx, path) return args.Get(0).(io.ReadCloser), args.Error(1) } -func (m *MockDataStore) PutFile(ctx context.Context, path string, in io.WriterTo) error { - args := m.Called(ctx, path, in) +func (m *MockDataStore) PutFile(ctx context.Context, path string, in io.WriterTo, metadata map[string]string) error { + args := m.Called(ctx, path, in, metadata) return args.Error(0) } -func (m *MockDataStore) PutFileIfNotExists(ctx context.Context, path string, in io.WriterTo) (bool, error) { - args := m.Called(ctx, path, in) +func (m *MockDataStore) PutFileIfNotExists(ctx context.Context, path string, in io.WriterTo, metadata map[string]string) (bool, error) { + args := m.Called(ctx, path, in, metadata) return args.Get(0).(bool), args.Error(1) } From f03edd0aab375e37df8e3cec46281e2d159d9858 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Mon, 3 Jun 2024 22:03:26 +0200 Subject: [PATCH 183/234] Bump XDR (#5330) --- Makefile | 2 +- gxdr/xdr_generated.go | 763 ++++++++++--- xdr/Stellar-ledger.x | 2 + xdr/Stellar-overlay.x | 116 +- xdr/xdr_commit_generated.txt | 2 +- xdr/xdr_generated.go | 2000 +++++++++++++++++++++++++++++----- 6 files changed, 2453 insertions(+), 432 deletions(-) diff --git a/Makefile b/Makefile index 13e51985ad..07037315e4 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ XDRS = $(DOWNLOADABLE_XDRS) xdr/Stellar-lighthorizon.x \ XDRGEN_COMMIT=e2cac557162d99b12ae73b846cf3d5bfe16636de -XDR_COMMIT=1a04392432dacc0092caaeae22a600ea1af3c6a5 +XDR_COMMIT=70180d5e8d9caee9e8645ed8a38c36a8cf403cd9 .PHONY: xdr xdr-clean xdr-update diff --git a/gxdr/xdr_generated.go b/gxdr/xdr_generated.go index e7c8f3ed58..7265cb5a71 100644 --- a/gxdr/xdr_generated.go +++ b/gxdr/xdr_generated.go @@ -1224,6 +1224,8 @@ type DiagnosticEvent struct { Event ContractEvent } +type DiagnosticEvents = []DiagnosticEvent + type SorobanTransactionMetaExtV1 struct { Ext ExtensionPoint // Total amount (in stroops) that has been charged for non-refundable @@ -1482,13 +1484,17 @@ const ( SCP_MESSAGE MessageType = 11 GET_SCP_STATE MessageType = 12 // new messages - HELLO MessageType = 13 - SURVEY_REQUEST MessageType = 14 - SURVEY_RESPONSE MessageType = 15 - SEND_MORE MessageType = 16 - SEND_MORE_EXTENDED MessageType = 20 - FLOOD_ADVERT MessageType = 18 - FLOOD_DEMAND MessageType = 19 + HELLO MessageType = 13 + SURVEY_REQUEST MessageType = 14 + SURVEY_RESPONSE MessageType = 15 + SEND_MORE MessageType = 16 + SEND_MORE_EXTENDED MessageType = 20 + FLOOD_ADVERT MessageType = 18 + FLOOD_DEMAND MessageType = 19 + TIME_SLICED_SURVEY_REQUEST MessageType = 21 + TIME_SLICED_SURVEY_RESPONSE MessageType = 22 + TIME_SLICED_SURVEY_START_COLLECTING MessageType = 23 + TIME_SLICED_SURVEY_STOP_COLLECTING MessageType = 24 ) type DontHave struct { @@ -1499,7 +1505,8 @@ type DontHave struct { type SurveyMessageCommandType int32 const ( - SURVEY_TOPOLOGY SurveyMessageCommandType = 0 + SURVEY_TOPOLOGY SurveyMessageCommandType = 0 + TIME_SLICED_SURVEY_TOPOLOGY SurveyMessageCommandType = 1 ) type SurveyMessageResponseType int32 @@ -1507,8 +1514,31 @@ type SurveyMessageResponseType int32 const ( SURVEY_TOPOLOGY_RESPONSE_V0 SurveyMessageResponseType = 0 SURVEY_TOPOLOGY_RESPONSE_V1 SurveyMessageResponseType = 1 + SURVEY_TOPOLOGY_RESPONSE_V2 SurveyMessageResponseType = 2 ) +type TimeSlicedSurveyStartCollectingMessage struct { + SurveyorID NodeID + Nonce Uint32 + LedgerNum Uint32 +} + +type SignedTimeSlicedSurveyStartCollectingMessage struct { + Signature Signature + StartCollecting TimeSlicedSurveyStartCollectingMessage +} + +type TimeSlicedSurveyStopCollectingMessage struct { + SurveyorID NodeID + Nonce Uint32 + LedgerNum Uint32 +} + +type SignedTimeSlicedSurveyStopCollectingMessage struct { + Signature Signature + StopCollecting TimeSlicedSurveyStopCollectingMessage +} + type SurveyRequestMessage struct { SurveyorPeerID NodeID SurveyedPeerID NodeID @@ -1517,11 +1547,23 @@ type SurveyRequestMessage struct { CommandType SurveyMessageCommandType } +type TimeSlicedSurveyRequestMessage struct { + Request SurveyRequestMessage + Nonce Uint32 + InboundPeersIndex Uint32 + OutboundPeersIndex Uint32 +} + type SignedSurveyRequestMessage struct { RequestSignature Signature Request SurveyRequestMessage } +type SignedTimeSlicedSurveyRequestMessage struct { + RequestSignature Signature + Request TimeSlicedSurveyRequestMessage +} + type EncryptedBody = []byte // bound 64000 type SurveyResponseMessage struct { @@ -1532,11 +1574,21 @@ type SurveyResponseMessage struct { EncryptedBody EncryptedBody } +type TimeSlicedSurveyResponseMessage struct { + Response SurveyResponseMessage + Nonce Uint32 +} + type SignedSurveyResponseMessage struct { ResponseSignature Signature Response SurveyResponseMessage } +type SignedTimeSlicedSurveyResponseMessage struct { + ResponseSignature Signature + Response TimeSlicedSurveyResponseMessage +} + type PeerStats struct { Id NodeID VersionStr string // bound 100 @@ -1557,6 +1609,29 @@ type PeerStats struct { type PeerStatList = []PeerStats // bound 25 +type TimeSlicedNodeData struct { + AddedAuthenticatedPeers Uint32 + DroppedAuthenticatedPeers Uint32 + TotalInboundPeerCount Uint32 + TotalOutboundPeerCount Uint32 + // SCP stats + P75SCPFirstToSelfLatencyMs Uint32 + P75SCPSelfToOtherLatencyMs Uint32 + // How many times the node lost sync in the time slice + LostSyncCount Uint32 + // Config data + IsValidator bool + MaxInboundPeerCount Uint32 + MaxOutboundPeerCount Uint32 +} + +type TimeSlicedPeerData struct { + PeerStats PeerStats + AverageLatencyMs Uint32 +} + +type TimeSlicedPeerDataList = []TimeSlicedPeerData // bound 25 + type TopologyResponseBodyV0 struct { InboundPeers PeerStatList OutboundPeers PeerStatList @@ -1573,12 +1648,20 @@ type TopologyResponseBodyV1 struct { MaxOutboundPeerCount Uint32 } +type TopologyResponseBodyV2 struct { + InboundPeers TimeSlicedPeerDataList + OutboundPeers TimeSlicedPeerDataList + NodeData TimeSlicedNodeData +} + type SurveyResponseBody struct { // The union discriminant Type selects among the following arms: // SURVEY_TOPOLOGY_RESPONSE_V0: // TopologyResponseBodyV0() *TopologyResponseBodyV0 // SURVEY_TOPOLOGY_RESPONSE_V1: // TopologyResponseBodyV1() *TopologyResponseBodyV1 + // SURVEY_TOPOLOGY_RESPONSE_V2: + // TopologyResponseBodyV2() *TopologyResponseBodyV2 Type SurveyMessageResponseType _u interface{} } @@ -1625,6 +1708,14 @@ type StellarMessage struct { // SignedSurveyRequestMessage() *SignedSurveyRequestMessage // SURVEY_RESPONSE: // SignedSurveyResponseMessage() *SignedSurveyResponseMessage + // TIME_SLICED_SURVEY_REQUEST: + // SignedTimeSlicedSurveyRequestMessage() *SignedTimeSlicedSurveyRequestMessage + // TIME_SLICED_SURVEY_RESPONSE: + // SignedTimeSlicedSurveyResponseMessage() *SignedTimeSlicedSurveyResponseMessage + // TIME_SLICED_SURVEY_START_COLLECTING: + // SignedTimeSlicedSurveyStartCollectingMessage() *SignedTimeSlicedSurveyStartCollectingMessage + // TIME_SLICED_SURVEY_STOP_COLLECTING: + // SignedTimeSlicedSurveyStopCollectingMessage() *SignedTimeSlicedSurveyStopCollectingMessage // GET_SCP_QUORUMSET: // QSetHash() *Uint256 // SCP_QUORUMSET: @@ -12018,6 +12109,73 @@ func (v *DiagnosticEvent) XdrRecurse(x XDR, name string) { } func XDR_DiagnosticEvent(v *DiagnosticEvent) *DiagnosticEvent { return v } +type _XdrVec_unbounded_DiagnosticEvent []DiagnosticEvent + +func (_XdrVec_unbounded_DiagnosticEvent) XdrBound() uint32 { + const bound uint32 = 4294967295 // Force error if not const or doesn't fit + return bound +} +func (_XdrVec_unbounded_DiagnosticEvent) XdrCheckLen(length uint32) { + if length > uint32(4294967295) { + XdrPanic("_XdrVec_unbounded_DiagnosticEvent length %d exceeds bound 4294967295", length) + } else if int(length) < 0 { + XdrPanic("_XdrVec_unbounded_DiagnosticEvent length %d exceeds max int", length) + } +} +func (v _XdrVec_unbounded_DiagnosticEvent) GetVecLen() uint32 { return uint32(len(v)) } +func (v *_XdrVec_unbounded_DiagnosticEvent) SetVecLen(length uint32) { + v.XdrCheckLen(length) + if int(length) <= cap(*v) { + if int(length) != len(*v) { + *v = (*v)[:int(length)] + } + return + } + newcap := 2 * cap(*v) + if newcap < int(length) { // also catches overflow where 2*cap < 0 + newcap = int(length) + } else if bound := uint(4294967295); uint(newcap) > bound { + if int(bound) < 0 { + bound = ^uint(0) >> 1 + } + newcap = int(bound) + } + nv := make([]DiagnosticEvent, int(length), newcap) + copy(nv, *v) + *v = nv +} +func (v *_XdrVec_unbounded_DiagnosticEvent) XdrMarshalN(x XDR, name string, n uint32) { + v.XdrCheckLen(n) + for i := 0; i < int(n); i++ { + if i >= len(*v) { + v.SetVecLen(uint32(i + 1)) + } + XDR_DiagnosticEvent(&(*v)[i]).XdrMarshal(x, x.Sprintf("%s[%d]", name, i)) + } + if int(n) < len(*v) { + *v = (*v)[:int(n)] + } +} +func (v *_XdrVec_unbounded_DiagnosticEvent) XdrRecurse(x XDR, name string) { + size := XdrSize{Size: uint32(len(*v)), Bound: 4294967295} + x.Marshal(name, &size) + v.XdrMarshalN(x, name, size.Size) +} +func (_XdrVec_unbounded_DiagnosticEvent) XdrTypeName() string { return "DiagnosticEvent<>" } +func (v *_XdrVec_unbounded_DiagnosticEvent) XdrPointer() interface{} { return (*[]DiagnosticEvent)(v) } +func (v _XdrVec_unbounded_DiagnosticEvent) XdrValue() interface{} { return ([]DiagnosticEvent)(v) } +func (v *_XdrVec_unbounded_DiagnosticEvent) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } + +type XdrType_DiagnosticEvents struct { + *_XdrVec_unbounded_DiagnosticEvent +} + +func XDR_DiagnosticEvents(v *DiagnosticEvents) XdrType_DiagnosticEvents { + return XdrType_DiagnosticEvents{(*_XdrVec_unbounded_DiagnosticEvent)(v)} +} +func (XdrType_DiagnosticEvents) XdrTypeName() string { return "DiagnosticEvents" } +func (v XdrType_DiagnosticEvents) XdrUnwrap() XdrType { return v._XdrVec_unbounded_DiagnosticEvent } + type XdrType_SorobanTransactionMetaExtV1 = *SorobanTransactionMetaExtV1 func (v *SorobanTransactionMetaExtV1) XdrPointer() interface{} { return v } @@ -12171,63 +12329,6 @@ func (v *_XdrVec_unbounded_ContractEvent) XdrPointer() interface{} { retur func (v _XdrVec_unbounded_ContractEvent) XdrValue() interface{} { return ([]ContractEvent)(v) } func (v *_XdrVec_unbounded_ContractEvent) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -type _XdrVec_unbounded_DiagnosticEvent []DiagnosticEvent - -func (_XdrVec_unbounded_DiagnosticEvent) XdrBound() uint32 { - const bound uint32 = 4294967295 // Force error if not const or doesn't fit - return bound -} -func (_XdrVec_unbounded_DiagnosticEvent) XdrCheckLen(length uint32) { - if length > uint32(4294967295) { - XdrPanic("_XdrVec_unbounded_DiagnosticEvent length %d exceeds bound 4294967295", length) - } else if int(length) < 0 { - XdrPanic("_XdrVec_unbounded_DiagnosticEvent length %d exceeds max int", length) - } -} -func (v _XdrVec_unbounded_DiagnosticEvent) GetVecLen() uint32 { return uint32(len(v)) } -func (v *_XdrVec_unbounded_DiagnosticEvent) SetVecLen(length uint32) { - v.XdrCheckLen(length) - if int(length) <= cap(*v) { - if int(length) != len(*v) { - *v = (*v)[:int(length)] - } - return - } - newcap := 2 * cap(*v) - if newcap < int(length) { // also catches overflow where 2*cap < 0 - newcap = int(length) - } else if bound := uint(4294967295); uint(newcap) > bound { - if int(bound) < 0 { - bound = ^uint(0) >> 1 - } - newcap = int(bound) - } - nv := make([]DiagnosticEvent, int(length), newcap) - copy(nv, *v) - *v = nv -} -func (v *_XdrVec_unbounded_DiagnosticEvent) XdrMarshalN(x XDR, name string, n uint32) { - v.XdrCheckLen(n) - for i := 0; i < int(n); i++ { - if i >= len(*v) { - v.SetVecLen(uint32(i + 1)) - } - XDR_DiagnosticEvent(&(*v)[i]).XdrMarshal(x, x.Sprintf("%s[%d]", name, i)) - } - if int(n) < len(*v) { - *v = (*v)[:int(n)] - } -} -func (v *_XdrVec_unbounded_DiagnosticEvent) XdrRecurse(x XDR, name string) { - size := XdrSize{Size: uint32(len(*v)), Bound: 4294967295} - x.Marshal(name, &size) - v.XdrMarshalN(x, name, size.Size) -} -func (_XdrVec_unbounded_DiagnosticEvent) XdrTypeName() string { return "DiagnosticEvent<>" } -func (v *_XdrVec_unbounded_DiagnosticEvent) XdrPointer() interface{} { return (*[]DiagnosticEvent)(v) } -func (v _XdrVec_unbounded_DiagnosticEvent) XdrValue() interface{} { return ([]DiagnosticEvent)(v) } -func (v *_XdrVec_unbounded_DiagnosticEvent) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } - type XdrType_SorobanTransactionMeta = *SorobanTransactionMeta func (v *SorobanTransactionMeta) XdrPointer() interface{} { return v } @@ -13371,48 +13472,56 @@ func (v *PeerAddress) XdrRecurse(x XDR, name string) { func XDR_PeerAddress(v *PeerAddress) *PeerAddress { return v } var _XdrNames_MessageType = map[int32]string{ - int32(ERROR_MSG): "ERROR_MSG", - int32(AUTH): "AUTH", - int32(DONT_HAVE): "DONT_HAVE", - int32(GET_PEERS): "GET_PEERS", - int32(PEERS): "PEERS", - int32(GET_TX_SET): "GET_TX_SET", - int32(TX_SET): "TX_SET", - int32(GENERALIZED_TX_SET): "GENERALIZED_TX_SET", - int32(TRANSACTION): "TRANSACTION", - int32(GET_SCP_QUORUMSET): "GET_SCP_QUORUMSET", - int32(SCP_QUORUMSET): "SCP_QUORUMSET", - int32(SCP_MESSAGE): "SCP_MESSAGE", - int32(GET_SCP_STATE): "GET_SCP_STATE", - int32(HELLO): "HELLO", - int32(SURVEY_REQUEST): "SURVEY_REQUEST", - int32(SURVEY_RESPONSE): "SURVEY_RESPONSE", - int32(SEND_MORE): "SEND_MORE", - int32(SEND_MORE_EXTENDED): "SEND_MORE_EXTENDED", - int32(FLOOD_ADVERT): "FLOOD_ADVERT", - int32(FLOOD_DEMAND): "FLOOD_DEMAND", + int32(ERROR_MSG): "ERROR_MSG", + int32(AUTH): "AUTH", + int32(DONT_HAVE): "DONT_HAVE", + int32(GET_PEERS): "GET_PEERS", + int32(PEERS): "PEERS", + int32(GET_TX_SET): "GET_TX_SET", + int32(TX_SET): "TX_SET", + int32(GENERALIZED_TX_SET): "GENERALIZED_TX_SET", + int32(TRANSACTION): "TRANSACTION", + int32(GET_SCP_QUORUMSET): "GET_SCP_QUORUMSET", + int32(SCP_QUORUMSET): "SCP_QUORUMSET", + int32(SCP_MESSAGE): "SCP_MESSAGE", + int32(GET_SCP_STATE): "GET_SCP_STATE", + int32(HELLO): "HELLO", + int32(SURVEY_REQUEST): "SURVEY_REQUEST", + int32(SURVEY_RESPONSE): "SURVEY_RESPONSE", + int32(SEND_MORE): "SEND_MORE", + int32(SEND_MORE_EXTENDED): "SEND_MORE_EXTENDED", + int32(FLOOD_ADVERT): "FLOOD_ADVERT", + int32(FLOOD_DEMAND): "FLOOD_DEMAND", + int32(TIME_SLICED_SURVEY_REQUEST): "TIME_SLICED_SURVEY_REQUEST", + int32(TIME_SLICED_SURVEY_RESPONSE): "TIME_SLICED_SURVEY_RESPONSE", + int32(TIME_SLICED_SURVEY_START_COLLECTING): "TIME_SLICED_SURVEY_START_COLLECTING", + int32(TIME_SLICED_SURVEY_STOP_COLLECTING): "TIME_SLICED_SURVEY_STOP_COLLECTING", } var _XdrValues_MessageType = map[string]int32{ - "ERROR_MSG": int32(ERROR_MSG), - "AUTH": int32(AUTH), - "DONT_HAVE": int32(DONT_HAVE), - "GET_PEERS": int32(GET_PEERS), - "PEERS": int32(PEERS), - "GET_TX_SET": int32(GET_TX_SET), - "TX_SET": int32(TX_SET), - "GENERALIZED_TX_SET": int32(GENERALIZED_TX_SET), - "TRANSACTION": int32(TRANSACTION), - "GET_SCP_QUORUMSET": int32(GET_SCP_QUORUMSET), - "SCP_QUORUMSET": int32(SCP_QUORUMSET), - "SCP_MESSAGE": int32(SCP_MESSAGE), - "GET_SCP_STATE": int32(GET_SCP_STATE), - "HELLO": int32(HELLO), - "SURVEY_REQUEST": int32(SURVEY_REQUEST), - "SURVEY_RESPONSE": int32(SURVEY_RESPONSE), - "SEND_MORE": int32(SEND_MORE), - "SEND_MORE_EXTENDED": int32(SEND_MORE_EXTENDED), - "FLOOD_ADVERT": int32(FLOOD_ADVERT), - "FLOOD_DEMAND": int32(FLOOD_DEMAND), + "ERROR_MSG": int32(ERROR_MSG), + "AUTH": int32(AUTH), + "DONT_HAVE": int32(DONT_HAVE), + "GET_PEERS": int32(GET_PEERS), + "PEERS": int32(PEERS), + "GET_TX_SET": int32(GET_TX_SET), + "TX_SET": int32(TX_SET), + "GENERALIZED_TX_SET": int32(GENERALIZED_TX_SET), + "TRANSACTION": int32(TRANSACTION), + "GET_SCP_QUORUMSET": int32(GET_SCP_QUORUMSET), + "SCP_QUORUMSET": int32(SCP_QUORUMSET), + "SCP_MESSAGE": int32(SCP_MESSAGE), + "GET_SCP_STATE": int32(GET_SCP_STATE), + "HELLO": int32(HELLO), + "SURVEY_REQUEST": int32(SURVEY_REQUEST), + "SURVEY_RESPONSE": int32(SURVEY_RESPONSE), + "SEND_MORE": int32(SEND_MORE), + "SEND_MORE_EXTENDED": int32(SEND_MORE_EXTENDED), + "FLOOD_ADVERT": int32(FLOOD_ADVERT), + "FLOOD_DEMAND": int32(FLOOD_DEMAND), + "TIME_SLICED_SURVEY_REQUEST": int32(TIME_SLICED_SURVEY_REQUEST), + "TIME_SLICED_SURVEY_RESPONSE": int32(TIME_SLICED_SURVEY_RESPONSE), + "TIME_SLICED_SURVEY_START_COLLECTING": int32(TIME_SLICED_SURVEY_START_COLLECTING), + "TIME_SLICED_SURVEY_STOP_COLLECTING": int32(TIME_SLICED_SURVEY_STOP_COLLECTING), } func (MessageType) XdrEnumNames() map[int32]string { @@ -13479,10 +13588,12 @@ func (v *DontHave) XdrRecurse(x XDR, name string) { func XDR_DontHave(v *DontHave) *DontHave { return v } var _XdrNames_SurveyMessageCommandType = map[int32]string{ - int32(SURVEY_TOPOLOGY): "SURVEY_TOPOLOGY", + int32(SURVEY_TOPOLOGY): "SURVEY_TOPOLOGY", + int32(TIME_SLICED_SURVEY_TOPOLOGY): "TIME_SLICED_SURVEY_TOPOLOGY", } var _XdrValues_SurveyMessageCommandType = map[string]int32{ - "SURVEY_TOPOLOGY": int32(SURVEY_TOPOLOGY), + "SURVEY_TOPOLOGY": int32(SURVEY_TOPOLOGY), + "TIME_SLICED_SURVEY_TOPOLOGY": int32(TIME_SLICED_SURVEY_TOPOLOGY), } func (SurveyMessageCommandType) XdrEnumNames() map[int32]string { @@ -13524,10 +13635,12 @@ func XDR_SurveyMessageCommandType(v *SurveyMessageCommandType) *SurveyMessageCom var _XdrNames_SurveyMessageResponseType = map[int32]string{ int32(SURVEY_TOPOLOGY_RESPONSE_V0): "SURVEY_TOPOLOGY_RESPONSE_V0", int32(SURVEY_TOPOLOGY_RESPONSE_V1): "SURVEY_TOPOLOGY_RESPONSE_V1", + int32(SURVEY_TOPOLOGY_RESPONSE_V2): "SURVEY_TOPOLOGY_RESPONSE_V2", } var _XdrValues_SurveyMessageResponseType = map[string]int32{ "SURVEY_TOPOLOGY_RESPONSE_V0": int32(SURVEY_TOPOLOGY_RESPONSE_V0), "SURVEY_TOPOLOGY_RESPONSE_V1": int32(SURVEY_TOPOLOGY_RESPONSE_V1), + "SURVEY_TOPOLOGY_RESPONSE_V2": int32(SURVEY_TOPOLOGY_RESPONSE_V2), } func (SurveyMessageResponseType) XdrEnumNames() map[int32]string { @@ -13566,6 +13679,88 @@ type XdrType_SurveyMessageResponseType = *SurveyMessageResponseType func XDR_SurveyMessageResponseType(v *SurveyMessageResponseType) *SurveyMessageResponseType { return v } +type XdrType_TimeSlicedSurveyStartCollectingMessage = *TimeSlicedSurveyStartCollectingMessage + +func (v *TimeSlicedSurveyStartCollectingMessage) XdrPointer() interface{} { return v } +func (TimeSlicedSurveyStartCollectingMessage) XdrTypeName() string { + return "TimeSlicedSurveyStartCollectingMessage" +} +func (v TimeSlicedSurveyStartCollectingMessage) XdrValue() interface{} { return v } +func (v *TimeSlicedSurveyStartCollectingMessage) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *TimeSlicedSurveyStartCollectingMessage) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%ssurveyorID", name), XDR_NodeID(&v.SurveyorID)) + x.Marshal(x.Sprintf("%snonce", name), XDR_Uint32(&v.Nonce)) + x.Marshal(x.Sprintf("%sledgerNum", name), XDR_Uint32(&v.LedgerNum)) +} +func XDR_TimeSlicedSurveyStartCollectingMessage(v *TimeSlicedSurveyStartCollectingMessage) *TimeSlicedSurveyStartCollectingMessage { + return v +} + +type XdrType_SignedTimeSlicedSurveyStartCollectingMessage = *SignedTimeSlicedSurveyStartCollectingMessage + +func (v *SignedTimeSlicedSurveyStartCollectingMessage) XdrPointer() interface{} { return v } +func (SignedTimeSlicedSurveyStartCollectingMessage) XdrTypeName() string { + return "SignedTimeSlicedSurveyStartCollectingMessage" +} +func (v SignedTimeSlicedSurveyStartCollectingMessage) XdrValue() interface{} { return v } +func (v *SignedTimeSlicedSurveyStartCollectingMessage) XdrMarshal(x XDR, name string) { + x.Marshal(name, v) +} +func (v *SignedTimeSlicedSurveyStartCollectingMessage) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%ssignature", name), XDR_Signature(&v.Signature)) + x.Marshal(x.Sprintf("%sstartCollecting", name), XDR_TimeSlicedSurveyStartCollectingMessage(&v.StartCollecting)) +} +func XDR_SignedTimeSlicedSurveyStartCollectingMessage(v *SignedTimeSlicedSurveyStartCollectingMessage) *SignedTimeSlicedSurveyStartCollectingMessage { + return v +} + +type XdrType_TimeSlicedSurveyStopCollectingMessage = *TimeSlicedSurveyStopCollectingMessage + +func (v *TimeSlicedSurveyStopCollectingMessage) XdrPointer() interface{} { return v } +func (TimeSlicedSurveyStopCollectingMessage) XdrTypeName() string { + return "TimeSlicedSurveyStopCollectingMessage" +} +func (v TimeSlicedSurveyStopCollectingMessage) XdrValue() interface{} { return v } +func (v *TimeSlicedSurveyStopCollectingMessage) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *TimeSlicedSurveyStopCollectingMessage) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%ssurveyorID", name), XDR_NodeID(&v.SurveyorID)) + x.Marshal(x.Sprintf("%snonce", name), XDR_Uint32(&v.Nonce)) + x.Marshal(x.Sprintf("%sledgerNum", name), XDR_Uint32(&v.LedgerNum)) +} +func XDR_TimeSlicedSurveyStopCollectingMessage(v *TimeSlicedSurveyStopCollectingMessage) *TimeSlicedSurveyStopCollectingMessage { + return v +} + +type XdrType_SignedTimeSlicedSurveyStopCollectingMessage = *SignedTimeSlicedSurveyStopCollectingMessage + +func (v *SignedTimeSlicedSurveyStopCollectingMessage) XdrPointer() interface{} { return v } +func (SignedTimeSlicedSurveyStopCollectingMessage) XdrTypeName() string { + return "SignedTimeSlicedSurveyStopCollectingMessage" +} +func (v SignedTimeSlicedSurveyStopCollectingMessage) XdrValue() interface{} { return v } +func (v *SignedTimeSlicedSurveyStopCollectingMessage) XdrMarshal(x XDR, name string) { + x.Marshal(name, v) +} +func (v *SignedTimeSlicedSurveyStopCollectingMessage) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%ssignature", name), XDR_Signature(&v.Signature)) + x.Marshal(x.Sprintf("%sstopCollecting", name), XDR_TimeSlicedSurveyStopCollectingMessage(&v.StopCollecting)) +} +func XDR_SignedTimeSlicedSurveyStopCollectingMessage(v *SignedTimeSlicedSurveyStopCollectingMessage) *SignedTimeSlicedSurveyStopCollectingMessage { + return v +} + type XdrType_SurveyRequestMessage = *SurveyRequestMessage func (v *SurveyRequestMessage) XdrPointer() interface{} { return v } @@ -13584,6 +13779,25 @@ func (v *SurveyRequestMessage) XdrRecurse(x XDR, name string) { } func XDR_SurveyRequestMessage(v *SurveyRequestMessage) *SurveyRequestMessage { return v } +type XdrType_TimeSlicedSurveyRequestMessage = *TimeSlicedSurveyRequestMessage + +func (v *TimeSlicedSurveyRequestMessage) XdrPointer() interface{} { return v } +func (TimeSlicedSurveyRequestMessage) XdrTypeName() string { return "TimeSlicedSurveyRequestMessage" } +func (v TimeSlicedSurveyRequestMessage) XdrValue() interface{} { return v } +func (v *TimeSlicedSurveyRequestMessage) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *TimeSlicedSurveyRequestMessage) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%srequest", name), XDR_SurveyRequestMessage(&v.Request)) + x.Marshal(x.Sprintf("%snonce", name), XDR_Uint32(&v.Nonce)) + x.Marshal(x.Sprintf("%sinboundPeersIndex", name), XDR_Uint32(&v.InboundPeersIndex)) + x.Marshal(x.Sprintf("%soutboundPeersIndex", name), XDR_Uint32(&v.OutboundPeersIndex)) +} +func XDR_TimeSlicedSurveyRequestMessage(v *TimeSlicedSurveyRequestMessage) *TimeSlicedSurveyRequestMessage { + return v +} + type XdrType_SignedSurveyRequestMessage = *SignedSurveyRequestMessage func (v *SignedSurveyRequestMessage) XdrPointer() interface{} { return v } @@ -13601,6 +13815,25 @@ func XDR_SignedSurveyRequestMessage(v *SignedSurveyRequestMessage) *SignedSurvey return v } +type XdrType_SignedTimeSlicedSurveyRequestMessage = *SignedTimeSlicedSurveyRequestMessage + +func (v *SignedTimeSlicedSurveyRequestMessage) XdrPointer() interface{} { return v } +func (SignedTimeSlicedSurveyRequestMessage) XdrTypeName() string { + return "SignedTimeSlicedSurveyRequestMessage" +} +func (v SignedTimeSlicedSurveyRequestMessage) XdrValue() interface{} { return v } +func (v *SignedTimeSlicedSurveyRequestMessage) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *SignedTimeSlicedSurveyRequestMessage) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%srequestSignature", name), XDR_Signature(&v.RequestSignature)) + x.Marshal(x.Sprintf("%srequest", name), XDR_TimeSlicedSurveyRequestMessage(&v.Request)) +} +func XDR_SignedTimeSlicedSurveyRequestMessage(v *SignedTimeSlicedSurveyRequestMessage) *SignedTimeSlicedSurveyRequestMessage { + return v +} + type XdrType_EncryptedBody struct { XdrVecOpaque } @@ -13629,6 +13862,23 @@ func (v *SurveyResponseMessage) XdrRecurse(x XDR, name string) { } func XDR_SurveyResponseMessage(v *SurveyResponseMessage) *SurveyResponseMessage { return v } +type XdrType_TimeSlicedSurveyResponseMessage = *TimeSlicedSurveyResponseMessage + +func (v *TimeSlicedSurveyResponseMessage) XdrPointer() interface{} { return v } +func (TimeSlicedSurveyResponseMessage) XdrTypeName() string { return "TimeSlicedSurveyResponseMessage" } +func (v TimeSlicedSurveyResponseMessage) XdrValue() interface{} { return v } +func (v *TimeSlicedSurveyResponseMessage) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *TimeSlicedSurveyResponseMessage) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%sresponse", name), XDR_SurveyResponseMessage(&v.Response)) + x.Marshal(x.Sprintf("%snonce", name), XDR_Uint32(&v.Nonce)) +} +func XDR_TimeSlicedSurveyResponseMessage(v *TimeSlicedSurveyResponseMessage) *TimeSlicedSurveyResponseMessage { + return v +} + type XdrType_SignedSurveyResponseMessage = *SignedSurveyResponseMessage func (v *SignedSurveyResponseMessage) XdrPointer() interface{} { return v } @@ -13646,6 +13896,25 @@ func XDR_SignedSurveyResponseMessage(v *SignedSurveyResponseMessage) *SignedSurv return v } +type XdrType_SignedTimeSlicedSurveyResponseMessage = *SignedTimeSlicedSurveyResponseMessage + +func (v *SignedTimeSlicedSurveyResponseMessage) XdrPointer() interface{} { return v } +func (SignedTimeSlicedSurveyResponseMessage) XdrTypeName() string { + return "SignedTimeSlicedSurveyResponseMessage" +} +func (v SignedTimeSlicedSurveyResponseMessage) XdrValue() interface{} { return v } +func (v *SignedTimeSlicedSurveyResponseMessage) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *SignedTimeSlicedSurveyResponseMessage) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%sresponseSignature", name), XDR_Signature(&v.ResponseSignature)) + x.Marshal(x.Sprintf("%sresponse", name), XDR_TimeSlicedSurveyResponseMessage(&v.Response)) +} +func XDR_SignedTimeSlicedSurveyResponseMessage(v *SignedTimeSlicedSurveyResponseMessage) *SignedTimeSlicedSurveyResponseMessage { + return v +} + type XdrType_PeerStats = *PeerStats func (v *PeerStats) XdrPointer() interface{} { return v } @@ -13741,6 +14010,111 @@ func XDR_PeerStatList(v *PeerStatList) XdrType_PeerStatList { func (XdrType_PeerStatList) XdrTypeName() string { return "PeerStatList" } func (v XdrType_PeerStatList) XdrUnwrap() XdrType { return v._XdrVec_25_PeerStats } +type XdrType_TimeSlicedNodeData = *TimeSlicedNodeData + +func (v *TimeSlicedNodeData) XdrPointer() interface{} { return v } +func (TimeSlicedNodeData) XdrTypeName() string { return "TimeSlicedNodeData" } +func (v TimeSlicedNodeData) XdrValue() interface{} { return v } +func (v *TimeSlicedNodeData) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *TimeSlicedNodeData) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%saddedAuthenticatedPeers", name), XDR_Uint32(&v.AddedAuthenticatedPeers)) + x.Marshal(x.Sprintf("%sdroppedAuthenticatedPeers", name), XDR_Uint32(&v.DroppedAuthenticatedPeers)) + x.Marshal(x.Sprintf("%stotalInboundPeerCount", name), XDR_Uint32(&v.TotalInboundPeerCount)) + x.Marshal(x.Sprintf("%stotalOutboundPeerCount", name), XDR_Uint32(&v.TotalOutboundPeerCount)) + x.Marshal(x.Sprintf("%sp75SCPFirstToSelfLatencyMs", name), XDR_Uint32(&v.P75SCPFirstToSelfLatencyMs)) + x.Marshal(x.Sprintf("%sp75SCPSelfToOtherLatencyMs", name), XDR_Uint32(&v.P75SCPSelfToOtherLatencyMs)) + x.Marshal(x.Sprintf("%slostSyncCount", name), XDR_Uint32(&v.LostSyncCount)) + x.Marshal(x.Sprintf("%sisValidator", name), XDR_bool(&v.IsValidator)) + x.Marshal(x.Sprintf("%smaxInboundPeerCount", name), XDR_Uint32(&v.MaxInboundPeerCount)) + x.Marshal(x.Sprintf("%smaxOutboundPeerCount", name), XDR_Uint32(&v.MaxOutboundPeerCount)) +} +func XDR_TimeSlicedNodeData(v *TimeSlicedNodeData) *TimeSlicedNodeData { return v } + +type XdrType_TimeSlicedPeerData = *TimeSlicedPeerData + +func (v *TimeSlicedPeerData) XdrPointer() interface{} { return v } +func (TimeSlicedPeerData) XdrTypeName() string { return "TimeSlicedPeerData" } +func (v TimeSlicedPeerData) XdrValue() interface{} { return v } +func (v *TimeSlicedPeerData) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *TimeSlicedPeerData) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%speerStats", name), XDR_PeerStats(&v.PeerStats)) + x.Marshal(x.Sprintf("%saverageLatencyMs", name), XDR_Uint32(&v.AverageLatencyMs)) +} +func XDR_TimeSlicedPeerData(v *TimeSlicedPeerData) *TimeSlicedPeerData { return v } + +type _XdrVec_25_TimeSlicedPeerData []TimeSlicedPeerData + +func (_XdrVec_25_TimeSlicedPeerData) XdrBound() uint32 { + const bound uint32 = 25 // Force error if not const or doesn't fit + return bound +} +func (_XdrVec_25_TimeSlicedPeerData) XdrCheckLen(length uint32) { + if length > uint32(25) { + XdrPanic("_XdrVec_25_TimeSlicedPeerData length %d exceeds bound 25", length) + } else if int(length) < 0 { + XdrPanic("_XdrVec_25_TimeSlicedPeerData length %d exceeds max int", length) + } +} +func (v _XdrVec_25_TimeSlicedPeerData) GetVecLen() uint32 { return uint32(len(v)) } +func (v *_XdrVec_25_TimeSlicedPeerData) SetVecLen(length uint32) { + v.XdrCheckLen(length) + if int(length) <= cap(*v) { + if int(length) != len(*v) { + *v = (*v)[:int(length)] + } + return + } + newcap := 2 * cap(*v) + if newcap < int(length) { // also catches overflow where 2*cap < 0 + newcap = int(length) + } else if bound := uint(25); uint(newcap) > bound { + if int(bound) < 0 { + bound = ^uint(0) >> 1 + } + newcap = int(bound) + } + nv := make([]TimeSlicedPeerData, int(length), newcap) + copy(nv, *v) + *v = nv +} +func (v *_XdrVec_25_TimeSlicedPeerData) XdrMarshalN(x XDR, name string, n uint32) { + v.XdrCheckLen(n) + for i := 0; i < int(n); i++ { + if i >= len(*v) { + v.SetVecLen(uint32(i + 1)) + } + XDR_TimeSlicedPeerData(&(*v)[i]).XdrMarshal(x, x.Sprintf("%s[%d]", name, i)) + } + if int(n) < len(*v) { + *v = (*v)[:int(n)] + } +} +func (v *_XdrVec_25_TimeSlicedPeerData) XdrRecurse(x XDR, name string) { + size := XdrSize{Size: uint32(len(*v)), Bound: 25} + x.Marshal(name, &size) + v.XdrMarshalN(x, name, size.Size) +} +func (_XdrVec_25_TimeSlicedPeerData) XdrTypeName() string { return "TimeSlicedPeerData<>" } +func (v *_XdrVec_25_TimeSlicedPeerData) XdrPointer() interface{} { return (*[]TimeSlicedPeerData)(v) } +func (v _XdrVec_25_TimeSlicedPeerData) XdrValue() interface{} { return ([]TimeSlicedPeerData)(v) } +func (v *_XdrVec_25_TimeSlicedPeerData) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } + +type XdrType_TimeSlicedPeerDataList struct { + *_XdrVec_25_TimeSlicedPeerData +} + +func XDR_TimeSlicedPeerDataList(v *TimeSlicedPeerDataList) XdrType_TimeSlicedPeerDataList { + return XdrType_TimeSlicedPeerDataList{(*_XdrVec_25_TimeSlicedPeerData)(v)} +} +func (XdrType_TimeSlicedPeerDataList) XdrTypeName() string { return "TimeSlicedPeerDataList" } +func (v XdrType_TimeSlicedPeerDataList) XdrUnwrap() XdrType { return v._XdrVec_25_TimeSlicedPeerData } + type XdrType_TopologyResponseBodyV0 = *TopologyResponseBodyV0 func (v *TopologyResponseBodyV0) XdrPointer() interface{} { return v } @@ -13777,9 +14151,26 @@ func (v *TopologyResponseBodyV1) XdrRecurse(x XDR, name string) { } func XDR_TopologyResponseBodyV1(v *TopologyResponseBodyV1) *TopologyResponseBodyV1 { return v } +type XdrType_TopologyResponseBodyV2 = *TopologyResponseBodyV2 + +func (v *TopologyResponseBodyV2) XdrPointer() interface{} { return v } +func (TopologyResponseBodyV2) XdrTypeName() string { return "TopologyResponseBodyV2" } +func (v TopologyResponseBodyV2) XdrValue() interface{} { return v } +func (v *TopologyResponseBodyV2) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } +func (v *TopologyResponseBodyV2) XdrRecurse(x XDR, name string) { + if name != "" { + name = x.Sprintf("%s.", name) + } + x.Marshal(x.Sprintf("%sinboundPeers", name), XDR_TimeSlicedPeerDataList(&v.InboundPeers)) + x.Marshal(x.Sprintf("%soutboundPeers", name), XDR_TimeSlicedPeerDataList(&v.OutboundPeers)) + x.Marshal(x.Sprintf("%snodeData", name), XDR_TimeSlicedNodeData(&v.NodeData)) +} +func XDR_TopologyResponseBodyV2(v *TopologyResponseBodyV2) *TopologyResponseBodyV2 { return v } + var _XdrTags_SurveyResponseBody = map[int32]bool{ XdrToI32(SURVEY_TOPOLOGY_RESPONSE_V0): true, XdrToI32(SURVEY_TOPOLOGY_RESPONSE_V1): true, + XdrToI32(SURVEY_TOPOLOGY_RESPONSE_V2): true, } func (_ SurveyResponseBody) XdrValidTags() map[int32]bool { @@ -13815,9 +14206,24 @@ func (u *SurveyResponseBody) TopologyResponseBodyV1() *TopologyResponseBodyV1 { return nil } } +func (u *SurveyResponseBody) TopologyResponseBodyV2() *TopologyResponseBodyV2 { + switch u.Type { + case SURVEY_TOPOLOGY_RESPONSE_V2: + if v, ok := u._u.(*TopologyResponseBodyV2); ok { + return v + } else { + var zero TopologyResponseBodyV2 + u._u = &zero + return &zero + } + default: + XdrPanic("SurveyResponseBody.TopologyResponseBodyV2 accessed when Type == %v", u.Type) + return nil + } +} func (u SurveyResponseBody) XdrValid() bool { switch u.Type { - case SURVEY_TOPOLOGY_RESPONSE_V0, SURVEY_TOPOLOGY_RESPONSE_V1: + case SURVEY_TOPOLOGY_RESPONSE_V0, SURVEY_TOPOLOGY_RESPONSE_V1, SURVEY_TOPOLOGY_RESPONSE_V2: return true } return false @@ -13834,6 +14240,8 @@ func (u *SurveyResponseBody) XdrUnionBody() XdrType { return XDR_TopologyResponseBodyV0(u.TopologyResponseBodyV0()) case SURVEY_TOPOLOGY_RESPONSE_V1: return XDR_TopologyResponseBodyV1(u.TopologyResponseBodyV1()) + case SURVEY_TOPOLOGY_RESPONSE_V2: + return XDR_TopologyResponseBodyV2(u.TopologyResponseBodyV2()) } return nil } @@ -13843,6 +14251,8 @@ func (u *SurveyResponseBody) XdrUnionBodyName() string { return "TopologyResponseBodyV0" case SURVEY_TOPOLOGY_RESPONSE_V1: return "TopologyResponseBodyV1" + case SURVEY_TOPOLOGY_RESPONSE_V2: + return "TopologyResponseBodyV2" } return "" } @@ -13865,6 +14275,9 @@ func (u *SurveyResponseBody) XdrRecurse(x XDR, name string) { case SURVEY_TOPOLOGY_RESPONSE_V1: x.Marshal(x.Sprintf("%stopologyResponseBodyV1", name), XDR_TopologyResponseBodyV1(u.TopologyResponseBodyV1())) return + case SURVEY_TOPOLOGY_RESPONSE_V2: + x.Marshal(x.Sprintf("%stopologyResponseBodyV2", name), XDR_TopologyResponseBodyV2(u.TopologyResponseBodyV2())) + return } XdrPanic("invalid Type (%v) in SurveyResponseBody", u.Type) } @@ -14033,26 +14446,30 @@ func (v _XdrVec_100_PeerAddress) XdrValue() interface{} { return ([]Pee func (v *_XdrVec_100_PeerAddress) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } var _XdrTags_StellarMessage = map[int32]bool{ - XdrToI32(ERROR_MSG): true, - XdrToI32(HELLO): true, - XdrToI32(AUTH): true, - XdrToI32(DONT_HAVE): true, - XdrToI32(GET_PEERS): true, - XdrToI32(PEERS): true, - XdrToI32(GET_TX_SET): true, - XdrToI32(TX_SET): true, - XdrToI32(GENERALIZED_TX_SET): true, - XdrToI32(TRANSACTION): true, - XdrToI32(SURVEY_REQUEST): true, - XdrToI32(SURVEY_RESPONSE): true, - XdrToI32(GET_SCP_QUORUMSET): true, - XdrToI32(SCP_QUORUMSET): true, - XdrToI32(SCP_MESSAGE): true, - XdrToI32(GET_SCP_STATE): true, - XdrToI32(SEND_MORE): true, - XdrToI32(SEND_MORE_EXTENDED): true, - XdrToI32(FLOOD_ADVERT): true, - XdrToI32(FLOOD_DEMAND): true, + XdrToI32(ERROR_MSG): true, + XdrToI32(HELLO): true, + XdrToI32(AUTH): true, + XdrToI32(DONT_HAVE): true, + XdrToI32(GET_PEERS): true, + XdrToI32(PEERS): true, + XdrToI32(GET_TX_SET): true, + XdrToI32(TX_SET): true, + XdrToI32(GENERALIZED_TX_SET): true, + XdrToI32(TRANSACTION): true, + XdrToI32(SURVEY_REQUEST): true, + XdrToI32(SURVEY_RESPONSE): true, + XdrToI32(TIME_SLICED_SURVEY_REQUEST): true, + XdrToI32(TIME_SLICED_SURVEY_RESPONSE): true, + XdrToI32(TIME_SLICED_SURVEY_START_COLLECTING): true, + XdrToI32(TIME_SLICED_SURVEY_STOP_COLLECTING): true, + XdrToI32(GET_SCP_QUORUMSET): true, + XdrToI32(SCP_QUORUMSET): true, + XdrToI32(SCP_MESSAGE): true, + XdrToI32(GET_SCP_STATE): true, + XdrToI32(SEND_MORE): true, + XdrToI32(SEND_MORE_EXTENDED): true, + XdrToI32(FLOOD_ADVERT): true, + XdrToI32(FLOOD_DEMAND): true, } func (_ StellarMessage) XdrValidTags() map[int32]bool { @@ -14223,6 +14640,66 @@ func (u *StellarMessage) SignedSurveyResponseMessage() *SignedSurveyResponseMess return nil } } +func (u *StellarMessage) SignedTimeSlicedSurveyRequestMessage() *SignedTimeSlicedSurveyRequestMessage { + switch u.Type { + case TIME_SLICED_SURVEY_REQUEST: + if v, ok := u._u.(*SignedTimeSlicedSurveyRequestMessage); ok { + return v + } else { + var zero SignedTimeSlicedSurveyRequestMessage + u._u = &zero + return &zero + } + default: + XdrPanic("StellarMessage.SignedTimeSlicedSurveyRequestMessage accessed when Type == %v", u.Type) + return nil + } +} +func (u *StellarMessage) SignedTimeSlicedSurveyResponseMessage() *SignedTimeSlicedSurveyResponseMessage { + switch u.Type { + case TIME_SLICED_SURVEY_RESPONSE: + if v, ok := u._u.(*SignedTimeSlicedSurveyResponseMessage); ok { + return v + } else { + var zero SignedTimeSlicedSurveyResponseMessage + u._u = &zero + return &zero + } + default: + XdrPanic("StellarMessage.SignedTimeSlicedSurveyResponseMessage accessed when Type == %v", u.Type) + return nil + } +} +func (u *StellarMessage) SignedTimeSlicedSurveyStartCollectingMessage() *SignedTimeSlicedSurveyStartCollectingMessage { + switch u.Type { + case TIME_SLICED_SURVEY_START_COLLECTING: + if v, ok := u._u.(*SignedTimeSlicedSurveyStartCollectingMessage); ok { + return v + } else { + var zero SignedTimeSlicedSurveyStartCollectingMessage + u._u = &zero + return &zero + } + default: + XdrPanic("StellarMessage.SignedTimeSlicedSurveyStartCollectingMessage accessed when Type == %v", u.Type) + return nil + } +} +func (u *StellarMessage) SignedTimeSlicedSurveyStopCollectingMessage() *SignedTimeSlicedSurveyStopCollectingMessage { + switch u.Type { + case TIME_SLICED_SURVEY_STOP_COLLECTING: + if v, ok := u._u.(*SignedTimeSlicedSurveyStopCollectingMessage); ok { + return v + } else { + var zero SignedTimeSlicedSurveyStopCollectingMessage + u._u = &zero + return &zero + } + default: + XdrPanic("StellarMessage.SignedTimeSlicedSurveyStopCollectingMessage accessed when Type == %v", u.Type) + return nil + } +} func (u *StellarMessage) QSetHash() *Uint256 { switch u.Type { case GET_SCP_QUORUMSET: @@ -14347,7 +14824,7 @@ func (u *StellarMessage) FloodDemand() *FloodDemand { } func (u StellarMessage) XdrValid() bool { switch u.Type { - case ERROR_MSG, HELLO, AUTH, DONT_HAVE, GET_PEERS, PEERS, GET_TX_SET, TX_SET, GENERALIZED_TX_SET, TRANSACTION, SURVEY_REQUEST, SURVEY_RESPONSE, GET_SCP_QUORUMSET, SCP_QUORUMSET, SCP_MESSAGE, GET_SCP_STATE, SEND_MORE, SEND_MORE_EXTENDED, FLOOD_ADVERT, FLOOD_DEMAND: + case ERROR_MSG, HELLO, AUTH, DONT_HAVE, GET_PEERS, PEERS, GET_TX_SET, TX_SET, GENERALIZED_TX_SET, TRANSACTION, SURVEY_REQUEST, SURVEY_RESPONSE, TIME_SLICED_SURVEY_REQUEST, TIME_SLICED_SURVEY_RESPONSE, TIME_SLICED_SURVEY_START_COLLECTING, TIME_SLICED_SURVEY_STOP_COLLECTING, GET_SCP_QUORUMSET, SCP_QUORUMSET, SCP_MESSAGE, GET_SCP_STATE, SEND_MORE, SEND_MORE_EXTENDED, FLOOD_ADVERT, FLOOD_DEMAND: return true } return false @@ -14384,6 +14861,14 @@ func (u *StellarMessage) XdrUnionBody() XdrType { return XDR_SignedSurveyRequestMessage(u.SignedSurveyRequestMessage()) case SURVEY_RESPONSE: return XDR_SignedSurveyResponseMessage(u.SignedSurveyResponseMessage()) + case TIME_SLICED_SURVEY_REQUEST: + return XDR_SignedTimeSlicedSurveyRequestMessage(u.SignedTimeSlicedSurveyRequestMessage()) + case TIME_SLICED_SURVEY_RESPONSE: + return XDR_SignedTimeSlicedSurveyResponseMessage(u.SignedTimeSlicedSurveyResponseMessage()) + case TIME_SLICED_SURVEY_START_COLLECTING: + return XDR_SignedTimeSlicedSurveyStartCollectingMessage(u.SignedTimeSlicedSurveyStartCollectingMessage()) + case TIME_SLICED_SURVEY_STOP_COLLECTING: + return XDR_SignedTimeSlicedSurveyStopCollectingMessage(u.SignedTimeSlicedSurveyStopCollectingMessage()) case GET_SCP_QUORUMSET: return XDR_Uint256(u.QSetHash()) case SCP_QUORUMSET: @@ -14429,6 +14914,14 @@ func (u *StellarMessage) XdrUnionBodyName() string { return "SignedSurveyRequestMessage" case SURVEY_RESPONSE: return "SignedSurveyResponseMessage" + case TIME_SLICED_SURVEY_REQUEST: + return "SignedTimeSlicedSurveyRequestMessage" + case TIME_SLICED_SURVEY_RESPONSE: + return "SignedTimeSlicedSurveyResponseMessage" + case TIME_SLICED_SURVEY_START_COLLECTING: + return "SignedTimeSlicedSurveyStartCollectingMessage" + case TIME_SLICED_SURVEY_STOP_COLLECTING: + return "SignedTimeSlicedSurveyStopCollectingMessage" case GET_SCP_QUORUMSET: return "QSetHash" case SCP_QUORUMSET: @@ -14496,6 +14989,18 @@ func (u *StellarMessage) XdrRecurse(x XDR, name string) { case SURVEY_RESPONSE: x.Marshal(x.Sprintf("%ssignedSurveyResponseMessage", name), XDR_SignedSurveyResponseMessage(u.SignedSurveyResponseMessage())) return + case TIME_SLICED_SURVEY_REQUEST: + x.Marshal(x.Sprintf("%ssignedTimeSlicedSurveyRequestMessage", name), XDR_SignedTimeSlicedSurveyRequestMessage(u.SignedTimeSlicedSurveyRequestMessage())) + return + case TIME_SLICED_SURVEY_RESPONSE: + x.Marshal(x.Sprintf("%ssignedTimeSlicedSurveyResponseMessage", name), XDR_SignedTimeSlicedSurveyResponseMessage(u.SignedTimeSlicedSurveyResponseMessage())) + return + case TIME_SLICED_SURVEY_START_COLLECTING: + x.Marshal(x.Sprintf("%ssignedTimeSlicedSurveyStartCollectingMessage", name), XDR_SignedTimeSlicedSurveyStartCollectingMessage(u.SignedTimeSlicedSurveyStartCollectingMessage())) + return + case TIME_SLICED_SURVEY_STOP_COLLECTING: + x.Marshal(x.Sprintf("%ssignedTimeSlicedSurveyStopCollectingMessage", name), XDR_SignedTimeSlicedSurveyStopCollectingMessage(u.SignedTimeSlicedSurveyStopCollectingMessage())) + return case GET_SCP_QUORUMSET: x.Marshal(x.Sprintf("%sqSetHash", name), XDR_Uint256(u.QSetHash())) return diff --git a/xdr/Stellar-ledger.x b/xdr/Stellar-ledger.x index dd58ae8d9e..d19462aa18 100644 --- a/xdr/Stellar-ledger.x +++ b/xdr/Stellar-ledger.x @@ -400,6 +400,8 @@ struct DiagnosticEvent ContractEvent event; }; +typedef DiagnosticEvent DiagnosticEvents<>; + struct SorobanTransactionMetaExtV1 { ExtensionPoint ext; diff --git a/xdr/Stellar-overlay.x b/xdr/Stellar-overlay.x index 4c964736dc..b398f883d2 100644 --- a/xdr/Stellar-overlay.x +++ b/xdr/Stellar-overlay.x @@ -119,7 +119,12 @@ enum MessageType SEND_MORE_EXTENDED = 20, FLOOD_ADVERT = 18, - FLOOD_DEMAND = 19 + FLOOD_DEMAND = 19, + + TIME_SLICED_SURVEY_REQUEST = 21, + TIME_SLICED_SURVEY_RESPONSE = 22, + TIME_SLICED_SURVEY_START_COLLECTING = 23, + TIME_SLICED_SURVEY_STOP_COLLECTING = 24 }; struct DontHave @@ -130,13 +135,41 @@ struct DontHave enum SurveyMessageCommandType { - SURVEY_TOPOLOGY = 0 + SURVEY_TOPOLOGY = 0, + TIME_SLICED_SURVEY_TOPOLOGY = 1 }; enum SurveyMessageResponseType { SURVEY_TOPOLOGY_RESPONSE_V0 = 0, - SURVEY_TOPOLOGY_RESPONSE_V1 = 1 + SURVEY_TOPOLOGY_RESPONSE_V1 = 1, + SURVEY_TOPOLOGY_RESPONSE_V2 = 2 +}; + +struct TimeSlicedSurveyStartCollectingMessage +{ + NodeID surveyorID; + uint32 nonce; + uint32 ledgerNum; +}; + +struct SignedTimeSlicedSurveyStartCollectingMessage +{ + Signature signature; + TimeSlicedSurveyStartCollectingMessage startCollecting; +}; + +struct TimeSlicedSurveyStopCollectingMessage +{ + NodeID surveyorID; + uint32 nonce; + uint32 ledgerNum; +}; + +struct SignedTimeSlicedSurveyStopCollectingMessage +{ + Signature signature; + TimeSlicedSurveyStopCollectingMessage stopCollecting; }; struct SurveyRequestMessage @@ -148,12 +181,26 @@ struct SurveyRequestMessage SurveyMessageCommandType commandType; }; +struct TimeSlicedSurveyRequestMessage +{ + SurveyRequestMessage request; + uint32 nonce; + uint32 inboundPeersIndex; + uint32 outboundPeersIndex; +}; + struct SignedSurveyRequestMessage { Signature requestSignature; SurveyRequestMessage request; }; +struct SignedTimeSlicedSurveyRequestMessage +{ + Signature requestSignature; + TimeSlicedSurveyRequestMessage request; +}; + typedef opaque EncryptedBody<64000>; struct SurveyResponseMessage { @@ -164,12 +211,24 @@ struct SurveyResponseMessage EncryptedBody encryptedBody; }; +struct TimeSlicedSurveyResponseMessage +{ + SurveyResponseMessage response; + uint32 nonce; +}; + struct SignedSurveyResponseMessage { Signature responseSignature; SurveyResponseMessage response; }; +struct SignedTimeSlicedSurveyResponseMessage +{ + Signature responseSignature; + TimeSlicedSurveyResponseMessage response; +}; + struct PeerStats { NodeID id; @@ -193,6 +252,34 @@ struct PeerStats typedef PeerStats PeerStatList<25>; +struct TimeSlicedNodeData +{ + uint32 addedAuthenticatedPeers; + uint32 droppedAuthenticatedPeers; + uint32 totalInboundPeerCount; + uint32 totalOutboundPeerCount; + + // SCP stats + uint32 p75SCPFirstToSelfLatencyMs; + uint32 p75SCPSelfToOtherLatencyMs; + + // How many times the node lost sync in the time slice + uint32 lostSyncCount; + + // Config data + bool isValidator; + uint32 maxInboundPeerCount; + uint32 maxOutboundPeerCount; +}; + +struct TimeSlicedPeerData +{ + PeerStats peerStats; + uint32 averageLatencyMs; +}; + +typedef TimeSlicedPeerData TimeSlicedPeerDataList<25>; + struct TopologyResponseBodyV0 { PeerStatList inboundPeers; @@ -214,12 +301,21 @@ struct TopologyResponseBodyV1 uint32 maxOutboundPeerCount; }; +struct TopologyResponseBodyV2 +{ + TimeSlicedPeerDataList inboundPeers; + TimeSlicedPeerDataList outboundPeers; + TimeSlicedNodeData nodeData; +}; + union SurveyResponseBody switch (SurveyMessageResponseType type) { case SURVEY_TOPOLOGY_RESPONSE_V0: TopologyResponseBodyV0 topologyResponseBodyV0; case SURVEY_TOPOLOGY_RESPONSE_V1: TopologyResponseBodyV1 topologyResponseBodyV1; +case SURVEY_TOPOLOGY_RESPONSE_V2: + TopologyResponseBodyV2 topologyResponseBodyV2; }; const TX_ADVERT_VECTOR_MAX_SIZE = 1000; @@ -269,6 +365,20 @@ case SURVEY_REQUEST: case SURVEY_RESPONSE: SignedSurveyResponseMessage signedSurveyResponseMessage; +case TIME_SLICED_SURVEY_REQUEST: + SignedTimeSlicedSurveyRequestMessage signedTimeSlicedSurveyRequestMessage; + +case TIME_SLICED_SURVEY_RESPONSE: + SignedTimeSlicedSurveyResponseMessage signedTimeSlicedSurveyResponseMessage; + +case TIME_SLICED_SURVEY_START_COLLECTING: + SignedTimeSlicedSurveyStartCollectingMessage + signedTimeSlicedSurveyStartCollectingMessage; + +case TIME_SLICED_SURVEY_STOP_COLLECTING: + SignedTimeSlicedSurveyStopCollectingMessage + signedTimeSlicedSurveyStopCollectingMessage; + // SCP case GET_SCP_QUORUMSET: uint256 qSetHash; diff --git a/xdr/xdr_commit_generated.txt b/xdr/xdr_commit_generated.txt index 52f66572f6..793097adeb 100644 --- a/xdr/xdr_commit_generated.txt +++ b/xdr/xdr_commit_generated.txt @@ -1 +1 @@ -1a04392432dacc0092caaeae22a600ea1af3c6a5 \ No newline at end of file +70180d5e8d9caee9e8645ed8a38c36a8cf403cd9 \ No newline at end of file diff --git a/xdr/xdr_generated.go b/xdr/xdr_generated.go index b336714562..c47ab760c9 100644 --- a/xdr/xdr_generated.go +++ b/xdr/xdr_generated.go @@ -42,9 +42,9 @@ var XdrFilesSHA256 = map[string]string{ "xdr/Stellar-exporter.x": "a00c83d02e8c8382e06f79a191f1fb5abd097a4bbcab8481c67467e3270e0529", "xdr/Stellar-internal.x": "227835866c1b2122d1eaf28839ba85ea7289d1cb681dda4ca619c2da3d71fe00", "xdr/Stellar-ledger-entries.x": "77dc7062ae6d0812136333e12e35b2294d7c2896a536be9c811eb0ed2abbbccb", - "xdr/Stellar-ledger.x": "888152fb940b79a01ac00a5218ca91360cb0f01af7acc030d5805ebfec280203", + "xdr/Stellar-ledger.x": "46c1c55972750b97650ff00788a2be4764975b787ef51c8fa931c56e2028a3c4", "xdr/Stellar-lighthorizon.x": "1aac09eaeda224154f653a0c95f02167be0c110fc295bb41b756a080eb8c06df", - "xdr/Stellar-overlay.x": "de3957c58b96ae07968b3d3aebea84f83603e95322d1fa336360e13e3aba737a", + "xdr/Stellar-overlay.x": "8c73b7c3ad974e7fc4aa4fdf34f7ad50053406254efbd7406c96657cf41691d3", "xdr/Stellar-transaction.x": "0d2b35a331a540b48643925d0869857236eb2487c02d340ea32e365e784ea2b8", "xdr/Stellar-types.x": "6e3b13f0d3e360b09fa5e2b0e55d43f4d974a769df66afb34e8aecbb329d3f15", } @@ -16754,6 +16754,86 @@ func (s DiagnosticEvent) xdrType() {} var _ xdrType = (*DiagnosticEvent)(nil) +// DiagnosticEvents is an XDR Typedef defines as: +// +// typedef DiagnosticEvent DiagnosticEvents<>; +type DiagnosticEvents []DiagnosticEvent + +// EncodeTo encodes this value using the Encoder. +func (s DiagnosticEvents) EncodeTo(e *xdr.Encoder) error { + var err error + if _, err = e.EncodeUint(uint32(len(s))); err != nil { + return err + } + for i := 0; i < len(s); i++ { + if err = s[i].EncodeTo(e); err != nil { + return err + } + } + return nil +} + +var _ decoderFrom = (*DiagnosticEvents)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *DiagnosticEvents) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding DiagnosticEvents: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + var l uint32 + l, nTmp, err = d.DecodeUint() + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding DiagnosticEvent: %w", err) + } + (*s) = nil + if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding DiagnosticEvent: length (%d) exceeds remaining input length (%d)", l, il) + } + (*s) = make([]DiagnosticEvent, l) + for i := uint32(0); i < l; i++ { + nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding DiagnosticEvent: %w", err) + } + } + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s DiagnosticEvents) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *DiagnosticEvents) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*DiagnosticEvents)(nil) + _ encoding.BinaryUnmarshaler = (*DiagnosticEvents)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s DiagnosticEvents) xdrType() {} + +var _ xdrType = (*DiagnosticEvents)(nil) + // SorobanTransactionMetaExtV1 is an XDR Struct defines as: // // struct SorobanTransactionMetaExtV1 @@ -19739,31 +19819,40 @@ var _ xdrType = (*PeerAddress)(nil) // SEND_MORE_EXTENDED = 20, // // FLOOD_ADVERT = 18, -// FLOOD_DEMAND = 19 +// FLOOD_DEMAND = 19, +// +// TIME_SLICED_SURVEY_REQUEST = 21, +// TIME_SLICED_SURVEY_RESPONSE = 22, +// TIME_SLICED_SURVEY_START_COLLECTING = 23, +// TIME_SLICED_SURVEY_STOP_COLLECTING = 24 // }; type MessageType int32 const ( - MessageTypeErrorMsg MessageType = 0 - MessageTypeAuth MessageType = 2 - MessageTypeDontHave MessageType = 3 - MessageTypeGetPeers MessageType = 4 - MessageTypePeers MessageType = 5 - MessageTypeGetTxSet MessageType = 6 - MessageTypeTxSet MessageType = 7 - MessageTypeGeneralizedTxSet MessageType = 17 - MessageTypeTransaction MessageType = 8 - MessageTypeGetScpQuorumset MessageType = 9 - MessageTypeScpQuorumset MessageType = 10 - MessageTypeScpMessage MessageType = 11 - MessageTypeGetScpState MessageType = 12 - MessageTypeHello MessageType = 13 - MessageTypeSurveyRequest MessageType = 14 - MessageTypeSurveyResponse MessageType = 15 - MessageTypeSendMore MessageType = 16 - MessageTypeSendMoreExtended MessageType = 20 - MessageTypeFloodAdvert MessageType = 18 - MessageTypeFloodDemand MessageType = 19 + MessageTypeErrorMsg MessageType = 0 + MessageTypeAuth MessageType = 2 + MessageTypeDontHave MessageType = 3 + MessageTypeGetPeers MessageType = 4 + MessageTypePeers MessageType = 5 + MessageTypeGetTxSet MessageType = 6 + MessageTypeTxSet MessageType = 7 + MessageTypeGeneralizedTxSet MessageType = 17 + MessageTypeTransaction MessageType = 8 + MessageTypeGetScpQuorumset MessageType = 9 + MessageTypeScpQuorumset MessageType = 10 + MessageTypeScpMessage MessageType = 11 + MessageTypeGetScpState MessageType = 12 + MessageTypeHello MessageType = 13 + MessageTypeSurveyRequest MessageType = 14 + MessageTypeSurveyResponse MessageType = 15 + MessageTypeSendMore MessageType = 16 + MessageTypeSendMoreExtended MessageType = 20 + MessageTypeFloodAdvert MessageType = 18 + MessageTypeFloodDemand MessageType = 19 + MessageTypeTimeSlicedSurveyRequest MessageType = 21 + MessageTypeTimeSlicedSurveyResponse MessageType = 22 + MessageTypeTimeSlicedSurveyStartCollecting MessageType = 23 + MessageTypeTimeSlicedSurveyStopCollecting MessageType = 24 ) var messageTypeMap = map[int32]string{ @@ -19787,6 +19876,10 @@ var messageTypeMap = map[int32]string{ 20: "MessageTypeSendMoreExtended", 18: "MessageTypeFloodAdvert", 19: "MessageTypeFloodDemand", + 21: "MessageTypeTimeSlicedSurveyRequest", + 22: "MessageTypeTimeSlicedSurveyResponse", + 23: "MessageTypeTimeSlicedSurveyStartCollecting", + 24: "MessageTypeTimeSlicedSurveyStopCollecting", } // ValidEnum validates a proposed value for this enum. Implements @@ -19937,16 +20030,19 @@ var _ xdrType = (*DontHave)(nil) // // enum SurveyMessageCommandType // { -// SURVEY_TOPOLOGY = 0 +// SURVEY_TOPOLOGY = 0, +// TIME_SLICED_SURVEY_TOPOLOGY = 1 // }; type SurveyMessageCommandType int32 const ( - SurveyMessageCommandTypeSurveyTopology SurveyMessageCommandType = 0 + SurveyMessageCommandTypeSurveyTopology SurveyMessageCommandType = 0 + SurveyMessageCommandTypeTimeSlicedSurveyTopology SurveyMessageCommandType = 1 ) var surveyMessageCommandTypeMap = map[int32]string{ 0: "SurveyMessageCommandTypeSurveyTopology", + 1: "SurveyMessageCommandTypeTimeSlicedSurveyTopology", } // ValidEnum validates a proposed value for this enum. Implements @@ -20023,18 +20119,21 @@ var _ xdrType = (*SurveyMessageCommandType)(nil) // enum SurveyMessageResponseType // { // SURVEY_TOPOLOGY_RESPONSE_V0 = 0, -// SURVEY_TOPOLOGY_RESPONSE_V1 = 1 +// SURVEY_TOPOLOGY_RESPONSE_V1 = 1, +// SURVEY_TOPOLOGY_RESPONSE_V2 = 2 // }; type SurveyMessageResponseType int32 const ( SurveyMessageResponseTypeSurveyTopologyResponseV0 SurveyMessageResponseType = 0 SurveyMessageResponseTypeSurveyTopologyResponseV1 SurveyMessageResponseType = 1 + SurveyMessageResponseTypeSurveyTopologyResponseV2 SurveyMessageResponseType = 2 ) var surveyMessageResponseTypeMap = map[int32]string{ 0: "SurveyMessageResponseTypeSurveyTopologyResponseV0", 1: "SurveyMessageResponseTypeSurveyTopologyResponseV1", + 2: "SurveyMessageResponseTypeSurveyTopologyResponseV2", } // ValidEnum validates a proposed value for this enum. Implements @@ -20106,85 +20205,140 @@ func (s SurveyMessageResponseType) xdrType() {} var _ xdrType = (*SurveyMessageResponseType)(nil) -// SurveyRequestMessage is an XDR Struct defines as: +// TimeSlicedSurveyStartCollectingMessage is an XDR Struct defines as: // -// struct SurveyRequestMessage +// struct TimeSlicedSurveyStartCollectingMessage // { -// NodeID surveyorPeerID; -// NodeID surveyedPeerID; +// NodeID surveyorID; +// uint32 nonce; // uint32 ledgerNum; -// Curve25519Public encryptionKey; -// SurveyMessageCommandType commandType; // }; -type SurveyRequestMessage struct { - SurveyorPeerId NodeId - SurveyedPeerId NodeId - LedgerNum Uint32 - EncryptionKey Curve25519Public - CommandType SurveyMessageCommandType +type TimeSlicedSurveyStartCollectingMessage struct { + SurveyorId NodeId + Nonce Uint32 + LedgerNum Uint32 } // EncodeTo encodes this value using the Encoder. -func (s *SurveyRequestMessage) EncodeTo(e *xdr.Encoder) error { +func (s *TimeSlicedSurveyStartCollectingMessage) EncodeTo(e *xdr.Encoder) error { var err error - if err = s.SurveyorPeerId.EncodeTo(e); err != nil { + if err = s.SurveyorId.EncodeTo(e); err != nil { return err } - if err = s.SurveyedPeerId.EncodeTo(e); err != nil { + if err = s.Nonce.EncodeTo(e); err != nil { return err } if err = s.LedgerNum.EncodeTo(e); err != nil { return err } - if err = s.EncryptionKey.EncodeTo(e); err != nil { - return err - } - if err = s.CommandType.EncodeTo(e); err != nil { - return err - } return nil } -var _ decoderFrom = (*SurveyRequestMessage)(nil) +var _ decoderFrom = (*TimeSlicedSurveyStartCollectingMessage)(nil) // DecodeFrom decodes this value using the Decoder. -func (s *SurveyRequestMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { +func (s *TimeSlicedSurveyStartCollectingMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { if maxDepth == 0 { - return 0, fmt.Errorf("decoding SurveyRequestMessage: %w", ErrMaxDecodingDepthReached) + return 0, fmt.Errorf("decoding TimeSlicedSurveyStartCollectingMessage: %w", ErrMaxDecodingDepthReached) } maxDepth -= 1 var err error var n, nTmp int - nTmp, err = s.SurveyorPeerId.DecodeFrom(d, maxDepth) + nTmp, err = s.SurveyorId.DecodeFrom(d, maxDepth) n += nTmp if err != nil { return n, fmt.Errorf("decoding NodeId: %w", err) } - nTmp, err = s.SurveyedPeerId.DecodeFrom(d, maxDepth) + nTmp, err = s.Nonce.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding NodeId: %w", err) + return n, fmt.Errorf("decoding Uint32: %w", err) } nTmp, err = s.LedgerNum.DecodeFrom(d, maxDepth) n += nTmp if err != nil { return n, fmt.Errorf("decoding Uint32: %w", err) } - nTmp, err = s.EncryptionKey.DecodeFrom(d, maxDepth) + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s TimeSlicedSurveyStartCollectingMessage) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *TimeSlicedSurveyStartCollectingMessage) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*TimeSlicedSurveyStartCollectingMessage)(nil) + _ encoding.BinaryUnmarshaler = (*TimeSlicedSurveyStartCollectingMessage)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s TimeSlicedSurveyStartCollectingMessage) xdrType() {} + +var _ xdrType = (*TimeSlicedSurveyStartCollectingMessage)(nil) + +// SignedTimeSlicedSurveyStartCollectingMessage is an XDR Struct defines as: +// +// struct SignedTimeSlicedSurveyStartCollectingMessage +// { +// Signature signature; +// TimeSlicedSurveyStartCollectingMessage startCollecting; +// }; +type SignedTimeSlicedSurveyStartCollectingMessage struct { + Signature Signature + StartCollecting TimeSlicedSurveyStartCollectingMessage +} + +// EncodeTo encodes this value using the Encoder. +func (s *SignedTimeSlicedSurveyStartCollectingMessage) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.Signature.EncodeTo(e); err != nil { + return err + } + if err = s.StartCollecting.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*SignedTimeSlicedSurveyStartCollectingMessage)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *SignedTimeSlicedSurveyStartCollectingMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding SignedTimeSlicedSurveyStartCollectingMessage: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.Signature.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding Curve25519Public: %w", err) + return n, fmt.Errorf("decoding Signature: %w", err) } - nTmp, err = s.CommandType.DecodeFrom(d, maxDepth) + nTmp, err = s.StartCollecting.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding SurveyMessageCommandType: %w", err) + return n, fmt.Errorf("decoding TimeSlicedSurveyStartCollectingMessage: %w", err) } return n, nil } // MarshalBinary implements encoding.BinaryMarshaler. -func (s SurveyRequestMessage) MarshalBinary() ([]byte, error) { +func (s SignedTimeSlicedSurveyStartCollectingMessage) MarshalBinary() ([]byte, error) { b := bytes.Buffer{} e := xdr.NewEncoder(&b) err := s.EncodeTo(e) @@ -20192,7 +20346,7 @@ func (s SurveyRequestMessage) MarshalBinary() ([]byte, error) { } // UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *SurveyRequestMessage) UnmarshalBinary(inp []byte) error { +func (s *SignedTimeSlicedSurveyStartCollectingMessage) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) o := xdr.DefaultDecodeOptions o.MaxInputLen = len(inp) @@ -20202,64 +20356,74 @@ func (s *SurveyRequestMessage) UnmarshalBinary(inp []byte) error { } var ( - _ encoding.BinaryMarshaler = (*SurveyRequestMessage)(nil) - _ encoding.BinaryUnmarshaler = (*SurveyRequestMessage)(nil) + _ encoding.BinaryMarshaler = (*SignedTimeSlicedSurveyStartCollectingMessage)(nil) + _ encoding.BinaryUnmarshaler = (*SignedTimeSlicedSurveyStartCollectingMessage)(nil) ) // xdrType signals that this type represents XDR values defined by this package. -func (s SurveyRequestMessage) xdrType() {} +func (s SignedTimeSlicedSurveyStartCollectingMessage) xdrType() {} -var _ xdrType = (*SurveyRequestMessage)(nil) +var _ xdrType = (*SignedTimeSlicedSurveyStartCollectingMessage)(nil) -// SignedSurveyRequestMessage is an XDR Struct defines as: +// TimeSlicedSurveyStopCollectingMessage is an XDR Struct defines as: // -// struct SignedSurveyRequestMessage +// struct TimeSlicedSurveyStopCollectingMessage // { -// Signature requestSignature; -// SurveyRequestMessage request; +// NodeID surveyorID; +// uint32 nonce; +// uint32 ledgerNum; // }; -type SignedSurveyRequestMessage struct { - RequestSignature Signature - Request SurveyRequestMessage +type TimeSlicedSurveyStopCollectingMessage struct { + SurveyorId NodeId + Nonce Uint32 + LedgerNum Uint32 } // EncodeTo encodes this value using the Encoder. -func (s *SignedSurveyRequestMessage) EncodeTo(e *xdr.Encoder) error { +func (s *TimeSlicedSurveyStopCollectingMessage) EncodeTo(e *xdr.Encoder) error { var err error - if err = s.RequestSignature.EncodeTo(e); err != nil { + if err = s.SurveyorId.EncodeTo(e); err != nil { return err } - if err = s.Request.EncodeTo(e); err != nil { + if err = s.Nonce.EncodeTo(e); err != nil { + return err + } + if err = s.LedgerNum.EncodeTo(e); err != nil { return err } return nil } -var _ decoderFrom = (*SignedSurveyRequestMessage)(nil) +var _ decoderFrom = (*TimeSlicedSurveyStopCollectingMessage)(nil) // DecodeFrom decodes this value using the Decoder. -func (s *SignedSurveyRequestMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { +func (s *TimeSlicedSurveyStopCollectingMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { if maxDepth == 0 { - return 0, fmt.Errorf("decoding SignedSurveyRequestMessage: %w", ErrMaxDecodingDepthReached) + return 0, fmt.Errorf("decoding TimeSlicedSurveyStopCollectingMessage: %w", ErrMaxDecodingDepthReached) } maxDepth -= 1 var err error var n, nTmp int - nTmp, err = s.RequestSignature.DecodeFrom(d, maxDepth) + nTmp, err = s.SurveyorId.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding Signature: %w", err) + return n, fmt.Errorf("decoding NodeId: %w", err) } - nTmp, err = s.Request.DecodeFrom(d, maxDepth) + nTmp, err = s.Nonce.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding SurveyRequestMessage: %w", err) + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.LedgerNum.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) } return n, nil } // MarshalBinary implements encoding.BinaryMarshaler. -func (s SignedSurveyRequestMessage) MarshalBinary() ([]byte, error) { +func (s TimeSlicedSurveyStopCollectingMessage) MarshalBinary() ([]byte, error) { b := bytes.Buffer{} e := xdr.NewEncoder(&b) err := s.EncodeTo(e) @@ -20267,7 +20431,7 @@ func (s SignedSurveyRequestMessage) MarshalBinary() ([]byte, error) { } // UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *SignedSurveyRequestMessage) UnmarshalBinary(inp []byte) error { +func (s *TimeSlicedSurveyStopCollectingMessage) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) o := xdr.DefaultDecodeOptions o.MaxInputLen = len(inp) @@ -20277,54 +20441,64 @@ func (s *SignedSurveyRequestMessage) UnmarshalBinary(inp []byte) error { } var ( - _ encoding.BinaryMarshaler = (*SignedSurveyRequestMessage)(nil) - _ encoding.BinaryUnmarshaler = (*SignedSurveyRequestMessage)(nil) + _ encoding.BinaryMarshaler = (*TimeSlicedSurveyStopCollectingMessage)(nil) + _ encoding.BinaryUnmarshaler = (*TimeSlicedSurveyStopCollectingMessage)(nil) ) // xdrType signals that this type represents XDR values defined by this package. -func (s SignedSurveyRequestMessage) xdrType() {} +func (s TimeSlicedSurveyStopCollectingMessage) xdrType() {} -var _ xdrType = (*SignedSurveyRequestMessage)(nil) +var _ xdrType = (*TimeSlicedSurveyStopCollectingMessage)(nil) -// EncryptedBody is an XDR Typedef defines as: +// SignedTimeSlicedSurveyStopCollectingMessage is an XDR Struct defines as: // -// typedef opaque EncryptedBody<64000>; -type EncryptedBody []byte - -// XDRMaxSize implements the Sized interface for EncryptedBody -func (e EncryptedBody) XDRMaxSize() int { - return 64000 +// struct SignedTimeSlicedSurveyStopCollectingMessage +// { +// Signature signature; +// TimeSlicedSurveyStopCollectingMessage stopCollecting; +// }; +type SignedTimeSlicedSurveyStopCollectingMessage struct { + Signature Signature + StopCollecting TimeSlicedSurveyStopCollectingMessage } // EncodeTo encodes this value using the Encoder. -func (s EncryptedBody) EncodeTo(e *xdr.Encoder) error { +func (s *SignedTimeSlicedSurveyStopCollectingMessage) EncodeTo(e *xdr.Encoder) error { var err error - if _, err = e.EncodeOpaque(s[:]); err != nil { + if err = s.Signature.EncodeTo(e); err != nil { + return err + } + if err = s.StopCollecting.EncodeTo(e); err != nil { return err } return nil } -var _ decoderFrom = (*EncryptedBody)(nil) +var _ decoderFrom = (*SignedTimeSlicedSurveyStopCollectingMessage)(nil) // DecodeFrom decodes this value using the Decoder. -func (s *EncryptedBody) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { +func (s *SignedTimeSlicedSurveyStopCollectingMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { if maxDepth == 0 { - return 0, fmt.Errorf("decoding EncryptedBody: %w", ErrMaxDecodingDepthReached) + return 0, fmt.Errorf("decoding SignedTimeSlicedSurveyStopCollectingMessage: %w", ErrMaxDecodingDepthReached) } maxDepth -= 1 var err error var n, nTmp int - (*s), nTmp, err = d.DecodeOpaque(64000) + nTmp, err = s.Signature.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding EncryptedBody: %w", err) + return n, fmt.Errorf("decoding Signature: %w", err) + } + nTmp, err = s.StopCollecting.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding TimeSlicedSurveyStopCollectingMessage: %w", err) } return n, nil } // MarshalBinary implements encoding.BinaryMarshaler. -func (s EncryptedBody) MarshalBinary() ([]byte, error) { +func (s SignedTimeSlicedSurveyStopCollectingMessage) MarshalBinary() ([]byte, error) { b := bytes.Buffer{} e := xdr.NewEncoder(&b) err := s.EncodeTo(e) @@ -20332,7 +20506,7 @@ func (s EncryptedBody) MarshalBinary() ([]byte, error) { } // UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *EncryptedBody) UnmarshalBinary(inp []byte) error { +func (s *SignedTimeSlicedSurveyStopCollectingMessage) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) o := xdr.DefaultDecodeOptions o.MaxInputLen = len(inp) @@ -20342,35 +20516,35 @@ func (s *EncryptedBody) UnmarshalBinary(inp []byte) error { } var ( - _ encoding.BinaryMarshaler = (*EncryptedBody)(nil) - _ encoding.BinaryUnmarshaler = (*EncryptedBody)(nil) + _ encoding.BinaryMarshaler = (*SignedTimeSlicedSurveyStopCollectingMessage)(nil) + _ encoding.BinaryUnmarshaler = (*SignedTimeSlicedSurveyStopCollectingMessage)(nil) ) // xdrType signals that this type represents XDR values defined by this package. -func (s EncryptedBody) xdrType() {} +func (s SignedTimeSlicedSurveyStopCollectingMessage) xdrType() {} -var _ xdrType = (*EncryptedBody)(nil) +var _ xdrType = (*SignedTimeSlicedSurveyStopCollectingMessage)(nil) -// SurveyResponseMessage is an XDR Struct defines as: +// SurveyRequestMessage is an XDR Struct defines as: // -// struct SurveyResponseMessage +// struct SurveyRequestMessage // { // NodeID surveyorPeerID; // NodeID surveyedPeerID; // uint32 ledgerNum; +// Curve25519Public encryptionKey; // SurveyMessageCommandType commandType; -// EncryptedBody encryptedBody; // }; -type SurveyResponseMessage struct { +type SurveyRequestMessage struct { SurveyorPeerId NodeId SurveyedPeerId NodeId LedgerNum Uint32 + EncryptionKey Curve25519Public CommandType SurveyMessageCommandType - EncryptedBody EncryptedBody } // EncodeTo encodes this value using the Encoder. -func (s *SurveyResponseMessage) EncodeTo(e *xdr.Encoder) error { +func (s *SurveyRequestMessage) EncodeTo(e *xdr.Encoder) error { var err error if err = s.SurveyorPeerId.EncodeTo(e); err != nil { return err @@ -20381,21 +20555,21 @@ func (s *SurveyResponseMessage) EncodeTo(e *xdr.Encoder) error { if err = s.LedgerNum.EncodeTo(e); err != nil { return err } - if err = s.CommandType.EncodeTo(e); err != nil { + if err = s.EncryptionKey.EncodeTo(e); err != nil { return err } - if err = s.EncryptedBody.EncodeTo(e); err != nil { + if err = s.CommandType.EncodeTo(e); err != nil { return err } return nil } -var _ decoderFrom = (*SurveyResponseMessage)(nil) +var _ decoderFrom = (*SurveyRequestMessage)(nil) // DecodeFrom decodes this value using the Decoder. -func (s *SurveyResponseMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { +func (s *SurveyRequestMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { if maxDepth == 0 { - return 0, fmt.Errorf("decoding SurveyResponseMessage: %w", ErrMaxDecodingDepthReached) + return 0, fmt.Errorf("decoding SurveyRequestMessage: %w", ErrMaxDecodingDepthReached) } maxDepth -= 1 var err error @@ -20415,21 +20589,436 @@ func (s *SurveyResponseMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, if err != nil { return n, fmt.Errorf("decoding Uint32: %w", err) } - nTmp, err = s.CommandType.DecodeFrom(d, maxDepth) + nTmp, err = s.EncryptionKey.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding SurveyMessageCommandType: %w", err) + return n, fmt.Errorf("decoding Curve25519Public: %w", err) } - nTmp, err = s.EncryptedBody.DecodeFrom(d, maxDepth) + nTmp, err = s.CommandType.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding EncryptedBody: %w", err) + return n, fmt.Errorf("decoding SurveyMessageCommandType: %w", err) } return n, nil } // MarshalBinary implements encoding.BinaryMarshaler. -func (s SurveyResponseMessage) MarshalBinary() ([]byte, error) { +func (s SurveyRequestMessage) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *SurveyRequestMessage) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*SurveyRequestMessage)(nil) + _ encoding.BinaryUnmarshaler = (*SurveyRequestMessage)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s SurveyRequestMessage) xdrType() {} + +var _ xdrType = (*SurveyRequestMessage)(nil) + +// TimeSlicedSurveyRequestMessage is an XDR Struct defines as: +// +// struct TimeSlicedSurveyRequestMessage +// { +// SurveyRequestMessage request; +// uint32 nonce; +// uint32 inboundPeersIndex; +// uint32 outboundPeersIndex; +// }; +type TimeSlicedSurveyRequestMessage struct { + Request SurveyRequestMessage + Nonce Uint32 + InboundPeersIndex Uint32 + OutboundPeersIndex Uint32 +} + +// EncodeTo encodes this value using the Encoder. +func (s *TimeSlicedSurveyRequestMessage) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.Request.EncodeTo(e); err != nil { + return err + } + if err = s.Nonce.EncodeTo(e); err != nil { + return err + } + if err = s.InboundPeersIndex.EncodeTo(e); err != nil { + return err + } + if err = s.OutboundPeersIndex.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*TimeSlicedSurveyRequestMessage)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *TimeSlicedSurveyRequestMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding TimeSlicedSurveyRequestMessage: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.Request.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding SurveyRequestMessage: %w", err) + } + nTmp, err = s.Nonce.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.InboundPeersIndex.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.OutboundPeersIndex.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s TimeSlicedSurveyRequestMessage) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *TimeSlicedSurveyRequestMessage) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*TimeSlicedSurveyRequestMessage)(nil) + _ encoding.BinaryUnmarshaler = (*TimeSlicedSurveyRequestMessage)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s TimeSlicedSurveyRequestMessage) xdrType() {} + +var _ xdrType = (*TimeSlicedSurveyRequestMessage)(nil) + +// SignedSurveyRequestMessage is an XDR Struct defines as: +// +// struct SignedSurveyRequestMessage +// { +// Signature requestSignature; +// SurveyRequestMessage request; +// }; +type SignedSurveyRequestMessage struct { + RequestSignature Signature + Request SurveyRequestMessage +} + +// EncodeTo encodes this value using the Encoder. +func (s *SignedSurveyRequestMessage) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.RequestSignature.EncodeTo(e); err != nil { + return err + } + if err = s.Request.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*SignedSurveyRequestMessage)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *SignedSurveyRequestMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding SignedSurveyRequestMessage: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.RequestSignature.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Signature: %w", err) + } + nTmp, err = s.Request.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding SurveyRequestMessage: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s SignedSurveyRequestMessage) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *SignedSurveyRequestMessage) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*SignedSurveyRequestMessage)(nil) + _ encoding.BinaryUnmarshaler = (*SignedSurveyRequestMessage)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s SignedSurveyRequestMessage) xdrType() {} + +var _ xdrType = (*SignedSurveyRequestMessage)(nil) + +// SignedTimeSlicedSurveyRequestMessage is an XDR Struct defines as: +// +// struct SignedTimeSlicedSurveyRequestMessage +// { +// Signature requestSignature; +// TimeSlicedSurveyRequestMessage request; +// }; +type SignedTimeSlicedSurveyRequestMessage struct { + RequestSignature Signature + Request TimeSlicedSurveyRequestMessage +} + +// EncodeTo encodes this value using the Encoder. +func (s *SignedTimeSlicedSurveyRequestMessage) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.RequestSignature.EncodeTo(e); err != nil { + return err + } + if err = s.Request.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*SignedTimeSlicedSurveyRequestMessage)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *SignedTimeSlicedSurveyRequestMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding SignedTimeSlicedSurveyRequestMessage: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.RequestSignature.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Signature: %w", err) + } + nTmp, err = s.Request.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding TimeSlicedSurveyRequestMessage: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s SignedTimeSlicedSurveyRequestMessage) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *SignedTimeSlicedSurveyRequestMessage) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*SignedTimeSlicedSurveyRequestMessage)(nil) + _ encoding.BinaryUnmarshaler = (*SignedTimeSlicedSurveyRequestMessage)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s SignedTimeSlicedSurveyRequestMessage) xdrType() {} + +var _ xdrType = (*SignedTimeSlicedSurveyRequestMessage)(nil) + +// EncryptedBody is an XDR Typedef defines as: +// +// typedef opaque EncryptedBody<64000>; +type EncryptedBody []byte + +// XDRMaxSize implements the Sized interface for EncryptedBody +func (e EncryptedBody) XDRMaxSize() int { + return 64000 +} + +// EncodeTo encodes this value using the Encoder. +func (s EncryptedBody) EncodeTo(e *xdr.Encoder) error { + var err error + if _, err = e.EncodeOpaque(s[:]); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*EncryptedBody)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *EncryptedBody) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding EncryptedBody: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + (*s), nTmp, err = d.DecodeOpaque(64000) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding EncryptedBody: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s EncryptedBody) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *EncryptedBody) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*EncryptedBody)(nil) + _ encoding.BinaryUnmarshaler = (*EncryptedBody)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s EncryptedBody) xdrType() {} + +var _ xdrType = (*EncryptedBody)(nil) + +// SurveyResponseMessage is an XDR Struct defines as: +// +// struct SurveyResponseMessage +// { +// NodeID surveyorPeerID; +// NodeID surveyedPeerID; +// uint32 ledgerNum; +// SurveyMessageCommandType commandType; +// EncryptedBody encryptedBody; +// }; +type SurveyResponseMessage struct { + SurveyorPeerId NodeId + SurveyedPeerId NodeId + LedgerNum Uint32 + CommandType SurveyMessageCommandType + EncryptedBody EncryptedBody +} + +// EncodeTo encodes this value using the Encoder. +func (s *SurveyResponseMessage) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.SurveyorPeerId.EncodeTo(e); err != nil { + return err + } + if err = s.SurveyedPeerId.EncodeTo(e); err != nil { + return err + } + if err = s.LedgerNum.EncodeTo(e); err != nil { + return err + } + if err = s.CommandType.EncodeTo(e); err != nil { + return err + } + if err = s.EncryptedBody.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*SurveyResponseMessage)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *SurveyResponseMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding SurveyResponseMessage: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.SurveyorPeerId.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding NodeId: %w", err) + } + nTmp, err = s.SurveyedPeerId.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding NodeId: %w", err) + } + nTmp, err = s.LedgerNum.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + nTmp, err = s.CommandType.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding SurveyMessageCommandType: %w", err) + } + nTmp, err = s.EncryptedBody.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding EncryptedBody: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s SurveyResponseMessage) MarshalBinary() ([]byte, error) { b := bytes.Buffer{} e := xdr.NewEncoder(&b) err := s.EncodeTo(e) @@ -20447,64 +21036,509 @@ func (s *SurveyResponseMessage) UnmarshalBinary(inp []byte) error { } var ( - _ encoding.BinaryMarshaler = (*SurveyResponseMessage)(nil) - _ encoding.BinaryUnmarshaler = (*SurveyResponseMessage)(nil) + _ encoding.BinaryMarshaler = (*SurveyResponseMessage)(nil) + _ encoding.BinaryUnmarshaler = (*SurveyResponseMessage)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s SurveyResponseMessage) xdrType() {} + +var _ xdrType = (*SurveyResponseMessage)(nil) + +// TimeSlicedSurveyResponseMessage is an XDR Struct defines as: +// +// struct TimeSlicedSurveyResponseMessage +// { +// SurveyResponseMessage response; +// uint32 nonce; +// }; +type TimeSlicedSurveyResponseMessage struct { + Response SurveyResponseMessage + Nonce Uint32 +} + +// EncodeTo encodes this value using the Encoder. +func (s *TimeSlicedSurveyResponseMessage) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.Response.EncodeTo(e); err != nil { + return err + } + if err = s.Nonce.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*TimeSlicedSurveyResponseMessage)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *TimeSlicedSurveyResponseMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding TimeSlicedSurveyResponseMessage: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.Response.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding SurveyResponseMessage: %w", err) + } + nTmp, err = s.Nonce.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint32: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s TimeSlicedSurveyResponseMessage) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *TimeSlicedSurveyResponseMessage) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*TimeSlicedSurveyResponseMessage)(nil) + _ encoding.BinaryUnmarshaler = (*TimeSlicedSurveyResponseMessage)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s TimeSlicedSurveyResponseMessage) xdrType() {} + +var _ xdrType = (*TimeSlicedSurveyResponseMessage)(nil) + +// SignedSurveyResponseMessage is an XDR Struct defines as: +// +// struct SignedSurveyResponseMessage +// { +// Signature responseSignature; +// SurveyResponseMessage response; +// }; +type SignedSurveyResponseMessage struct { + ResponseSignature Signature + Response SurveyResponseMessage +} + +// EncodeTo encodes this value using the Encoder. +func (s *SignedSurveyResponseMessage) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.ResponseSignature.EncodeTo(e); err != nil { + return err + } + if err = s.Response.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*SignedSurveyResponseMessage)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *SignedSurveyResponseMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding SignedSurveyResponseMessage: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.ResponseSignature.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Signature: %w", err) + } + nTmp, err = s.Response.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding SurveyResponseMessage: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s SignedSurveyResponseMessage) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *SignedSurveyResponseMessage) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*SignedSurveyResponseMessage)(nil) + _ encoding.BinaryUnmarshaler = (*SignedSurveyResponseMessage)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s SignedSurveyResponseMessage) xdrType() {} + +var _ xdrType = (*SignedSurveyResponseMessage)(nil) + +// SignedTimeSlicedSurveyResponseMessage is an XDR Struct defines as: +// +// struct SignedTimeSlicedSurveyResponseMessage +// { +// Signature responseSignature; +// TimeSlicedSurveyResponseMessage response; +// }; +type SignedTimeSlicedSurveyResponseMessage struct { + ResponseSignature Signature + Response TimeSlicedSurveyResponseMessage +} + +// EncodeTo encodes this value using the Encoder. +func (s *SignedTimeSlicedSurveyResponseMessage) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.ResponseSignature.EncodeTo(e); err != nil { + return err + } + if err = s.Response.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*SignedTimeSlicedSurveyResponseMessage)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *SignedTimeSlicedSurveyResponseMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding SignedTimeSlicedSurveyResponseMessage: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.ResponseSignature.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Signature: %w", err) + } + nTmp, err = s.Response.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding TimeSlicedSurveyResponseMessage: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s SignedTimeSlicedSurveyResponseMessage) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *SignedTimeSlicedSurveyResponseMessage) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*SignedTimeSlicedSurveyResponseMessage)(nil) + _ encoding.BinaryUnmarshaler = (*SignedTimeSlicedSurveyResponseMessage)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s SignedTimeSlicedSurveyResponseMessage) xdrType() {} + +var _ xdrType = (*SignedTimeSlicedSurveyResponseMessage)(nil) + +// PeerStats is an XDR Struct defines as: +// +// struct PeerStats +// { +// NodeID id; +// string versionStr<100>; +// uint64 messagesRead; +// uint64 messagesWritten; +// uint64 bytesRead; +// uint64 bytesWritten; +// uint64 secondsConnected; +// +// uint64 uniqueFloodBytesRecv; +// uint64 duplicateFloodBytesRecv; +// uint64 uniqueFetchBytesRecv; +// uint64 duplicateFetchBytesRecv; +// +// uint64 uniqueFloodMessageRecv; +// uint64 duplicateFloodMessageRecv; +// uint64 uniqueFetchMessageRecv; +// uint64 duplicateFetchMessageRecv; +// }; +type PeerStats struct { + Id NodeId + VersionStr string `xdrmaxsize:"100"` + MessagesRead Uint64 + MessagesWritten Uint64 + BytesRead Uint64 + BytesWritten Uint64 + SecondsConnected Uint64 + UniqueFloodBytesRecv Uint64 + DuplicateFloodBytesRecv Uint64 + UniqueFetchBytesRecv Uint64 + DuplicateFetchBytesRecv Uint64 + UniqueFloodMessageRecv Uint64 + DuplicateFloodMessageRecv Uint64 + UniqueFetchMessageRecv Uint64 + DuplicateFetchMessageRecv Uint64 +} + +// EncodeTo encodes this value using the Encoder. +func (s *PeerStats) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.Id.EncodeTo(e); err != nil { + return err + } + if _, err = e.EncodeString(string(s.VersionStr)); err != nil { + return err + } + if err = s.MessagesRead.EncodeTo(e); err != nil { + return err + } + if err = s.MessagesWritten.EncodeTo(e); err != nil { + return err + } + if err = s.BytesRead.EncodeTo(e); err != nil { + return err + } + if err = s.BytesWritten.EncodeTo(e); err != nil { + return err + } + if err = s.SecondsConnected.EncodeTo(e); err != nil { + return err + } + if err = s.UniqueFloodBytesRecv.EncodeTo(e); err != nil { + return err + } + if err = s.DuplicateFloodBytesRecv.EncodeTo(e); err != nil { + return err + } + if err = s.UniqueFetchBytesRecv.EncodeTo(e); err != nil { + return err + } + if err = s.DuplicateFetchBytesRecv.EncodeTo(e); err != nil { + return err + } + if err = s.UniqueFloodMessageRecv.EncodeTo(e); err != nil { + return err + } + if err = s.DuplicateFloodMessageRecv.EncodeTo(e); err != nil { + return err + } + if err = s.UniqueFetchMessageRecv.EncodeTo(e); err != nil { + return err + } + if err = s.DuplicateFetchMessageRecv.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*PeerStats)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *PeerStats) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding PeerStats: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.Id.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding NodeId: %w", err) + } + s.VersionStr, nTmp, err = d.DecodeString(100) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding VersionStr: %w", err) + } + nTmp, err = s.MessagesRead.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint64: %w", err) + } + nTmp, err = s.MessagesWritten.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint64: %w", err) + } + nTmp, err = s.BytesRead.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint64: %w", err) + } + nTmp, err = s.BytesWritten.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint64: %w", err) + } + nTmp, err = s.SecondsConnected.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint64: %w", err) + } + nTmp, err = s.UniqueFloodBytesRecv.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint64: %w", err) + } + nTmp, err = s.DuplicateFloodBytesRecv.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint64: %w", err) + } + nTmp, err = s.UniqueFetchBytesRecv.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint64: %w", err) + } + nTmp, err = s.DuplicateFetchBytesRecv.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint64: %w", err) + } + nTmp, err = s.UniqueFloodMessageRecv.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint64: %w", err) + } + nTmp, err = s.DuplicateFloodMessageRecv.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint64: %w", err) + } + nTmp, err = s.UniqueFetchMessageRecv.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint64: %w", err) + } + nTmp, err = s.DuplicateFetchMessageRecv.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding Uint64: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s PeerStats) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *PeerStats) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*PeerStats)(nil) + _ encoding.BinaryUnmarshaler = (*PeerStats)(nil) ) // xdrType signals that this type represents XDR values defined by this package. -func (s SurveyResponseMessage) xdrType() {} +func (s PeerStats) xdrType() {} -var _ xdrType = (*SurveyResponseMessage)(nil) +var _ xdrType = (*PeerStats)(nil) -// SignedSurveyResponseMessage is an XDR Struct defines as: +// PeerStatList is an XDR Typedef defines as: // -// struct SignedSurveyResponseMessage -// { -// Signature responseSignature; -// SurveyResponseMessage response; -// }; -type SignedSurveyResponseMessage struct { - ResponseSignature Signature - Response SurveyResponseMessage +// typedef PeerStats PeerStatList<25>; +type PeerStatList []PeerStats + +// XDRMaxSize implements the Sized interface for PeerStatList +func (e PeerStatList) XDRMaxSize() int { + return 25 } // EncodeTo encodes this value using the Encoder. -func (s *SignedSurveyResponseMessage) EncodeTo(e *xdr.Encoder) error { +func (s PeerStatList) EncodeTo(e *xdr.Encoder) error { var err error - if err = s.ResponseSignature.EncodeTo(e); err != nil { + if _, err = e.EncodeUint(uint32(len(s))); err != nil { return err } - if err = s.Response.EncodeTo(e); err != nil { - return err + for i := 0; i < len(s); i++ { + if err = s[i].EncodeTo(e); err != nil { + return err + } } return nil } -var _ decoderFrom = (*SignedSurveyResponseMessage)(nil) +var _ decoderFrom = (*PeerStatList)(nil) // DecodeFrom decodes this value using the Decoder. -func (s *SignedSurveyResponseMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { +func (s *PeerStatList) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { if maxDepth == 0 { - return 0, fmt.Errorf("decoding SignedSurveyResponseMessage: %w", ErrMaxDecodingDepthReached) + return 0, fmt.Errorf("decoding PeerStatList: %w", ErrMaxDecodingDepthReached) } maxDepth -= 1 var err error var n, nTmp int - nTmp, err = s.ResponseSignature.DecodeFrom(d, maxDepth) + var l uint32 + l, nTmp, err = d.DecodeUint() n += nTmp if err != nil { - return n, fmt.Errorf("decoding Signature: %w", err) + return n, fmt.Errorf("decoding PeerStats: %w", err) } - nTmp, err = s.Response.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding SurveyResponseMessage: %w", err) + if l > 25 { + return n, fmt.Errorf("decoding PeerStats: data size (%d) exceeds size limit (25)", l) + } + (*s) = nil + if l > 0 { + if il, ok := d.InputLen(); ok && uint(il) < uint(l) { + return n, fmt.Errorf("decoding PeerStats: length (%d) exceeds remaining input length (%d)", l, il) + } + (*s) = make([]PeerStats, l) + for i := uint32(0); i < l; i++ { + nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding PeerStats: %w", err) + } + } } return n, nil } // MarshalBinary implements encoding.BinaryMarshaler. -func (s SignedSurveyResponseMessage) MarshalBinary() ([]byte, error) { +func (s PeerStatList) MarshalBinary() ([]byte, error) { b := bytes.Buffer{} e := xdr.NewEncoder(&b) err := s.EncodeTo(e) @@ -20512,7 +21546,7 @@ func (s SignedSurveyResponseMessage) MarshalBinary() ([]byte, error) { } // UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *SignedSurveyResponseMessage) UnmarshalBinary(inp []byte) error { +func (s *PeerStatList) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) o := xdr.DefaultDecodeOptions o.MaxInputLen = len(inp) @@ -20522,196 +21556,225 @@ func (s *SignedSurveyResponseMessage) UnmarshalBinary(inp []byte) error { } var ( - _ encoding.BinaryMarshaler = (*SignedSurveyResponseMessage)(nil) - _ encoding.BinaryUnmarshaler = (*SignedSurveyResponseMessage)(nil) + _ encoding.BinaryMarshaler = (*PeerStatList)(nil) + _ encoding.BinaryUnmarshaler = (*PeerStatList)(nil) ) // xdrType signals that this type represents XDR values defined by this package. -func (s SignedSurveyResponseMessage) xdrType() {} +func (s PeerStatList) xdrType() {} -var _ xdrType = (*SignedSurveyResponseMessage)(nil) +var _ xdrType = (*PeerStatList)(nil) -// PeerStats is an XDR Struct defines as: +// TimeSlicedNodeData is an XDR Struct defines as: // -// struct PeerStats +// struct TimeSlicedNodeData // { -// NodeID id; -// string versionStr<100>; -// uint64 messagesRead; -// uint64 messagesWritten; -// uint64 bytesRead; -// uint64 bytesWritten; -// uint64 secondsConnected; +// uint32 addedAuthenticatedPeers; +// uint32 droppedAuthenticatedPeers; +// uint32 totalInboundPeerCount; +// uint32 totalOutboundPeerCount; // -// uint64 uniqueFloodBytesRecv; -// uint64 duplicateFloodBytesRecv; -// uint64 uniqueFetchBytesRecv; -// uint64 duplicateFetchBytesRecv; +// // SCP stats +// uint32 p75SCPFirstToSelfLatencyMs; +// uint32 p75SCPSelfToOtherLatencyMs; // -// uint64 uniqueFloodMessageRecv; -// uint64 duplicateFloodMessageRecv; -// uint64 uniqueFetchMessageRecv; -// uint64 duplicateFetchMessageRecv; +// // How many times the node lost sync in the time slice +// uint32 lostSyncCount; +// +// // Config data +// bool isValidator; +// uint32 maxInboundPeerCount; +// uint32 maxOutboundPeerCount; // }; -type PeerStats struct { - Id NodeId - VersionStr string `xdrmaxsize:"100"` - MessagesRead Uint64 - MessagesWritten Uint64 - BytesRead Uint64 - BytesWritten Uint64 - SecondsConnected Uint64 - UniqueFloodBytesRecv Uint64 - DuplicateFloodBytesRecv Uint64 - UniqueFetchBytesRecv Uint64 - DuplicateFetchBytesRecv Uint64 - UniqueFloodMessageRecv Uint64 - DuplicateFloodMessageRecv Uint64 - UniqueFetchMessageRecv Uint64 - DuplicateFetchMessageRecv Uint64 +type TimeSlicedNodeData struct { + AddedAuthenticatedPeers Uint32 + DroppedAuthenticatedPeers Uint32 + TotalInboundPeerCount Uint32 + TotalOutboundPeerCount Uint32 + P75ScpFirstToSelfLatencyMs Uint32 + P75ScpSelfToOtherLatencyMs Uint32 + LostSyncCount Uint32 + IsValidator bool + MaxInboundPeerCount Uint32 + MaxOutboundPeerCount Uint32 } // EncodeTo encodes this value using the Encoder. -func (s *PeerStats) EncodeTo(e *xdr.Encoder) error { +func (s *TimeSlicedNodeData) EncodeTo(e *xdr.Encoder) error { var err error - if err = s.Id.EncodeTo(e); err != nil { + if err = s.AddedAuthenticatedPeers.EncodeTo(e); err != nil { return err } - if _, err = e.EncodeString(string(s.VersionStr)); err != nil { - return err - } - if err = s.MessagesRead.EncodeTo(e); err != nil { - return err - } - if err = s.MessagesWritten.EncodeTo(e); err != nil { - return err - } - if err = s.BytesRead.EncodeTo(e); err != nil { - return err - } - if err = s.BytesWritten.EncodeTo(e); err != nil { - return err - } - if err = s.SecondsConnected.EncodeTo(e); err != nil { + if err = s.DroppedAuthenticatedPeers.EncodeTo(e); err != nil { return err } - if err = s.UniqueFloodBytesRecv.EncodeTo(e); err != nil { + if err = s.TotalInboundPeerCount.EncodeTo(e); err != nil { return err } - if err = s.DuplicateFloodBytesRecv.EncodeTo(e); err != nil { + if err = s.TotalOutboundPeerCount.EncodeTo(e); err != nil { return err } - if err = s.UniqueFetchBytesRecv.EncodeTo(e); err != nil { + if err = s.P75ScpFirstToSelfLatencyMs.EncodeTo(e); err != nil { return err } - if err = s.DuplicateFetchBytesRecv.EncodeTo(e); err != nil { + if err = s.P75ScpSelfToOtherLatencyMs.EncodeTo(e); err != nil { return err } - if err = s.UniqueFloodMessageRecv.EncodeTo(e); err != nil { + if err = s.LostSyncCount.EncodeTo(e); err != nil { return err } - if err = s.DuplicateFloodMessageRecv.EncodeTo(e); err != nil { + if _, err = e.EncodeBool(bool(s.IsValidator)); err != nil { return err } - if err = s.UniqueFetchMessageRecv.EncodeTo(e); err != nil { + if err = s.MaxInboundPeerCount.EncodeTo(e); err != nil { return err } - if err = s.DuplicateFetchMessageRecv.EncodeTo(e); err != nil { + if err = s.MaxOutboundPeerCount.EncodeTo(e); err != nil { return err } return nil } -var _ decoderFrom = (*PeerStats)(nil) +var _ decoderFrom = (*TimeSlicedNodeData)(nil) // DecodeFrom decodes this value using the Decoder. -func (s *PeerStats) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { +func (s *TimeSlicedNodeData) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { if maxDepth == 0 { - return 0, fmt.Errorf("decoding PeerStats: %w", ErrMaxDecodingDepthReached) + return 0, fmt.Errorf("decoding TimeSlicedNodeData: %w", ErrMaxDecodingDepthReached) } maxDepth -= 1 var err error var n, nTmp int - nTmp, err = s.Id.DecodeFrom(d, maxDepth) + nTmp, err = s.AddedAuthenticatedPeers.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding NodeId: %w", err) + return n, fmt.Errorf("decoding Uint32: %w", err) } - s.VersionStr, nTmp, err = d.DecodeString(100) + nTmp, err = s.DroppedAuthenticatedPeers.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding VersionStr: %w", err) + return n, fmt.Errorf("decoding Uint32: %w", err) } - nTmp, err = s.MessagesRead.DecodeFrom(d, maxDepth) + nTmp, err = s.TotalInboundPeerCount.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding Uint64: %w", err) + return n, fmt.Errorf("decoding Uint32: %w", err) } - nTmp, err = s.MessagesWritten.DecodeFrom(d, maxDepth) + nTmp, err = s.TotalOutboundPeerCount.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding Uint64: %w", err) + return n, fmt.Errorf("decoding Uint32: %w", err) } - nTmp, err = s.BytesRead.DecodeFrom(d, maxDepth) + nTmp, err = s.P75ScpFirstToSelfLatencyMs.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding Uint64: %w", err) + return n, fmt.Errorf("decoding Uint32: %w", err) } - nTmp, err = s.BytesWritten.DecodeFrom(d, maxDepth) + nTmp, err = s.P75ScpSelfToOtherLatencyMs.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding Uint64: %w", err) + return n, fmt.Errorf("decoding Uint32: %w", err) } - nTmp, err = s.SecondsConnected.DecodeFrom(d, maxDepth) + nTmp, err = s.LostSyncCount.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding Uint64: %w", err) + return n, fmt.Errorf("decoding Uint32: %w", err) } - nTmp, err = s.UniqueFloodBytesRecv.DecodeFrom(d, maxDepth) + s.IsValidator, nTmp, err = d.DecodeBool() n += nTmp if err != nil { - return n, fmt.Errorf("decoding Uint64: %w", err) + return n, fmt.Errorf("decoding Bool: %w", err) } - nTmp, err = s.DuplicateFloodBytesRecv.DecodeFrom(d, maxDepth) + nTmp, err = s.MaxInboundPeerCount.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding Uint64: %w", err) + return n, fmt.Errorf("decoding Uint32: %w", err) } - nTmp, err = s.UniqueFetchBytesRecv.DecodeFrom(d, maxDepth) + nTmp, err = s.MaxOutboundPeerCount.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding Uint64: %w", err) + return n, fmt.Errorf("decoding Uint32: %w", err) } - nTmp, err = s.DuplicateFetchBytesRecv.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding Uint64: %w", err) + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s TimeSlicedNodeData) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *TimeSlicedNodeData) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*TimeSlicedNodeData)(nil) + _ encoding.BinaryUnmarshaler = (*TimeSlicedNodeData)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s TimeSlicedNodeData) xdrType() {} + +var _ xdrType = (*TimeSlicedNodeData)(nil) + +// TimeSlicedPeerData is an XDR Struct defines as: +// +// struct TimeSlicedPeerData +// { +// PeerStats peerStats; +// uint32 averageLatencyMs; +// }; +type TimeSlicedPeerData struct { + PeerStats PeerStats + AverageLatencyMs Uint32 +} + +// EncodeTo encodes this value using the Encoder. +func (s *TimeSlicedPeerData) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.PeerStats.EncodeTo(e); err != nil { + return err } - nTmp, err = s.UniqueFloodMessageRecv.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding Uint64: %w", err) + if err = s.AverageLatencyMs.EncodeTo(e); err != nil { + return err } - nTmp, err = s.DuplicateFloodMessageRecv.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding Uint64: %w", err) + return nil +} + +var _ decoderFrom = (*TimeSlicedPeerData)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *TimeSlicedPeerData) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding TimeSlicedPeerData: %w", ErrMaxDecodingDepthReached) } - nTmp, err = s.UniqueFetchMessageRecv.DecodeFrom(d, maxDepth) + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.PeerStats.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding Uint64: %w", err) + return n, fmt.Errorf("decoding PeerStats: %w", err) } - nTmp, err = s.DuplicateFetchMessageRecv.DecodeFrom(d, maxDepth) + nTmp, err = s.AverageLatencyMs.DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding Uint64: %w", err) + return n, fmt.Errorf("decoding Uint32: %w", err) } return n, nil } // MarshalBinary implements encoding.BinaryMarshaler. -func (s PeerStats) MarshalBinary() ([]byte, error) { +func (s TimeSlicedPeerData) MarshalBinary() ([]byte, error) { b := bytes.Buffer{} e := xdr.NewEncoder(&b) err := s.EncodeTo(e) @@ -20719,7 +21782,7 @@ func (s PeerStats) MarshalBinary() ([]byte, error) { } // UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *PeerStats) UnmarshalBinary(inp []byte) error { +func (s *TimeSlicedPeerData) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) o := xdr.DefaultDecodeOptions o.MaxInputLen = len(inp) @@ -20729,27 +21792,27 @@ func (s *PeerStats) UnmarshalBinary(inp []byte) error { } var ( - _ encoding.BinaryMarshaler = (*PeerStats)(nil) - _ encoding.BinaryUnmarshaler = (*PeerStats)(nil) + _ encoding.BinaryMarshaler = (*TimeSlicedPeerData)(nil) + _ encoding.BinaryUnmarshaler = (*TimeSlicedPeerData)(nil) ) // xdrType signals that this type represents XDR values defined by this package. -func (s PeerStats) xdrType() {} +func (s TimeSlicedPeerData) xdrType() {} -var _ xdrType = (*PeerStats)(nil) +var _ xdrType = (*TimeSlicedPeerData)(nil) -// PeerStatList is an XDR Typedef defines as: +// TimeSlicedPeerDataList is an XDR Typedef defines as: // -// typedef PeerStats PeerStatList<25>; -type PeerStatList []PeerStats +// typedef TimeSlicedPeerData TimeSlicedPeerDataList<25>; +type TimeSlicedPeerDataList []TimeSlicedPeerData -// XDRMaxSize implements the Sized interface for PeerStatList -func (e PeerStatList) XDRMaxSize() int { +// XDRMaxSize implements the Sized interface for TimeSlicedPeerDataList +func (e TimeSlicedPeerDataList) XDRMaxSize() int { return 25 } // EncodeTo encodes this value using the Encoder. -func (s PeerStatList) EncodeTo(e *xdr.Encoder) error { +func (s TimeSlicedPeerDataList) EncodeTo(e *xdr.Encoder) error { var err error if _, err = e.EncodeUint(uint32(len(s))); err != nil { return err @@ -20762,12 +21825,12 @@ func (s PeerStatList) EncodeTo(e *xdr.Encoder) error { return nil } -var _ decoderFrom = (*PeerStatList)(nil) +var _ decoderFrom = (*TimeSlicedPeerDataList)(nil) // DecodeFrom decodes this value using the Decoder. -func (s *PeerStatList) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { +func (s *TimeSlicedPeerDataList) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { if maxDepth == 0 { - return 0, fmt.Errorf("decoding PeerStatList: %w", ErrMaxDecodingDepthReached) + return 0, fmt.Errorf("decoding TimeSlicedPeerDataList: %w", ErrMaxDecodingDepthReached) } maxDepth -= 1 var err error @@ -20776,22 +21839,22 @@ func (s *PeerStatList) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { l, nTmp, err = d.DecodeUint() n += nTmp if err != nil { - return n, fmt.Errorf("decoding PeerStats: %w", err) + return n, fmt.Errorf("decoding TimeSlicedPeerData: %w", err) } if l > 25 { - return n, fmt.Errorf("decoding PeerStats: data size (%d) exceeds size limit (25)", l) + return n, fmt.Errorf("decoding TimeSlicedPeerData: data size (%d) exceeds size limit (25)", l) } (*s) = nil if l > 0 { if il, ok := d.InputLen(); ok && uint(il) < uint(l) { - return n, fmt.Errorf("decoding PeerStats: length (%d) exceeds remaining input length (%d)", l, il) + return n, fmt.Errorf("decoding TimeSlicedPeerData: length (%d) exceeds remaining input length (%d)", l, il) } - (*s) = make([]PeerStats, l) + (*s) = make([]TimeSlicedPeerData, l) for i := uint32(0); i < l; i++ { nTmp, err = (*s)[i].DecodeFrom(d, maxDepth) n += nTmp if err != nil { - return n, fmt.Errorf("decoding PeerStats: %w", err) + return n, fmt.Errorf("decoding TimeSlicedPeerData: %w", err) } } } @@ -20799,7 +21862,7 @@ func (s *PeerStatList) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { } // MarshalBinary implements encoding.BinaryMarshaler. -func (s PeerStatList) MarshalBinary() ([]byte, error) { +func (s TimeSlicedPeerDataList) MarshalBinary() ([]byte, error) { b := bytes.Buffer{} e := xdr.NewEncoder(&b) err := s.EncodeTo(e) @@ -20807,7 +21870,7 @@ func (s PeerStatList) MarshalBinary() ([]byte, error) { } // UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *PeerStatList) UnmarshalBinary(inp []byte) error { +func (s *TimeSlicedPeerDataList) UnmarshalBinary(inp []byte) error { r := bytes.NewReader(inp) o := xdr.DefaultDecodeOptions o.MaxInputLen = len(inp) @@ -20817,14 +21880,14 @@ func (s *PeerStatList) UnmarshalBinary(inp []byte) error { } var ( - _ encoding.BinaryMarshaler = (*PeerStatList)(nil) - _ encoding.BinaryUnmarshaler = (*PeerStatList)(nil) + _ encoding.BinaryMarshaler = (*TimeSlicedPeerDataList)(nil) + _ encoding.BinaryUnmarshaler = (*TimeSlicedPeerDataList)(nil) ) // xdrType signals that this type represents XDR values defined by this package. -func (s PeerStatList) xdrType() {} +func (s TimeSlicedPeerDataList) xdrType() {} -var _ xdrType = (*PeerStatList)(nil) +var _ xdrType = (*TimeSlicedPeerDataList)(nil) // TopologyResponseBodyV0 is an XDR Struct defines as: // @@ -21039,6 +22102,91 @@ func (s TopologyResponseBodyV1) xdrType() {} var _ xdrType = (*TopologyResponseBodyV1)(nil) +// TopologyResponseBodyV2 is an XDR Struct defines as: +// +// struct TopologyResponseBodyV2 +// { +// TimeSlicedPeerDataList inboundPeers; +// TimeSlicedPeerDataList outboundPeers; +// TimeSlicedNodeData nodeData; +// }; +type TopologyResponseBodyV2 struct { + InboundPeers TimeSlicedPeerDataList + OutboundPeers TimeSlicedPeerDataList + NodeData TimeSlicedNodeData +} + +// EncodeTo encodes this value using the Encoder. +func (s *TopologyResponseBodyV2) EncodeTo(e *xdr.Encoder) error { + var err error + if err = s.InboundPeers.EncodeTo(e); err != nil { + return err + } + if err = s.OutboundPeers.EncodeTo(e); err != nil { + return err + } + if err = s.NodeData.EncodeTo(e); err != nil { + return err + } + return nil +} + +var _ decoderFrom = (*TopologyResponseBodyV2)(nil) + +// DecodeFrom decodes this value using the Decoder. +func (s *TopologyResponseBodyV2) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { + if maxDepth == 0 { + return 0, fmt.Errorf("decoding TopologyResponseBodyV2: %w", ErrMaxDecodingDepthReached) + } + maxDepth -= 1 + var err error + var n, nTmp int + nTmp, err = s.InboundPeers.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding TimeSlicedPeerDataList: %w", err) + } + nTmp, err = s.OutboundPeers.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding TimeSlicedPeerDataList: %w", err) + } + nTmp, err = s.NodeData.DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding TimeSlicedNodeData: %w", err) + } + return n, nil +} + +// MarshalBinary implements encoding.BinaryMarshaler. +func (s TopologyResponseBodyV2) MarshalBinary() ([]byte, error) { + b := bytes.Buffer{} + e := xdr.NewEncoder(&b) + err := s.EncodeTo(e) + return b.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler. +func (s *TopologyResponseBodyV2) UnmarshalBinary(inp []byte) error { + r := bytes.NewReader(inp) + o := xdr.DefaultDecodeOptions + o.MaxInputLen = len(inp) + d := xdr.NewDecoderWithOptions(r, o) + _, err := s.DecodeFrom(d, o.MaxDepth) + return err +} + +var ( + _ encoding.BinaryMarshaler = (*TopologyResponseBodyV2)(nil) + _ encoding.BinaryUnmarshaler = (*TopologyResponseBodyV2)(nil) +) + +// xdrType signals that this type represents XDR values defined by this package. +func (s TopologyResponseBodyV2) xdrType() {} + +var _ xdrType = (*TopologyResponseBodyV2)(nil) + // SurveyResponseBody is an XDR Union defines as: // // union SurveyResponseBody switch (SurveyMessageResponseType type) @@ -21047,11 +22195,14 @@ var _ xdrType = (*TopologyResponseBodyV1)(nil) // TopologyResponseBodyV0 topologyResponseBodyV0; // case SURVEY_TOPOLOGY_RESPONSE_V1: // TopologyResponseBodyV1 topologyResponseBodyV1; +// case SURVEY_TOPOLOGY_RESPONSE_V2: +// TopologyResponseBodyV2 topologyResponseBodyV2; // }; type SurveyResponseBody struct { Type SurveyMessageResponseType TopologyResponseBodyV0 *TopologyResponseBodyV0 TopologyResponseBodyV1 *TopologyResponseBodyV1 + TopologyResponseBodyV2 *TopologyResponseBodyV2 } // SwitchFieldName returns the field name in which this union's @@ -21068,6 +22219,8 @@ func (u SurveyResponseBody) ArmForSwitch(sw int32) (string, bool) { return "TopologyResponseBodyV0", true case SurveyMessageResponseTypeSurveyTopologyResponseV1: return "TopologyResponseBodyV1", true + case SurveyMessageResponseTypeSurveyTopologyResponseV2: + return "TopologyResponseBodyV2", true } return "-", false } @@ -21090,6 +22243,13 @@ func NewSurveyResponseBody(aType SurveyMessageResponseType, value interface{}) ( return } result.TopologyResponseBodyV1 = &tv + case SurveyMessageResponseTypeSurveyTopologyResponseV2: + tv, ok := value.(TopologyResponseBodyV2) + if !ok { + err = errors.New("invalid value, must be TopologyResponseBodyV2") + return + } + result.TopologyResponseBodyV2 = &tv } return } @@ -21144,6 +22304,31 @@ func (u SurveyResponseBody) GetTopologyResponseBodyV1() (result TopologyResponse return } +// MustTopologyResponseBodyV2 retrieves the TopologyResponseBodyV2 value from the union, +// panicing if the value is not set. +func (u SurveyResponseBody) MustTopologyResponseBodyV2() TopologyResponseBodyV2 { + val, ok := u.GetTopologyResponseBodyV2() + + if !ok { + panic("arm TopologyResponseBodyV2 is not set") + } + + return val +} + +// GetTopologyResponseBodyV2 retrieves the TopologyResponseBodyV2 value from the union, +// returning ok if the union's switch indicated the value is valid. +func (u SurveyResponseBody) GetTopologyResponseBodyV2() (result TopologyResponseBodyV2, ok bool) { + armName, _ := u.ArmForSwitch(int32(u.Type)) + + if armName == "TopologyResponseBodyV2" { + result = *u.TopologyResponseBodyV2 + ok = true + } + + return +} + // EncodeTo encodes this value using the Encoder. func (u SurveyResponseBody) EncodeTo(e *xdr.Encoder) error { var err error @@ -21161,6 +22346,11 @@ func (u SurveyResponseBody) EncodeTo(e *xdr.Encoder) error { return err } return nil + case SurveyMessageResponseTypeSurveyTopologyResponseV2: + if err = (*u.TopologyResponseBodyV2).EncodeTo(e); err != nil { + return err + } + return nil } return fmt.Errorf("Type (SurveyMessageResponseType) switch value '%d' is not valid for union SurveyResponseBody", u.Type) } @@ -21197,6 +22387,14 @@ func (u *SurveyResponseBody) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, err return n, fmt.Errorf("decoding TopologyResponseBodyV1: %w", err) } return n, nil + case SurveyMessageResponseTypeSurveyTopologyResponseV2: + u.TopologyResponseBodyV2 = new(TopologyResponseBodyV2) + nTmp, err = (*u.TopologyResponseBodyV2).DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding TopologyResponseBodyV2: %w", err) + } + return n, nil } return n, fmt.Errorf("union SurveyResponseBody has invalid Type (SurveyMessageResponseType) switch value '%d'", u.Type) } @@ -21578,6 +22776,20 @@ var _ xdrType = (*FloodDemand)(nil) // case SURVEY_RESPONSE: // SignedSurveyResponseMessage signedSurveyResponseMessage; // +// case TIME_SLICED_SURVEY_REQUEST: +// SignedTimeSlicedSurveyRequestMessage signedTimeSlicedSurveyRequestMessage; +// +// case TIME_SLICED_SURVEY_RESPONSE: +// SignedTimeSlicedSurveyResponseMessage signedTimeSlicedSurveyResponseMessage; +// +// case TIME_SLICED_SURVEY_START_COLLECTING: +// SignedTimeSlicedSurveyStartCollectingMessage +// signedTimeSlicedSurveyStartCollectingMessage; +// +// case TIME_SLICED_SURVEY_STOP_COLLECTING: +// SignedTimeSlicedSurveyStopCollectingMessage +// signedTimeSlicedSurveyStopCollectingMessage; +// // // SCP // case GET_SCP_QUORUMSET: // uint256 qSetHash; @@ -21598,26 +22810,30 @@ var _ xdrType = (*FloodDemand)(nil) // FloodDemand floodDemand; // }; type StellarMessage struct { - Type MessageType - Error *Error - Hello *Hello - Auth *Auth - DontHave *DontHave - Peers *[]PeerAddress `xdrmaxsize:"100"` - TxSetHash *Uint256 - TxSet *TransactionSet - GeneralizedTxSet *GeneralizedTransactionSet - Transaction *TransactionEnvelope - SignedSurveyRequestMessage *SignedSurveyRequestMessage - SignedSurveyResponseMessage *SignedSurveyResponseMessage - QSetHash *Uint256 - QSet *ScpQuorumSet - Envelope *ScpEnvelope - GetScpLedgerSeq *Uint32 - SendMoreMessage *SendMore - SendMoreExtendedMessage *SendMoreExtended - FloodAdvert *FloodAdvert - FloodDemand *FloodDemand + Type MessageType + Error *Error + Hello *Hello + Auth *Auth + DontHave *DontHave + Peers *[]PeerAddress `xdrmaxsize:"100"` + TxSetHash *Uint256 + TxSet *TransactionSet + GeneralizedTxSet *GeneralizedTransactionSet + Transaction *TransactionEnvelope + SignedSurveyRequestMessage *SignedSurveyRequestMessage + SignedSurveyResponseMessage *SignedSurveyResponseMessage + SignedTimeSlicedSurveyRequestMessage *SignedTimeSlicedSurveyRequestMessage + SignedTimeSlicedSurveyResponseMessage *SignedTimeSlicedSurveyResponseMessage + SignedTimeSlicedSurveyStartCollectingMessage *SignedTimeSlicedSurveyStartCollectingMessage + SignedTimeSlicedSurveyStopCollectingMessage *SignedTimeSlicedSurveyStopCollectingMessage + QSetHash *Uint256 + QSet *ScpQuorumSet + Envelope *ScpEnvelope + GetScpLedgerSeq *Uint32 + SendMoreMessage *SendMore + SendMoreExtendedMessage *SendMoreExtended + FloodAdvert *FloodAdvert + FloodDemand *FloodDemand } // SwitchFieldName returns the field name in which this union's @@ -21654,6 +22870,14 @@ func (u StellarMessage) ArmForSwitch(sw int32) (string, bool) { return "SignedSurveyRequestMessage", true case MessageTypeSurveyResponse: return "SignedSurveyResponseMessage", true + case MessageTypeTimeSlicedSurveyRequest: + return "SignedTimeSlicedSurveyRequestMessage", true + case MessageTypeTimeSlicedSurveyResponse: + return "SignedTimeSlicedSurveyResponseMessage", true + case MessageTypeTimeSlicedSurveyStartCollecting: + return "SignedTimeSlicedSurveyStartCollectingMessage", true + case MessageTypeTimeSlicedSurveyStopCollecting: + return "SignedTimeSlicedSurveyStopCollectingMessage", true case MessageTypeGetScpQuorumset: return "QSetHash", true case MessageTypeScpQuorumset: @@ -21757,6 +22981,34 @@ func NewStellarMessage(aType MessageType, value interface{}) (result StellarMess return } result.SignedSurveyResponseMessage = &tv + case MessageTypeTimeSlicedSurveyRequest: + tv, ok := value.(SignedTimeSlicedSurveyRequestMessage) + if !ok { + err = errors.New("invalid value, must be SignedTimeSlicedSurveyRequestMessage") + return + } + result.SignedTimeSlicedSurveyRequestMessage = &tv + case MessageTypeTimeSlicedSurveyResponse: + tv, ok := value.(SignedTimeSlicedSurveyResponseMessage) + if !ok { + err = errors.New("invalid value, must be SignedTimeSlicedSurveyResponseMessage") + return + } + result.SignedTimeSlicedSurveyResponseMessage = &tv + case MessageTypeTimeSlicedSurveyStartCollecting: + tv, ok := value.(SignedTimeSlicedSurveyStartCollectingMessage) + if !ok { + err = errors.New("invalid value, must be SignedTimeSlicedSurveyStartCollectingMessage") + return + } + result.SignedTimeSlicedSurveyStartCollectingMessage = &tv + case MessageTypeTimeSlicedSurveyStopCollecting: + tv, ok := value.(SignedTimeSlicedSurveyStopCollectingMessage) + if !ok { + err = errors.New("invalid value, must be SignedTimeSlicedSurveyStopCollectingMessage") + return + } + result.SignedTimeSlicedSurveyStopCollectingMessage = &tv case MessageTypeGetScpQuorumset: tv, ok := value.(Uint256) if !ok { @@ -22092,6 +23344,106 @@ func (u StellarMessage) GetSignedSurveyResponseMessage() (result SignedSurveyRes return } +// MustSignedTimeSlicedSurveyRequestMessage retrieves the SignedTimeSlicedSurveyRequestMessage value from the union, +// panicing if the value is not set. +func (u StellarMessage) MustSignedTimeSlicedSurveyRequestMessage() SignedTimeSlicedSurveyRequestMessage { + val, ok := u.GetSignedTimeSlicedSurveyRequestMessage() + + if !ok { + panic("arm SignedTimeSlicedSurveyRequestMessage is not set") + } + + return val +} + +// GetSignedTimeSlicedSurveyRequestMessage retrieves the SignedTimeSlicedSurveyRequestMessage value from the union, +// returning ok if the union's switch indicated the value is valid. +func (u StellarMessage) GetSignedTimeSlicedSurveyRequestMessage() (result SignedTimeSlicedSurveyRequestMessage, ok bool) { + armName, _ := u.ArmForSwitch(int32(u.Type)) + + if armName == "SignedTimeSlicedSurveyRequestMessage" { + result = *u.SignedTimeSlicedSurveyRequestMessage + ok = true + } + + return +} + +// MustSignedTimeSlicedSurveyResponseMessage retrieves the SignedTimeSlicedSurveyResponseMessage value from the union, +// panicing if the value is not set. +func (u StellarMessage) MustSignedTimeSlicedSurveyResponseMessage() SignedTimeSlicedSurveyResponseMessage { + val, ok := u.GetSignedTimeSlicedSurveyResponseMessage() + + if !ok { + panic("arm SignedTimeSlicedSurveyResponseMessage is not set") + } + + return val +} + +// GetSignedTimeSlicedSurveyResponseMessage retrieves the SignedTimeSlicedSurveyResponseMessage value from the union, +// returning ok if the union's switch indicated the value is valid. +func (u StellarMessage) GetSignedTimeSlicedSurveyResponseMessage() (result SignedTimeSlicedSurveyResponseMessage, ok bool) { + armName, _ := u.ArmForSwitch(int32(u.Type)) + + if armName == "SignedTimeSlicedSurveyResponseMessage" { + result = *u.SignedTimeSlicedSurveyResponseMessage + ok = true + } + + return +} + +// MustSignedTimeSlicedSurveyStartCollectingMessage retrieves the SignedTimeSlicedSurveyStartCollectingMessage value from the union, +// panicing if the value is not set. +func (u StellarMessage) MustSignedTimeSlicedSurveyStartCollectingMessage() SignedTimeSlicedSurveyStartCollectingMessage { + val, ok := u.GetSignedTimeSlicedSurveyStartCollectingMessage() + + if !ok { + panic("arm SignedTimeSlicedSurveyStartCollectingMessage is not set") + } + + return val +} + +// GetSignedTimeSlicedSurveyStartCollectingMessage retrieves the SignedTimeSlicedSurveyStartCollectingMessage value from the union, +// returning ok if the union's switch indicated the value is valid. +func (u StellarMessage) GetSignedTimeSlicedSurveyStartCollectingMessage() (result SignedTimeSlicedSurveyStartCollectingMessage, ok bool) { + armName, _ := u.ArmForSwitch(int32(u.Type)) + + if armName == "SignedTimeSlicedSurveyStartCollectingMessage" { + result = *u.SignedTimeSlicedSurveyStartCollectingMessage + ok = true + } + + return +} + +// MustSignedTimeSlicedSurveyStopCollectingMessage retrieves the SignedTimeSlicedSurveyStopCollectingMessage value from the union, +// panicing if the value is not set. +func (u StellarMessage) MustSignedTimeSlicedSurveyStopCollectingMessage() SignedTimeSlicedSurveyStopCollectingMessage { + val, ok := u.GetSignedTimeSlicedSurveyStopCollectingMessage() + + if !ok { + panic("arm SignedTimeSlicedSurveyStopCollectingMessage is not set") + } + + return val +} + +// GetSignedTimeSlicedSurveyStopCollectingMessage retrieves the SignedTimeSlicedSurveyStopCollectingMessage value from the union, +// returning ok if the union's switch indicated the value is valid. +func (u StellarMessage) GetSignedTimeSlicedSurveyStopCollectingMessage() (result SignedTimeSlicedSurveyStopCollectingMessage, ok bool) { + armName, _ := u.ArmForSwitch(int32(u.Type)) + + if armName == "SignedTimeSlicedSurveyStopCollectingMessage" { + result = *u.SignedTimeSlicedSurveyStopCollectingMessage + ok = true + } + + return +} + // MustQSetHash retrieves the QSetHash value from the union, // panicing if the value is not set. func (u StellarMessage) MustQSetHash() Uint256 { @@ -22362,6 +23714,26 @@ func (u StellarMessage) EncodeTo(e *xdr.Encoder) error { return err } return nil + case MessageTypeTimeSlicedSurveyRequest: + if err = (*u.SignedTimeSlicedSurveyRequestMessage).EncodeTo(e); err != nil { + return err + } + return nil + case MessageTypeTimeSlicedSurveyResponse: + if err = (*u.SignedTimeSlicedSurveyResponseMessage).EncodeTo(e); err != nil { + return err + } + return nil + case MessageTypeTimeSlicedSurveyStartCollecting: + if err = (*u.SignedTimeSlicedSurveyStartCollectingMessage).EncodeTo(e); err != nil { + return err + } + return nil + case MessageTypeTimeSlicedSurveyStopCollecting: + if err = (*u.SignedTimeSlicedSurveyStopCollectingMessage).EncodeTo(e); err != nil { + return err + } + return nil case MessageTypeGetScpQuorumset: if err = (*u.QSetHash).EncodeTo(e); err != nil { return err @@ -22531,6 +23903,38 @@ func (u *StellarMessage) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) return n, fmt.Errorf("decoding SignedSurveyResponseMessage: %w", err) } return n, nil + case MessageTypeTimeSlicedSurveyRequest: + u.SignedTimeSlicedSurveyRequestMessage = new(SignedTimeSlicedSurveyRequestMessage) + nTmp, err = (*u.SignedTimeSlicedSurveyRequestMessage).DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding SignedTimeSlicedSurveyRequestMessage: %w", err) + } + return n, nil + case MessageTypeTimeSlicedSurveyResponse: + u.SignedTimeSlicedSurveyResponseMessage = new(SignedTimeSlicedSurveyResponseMessage) + nTmp, err = (*u.SignedTimeSlicedSurveyResponseMessage).DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding SignedTimeSlicedSurveyResponseMessage: %w", err) + } + return n, nil + case MessageTypeTimeSlicedSurveyStartCollecting: + u.SignedTimeSlicedSurveyStartCollectingMessage = new(SignedTimeSlicedSurveyStartCollectingMessage) + nTmp, err = (*u.SignedTimeSlicedSurveyStartCollectingMessage).DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding SignedTimeSlicedSurveyStartCollectingMessage: %w", err) + } + return n, nil + case MessageTypeTimeSlicedSurveyStopCollecting: + u.SignedTimeSlicedSurveyStopCollectingMessage = new(SignedTimeSlicedSurveyStopCollectingMessage) + nTmp, err = (*u.SignedTimeSlicedSurveyStopCollectingMessage).DecodeFrom(d, maxDepth) + n += nTmp + if err != nil { + return n, fmt.Errorf("decoding SignedTimeSlicedSurveyStopCollectingMessage: %w", err) + } + return n, nil case MessageTypeGetScpQuorumset: u.QSetHash = new(Uint256) nTmp, err = (*u.QSetHash).DecodeFrom(d, maxDepth) From 89068a97ead78fbce9168f53ce6898a866b85815 Mon Sep 17 00:00:00 2001 From: Dmytro Kozhevin Date: Tue, 4 Jun 2024 14:02:03 -0400 Subject: [PATCH 184/234] Bump core to the stable v21 build. (#5323) * Bump core to the stable v21 build. * Try using 'unsafe-stellar-core' docker image * Revert "Try using 'unsafe-stellar-core' docker image" This reverts commit 19ac62ffeb2fad774ab3212dd3fac79e854d5989. * Add DEPRECATED_SQL_LEDGER_STATE=false to (most) of the Core configs * Try using `EXPERIMENTAL_BUCKETLIST_DB` for compatibility reasons * Update horizon.yml force all integration tests to have HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_USE_DB=true by default * captive core use_db flag default to true for integration tests should always be set to true, that flag is deprecated and defaulted to true, there is no test paths validating it being false. * #5295: dealing with cc use db assumptions in tests * #5295: fixing captive core use_db flag assumptions in tests --------- Co-authored-by: shawn Co-authored-by: Shawn Reuland --- .github/workflows/horizon.yml | 13 +++++++------ .../build/ledgerexporter/captive-core-pubnet.cfg | 2 ++ .../build/ledgerexporter/captive-core-testnet.cfg | 2 ++ exp/tools/dump-ledger-state/Dockerfile | 2 +- .../dump-ledger-state/stellar-core-testnet.cfg | 2 ++ exp/tools/dump-ledger-state/stellar-core.cfg | 2 ++ .../ledgerbackend/configs/captive-core-pubnet.cfg | 2 ++ .../ledgerbackend/configs/captive-core-testnet.cfg | 2 ++ .../captive-core-classic-integration-tests.cfg | 2 ++ .../docker/captive-core-integration-tests.cfg | 2 ++ .../captive-core-integration-tests.soroban-rpc.cfg | 2 ++ ...ore-reingest-range-classic-integration-tests.cfg | 2 ++ ...aptive-core-reingest-range-integration-tests.cfg | 1 + services/horizon/docker/captive-core-standalone.cfg | 2 ++ .../stellar-core-classic-integration-tests.cfg | 1 + .../docker/stellar-core-integration-tests.cfg | 1 + services/horizon/docker/stellar-core-pubnet.cfg | 2 ++ services/horizon/docker/stellar-core-standalone.cfg | 1 + services/horizon/docker/stellar-core-testnet.cfg | 2 ++ .../docker/verify-range/captive-core-pubnet.cfg | 2 ++ .../horizon/docker/verify-range/stellar-core.cfg | 2 ++ services/horizon/internal/flags_test.go | 2 ++ .../horizon/internal/integration/parameters_test.go | 2 +- .../internal/test/integration/integration.go | 2 +- 24 files changed, 46 insertions(+), 9 deletions(-) diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index ddf0da7f11..fe7b2df7a8 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -32,11 +32,12 @@ jobs: env: HORIZON_INTEGRATION_TESTS_ENABLED: true HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL: ${{ matrix.protocol-version }} - PROTOCOL_21_CORE_DEBIAN_PKG_VERSION: 21.0.0-1812.rc1.a10329cca.focal - PROTOCOL_21_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:21.0.0-1812.rc1.a10329cca.focal + HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_USE_DB: true + PROTOCOL_21_CORE_DEBIAN_PKG_VERSION: 21.0.0-1872.c6f474133.focal + PROTOCOL_21_CORE_DOCKER_IMG: stellar/stellar-core:21 PROTOCOL_21_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:21.0.0-rc2-73 - PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 21.0.0-1812.rc1.a10329cca.focal - PROTOCOL_20_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:21.0.0-1812.rc1.a10329cca.focal + PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 21.0.0-1872.c6f474133.focal + PROTOCOL_20_CORE_DOCKER_IMG: stellar/stellar-core:21 PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:21.0.0-rc2-73 PGHOST: localhost PGPORT: 5432 @@ -125,7 +126,7 @@ jobs: name: Test (and push) verify-range image runs-on: ubuntu-22.04 env: - STELLAR_CORE_VERSION: 19.14.0-1500.5664eff4e.focal + STELLAR_CORE_VERSION: 21.0.0-1872.c6f474133.focal CAPTIVE_CORE_STORAGE_PATH: /tmp steps: - uses: actions/checkout@v3 @@ -155,7 +156,7 @@ jobs: name: Test and push the Ledger Exporter images runs-on: ubuntu-latest env: - STELLAR_CORE_VERSION: 21.0.0-1812.rc1.a10329cca.focal + STELLAR_CORE_VERSION: 21.0.0-1872.c6f474133.focal steps: - uses: actions/checkout@v3 with: diff --git a/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg b/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg index d891626756..6379725b8d 100644 --- a/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg +++ b/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg @@ -3,6 +3,8 @@ DATABASE = "sqlite3:///cc/stellar.db" FAILURE_SAFETY=1 +EXPERIMENTAL_BUCKETLIST_DB=true + # WARNING! Do not use this config in production. Quorum sets should # be carefully selected manually. NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" diff --git a/exp/lighthorizon/build/ledgerexporter/captive-core-testnet.cfg b/exp/lighthorizon/build/ledgerexporter/captive-core-testnet.cfg index 0cd9b2f496..9c7dadc527 100644 --- a/exp/lighthorizon/build/ledgerexporter/captive-core-testnet.cfg +++ b/exp/lighthorizon/build/ledgerexporter/captive-core-testnet.cfg @@ -4,6 +4,8 @@ DATABASE = "sqlite3:///cc/stellar.db" UNSAFE_QUORUM=true FAILURE_SAFETY=1 +EXPERIMENTAL_BUCKETLIST_DB=true + [[HOME_DOMAINS]] HOME_DOMAIN="testnet.stellar.org" QUALITY="HIGH" diff --git a/exp/tools/dump-ledger-state/Dockerfile b/exp/tools/dump-ledger-state/Dockerfile index 91b258f0a8..5ffcb9c0a2 100644 --- a/exp/tools/dump-ledger-state/Dockerfile +++ b/exp/tools/dump-ledger-state/Dockerfile @@ -1,6 +1,6 @@ FROM ubuntu:22.04 -ENV STELLAR_CORE_VERSION=19.14.0-1500.5664eff4e.focal +ENV STELLAR_CORE_VERSION=21.0.0-1872.c6f474133.focal ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl wget gnupg apt-utils diff --git a/exp/tools/dump-ledger-state/stellar-core-testnet.cfg b/exp/tools/dump-ledger-state/stellar-core-testnet.cfg index 5162295622..a02221e795 100644 --- a/exp/tools/dump-ledger-state/stellar-core-testnet.cfg +++ b/exp/tools/dump-ledger-state/stellar-core-testnet.cfg @@ -8,6 +8,8 @@ UNSAFE_QUORUM=true FAILURE_SAFETY=1 CATCHUP_RECENT=8640 +EXPERIMENTAL_BUCKETLIST_DB=true + [HISTORY.cache] get="cp /opt/stellar/history-cache/{0} {1}" diff --git a/exp/tools/dump-ledger-state/stellar-core.cfg b/exp/tools/dump-ledger-state/stellar-core.cfg index 0b4e454681..0d97346ce6 100644 --- a/exp/tools/dump-ledger-state/stellar-core.cfg +++ b/exp/tools/dump-ledger-state/stellar-core.cfg @@ -6,6 +6,8 @@ DATABASE="postgresql://dbname=core host=localhost user=circleci" NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" CATCHUP_RECENT=1 +EXPERIMENTAL_BUCKETLIST_DB=true + [HISTORY.cache] get="cp /opt/stellar/history-cache/{0} {1}" diff --git a/ingest/ledgerbackend/configs/captive-core-pubnet.cfg b/ingest/ledgerbackend/configs/captive-core-pubnet.cfg index 5af59efaf9..6d23046f56 100644 --- a/ingest/ledgerbackend/configs/captive-core-pubnet.cfg +++ b/ingest/ledgerbackend/configs/captive-core-pubnet.cfg @@ -5,6 +5,8 @@ FAILURE_SAFETY=1 HTTP_PORT=11626 PEER_PORT=11725 +EXPERIMENTAL_BUCKETLIST_DB=true + [[HOME_DOMAINS]] HOME_DOMAIN="publicnode.org" QUALITY="HIGH" diff --git a/ingest/ledgerbackend/configs/captive-core-testnet.cfg b/ingest/ledgerbackend/configs/captive-core-testnet.cfg index 9abeecc8f5..3f0dec5095 100644 --- a/ingest/ledgerbackend/configs/captive-core-testnet.cfg +++ b/ingest/ledgerbackend/configs/captive-core-testnet.cfg @@ -2,6 +2,8 @@ NETWORK_PASSPHRASE="Test SDF Network ; September 2015" UNSAFE_QUORUM=true FAILURE_SAFETY=1 +EXPERIMENTAL_BUCKETLIST_DB=true + [[HOME_DOMAINS]] HOME_DOMAIN="testnet.stellar.org" QUALITY="HIGH" diff --git a/services/horizon/docker/captive-core-classic-integration-tests.cfg b/services/horizon/docker/captive-core-classic-integration-tests.cfg index ed8ac3ed73..2f95a0ee54 100644 --- a/services/horizon/docker/captive-core-classic-integration-tests.cfg +++ b/services/horizon/docker/captive-core-classic-integration-tests.cfg @@ -4,6 +4,8 @@ ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true UNSAFE_QUORUM=true FAILURE_SAFETY=0 +EXPERIMENTAL_BUCKETLIST_DB=true + [[VALIDATORS]] NAME="local_core" HOME_DOMAIN="core.local" diff --git a/services/horizon/docker/captive-core-integration-tests.cfg b/services/horizon/docker/captive-core-integration-tests.cfg index 275599bacd..02c59d7057 100644 --- a/services/horizon/docker/captive-core-integration-tests.cfg +++ b/services/horizon/docker/captive-core-integration-tests.cfg @@ -4,6 +4,8 @@ ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true UNSAFE_QUORUM=true FAILURE_SAFETY=0 +EXPERIMENTAL_BUCKETLIST_DB=true + ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true # Lower the TTL of persistent ledger entries # so that ledger entry extension/restoring becomes testeable diff --git a/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg b/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg index 9a7ad9d769..cf4d514975 100644 --- a/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg +++ b/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg @@ -4,6 +4,8 @@ ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true UNSAFE_QUORUM=true FAILURE_SAFETY=0 +EXPERIMENTAL_BUCKETLIST_DB=true + ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true # Lower the TTL of persistent ledger entries # so that ledger entry extension/restoring becomes testeable diff --git a/services/horizon/docker/captive-core-reingest-range-classic-integration-tests.cfg b/services/horizon/docker/captive-core-reingest-range-classic-integration-tests.cfg index 4902cf8d15..735f58b739 100644 --- a/services/horizon/docker/captive-core-reingest-range-classic-integration-tests.cfg +++ b/services/horizon/docker/captive-core-reingest-range-classic-integration-tests.cfg @@ -1,5 +1,7 @@ ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true +EXPERIMENTAL_BUCKETLIST_DB=true + [[VALIDATORS]] NAME="local_core" HOME_DOMAIN="core.local" diff --git a/services/horizon/docker/captive-core-reingest-range-integration-tests.cfg b/services/horizon/docker/captive-core-reingest-range-integration-tests.cfg index 44820f5933..4744fd390e 100644 --- a/services/horizon/docker/captive-core-reingest-range-integration-tests.cfg +++ b/services/horizon/docker/captive-core-reingest-range-integration-tests.cfg @@ -2,6 +2,7 @@ ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE=true TESTING_MINIMUM_PERSISTENT_ENTRY_LIFETIME=10 ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true +EXPERIMENTAL_BUCKETLIST_DB=true [[VALIDATORS]] NAME="local_core" diff --git a/services/horizon/docker/captive-core-standalone.cfg b/services/horizon/docker/captive-core-standalone.cfg index d54b0ecae1..f5042a14df 100644 --- a/services/horizon/docker/captive-core-standalone.cfg +++ b/services/horizon/docker/captive-core-standalone.cfg @@ -3,6 +3,8 @@ PEER_PORT=11725 UNSAFE_QUORUM=true FAILURE_SAFETY=0 +EXPERIMENTAL_BUCKETLIST_DB=true + [[VALIDATORS]] NAME="local_core" HOME_DOMAIN="core.local" diff --git a/services/horizon/docker/stellar-core-classic-integration-tests.cfg b/services/horizon/docker/stellar-core-classic-integration-tests.cfg index e27cfe14ed..f2b20b4927 100644 --- a/services/horizon/docker/stellar-core-classic-integration-tests.cfg +++ b/services/horizon/docker/stellar-core-classic-integration-tests.cfg @@ -13,6 +13,7 @@ UNSAFE_QUORUM=true FAILURE_SAFETY=0 DATABASE="postgresql://user=postgres password=mysecretpassword host=core-postgres port=5641 dbname=stellar" +EXPERIMENTAL_BUCKETLIST_DB=true [QUORUM_SET] THRESHOLD_PERCENT=100 diff --git a/services/horizon/docker/stellar-core-integration-tests.cfg b/services/horizon/docker/stellar-core-integration-tests.cfg index 594a35b244..0d5ec5cc43 100644 --- a/services/horizon/docker/stellar-core-integration-tests.cfg +++ b/services/horizon/docker/stellar-core-integration-tests.cfg @@ -13,6 +13,7 @@ UNSAFE_QUORUM=true FAILURE_SAFETY=0 DATABASE="postgresql://user=postgres password=mysecretpassword host=core-postgres port=5641 dbname=stellar" +EXPERIMENTAL_BUCKETLIST_DB=true # Lower the TTL of persistent ledger entries # so that ledger entry extension/restoring becomes testeable diff --git a/services/horizon/docker/stellar-core-pubnet.cfg b/services/horizon/docker/stellar-core-pubnet.cfg index 94b29c4b4e..7f250b10c7 100644 --- a/services/horizon/docker/stellar-core-pubnet.cfg +++ b/services/horizon/docker/stellar-core-pubnet.cfg @@ -9,6 +9,8 @@ DATABASE="postgresql://user=postgres password=mysecretpassword host=host.docker. NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" CATCHUP_RECENT=100 +EXPERIMENTAL_BUCKETLIST_DB=true + [HISTORY.cache] get="cp /opt/stellar/history-cache/{0} {1}" diff --git a/services/horizon/docker/stellar-core-standalone.cfg b/services/horizon/docker/stellar-core-standalone.cfg index a2b7e806c9..41c8a377aa 100644 --- a/services/horizon/docker/stellar-core-standalone.cfg +++ b/services/horizon/docker/stellar-core-standalone.cfg @@ -14,6 +14,7 @@ UNSAFE_QUORUM=true FAILURE_SAFETY=0 DATABASE="postgresql://user=postgres password=mysecretpassword host=host.docker.internal port=5641 dbname=stellar" +EXPERIMENTAL_BUCKETLIST_DB=true [QUORUM_SET] THRESHOLD_PERCENT=100 diff --git a/services/horizon/docker/stellar-core-testnet.cfg b/services/horizon/docker/stellar-core-testnet.cfg index cf8546a3e9..55c5cea462 100644 --- a/services/horizon/docker/stellar-core-testnet.cfg +++ b/services/horizon/docker/stellar-core-testnet.cfg @@ -12,6 +12,8 @@ UNSAFE_QUORUM=true FAILURE_SAFETY=1 CATCHUP_RECENT=100 +EXPERIMENTAL_BUCKETLIST_DB=true + [HISTORY.cache] get="cp /opt/stellar/history-cache/{0} {1}" diff --git a/services/horizon/docker/verify-range/captive-core-pubnet.cfg b/services/horizon/docker/verify-range/captive-core-pubnet.cfg index 5a702711fe..cedbbc65ab 100644 --- a/services/horizon/docker/verify-range/captive-core-pubnet.cfg +++ b/services/horizon/docker/verify-range/captive-core-pubnet.cfg @@ -2,6 +2,8 @@ PEER_PORT=11725 FAILURE_SAFETY=1 +EXPERIMENTAL_BUCKETLIST_DB=true + [[HOME_DOMAINS]] HOME_DOMAIN="stellar.org" QUALITY="HIGH" diff --git a/services/horizon/docker/verify-range/stellar-core.cfg b/services/horizon/docker/verify-range/stellar-core.cfg index 6139f9f528..91bb190131 100644 --- a/services/horizon/docker/verify-range/stellar-core.cfg +++ b/services/horizon/docker/verify-range/stellar-core.cfg @@ -4,6 +4,8 @@ LOG_FILE_PATH="" NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" CATCHUP_RECENT=100 +EXPERIMENTAL_BUCKETLIST_DB=true + AUTOMATIC_MAINTENANCE_COUNT=0 NODE_NAMES=[ diff --git a/services/horizon/internal/flags_test.go b/services/horizon/internal/flags_test.go index a30ea3a404..65d1da524c 100644 --- a/services/horizon/internal/flags_test.go +++ b/services/horizon/internal/flags_test.go @@ -86,6 +86,7 @@ func Test_createCaptiveCoreDefaultConfig(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + tt.config.CaptiveCoreTomlParams.UseDB = true e := setCaptiveCoreConfiguration(&tt.config, ApplyOptions{RequireCaptiveCoreFullConfig: true}) if tt.errStr == "" { @@ -192,6 +193,7 @@ func Test_createCaptiveCoreConfig(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + tt.config.CaptiveCoreTomlParams.UseDB = true e := setCaptiveCoreConfiguration(&tt.config, ApplyOptions{RequireCaptiveCoreFullConfig: tt.requireCaptiveCoreConfig}) if tt.errStr == "" { diff --git a/services/horizon/internal/integration/parameters_test.go b/services/horizon/internal/integration/parameters_test.go index eb22e9a068..133950d6f3 100644 --- a/services/horizon/internal/integration/parameters_test.go +++ b/services/horizon/internal/integration/parameters_test.go @@ -505,7 +505,7 @@ func TestDeprecatedOutputs(t *testing.T) { stdLog.SetOutput(os.Stderr) testConfig := integration.GetTestConfig() - testConfig.HorizonIngestParameters = map[string]string{"captive-core-use-db": "false"} + testConfig.HorizonIngestParameters = map[string]string{"captive-core-use-db": "true"} test := integration.NewTest(t, *testConfig) err := test.StartHorizon() assert.NoError(t, err) diff --git a/services/horizon/internal/test/integration/integration.go b/services/horizon/internal/test/integration/integration.go index d755d00252..e12f77f693 100644 --- a/services/horizon/internal/test/integration/integration.go +++ b/services/horizon/internal/test/integration/integration.go @@ -441,7 +441,7 @@ func (i *Test) getDefaultIngestArgs(postgres *dbtest.DB) map[string]string { "stellar-core-binary-path": i.coreConfig.binaryPath, "captive-core-config-path": i.coreConfig.configPath, "captive-core-http-port": "21626", - "captive-core-use-db": strconv.FormatBool(i.coreConfig.useDB), + "captive-core-use-db": "true", "captive-core-storage-path": i.coreConfig.storagePath, "ingest": "true"}) } From 4a48b7c29b9d716ce9a01306b93033ec2f28569c Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Thu, 6 Jun 2024 11:42:14 -0400 Subject: [PATCH 185/234] services/horizon: Add hash to the txsub timeout response (#5328) * Change the txsub timeout response * Fix timeout test --- .../internal/actions/submit_transaction.go | 26 +++++++++++++++++-- .../actions/submit_transaction_test.go | 15 ++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/services/horizon/internal/actions/submit_transaction.go b/services/horizon/internal/actions/submit_transaction.go index 00049a9172..703e7a554d 100644 --- a/services/horizon/internal/actions/submit_transaction.go +++ b/services/horizon/internal/actions/submit_transaction.go @@ -92,7 +92,18 @@ func (handler SubmitTransactionHandler) response(r *http.Request, info envelopeI } if result.Err == txsub.ErrTimeout { - return nil, &hProblem.Timeout + return nil, &problem.P{ + Type: "transaction_submission_timeout", + Title: "Transaction Submission Timeout", + Status: http.StatusGatewayTimeout, + Detail: "Your transaction submission request has timed out. This does not necessarily mean the submission has failed. " + + "Before resubmitting, please use the transaction hash provided in `extras.hash` to poll the GET /transactions endpoint for sometime and " + + "check if it was included in a ledger.", + Extras: map[string]interface{}{ + "hash": info.hash, + "envelope_xdr": info.raw, + }, + } } if result.Err == txsub.ErrCanceled { @@ -191,6 +202,17 @@ func (handler SubmitTransactionHandler) GetResource(w HeaderWriter, r *http.Requ if r.Context().Err() == context.Canceled { return nil, hProblem.ClientDisconnected } - return nil, hProblem.Timeout + return nil, &problem.P{ + Type: "transaction_submission_timeout", + Title: "Transaction Submission Timeout", + Status: http.StatusGatewayTimeout, + Detail: "Your transaction submission request has timed out. This does not necessarily mean the submission has failed. " + + "Before resubmitting, please use the transaction hash provided in `extras.hash` to poll the GET /transactions endpoint for sometime and " + + "check if it was included in a ledger.", + Extras: map[string]interface{}{ + "hash": info.hash, + "envelope_xdr": raw, + }, + } } } diff --git a/services/horizon/internal/actions/submit_transaction_test.go b/services/horizon/internal/actions/submit_transaction_test.go index eb1987bdea..8d9979ae4b 100644 --- a/services/horizon/internal/actions/submit_transaction_test.go +++ b/services/horizon/internal/actions/submit_transaction_test.go @@ -100,6 +100,19 @@ func TestTimeoutSubmission(t *testing.T) { form := url.Values{} form.Set("tx", "AAAAAAGUcmKO5465JxTSLQOQljwk2SfqAJmZSG6JH6wtqpwhAAABLAAAAAAAAAABAAAAAAAAAAEAAAALaGVsbG8gd29ybGQAAAAAAwAAAAAAAAAAAAAAABbxCy3mLg3hiTqX4VUEEp60pFOrJNxYM1JtxXTwXhY2AAAAAAvrwgAAAAAAAAAAAQAAAAAW8Qst5i4N4Yk6l+FVBBKetKRTqyTcWDNSbcV08F4WNgAAAAAN4Lazj4x61AAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABLaqcIQAAAEBKwqWy3TaOxoGnfm9eUjfTRBvPf34dvDA0Nf+B8z4zBob90UXtuCqmQqwMCyH+okOI3c05br3khkH0yP4kCwcE") + expectedTimeoutResponse := &problem.P{ + Type: "transaction_submission_timeout", + Title: "Transaction Submission Timeout", + Status: http.StatusGatewayTimeout, + Detail: "Your transaction submission request has timed out. This does not necessarily mean the submission has failed. " + + "Before resubmitting, please use the transaction hash provided in `extras.hash` to poll the GET /transactions endpoint for sometime and " + + "check if it was included in a ledger.", + Extras: map[string]interface{}{ + "hash": "3389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c8889", + "envelope_xdr": "AAAAAAGUcmKO5465JxTSLQOQljwk2SfqAJmZSG6JH6wtqpwhAAABLAAAAAAAAAABAAAAAAAAAAEAAAALaGVsbG8gd29ybGQAAAAAAwAAAAAAAAAAAAAAABbxCy3mLg3hiTqX4VUEEp60pFOrJNxYM1JtxXTwXhY2AAAAAAvrwgAAAAAAAAAAAQAAAAAW8Qst5i4N4Yk6l+FVBBKetKRTqyTcWDNSbcV08F4WNgAAAAAN4Lazj4x61AAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABLaqcIQAAAEBKwqWy3TaOxoGnfm9eUjfTRBvPf34dvDA0Nf+B8z4zBob90UXtuCqmQqwMCyH+okOI3c05br3khkH0yP4kCwcE", + }, + } + request, err := http.NewRequest( "POST", "https://horizon.stellar.org/transactions", @@ -115,7 +128,7 @@ func TestTimeoutSubmission(t *testing.T) { w := httptest.NewRecorder() _, err = handler.GetResource(w, request) assert.Error(t, err) - assert.Equal(t, hProblem.Timeout, err) + assert.Equal(t, expectedTimeoutResponse, err) } func TestClientDisconnectSubmission(t *testing.T) { From f30d11432e81c7a7cbb739a694520f729bbb31dd Mon Sep 17 00:00:00 2001 From: Molly Karcher Date: Fri, 7 Jun 2024 16:49:17 -0400 Subject: [PATCH 186/234] Remove internal API reference (#5336) --- .../horizon/internal/docs/SDK_API_GUIDE.md | 17 - .../internal/docs/plans/images/adapters.png | Bin 38559 -> 0 bytes .../docs/plans/images/historyarchive.png | Bin 82839 -> 0 bytes .../horizon/internal/docs/plans/images/io.png | Bin 86548 -> 0 bytes .../docs/plans/images/ledgerbackend.png | Bin 57458 -> 0 bytes .../internal/docs/plans/images/pipeline.png | Bin 64608 -> 0 bytes .../internal/docs/plans/images/system.png | Bin 59201 -> 0 bytes .../internal/docs/plans/new_horizon_ingest.md | 181 ---- .../reference/endpoints/accounts-single.md | 164 ---- .../docs/reference/endpoints/accounts.md | 177 ---- .../docs/reference/endpoints/assets-all.md | 159 ---- .../reference/endpoints/data-for-account.md | 63 -- .../docs/reference/endpoints/effects-all.md | 133 --- .../endpoints/effects-for-account.md | 124 --- .../reference/endpoints/effects-for-ledger.md | 143 --- .../endpoints/effects-for-operation.md | 142 --- .../endpoints/effects-for-transaction.md | 142 --- .../docs/reference/endpoints/fee-stats.md | 124 --- .../docs/reference/endpoints/ledgers-all.md | 205 ----- .../reference/endpoints/ledgers-single.md | 92 -- .../docs/reference/endpoints/metrics.md | 126 --- .../docs/reference/endpoints/offer-details.md | 71 -- .../reference/endpoints/offers-for-account.md | 131 --- .../docs/reference/endpoints/offers.md | 122 --- .../reference/endpoints/operations-all.md | 192 ---- .../endpoints/operations-for-account.md | 162 ---- .../endpoints/operations-for-ledger.md | 140 --- .../endpoints/operations-for-transaction.md | 115 --- .../reference/endpoints/operations-single.md | 97 -- .../reference/endpoints/orderbook-details.md | 118 --- .../endpoints/path-finding-strict-receive.md | 103 --- .../endpoints/path-finding-strict-send.md | 105 --- .../docs/reference/endpoints/path-finding.md | 106 --- .../docs/reference/endpoints/payments-all.md | 198 ----- .../endpoints/payments-for-account.md | 168 ---- .../endpoints/payments-for-ledger.md | 119 --- .../endpoints/payments-for-transaction.md | 124 --- .../reference/endpoints/trade_aggregations.md | 155 ---- .../reference/endpoints/trades-for-account.md | 140 --- .../reference/endpoints/trades-for-offer.md | 139 --- .../docs/reference/endpoints/trades.md | 178 ---- .../reference/endpoints/transactions-all.md | 189 ---- .../endpoints/transactions-create.md | 132 --- .../endpoints/transactions-for-account.md | 166 ---- .../endpoints/transactions-for-ledger.md | 222 ----- .../endpoints/transactions-single.md | 112 --- .../horizon/internal/docs/reference/errors.md | 32 - .../docs/reference/errors/bad-request.md | 44 - .../docs/reference/errors/before-history.md | 41 - .../docs/reference/errors/not-acceptable.md | 42 - .../docs/reference/errors/not-found.md | 45 - .../docs/reference/errors/not-implemented.md | 41 - .../reference/errors/rate-limit-exceeded.md | 41 - .../docs/reference/errors/server-error.md | 48 - .../docs/reference/errors/stale-history.md | 39 - .../internal/docs/reference/errors/timeout.md | 49 - .../reference/errors/transaction-failed.md | 83 -- .../reference/errors/transaction-malformed.md | 53 -- .../horizon/internal/docs/reference/paging.md | 12 - .../internal/docs/reference/rate-limiting.md | 27 - .../horizon/internal/docs/reference/readme.md | 26 - .../docs/reference/resources/account.md | 199 ----- .../docs/reference/resources/asset.md | 72 -- .../internal/docs/reference/resources/data.md | 22 - .../docs/reference/resources/effect.md | 132 --- .../docs/reference/resources/ledger.md | 100 --- .../docs/reference/resources/offer.md | 81 -- .../docs/reference/resources/operation.md | 840 ------------------ .../docs/reference/resources/orderbook.md | 50 -- .../internal/docs/reference/resources/page.md | 92 -- .../internal/docs/reference/resources/path.md | 54 -- .../docs/reference/resources/trade.md | 66 -- .../reference/resources/trade_aggregation.md | 39 - .../docs/reference/resources/transaction.md | 119 --- .../internal/docs/reference/responses.md | 75 -- .../internal/docs/reference/streaming.md | 20 - .../tutorials/follow-received-payments.md | 213 ----- .../horizon/internal/docs/reference/xdr.md | 26 - 78 files changed, 8319 deletions(-) delete mode 100644 services/horizon/internal/docs/SDK_API_GUIDE.md delete mode 100644 services/horizon/internal/docs/plans/images/adapters.png delete mode 100644 services/horizon/internal/docs/plans/images/historyarchive.png delete mode 100644 services/horizon/internal/docs/plans/images/io.png delete mode 100644 services/horizon/internal/docs/plans/images/ledgerbackend.png delete mode 100644 services/horizon/internal/docs/plans/images/pipeline.png delete mode 100644 services/horizon/internal/docs/plans/images/system.png delete mode 100644 services/horizon/internal/docs/plans/new_horizon_ingest.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/accounts-single.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/accounts.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/assets-all.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/data-for-account.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/effects-all.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/effects-for-account.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/effects-for-ledger.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/effects-for-operation.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/effects-for-transaction.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/fee-stats.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/ledgers-all.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/ledgers-single.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/metrics.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/offer-details.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/offers-for-account.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/offers.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/operations-all.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/operations-for-account.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/operations-for-ledger.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/operations-for-transaction.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/operations-single.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/orderbook-details.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/path-finding-strict-receive.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/path-finding-strict-send.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/path-finding.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/payments-all.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/payments-for-account.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/payments-for-ledger.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/payments-for-transaction.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/trade_aggregations.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/trades-for-account.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/trades-for-offer.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/trades.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/transactions-all.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/transactions-create.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/transactions-for-account.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/transactions-for-ledger.md delete mode 100644 services/horizon/internal/docs/reference/endpoints/transactions-single.md delete mode 100644 services/horizon/internal/docs/reference/errors.md delete mode 100644 services/horizon/internal/docs/reference/errors/bad-request.md delete mode 100644 services/horizon/internal/docs/reference/errors/before-history.md delete mode 100644 services/horizon/internal/docs/reference/errors/not-acceptable.md delete mode 100644 services/horizon/internal/docs/reference/errors/not-found.md delete mode 100644 services/horizon/internal/docs/reference/errors/not-implemented.md delete mode 100644 services/horizon/internal/docs/reference/errors/rate-limit-exceeded.md delete mode 100644 services/horizon/internal/docs/reference/errors/server-error.md delete mode 100644 services/horizon/internal/docs/reference/errors/stale-history.md delete mode 100644 services/horizon/internal/docs/reference/errors/timeout.md delete mode 100644 services/horizon/internal/docs/reference/errors/transaction-failed.md delete mode 100644 services/horizon/internal/docs/reference/errors/transaction-malformed.md delete mode 100644 services/horizon/internal/docs/reference/paging.md delete mode 100644 services/horizon/internal/docs/reference/rate-limiting.md delete mode 100644 services/horizon/internal/docs/reference/readme.md delete mode 100644 services/horizon/internal/docs/reference/resources/account.md delete mode 100644 services/horizon/internal/docs/reference/resources/asset.md delete mode 100644 services/horizon/internal/docs/reference/resources/data.md delete mode 100644 services/horizon/internal/docs/reference/resources/effect.md delete mode 100644 services/horizon/internal/docs/reference/resources/ledger.md delete mode 100644 services/horizon/internal/docs/reference/resources/offer.md delete mode 100644 services/horizon/internal/docs/reference/resources/operation.md delete mode 100644 services/horizon/internal/docs/reference/resources/orderbook.md delete mode 100644 services/horizon/internal/docs/reference/resources/page.md delete mode 100644 services/horizon/internal/docs/reference/resources/path.md delete mode 100644 services/horizon/internal/docs/reference/resources/trade.md delete mode 100644 services/horizon/internal/docs/reference/resources/trade_aggregation.md delete mode 100644 services/horizon/internal/docs/reference/resources/transaction.md delete mode 100644 services/horizon/internal/docs/reference/responses.md delete mode 100644 services/horizon/internal/docs/reference/streaming.md delete mode 100644 services/horizon/internal/docs/reference/tutorials/follow-received-payments.md delete mode 100644 services/horizon/internal/docs/reference/xdr.md diff --git a/services/horizon/internal/docs/SDK_API_GUIDE.md b/services/horizon/internal/docs/SDK_API_GUIDE.md deleted file mode 100644 index bc8268ddb1..0000000000 --- a/services/horizon/internal/docs/SDK_API_GUIDE.md +++ /dev/null @@ -1,17 +0,0 @@ -# **Horizon SDK and API Guide** - -Now, let's get familiar with Horizon's API, SDK and the tooling around them. The [API documentation](https://developers.stellar.org/api/) is particularly useful and you will find yourself consulting it often. - -Spend a few hours reading before getting your hands dirty writing code: - -- Skim through the [developer documentation](https://developers.stellar.org/docs/) in general. -- Try to understand what an [Account](https://developers.stellar.org/docs/glossary/accounts/) is. -- Try to understand what a [Ledger](https://developers.stellar.org/docs/glossary/ledger/), [Transaction](https://developers.stellar.org/docs/glossary/transactions/) and [Operation](https://developers.stellar.org/docs/glossary/operations/) is and their hierarchical nature. Make sure you understand how sequence numbers work. -- Go through the different [Operation](https://developers.stellar.org/docs/start/list-of-operations/) types. Take a look at the Go SDK machinery for the [Account Creation](https://godoc.org/github.com/stellar/go/txnbuild#CreateAccount) and [Payment](https://godoc.org/github.com/stellar/go/txnbuild#Payment) operations and read the documentation examples. -- You will use the Testnet network frequently during Horizon's development. Get familiar with [what it is and how it is useful](https://developers.stellar.org/docs/glossary/testnet/). Try to understand what [Friendbot](https://github.com/stellar/go/tree/master/services/friendbot) is. -- Read Horizon's API [introduction](https://developers.stellar.org/api/introduction/) and make sure you understand what's HAL and XDR. Also, make sure you understand how streaming responses work. -- Get familiar with Horizon's REST API endpoints. There are two type of endpoints: - - **Querying Endpoints**. They give you information about the network status. The output is based on the information obtained from Core or derived from it. These endpoints refer to resources. These resources can: - - Exist in the Stellar network. Take a look at the [endpoints associated with each resource](https://developers.stellar.org/api/resources/). - - Are abstractions which don't exist in the Stellar network but they are useful to the end user. Take a look at their [endpoints](https://developers.stellar.org/api/aggregations/). - - **Submission Endpoints**. There is only one, the [transaction submission endpoint](https://www.stellar.org/developers/horizon/reference/endpoints/transactions-create.html). You will be using it explicitly next. \ No newline at end of file diff --git a/services/horizon/internal/docs/plans/images/adapters.png b/services/horizon/internal/docs/plans/images/adapters.png deleted file mode 100644 index 807cd4624299d6d381016d22622e209a721bee89..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38559 zcmagGbyQbR)b>qxcS(15cT0b1kPhkYZjkO&TDnU@O1e8llf`NgT1AjT;V1Ofjn;!jOV0h|Q5)vx! zB_v2yoE<(|*_wlaX@{kHL9449;-0o9kFg^C`9t-G^v|EzZZL`6sk|ied~!J2qF{tz zI2{e2A3yuuEIHkj3E_fGsH>Wp(FUUg z)}G--W!DksMpqD$xFh6&6XwBSh-PgOzALjaH3eV0*nM6#_reqXd}^Wkx&Ed3`BiuC zd1$&5Oq84T<~;faIpmxROeM23F$E8dwVYL-RhZft)2BNl5`qwJb4Zb>HkN~@ZjxKL zu7A_01a%VztQ3wCg9;4Hgiv8O`Vu*C8_lrW4r$$AY@PK`Fs8{D^1VNp<4LfWdsvv1 ztqCq(^!<~BQg^`c)jdP9B*I;Ev)>ZEI*izKV*b~@*U9y}fb`m_pCbH{YjSX3Gk)4V zAjL07C%|1QDg>VUw#27$#o!w~aSS;`lMKN58dXMaa*G6y^5~Kfb96(P>WCx-@x`-S0SSCb*$eQCCn-q;NOQfZ_R$|mpnspFm-Rh5=bW%U3Rm1@C$)}IWo zBDpvA`r9^z&@-L1yTDUQ?ViXBc>`pKLY2FtjzV6@aNHLd!aTUL9Dh(rFDM2R@} zy1UJ2lBI6PWsfZ|zTZxRhFhLyF*$#fR%;UTxdonR1^MPYlbOLxtM_;xvpZ?4@I1xs zM`d)R27dC|E=OL#Q)e#OE{Dj0;fRvpfeE7_YLTFU2aBn^`W^~C*?jMZO8|q00%K43 zmH)KA-ae0kB62K-aR)`g{^?~|*`Xp8X~XE@!SLWl4dKqAX~6Cp4xGe@2xd%-_DR6K zLhH`ZPHF9(0{jEXac6B+zT+R;N5d>;g}R^leJ_clGLI3Mrk~ilGmK(bH)v5(aO5D( z!A0EG=;KwT%AV_PM4t9g_r$I!z2Su7{P(d_lN%Q56o zi{M8i%nP+uh(r;J;B023ta{s0jmlr*wm+}Dez*y^KZ^&guJ;aapz&&cK~Xwd6^g`= zEb3C_9=yt>Xtb-CTy>g%UA}O5et5;R61lT`aZobjzM!v%^}=0>J`()~A-rX0Mp%#P z<%0Q|Ep__s<1~q#`-hv*Zs*_^84@d*-5pu!QGRvc z%9xDc6S_%+Cz-JuV3Jtm@QIPU!Ms7G(wwBaXddYC zXcC{C`iM;6;S=U$aVSMp5z3I7FxbCWNx^NL8sQS2r%&>}^l z_eUfJThzjjpl?SbDAZNaFSRIzpMyBy$$}XVOOvc9RGens19KJRN7N6l z7UL=WzCM3N+!6D`NSn0)58IQg%_Nv@I71>sP62ZVa%8TRQ)5qKTVqb+`$nopEvwwAiP`l%$-S$6S=Yz?k^Sn~*eRxIj+xQv53>=| zBn34kr$xCRyV*an;<4YcnX!K{ZW(0l|73<`HfM2VK4v61`g8m)2fmkb#B2m>EM?F& zu`JgrC^JYWaVe57sP)S|^(8N2l2mAa3RMbK?l!_G>nMw+y{5hn&(sVr(5n*MiP!( zW<%Ol_)g|dXn$sZ%opt!`WOEf4R8>+Opt33br4Umr?r?;D1Z!J*8y}kt+aVPp6vI zf!17?rL0w`<(!SeDb63o6+idel;S|@_YHC`deO?G9}XKATDZHo-+$NjQI$~{R4$R; zEv#;_ueA?3u@KS`iZW9eLka0~&2>ML>neZFl2f}6NWmI=(cb{YLBmCozqr>C$cUMFg?+gT@_y+jh`KS54 zc<1?^9l0HuU5PysotAblhEjXEhMLBa4|ldSH@|J!?-?AutVj$~jGB7J@8yIFb>tA` zY$YvAMA_$N^YpGxP>p9ST@3l$;T1b6E4+UD~abou`TGQNIkD&ss^%?nAdz zy}iy6%0w%?8v$QM4EbKSKx5Nqg}uvv{G=6_5&~`rH zeepaxKgYzO`6U-77oS$03Y~_WdYHU)WDX4rF3v zNL*y*Z(MR*np}09+xigcCM@FyH=TmM{ZDec8uJygi=j0doE+S{-$a=rnGmajsy6Ma z?LrpEt3AHiH1ez_bb^+Dtb+WL??1jUJynf;AZ$*yXZaJ^&Jn<~cA{RQwR`Bpe4FF3 zK3CQ7(zIxB{CVV=Nm)Io5xEq( z5%q+){aXET|BnUC5{As3Q9ex%{BBr6S&!i- zion-w=lHhE}}s2`}0X$+O`mBu@w{hM!uFCoVxlH%J*4vV)-1U|-mg!%Z# zVs%n#qH4mzui#bvT<7SriG|;)8VG|6M_42U)!Dcqr>HDA)zrLVIn1h)ggUgC_~RGNnlMA*i;KdH!m32 zil4ybGM((*MNr_XQUaMcZdsIx#3q}%whq0P9?T0jxP%1xx0&^Q34CI9IyS9Zh;L{& z{z~T6g}+_e`F-!7LV`WXxQw+D(WY71P@RO}?IWdzAHT~LiGSrXw=`QB;T|p;S*AZ@ z@KtTEb`@RH43ppPurAaMZmWhd*jze6P!y@s*EMYV;C6Fxu#bMVaKlg2P?Nwq%%FoJ{9Vm5jzWoV< zT4l2??4CNhl|RdM{dE!b`ETO8Yb9GwOCGBE^YY^ zA3Cq44pp2l-td$(6IqVikABZtHSjGgDBRg;`tYY&<|JnI-HC16?rQ(Sb>8B@#f+;uUCl2>Hz2z#H*a5R4^1a`-;*cb8Jgrt_1wcwynswy-iC~``ODAO9-~ET z+Y`C40Zr9}wzfxohA5HZ6L|yEY8lYMy^V&PRU8ILL4d z>I9J~fz;qt4>Tt<>NL&O-)S;vcr30h;DuQPMc{m2_;lm+&O;ylk46XC^C(ON(y;XR z*!@_M4A%^t&;6OnOumeeRpgbuw!XGi$DBu0M7-}JmEcOucjK9Rt8af3{23K;zNPen z@oM!XOfd8P=}QJo1DSJrfQzW2(R-DTj*_2jN|iD!d65M%smTWmI%O^gJ^RxJ{=^0( zvZnIpHrA(mlT7;8VRfo294t5PTs_L%Wj91F+;`HQ&*=qOa5~9h*Tmhte)RMR_KIdE8w~I)Hst$|;%t3fq%p}Pidb>po~^LP1QDCJk2$Q%5KXF&p}uZ)j(_{ z(C;5x`1uli(eH#%P@^5**W0)2ZW18_aSZ+=VkCkaf)7H?C#=YkC}Au_w85xFxeQs~ zl(An_-zq+gXc5S2jU1(Ukr5)U+GlMsZIxiNV|igMryQm1q@tFdl{fY@r&%>&x#3(R z60{L0dpw@JRAyF&&C@Sx&Ji!XpV^+an{8O|DI+QEGFGv(G!5lwvuIzGoHko%{5975 zW!(2?!OxkMsD<)H{$A-$|c%> zZfRSmx!K0pYD|^;@B5?s0%UnJEi-ttu(HNdo@`HJ{hDt)ihj#?^jPr~BPDpYd`fmv3Zm2sHi#WXmA~Pf}v2zvh zIWVC>VNzB&woqtt0ucPJ7onp8ZbW zPSBp#z9?Kd541rcD)NQ0vto0U-)75aR`NLVeH2N0MF$-drvsf5hJ&mUma^{YqH#x+ z7gV_wzp>r;5q%*#{*t4|Ta9wUODvV+(5`hy`y}^d10fVlB_7oCUFsA0W`S0JeZL>2 zE0vTxx<{3qUz$G+2jwzjIwK2>NO3|{>DN1{GR0OED$u<0xVXM%h#L<`O}9c%M)kJV zpymZvf}Y}|sXHdK8X1R_3qv&RD5j?Vf}tJs@t5Ox-YeexFUWfJdXK+#1buJjd^#@& z688L9h2=X>pQVQzz#D^h#QJ}bNO(kGhXurOMwiD^_eu75M)Am5$|uQHFAX+#38qq2 zkwWHXx*P2qIo{~mujJQKj6+`y@N zE!R1`9Ue-zYP0F{H6yFBY3Cv+uf8qp&xg+kqfIu4G69daL-xlcpU(#&AdJ9{0!d`S zp!47r0wK>}j7jmg0##LDt3zbgkhKG+&|oRQV?Rrl!nHwNOXQ+8!a;`;l0!N{pA!#9 zUy0{PO%$epGE2!CdHIor5C%Jm5Nm^elF>_)bALgqCT59q$kY7VsaQ*^;6DGnJ*D3lXUwT3h}wM;a0 z)&0w63V#)7R{6_PSb2Q;7^nI922~$gNaR^CgrFCvkZ^!U!9MFIOHrGYk*e}bCF;+{ zUlYrSe4c!o{IRWYEim7kJdJL7ebT(#d{C44{SJH^o`Z+nU#P*WgZ9Nd#4W}BdicqC z$&(9KBoF)N`lknEs0HOVl;f5KY7ObOZ3!&ALDVmz&I`WWbQbI^`?a>h8+)I7Mx~sn zq6WLt44C7q)J5(OT@{<2GDjLnY$R}xyRex^blLP%4Fy_XPHs1poOy-*$4ysN{iTS!%-esoCZX01yG!%EeU-r?kx&%XvoC$pcJMJZTW z_9Q;(TN<{Q_s!CJjJPT|6DdDwZ#-`aJkqNXsRa~ine&^U6YCANW_s+^26Q@s`>Yp| zziw=A;ul?+&ts3I#w*`ziOpkMXiOr{4=gw>hybdavy;-Le@|CV-tB|`cN^Hl@xmGTF|ydTiWj%t>BlK1u+QuE@MjFR zHJ)s?#DaKiVxmyQ$uI5Mq;{=c_Tk`meCK=jZBbG0sNU$c$ee%5v-bV|Q-gV!d072S zef@*{uGUV?ipECy^TGLi5LY=@c2j86s13*A(_!^t*dglTV!-J0&70Rt2C z^Ark_#h{ zl9CEKn|JMfzjxuvVCBR>m^hldBV2M4o*vjqzqA0HnJD?1B2I}>mPlZ%(V ztFb4Oy$i*Ejr`wsB+Xq+ovj>QtsLx0-`X`cad2}LA}4=)(Et4Tub<|gR{!rw_AdX1 z1q_ho?F;FGz{@)Y-*PS~5 z?@kU@-v52)|2p&UPC=Hp0sOB4{Wn{Gj{?&rj3CJJKT|J^pqYJB0R|=t_Fhs<-4pyM z8#;qhs(vuJL;p8wbN_ZNBua#7U0DC0KPVzwI6mDQj3?gR8;&jCnI<+)7*9@MZ>tTS zHqfP%>@?i26((IL75Z-7v^3<*wnQ4(RmmHq&ftZ_A$u-c4+evZR&s3i!v7z^PZU#G>q8*KA%pG)_w+c5%JlkLi~BK4Qu3 z5yu5mtika@#EBx3u0@qYBm9tZgdzf$=|-oO?#9+&<38DZI~zzb1p(JmU>zjxi2nDi zBUBW+qm)KW?Bn0FYjBNV5PstXgVjxD-PR zI?=u*uIqNZaPfD=?%;0j+NkFpZv7if{o7!c3Y>oY8;oc-cIqM|Flu1%L1}Lzm&c7q z-2LzBwWyK9|2s?a6u>MMMGuC#{5MOEQrm8S?*f;}D?+zi9L8eGE?f<*s8{1(xz|f6 zq4)i?yytl4dM}#Kq|u+L9cTFA$kX{ry8kP&o;QZS#)UptS1d||?fK&&1wERR^M9>c z>Oei2MG+WseoM01igWYST*o~fFtNyV^8K{4Ezmx_ zztYSg zl?(X2jlfBH@@@8jd0>X(uPl9eTzw5E33y_AdN{0cqxzj0l(;)BLxP+e@Zx1bz1jZm zZH+lXwSff*AJJCd;*MTVNuY)~4dBxdd9DT3xupLw^uOO;pHow|OJ32?(#OWpcbz`# z$2DwpV)4oFy%_FooK^aO^R;+&*`e#5lHhsYSs$kI9MU29-&eX<1*bmE=gtmoEFf5r zyYybVYI4U1M59rgmak{>{Rq|D>ac>WKG$Y2x&vxIqPnw8<2d3+a$vqqT95>ZI zo_vp3?~OZ(oxgH&dPepeq z^vl#JOBWFg3Q=icX~)ekhNr7p<%0=BP`g9tz4Z1Ui(J1HT;D~*fakOGK@!-RKLM}L zse?-vx$p4y+1^>-Qymfe9HFa(D<~$NLZa}BrXK0qR27|nYS@TkuzulLwrx0@k!LCU z{>t|^03@T87PzpQHaq^DW{6CBe>YFw17~Da`*_-Am|JwN^zwWfa47V$8Ovk9zGRlX z-#`n29B|yUc);O5tH7a1a1+H)Eo+9rc1_4?6u2_Ue^^OB8-{C8I(b;tY*2qO!f^Op zH%R<>C?&U8!uzvBXKT37#Sq23VevCY&sy_ZFe3FRV?)e}LtQ@({ixvOSm7i!#d=Fb z)W&dD`aoT+-aS*-)5WmQX-9f7xWk2Y{TT0NOk`2>^UX5Ns^6{tNW`5ybF10K0HH(a z=NPEus^%5l)8UH$0!nren4L05sdby7-?aw4K}5+5W$36ejUM_zgHhV90ksnZkez{!r;b zA~M6uw~EHrvknx)1L+c+0%1e(EBc~mOSFlsS%>^{9N$jH+&dBcj#7buzQI@$7*zJ2 z>D-t+4E)w2e|G4S{VX;qkYJs`950>>8cwI4`8&ngzMnlY)Mj>!=!m^$6;sn1W?WuY9F;x)g{a{)k~*t2 zRsc04i=%45zlPCxBTlRj2$&)x7V;MxbwL?!Wlf6)A7bBWhlr;g!9|uy#xM*JxhN=N z7{tKnl$3|D6yKfof9Vh^L9)2qoBX>Hx5z{vXL8&HKul@2fUGhJnyXl@|ON@rDp;R zb&$Dz(E1lz@&@1daS*G>qVF$6{r}#4wLL^WRNYjs!==Y0d~*5^%mmO&3uJz7*sS|X z&I=qy?*97Qn0<)o>JeGl(yo0nChpun%=kd6A~oq492RzouF*`Zi97T6(v@h@^qd?6 z=#P!1{}&c_DO8kwY+%zpI@Jq{r<|h@&_BZum7=vq{}%!YgS@eHEl9W301VE16^>>W~oSx@;7&v0S1Pb3*3Ev5kSKn>OF6o)Bmur^o=$M<&Z%C8lbAtbt zN1Fx8LQsXZ?}eVPW{-D33>#@5?8~g$f#7a66)*fiJ3a`Yi|TFr=k`0V^^xBz{*T9w z8<8~OG&!CHj2Vx75Xe$_r~(0p&s^#d77G;UdwVz`uP?Vzru^#t+LzW^S<{prvb-Fq1#XZ{aBIJa9$%w{NcWz zv{e-I0i0y@fRoPo;~>K{4*u@>x+VY_=&+{|XVdG;Bg1RMq_BV(iW zu|weMIR-7nhoJCVrtrRj3j2!ZX4&h-C`*mj$m8{*p|U-&LauZO{#+RpH>6DFzFg6` zH87mR!Q!*vQ&A-=a%YAElRcVr+WE-5EpbPcuE+i(`w>8DL3}|^NAkjQvn;D(5KrcW zsy{*YF}n#+938hZIkr#GjmP$J^KMWoF744KFR6c0tzx$&l~x=)x z{xE<%sYlp6a^2*?wF*cu*&OJ!;l#_=OfW>ra_6JP9!|JLslE6`IZ%^~8KOsOY(cK-iT-h5GPn5|D7&j|Er=)T+ z$gqwfj7-JU`9*2E^uOJg?TmpGK4bAB*;;h_Tl)+9y(HV7`UVuHLY`A*z1c{@bzVQt z2gWCWqL45ZIYDSeKf{|{5w7k5IE62eym|ppXP&N2_dHXn9f1E{B#tpOZs1em_B{q~ zl?GVYD8r-$J=xV>L+SSp@L^4L1qAdb?oJ9n#zZH@9Q6P}%OT$14eCI8?Utwcq?Gsv zdMPS67FZ}cqz!xJqwnkUt?>w5$($QL!974)lGsY^#2mX{T&&x*47{XjzNPi`0(!C1 zo|a<4=IbJ^nHk0afeoYEu0szzjl${i$1%1+BzE0tnGJDTY=TX@Z%foevSN$A27cG` zwZqh(OS@uJu12yjLrFA4*)gUqUj9-Dz?#tbp#3%-?E8o7!}Nh>!W2x(LK+vjRALmC z_A_y#KPV<0wA3-;?!T;P+~}ja-XPQ$i%9Mx5HsU-_Bvvc<|aM0?{!46MDPl>>L7@G zj_R(7p}G69)U<1{cISRL5C!q0pP_4H2YQ`Z!x?GD)df9kn_0Mm={xmfuLA-K@%3&{ zq%m>u>oU?iy!UKbd;qS_gzjY7Fl!L8_ZLSQwQi@$KWlfVyQPrMy02G%KbZ;h!IH^I zlpuI-pWpz8HDQf-L<&L59M<<<;=+1RhYXSof|Mjg$D;@b-aa&ve?6A{RJpC92I76~t5 zXoyk2(4=GJQxm>?Z*_sJI!ayWtkJSvvp&H`AjT~CEEJG}Aj(M2`%PHT8%|wg4~A)S z70gmH2*_%yH7Ikn2kBV0xPoa8h0$z24uuoiXjGCpO%kLUla2Hg91sw#!-e?h_&QP% ziJXxUi!3Bn`Z5ajJYD9#&bex{a0D#i^{cWbmQAMM*2m+qiTC~@z}`Nb>p;&#;fyN3 zh*~U_F2s1>ceQ-=vnn0khYe74NZBTIh>Hdyw!6>?H`2GBf1tpn5fmJj+K^1tPAaw7 z`Z-TpHjDX;%VW`@C@{wcipb_xgeXwhl4N_?^x;s8F$Clk#Mo1*VsB|HwIwhp;Ij*2 zwn?ar2J(e2h9AqU>Ryt0q%z){qzu;IWt4JQk8N-PV)7u1MBAi(VvbI`FLYnv7)-4c zt-TwG&!<jil?87>=`EKOFbE9tXr@XE%$j!PNM>YafpF-Jk(+484kGuKz>Xh$|3T zOVq*@aqD5>(HP2!Q#kfI;|GA_#6e{yy z+vS*nS2#ZVcmcN5Nt=OZj(=er;jW9<#s8lp#tU&xYUE8*VQNBr!NmED0bws(5S4iD z^N*`R0CUq8LRGJ;q-qHkIGaWh

b8-okrs?O^@3Az2dmH@!aa_|J7e_k6WF1VKZ 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 diff --git a/services/horizon/internal/docs/plans/images/io.png b/services/horizon/internal/docs/plans/images/io.png deleted file mode 100644 index 5f49556fb4997073202c08389dfd1f18cdcfb542..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86548 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 diff --git a/services/horizon/internal/docs/plans/images/ledgerbackend.png b/services/horizon/internal/docs/plans/images/ledgerbackend.png deleted file mode 100644 index 39990c77dabd24b6b6095a13a77e313b078bfdf8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57458 zcmb@uby!u~7cNXmccVymhteG)jevAF(%oGG3P`trAR^r$-5r9`oq}{X-`vM@&hO~G z_rK3)`#jsZ*P3g_m}89heaA9fNkIw?`5`hC6cn2Da|vZAC|E`)C>RVxc+leXShN=k z3fkLJTwF<7T%1hF(ay}$+7t?k?wzrL0fRIXU9X{`fkE#OBR#UCoAT?|Zy2vtKuAH8ZVzLP}&DZxpl3L^pl}x*~F&C;+2-+kP7y2=x$s%<5IQb3+nJaek zP$!HU4)z+Vi7eDKj5KBq{Ob&KLv8=5KB91Xks5?vme-G3{l_~b+vZx8C*S!uNW<6j z=CSdMWOv1-5ZB`Xk^>e8tC%jo7e-D0ox)6J~m43Bx(*~%8K@9!%g!oprt z!H2fs+~40f{kXqxi3~V{UEL8uhoW+`TzX@LjR7_V&QevwSwmiq&)Cj}#n8mg$dtw1 z#vbew6qJBFANXox>TF2nZewlh#OE$Z@z)i6;5($5m4fWAOPs9)DKzAj$i(d&P060J zJYiv@5JDy+BNK2mG2>H~c=q@0;9r6i7S7K0e5|Z)Zf-1YoGf;Z=B(_zyu7Sz9IPB1 z%-{-UCl6a^Lw9CdC(2)){L_zwsgtpzrM0{qt*_rtX&i zdy=iw-_rsUWQF{~%Fe>Z`hUC4+0yL)aU0~9U$^}=uV0T7fON*EWa(~dttnw?V`}RJ zh9>lsgInOQ$NV3^{BNNDxl`kRck-~a|M$-S{PLeWAtT{abhI=Div(E;A#l(C(f0TC z0<4gA`p>%k+LXUq!LA4)3$XsX!$Qb@&(PtZphTdgB}7%-p?5MKH0pGn_H0sP1j5rP zBSHr@$i_gmX;;-HW#!@KKmQ8n7O3)s9@oOb4M`bMZw_g%jasDQ{&Kiwlf!GVU)N)Q zxA`_>#C)-d-|d9|d@s$(d^n{hoj_e|!E0Y|vc>Nnn>|ARA-8S1m7Seilh-*5NkMM{ z5)Mt>d%wGB`6pi`O4LjBB#rPe?$(p^=PnLcb>9p5L`X3>Nba&g=lABB?b)TO+Pp8E zA3FD*99zsSt{G{#c9Y+=Ex9+Xz9aova`$mgYwuLehI{UnjaT)34_(8|;?uDSaH)Dj z;iJO#k{Kbpk9Q?HDE@CG;3PzT`|zds$0sPj7wt&?)1DgE>bB^!nd!Mre+~x3rI`0p z1?xu}TyD|H((udifA43YY>mw=H}3iJ-uJ4YA~=E>hGLW+Zf0QJbH*~izS-wGF;eY- z4@2OL=KEB-flSa4(iw}5jm?8&R=nOTX`!h>RZ4xoTNjISEGgm_@{V~}&{&xN`FwgO zGJCz>q(9c`yZV;fH9E?-s@o`)^U`sEd9$Q>dpm+LIyN})<=fbh*zWv^lkaq|pWp@+ zzRJLU;?!Ve*)XGfIS{Nhb`O3|vCGz`SiHo|QiY>E`t>t&y=V~<7H>}rZ zvv%{^#tV*>Lwwp@tX9P%C&G;tcO16g7k*ZsxK+1oR*i1Y>)y&g{%qsLS>4dIICyJe z^}_P!_YHUqk{vCZP{XM*!vJYsp^#RG)hCq%Rj+1H!^~{CPkTN*dupTJkT&^9X%`x*0jO%9{i{%B7C4_8G}==<01etqFf64{U^=dXR(ulqlH z{_?(cElzPSQR@|b&qfS37G}U6EtzTF^|i>;e@)8*6J~dJ*LJ%ge**msZH)ADD53>X z^4@bDx&h?7nhKS;SoWnA@=L^jXEm zFRo?=jH6-Gt+?ST&CzPhsbS>vt)O}XG{bf0oVMIe)o<&>#X#_P*ZXET+Oz8p6j z<8o=R6iEOU&q#{teC?wr?iW|}*ORy0iqo#rjI4H#Q+-Fi+ajtm8TUlFC<@3 z-9Y>iohhK{88Z;oH6#CHJ`!-Z$B!1FYe_TD^q7;t zkyqsr3s*voW*gpADHwh1{YqZH*gK(e z{=TbH?w%Ww{6#7{YCw_WpJ!UtHIdzdri~Ky>Yzm?!K1J{;Ml~g;H1X4;%$G zBaiZ0_8az_E6|45kar?ko9#bHMYE)7Dn^K;k@{XNXMUrqmi%W%fBEug2JQ~*BZ=){DYG6MT;ZiYN* z;k*6jYZfYFgJ?5VR*&o5+Ig@;Kg=>cHatx=9fasm-g_TOKR^GjuKzN+%O_9V{h zY}y<)OqwPox5V43_|fg%ZtY^Ib%jMobz=U>f7eMc8cY;lFqcoh>-d$Oo#Lk#$+^C8 zp67ea&8Hm&>Hl?%sqw9)52Z35vf0k@xE&91HzetK_Q`Wio7hIsf3&wMDi2NbIh*E> z7a`VG>m18TP}iH57rI<~yn=PG97g)3u;fes>DGA9?a=_M?YzsR%b-Z(`GS|B^cXT@ z5$4BOro$i%p&umfOVHfkRaHXV*M^QbaE`n#*ONNb6RVnzhxxNvWSOhu(=0!xFllKV zogDU%HvBAU7|xbpi>z7>ee`83|GiLn2hwgcxORRuT9{;;edx(J&ry!t$8w|WomsmU z!EYHUalD)9aa#RB^35;)V9bD(OGQv5pQ(dy}kNbIuBy! z;}w?Xi2zMsx?@44ndFbxn)Wkofnt`>iQ_h0iPgE^&ILRh3W z1=~J=Mk2t&I*cKF6QgbNPEzAKLEEnB)X{f|xTNKBBdwlyBbhSTiTO^)zU_gBhmQMl zh{xn=-Ih10&&k(V2lHgZ(7CB{WAQuR6lL2;<{COezp%%yy_b2iL1{1WErI1-Cl1tI>vT3ttV zDvD6gSlY;eE;G#9d|~9akzyXYKRB9fKx!n$Ux8?I3u3w0w>Skp`ayS%FVu+|?Ft<` zeR%3RSj{bGGggN1l=aDchyC%_C*z98Vm2QwCw1Liz{1+sZQv!a1##|HjejZHHRbx4 zPVDmSMO#5Yize~9AwSh`ZXjTfLW$dSz4>|%<=OHRlW}=&v}aSOo~%tLW4Sg-I*!sB z&+x*^Z+b^CgH5Gd?r-Rj88XgFQZIq3|c-o88kDn)AZB9>CNU5?05Bzi^SaV9u` zQd*rO={&+t?{drIc57yb=bc6bZ9lx_s>oV%|0+okB6=f^As8S00)L=c$@Y1e5=}q+ zy5QNAv9wO2Ci(r%rf?kFj6DDAeREeao?VIT@CTG`OfL2z8m+Py|^-3Vqn|Oj8&<8yEDaqydW7&hJ2PS7vrFRGTWnr zLCz64{-~*oOi{rgr$=*~2WJCq#GG)wmz(eRfoH=XpdkjHW^e7f_Z{W@&G86nGpT(m zw4ull)7x;5f{J}QT-3YrO12P$2i>g46?DsWJtSUr%N=2~BuGXf&;zW_z+FZMGJ{&#{;8tkL((kGlB2SqGE52en1UHgV^5=4kM1WXW7p~*|oVJ zUq1vfN727bJWI7IaqyNa%Q%$XZJ(5xk>#bK6aGkM=(n2WMO{gEqP`P*k!&1=bS9ch zy*HXI-qCY3_Tsnj#1|P`3$P`o{w=d#%m6`JT&b@CV;)JT52W=@>6*|HhFmmd;o1F zjf$e63)@_+JK@DKJAE}SnyD3VQW!57E$xbmG6 zcQIqM#(h4@?4W?C?{QJMmxO1 z=+^kO?cjY1ZG1DZC-#Cgh5yDVi7ZZ}Z$quk`YpcFf+>~;inwR&(;(J%VAGlrALw+o z&NtvItW+QjfP+K2a=+)+BHO-eQd~VzF`fly(|oyMyXbQ+kDI=hBW&oAz>LSg9E>vu z0y{yX-g0r|y6m?A4mPS0hN)WXvNrfVfWguFr3ADqHd3uNOoJ!_cF`@lX00pN9&+)a zo~Y|MbWx#Vm`@cL%)$`BNwRD_pZZ)_@-Y6PnY^4mw*S~SqTM{y#O9PNcV%H>I)g$S zeyK7ZzTZ+2k$lX9pdYcZrtk5Dy<(4LlN}p&Yqa~C1R9rJ=^_OMyp1D#8{6OQEGo0; zpTUqf?^dn|+Tsl*aWuX2{S>7BG1h&j7+W^cN9~45#^M-()H`NPrAU59vg3|LF9pek z8Gpa510BV~b2if?8fOfDg?~igcV50VAvHF7E~+UzgUP@Ep@GyfF(D>e@%p98 zgLafKI`=^>$3E&HY=0xMbdaaeNZWc;Dk6zJHln5qPd1nW1G7Cr zZo@3iW7fydDw@K&m6GawQe;!(T%#W0Q+legf2QT51Z7M;+V1jO@*VIB>0;d|{J2or zqbgr_-1YU`5bN0V_XRFyllO@>HPau(@i2rgmdf;>hoK<J6|-!wi=55AYp#SCwM+dT z)hI?E!aejg%X|7owD`B7LSsG0iK&RC$=Uy-J2X)M6U3c^7|4HfcoZ=Zv>`9C_%E~Y z%bh$71WZEc5BQg2zr_^^GI`KV>g(n|;NTNDfMw9WqJ0kj9YBhtf^ME*`4Ie$Su79% zmU)=-NEPX~*nk=E1axCbamDic7)LPyPw_;8`xEvbld=Tej7gmQho$%ngpwmx`5oLL zP&3A+#V~s468teYi7afG)$X@W>nE5;R{PveEwPt1r?37W7y4YCI6y8O=O~l;#8C3b z^pdauv10n^Wy~M#)DU7tOxEF#Cx3YZmV-sSIPf>0Eds|t4ww{dtq8_H=4}ePaVC0& zNA-KV5)xRZgi&RkKUR;05_B`5R7mzmyA0xU&4S>>mET+|!1hEC02-?G;12+*DhBqL zx(ZkQ_pQ$w0NreHd}Db2M>{smE?7>+$HMAY75^uaq=uuBOJ3^VCKu{BTWIo18m=uq zG1dK>So_y30%Q=Ys;lcOJYSEL&C60>{<+frh~FfjOsOMDDAE#A*%-_JSZ{O4K4_XG zl>X5!4;JuSuwwWh`@k0o_F6zy|pX$PS!h^LF{6Oj@$EYB9`i*29|LHIKH|rV^m6)gO8d<#$7^@3pM{Ma5 zUNj-z9CTpV#!Ax^sJr${{pzp_5%uAdUI-v$o_O_C9nO;mdb;Rj(D!BGZ}yS31)=}0 zSX5zuZ#IXpdYsoUi+^JT0zN`$xvd5zwBI~YK&CX<)sY?bYMI}*FEvx-Iv5EjwHfQx z?=-|~kz>r>bw`#+lCitv5#c1;mnHxAeak9>xVO+<9Qh|Nw!CL$^e0~aWq$$PJ0mtGIY50o+odr+1-pGR|%l0-;MptZ?c8iX|4VfIyHA|BJip z{rSbaGw#WJMN)2%Rn!B-lw-L4G2(BQ1#lGiQnFiQ0P~6c?O^ESmp~$hghi9#JO*+@ zou=*2g@HLvfHEMc)AAVw0cR@;@E9>|W3sHfp9{24^BD8~wisvhfV1H~wR*@I5gDOs zy_%6C^?CBdHsAsNr@D8*y&F8|9GByKnO>s-gNj>PzwwZ}3sj$ZZSKBX{>_Yzb zh$3!~UCB6kSp)j1#U|vo)zRQe$iv1zuN)C_iWa`BOEU~5nq#UQ#Eu16qrs5D3xGh^ z^p=~Wfi~OVbq&BQ=)dmZD0W-$=$S4WtX=f|>3ef1WnM!KE{XJ74k4KH+HcwfID92$ z_^9^E=VCd`77&;ve)o5Kc}aSX5KdSdfau2_O}A&W5Hi*k5N|1zrDtdN_a3yu+v1u! zX?}N?y5y2b+DZADp8bHYp&7ivw0kFsU!D_3>5?@EAzaqPFKafE1=x~J=(TtQ5GsY4 zU}OGS`WG;Fq_WbQw)N|dK?oQuL{bEjStM}iV3oW_)&XG_=4p~IAdy7|EzB9~wGvM2 zc)j1^H_S*}4>S$@H&O=Kb$@|Y#Yw;fq^l#h-}3bkI|Y}$cUeGzG?E2 z#WtI?Z91K>-|eG)Kd4FK`gXM-Kl5S9aoA@2NU}3sm}jRYkKaytkE32nuT6Ou{^Jsd zTY%r?j@E;Z4BriJ`op64daToRp7ty3efki!jTz>wCyL@w(sFl|{(ins7jUH1_1GTK zf@i&#>#ukT-KlAFCrPnB5d`3=X^?ZVNaX4-kS z2X@{>Z3P^Bj7stWrph-RiEz^(wVD&YJB-FeFutWu(&1Qhm@*6t(M&V!2n}%umQs4X z?|1)bSQy}(o+5NHUaR8j#MLW3uif>=9GrG(zpcEaFAoTGMjr$IVXvRD6xDRW@BVg} zk!_d}k(|VoLp(@Hz5gC?_;Y}c+G|`{DZ0U;d{$T5eAUm6Qn3=j$aiG^Z0pO<5+^`Q z+0i9A>m8zSGB8COCz=Dk#lGco$Zk#);qjWD&*^t1g`1wlF?R84KuUzwfvJ{27>QL2 zzJCJ!eMIpa3$r{_GTo9#&_hMv(DFF0NCSZR^SD76-{k5kKo2-Y3miRPcy!C;&}8%6 zwl{{{&~>?H))pmi&Sg?}nV%EY*HW!L)9GHA#+;K)r)(N8?S-LQnDgMXivZu)L>x2U z=l89!7{T_X_QCs|rx9X$bFdf!Y&ypSm0~=Xv5C_UOD(7ziUkUBDxdlmsK5T1kWWRj zTct@+sI1YOFUZ$0toN zit_GTXNfQFfNkR~#ni&$ix7DU_oPx8kBb^RfQ2$E1@C_G1<24wQ{kHLuB^iHivcg# z386q;fa8(}vjsVOP*X4WqW7wT@U3(0PQ?J9u*L{6K?AB8VRCkBjL%;ECc6J^Y=u3Q zVx=OvzbDyL@sjV?djxx5GSc>qbf;na6J*8YuNtNaFWZ?S)oF-H&1iw+$ixgwL4j+) z2`0i>Ni&EP6PhYdX6}D7E{9_#kz8+p59GZCdmslmC!dnHbP?za3?du8x!~tl--7 z*sTsQt@AHfOUputD9oyUP;Phcyc@bw!gpmGl=#YW_54?=I@%7`U!at#NiX$$@~5N- zC*vu_^p{IE%B3U?YP-{v$4hUU7oBop<7HN~R%#bK8P{w$@z=`R;2#C2VA%=SeQiI? ziDW@`0Oq-UFwaQVw$aIm$6MM9r;WM5bm=EdEj(zg6gBkYqRmH-SB=(8cCZK;_D^{!0jn6x*Bd2F$qScAT)5aVM&LQlU0EsV= zE?;VMq=+UA7;fZqEQu^_Bvp<|3Oj>4;d~I*v&=CKHag7siX>I!pS~1rfQ(tseM!vy zXLJvvnPr(_Ta+;%Q!6bD@w;?zXHf?M&|K03zJ)E4{c5fJjwINN&J{P6< zrI{=ahs-TIf2=(B{I}xr?uUl(A$0DCsURZ}x9-p05epQ=aAk})54@F~K_SfSJm5G3 z{H=DCy^ix&$>Zjqk$=TG5i3Zdq;*?75g`&y(+WjO-8Nzd*WrZv^^z?akkxS+?;kjD z4)YirC`s!CnRV|4!J{DuIpJc$uQIW75_EQ&O5+Z6DZvfivOT*-gmr;GBivlob6@b- zbb$c#3lfS8VZ!lBIOIG;w_c_Mv9E^K>YT z5Dj!)7#KuH8YkMeQ6z#aWAS!N(a+Jho4QVAV+Oc_pEsy7#6$BQn;D|n?azhq=eWJJ zN!$k6d#HvH>U=w~r0U*s&CoF&ukgDVcsyFaNO>6;*osE!H)C+KlN$94Efh;;9Tb*L z7-da>*nUC=b9;5_wBRo^_>2v)`(vOe-x>a(GB}SchG$(Peho4!*o$02onH7yfB+2p zpwE)Vt>PcX79Tk^OW2PRNXyX~{~(_6Or5=LbrSgF>n!5rz>YVtMl!Koc?yG6*>8Zx zp+lZrffbLUqM8idZbcY@=fOr97#oO&bX{gJYt+sgfs736Dmd6nMn~hzn=0wFz$4^N zCEh2pWe6Y2cJ$t5LNf@)JR;o<8`CT{6#el-Yp0vBr*7Ru=5Ti?%vDM`pRxv9^|;Z;Ly~dRJUNypKIoiv$KXIH5#RcC5exBh`@&Oqnz+{@O^Cf_d3sa{(TE@g zld)Diz{No&i+r3{8YEu^6JDpBt*!Ol9ICfwXj zW+%f-iz5tc)IHR!unsNSKG=Kb!qV_t1UmMjh*HxBm2}c|Xe;c)+>#-$eN!})VwI4M z%K5tBe|Cnyo$kX?AM%B#rAHv>M{K#~-*0rnR?wLVdf~&wJ@2fdN4%09<9;2BiKgB` zr?Zw;Vw{e#X>Y*UYt%2t*9<(yQwYuzm$#J2`iYoTRMdFXPwj#^E%>*8R18usd6_H{HI?tsg!9f`J%b3}mCeZ4ZLX3lY-voJGc3vg4Tw-6xg zNBRl=f8EM00eL_n9(PA`tbL;hQjgFL0b7RvX#9(ZKH)&e#{)HpeP>tYxpX!m7o({n z4J3P8g}6a?pPYyPLf>!Cp}K;Hl&1Zw(#s5Y$_zXDCC6Q`ES|$GJSaOZ@qbidd-^Fp zKEYE-mE_+sph+$PpEZ*ENoflt_YK+GCH=2OsNp=X&M1!H)%xbM>cFMC3QNF|If3e& zwv11UOy%PP`2spO${!bpn#Tti)%+K?1iu;fQ71Wv2t~4d`Sgh0NFp=9zP#}jQa;H+ zYttdv4UHrYTQUIfV@%qk#W#J10h+SQaPTKe0D>pPX84G0nkm?+9Uf=UM1=dmxXppIz z)}4G)QLGmu#SH^;>4FWQc!vq`1_~4Hx1pm!PRMFarzxv6_0MSWVFS!}uiZvVq@$nDQLNiT}+Lx>#^#0kM1fm3WeV^~Q*NgYVin(^66F_5e z{X;Z~2qLk$w;Xmq>KKa#B?R4Q#5|*a37a|LH2sWbb9D}TAJc3dt(CyocdI0vF^M{6=11VAF+C7j^zogkUPamGW=dRV!x2gVqOac;FYDnjvw%SU#v=-Cf+{g=i151Fj-RsYADlrQp*iBQoH>HKpDt(Z;*%uJ*%`| z739VKEN!{-v>@O_2c;n4X~4J7gREcQKL3h9-}~!4aJ+_R!LH0TQ%2;r+1#Vj(09Qp zkGG>@Yyr+g-(<_1KY?CWkz)_hHh%l`XzC3BYK>c=0~`ZPt0bO3$j>1$N+_#@&I}m2 zL+{94>oJgykQ|Z^XY4!BI*y(+lzc!mA#bg81tfkwK!dFAi2Oh!O!q*X1z=>}EX^j7 z`cW5%AwR@my6zd>45?M)A&J7bASk=%ih`%A=aVgW)#9$V*XM4E0*4Z1KFbu3IY{+ zs{@s@uDE7~fz<}o;T<)9P#OY)fixkkcO0N^X%)-M?V|{>_Q8Q6M^ppsOC$fqQqU^h zayp*?;7=sEdpk+rVF%UQB9AZ#kF(Wg{7HQ^x;d2uPetD_(RB!7QnPt20S+D z0;f>2e~{4`AB3Bp&$~spZ0o==P_O~j`hjW4A~M(OH8)s8S4m`9Njf}DIKB-3!~hpQ zBZ!PW`V!6-kP(3q$m_;V<-lgbYCoZVa|5cqTm-Tu`wnZ~C(JM+)HRS?(|!JA?*rsr zw?XWay?K73H#ku_%5o5t#@5h|ib9GT_1JBWJ$$$bwf&8Z0IYG4PvmPWHBJfh#GIHM zF`b>Dvw640wYQad<~8df1bV$Hs-G}q5YHqaqB1fALvGkDWwZcxg$$#9V$0(fOC^wW zIN_>YjsjGmL3P~#P9LeV5D)@xN=y77h}Y?1D3k;NHTAtxMFzM;c#77sEaPZFt^zFC zG$k30m5^eD_E=Wxh#n8_d=a8G4|9 ziImX;!vFlh8HlFjq+v?>)V9e3!$hFtFZ1F$=D3n-XtkLPg$+cjD*(Ghf7wvnvPO~PXHz^4r z-z?x@=pvXGp zLM1z!URehjExVe8n!Wcqlh#2{LAMKFuaKIu3!&CvVy0x7l<5P=GL;zaPB4J$I zLc*&hY@iWm^fD9{t!PA!994=15>#nupd)Zn*~$`!IVFy>Ga9+Fa`Cr6O|&vp5zqKk zIAr^ExJrs^B`G#pme0n!Se4`p&0ie+s5{)ny^;b(_JkJvgu72ZB!N&B^?$lK=*-p- z_!y28m8;HF6PJ^Lj$+4kcrX;{*`oWE(P-4$2Q1OXqUCIDDx=T{#F(z}w6@m#V#L1g z<QI@XVHle(4$wMmbY@lKa1Xl8d^LPS z;-GL`&$<`jLxQL+>twt6v1O2LLsN=Hmfe985A>~y9h904q&Ms7y*$^V3SM62$>GY8R2vUKMZ4& zx0Y*N5)7Rdj*hRTi=~w(Ol=LoA&F%Yo{yU)TDK8Vmtu!c+ttpyw%u}E&$p7A%Kpt! zbRb>$5&tSuy7U{SUj2~+LZ83BLOUJJW>WSyO8)h);)oZbc6F?E+%i6mVll`4(tSL> zmde8zMX*FIwPJmB@sgR(>%GH&Hra*Lfg*#K!y^{BL$f0mYmMxl!;IL{0>Y8+jw>IL zH{JHl*b2+W9WPXgaqOqoS=4v$1t5?hIy5OObCjIN;IDL@vXS_9^fXraE+(YEPw4YV z>i1Yy`}p{F@cCDP`Xx6k#j!_976d;gUkQ@67G(1g3j6_C7yw$td3R|sP60mXOcCYRrV zD<+Q?@tz`>p zJJ;y1PWw_Z_-0+tjJ2Jq2VdJ7%Y4-$-^RTsWxPk;7eKamIC8GAD@E&GW1D%zMI9ALslTrF zNwxeajhkY$Zges{d~BvFY|!f8N!F~9>bUHV^Z~sp&AdK7ztYj8YqD6E>Drh+T>>rw zi|~nsQr*3bO^p^)al2%eCSuuFR@NxO57(LzW(BUFXovM>MHvH?^xkCVwo-**Qby%ZD zp>8uLR243mG7A6FSN6!WObzBcbn^hO@dOFl3?8Cwsmio2WB~d0DC+wc_`oBey*CH-(Rj5UL zwlW}tA2ouT(hpY5&Y0Fl4N3h+s?>c3sJ;JRL_}pZhU`2DV*2k#_gR(?NaXWS80$?< zwsOBP{Pruj(h|n{nT{I9Vfc*K`Cf)X+IWH#0M%11eexmTKW+ z&R4WV7>oazOoHg=WE=^&NM)+eM66mFR`aHG zMV0`zK*?hhq=PUr5K=}4aggt4ku9j}*#m@o2~`S2e|0VZgv)Hss{P*u=D)Yrd}^UE zX;6&;y6{gcs>Tx0?8^zOnpAtE94X?2a{+MPocVj74NytG+8s^0?|YoKSa;?sh(9!6 zcv{*tPb;T+jBf$hS+|>Bna`9^iWBsT9DP&}9>)pPXnDLO^0~h~w*|`91gAD$)3Cg% zY>TVyP&|0SLZlLFv`VJ(GVza#b{EdXQ~ z=h8rHng>$R3Zb(s1LXlI%M97v1FX4Yx*~fQ?jGs=g_LQ^#ngvqZ;@q&LF%XSVg?{2 z=96WRiE6y~qWsS9Vj1BaNGLp+b~BEEqv2~u?t?t0f%<*xWDd0sMbYN!LV$1@fEp*tXY9SKlKN2kDbl&D&F zxBGq#Ko^@jOiUD|4@#MLon>Z1D~y*>XT{i_Z*a?dS;XV3^UPb%;hB8=5?DR^&2%RP zLJeUs)jc3z*tz2j`#1v9IXDsWZquId{iPyK^=GfbE5HpjfK4CY1(KfVi)=2`gBN$n zU)L@NDd@fq?yz5!noQG-(5%k^a@`gX_(RI7VLXE$r#bxz(uTMf1#a}%es-g7>$=Ua z<7*fM#MpvLH5oGvfQ#WFWtO1_Dhj;RbBY~d7#NFx)iOgw0J_2y*w0&&X11lVW~4PdfmnmdKbnA33j8^ER{6UT9xt`*LV%%~R{iDDj8hM>}d?r`u$$s1?AuI6zm2$^B8 z_&Nh7NMbuJ(P6cl*DSn@G-vokGMg_hN|W^})E%HiDUEI^AI(fj&p0Mia8|-Z=}pV} z#>f`pOMw8>$59)E&ezKf%BB-dd2Fk`J~JF0Fd!OcEUr>^6qHTDCt^9l+tn}%rw&9z z;6$BMpK>x#>=Jwm6v1`0e(4}F`S+Zhgt7(Kaqgr5o0bt|H=PKcV_bs8_wFO(AI2bo zQb1 z0&-CUv@ci4m(lyVAg0X<0a73&w}xvzPm~xEy512D&lWM$Ns$(HXSqK8xtVM!8gBfN zI?_BnhUZHBz2m+D-&XFjcyl?nxlL|n=X!I{ysC=)`X@>sk^8cEG@*~y4y?yUVMGdV z3{qaM2!(`8mv82e`BvXJL+aN8(BW<{RHYB^lO28gZSY-D>G~Re4IfxBe`f2>H|lY@xm}iEHXR!Hu7^b<~HKNzaK6Z$ubaGfC}3 z_N!(wXyPagl85bpxQ%AWzF1^>9bokQXA(^42hy9cqdPzqgzo#+tG9C%1PdPq9jDhS z_BMeeRN8)mW4Q%zV8J)V%<)#6zGaV&))r_6M|P-8?N@9l#p=2!nFenAIAP6`?{@9{ zWcw=}Eu)b}0@PXfI>}4)WlXbSJ_&BT8T-=MyqQXh8D<4)DNyxg`vLd%a77QG zE@^?QK+j84Ll2*>N-?ihd#}*91wQ8k!r~;|fgjX!9DUEIHt_<`C!>U}e%NDRLTX}V zw~c5M_ra?QvB#{ij(W>zbE{q{b#t6+@0v%L-#hk*jlg=mFvn@oSJQR*p8I%?zx(aQ z*-eJ`Q6G7Vx)HKa14LPM(hILCa_Y(?rBEF!l@4{`(=-ghbA&}eFH*7Xe+Kt3&i~dH zuwOoJHQTVO#cF#$>mP9C50@hN`8?~PX$bVL=g;CKZV~#I)bOOJs1>MHtKNil zmru|7vQ_|n+;>TB(kJX*2phx$&mLb6%7->efCj^sJn*@Qjcw&U(-slcwW%dCn8)^-*YRE*L_M*ti@ANXE}4=NGB#+ zA0DqiYjMnI2T}{bR;H*Q-9id*G&yM|S{#V4PlMU*qY@0P?1(eXUM(&go-cxzD6ozw z-V0QmJQT^r#d}nD5Iqg= zf&bp`2z6T#_>;AcL1b8@&UmPWrE6-=M5Y_g8o#ARQOSj?#k4VZC9|LhkFscqaus&u z*Y!?YVY5Y!;*>w7rw>7M$6Smu#Xw5RHfyYKC zF=JMwpEUv5RI6@?c-lXyjnBo{z71-sa~3by9$pvKbLB2_w&x8@klYblv*y4@CMyh? z_uO+eE8-a*>_-LY$5hr(xbPmeiPhGs3g=(+zSEsYm zHa}>YhNN^~q==J;@6&IK`YCap>u7ySD=)u(@SryS;HdRVV4z4jJ&7JBH~*GKU~z$l z?psC@_3L@oxmsV%R7v@Gydx2!r{V9V%|hgqe0LrM-|kHbruzCmkE_;azV{@_EBY4q z@#vK*OmR+8Z|lOn`nx9>&=X3>hp^aTZ(%omV=yO{W~+^g$x;fiY4k)b`5+#`h8!6B@Mij9PLxvKkvj7zx5$J%Jujj?w{sG|*3eg&4pMN~|L6Y# z@Ev_;V*0bIoKv(&;4J+BcEO;-U-xyfO3y|cqb51a&#T*L+XGbzf6}b_0oIl|NJ(0e zLq1cfGxg7nanoQqj+<;$VN4Wu&DALRVD^r?c8uVl7f!1=r8rHViv4w+~Kw^8Ax%M; z)u1jT)(EL53m=ibx+E`(^`&kGe76W3GFD804><#})pHnBYe1X#89aZej@dDGLHb#L zbN>cAv6FCqg#)vjF!RFFP-#$vw5^QDSYLP`RzloE1TnHB4)LzD#S+!_`D)p#;ahEG zP;7WjO){_|u+rvDJj@F#XQf>pfZw0wXGH=}>{~BIxYOA^j4EMIIY8EkFY3@9gpZAJ zH11(E7a6JEiJHZX4r_6b&rQgNW2S&>4;PcjV?%AK$T{?coNbULu)BZ>4 zhGFWw8adx8^q)PLhe^sD?=c=m2ZF5Xy1U$6)Op2Gg^I zXqaRNui||`5;Dmo@R0p_8SQuuz(SIRSMS|d!fk`k6T1xxr=aMp9y5_d5O4~DIslYp z_5;!-L537I@kGfV`6sOrYZ#RDKsavmDYo)?&5o$*LcL!xnpM+1{1HLKUcp! zS?{MBF>0SZ)XJ^gA2D+IS>hP^J)dOxY$v`ya++4paS3g>@h&@_-Km$ei}Q`T@Bm7w zxo^EeZ)}Rr9;KP&XRNeSlf)f}x;YZJ-r+Rd6cPJan2`&`>Tri}u?7EGa|rG$#>o{2 zIbq?$rx_6XM5Gu}d@ZB2)$j)D-v6@4uH6wKk%i6uB(tPF78X7|Z((W0H9-@nMQ_mq z*2Uazf>|+&>HW%kKPF#h&x?K<|FgtzC9AecwP_nQ-`Wd#XU33Q(4RJz*HY`(tihgl zaUDo+Mx&^jQ4sPRa6Q!<>s(6XZfwM(R!mID9?WuY3tm@PNTgsAKjM1OmSMx(kBm&| z(;4nn2mi1Bbz zBL=ZsSs@fLmudLREstQ@M%xZ}TgeKsmWbYrLkU-2qrXRFJXjQmQx7$<(VQ9uy&<@gYJkdW zx@&CNv5xZPhWb%eQ)7bgM_X+U!~~M@d09I_ z?LEPwyp}Ia$`M5CHm9Jl`}nK~D9kU6&nr(cDya{TM|=iv-)8-oxovmjs*?Ch#&o;P z=dlRhq#O3?LYYKjq&mnSno4qCy`Vy7N(nJld8Un@sx#=`Zbt?qoXD4IPdf9gKohsM z==Rkm>%deiEWh%ovs7?|(wupM46cgPuKpB~<<`=Q5T z>}fl2TQ9_{SZ(McWAM9X+_K#W?+{;OY3R(kr7nH%Yd=)oKsXKOCP86!J{brPOfzZ- zC14idx`dXFB{f|&e0|kfV%mg_*KXw2;JZOrCv(~_@?24?G&tl{s+EBi3d#Gy2h0n; zpE#92p*r1LpUROIlw@&W?>%QMOQXNQ%1BL=w(xSzF=27pL0^g?r9->-k9{2OTc5=M=_878LP1ojST?K`(Cud!P1w7zUPTzeWin>A9{Qn61 z3aBcSE?f!eMgd7Br5mJ6Iz{OQDe3N#76C!JL!`UAJEa?>yF1?;?_c+>x87Teg&-&9 zOzfH2`wQYb?PZ^D6@D(FEH?g02O83;mYM$9sl5P}%xN`Dbkp)<`n26)^?Q~MeX}dk zI%)xcsL7LIALTQ(ogNsw5Y#3--R-if&=J-EG6f5M+n14~Q=JsYTlz$ZA2JKBs+;wS z5t>ncWF#<|RP&MTrSgfx5u%LT6SOj2>FK~?6DOPrMzh_icEHdHlz%g?F@XtlUdD~Z zO=Ug$DEJH=saB5I{A?>W(T{`zF7bi>tpab27~uuqOBA6O<<|YFhY$2k>=buzoQDNp z6?Xpa@kTW4hn0(ojc$c=wn;dvMIGsZnQHJ{@C>}F5exNiCG>c)=}#(5GV^k1f ziSLXSWp4=e^!0ugU@-RIOA>;tyQBk}F}~CYG|@@S10_rc_}mQZ$`IeX@o+ zK#SYT`H06*apyYs$Di>6r`wPpC#D~vFK#Em%?(K!$CNDck(q`DoGsl|!}o=rg=Qj? zDxgCL650l}Kc$!ED3RYr((6U&=X6F<)o(k=pT@~hAfiJ(X1YlEl$VPnid^(nO)gqn zT!+b)<{QhMAceKAX$(CgS(2fZb}kaNYX0Gpc7QRPQ5P1Q=KYP3s48&)U&uNpZf7K6 z!`T&-anL5b4AIJSTC$<+`C5_@|8@OwfL<`Bn{C$SLgFBre#Bs;*9@eVsgd8}ywWfC zyALH<+vJja=>e#@hMh~l!xk{s_3T=)k`ynVsP&WUT#aSVyU|&-yQ$X^br?vZ-XYbd z$WBGO)y?0($fkCXtBndDF{-~DH4S=Sq!~kYbFciF9!qQ+;qtX*o?6-3rDd<3eFOs; z5eWgKK#qJyX_U8T#{Rs>1h1QtCY&qLwz;+k4I-yP+%IyeNuthr!qRlXFO7O=AKqbwvkzy&1<8{ z06~2K&|i)yl%>sWrU5g*zln3bLUm*Xc?x^au9r2lRcx=()pkA|=W}JKo=8gPx^WU! zk2^gpdbM~BPke^tvPu&bvy`CjX@#_IsyE-Sc4#c!UOMU-ItK7Y;E=8l-MGT|hZq6D zWfnjeX=C!nU9YQ!&AfpNc4j!?APPQq&8$kF*zF|ndiI=9kjbq>#=BY%5$@L1NgxpF@b(f zOLwSuD&-h0t{T&EA~esAtG+Wif-0pHmL*DOrq7X?jnsdBO!3i~3XO9uK%z;0T7D2kGbt@~d(kiz1R^%rQs+FhGc`O>3A%5HS(KpS-O;_oO3wZ2R)I@7rPN`#87+cATQo!7WxTL z8b=l&s7@#J9Cwfa%tJUMr^uptBK2>sBfTI%`q7W{e#-b;S%{4c6wn5z&GPIP5SBfH z*$-&&mtbz@ac-zXkPA zaADd#hQ&L(CYoq}Am1bMjwQV)ZaJs-?6=SfcJ-+N_(R|0OT$<#8DqN zr(e3Z_mKBWOMuD1OBpLA3e3K+;@vC`Wfl9;}@*NbNb)#g1-3=tGYEFd7Gpg;;Lo;s65Q*@#Hk zLiCK;1|uf%$c^s`ssDiNFu)sMdWVq!+15eJ=b0&gKc=;3KVKbyuh2?f{D}4 zHxC3LkKAn;3n1kfkM{>DE49X5VN0?#{6{S?zb_pCdD9w9Xwf)6UVxfkfUx6!bJ0vt zYGDJ!>u*qU5CbI#O%@sILxxSbh3W;k|uc57{%6e>g-#J>nxy9P4-^cEPQIRMKz={uDYKUqMRh$KA##ZmJ4opsJlWIX_8)ox=nn_0Y| z`~kA+O5fe@by+vz$+#Xb1UBKnaS4>j^ieGdo8P5TZNl?P^2+Icbg0NA!P{&&iNqZ~ zoOs7XY z@#6AwPQn#GqqJH5lVJ`vu-TXJ)C})DG|^#dO5OjOx!w?|$VBXL;=4_5S-05h&q8(a zwQCqvvViYmZ`R4|z5CZWClKBOZrZqU-ptyq8oTgR^bX$yY^kMozB#nr^!J3%Y1XNV zCqMu4-gbDFwf~UrYhwoC&Ti~r09iBVJc-&NwcEjRhZU^h9BYr&8w1oLm#o*h@|9G) zWZK(>9JQmtg6}a(9z{HHYN&SzR3bO|@;zJULL*5Hv&f0H3dnpuMLmC1X6n}EAufo@ z!}67U5c&yEo;7M63Kf8QaBb|`PhY)uhgG&lQZ(@!RMiKGfD%>uS5(epB9X($w@s;=6((_B4MB8}xCo9q`bI&_j2-VMVB5UYxF#pIFepi3 zAo+>XcJ-_L2G2M@+XFrLo`k@<>$h4zhktCD!T^NHIuCOC5mBL|0Zdy4-^6(jOOxJMU&NXF!b_LK;T1$oZmIY zw^yFLTT~0_fyDSHWN02*GGFPh-wlKO+nddOb1WPb#e2H<5|)=wZE6E0cTsP#V;%1{lhMEAOKMsn_uE#ub*2zV8|;_5bvC`X%)OW?_ZGH9B5}ESQQ@}= zIN`}6YQEH|KsAp1%#30LTBx@>*NwP7pr<}6A`MZ9N;hqELtFI-VMPb}L{_HCMBRP^PN zn}Etr`Cf}5s~NxScyrz-7_1y+wbE9{TFTBxzcCmrxX!y(en47?L}#VFh#7nMe0Xz57f93@?0m6)TciUKZ!8()bS}LEf?-eSiA$AwLSte@Hi?vRD zt6o{P9(4{ERW}8GP(;flpChoWc@>pxnLQ|`nk=EqYH&KhDjyf&Ljj|-I#F};qvT}=16XGE^tb;8(lg_%l_w0fhYuACAkg1*-v^ zXeQ7@>CXR;wh!Kk1L0Y>LRvVs0 zztR-qQuf2~V#QwdFp?ks1vWU#@b!|+K>NiSp2%L| zWPy>w##4}lH~G3cP4W$u^KvkS*^CYTNegkuk2rXi^2}oxcP9X!<(b?t@D+~N?nQQ_ zorb^mu`aRDEo@-^iTbc_`TBF^!*tFSl;IZg*CE;IUyJA&duF2D1%}=6H}L_yJov@- zov{nxXhz&r{=i=))Zz(SPke*M6HR9k#+m{+Ds3v053!NLmvsxA+SkYzd@;mxPwNFr zn2#T8+bPT?OWL`1axy<_efPR0HczJ0$|c)x59@)QLrtNFor61o3bQUZ?pe8sud~*9 zual%@jee_!vKg%%f)xf)20Ud=X$>eK3~%srxKb?gU^?$-6Or`1peOwOMV#{^kYYm; zzim815^ZZtFlPR!yoP?EINj8Okw{%$-)7m>kgV5kct2F$dx&)L&VNnb>*l@n?NFuE zA@%6DLqIrbH%)QQVs-D1>D!6K719A08`oz({2n(^<3ch55u7E&6Z?Gi%y_%{3L&3c z1m2@KGPiYMq%hs;Iu8guR50m&4gI~ki;{6|Jk&ibNA61#;(N@vVyyj{nFg-hok@ld zERgm}smoG?U`4O87=wV3(hPmILGZ?o_=M;!U#TIzC+a?-}_Dw&d5)xc>hq3qPtLoy`8g3(VUrM;q$!a{(>;=+Q+dBN*3iCdRQyE&? zpJ_I!UBXu+4@ZGa5(Pgs-wQK2GEi*aU``z*3f~)vO`b(CQ=$&zea5Uo%@7fc7x3Ij z7x_9J63^AS{8X^6=nlp}S2i9+y;jciHE(Ge#@X%^Bkb{L25pw+ZB9QpQWNfk+V=IM z@S?4>_!|2xmFX)EmaEb+w!J&r7I(HyJz??Q3~{pq_$m}h7v^>c>}Rd^&DN;?q>i2n zgM3N_`;JTLZ)R#I#KpnAXp-WafUBt%+9#Lq7_ZNUS+yJp$bYuKm|pS!Q+yzL{3?4S z)7cGTLD^VCrq0E)#kH<-N|X~kLD$6KhMG!uYL&3Tt7nP560xY*QNF5p^PRrf^~y!& zA>F?gk-GK#hh8ij9jxH3E6NRcVI6d943bfl~no*y|g;n=eaK3eCkWe z8<+Jws;0r2i7+sG_{Cb6zTt$TV<~4V$LsL^@>5rk>sGR@*nBDXzJT$5lzTIlmCGnH zZl)zu^RA%7C5ne}?L~OqkK4=lFeO+S& zn1GQ&KG^xVR$a1q&+d119+6JI$lZR6Z>^ZmY6yk>vb za9*@tIa>`(wdY5*sOsQew-9vJ6_ycWh2j1uC+&k)1sF_QS#slcRWnBA`gLonMIIlZ zDF)^;U=2Ix^89YF!%kGBc8=+~u32w8%+c;2Hw7NK)Y?P|r&&(hWu$X)p=>;lU9v7e zByo6HN}=4xE9;KgoI~za4iNUu?wLcl88r;evuALu7xI&EYMuDIjl+Ikvjv?HOijBs zfsJO2_b~@H&u3NhVDww%lBjmStHrVR)2E;Lp6i!-t=Qm6&p7*rW<)CxID(7TBp`j;0OgF=!g7>B81u;dgiQ!hS51vB%BQ3Nt_i^-H7vGr z@0(DU0MI|M4C);^orZh)T4m9jFDbKSB!lID+a<&{DqS`;F^@~e{@E2oPfj%4q{T3y`E%5}fMN^c+^ zqcx{_iW?|DM0cU5Mecg$zD2VV-9y*487xP?z7^8NI)I02b&GR(Qg-DZ<|#(*yjtoS zMH6@vx2u2BCNf^yO`I4d9p}P+VwLmVTA-xIPDP4$XDd2xz;H~=EIe8WPtlwNL*NaX zLkY*V!)-9FiNH5$-hz1C*E{R*!+OmGMW+ks%b($?`&y+j^>D1#dLn{x+1d)08d%~x zPNht?SOru8L1P$Hi@Nw098c{(8s4@VuOxqEnQ>~T4U?SNaD8J|JO17CQp(6AUq!HR z=V^Es??s&0+tV#rs)IyG0pEq8rjdo{Br|!O=YrD6WyOOQEu2=Du)xbYeb3k+(vbv! z|0M0}!-M)+{is;xXHJsK3PWvDG_!{~s#zXRQyEwAR!`c)S;c7dlKMl-OX~PQt%$w>wmA7_DX?AZ|odrWaeM3TKgO3v6l_!L@j6CjN0_DL9>W}3M^|BgE;O_E} zn_$1KjFrzF+B!2sb>1Fjj)5<-&Niixkb!;o8n`T^@@m*Pr1x$Sn3XPSoX?R5kxsI`X?%5U_oubXjh*Sb*~3T4Ped9=?cN#K5UzgSo+NMo-_S7E?Y zY&`x7#kxGG+DFP!iVkHVOY?|9jfT zWxY#p_&5AY`r+!E$}^iJ`P$RzXvfl;v+&vxdYnLO^YMX{%nuD$_+d~b)yL8yE6W+1 zo-IU4<_c8vsIhjKT6rGRg)W9KOI!7HGV}vh_pm=m^qs%Y+Hc<$&O$++$X8IUt;3NL zi6g;)!#&b2MXNIHT!B#K(%2DuTAJ8zwF)g?L3?tEo6z_50yxQwuM5Xw>#;wdw(B%J zpLmb=RZnIhEgbbF#rZ35y$YS7%?1ypmBM_@q1Wu5+hIH>J!II`wdw0yBA`lwhUObt$de z{=j-?+2K}WZ%FhZ2BhX!u|@`@Rxd(7Q9?VsY6;t3v?QwuYy`U-Aqi}_DxiDnOlzbw zJQa;Ex&B#=jOb>2wJs44KlKhrDfwh0xw^dJ_r7FXqOqgNrP$5T+}97|Y(|p}h2E#q zztnAC_S~n69u0NrzQEnyn*x}Ma^Ly)i=G|7)AJNdeK?8X;uq*P3rj}tf>SF!Ce{{| zOz*IImy6s1f_x{BR4EKJ4;$0OfKNO3=0v221=FhGJxv0ja}C0xajju&@~QiK2;ebE z?8tks>+qiNj-j+c)C5RJun{3B?iyNSGWB4Dvk%ZUT_oK0CU}`{1f4%WQ5J1S(B1fp z(aKI#<`p1}i+NL!{mJ=5$T*-Rf&*QqW*U>C$OqB7_|I2bCqhMey}cxLtwg~`DXo8K zX+m(xM=`2^UX(lAkAGL`(4ztMzw9K)&kF2N4S_L0&hB8bOWBB4>AkY>KUgzg5Yq}i z{u23EuE?xT@mFj9^L+~mFm38Sm-YSUfByO*Cl1`0zeG^~(?&!E<6Engih}Zs=a@A> z&Yb2;anJuYi0?aiTFOkZ>_Q&kMf}L!E$;h^$z(6!;{yZl=tc1X23#r<039gzMLG-6 zIRmVWx#L_>ZRg)lodBJs4iNVyBSS|Y!dn0`cORtbh@F6TKrlo!0~0z{$Fmk9YCR8x zur$a6KL6E~c@%&Zq;4yv;si}wP$E3&lHz%@eE<^V)_^!&2QvBlpN5JjSf>FDb{@hT zhY)Uns8p76clt$$AUrfL!2n!lK@!kUvkc?f*uoJ1S07JcgSsH_m?&#S-~~CA10Wu3 z3u+O~JRp}#P~bSIk_?eje@z2~2P2$0bS`tdwBC9z;Xix+wwXr@Rtv(_s(1#fMf4L3 zeD$Rq3cA!?KujY)OPAe|lNXXRss@~cdXT7T38cR+K)0ixKi_*72w+S8q_UXe_J z;xGbM;X|C#3?MDE1&Yl|e7PG~j&< z%7WKQ(8@*4R=gC1J}&43i2OqwwU3yCMQ1IZ%HP4PtxE+@KRAS$NE3rRK!$bjneEt~ z)Vac{+Vg(Va#}HAam*_(@TV`#rLhzjCmsJ*vdu{mECwuxT1ArBe15yEXFf$Qbzxu# zUaQ6@Xfb0&6M^ST=gvGQOV328StbhKgw=R%q$Go*W&dul2oHq_GSF!#KEPP%&vk#h zQ~gYlWJ>ubxfYI(CAor$a%Wy9cy`3SD)>>+%&}8qYkCLt!GJQwV4HTrv~<*&!@o2d zLGyqX3PdJH-qpn>iyxW>I#i|w4wgeuVoIqTsJ~`4w~v`CjWV;>Lu70VC0<(OM;fKg z{98uWrw#D*q#&}7ZN1OIbLG><3%*QfgU_CE_sd9W8lf!C#4!u+g?}}Yk77AZSBrMa zbpJEtslNo$(`=zFxEQk{=>apVNIsPxqQpo0Tz2YhT0f#WxDx4M_ka~qZ$1O+bNPU| z<>y$*DD(+1NNpg#Du9r06|DN`g_r_3Rii~0hLY66@}w;VQjZV!3%sWTt3TuPzm&m7T;&$X=ogvKD}S{HoTVBtmnLO_lM2JeHAJOknE#f}I;<8?9_1?q z=?D#M9Cf@=f+^wfQxfbs1Y59(Kss(^{p;Mgb%{n zftkpu%`XFygqz>A7AyxMlErz6w`miIdjMFaH=ND}@0QtHPWl($FNYDVrt|gO`L}7} zn7SB7Fr~y3OzdL+4yS3<2qy1)er{1JvCu*pJvm}{^=Hi#k?OL3(^5+5`}3PJghCkY zs_}1Yr9Z(L!-o>`-@)9{N`DiE_4)6keUK1M)U^Ctp8ZGcH?0VQFMOWOvTF$q$?}0< zVC$qJvJL`X1%?lBkm)*(_=|Nop6cD;Gm)BWyC()M5`bdk{?~S*1bYD#%oU2^ToCR*$g+!)o~Wc=xvchH@cqNLL`=|Q1*CoVWKQ9G4NQ8 zzJ8A|yDU-3B^}9w1qAF;WsAC5;1&jq59A;QRbi@6|NBAye5el)3NZV4lo+hK-;?qW z?E$D}5tDiG|Jg-C#lYV)&x3ZzpizP3uD{kE#ee=rIzj{~*~2?T&zQpEsjg}X|KZVV ziRvCLw}yrP!6d&;dw`4!=v`p^31BhY?0U&_>+@?|b@b6jsrw!^5LEy<9 zt4d{vLX6QsTuDJu#@jD+|79CHKqjc%%ji&qv$xP>VqZrdF!@RiCMf^4*PCELvy@mx zhAFa_W9lx{a2WS@NtJ>Rx+9_s(Y|_tO2Xg3{{q6hhAc!s!CUx4uW#i#uL zAOeA$C6hac#7naf{$>>iz1H3_Q6bzHK^RP3R&^l%_2(1Z0F`k?ub+K?x1D&Kh6WjX zsEjhmgZ}3$M*tEzjPnRZ)4DOhsvsVV{uK1DCB{Yp1vJC{8si?k1`xTy3bx1b0pg^7 z1v62~f6WC29dJ3?7AzzXG3yt9F(a7;S#8N6GCZgxIIY8epG`&;TzDv|!9{`FS_ za{R#1N|{g>AynY`K*7M(0PBU?PW8fn_Xpr)BZ0<+j$-EpBCx%Lnp`$}41(LkQ#f=!shzcUV@k6hCkRwsL ze(j^_NY1;=$TvymK(Ydj03Q1pyb+gv>%*&k?V8z>us16~Z_LD9wz=0uM`H63KFkS( zS_^!veFWt9pQygAJ4GmtdPIVM?6=+0So`~L z2!9;b4m~UsS2eJm7VJy24$n3X(J>BsyNQrO4@&5^`5_h+5DT|M9N9E;hk-d@?5S^m z(hmoaK}yQW=?y$IJr-E*bqLvw)m|4}t=B3i+a6W4aM^ZWIT@WgYjlO4xR57XK+if5 zs5j~^xO7V}aeq|P_CIg}zY4Es#ba~Q`}aw+QiVnvKz=`+yxc+fc&$Zlyo=0(Qzu_G z5A!&+=+ZLtF8c9y&q!nok>B;*9sR+`S63?H0@bk-nCRO_KC4gpB zNf1*4p%GiS7?HxC{D<&t%ysG}`Jv&#T1>5l)8WCT`^7;rkKGQdU7y?HWm2x8Yi-JR zw(WIShOwyP$?X?rw{nTvz)VVmU%I;~KVG}YHDw^%^AZKfY7D0H<#roG&wO@$CjnP# zCcAiDM@|K;gW2-nI$L2Hj}2rc`w8cu5@LP?keed4TIu-ENnC}QY3~WwZmT)p4n?PE zjX7Pz_B9G<9lP|L-%+Igsu*^QG%4CFow$l_lOjG_mht+-4|*N?F=-b0jd#tWY1#;EY-}}bwh}O9#_7Tq z&G#3O^9?e4k$>A(!vE}MkZHP)pli8RNvroYB}uSID{ms$Wh71LRmKn9R9Z(1t)!4I zL;ny^u;+$Cq)qljj{2W9{U;7+!jc!PWsLd`r^|H&D9C|b&kZZh^ZoETB=&`Spa0r} zrJ{o7^Y`|PPO;5alKXp9V1>p~-q{PFuK({21G(qDN03n6rgl)_J>;UM!L@5ngGIyA80azEm;)m-bibl$0zoozl@Q-Lxvb`X-+Oz$wk$iw#chlcsO9zbz z{qEWGC?Oes(@&rx$`DgRTKDScH$?Kow6Q-G^b#(quE!Y= zA)NKM%_;~2pNg%rs%nzsX{WXdEMAAn%l9hPGV#xSA`whrLL;qv%Mt9Up?5Ot$M+SC zXvi@{Bc#a!*(xh5pUctExT~rBb>05&@hR;YGy<*YItGc}dvKma>Yh`$`o9=3P{ZOA z5NN&Q>JZO)dXc3Nt@DowjRsj(?Fv)k!a!-u5fxQ2d!lm`7D*=1PjD*mjZGonZg^!#T4!vu|}>j_oh_;2_9XDwY4v!kjS3br~E;1eKePjw#r>uCPhlEn~B@ZL#rQqI~zg!GqR{k&b@G_Y4tkwNnb z7-(@FVoi_y+rkjk!USWgQY{_-_j_522w3IAwNo^?e_OZX&%w-aaH#*3sxiRsg zT0mMXNS!jhCSRsyK%~Mlt528bWgUQLRl_Lu$_Woka)i5Hi}Fo1=Ev5v3#0C}FJ4^Gd+?^&RsS<;B z)}ZaL0byZd+{0n?rHc7}PrgeeNaXqHb_WuH6QWvh9z7$nMl8BL$P74M^V0ZkBj%*w z7%AE5`LCIN?2pH!NpgU=qIe66h=^o=PP2^V`L;9umDUC`2L{?ijE1NuCv37cR2j#O z&S&VLeHW@>vS7SjOOf~=y#dzXD2!Fz|6X0dG&vUpv-ASG4%K3h>k9$-g-nkP z50DLA5}Lkn|G=v_++mDT-f@gd9_hT^MuNu^vv!Ln&#LP8W{|V#+cX1&tBo9NqP8z216RK! zF%)`a5;+mgTJHMM_3-g=jz0%m$=n;KkSrfvOHGXiUn`G-0p3QaxMio_UxZscy!zF@ ztC%y4;#d-V$%?7{&D6T^LEQ|Q=Q9(ABC0&Ra?{&1Bv^M3d*q#*SV3VT2hSpJ$){G$ z+qcWwt#owi3gVkrh)*Ye(O9cI120U0xuvPsIagNiQl3PTb>(obt$Vx^MS**&c7ke{ zU!8uA|7nSd!#PwcL_q1|R9WN~`eE>_N~&deVS1gbVe`jhF>Mohw@cfQ%ae>RpiA}( zJ)zq;p-$D^&o5~;GoCu8DfQv8b^uG&h(vZXW!09cVjY1g0H4n5(C)9vj}#Imh0cX^ z;Au`~Q@dz=Sy1ZRqdfT=-IP;Dt07Z|ag>VpqV)0<*m#?q1PLsLQYOV9+r(TA*bwk~ zrE5XYz^J|Ok$bM}>cS^!Wkp(MlB#{sxtZ$NPV8yYL{v}G@$2irzqJ55Dk}9_4?M1_ zqTuWTwJvS@3mfWEpVC;8o&fAaH_-RhwuHFgY&jN|v>}1=L6sdmp|$s%NoFWcUCIXR zb$q!lZYD%STJ{^Az}g|uibmQ5`cc%?KnvPf>rhI-^LB2jZs~4=6Jj2t@md2Y!RF5C zhtSVM9Dp2wP?3L;%4r7xfWbR~a9jsaY&C(zu00Ue{lwR%y&Ax>UIoeEL=0j%VBBIq zW8={6ExY)!JRDc9=}teP{ywID!OzCP7pHPi{)0mGuOfw1WulrYv&GX#O&-=~O4fj} z7|!!>wYty|j9soj_bI<{9yl%@Ld+2hPFanI>>qEYn&$zucB|>`M7kO%!PHxM-OgD8 zGRWXbcevt8QAOd)Mi=c2gElx7Ik(duvhW&+GL*5KL{eeOwP|+p1{%*^$p;Myte=G9 z{7t9Hhj0A4;EA2n`YYTj)|c)NoE9MZX27EN{Ut!~%xI7d>%3Ev!`>DCN8)8fY@$sw%HXt>Cs@Kpz zu0^ov0ddd;+0pc(4~5HoOc`L7ZK1i|dU@P6hx6aH-1nn1t%Oxle~_@k+qfxi3e(72 zI{EE1FJEK;NAQM5hM`;638W|g$f1du^sF;3)dK0Q_WNac0fbl`Q1_|^>F=e^r^8fuEK7&hk5+v~Nw#i7JOG|F2*VG6 zx2{!|#&0oOp-|-R5ElZ3*MC4vpoJ4v!vvpUy*>$dABj2I{0%(UHhS_hI~br4sgH7M7t^`8&w$-VJ5UXdIj z!@v|nry4jMteqq7eSd0v6t*_BH;4fkNfdAcQxm{TToQN60KK9#j#5Z*|vW_{vVNp`WC zYa1ZlH(KJ5$W$Fj0!$b=%Q7!3I9Arwyqu|!tq^I`E>CZgYr{BskS+Y=i2 z>8@AChVc}!HpVG7KNI=P9^OK)3)YX0o@6C%Uc&8G_N@F)MvLay893{F8V#|XWi@-q z*SV)5QzZtFpaU>f(TNDLaVc|-zIp-ZA3AHt+A$!jJA#~`8{V@hd`=_62@_fzh}iCm z)2EXp6E<4=iom!8@?ECcFi1^ltv42_Jaai&!h4f?{GC<6f>-UsL6T1D>XOG$nJq2fIdVs-$gU{XJm*JmMR&CAD!rN?}! zIZ=0;*a2}ax|QSYnq4fgU;F!4*=($dXM6;@uT7f}?bn4X*2HU_O^yQ?=k zuJ>~lT3#eM9$Ja@wjue3rrxbl#q%5i0a^HSipVzyj9%9v^*|+@b?Th~>+bBq;9!nB zsIcnAIk1G@(?$iN9nYP?o&-+23KP%Af0{fyZ-KABNCRVqd%Loq8jDj(U z6J3p@@!~ob#&h4dqx1o96|D&i5Yb9 z+{#tAdi>tKq-(D6yqLdE7*{=tM6dPQ0B%%-oF98IZnrO%AdDu(!0WFh0*eqcgLo*Y z%R3Mk(S)xd6tRIGggZd84tygOnkX1u@q7Tk38*WEaUcg8tw7A*KrATAd{~t#Z127J z#%&KWxAmZC$=Crlq+!Dez0n6?c|=t4iuV-YBSDHV-W3O${z{zS;%YTK@J6Cu{9Y(T z$^41XtugD#_>`6VD#5ERHp6xl?0rVl5n_lzkPw2Q{+9;QIEp#NS-9Cm;Cx2o&ZHNL zzPMtz^UxiEE2h2>TfT8cLHllmuJRhyN|@-}gUO*Hwm9>wb#Ib$Zt6WHN^P@gg@Wm< z9v&-HmtEIsSSETJBg=b>ly-UkYjVxinQXK;vQSWK_Dj9u+~>JRzkK22EkyrW(9W)0 z191O*m-K<`&|bU|hMWhWCo^((vr08T#ZGZJ3HENal39BV^BWIa*C@!KZCEC=#wg}x zg}^w;ZYGADC(zV~*gJ6f0VwJeG%lrDK?8)OXJRm^Y7i>ZtW_|ioH_qW1o@#gk!21q zcx)b9m6ah)Ha)d+mqSdFRV*V^j8%jv`(ABM-efG?;HO5qXWqBE=OMRdfk)nQ&-R00lO*W;|%%-B2Q z>5}dYaS|_%x?O>AaS?`QX4kt~6+6*bE+?SF9bhjx=QI?5>bOknyisR$Mfzwh*3=q= z2Dp&}E^S=`4^ZI^2|On-TLq_A$D#6TJoi25pGXFkLFeyozLee-vISTUVuHoX!X zDI>U=sa&hpxy5p+v_9G;NKd*ZL$PuFO^Tgu#cQ{L-P{y0e8mGOG%ODd3x*rK@Y)hV z4asgb2eh_p1ijY47$#&3AW1^q4Pqt9wD|>AiAGzB*I}AHmEXV$fLU4Ic6|=l`UtU7 zDmcZVx@_S*iLr;}sgK#6OMGHDdP$_cX1t1gGkjiPh?V~v%7w8yOgVvmbTIZJiOVii zC&fw9zF!Mnthj-l690UI=a+P*c1Vi*ahA!|s{C7dzI&Oo*~7Co|RT~tGo!wZse=@T~9GKJN^{@ZcSG4 z+ijksBu_5+`rwewcJvQzwZIpQi8g7S(P9STyHs%0>pptm@{09Gr+@&L(nxcvd+^;d zyk^8+#DK8d>4pH*aOoxKFRr8@&dRWuN#@w%v;wBuv0w5o^>lyRTR{yVPuX?JR<6*|HcYdl8+*2M|RuXe)xOGz%t@Gi3aN@TYjfWnL zcHt7;^5%v`5mYzr?!#ns4c3|!VCx`DUOVkL%nakE$q;CH?)K17f;mmKdi-p%Lo95Z035+GMJR8xrq1SPBWc)HQqi+ybu`aifXl=s@Jac7kH!FoAQoo_Zj)g@W z?XfpW4(gL zepo5O<#1Q+u>1;-*HnX_{HfVIC4TZ-AegX<6B@XEDt|&p zNJ$q~0fN<<^?<+-gbw?g6CXoH4lq_@kef7OT{sFmzoWjm-Ij^GWrwP#lLra#SP)}x+qVLzbvpifZ7n4w<0Xx@h3Di zoW$+U(BQYt!rWSLLlh{V=pT&Z>h@qUts~$u4M^m6h$-&3usk{AIsnnr?z{1T4eQ-) z^iOx@{=wt&h6&qoR2(~l11?aF=pG<1PiShl_0%Ud8ILOzx&2Mhr7+ACenRw4c|piG z#=djK2&WVrI2+b&lNhVf%2lU!n3f6xgRPMCSepihy5@+Cp0A>XK2K00q@;=a1|CH< z_@n(T+K{=Qe%RgJmbd^-u6e~%f5LrYGaKuHHLPaMBthTQUY)k;;84q(uQLak!D9h=d)eQA!UW%1n+bImrD*Q7$4*)Y|B#iwQTY7(c`UeFxCL=u9|EPJe2D&k z+qV8NNn%@?g9P`<+Zmfll3xOP={_8AX6S4pCdJcNpD&OKKbulT9@{XEj;mF)PrP>G z^*DjtHjHPVPfn#pl$0W|h5P`Mv;$tLY| zdGWV<+{}Eu41Toc)f@f5*`63e{~Mmfqajz2A`Hr^EM0WaI85v_6bcM3z91nqA`#su z6@oLn2Y~%S3sJ;$Wg=;RCxo|WQ0C)hCus-EBk#(4HSL&?nf~Kg=-lGD4^fB7v;vjE zPyMhmrlauUZXu>RVDZU9Nq*J&_Om^c2P?$5$H*@OLa$5`b53iO=Z14g+x!RF)pd3O)KNkWgO|{+yP^zO8IziI9MLBrYwX6ZT19 z-X#9J({4`3wvzRx&ZkN%eilib$i>WBt1p6`J|AD%@9B(XqyUfz*!TXz)u|>uKn^N8>RW@OT^NG2whn)<Z zuIy!?%-HfRIIvAdq=b9;Vc_8%kG7egT7h~YGZs6hGZH?Ms^KP| zd{llz9$~{Pen!Ikp5sNIDBR@PD?d)-MA;EJCTd)#v&QTsTPdc@4*PA5r5oh{%r?Hy z37z3DI2xFn<=z`(7UgMKp2$5~?e_s?g5-(r6re<-$ASxI+AVR+jN=f^O?a4CyJ^H? zvbTIxzjV(aBV!ZhCd$)Z?klJ?#3wpk*<0XzE9B`+iI>^gE%cSqnSqHqHlM+Vk~?k4 zR{CnDyYn9A{1-e)9Tzfp)Lw)0xs>&(@i5LMM^}D9V~ob=T)6UFI`tJmPZH5QfqCbM zv?^5XW6_}Z1)0c5M=G~unT6r@}B0W7pte*PgAn@zhwi0M@yX`9gmoMBStyW37i zPs}}yQhy4MUA)?`7887&$%nLBA6#D@e5ES?XpOih(A!84DRL*tmv~D#jC&&)CAYZe zZV%>@eK1qj_bTzl=^E2s$E-hszYQI5_sWe4vwEld>K(P%DfhUH6~C;>45s@(-Mw{G zR$KTjEZrrN(kb2DARsA%(j6}#-JqnDN=r9Ls&pfWbT>#N9fGuUc-QuL&UcRYcmMpx z9pgIC@eYI?Yp=c5^UP;Hb29FFrZX25Qw^LcQw|vvZ*d0F3G37xSm54Ad1x7qJG2-> zvg;X&C4dZD#}!mI3;QG%$o#$5`Yx0FT(WVi5VB!V#}jpaWLkckes=okkiSSXNJ)m9|I zDeu>lLXUjP+6}uMgfsNksu`V{&~58({RD1)4~&SBGxp> zsda6xRauuqF|76IRGE{evZT8f*-d3!Mqm5ld|&QxhD~{pK>w;8$&D z&66cOaLu9IJlT>NJs6Qq5yBx#dX@)z*7Yg~O8OmSx?IRtm{()rF;)o5`!tw$vG(=% zvv*)vn09Z_A`@;PX7X8**_(ETRHgcYTr=#YE(VTry6obLOT|>V`u^-k73HYDD9@?m zqV!GR=k@?80w3Yy3+%qnr1eh{40^Gq++_qD#;V&?B{3MyUJdPSCL!t4$#>N{*0oxC zI@$%KoSO&S;o-rw?{hQj3`fKHz-MK>oXy4G)ixyl^8h(406p;K+2Zt7*JtZtlJ;3v z*2yH33LOSy3*I~Z-1~Nss$z*|49w@Z%Fm)Ys(J4S1~HD(VI|R6Q5WT!^dOLCTEE62 z>^AcbjnhA`bg>nb6t^y0(Y;rq*Jk3oBf?%ZU4K^V7J1;}-Uxh2VOWjI%@x}hgA8R| zuF-t2DUG34I^A3fBl}P2D@9Zwr8u&In z@8gi$-8pw0YRgUV!&~(x^0K$pzf%kYB z{>hw!H+<~TkLUGi5#^jdgBzR=*8;8SW79ETFx4jW;9>LFjveZX9*E`^R}%7{8`#&^ zGv>R!HR|Hh36l$Y9&Fr|ux;fx3V>2L{+L%J978!m8F1q3Rw9Q&qZW-XSn_P0Oz0^* zDzIJ|tt6=sFSCo+gk?@T@xx`1e4w4ys_;8^l8OnCItkKt-$eH-9`<;J#N~a=sYz_{ zei8G%-LSO9;?n(3PxBu>I7$-Y!WCP7Nvq!R>h+tJ@{Ya~Jom%q{0aA&9)g(ZuQS!= zYi<3k?1jT+JV{yQ{0;gTVeWywJmC33%o=bu$A;X&|M5f3C1cD!5IMlW=? zmlc~J0Vi_a{lOR_4JjR|Ijou4L}3o~PC~-kt0RR_xe$-9oJqwK*9m;Pii zDL5mAERuAaI@%C;2%Au~vyzfC!qmJIZ+kS2Cd_JTdt-j=D{dPHe#XY>4*ko@+EO-s zSs2Cl_0>#hECHiv7uZP-R5>MNdN06yJB~elV{b?vbJm#~7}HxQb*5g~#j>W=^7mi0 zmrmDb?0@pi{A&Mw^K>rh^eeh3>9FzW+{ojk@$ri^VDF9&OSioaD{6+b^WVGIOq#2v+ut6;8x@T;IvWkL$3c zcIXw(t#^SDIzWRz91?71@Sb`UL*Ca*Lff&;yn=zqdogeB%f@jsK#Yw=9-FD~Ik;ET zmV0dZp|4)*(~aj(Q%%<%UaH&$MNYLO1`TS$!^cCDx9~jrZ?#W~aAvJ#bE{xHmC?|l z+2Pu>Y?hXZ=_4@@x@~i3L{m*c;WFLhYY!?*PiXR)a{2&QAYy;%vVHjEL{eMABq62q zg0l7!@udwZ3yhkx>n-g4Bqd9gIuz+eY~k|M-H*}{SmrOA;w5JG#J9cW#XKDyyaIy12cwJ zm%x>2RaLS$QjeGSPKGi%oM+LI5yquvVw|@fE5zuY_w%NjZEKCbvQfr0?w89r>D^*A zS%#%p<#E-TI7&UQ0&aXkR>@qEsoRdD+OzPNXV2+++4LS1HA~ohwkojJ8fZiqNKJOX zgZg!cgXFl}@t$bT+UqVm&K{h%rO3Mb}Do zJf^%xf>WkO(UmW({Wi%=9o`nkK)$ZF0aYm{d3y7*fmVKsXWQ+=j+`3AS_-!# zrTo;d<>{r;l|splPNn3B+Q>Yt3_QzHuWcnNlk-pVmP{9$v*yU+(~-jzNwsXo>S8&@ z7O8^vrTU~B<6=dpjZwacL?Tm46b)s!NaLFNhnhuO2G%*Y{xp+&edf4I%UxV%QJ z6;8kH)K9(03M<3>k@U-Fn)L}I^@ z9k(V=C$)J}Cy?85E3BH^(K27BWE;0kice@c*|@A#>@ANhRgG|~o9*OCUGfU)5=lF^ zf{7bkF}YjgcpaPJC3`{fOMzu&eyZ*3^J zF$ZOfS-fqVA>!wz65Tz<28O(rot>XPqJm|TdG`jOC6k+6drudWd!F7%nQ^HsjJ8)Q;0Z01c(&d zwck!h8dpLu?3domP%f8WD5M$>@p@(L=8Txk6NYs-=|^FpESZ!1oQfIkmHApIQ6w*m zYZ^^K6o>o_3;+0@J$K;n0HSS0ChKF%l~!CG_JTQvMcj;G=Pb?U~Lk^WXXUhuj~iUW1-WmTER~`sxUwIK9tjzQYO^3{jgacucHVp z&!LFDT<#6ljI&y;V#|}wQ{C!C0SZREuDvLSZg46!El$JHeJr#6*^!?QW-(&O(`_F_ zxr(stcq5&p#J@f5oz)2axXBlRcT95=P8a3n7gevm%%FUwL-SRA7JYd!Ib!40saBkN zXsp-EE&_=Ye7Gea#N2Nlr#*^1yF-zm7K0{Vo^%b;e}0!}NK0_lab$Abi_=}vdLgAc zZBZ$xzR4I36SN<5v58jt4b?^|$-n za9q-rX0eaR#un)IWUoA~<|S5pFy?e_t{M1rbdKTOCXPMz$;L|0URB{TWO$T3lK9k& z-*K5>?&+(SaO=;sm{fP5(d&{STdtQr@S6`Wb9= zNiyFT^s7u?RXqdv9mLrdPB0O1g>IJq1EN`6H5d6xX+^avX7>)Yt<&#yb5IR23y?pp z7+FP9ItX~dr)F48cAi|{kMK0UJypYe@L>PZbmIig2a^K^SWMyt57685?c_g7bqwN? z?@(&e@3RrCV5T@fdEd8xP}88}q+^lYZ^&al5(4MP{y9$IhDPiID&fPc)PR?hWWjJc zje#7&^j^J*IHWW#{140}Em~g-jP=~+zP7dBS2e@`ERkU8Fm#ucCu}T;V8wFdy68a& zz~3vz9+h0)-J@#cXI`nL#kptH)bgPC=#iqC#&%=-%Bk+>;H^qAl&{Kkhn3zWd#F>s zpYE?`q)Kd+8X?3USfb>$8(8Z}rTDt|7e&q@%Z>+nyD=JuVpNC5#hLP}HfH)b%{9ck0^X zaGLSq5NLWD6i9OXAx&7<`eNKc-!eCyVJ|7bB*DL}DQSawL9C53A9);UMpPLE{Yxbv zUdVyuD7v<;*cBw={YBU&!lruH&f#;L6V^$-s1;lajSQblCTgbt{iB+c#j(~}oYsUwo|r7(`+V1AKYH1yc=uiPlO(A_?}X8lr9|Of#(0f5s#&GH9T|69 z^2-l#2QnI6uwt76xwULA#qBpD%D9m(dF+b}^SRd++&H|V3VLI?l$Qo9p9%d^_SAJo zQ4ikbe{lC+El#`C8{lRt8AYWd8Ye;@_*lA{QYC$(^s0f>PPRm_+lcA-lbV;a0ZSfs z`5=e2NG8jm`rwzhws|!wd8=B!L=4dpEU1-e*XmEJt|X`NSSPKx^MKJ zDh51bFE|9YWM!Oi)|!N{w>&Er%a`1Db$^hB-^&3S zBO3G`c|1_1|&w2_dtBRd@gk z@dl}U2Dd23{`Ke|EPclkz~j%0kugWkFZjc<5it8UajSdloL1&Qm1&qB?w$rX=o-G2 zjg8CE`hdnYJ7u=?Ye^}1jRI9RPqBfH=}OBFk@;}+65z)S-bkth$Ai}Ik2lws4Kmn5 zxk`kF$+rV`C9}%QM^sYzi#1+z`&_sRfCju;V0Z7TcmasK&)@_2CbZi|GWi7r67}aI z_-DF-OSjAW9Qs&|TX>tq@SmQax&!>=TT=TLZ+r?m%P(s{PLmf<12?%tW<12r7Y~3a z1SNpw{Uu6JHXPzA070(|k+nDt^|J)Xr&JwH6sU4SUb&7x3fAg?=8=qB6`3PCEj;;s ze{pX>wkMC%^pdd~oF za{}=f;U-=Y{hhEbnpiMFz|8HOFmzCoDRD=%TON4QN8Fj-LR?q=<2E0b=16 z##SLgbsWaEEYFIqwlJPIIC%GRRZp3F(anDzpW(j(i~VUx&pI9 z51<5Gwf*ITRzX4`%UYg>)IBG>O&DSlUP(0zeFPBlYy*alKV5+2t7>s&-kEHcIeeaI zAHBR`+pVoO8&rUiK7cpS)HY#oiIF8#@X<&jY4^5?;G5`1=dm886EN$VZT~C4BesFeGJrtC0j^Gds{pD_Qo+q*7%Ep2@+-xlubM!8e&emE^uCy_FSfO zXz$wiqGjY37te2(YFC4{qsi?Mm#Aae>I)ILyi6ya2NP&^f8^+KyqK>>bCghem-E8~ ztdee^sT@@oXY0-o?=_1X@3&1JuO{GwSh=6J=m3c+9>aY;&#(wXIa1CLxlp)cAjT)B{i2`|rD~&f7yecoH4`Je#qIrwM}PpbTUcLTp9Ny2 zPx)HdQAi<` zhQrfx%c9&HvJ1r!8`DfI6ftzfdDOnGSYFeN@pA-_k5YKP0&XaOyX7}XcRHCBa=%OF z^kq|7_bz!RFS3Gi)Drc@sZU_RSBd_GrU*zptvGz^xc zkdDv5Qq{aw0*FG6*wxMx`i>zl7wsI7CT;?^+WxcZsw-pmvlU9aSs+%MvKD%H*y5L_ z!w`+F$iN9KmzpcbeaCMv8@k>nueR*R*wh(9{5FQ8Auw=2HqArubA9??eX2#T>B7{F zn_@5$5bPgj*!GxbY?%6-A>fEc^GJ+aZy7X+&uw9x*KkRe9Ncd-P^ZkO7ZcW&^xsJQ zHzmR1YB+DQh|?uyVr|Tua~o1xvwX{iU-}B45gDt)qv?qJw7S%X%_Ti~Q2p`HO@4hC z`^S6j$3Q@AE`m&GR19ovCde+E!`Xv4$dIe;_ZuLy6{4fOP$;ypkA$V*UDG9yFo;73 zBv{B3lXYHi1Rut1Kn}vYz=_G3m0J`rvSgYd-T+cDn#6;>cKnB!8=uNHu9}&x46hbh z(#9n4HklE74NzXV$6Opn`CZZ82{z;J%wk_2tw92un}vDEZK`$7`)B}Yy=1uf`cwG+ z1k6}RUw^#Fm7l*M5S9&K0xLDPoIZwD`sr_bTyEOm&ZU zIAoQ(alW;8LQg`Q?s`DBxzCO#Ki}|O<#XhT246UezHu03=gm-~q=??iM2(ZrN(vfJ zPKmG5y#-@S8ZdFE6-w>Jw7+V!RukMnVN710^^!Py*|;T`cFMfI3D}`g(4-Ilyb3pi zS4G{v@ns^p&)38_>D>XAt6*qNh$wIdNqV+xmgM3~LHMm+5}SRg zA@K+iuLN@X+U+)q4a#alEqkl@Jb_qwws+^3tAkls>w)DC#ZsP5z=O4t(d04b*-%d( z1r7A)356J6H- zXS_aj*76S$g>42$>j9^~?VY}lXCuWO(ueJru;V2;?FBji4Z{?IoS4goJ}b>6yQRQ3 zYy!&^g26=YHhg%Z^NlZ~_F$Q0-waEpv9+2W70fhaxZUs^ruh;-0)_vWRr|PmKAKua zTFth}sXxv6E18MU3PtNoz4t;>xyRSja0uKAVUqN>V;f%>4sHBx>1f3I8ZD%0ZC5F7mo^t z-+01xLX^_vBP*Il9mTU9M)*1KY%=OQXE9!S&oOYf-NXePME9zNxJ?=q{SN80ISaELT+qG3ME_sPA;cG+6V8obymj>OQr%>-Ezb)iuC0f$b%kqoU!SzJ7JO?{`S9SAI0(&Ogv66~ zVGAz$H!nrYV52E$8@}-oBpM56+%FI(LwBZ86AoafE-DkFUPtw|GQSG9yobg>dbKVZ zp~_c#Srz%o6`09bZI;w6zjnMmhDLa1(iD6?4TKqzb&3+nnJ*`Sm0z+>P^yo`l2H#* zPY|Ca0~x0G8|*O>A-qji_=0qI-TX*eTz#O2ZL%+Iu#}u1i9;q>X5}MA^+1@gE@7*v zBwGCZl?dU4?`gvVR_dcEFT}epfa=*u9&Ym)?Il?N##E9PnOS9014*v(Ty%BqOMVg3V#go>FL21RU~)NRWxe)x$UE)P|k6?5rFd-dA9=u#pNn>L>)XL$a05c-a zGyyF%8^qBfrON3gQ-yr;{qiuA(ekiZ{p(+My-W?ZAE%vJo6=8&i11&nJ69=UeHwqn z^OS7pAYa{*l3idUKg{i@I!mZ+(2~aV?m!R(>Mw@PcW|xCi8FgpX6VTBEt8CwI2T$?4&A<& zcZCF>Bt1Pa;S!fdPf1WDhrc~z!ArEClTj&KrV^d|rQ~r6PHU<^;iRBb#`9MpIwd!# zk3U2*WXfz2r4Vf0JN9q79a>K{gicD(k3CXmtokae-(~#KV5tIDtHgQxYTsT7iSo#K z=K%$c4ad`TMkRS4&te6Zn`gAc^g;1-yc)5cd`wQ%bl)hexyS|g5cb2?xD)LxqxxUn z#+mXU=PO&_=5}~6QE(<(WwLKt8M7J>r(B}*R+qnhYU-$o7*>?@jytruT~Y~5kl0(t zF9Jk(iZwHfO9}=i0g0RvnRopskfBE(63N$VETFd2^Z$mjo}ZLs{J|bO_ObhEo9%6U zE*F?@2&c3l@)IC>t56p%fyhxsG0x}Gn7VYi@Y6TxT?B@>er-$Bxka-pkhl$U<8^00 zj|d6jWBfj7fenX+PfDi0!k#R5rJL7v@)gsav8sj9sUmC?_|y+6=4hR%(x^oU_eQ!E zq49KGa9q%Bq1sdw8OaNlTPE~8m@rw=e*R`$I^mKPEI!F)#-=U(^*so>%FjE`$vw8( zR(M@m;mKvPl+ao72-8Cb2>nd82Hu{~UiB9Y2 zUoV9DjQEzxn*ioy*#ex3LAHb&v_a7VQXT&u3WKa$N~ff7i6MwszZDQk-Y^M^=5y9P5B0T|=z zd=(F>(6~;GfM%lqW zm-h^$tQ?iN7>RQBnyeS5Gah>MPNyKUBH?zW0t1olJYmXR27Kjx`ThJ7s$8nNsgHO; zzKAZ{mk%U#j9#zxWX$vW2wn?B_Mi}c(I_3xnpr2VLPB-g@16_|MLoQ#bjGbo(g5#n zbjj&s_=qW8!iyj+GIvh$R68cv?b}0y2spD$VaU;)kshSO$r~y03Nsriz;)j$2g}Fed^j5 z8(i&xA(LYDVt9))NA_YBjfW5{`^<=*8Ois%PemDztZ$P1vnV(NnxEm41>U`@y)R;? z*Bt2;)WM}D1QN8igFUe9_p|$+MCP9M9l*Vsm_V$W`xOFhlTz0ZAyov*N7(o_my#xe78$8PkkcG^ z!&1B!aNUrk5W7VDJ)>~oIfw9%nI|jLykRoNKZi2rJ^h~U$YqPC^&t0`IQ~c;XPF_n za6x2B#SM^e5^tRHeBq62=;PU3|g0YkdR$JdNomcr_!NU1IFp3!*Gm~THO)eWL0?bjD^EKg_RVOuRFjD))#h(_u) zHZ}af)KuDXX~72<_UUO%&GL6Lcv0Y@;Jjs(CsZ?i6{(zhVF6?K{xPwXJf@S!%A#k!$8?L00QbQ2SRL;ewXji_ILZ6#lglVnblcIk0jh}WSe z-m^(qG2JZHA;aK;0V+mRHT5Nm{;lT$=o>nb!+SO1c&zhDS&QG^(V(CGZF`-7&f%jbN=zL1kqv z6gcAeX9V``O%0oIKEKL*wI_xTKsH8o^e8rt!ah5b zBNXZf7Mgp<=f;GyoTu^!8%zy1mvv{rF=6xYyGyT#wkGKh(KgeOth_dLi$fC=S4iB} zXUnuulCW!dD?az~n;s%FqkmR{9JY0lC{xGq6I4zai|1d=-aG#ok#8`%-qLEJ3s2T2 z#4{~n_=OV3;~G=F{zkrooIl*1>PXS|6;$yT|O?eL^|vF zyC)yfzo_=3w4$#&uzGf=5{C+>3rz7OaXxv@2z#UefYEUwxQTtpEUYD;@(2kWusra# z4bpU{L5U@v9kb>iU@AJK5rdoc z0^HGW^|D_d#zVjr&p19Q`U71jfIxa`Nu)m%XbQ0agj>DzUi*);GSdxH>k;9QaJL`C z0HFFWrJPMq_&{W=t+V^_zg+1a2S^U^u?!3hJn7DV3w6EAOS-XAlB9!Es9k1Rf}Uu8 zxGy8irv>jmefm^a%I)Kb;ELZH^p6yZ8g;-@Tc{Rs;IDg-v_rlBXcgf9Dr{_Q!7jK2 zO+_F6xCa_Fbhx1Sx6Uu4%HDbGeGOi*yZ1lq!eC)xl@yXK)7n0@Ejq7=S3hL?{Kui@ z({BA%{1NcOeJuK)u`SdBYAIXCgG>wm`K93jFyi0V1++W<84ze#0-ixiE{1z)hrfge z|M`{qG-&Gh20r=!```h)Dp}RlUwlB?>;XEc(^G|9V5jUwihmVYhF2gtDW_U}Bp|+4 zhyoA#WGeLRs=v&E&O!{B$L?JHE&zQ!Agx*%$G;zSz%I}a5h0MX_@&3UdmjRnp-KW$ zSXKLNr8+1xMr8(2u&dPPkHFveS4HX=st$dc=UUT3{0J9NQ^QXr8)NfSaS~A_Lv$Ky zD*)9SYRhk+t@^m1uL=Y7B*k3@bdZ@u<64*C*`$@ zo^@=)B41K2W2*LEy*r=`u;%&0K90Q!eV2h~@z0R+f%(a?-tR(}aHRj9caAir0}45x zfWRei(uH0{bNnp3dj&p%05*IJdN%nE;bq)|gnYf0sDwAO0%;bmOSXG&-6Hc*xG(jX zdHw3F#g!o=O5#TF5aBfVY6rkjF+`r+q7W0dOoOC* zSCJ5p2nRC6L_qIm^;-fheXiKd{*I{@2td7}?{fhvk@tbIp2ZJ%S|?tt)IB zar}9;ArYv6@4MeHz&~3Dyaj2Jo@hRjBr3B!z@Z{g4(-3nTL_qmb!ltrR&_gMEhKM>H=y` zK$KdRLT$ywH0ieqJCrRx`n|HL;lU|ahxJm6Bkdq-;=!P0<$=sCYJVwFN|?I#Wq=tY z3jl^lyJpbGgaQ2N=L8}2sO}(_2z3a$HKChihJ~z)FbC2BSV5bBuZr4IJHW#d>s7@w zeNLwLQ~5Y-G5Upx#BGYoLuNf!BHDyg>dKmR6F`CW9?PI|(2YMIeXfAh^8F2!**V%M zQ9y=DTGXzTer6T*23u|&HH&-bo7eyC1h({e8fk!w&QqtbCak{56;?YzYD-7uSA_J;c$)I&#Q=$XfGI zm~e0<|6yi^8xtS~QnMEUjo5B@8cjvo=7jFC7g+aq%JVd<&~oQ5LtFEh^y|_lfA&x z#UFEyemF{P7*wQArb}JLo0OM5UH4;DDR9`Om#F|zr4QIHrcQtP5a&5|Ht<9|sIiXW zwI}CK(^Ebm6ZmVh-?W7vEpj~}SG%AYMnKi~KImvc#MI!Zxj7cGM>?YI&nA{UItTBT zGdk(w(gkqK8Y?r-KmzP#mm%qyTfEfmUHt6|U7DSO%C)1y$OmAP+D%${Xt8Y+->%-nQ!(1KyS;lOoegGEe^Sf(#ftu@lDGgkDC z#6QD_4qaPtLzd`ENgP^ZgcaTnSOTR=xKPc6Y_#O?QtHr$(x$BwuzqC;nLHBZcz(ax zq?UawGRsP{mb zLqzq^-blF#7x$_`b57Dh+#PCxE{^V@UAMdW#1L7WG(wl0|FW7E)oOj%-Ep4GLIIuE zx)(*@Fihr1mSPGnoR9arbVy}IFy+PHl#FypFN;x9>kZBD7@Dzgyp4$mJNv;gdm~S9 z@J>CMQw1{O5pxw%7o}gLSut9qL7S@B@whT)6|3N3be~R2A8V!ZRA*A~zLnLmAXVL> zwq6A&qL{zPpKlYZ9)LX z43rDz<1UIn|9aY_)s?3)@z<*M5dnnp^GodSF`B6>zLI!Om z7qBM?^D`S`Mg9s?K&0Zz0M6vb;FI^4WVZ6Hh9xk=xRdmb&I0Dt(r1>{aO+N zgrO)`)gi6o*Ux^TYksZ;i4a0xSs(DMNCn*WTSR4dkOVMTh&BH=js=y1|7k2itPJWH zuW>wFPnpi=yQH6Ih9tIM`2IV2fj&!;AaaVZGlb$66HX<$?{~Rjo1S|{V8RhDKEwcaodoK`N zBzG3m-tZzP7A$AU|nLEA*VMcZL-bbOjQN8Gosx#90^w_2{HT7G3fl? ztOzb(3$%t}StN0p$^9$CF-Z2Wf#2@+koN)!;HD!y?4}wt6@Q#G@|*u9)iqJ1vkUSG z_cW!-0aG>}F^8{>Z@r8|3~fkL(m zh#ns7Lxh#nRt%1y1XYRtq&{JH${5sk=8cRc^*Z3T$Ql z7O=pwstbn4PghsbrE zot@F4fD*NB54arxSl@boicA83~9j|m!M#R zwsd_F8C+uI+(REjq}qC(?mokG=r9`rDv=-dpM#7i;wno#h#n_FqBnG94rEdUUfI;K zp^nr*I$|{nGDip=4T&MDd8>c^aq$I)H0ycqa{y3r(Yp@h#UE&sQoMgux1Wpv4#k>Zq zuw)m$<8Nb(p0Y=aNWsQ$+@L_&1-Q!qSG?yQ7$oibPc7c+DVZzJUl?{pmx&mIg$$2hNhv{S{Bwvz-u}C zb^{1A4Gp^=S;&llUQgte*HXIy2vL{FNb$&0vZa4f47~vwj{~Jx8%UTN!EC319w9vO zE-SD8d_6VYVNQm374LJM_nIti5KqLb>yvanV^1)ULWkiE>nS#nq8oAKG~57&suW8# zpgVj70$uHRofyxSKHmKOu;p(@lW5D2YU+YXPk<+h>j{RGCtnMd7*K^G8bJAV9{4%q z1z{=IIbMnHH6D?@h|H9C0s31ef>HkE5E)|y#M1nz^oHAujVBD4U-7U3A>^}Asg7eP zrLQOP_s@Xp)*=B@s{^qVBPtH0jfBg9D+h`Q{f;}}p}K+OOP40* zgD2|azq2$lQSDS2QZg_a^VzImlx?~bo%-;rRe!^Cl3&msRI0n>0%G8Ouo=Q}*-)KP z36`x84yHavDn{B+!HB`U`*UET&78pA#+a~&aR8M)eh++>yMGR|75HhsPf*_Wl|ngz&u!;G+7(HTp? zG7~=Bu?PB%=Xc85u*v?v7ZwtoH7SgbM*X1=-#^E*xZd_+2WT&`V;&9^y*^?k=BOFi z1!I_kglDo0MDGTLyKt22IKOe90-OcW4o(l3N6<1r5Tj4WJCZ7(O*wqqntZJeklc8_ zz9I_9CJQA*EykOrr*!V7wg)5?%%b6mvZT*RN(r^meq)76nYm@A4e8S%i0e_$s{l!F z>LJ8;{S`Ze;#zI>dmy_$K#K>Psbm4ZHkjdI>j5q0#iiXF$#5=nOPK*aquJy=1@BBg zp&TSl1iAK5QX0Zrr^K75)%zZOC2(2kjBxobS_3k|nRnw0sCU2bcQib@l78v|d}t6` z)&TtEE)$B4MVe5zA0J+)<`wH3`#D19N|bDwl(l5Z;c&Ib$w3%$I)Jv*3bf(U+!&dZ zell7I#q(mSL*$WPk;_Ybn3Mih!JiKLGpTUCaCqxBHPJyNhVy?@^jTJe4Ty9w0{Ec zCR7uE>iBJV7`s9Qq`*ZNzz_|!OOOD3D^`_9C7$oyuw#XKUn~+3Ax^I+si6H6$dMv$ z{)8fgY&qdC(v*pxj(zzK^98{k`iu&$KMWTcSF?Zop7oo@m>7Yc${+j7Z;|aF@8N%2 z`20EDaPux(i=+drb658}1)&G3y6>ft2cRQ%JtPv9YZd}sz%MA>3ERA}OdvrdEE_Ra z&Kn?q_*2lGy{~!@(JxH`s?o!tkS*F$OKkLT>?yX8l+{7EPaKSI z(O;Pg(r#CYRV!*8{H4|WgB3m;1-*yLeISaQwCxtng2zy6{wVlASHu8s2HlQPm)lPMkP zd>~HTUn-a{ipq>CgG%2nl@qB$jO}OT)W62e;nkrt)3ZQ0(PU?UXCbd91gHYi!g0Z} zH>l4ELxV^D31U)!$W1Xz9kT^^*f=NitQT<*JRTqj$BSR5TZ_}^8Iwp?K@n8!KMf;> zLbc!k@3WXg%5BXl(xhYdq|ovVB&jTxx`UN2ks6Kld8Sn?EJbdI#_eEks>gB6-}9JO z06sMjVvj%_axW?l+hS#{av)9`oTygO$;gD6|4PqSkr z)2eVtv+#D&%2d!IWLiFjKsnX`Sn96I%Zh*ZNSK1)yWMyb>^vY5SY&}O+AE)LZxa4M z`#V_P6oltS*cO~n{FC;N2<}kPpS~2j!+(8|%Y*Eut6Wp@-`@nE>7kqop4ID*oB|vk z=$S`WtN+nGa4$eo!^Xx2?duw_i~4V_!c6;b^?#<{>i^I9;S7nVhKGkMsSAJnb8dP( zNo^+P&@qG z7(;>W+JEJsl6+`Z0X}@z%NJ7pl@9+Oa9=~(s-`{5fPa#fP$@ecbp?gscCU34jfW7#xN? zL>KLMEd2MK#YuxX!Bn-X^~cm|gMezzam7EU+#Pro(6GYxUE=@Xim|h^^H=qA3p8ph zdEP}gn)pMr9u9Ps0Hysv7VzGnrZ%$B8=n91(&7fZ2IeXta;bvl(e$JL%L)t{88sI8 zcVCzRy!QSH#hpKMeKbTMAdT;x={{o(H8#-)6>*gEeJLt)WO&`Hm;JErX%NS$d#=uY zEXVHC)dqF@^Fz(=-VfOX^n2N!i!C_BH)uz6`+S;v{&jjsm;N-hjM-y%BYSvSakoC0 z?T%l$&0393yWZ|KL(y9Lo_cyP8=wB1pIAcbm6oo1yvWU+T#?IA5Pu$6Xr?se$_}bqm&qzsdNPm@;Ogn>>VqO5s6u|@eqv+u0W3%^K)_Ey zE`6)EHyvFgq2O85!kyf?=OxZW)48stHj65rr5b2RvIAy=!A>;J<57=L*SrXpLk wY7suuyU98iVxDf{Ul@+oT!j+YX?_k2a6WOuEImFPE z1%9x8(3nI9L?M*K#Xi0g*zvH`ln@tmIcu+v-sT-mtLlf#k7=Ga3$`~UyjWmkN{U8C zlzxal)o0jY88!G>;bk}kPO#bFIN9$|1yZ+g@Jh@L4Z(k3>^`pjSTQsyDj67<0gm);_$7kRHnQ$_OZas!!F7g1o^K5vZ$5c}*_`;f zIt2!bS{Ptqhu=R5%YOGByt=1}6M?-8Z}MEAR(dBm8IzO$<8^%f&O51k;uk-c$ZtvL z{N!Jj5Aab-;nC2SGSWWh9?ek+tl!@2Ju&~Z4kze`_RuR2-DKnU8)nxaz-RsrVW`d@ z>&yAAMb82q|EqD0@a;Jg6H_1~k1#TfAY&mSw`bkpWY7&a6AdMb)0}rTzo@Qrku^zZ zZGm%K7Y*AF1!wV>aDm!=g8Q{N2)y(rORSG4hmekYHY7z#D*3eoBqXvq zt7$JH?6SDti0j5}65eMjF(>Y)`073W7vj1%{?ZjrHtN#Z{)3U}?{udUpDh@yNP-W! zUSfnJooenj!wD9?+blV6fpIR|4(M*V7=6oJkzK2b$zkJuqU7P6afzb?GpyX>c+BXe ztiW~=vKkgw7w!MWVX+i?0Yjd$V6o&c35F>^fDOin45vzf4DKf=|LSqb^JL!E2OSOe z77~mpdNJo|f4zMc6_Ni~5cLj{gel}@NzS?~0e(a8;X(J{<}>V_bwj`9H8ePZ9`3sl zLCPm?r!v($T}#>Dc+xNr1jn7#6*)F57LU5Abka4ya(=wT42wSoqZx)Ueoxl>#;`$& z7>_CW#u%L6@i%ppqG-u;%?=Pt-fmBc;BBzDXgvc4sIZGsH7vx z9^bS-x1y8nIna*Fw2sErcHlG(zxg zSsLNgBDvb5y=I7>HkwQlSUPFm1bnyidl4rv6W`sD5F6Hs&WO=ovI-}n_+dfE@Er0} zL{eTr4?c^o{W9)ey4SP7`FwDAs06&)HPXWR^V(747*xpip!b(8Gcc!K^Ed2g1R7_I zUD%yIKK!4%jfk_Z)A>d@g?zH$!L$LN&vk_>1d-!TZji9l0Xy ze1}kbSB4C!PxK)K#UR*)NM8aqHdu-1<4@#TpCdxmpE$5SJvnM$=;Oh+1q0>ZE4`f+ zOv*W&&@RUM5TGN3HUWQy@en9L3=K^V$u0y*IjzWjj1f=vW+2C?RJoPeWe2*t8gC2I7D{_1$P-qDWbu%p~0& zm;(Vf`13$*WWZ`5>F+8d3V+`Gcyn~UuPO;1ZPTS zD2nM7__H&!b%+>{NueJgk%Va1p>q;#g3n^lB7%a%0>py*Bglhxpk?qzc~covvqZ#l zStjH6#HEQ6(KJKkLbJ?l>wD^3>oe;=)sxk$nq^IlO|S2X>|O0kI6UqT?N?4mOnjVV zo*JIioDQBO$gL_mEyyzY&J@Cc&2-0T#FVbzJiyQwVuWrqV|rygqQ^7*YxFJ?rk8Zc zXb62Ie!wB7B+JY<#aBIMA(Yd%CHufubTXfr<1H9wCJ$b!oBcFfRhnof^#i)j}hct!U zvYpZrFlW)}Qm(;t(sjP|qVqy~QG20&@p@4PR{$6Hb?_zkW%qLtR2KB^ChnH)eoLf7 zB$d-4gcYCzvl*Zq_$Dw4?iTtL83n@@laq3an(5;Z`B&;xQa3>drEg-=nzWMT8b>W0 zVp_^PBJGME0uV$wq$rdMbmtjkx3Q!OJ+zO4p9%GZ;Nfe#~{M6PJm57Q6^W$nH4TYnwj|O_Lb@@ z-dCD~p##PP)tRm@5@rEkX3V8eF;`?(J)Lgj3w_8x)k)fGh06_V9@ft_vvsk3YSVC6 z6qg^6D-zqyt8BKaw(>tQEh=F{}7NZpt79*uvYr9>Ymku*`iktnDvPGoh+l93h;^f$r3%*&7YmX0n zA4!jGTP@u5A8z4yzuicmjLeLbyy=IcGeW7OR40LAMa&!$Glnw0-KE@$-+3Cl9`;P8 zNK=h%a{75Y(c5bmEQh1s@$D|$K3p7ZQKUM)In@Tu(8mXz zBjshyTs6A`ju)4s^K&#zibcsl$*9E2gtv(Z35N;n$~MYdN>9aCWgc#dyGEN-C!bx5 zyfP5Eo^ywqOA9peTeskMq?c2WOf$xA(m(U6s`e_xsn+S>=x}?jo_MHcNNQ{Is(QY> z{So*Jju`*zi-+rz1xW8ihZv2Fw-Sg$k&#zx>e%apjg$(?>gX_YYrWe+w>kM;m zc;IqG=S_VKI*}Q~o>#lpJa3ZO9NJ{3fG2+-Kc>)?yO$m92={8b;k$e@8XOzdPHNeB&i+T$#6Q-_GT6|K#uILd2@C8iPE^z=&ka3u6^3I{4ToSs;|pYW&4$ zb%<@SU}%Z@jK)K;z0yHoK_yUXyW?w~hF@#tJDtsi6IdDk3T+MD22IDCgM)pPtJQ-w zregMT7HWo7YaymtHbZ7jmeaZ~_2u=YwI|gnwZcnv+%_CjoJ1LO>8jekIAa-mf^|?0 zri1oV=HDL?_e

!&*0M%wL*y1!q!utU*+U z=fW#-sv%?znliDHBhg-M*RPMb?YF`b%E@$@Ul|Ilr8sz6&jGn$Z@fl-ue515B)t!c zCQ5+``hZCE{+nNHV2lp2r0L76FAvdP>q7UZUOT<$+aiL!>x`IyK_!#IzGS-^;x+KH zwf^|_fR=B*4F+XK2=Px~P^>`K4GJkfx9<*1NOhTS_?*J;va9=|3G8OQ)&p0)^yx=V zZXv#m`^-G*==|Y%Y*QU;9Z)m^e(>;21IjMcp93BK7D3sHbdYudNGQ=U$~ch>BKcQq zA~*X^PDRC)AyUqA8u{78A)JXN%PRCbqy^nI@o+>!7n&1N{0c4~wSmRZ5+kPNiRwdv z;AamAP@iPGpB!@Q^*J=ANkZ_OdanESD+J_1MT&Yl^)^~

eSHMPnsq^xakniRtj{ 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�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 Gf&UNox^Ub8 diff --git a/services/horizon/internal/docs/plans/new_horizon_ingest.md b/services/horizon/internal/docs/plans/new_horizon_ingest.md deleted file mode 100644 index 615d671948..0000000000 --- a/services/horizon/internal/docs/plans/new_horizon_ingest.md +++ /dev/null @@ -1,181 +0,0 @@ -# New Horizon Ingest - -This describes the goals, design, and implementation plan for the new Horizon ingestion system. - -## Project Goals - -- Handle need for Horizon to re-ingest, catch up after outage, or fill gaps -- No more stellar-core DB access from Horizon -- Full history ingestion of Stellar Public Network ledger shouldn’t take longer than 24h on x1.32xlarge AWS machine -- Horizon maintains own state of the ledger. Order books, at least, need to be kept in-memory to allow fast pathfinding. -- Multi-writer ingestion is provided for re-ingestion (speed) and for ledger stream (high availability) -- Ingestion is a collection of micro-services (trade aggregations, trades, txn history, etc…) -- Support plugins, so 3rd parties can implement custom ingestion schemes and easily plug them in -- Can run as standalone process, separate from Horizon -- Has clear API for building clients on top of it - -## Design - -### Inputs - -The ingestion system will read data from two sources: - -1. A History Archive [1](https://www.stellar.org/developers/stellar-core/software/admin.html#history-archives),[2](https://github.com/stellar/stellar-core/blob/master/docs/history.md), which is generated by `stellar-core` and provides a complete copy of a recent [ledger](https://www.stellar.org/developers/guides/concepts/ledger.html) as well as a history of `TransactionResult` XDR objects, which in turn contain [operation results](https://github.com/stellar/stellar-core/blob/master/src/xdr/Stellar-transaction.x#L382-L834). -2. A full ledger reader, which provides random and up-to-date access to the transaction sets & [`TransactionMeta`](https://github.com/stellar/stellar-core/blob/master/src/xdr/Stellar-ledger.x#L271-L318) in each ledger close. `TransactionMeta` info is essential for keeping a view of the ledger state up to date, as it describes the changes to ledger state that result from each operation. - -#### History Archive Reader - -The ingestion system needs to provide a full copy of current ledger state so that Horizon and other consumers of the ingestion system have no need to access the `stellar-core` database. The history archive reader allows ingestion to read the (near) current ledger state from a history archive. The advantage of this approach is that ingestion puts no load on the `stellar-core` database to pull in ledger state, since it reads entirely from a history archive, which is often stored in S3 or a separate file system. - -For context, a `stellar-core` can be configured to write out a history archive, which stores snapshots of the ledger state every 64 ledgers (approximately every 5 minutes). We envision that an administrator will run a `stellar-core`, configure it to write out a history archive, and then point the ingestion system at that history archive. This has two advantages: - -1. The ingestion does not put load on any external service (like SDF's history archives) -2. The administrator does not need to trust third party history archives - -Typically, the ingestion system will only access the history archive on startup to get a full copy of ledger state, and will then keep that copy of ledger state up to date using data from the separate ledger transaction set backend. However, the ingestion system could access older snapshots to construct a history of ledger state, or it could periodically re-ingest the full ledger state to detect any errors that accumulate over time from updating the ledger state. - -The history archive reader supports multiple backends to handle different ways that a history archive can be stored: - -1. S3 backend -2. HTTP backend -3. File backend - -UML Class diagram - -![History Archive Reader Class Diagram](images/historyarchive.png) - -Example of reading a history archive using `stellar-core`: - -```sh -wget http://history.stellar.org/prd/core-live/core_live_001/results/01/4d/f7/results-014df7ff.xdr.gz -gunzip results-014df7ff.xdr.gz -~/src/stellar-core/src/stellar-core dump-xdr results-014df7ff.xdr | head -n 40 -``` - -#### Full Ledger Reader - -The ingestion system needs a way to keep the current ledger state up to date, as well as a way to construct a history of events (typically transactions, operations, and effects) that have occurred over time on the Stellar network. The full ledger reader provides this ability. Specifically, it allows the ingestion system to access a stream of transaction metadata that encode state changes that happen as a result of every transaction. This information is missing from the history archives, and is also updated after every ledger close (vs. after every 64 in the history archives). The full ledger reader also has access to transaction sets for each ledger. - -Here's a summary of the features provided by the full ledger reader vs. the history archive reader: - -| Reader | txn resultsets | ledger state snapshots | transaction metadata | near-realtime (updates at every ledger close) | -| --- |:---:|:---:|:---:|:---:| -| history archive | X | X | | | -| full ledger | X | | X | X | - -The long term plan for the full ledger reader is for `stellar-core` to write transaction metadata out to an S3 bucket, which will allow the following: - -1. Reading transaction metadata without creating load on `stellar-core`'s database -2. Fast, parallel access to historical transaction metadata (allowing fast `CATCHUP_COMPLETE` ingestion) -3. Multiple `stellar-core`s writing the latest update to the same S3 object. This allows redundancy: one `stellar-core` can fail, and the stream of S3 objects will continue uninterrupted. - -However, this requires a change to `stellar-core`, which is estimated to happen in Q3 2019. Until then, the ingestion system will read from the `txfeehistory` and [`txhistory`](https://github.com/stellar/stellar-core/blob/master/src/transactions/TransactionFrame.cpp#L683) tables in the `stellar-core` database as it does currently. Unfortunately, we won't get any of the benefits listed above until the change is made to `stellar-core`. - -The full ledger reader will support multiple backends: - -1. `stellar-core` database reader (this will be implemented first, and is exactly what happens now) -2. File backend -3. S3 backend - -UML Class diagram - -![Full Ledger Reader Class Diagram](images/ledgerbackend.png) - -### Data Transformation - -The ingestion system has a data transformation pipeline in between its inputs (full ledger backend + history archive backend), and its outputs. - -#### Input Adapters - -The first step is to adapt the format of the data flowing from the two input backends into a format & interface that is easy for the ingestion system to use for both random access, and accessing the latest data. - -UML Class diagram - -![Input Adapters Class Diagram](images/adapters.png) - -Notes: - -- the `HistoryArchiveAdapter` supports both reading a ledger transaction result set via `GetLedger()` and reading ledger state via `GetState()`. -- Both adapters support `GetLatestLedgerSequence()`, which allows a consumer to look up the most recent ledger information in the backend. -- The adapters, rather than returning state and ledgers as objects stored in memory, return them as `ReadCloser` objects. This is because the ledger state or a particular transaction state may not fit in memory, and must be processed as a stream. - -Both `ReadCloser` and `WriteCloser` interfaces include `Close()` method. When `Close()` is called on `ReadCloser` it tells the reader that no more data is needed and it should stop streaming, ex. close buffered channels, close network connections, etc. When `Close` is is called on `WriteCloser` it means that no more data will be written to it. This is especially helpful when writer is writing to a pipe so the reader on the other end knows that no more objects will be streamed and can return `EOF`. - -The `ReadCloser` structs come from the `ingest/io` package, shown in the UML class diagram below: - -![IO package](images/io.png) - -#### Ingestion Pipeline - -At the center of ingestion is a `Pipeline`, which is initialized with a series of processors for each kind of data that ingestion handles (ledger state, a full ledger update, and an archive ledger update). Each processor implements a function that reads data from a `ReadCloser`, processes/filters it, and writes it to a `WriteCloser`. A few example processors: - -- A processor that passes on only information about a certain account -- A processor that only looks at certain kinds of events, such as offers being placed -- A processor sending a mobile notification for incoming payments -- A processor saving data to a database or in-memory store. - -See the rough UML diagram: - -![Filter package](images/pipeline.png) - -Notes: - -- The processors form a tree and are processed from root to leaves. -- Processing does not change the type of the data, but it can remove or change fields in the data -- Processors can write/read artifacts (ex. average balances) in a `Store` that is shared between all processors in a pipeline. `Store` is ephemeral, its contents is removed when pipeline processing is done. - -### Tying it all together - -The ingestion system can run as a stand-alone process or as part of a larger process like Horizon. It can handle three different kinds of sessions: - -- `IngestRangeSession`: ingest data over a range of ledgers. Used for ingesting historical data in parallel -- `LiveSession`: ingest the latest ledgers as soon as the close. This is the standard operating mode for a live Horizon instance -- `CheckpointsOnlySession`: ingest only history archive checkpoint data. - -Sessions are responsible for coordinating pipelines and ensuring data is in sync. When processors write to a single storage type, keeping data in sync is trivial. For example, for Postgres processors can share a single transaction object and commit when processing is done. However, there's also a significant challenge keeping data in sync when processors are writing to different storage types: any read operation that reads across stores or across data updated by multiple `Processes`s is at risk of reading inconsistent values. `System` or `Session` objects (TBD) should be responsible by keeping different stores in sync. For example: they can ensure that all queries sent to stores have the latest ledger sequence that was successfully saved all stores appended. This probably requires stores to keep data for the two (2) latest ledgers. - -![ingest package](images/system.png) - -## Open questions - -There are several open questions, and the design above isn't comprehensive. A few questions below: - -- What is the story around ingestion plugins? How do developers use the ingestion system in their own projects? -- How do we enable ingestion via multiple languages? -- Will we split Horizon into an ingestion process and an API server process, or keep a single process? -- Should a stand-alone ingestion process expose an RPC server or a more standard REST API? - - What's the exact API that an ingestion server exposes? -- Where is parallel ingestion logic handled? -- How exactly do we organize `Store`s, `ProcessingPipeline`, and `Process`es? -- How do we keep reads from multiple stores consistent? -- How do we make the `Store`s and `Process`es in `ingest/stores` reusable? - -## Implementation Plan - -This gives an overview of how we could move Horizon over to using the new ingestion system. - -### As we implement components - -We can create a command-line tool that computes basic stats and exercises all the components of ingestion to prove that they work together. For example, the tool can compute the total number of accounts every 5 minutes, or compute the number of transactions per second. - -### The proof of concept - -We can implement [accounts for signer](https://github.com/stellar/go/issues/432) using the new ingestion system. It's a good candidate because it's something we urgently need, and it's a new feature, so we don't risk breaking anything existing if there's an issue with the system. - -### Chunks that can be broken off and implemented separately - -- The projects below depend on the `io` package interfaces, which should be pretty minimal to add. -- History Archive Backend and History Archive Adapter (Nikhil is already on this) - - command-line tool takes a history archive file and prints out basic stats like # of accounts and transactions per second -- `DatabaseBackend` implementation of `LedgerBackend` and `LedgerBackendAdapter` - - command-line tool takes a database URI and computes stats like # of transactions in the latest ledger (it runs live and updates with each new ledger in the `DatabaseBackend`) -- `ingest/pipeline`: - - command-line tool that implements a few demo processors and given a DB URL and/or a historyarchive file location, streams out the data that passes the filter - -Once the above are all done, put together a basic ingestion server as laid out in the top-level `ingest` package. - -At this point, we'll have a full end-to-end implementation! Next, we start filling in the extra features we didn't need for the proof of concept. Once it's reasonably stable, we can release Horizon with the new `accoutns for signer` feature to get some real-world testing. - -### Porting the rest of Horizon over - -Once the proof of concept is functional, tables / features can be ported over one by one. Features that depend on `stellar-core` tables currently can be moved to equivalent tables maintained by Horizon. I imagine most features will still use postgres, but a few, such as pathfinding, will need in-memory storage for speed. If pathfinding can maintain a copy of order books [in memory](https://github.com/stellar/go/issues/849) it should run orders of magnitude faster. Horizon should be split out into a microservices architecture, with each feature implemented using a different `StateInitProcess` or `LedgerProcess` object. diff --git a/services/horizon/internal/docs/reference/endpoints/accounts-single.md b/services/horizon/internal/docs/reference/endpoints/accounts-single.md deleted file mode 100644 index ca0ccfdcf9..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/accounts-single.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -title: Account Details -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=accounts&endpoint=single -replacement: https://developers.stellar.org/api/resources/accounts/single/ ---- - -Returns information and links relating to a single [account](../resources/account.md). - -The balances section in the returned JSON will also list all the -[trustlines](https://www.stellar.org/developers/learn/concepts/assets.html) this account -established. Note this will only return trustlines that have the necessary authorization to work. -Meaning if an account `A` trusts another account `B` that has the -[authorization required](https://www.stellar.org/developers/guides/concepts/accounts.html#flags) -flag set, the trustline won't show up until account `B` -[allows](https://www.stellar.org/developers/guides/concepts/list-of-operations.html#allow-trust) -account `A` to hold its assets. - -## Request - -``` -GET /accounts/{account} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `account` | required, string | Account ID | GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.accounts() - .accountId("GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB") - .call() - .then(function (accountResult) { - console.log(accountResult); - }) - .catch(function (err) { - console.error(err); - }) -``` - -## Response - -This endpoint responds with the details of a single account for a given ID. See [account resource](../resources/account.md) for reference. - -### Example Response -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB" - }, - "transactions": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/transactions{?cursor,limit,order}", - "templated": true - }, - "operations": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/operations{?cursor,limit,order}", - "templated": true - }, - "payments": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/payments{?cursor,limit,order}", - "templated": true - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/effects{?cursor,limit,order}", - "templated": true - }, - "offers": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/offers{?cursor,limit,order}", - "templated": true - }, - "trades": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/trades{?cursor,limit,order}", - "templated": true - }, - "data": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/data/{key}", - "templated": true - } - }, - "id": "GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB", - "paging_token": "", - "account_id": "GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB", - "sequence": 7275146318446606, - "last_modified_ledger": 22379074, - "subentry_count": 4, - "thresholds": { - "low_threshold": 0, - "med_threshold": 0, - "high_threshold": 0 - }, - "flags": { - "auth_required": false, - "auth_revocable": false, - "auth_immutable": false - }, - "balances": [ - { - "balance": "1000000.0000000", - "limit": "922337203685.4775807", - "buying_liabilities": "0.0000000", - "selling_liabilities": "0.0000000", - "last_modified_ledger": 632070, - "is_authorized": true, - "asset_type": "credit_alphanum4", - "asset_code": "FOO", - "asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR" - }, - { - "balance": "10000.0000000", - "buying_liabilities": "0.0000000", - "selling_liabilities": "0.0000000", - "asset_type": "native" - } - ], - "signers": [ - { - "public_key": "GDLEPBJBC2VSKJCLJB264F2WDK63X4NKOG774A3QWVH2U6PERGDPUCS4", - "weight": 1, - "key": "GDLEPBJBC2VSKJCLJB264F2WDK63X4NKOG774A3QWVH2U6PERGDPUCS4", - "type": "ed25519_public_key" - }, - { - "public_key": "GBPOFUJUHOFTZHMZ63H5GE6NX5KVKQRD6N3I2E5AL3T2UG7HSLPLXN2K", - "weight": 1, - "key": "GBPOFUJUHOFTZHMZ63H5GE6NX5KVKQRD6N3I2E5AL3T2UG7HSLPLXN2K", - "type": "sha256_hash" - }, - { - "public_key": "GDUDIN23QQTB23Q3Q6GUL6I7CEAQY4CWCFVRXFWPF4UJAQO47SPUFCXG", - "weight": 1, - "key": "GDUDIN23QQTB23Q3Q6GUL6I7CEAQY4CWCFVRXFWPF4UJAQO47SPUFCXG", - "type": "preauth_tx" - }, - { - "public_key": "GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB", - "weight": 1, - "key": "GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB", - "type": "ed25519_public_key" - } - ], - "data": { - "best_friend": "c3Ryb29weQ==" - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no account whose ID matches the `account` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/accounts.md b/services/horizon/internal/docs/reference/endpoints/accounts.md deleted file mode 100644 index 29bfd2bd40..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/accounts.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -title: Accounts -replacement: https://developers.stellar.org/api/resources/accounts/ ---- - -This endpoint allows filtering accounts who have a given `signer` or have a trustline to an `asset`. The result is a list of [accounts](../resources/account.md). - -To find all accounts who are trustees to an asset, pass the query parameter `asset` using the canonical representation for an issued assets which is `Code:IssuerAccountID`. Read more about canonical representation of assets in [SEP-0011](https://github.com/stellar/stellar-protocol/blob/0c675fb3a482183dcf0f5db79c12685acf82a95c/ecosystem/sep-0011.md#values). - -### Notes -- The default behavior when filtering by `asset` is to return accounts with `authorized` and `unauthorized` trustlines. - -## Request - -``` -GET /accounts{?signer,asset,cursor,limit,order} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `?signer` | optional, string | Account ID | GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB | -| `?asset` | optional, string | An issued asset represented as "Code:IssuerAccountID". | `USD:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V,native` | -| `?cursor` | optional, default _null_ | A paging token, specifying where to start returning records from. | `GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default `10` | Maximum number of records to return. | `200` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/accounts?signer=GBPOFUJUHOFTZHMZ63H5GE6NX5KVKQRD6N3I2E5AL3T2UG7HSLPLXN2K" -``` - - - - - - - - - - - - - - - - - -## Response - -This endpoint responds with the details of all accounts matching the filters. See [account resource](../resources/account.md) for reference. - -### Example Response -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/accounts?cursor=\u0026limit=10\u0026order=asc\u0026signer=GBPOFUJUHOFTZHMZ63H5GE6NX5KVKQRD6N3I2E5AL3T2UG7HSLPLXN2K" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/accounts?cursor=GDRREYWHQWJDICNH4SAH4TT2JRBYRPTDYIMLK4UWBDT3X3ZVVYT6I4UQ\u0026limit=10\u0026order=asc\u0026signer=GBPOFUJUHOFTZHMZ63H5GE6NX5KVKQRD6N3I2E5AL3T2UG7HSLPLXN2K" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/accounts?cursor=GDRREYWHQWJDICNH4SAH4TT2JRBYRPTDYIMLK4UWBDT3X3ZVVYT6I4UQ\u0026limit=10\u0026order=desc\u0026signer=GBPOFUJUHOFTZHMZ63H5GE6NX5KVKQRD6N3I2E5AL3T2UG7HSLPLXN2K" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB" - }, - "transactions": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/transactions{?cursor,limit,order}", - "templated": true - }, - "operations": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/operations{?cursor,limit,order}", - "templated": true - }, - "payments": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/payments{?cursor,limit,order}", - "templated": true - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/effects{?cursor,limit,order}", - "templated": true - }, - "offers": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/offers{?cursor,limit,order}", - "templated": true - }, - "trades": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/trades{?cursor,limit,order}", - "templated": true - }, - "data": { - "href": "https://horizon-testnet.stellar.org/accounts/GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB/data/{key}", - "templated": true - } - }, - "id": "GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB", - "paging_token": "", - "account_id": "GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB", - "sequence": 7275146318446606, - "last_modified_ledger": 22379074, - "subentry_count": 4, - "thresholds": { - "low_threshold": 0, - "med_threshold": 0, - "high_threshold": 0 - }, - "flags": { - "auth_required": false, - "auth_revocable": false, - "auth_immutable": false - }, - "balances": [ - { - "balance": "1000000.0000000", - "limit": "922337203685.4775807", - "buying_liabilities": "0.0000000", - "selling_liabilities": "0.0000000", - "last_modified_ledger": 632070, - "asset_type": "credit_alphanum4", - "asset_code": "FOO", - "asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR", - "is_authorized": true - }, - { - "balance": "10000.0000000", - "buying_liabilities": "0.0000000", - "selling_liabilities": "0.0000000", - "asset_type": "native" - } - ], - "signers": [ - { - "public_key": "GDLEPBJBC2VSKJCLJB264F2WDK63X4NKOG774A3QWVH2U6PERGDPUCS4", - "weight": 1, - "key": "GDLEPBJBC2VSKJCLJB264F2WDK63X4NKOG774A3QWVH2U6PERGDPUCS4", - "type": "ed25519_public_key" - }, - { - "public_key": "GBPOFUJUHOFTZHMZ63H5GE6NX5KVKQRD6N3I2E5AL3T2UG7HSLPLXN2K", - "weight": 1, - "key": "GBPOFUJUHOFTZHMZ63H5GE6NX5KVKQRD6N3I2E5AL3T2UG7HSLPLXN2K", - "type": "sha256_hash" - }, - { - "public_key": "GDUDIN23QQTB23Q3Q6GUL6I7CEAQY4CWCFVRXFWPF4UJAQO47SPUFCXG", - "weight": 1, - "key": "GDUDIN23QQTB23Q3Q6GUL6I7CEAQY4CWCFVRXFWPF4UJAQO47SPUFCXG", - "type": "preauth_tx" - }, - { - "public_key": "GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB", - "weight": 1, - "key": "GD42RQNXTRIW6YR3E2HXV5T2AI27LBRHOERV2JIYNFMXOBA234SWLQQB", - "type": "ed25519_public_key" - } - ], - "data": { - "best_friend": "c3Ryb29weQ==" - } - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). diff --git a/services/horizon/internal/docs/reference/endpoints/assets-all.md b/services/horizon/internal/docs/reference/endpoints/assets-all.md deleted file mode 100644 index c5281a6b32..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/assets-all.md +++ /dev/null @@ -1,159 +0,0 @@ ---- -title: All Assets -clientData: - laboratoryUrl: -replacement: https://developers.stellar.org/api/resources/assets/ ---- - -This endpoint represents all [assets](../resources/asset.md). -It will give you all the assets in the system along with various statistics about each. - -## Request - -``` -GET /assets{?asset_code,asset_issuer,cursor,limit,order} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `?asset_code` | optional, string, default _null_ | Code of the Asset to filter by | `USD` | -| `?asset_issuer` | optional, string, default _null_ | Issuer of the Asset to filter by | `GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36` | -| `?cursor` | optional, any, default _null_ | A paging token, specifying where to start returning records from. | `1` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc", ordered by asset_code then by asset_issuer. | `asc` | -| `?limit` | optional, number, default: `10` | Maximum number of records to return. | `200` | - -### curl Example Request - -```sh -# Retrieve the 200 assets, ordered alphabetically: -curl "https://horizon-testnet.stellar.org/assets?limit=200" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.assets() - .call() - .then(function (result) { - console.log(result.records); - }) - .catch(function (err) { - console.log(err) - }) -``` - -## Response - -If called normally this endpoint responds with a [page](../resources/page.md) of assets. - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "/assets?order=asc\u0026limit=10\u0026cursor=" - }, - "next": { - "href": "/assets?order=asc\u0026limit=10\u0026cursor=3" - }, - "prev": { - "href": "/assets?order=desc\u0026limit=10\u0026cursor=1" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "toml": { - "href": "https://www.stellar.org/.well-known/stellar.toml" - } - }, - "asset_type": "credit_alphanum12", - "asset_code": "BANANA", - "asset_issuer": "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", - "paging_token": "BANANA_GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN_credit_alphanum4", - "accounts": { - "authorized": 2126, - "authorized_to_maintain_liabilities": 32, - "unauthorized": 5, - "claimable_balances": 18 - }, - "balances": { - "authorized": "10000.0000000", - "authorized_to_maintain_liabilities": "3000.0000000", - "unauthorized": "4000.0000000", - "claimable_balances": "2380.0000000" - }, - "flags": { - "auth_required": true, - "auth_revocable": false - } - }, - { - "_links": { - "toml": { - "href": "https://www.stellar.org/.well-known/stellar.toml" - } - }, - "asset_type": "credit_alphanum4", - "asset_code": "BTC", - "asset_issuer": "GBAUUA74H4XOQYRSOW2RZUA4QL5PB37U3JS5NE3RTB2ELJVMIF5RLMAG", - "paging_token": "BTC_GBAUUA74H4XOQYRSOW2RZUA4QL5PB37U3JS5NE3RTB2ELJVMIF5RLMAG_credit_alphanum4", - "accounts": { - "authorized": 32, - "authorized_to_maintain_liabilities": 124, - "unauthorized": 6, - "claimable_balances": 18 - }, - "balances": { - "authorized": "5000.0000000", - "authorized_to_maintain_liabilities": "8000.0000000", - "unauthorized": "2000.0000000", - "claimable_balances": "1200.0000000" - }, - "flags": { - "auth_required": false, - "auth_revocable": false - } - }, - { - "_links": { - "toml": { - "href": "https://www.stellar.org/.well-known/stellar.toml" - } - }, - "asset_type": "credit_alphanum4", - "asset_code": "USD", - "asset_issuer": "GBAUUA74H4XOQYRSOW2RZUA4QL5PB37U3JS5NE3RTB2ELJVMIF5RLMAG", - "paging_token": "USD_GBAUUA74H4XOQYRSOW2RZUA4QL5PB37U3JS5NE3RTB2ELJVMIF5RLMAG_credit_alphanum4", - "accounts": { - "authorized": 91547871, - "authorized_to_maintain_liabilities": 45773935, - "unauthorized": 22886967, - "claimable_balances": 11443483 - }, - "balances": { - "authorized": "1000000000.0000000", - "authorized_to_maintain_liabilities": "500000000.0000000", - "unauthorized": "250000000.0000000", - "claimable_balances": "12500000.0000000" - }, - "flags": { - "auth_required": false, - "auth_revocable": false - } - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#standard-errors). diff --git a/services/horizon/internal/docs/reference/endpoints/data-for-account.md b/services/horizon/internal/docs/reference/endpoints/data-for-account.md deleted file mode 100644 index 98ff017623..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/data-for-account.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Data for Account -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=data&endpoint=for_account -replacement: https://developers.stellar.org/api/resources/accounts/data/ ---- - -This endpoint represents a single [data](../resources/data.md) associated with a given [account](../resources/account.md). - -## Request - -``` -GET /accounts/{account}/data/{key} -``` - -### Arguments - -| name | notes | description | example | -| ------ | ------- | ----------- | ------- | -| `key`| required, string | Key name | `user-id`| - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/accounts/GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36/data/user-id" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.accounts() - .accountId("GAKLBGHNHFQ3BMUYG5KU4BEWO6EYQHZHAXEWC33W34PH2RBHZDSQBD75") - .call() - .then(function (account) { - return account.data({key: 'user-id'}) - }) - .then(function(dataValue) { - console.log(dataValue) - }) - .catch(function (err) { - console.log(err) - }) -``` - -## Response - -This endpoint responds with a value of the data field for the given account. See [data resource](../resources/data.md) for reference. - -### Example Response - -```json -{ - "value": "MTAw" -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no account whose ID matches the `account` argument or there is no data field with a given key. diff --git a/services/horizon/internal/docs/reference/endpoints/effects-all.md b/services/horizon/internal/docs/reference/endpoints/effects-all.md deleted file mode 100644 index 3150ed6fd7..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/effects-all.md +++ /dev/null @@ -1,133 +0,0 @@ ---- -title: All Effects -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=effects&endpoint=all -replacement: https://developers.stellar.org/api/resources/effects/list/ ---- - -This endpoint represents all [effects](../resources/effect.md). - -This endpoint can also be used in [streaming](../streaming.md) mode so it is possible to use it to listen for new effects as transactions happen in the Stellar network. -If called in streaming mode Horizon will start at the earliest known effect unless a `cursor` is set. In that case it will start from the `cursor`. You can also set `cursor` value to `now` to only stream effects created since your request time. - -## Request - -``` -GET /effects{?cursor,limit,order} -``` - -## Arguments - -| name | notes | description | example | -| ------ | ------- | ----------- | ------- | -| `?cursor` | optional, default _null_ | A paging token, specifying where to start returning records from. When streaming this can be set to `now` to stream object created since your request time. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default `10` | Maximum number of records to return. | `200` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/effects" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.effects() - .call() - .then(function (effectResults) { - //page 1 - console.log(effectResults.records) - }) - .catch(function (err) { - console.log(err) - }) -``` - -### JavaScript Streaming Example - -```javascript -var StellarSdk = require('stellar-sdk') -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var effectHandler = function (effectResponse) { - console.log(effectResponse); -}; - -var es = server.effects() - .cursor('now') - .stream({ - onmessage: effectHandler - }) -``` - -## Response - -The list of effects. - -### Example Response - -```json -{ - "_embedded": { - "records": [ - { - "_links": { - "operation": { - "href": "/operations/279172878337" - }, - "precedes": { - "href": "/effects?cursor=279172878337-1\u0026order=asc" - }, - "succeeds": { - "href": "/effects?cursor=279172878337-1\u0026order=desc" - } - }, - "account": "GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K", - "paging_token": "279172878337-1", - "starting_balance": "10000000.0", - "type_i": 0, - "type": "account_created" - }, - { - "_links": { - "operation": { - "href": "/operations/279172878337" - }, - "precedes": { - "href": "/effects?cursor=279172878337-2\u0026order=asc" - }, - "succeeds": { - "href": "/effects?cursor=279172878337-2\u0026order=desc" - } - }, - "account": "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", - "amount": "10000000.0", - "asset_type": "native", - "paging_token": "279172878337-2", - "type_i": 3, - "type": "account_debited" - } - ] - }, - "_links": { - "next": { - "href": "/effects?order=asc\u0026limit=2\u0026cursor=279172878337-2" - }, - "prev": { - "href": "/effects?order=desc\u0026limit=2\u0026cursor=279172878337-1" - }, - "self": { - "href": "/effects?order=asc\u0026limit=2\u0026cursor=" - } - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there are no effects for the given account. diff --git a/services/horizon/internal/docs/reference/endpoints/effects-for-account.md b/services/horizon/internal/docs/reference/endpoints/effects-for-account.md deleted file mode 100644 index b60fbdacd8..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/effects-for-account.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: Effects for Account -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=effects&endpoint=for_account -replacement: https://developers.stellar.org/api/resources/accounts/effects/ ---- - -This endpoint represents all [effects](../resources/effect.md) that changed a given -[account](../resources/account.md). It will return relevant effects from the creation of the -account to the current ledger. - -This endpoint can also be used in [streaming](../streaming.md) mode so it is possible to use it to -listen for new effects as transactions happen in the Stellar network. -If called in streaming mode Horizon will start at the earliest known effect unless a `cursor` is -set. In that case it will start from the `cursor`. You can also set `cursor` value to `now` to only -stream effects created since your request time. - -## Request - -``` -GET /accounts/{account}/effects{?cursor,limit,order} -``` - -## Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `account` | required, string | Account ID | `GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36` | -| `?cursor` | optional, default _null_ | A paging token, specifying where to start returning records from. When streaming this can be set to `now` to stream object created since your request time. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default `10` | Maximum number of records to return. | `200` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/accounts/GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36/effects?limit=1" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.effects() - .forAccount("GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36") - .call() - .then(function (effectResults) { - // page 1 - console.log(effectResults.records) - }) - .catch(function (err) { - console.log(err) - }) -``` - -### JavaScript Streaming Example - -```javascript -var StellarSdk = require('stellar-sdk') -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var effectHandler = function (effectResponse) { - console.log(effectResponse); -}; - -var es = server.effects() - .forAccount("GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36") - .cursor('now') - .stream({ - onmessage: effectHandler - }) -``` - -## Response - -The list of effects. - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/accounts/GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36/effects?cursor=&limit=1&order=asc" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/accounts/GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36/effects?cursor=1919197546291201-1&limit=1&order=asc" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/accounts/GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36/effects?cursor=1919197546291201-1&limit=1&order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "operation": { - "href": "https://horizon-testnet.stellar.org/operations/1919197546291201" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=1919197546291201-1" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=1919197546291201-1" - } - }, - "id": "0001919197546291201-0000000001", - "paging_token": "1919197546291201-1", - "account": "GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36", - "type": "account_created", - "type_i": 0, - "created_at": "2019-03-25T22:43:38Z", - "starting_balance": "10000.0000000" - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there are no effects for the given account. diff --git a/services/horizon/internal/docs/reference/endpoints/effects-for-ledger.md b/services/horizon/internal/docs/reference/endpoints/effects-for-ledger.md deleted file mode 100644 index a40f49e1f2..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/effects-for-ledger.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -title: Effects for Ledger -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=effects&endpoint=for_ledger ---- - -Effects are the specific ways that the ledger was changed by any operation. - -This endpoint represents all [effects](../resources/effect.md) that occurred in the given [ledger](../resources/ledger.md). - -## Request - -``` -GET /ledgers/{sequence}/effects{?cursor,limit,order} -``` - -## Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `sequence` | required, number | Ledger Sequence Number | `680777` | -| `?cursor` | optional, default _null_ | A paging token, specifying where to start returning records from. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default `10` | Maximum number of records to return. | `200` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/ledgers/680777/effects?limit=1" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.effects() - .forLedger("680777") - .call() - .then(function (effectResults) { - //page 1 - console.log(effectResults.records) - }) - .catch(function (err) { - console.log(err) - }) - -``` - -## Response - -This endpoint responds with a list of effects that occurred in the ledger. See [effect resource](../resources/effect.md) for reference. - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/ledgers/680777/effects?cursor=&limit=10&order=asc" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/ledgers/680777/effects?cursor=2923914950873089-3&limit=10&order=asc" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/ledgers/680777/effects?cursor=2923914950873089-1&limit=10&order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "operation": { - "href": "https://horizon-testnet.stellar.org/operations/2923914950873089" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=2923914950873089-1" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=2923914950873089-1" - } - }, - "id": "0002923914950873089-0000000001", - "paging_token": "2923914950873089-1", - "account": "GC4ALQ3GTT5BTHTOULHCJGAT4P3MUSPLU4OEE74BAVIJ6K443O6RVLRT", - "type": "account_created", - "type_i": 0, - "created_at": "2019-04-08T20:47:22Z", - "starting_balance": "10000.0000000" - }, - { - "_links": { - "operation": { - "href": "https://horizon-testnet.stellar.org/operations/2923914950873089" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=2923914950873089-2" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=2923914950873089-2" - } - }, - "id": "0002923914950873089-0000000002", - "paging_token": "2923914950873089-2", - "account": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", - "type": "account_debited", - "type_i": 3, - "created_at": "2019-04-08T20:47:22Z", - "asset_type": "native", - "amount": "10000.0000000" - }, - { - "_links": { - "operation": { - "href": "https://horizon-testnet.stellar.org/operations/2923914950873089" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=2923914950873089-3" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=2923914950873089-3" - } - }, - "id": "0002923914950873089-0000000003", - "paging_token": "2923914950873089-3", - "account": "GC4ALQ3GTT5BTHTOULHCJGAT4P3MUSPLU4OEE74BAVIJ6K443O6RVLRT", - "type": "signer_created", - "type_i": 10, - "created_at": "2019-04-08T20:47:22Z", - "weight": 1, - "public_key": "GC4ALQ3GTT5BTHTOULHCJGAT4P3MUSPLU4OEE74BAVIJ6K443O6RVLRT", - "key": "" - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there are no effects for a given ledger. diff --git a/services/horizon/internal/docs/reference/endpoints/effects-for-operation.md b/services/horizon/internal/docs/reference/endpoints/effects-for-operation.md deleted file mode 100644 index dd6b880442..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/effects-for-operation.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: Effects for Operation -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=effects&endpoint=for_operation -replacement: https://developers.stellar.org/api/resources/operations/effects/ ---- - -This endpoint represents all [effects](../resources/effect.md) that occurred as a result of a given [operation](../resources/operation.md). - -## Request - -``` -GET /operations/{id}/effects{?cursor,limit,order} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `id` | required, number | An operation ID | `1919197546291201` | -| `?cursor` | optional, default _null_ | A paging token, specifying where to start returning records from. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default `10` | Maximum number of records to return. | `200` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/operations/1919197546291201/effects" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.effects() - .forOperation("1919197546291201") - .call() - .then(function (effectResults) { - // page 1 - console.log(effectResults.records) - }) - .catch(function (err) { - console.log(err) - }) - -``` - -## Response - -This endpoint responds with a list of effects on the ledger as a result of a given operation. See [effect resource](../resources/effect.md) for reference. - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/operations/1919197546291201/effects?cursor=&limit=10&order=asc" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/operations/1919197546291201/effects?cursor=1919197546291201-3&limit=10&order=asc" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/operations/1919197546291201/effects?cursor=1919197546291201-1&limit=10&order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "operation": { - "href": "https://horizon-testnet.stellar.org/operations/1919197546291201" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=1919197546291201-1" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=1919197546291201-1" - } - }, - "id": "0001919197546291201-0000000001", - "paging_token": "1919197546291201-1", - "account": "GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF", - "type": "account_created", - "type_i": 0, - "created_at": "2019-03-25T22:43:38Z", - "starting_balance": "10000.0000000" - }, - { - "_links": { - "operation": { - "href": "https://horizon-testnet.stellar.org/operations/1919197546291201" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=1919197546291201-2" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=1919197546291201-2" - } - }, - "id": "0001919197546291201-0000000002", - "paging_token": "1919197546291201-2", - "account": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", - "type": "account_debited", - "type_i": 3, - "created_at": "2019-03-25T22:43:38Z", - "asset_type": "native", - "amount": "10000.0000000" - }, - { - "_links": { - "operation": { - "href": "https://horizon-testnet.stellar.org/operations/1919197546291201" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=1919197546291201-3" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=1919197546291201-3" - } - }, - "id": "0001919197546291201-0000000003", - "paging_token": "1919197546291201-3", - "account": "GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF", - "type": "signer_created", - "type_i": 10, - "created_at": "2019-03-25T22:43:38Z", - "weight": 1, - "public_key": "GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF", - "key": "" - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` errors will be returned if there are no effects for operation whose ID matches the `id` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/effects-for-transaction.md b/services/horizon/internal/docs/reference/endpoints/effects-for-transaction.md deleted file mode 100644 index 8bdc302d1b..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/effects-for-transaction.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: Effects for Transaction -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=effects&endpoint=for_transaction -replacement: https://developers.stellar.org/api/resources/transactions/effects/ ---- - -This endpoint represents all [effects](../resources/effect.md) that occurred as a result of a given [transaction](../resources/transaction.md). - -## Request - -``` -GET /transactions/{hash}/effects{?cursor,limit,order} -``` - -## Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `hash` | required, string | A transaction hash, hex-encoded, lowercase. | `7e2050abc676003efc3eaadd623c927f753b7a6c37f50864bf284f4e1510d088` | -| `?cursor` | optional, default _null_ | A paging token, specifying where to start returning records from. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default `10` | Maximum number of records to return. | `200` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/transactions/7e2050abc676003efc3eaadd623c927f753b7a6c37f50864bf284f4e1510d088/effects?limit=1" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.effects() - .forTransaction("7e2050abc676003efc3eaadd623c927f753b7a6c37f50864bf284f4e1510d088") - .call() - .then(function (effectResults) { - //page 1 - console.log(effectResults.records) - }) - .catch(function (err) { - console.log(err) - }) - -``` - -## Response - -This endpoint responds with a list of effects on the ledger as a result of a given transaction. See [effect resource](../resources/effect.md) for reference. - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/transactions/7e2050abc676003efc3eaadd623c927f753b7a6c37f50864bf284f4e1510d088/effects?cursor=&limit=10&order=asc" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/transactions/7e2050abc676003efc3eaadd623c927f753b7a6c37f50864bf284f4e1510d088/effects?cursor=1919197546291201-3&limit=10&order=asc" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/transactions/7e2050abc676003efc3eaadd623c927f753b7a6c37f50864bf284f4e1510d088/effects?cursor=1919197546291201-1&limit=10&order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "operation": { - "href": "https://horizon-testnet.stellar.org/operations/1919197546291201" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=1919197546291201-1" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=1919197546291201-1" - } - }, - "id": "0001919197546291201-0000000001", - "paging_token": "1919197546291201-1", - "account": "GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF", - "type": "account_created", - "type_i": 0, - "created_at": "2019-03-25T22:43:38Z", - "starting_balance": "10000.0000000" - }, - { - "_links": { - "operation": { - "href": "https://horizon-testnet.stellar.org/operations/1919197546291201" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=1919197546291201-2" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=1919197546291201-2" - } - }, - "id": "0001919197546291201-0000000002", - "paging_token": "1919197546291201-2", - "account": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", - "type": "account_debited", - "type_i": 3, - "created_at": "2019-03-25T22:43:38Z", - "asset_type": "native", - "amount": "10000.0000000" - }, - { - "_links": { - "operation": { - "href": "https://horizon-testnet.stellar.org/operations/1919197546291201" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=1919197546291201-3" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=1919197546291201-3" - } - }, - "id": "0001919197546291201-0000000003", - "paging_token": "1919197546291201-3", - "account": "GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF", - "type": "signer_created", - "type_i": 10, - "created_at": "2019-03-25T22:43:38Z", - "weight": 1, - "public_key": "GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF", - "key": "" - } - ] - } -} -``` - -## Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there are no effects for transaction whose hash matches the `hash` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/fee-stats.md b/services/horizon/internal/docs/reference/endpoints/fee-stats.md deleted file mode 100644 index 1f8be226fc..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/fee-stats.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: Fee Stats -clientData: - laboratoryUrl: -replacement: https://developers.stellar.org/api/aggregations/fee-stats/ ---- - -This endpoint gives useful information about per-operation fee stats in the last 5 ledgers. It can be used to -predict a fee set on the transaction that will be submitted to the network. - -## Request - -``` -GET /fee_stats -``` - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/fee_stats" -``` - -## Response - -Response contains the following fields: - -| Field | | -| - | - | -| last_ledger | Last ledger sequence number | -| last_ledger_base_fee | Base fee as defined in the last ledger | -| ledger_capacity_usage | Average capacity usage over the last 5 ledgers. (0 is no usage, 1.0 is completely full ledgers) | -| fee_charged | fee charged object | -| max_fee | max fee object | - -### Fee Charged Object - -Information about the fee charged for transactions in the last 5 ledgers. - -| Field | | -| - | - | -| min | Minimum fee charged over the last 5 ledgers. | -| mode | Mode fee charged over the last 5 ledgers. | -| p10 | 10th percentile fee charged over the last 5 ledgers. | -| p20 | 20th percentile fee charged over the last 5 ledgers. | -| p30 | 30th percentile fee charged over the last 5 ledgers. | -| p40 | 40th percentile fee charged over the last 5 ledgers. | -| p50 | 50th percentile fee charged over the last 5 ledgers. | -| p60 | 60th percentile fee charged over the last 5 ledgers. | -| p70 | 70th percentile fee charged over the last 5 ledgers. | -| p80 | 80th percentile fee charged over the last 5 ledgers. | -| p90 | 90th percentile fee charged over the last 5 ledgers. | -| p95 | 95th percentile fee charged over the last 5 ledgers. | -| p99 | 99th percentile fee charged over the last 5 ledgers. | - -Note: The difference between `fee_charged` and `max_fee` is that the former -represents the actual fee paid for the transaction while `max_fee` represents -the maximum bid the transaction creator was willing to pay for the transaction. - -### Max Fee Object - -Information about max fee bid for transactions over the last 5 ledgers. - -| Field | | -| - | - | -| min | Minimum (lowest) value of the maximum fee bid over the last 5 ledgers. | -| mode | Mode max fee over the last 5 ledgers. | -| p10 | 10th percentile max fee over the last 5 ledgers. | -| p20 | 20th percentile max fee over the last 5 ledgers. | -| p30 | 30th percentile max fee over the last 5 ledgers. | -| p40 | 40th percentile max fee over the last 5 ledgers. | -| p50 | 50th percentile max fee over the last 5 ledgers. | -| p60 | 60th percentile max fee over the last 5 ledgers. | -| p70 | 70th percentile max fee over the last 5 ledgers. | -| p80 | 80th percentile max fee over the last 5 ledgers. | -| p90 | 90th percentile max fee over the last 5 ledgers. | -| p95 | 95th percentile max fee over the last 5 ledgers. | -| p99 | 99th percentile max fee over the last 5 ledgers. | - - -### Example Response - -```json -{ - "last_ledger": "22606298", - "last_ledger_base_fee": "100", - "ledger_capacity_usage": "0.97", - "fee_charged": { - "max": "100", - "min": "100", - "mode": "100", - "p10": "100", - "p20": "100", - "p30": "100", - "p40": "100", - "p50": "100", - "p60": "100", - "p70": "100", - "p80": "100", - "p90": "100", - "p95": "100", - "p99": "100" - }, - "max_fee": { - "max": "100000", - "min": "100", - "mode": "100", - "p10": "100", - "p20": "100", - "p30": "100", - "p40": "100", - "p50": "100", - "p60": "100", - "p70": "100", - "p80": "100", - "p90": "15000", - "p95": "100000", - "p99": "100000" - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#standard-errors). diff --git a/services/horizon/internal/docs/reference/endpoints/ledgers-all.md b/services/horizon/internal/docs/reference/endpoints/ledgers-all.md deleted file mode 100644 index 242c0df697..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/ledgers-all.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -title: All Ledgers -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=ledgers&endpoint=all -replacement: https://developers.stellar.org/api/resources/ledgers/ ---- - -This endpoint represents all [ledgers](../resources/ledger.md). -This endpoint can also be used in [streaming](../streaming.md) mode so it is possible to use it to get notifications as ledgers are closed by the Stellar network. -If called in streaming mode Horizon will start at the earliest known ledger unless a `cursor` is set. In that case it will start from the `cursor`. You can also set `cursor` value to `now` to only stream ledgers created since your request time. - -## Request - -``` -GET /ledgers{?cursor,limit,order} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `?cursor` | optional, any, default _null_ | A paging token, specifying where to start returning records from. When streaming this can be set to `now` to stream object created since your request time. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default: `10` | Maximum number of records to return. | `200` | - -### curl Example Request - -```sh -# Retrieve the 200 latest ledgers, ordered chronologically -curl "https://horizon-testnet.stellar.org/ledgers?limit=200&order=desc" -``` - -### JavaScript Example Request - -```javascript -server.ledgers() - .call() - .then(function (ledgerResult) { - // page 1 - console.log(ledgerResult.records) - return ledgerResult.next() - }) - .then(function (ledgerResult) { - // page 2 - console.log(ledgerResult.records) - }) - .catch(function(err) { - console.log(err) - }) -``` - - -### JavaScript Streaming Example - -```javascript -var StellarSdk = require('stellar-sdk') -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var ledgerHandler = function (ledgerResponse) { - console.log(ledgerResponse); -}; - -var es = server.ledgers() - .cursor('now') - .stream({ - onmessage: ledgerHandler -}) -``` - -## Response - -This endpoint responds with a list of ledgers. See [ledger resource](../resources/ledger.md) for reference. - -### Example Response - -```json -{ - "_embedded": { - "records": [ - { - "_links": { - "effects": { - "href": "/ledgers/1/effects/{?cursor,limit,order}", - "templated": true - }, - "operations": { - "href": "/ledgers/1/operations/{?cursor,limit,order}", - "templated": true - }, - "self": { - "href": "/ledgers/1" - }, - "transactions": { - "href": "/ledgers/1/transactions/{?cursor,limit,order}", - "templated": true - } - }, - "id": "e8e10918f9c000c73119abe54cf089f59f9015cc93c49ccf00b5e8b9afb6e6b1", - "paging_token": "4294967296", - "hash": "e8e10918f9c000c73119abe54cf089f59f9015cc93c49ccf00b5e8b9afb6e6b1", - "sequence": 1, - "transaction_count": 0, - "successful_transaction_count": 0, - "failed_transaction_count": 0, - "operation_count": 0, - "tx_set_operation_count": 0, - "closed_at": "1970-01-01T00:00:00Z", - "total_coins": "100000000000.0000000", - "fee_pool": "0.0000000", - "base_fee_in_stroops": 100, - "base_reserve_in_stroops": 100000000, - "max_tx_set_size": 50 - }, - { - "_links": { - "effects": { - "href": "/ledgers/2/effects/{?cursor,limit,order}", - "templated": true - }, - "operations": { - "href": "/ledgers/2/operations/{?cursor,limit,order}", - "templated": true - }, - "self": { - "href": "/ledgers/2" - }, - "transactions": { - "href": "/ledgers/2/transactions/{?cursor,limit,order}", - "templated": true - } - }, - "id": "e12e5809ab8c59d8256e691cb48a024dd43960bc15902d9661cd627962b2bc71", - "paging_token": "8589934592", - "hash": "e12e5809ab8c59d8256e691cb48a024dd43960bc15902d9661cd627962b2bc71", - "prev_hash": "e8e10918f9c000c73119abe54cf089f59f9015cc93c49ccf00b5e8b9afb6e6b1", - "sequence": 2, - "transaction_count": 0, - "successful_transaction_count": 0, - "failed_transaction_count": 0, - "operation_count": 0, - "closed_at": "2015-07-16T23:49:00Z", - "total_coins": "100000000000.0000000", - "fee_pool": "0.0000000", - "base_fee_in_stroops": 100, - "base_reserve_in_stroops": 100000000, - "max_tx_set_size": 100 - } - ] - }, - "_links": { - "next": { - "href": "/ledgers?order=asc&limit=2&cursor=8589934592" - }, - "prev": { - "href": "/ledgers?order=desc&limit=2&cursor=4294967296" - }, - "self": { - "href": "/ledgers?order=asc&limit=2&cursor=" - } - } -} -``` - -### Example Streaming Event - -```json -{ - "_links": { - "effects": { - "href": "/ledgers/69859/effects/{?cursor,limit,order}", - "templated": true - }, - "operations": { - "href": "/ledgers/69859/operations/{?cursor,limit,order}", - "templated": true - }, - "self": { - "href": "/ledgers/69859" - }, - "transactions": { - "href": "/ledgers/69859/transactions/{?cursor,limit,order}", - "templated": true - } - }, - "id": "4db1e4f145e9ee75162040d26284795e0697e2e84084624e7c6c723ebbf80118", - "paging_token": "300042120331264", - "hash": "4db1e4f145e9ee75162040d26284795e0697e2e84084624e7c6c723ebbf80118", - "prev_hash": "4b0b8bace3b2438b2404776ce57643966855487ba6384724a3c664c7aa4cd9e4", - "sequence": 69859, - "transaction_count": 0, - "successful_transaction_count": 0, - "failed_transaction_count": 0, - "operation_count": 0, - "closed_at": "2015-07-20T15:51:52Z", - "total_coins": "100000000000.0000000", - "fee_pool": "0.0025600", - "base_fee_in_stroops": 100, - "base_reserve_in_stroops": "100000000", - "max_tx_set_size": 50 -} -``` - -## Errors - -- The [standard errors](../errors.md#standard-errors). diff --git a/services/horizon/internal/docs/reference/endpoints/ledgers-single.md b/services/horizon/internal/docs/reference/endpoints/ledgers-single.md deleted file mode 100644 index f16c9485d8..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/ledgers-single.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Ledger Details -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=ledgers&endpoint=single -replacement: https://developers.stellar.org/api/resources/ledgers/single/ ---- - -The ledger details endpoint provides information on a single [ledger](../resources/ledger.md). - -## Request - -``` -GET /ledgers/{sequence} -``` - -### Arguments - -| name | notes | description | example | -| ------ | ------- | ----------- | ------- | -| `sequence` | required, number | Ledger Sequence | `69859` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/ledgers/69859" -``` - -### JavaScript Example Request - -```js -var StellarSdk = require('stellar-sdk') -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.ledgers() - .ledger('69858') - .call() - .then(function(ledgerResult) { - console.log(ledgerResult) - }) - .catch(function(err) { - console.log(err) - }) - -``` -## Response - -This endpoint responds with a single Ledger. See [ledger resource](../resources/ledger.md) for reference. - -### Example Response - -```json -{ - "_links": { - "effects": { - "href": "/ledgers/69859/effects/{?cursor,limit,order}", - "templated": true - }, - "operations": { - "href": "/ledgers/69859/operations/{?cursor,limit,order}", - "templated": true - }, - "self": { - "href": "/ledgers/69859" - }, - "transactions": { - "href": "/ledgers/69859/transactions/{?cursor,limit,order}", - "templated": true - } - }, - "id": "4db1e4f145e9ee75162040d26284795e0697e2e84084624e7c6c723ebbf80118", - "paging_token": "300042120331264", - "hash": "4db1e4f145e9ee75162040d26284795e0697e2e84084624e7c6c723ebbf80118", - "prev_hash": "4b0b8bace3b2438b2404776ce57643966855487ba6384724a3c664c7aa4cd9e4", - "sequence": 69859, - "transaction_count": 0, - "successful_transaction_count": 0, - "failed_transaction_count": 0, - "operation_count": 0, - "tx_set_operation_count": 0, - "closed_at": "2015-07-20T15:51:52Z", - "total_coins": "100000000000.0000000", - "fee_pool": "0.0025600", - "base_fee_in_stroops": 100, - "base_reserve_in_stroops": 100000000, - "max_tx_set_size": 50 -} -``` - -## Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no ledger whose sequence number matches the `sequence` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/metrics.md b/services/horizon/internal/docs/reference/endpoints/metrics.md deleted file mode 100644 index c0d1fba62b..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/metrics.md +++ /dev/null @@ -1,126 +0,0 @@ ---- -title: Metrics ---- - -The metrics endpoint returns a host of [Prometheus](https://prometheus.io/) metrics for monitoring the health of the underlying Horizon process. - -There is an [official Grafana Dashboard](https://grafana.com/grafana/dashboards/13793) to easily visualize those metrics. - -Since Horizon 1.0.0 this endpoint is not part of the public API. It's available in the internal server (listening on the internal port set via `ADMIN_PORT` env variable or `--admin-port` CLI param). - -## Request - -``` -GET /metrics -``` - -### curl Example Request - -Assuming a local Horizon instance is running with an admin port of 9090 (i.e. `ADMIN_PORT=9090` env variable or `--admin-port=9090`) - -```sh -curl "https://localhost:9090/metrics" -``` - - -## Response - -The `/metrics` endpoint returns a [Prometheus text-formated](https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format) response. It is meant to be scraped by Prometheus. - -Below, each section of related data points are grouped together and annotated (***note**: this endpoint returns ALL this data in one response*). - - -#### Goroutines - -Horizon utilizes Go's built in concurrency primitives ([goroutines](https://gobyexample.com/goroutines) and [channels](https://gobyexample.com/channels)). The `goroutine` metric monitors the number of currently running goroutines on this Horizon's process. - - -#### History - -Horizon maintains its own database (postgres), a verbose and user friendly account of activity on the Stellar network. - -| Metric | Description | -| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| history.elder_ledger | The sequence number of the oldest ledger recorded in Horizon's database. | -| history.latest_ledger | The sequence number of the youngest (most recent) ledger recorded in Horizon's database. | -| history.open_connections | The number of open connections to the Horizon database. | - - -#### Ingester - -Ingester represents metrics specific to Horizon's [ingestion](https://github.com/stellar/go/blob/master/services/horizon/internal/docs/reference/admin.md#ingesting-stellar-core-data) process, or the process by which Horizon consumes transaction results from a connected Stellar Core instance. - -| Metric | Description | -| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| ingester.clear_ledger | The count and rate of clearing (per ledger) for this Horizon process. | -| ingester.ingest_ledger | The count and rate of ingestion (per ledger) for this Horizon process. | - -These metrics contain useful [sub metrics](#sub-metrics). - - -#### Logging - -Horizon utilizes the standard `debug`, `error`, etc. levels of logging. This metric outputs stats for each level of log message produced, useful for a high-level monitoring of "is my Horizon instance functioning properly?" In order of increasing severity: - -* logging.debug -* logging.info -* logging.warning -* logging.error -* logging.panic - -These metrics contain useful [sub metrics](#sub-metrics). - -#### Requests - -Requests represent an overview of Horizon's incoming traffic. - -These metrics contain useful [sub metrics](#sub-metrics). - -| Metric | Description | -| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| requests.failed | Failed requests are those that return a status code in [400, 600). | -| requests.succeeded | Successful requests are those that return a status code in [200, 400). | -| requests.total | Total number of received requests. | - -#### Stellar Core -As noted above, Horizon relies on Stellar Core to stay in sync with the Stellar network. These metrics are specific to the underlying Stellar Core instance. - -| Metric | Description | -| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| stellar_core.latest_ledger | The sequence number of the latest (most recent) ledger recorded in Stellar Core's database. | -| stellar_core.open_connections | The number of open connections to the Stellar Core postgres database. | - -#### Transaction Submission - -Horizon does not submit transactions directly to the Stellar network. Instead, it sequences transactions and sends the base64 encoded, XDR serialized blob to its connected Stellar Core instance. - -##### Horizon Transaction Sequencing and Submission - -The following is a simplified version of the transaction submission process that glosses over the finer details. To dive deeper, check out the [source code](https://github.com/stellar/go/tree/master/services/horizon/internal/txsub). - -Horizon's sequencing mechanism consists of a [manager](https://github.com/stellar/go/blob/master/services/horizon/internal/txsub/sequence/manager.go) that keeps track of [submission queues](https://github.com/stellar/go/blob/master/services/horizon/internal/txsub/sequence/queue.go) for a set of addresses. A submission queue is a priority queue, prioritized by minimum transaction sequence number, that holds a set of pending transactions for an account. A pending transaction is represented as an object with a sequence number and a channel. Periodically, this queue is updated, popping off finished transactions, sending down the transaction's channel a successful/failure response. - -These metrics contain useful [sub metrics](#sub-metrics). - - -| Metric | Description | -| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| txsub.buffered | The count of submissions buffered behind this Horizon's submission queue. | -| txsub.failed | The rate of failed transactions that have been submitted to this Horizon. | -| txsub.open | The count of "open" submissions (i.e.) submissions whose transactions haven't been confirmed successful or failed. | -| txsub.succeeded | The rate of successful transactions that have been submitted to this Horizon. | -| txsub.total | Both the rate and count of all transactions submitted to this Horizon. | - -### Sub Metrics -Various sub metrics related to a certain metric's performance. - -| Metric | Description | -| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| `1m.rate`, `5min.rate`, `etc.` | The per-minute moving average rate of events per second at the given time interval. | -| `75%`, `95%`, `etc.` | Counts at different percentiles. | -| `count` | Sum total of a certain metric value. | -| `max`, `mean`, `etc.` | Common statistic calculations. | - - - - diff --git a/services/horizon/internal/docs/reference/endpoints/offer-details.md b/services/horizon/internal/docs/reference/endpoints/offer-details.md deleted file mode 100644 index 978d88f5f4..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/offer-details.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: Offer Details -replacement: https://developers.stellar.org/api/resources/offers/ ---- - -Returns information and links relating to a single [offer](../resources/offer.md). - -## Request - -``` -GET /offers/{offer} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `offer` | required, string | Offer ID | `126628073` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/offers/1347876" -``` - - - -## Response - -This endpoint responds with the details of a single offer for a given ID. See [offer resource](../resources/offer.md) for reference. - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/offers/1347876" - }, - "offer_maker": { - "href": "https://horizon-testnet.stellar.org/accounts/GAQHWQYBBW272OOXNQMMLCA5WY2XAZPODGB7Q3S5OKKIXVESKO55ZQ7C" - } - }, - "id": "1347876", - "paging_token": "1347876", - "seller": "GAQHWQYBBW272OOXNQMMLCA5WY2XAZPODGB7Q3S5OKKIXVESKO55ZQ7C", - "selling": { - "asset_type": "credit_alphanum4", - "asset_code": "DSQ", - "asset_issuer": "GBDQPTQJDATT7Z7EO4COS4IMYXH44RDLLI6N6WIL5BZABGMUOVMLWMQF" - }, - "buying": { - "asset_type": "credit_alphanum4", - "asset_code": "USD", - "asset_issuer": "GAA4MFNZGUPJAVLWWG6G5XZJFZDHLKQNG3Q6KB24BAD6JHNNVXDCF4XG" - }, - "amount": "60.4544008", - "price_r": { - "n": 84293, - "d": 2000000 - }, - "price": "0.0421465", - "last_modified_ledger": 1429506, - "last_modified_time": "2019-10-29T22:08:23Z" -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#standard-errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no offer whose ID matches the `offer` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/offers-for-account.md b/services/horizon/internal/docs/reference/endpoints/offers-for-account.md deleted file mode 100644 index 906e302b6a..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/offers-for-account.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: Offers for Account -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=offers&endpoint=for_account -replacement: https://developers.stellar.org/api/resources/accounts/offers/ ---- - -People on the Stellar network can make [offers](../resources/offer.md) to buy or sell assets. This -endpoint represents all the offers a particular account makes. - -This endpoint can also be used in [streaming](../streaming.md) mode so it is possible to use it to -listen as offers are processed in the Stellar network. If called in streaming mode Horizon will -start at the earliest known offer unless a `cursor` is set. In that case it will start from the -`cursor`. You can also set `cursor` value to `now` to only stream offers created since your request -time. - -## Request - -``` -GET /accounts/{account}/offers{?cursor,limit,order} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `account` | required, string | Account ID | `GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF` | -| `?cursor` | optional, any, default _null_ | A paging token, specifying where to start returning records from. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default: `10` | Maximum number of records to return. | `200` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/accounts/GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF/offers" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.offers('accounts', 'GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF') - .call() - .then(function (offerResult) { - console.log(offerResult); - }) - .catch(function (err) { - console.error(err); - }) -``` - -### JavaScript Streaming Example - -```javascript -var StellarSdk = require('stellar-sdk') -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var offerHandler = function (offerResponse) { - console.log(offerResponse); -}; - -var es = server.offers('accounts', 'GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF') - .cursor('now') - .stream({ - onmessage: offerHandler - }) -``` - -## Response - -The list of offers. - -**Note:** a response of 200 with an empty records array may either mean there are no offers for -`account_id` or `account_id` does not exist. - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/accounts/GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF/offers?cursor=&limit=10&order=asc" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/accounts/GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF/offers?cursor=5443256&limit=10&order=asc" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/accounts/GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF/offers?cursor=5443256&limit=10&order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/offers/5443256" - }, - "offer_maker": { - "href": "https://horizon-testnet.stellar.org/accounts/GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF" - } - }, - "id": "5443256", - "paging_token": "5443256", - "seller": "GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF", - "selling": { - "asset_type": "native" - }, - "buying": { - "asset_type": "credit_alphanum4", - "asset_code": "FOO", - "asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR" - }, - "amount": "10.0000000", - "price_r": { - "n": 1, - "d": 1 - }, - "price": "1.0000000", - "last_modified_ledger": 694974, - "last_modified_time": "2019-04-09T17:14:22Z" - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#standard-errors). diff --git a/services/horizon/internal/docs/reference/endpoints/offers.md b/services/horizon/internal/docs/reference/endpoints/offers.md deleted file mode 100644 index 6f8273487e..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/offers.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: Offers -replacement: https://developers.stellar.org/api/resources/offers/list/ ---- - -People on the Stellar network can make [offers](../resources/offer.md) to buy or sell assets. This -endpoint represents all the current offers, allowing filtering by `seller`, `selling_asset` or `buying_asset`. - -## Request - -``` -GET /offers{?selling_asset_type,selling_asset_issuer,selling_asset_code,buying_asset_type,buying_asset_issuer,buying_asset_code,seller,cursor,limit,order} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `?seller` | optional, string | Account ID of the offer creator | `GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36` | -| `?selling` | optional, string | Asset being sold | `native` or `EUR:GD6VWBXI6NY3AOOR55RLVQ4MNIDSXE5JSAVXUTF35FRRI72LYPI3WL6Z` | -| `?buying` | optional, string | Asset being bought | `native` or `USD:GD6VWBXI6NY3AOOR55RLVQ4MNIDSXE5JSAVXUTF35FRRI72LYPI3WL6Z` | -| `?cursor` | optional, any, default _null_ | A paging token, specifying where to start returning records from. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default: `10` | Maximum number of records to return. | `200` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/offers{?selling_asset_type,selling_asset_issuer,selling_asset_code,buying_asset_type,buying_asset_issuer,buying_asset_code,seller,cursor,limit,order}" -``` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -## Response - -The list of offers. - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/offers?cursor=&limit=10&order=asc" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/offers?cursor=5443256&limit=10&order=asc" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/offers?cursor=5443256&limit=10&order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/offers/5443256" - }, - "offer_maker": { - "href": "https://horizon-testnet.stellar.org/" - } - }, - "id": "5443256", - "paging_token": "5443256", - "seller": "GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF", - "selling": { - "asset_type": "native" - }, - "buying": { - "asset_type": "credit_alphanum4", - "asset_code": "FOO", - "asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR" - }, - "amount": "10.0000000", - "price_r": { - "n": 1, - "d": 1 - }, - "price": "1.0000000", - "last_modified_ledger": 694974, - "last_modified_time": "2019-04-09T17:14:22Z" - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#standard-errors). diff --git a/services/horizon/internal/docs/reference/endpoints/operations-all.md b/services/horizon/internal/docs/reference/endpoints/operations-all.md deleted file mode 100644 index c44f9f4fc6..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/operations-all.md +++ /dev/null @@ -1,192 +0,0 @@ ---- -title: All Operations -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=operations&endpoint=all -replacement: https://developers.stellar.org/api/resources/operations/ ---- - -This endpoint represents [operations](../resources/operation.md) that are part of successfully validated [transactions](../resources/transaction.md). -Please note that this endpoint returns operations that are part of failed transactions if `include_failed` parameter is `true` -and Horizon is ingesting failed transactions. -This endpoint can also be used in [streaming](../streaming.md) mode so it is possible to use it to listen as operations are processed in the Stellar network. -If called in streaming mode Horizon will start at the earliest known operation unless a `cursor` is set. In that case it will start from the `cursor`. You can also set `cursor` value to `now` to only stream operations created since your request time. - -## Request - -``` -GET /operations{?cursor,limit,order,include_failed} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `?cursor` | optional, any, default _null_ | A paging token, specifying where to start returning records from. When streaming this can be set to `now` to stream object created since your request time. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default: `10` | Maximum number of records to return. | `200` | -| `?include_failed` | optional, bool, default: `false` | Set to `true` to include operations of failed transactions in results. | `true` | -| `?join` | optional, string, default: _null_ | Set to `transactions` to include the transactions which created each of the operations in the response. | `transactions` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/operations?limit=200&order=desc" -``` - -### JavaScript Example Request - -```js -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.operations() - .call() - .then(function (operationsResult) { - //page 1 - console.log(operationsResult.records) - return operationsResult.next() - }) - .then(function (operationsResult) { - //page 2 - console.log(operationsResult.records) - }) - .catch(function (err) { - console.log(err) - }) -``` - -### JavaScript Streaming Example - -```javascript -var StellarSdk = require('stellar-sdk') -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var operationHandler = function (operationResponse) { - console.log(operationResponse); -}; - -var es = server.operations() - .cursor('now') - .stream({ - onmessage: operationHandler - }) -``` - -## Response - -This endpoint responds with a list of operations. See [operation resource](../resources/operation.md) for reference. - -### Example Response - -```json -{ - "_embedded": { - "records": [ - { - "_links": { - "effects": { - "href": "/operations/77309415424/effects/{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=77309415424&order=asc" - }, - "self": { - "href": "/operations/77309415424" - }, - "succeeds": { - "href": "/operations?cursor=77309415424&order=desc" - }, - "transactions": { - "href": "/transactions/77309415424" - } - }, - "account": "GBIA4FH6TV64KSPDAJCNUQSM7PFL4ILGUVJDPCLUOPJ7ONMKBBVUQHRO", - "funder": "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ", - "id": 77309415424, - "paging_token": "77309415424", - "starting_balance": "1000.0000000", - "transaction_successful": true, - "type_i": 0, - "type": "create_account" - }, - { - "_links": { - "effects": { - "href": "/operations/463856472064/effects/{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=463856472064&order=asc" - }, - "self": { - "href": "/operations/463856472064" - }, - "succeeds": { - "href": "/operations?cursor=463856472064&order=desc" - }, - "transactions": { - "href": "/transactions/463856472064" - } - }, - "account": "GC2ADYAIPKYQRGGUFYBV2ODJ54PY6VZUPKNCWWNX2C7FCJYKU4ZZNKVL", - "funder": "GBIA4FH6TV64KSPDAJCNUQSM7PFL4ILGUVJDPCLUOPJ7ONMKBBVUQHRO", - "id": 463856472064, - "paging_token": "463856472064", - "starting_balance": "1000.0000000", - "transaction_successful": true, - "type_i": 0, - "type": "create_account" - } - ] - }, - "_links": { - "next": { - "href": "/operations?order=asc&limit=2&cursor=463856472064" - }, - "prev": { - "href": "/operations?order=desc&limit=2&cursor=77309415424" - }, - "self": { - "href": "/operations?order=asc&limit=2&cursor=" - } - } -} -``` - -### Example Streaming Event - -```json -{ - "_links": { - "effects": { - "href": "/operations/77309415424/effects/{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=77309415424&order=asc" - }, - "self": { - "href": "/operations/77309415424" - }, - "succeeds": { - "href": "/operations?cursor=77309415424&order=desc" - }, - "transactions": { - "href": "/transactions/77309415424" - } - }, - "account": "GBIA4FH6TV64KSPDAJCNUQSM7PFL4ILGUVJDPCLUOPJ7ONMKBBVUQHRO", - "funder": "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ", - "id": 77309415424, - "paging_token": "77309415424", - "starting_balance": "1000.0000000", - "transaction_successful": true, - "type_i": 0, - "type": "create_account" -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#standard-errors). diff --git a/services/horizon/internal/docs/reference/endpoints/operations-for-account.md b/services/horizon/internal/docs/reference/endpoints/operations-for-account.md deleted file mode 100644 index 0b44988134..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/operations-for-account.md +++ /dev/null @@ -1,162 +0,0 @@ ---- -title: Operations for Account -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=operations&endpoint=for_account -replacement: https://developers.stellar.org/api/resources/accounts/offers/ ---- - -This endpoint represents successful [operations](../resources/operation.md) that were included in valid [transactions](../resources/transaction.md) that affected a particular [account](../resources/account.md). - -This endpoint can also be used in [streaming](../streaming.md) mode so it is possible to use it to listen for new operations that affect a given account as they happen. -If called in streaming mode Horizon will start at the earliest known operation unless a `cursor` is set. In that case it will start from the `cursor`. You can also set `cursor` value to `now` to only stream operations created since your request time. - -## Request - -``` -GET /accounts/{account}/operations{?cursor,limit,order,include_failed} -``` - -### Arguments - -| name | notes | description | example | -| ------ | ------- | ----------- | ------- | -| `account`| required, string | Account ID | `GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36`| -| `?cursor`| optional, default _null_ | A paging token, specifying where to start returning records from. When streaming this can be set to `now` to stream object created since your request time. | `12884905984` | -| `?order` | optional, string, default `asc`| The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default `10` | Maximum number of records to return. | `200` -| `?include_failed` | optional, bool, default: `false` | Set to `true` to include operations of failed transactions in results. | `true` | | -| `?join` | optional, string, default: _null_ | Set to `transactions` to include the transactions which created each of the operations in the response. | `transactions` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/accounts/GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36/operations" -``` - -### JavaScript Example Request - -```js -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.operations() - .forAccount("GAKLBGHNHFQ3BMUYG5KU4BEWO6EYQHZHAXEWC33W34PH2RBHZDSQBD75") - .call() - .then(function (operationsResult) { - console.log(operationsResult.records) - }) - .catch(function (err) { - console.log(err) - }) -``` - -### JavaScript Streaming Example - -```javascript -var StellarSdk = require('stellar-sdk') -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var operationHandler = function (operationResponse) { - console.log(operationResponse); -}; - -var es = server.operations() - .forAccount("GAKLBGHNHFQ3BMUYG5KU4BEWO6EYQHZHAXEWC33W34PH2RBHZDSQBD75") - .cursor('now') - .stream({ - onmessage: operationHandler - }) -``` - -## Response - -This endpoint responds with a list of operations that affected the given account. See [operation resource](../resources/operation.md) for reference. - -### Example Response - -```json -{ - "_embedded": { - "records": [ - { - "_links": { - "effects": { - "href": "/operations/46316927324160/effects/{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=46316927324160&order=asc" - }, - "self": { - "href": "/operations/46316927324160" - }, - "succeeds": { - "href": "/operations?cursor=46316927324160&order=desc" - }, - "transactions": { - "href": "/transactions/46316927324160" - } - }, - "account": "GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB", - "funder": "GBIA4FH6TV64KSPDAJCNUQSM7PFL4ILGUVJDPCLUOPJ7ONMKBBVUQHRO", - "id": 46316927324160, - "paging_token": "46316927324160", - "starting_balance": 1e+09, - "transaction_successful": true, - "type_i": 0, - "type": "create_account" - } - ] - }, - "_links": { - "next": { - "href": "/accounts/GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB/operations?order=asc&limit=10&cursor=46316927324160" - }, - "prev": { - "href": "/accounts/GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB/operations?order=desc&limit=10&cursor=46316927324160" - }, - "self": { - "href": "/accounts/GBBM6BKZPEHWYO3E3YKREDPQXMS4VK35YLNU7NFBRI26RAN7GI5POFBB/operations?order=asc&limit=10&cursor=" - } - } -} -``` - -### Example Streaming Event - -```json -{ - "_links": { - "effects": { - "href": "/operations/77309415424/effects/{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=77309415424&order=asc" - }, - "self": { - "href": "/operations/77309415424" - }, - "succeeds": { - "href": "/operations?cursor=77309415424&order=desc" - }, - "transactions": { - "href": "/transactions/77309415424" - } - }, - "account": "GBIA4FH6TV64KSPDAJCNUQSM7PFL4ILGUVJDPCLUOPJ7ONMKBBVUQHRO", - "funder": "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ", - "id": 77309415424, - "paging_token": "77309415424", - "starting_balance": "1000.0000000", - "transaction_successful": true, - "type_i": 0, - "type": "create_account" -} -``` - - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no account whose ID matches the `account` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/operations-for-ledger.md b/services/horizon/internal/docs/reference/endpoints/operations-for-ledger.md deleted file mode 100644 index 85155a5726..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/operations-for-ledger.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: Operations for Ledger -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=operations&endpoint=for_ledger -replacement: https://developers.stellar.org/api/resources/ledgers/operations/ ---- - -This endpoint returns successful [operations](../resources/operation.md) that occurred in a given [ledger](../resources/ledger.md). - -## Request - -``` -GET /ledgers/{sequence}/operations{?cursor,limit,order,include_failed} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `sequence` | required, number | Ledger Sequence | `681637` | -| `?cursor` | optional, default _null_ | A paging token, specifying where to start returning records from. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default `10` | Maximum number of records to return. | `200` | -| `?include_failed` | optional, bool, default: `false` | Set to `true` to include operations of failed transactions in results. | `true` | -| `?join` | optional, string, default: _null_ | Set to `transactions` to include the transactions which created each of the operations in the response. | `transactions` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/ledgers/681637/operations?limit=1" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.operations() - .forLedger("681637") - .call() - .then(function (operationsResult) { - console.log(operationsResult.records); - }) - .catch(function (err) { - console.log(err) - }) -``` - -## Response - -This endpoint responds with a list of operations in a given ledger. See [operation resource](../resources/operation.md) for reference. - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/ledgers/681637/operations?cursor=&limit=10&order=asc" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/ledgers/681637/operations?cursor=2927608622751745&limit=10&order=asc" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/ledgers/681637/operations?cursor=2927608622747649&limit=10&order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/operations/2927608622747649" - }, - "transaction": { - "href": "https://horizon-testnet.stellar.org/transactions/4a3365180521e16b478d9f0c9198b97a9434fc9cb07b34f83ecc32fc54d0ca8a" - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/operations/2927608622747649/effects" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=2927608622747649" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=2927608622747649" - } - }, - "id": "2927608622747649", - "paging_token": "2927608622747649", - "transaction_successful": true, - "source_account": "GCGXZPH2QNKJP4GI2J77EFQQUMP3NYY4PCUZ4UPKHR2XYBKRUYKQ2DS6", - "type": "payment", - "type_i": 1, - "created_at": "2019-04-08T21:59:27Z", - "transaction_hash": "4a3365180521e16b478d9f0c9198b97a9434fc9cb07b34f83ecc32fc54d0ca8a", - "asset_type": "native", - "from": "GCGXZPH2QNKJP4GI2J77EFQQUMP3NYY4PCUZ4UPKHR2XYBKRUYKQ2DS6", - "to": "GDGEQS64ISS6Y2KDM5V67B6LXALJX4E7VE4MIA54NANSUX5MKGKBZM5G", - "amount": "404.0000000" - }, - { - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/operations/2927608622751745" - }, - "transaction": { - "href": "https://horizon-testnet.stellar.org/transactions/fdabcee816bd439dd1d20bcb0abab5aa939c15cca5fccc1db060ba6096a5e0ed" - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/operations/2927608622751745/effects" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=2927608622751745" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=2927608622751745" - } - }, - "id": "2927608622751745", - "paging_token": "2927608622751745", - "transaction_successful": true, - "source_account": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", - "type": "create_account", - "type_i": 0, - "created_at": "2019-04-08T21:59:27Z", - "transaction_hash": "fdabcee816bd439dd1d20bcb0abab5aa939c15cca5fccc1db060ba6096a5e0ed", - "starting_balance": "10000.0000000", - "funder": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", - "account": "GCD5UL3DHC5TQRQVJKFTM66CLFTHGULOQ2HEAXNSA2JWUGBCT36BP55F" - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no ledger whose ID matches the `id` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/operations-for-transaction.md b/services/horizon/internal/docs/reference/endpoints/operations-for-transaction.md deleted file mode 100644 index 14ef13f850..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/operations-for-transaction.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: Operations for Transaction -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=operations&endpoint=for_transaction -replacement: https://developers.stellar.org/api/resources/transactions/operations/ ---- - -This endpoint represents successful [operations](../resources/operation.md) that are part of a given [transaction](../resources/transaction.md). - -### Warning - failed transactions - -The "Operations for Transaction" endpoint returns a list of operations in a successful or failed -transaction. Make sure to always check the operation status in this endpoint using -`transaction_successful` field! - -## Request - -``` -GET /transactions/{hash}/operations{?cursor,limit,order} -``` - -## Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `hash` | required, string | A transaction hash, hex-encoded, lowercase. | `4a3365180521e16b478d9f0c9198b97a9434fc9cb07b34f83ecc32fc54d0ca8a` | -| `?cursor` | optional, default _null_ | A paging token, specifying where to start returning records from. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default `10` | Maximum number of records to return. | `200` | -| `?join` | optional, string, default: _null_ | Set to `transactions` to include the transactions which created each of the operations in the response. | `transactions` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/transactions/4a3365180521e16b478d9f0c9198b97a9434fc9cb07b34f83ecc32fc54d0ca8a/operations?limit=1" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.operations() - .forTransaction("4a3365180521e16b478d9f0c9198b97a9434fc9cb07b34f83ecc32fc54d0ca8a") - .call() - .then(function (operationsResult) { - console.log(operationsResult.records); - }) - .catch(function (err) { - console.log(err) - }) -``` - -## Response - -This endpoint responds with a list of operations that are part of a given transaction. See [operation resource](../resources/operation.md) for reference. - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/transactions/4a3365180521e16b478d9f0c9198b97a9434fc9cb07b34f83ecc32fc54d0ca8a/operations?cursor=&limit=10&order=asc" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/transactions/4a3365180521e16b478d9f0c9198b97a9434fc9cb07b34f83ecc32fc54d0ca8a/operations?cursor=2927608622747649&limit=10&order=asc" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/transactions/4a3365180521e16b478d9f0c9198b97a9434fc9cb07b34f83ecc32fc54d0ca8a/operations?cursor=2927608622747649&limit=10&order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/operations/2927608622747649" - }, - "transaction": { - "href": "https://horizon-testnet.stellar.org/transactions/4a3365180521e16b478d9f0c9198b97a9434fc9cb07b34f83ecc32fc54d0ca8a" - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/operations/2927608622747649/effects" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=2927608622747649" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=2927608622747649" - } - }, - "id": "2927608622747649", - "paging_token": "2927608622747649", - "transaction_successful": true, - "source_account": "GCGXZPH2QNKJP4GI2J77EFQQUMP3NYY4PCUZ4UPKHR2XYBKRUYKQ2DS6", - "type": "payment", - "type_i": 1, - "created_at": "2019-04-08T21:59:27Z", - "transaction_hash": "4a3365180521e16b478d9f0c9198b97a9434fc9cb07b34f83ecc32fc54d0ca8a", - "asset_type": "native", - "from": "GCGXZPH2QNKJP4GI2J77EFQQUMP3NYY4PCUZ4UPKHR2XYBKRUYKQ2DS6", - "to": "GDGEQS64ISS6Y2KDM5V67B6LXALJX4E7VE4MIA54NANSUX5MKGKBZM5G", - "amount": "404.0000000" - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no account whose ID matches the `hash` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/operations-single.md b/services/horizon/internal/docs/reference/endpoints/operations-single.md deleted file mode 100644 index 2693cd4d41..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/operations-single.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: Operation Details -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=operations&endpoint=single -replacement: https://developers.stellar.org/api/resources/operations/single/ ---- - -The operation details endpoint provides information on a single -[operation](../resources/operation.md). The operation ID provided in the `id` argument specifies -which operation to load. - -### Warning - failed transactions - -Operations can be part of successful or failed transactions (failed transactions are also included -in Stellar ledger). Always check operation status using `transaction_successful` field! - -## Request - -``` -GET /operations/{id} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `id` | required, number | An operation ID. | 2927608622747649 | -| `?join` | optional, string, default: _null_ | Set to `transactions` to include the transactions which created each of the operations in the response. | `transactions` | - -### curl Example Request - -```sh -curl https://horizon-testnet.stellar.org/operations/2927608622747649 -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.operations() - .operation('2927608622747649') - .call() - .then(function (operationsResult) { - console.log(operationsResult) - }) - .catch(function (err) { - console.log(err) - }) -``` - -## Response - -This endpoint responds with a single Operation. See [operation resource](../resources/operation.md) for reference. - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/operations/2927608622747649" - }, - "transaction": { - "href": "https://horizon-testnet.stellar.org/transactions/4a3365180521e16b478d9f0c9198b97a9434fc9cb07b34f83ecc32fc54d0ca8a" - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/operations/2927608622747649/effects" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=2927608622747649" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=2927608622747649" - } - }, - "id": "2927608622747649", - "paging_token": "2927608622747649", - "transaction_successful": true, - "source_account": "GCGXZPH2QNKJP4GI2J77EFQQUMP3NYY4PCUZ4UPKHR2XYBKRUYKQ2DS6", - "type": "payment", - "type_i": 1, - "created_at": "2019-04-08T21:59:27Z", - "transaction_hash": "4a3365180521e16b478d9f0c9198b97a9434fc9cb07b34f83ecc32fc54d0ca8a", - "asset_type": "native", - "from": "GCGXZPH2QNKJP4GI2J77EFQQUMP3NYY4PCUZ4UPKHR2XYBKRUYKQ2DS6", - "to": "GDGEQS64ISS6Y2KDM5V67B6LXALJX4E7VE4MIA54NANSUX5MKGKBZM5G", - "amount": "404.0000000" -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if the - there is no operation that matches the ID argument, i.e. the operation does not exist. diff --git a/services/horizon/internal/docs/reference/endpoints/orderbook-details.md b/services/horizon/internal/docs/reference/endpoints/orderbook-details.md deleted file mode 100644 index f408aef7df..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/orderbook-details.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -title: Orderbook Details -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=order_book&endpoint=details -replacement: https://developers.stellar.org/api/aggregations/order-books/ ---- - -People on the Stellar network can make [offers](../resources/offer.md) to buy or sell assets. -These offers are summarized by the assets being bought and sold in -[orderbooks](../resources/orderbook.md). - -Horizon will return, for each orderbook, a summary of the orderbook and the bids and asks -associated with that orderbook. - -This endpoint can also be used in [streaming](../streaming.md) mode so it is possible to use it to -listen as offers are processed in the Stellar network. If called in streaming mode Horizon will -start at the earliest known offer unless a `cursor` is set. In that case it will start from the -`cursor`. You can also set `cursor` value to `now` to only stream offers created since your request -time. - -## Request - -``` -GET /order_book?selling_asset_type={selling_asset_type}&selling_asset_code={selling_asset_code}&selling_asset_issuer={selling_asset_issuer}&buying_asset_type={buying_asset_type}&buying_asset_code={buying_asset_code}&buying_asset_issuer={buying_asset_issuer}&limit={limit} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `selling_asset_type` | required, string | Type of the Asset being sold | `native` | -| `selling_asset_code` | optional, string | Code of the Asset being sold | `USD` | -| `selling_asset_issuer` | optional, string | Account ID of the issuer of the Asset being sold | `GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36` | -| `buying_asset_type` | required, string | Type of the Asset being bought | `credit_alphanum4` | -| `buying_asset_code` | optional, string | Code of the Asset being bought | `BTC` | -| `buying_asset_issuer` | optional, string | Account ID of the issuer of the Asset being bought | `GD6VWBXI6NY3AOOR55RLVQ4MNIDSXE5JSAVXUTF35FRRI72LYPI3WL6Z` | -| `limit` | optional, string | Limit the number of items returned | `20` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/order_book?selling_asset_type=native&buying_asset_type=credit_alphanum4&buying_asset_code=FOO&buying_asset_issuer=GBAUUA74H4XOQYRSOW2RZUA4QL5PB37U3JS5NE3RTB2ELJVMIF5RLMAG&limit=20" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.orderbook(new StellarSdk.Asset.native(), new StellarSdk.Asset('FOO', 'GBAUUA74H4XOQYRSOW2RZUA4QL5PB37U3JS5NE3RTB2ELJVMIF5RLMAG')) - .call() - .then(function(resp) { - console.log(resp); - }) - .catch(function(err) { - console.log(err); - }) -``` - -### JavaScript Streaming Example - -```javascript -var StellarSdk = require('stellar-sdk') -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var orderbookHandler = function (orderbookResponse) { - console.log(orderbookResponse); -}; - -var es = server.orderbook(new StellarSdk.Asset.native(), new StellarSdk.Asset('FOO', 'GBAUUA74H4XOQYRSOW2RZUA4QL5PB37U3JS5NE3RTB2ELJVMIF5RLMAG')) - .cursor('now') - .stream({ - onmessage: orderbookHandler - }) -``` - -## Response - -The summary of the orderbook and its bids and asks. - -## Example Response -```json -{ - "bids": [ - { - "price_r": { - "n": 100000000, - "d": 12953367 - }, - "price": "7.7200005", - "amount": "12.0000000" - } - ], - "asks": [ - { - "price_r": { - "n": 194, - "d": 25 - }, - "price": "7.7600000", - "amount": "238.4804125" - } - ], - "base": { - "asset_type": "native" - }, - "counter": { - "asset_type": "credit_alphanum4", - "asset_code": "FOO", - "asset_issuer": "GBAUUA74H4XOQYRSOW2RZUA4QL5PB37U3JS5NE3RTB2ELJVMIF5RLMAG" - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#standard-errors). diff --git a/services/horizon/internal/docs/reference/endpoints/path-finding-strict-receive.md b/services/horizon/internal/docs/reference/endpoints/path-finding-strict-receive.md deleted file mode 100644 index 8e8444878d..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/path-finding-strict-receive.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: Strict Receive Payment Paths -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=paths&endpoint=all -replacement: https://developers.stellar.org/api/aggregations/paths/strict-receive/ ---- - -The Stellar Network allows payments to be made across assets through _path payments_. A path -payment specifies a series of assets to route a payment through, from source asset (the asset -debited from the payer) to destination asset (the asset credited to the payee). - -A [Path Payment Strict Receive](../../../guides/concepts/list-of-operations.html#path-payment-strict-receive) allows a user to specify the *amount of the asset received*. The amount sent varies based on offers in the order books. If you would like to search for a path specifying the amount to be sent, use the [Find Payment Paths (Strict Send)](./path-finding-strict-send.html). - -A strict receive path search is specified using: - -- The source account id or source assets. -- The asset and amount that the destination account should receive. - -As part of the search, horizon will load a list of assets available to the source account id and -will find any payment paths from those source assets to the desired destination asset. The search's -amount parameter will be used to determine if a given path can satisfy a payment of the -desired amount. - -## Request - -``` -GET /paths/strict-receive?source_account={sa}&destination_asset_type={at}&destination_asset_code={ac}&destination_asset_issuer={di}&destination_amount={amount}&destination_account={da} -``` - -## Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `?source_account` | string | The sender's account id. Any returned path must use an asset that the sender has a trustline to. | `GARSFJNXJIHO6ULUBK3DBYKVSIZE7SC72S5DYBCHU7DKL22UXKVD7MXP` | -| `?source_assets` | string | A comma separated list of assets. Any returned path must use an asset included in this list | `USD:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V,native` | -| `?destination_account` | string | The destination account that any returned path should use | `GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V` | -| `?destination_asset_type` | string | The type of the destination asset | `credit_alphanum4` | -| `?destination_asset_code` | required if `destination_asset_type` is not `native`, string | The destination asset code, if destination_asset_type is not "native" | `USD` | -| `?destination_asset_issuer` | required if `destination_asset_type` is not `native`, string | The issuer for the destination asset, if destination_asset_type is not "native" | `GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V` | -| `?destination_amount` | string | The amount, denominated in the destination asset, that any returned path should be able to satisfy | `10.1` | - -The endpoint will not allow requests which provide both a `source_account` and a `source_assets` parameter. All requests must provide one or the other. -The assets in `source_assets` are expected to be encoded using the following format: - -XLM should be represented as `"native"`. Issued assets should be represented as `"Code:IssuerAccountID"`. `"Code"` must consist of alphanumeric ASCII characters. - - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/paths/strict-receive?destination_account=GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V&source_account=GARSFJNXJIHO6ULUBK3DBYKVSIZE7SC72S5DYBCHU7DKL22UXKVD7MXP&destination_asset_type=native&destination_amount=20" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var sourceAccount = "GARSFJNXJIHO6ULUBK3DBYKVSIZE7SC72S5DYBCHU7DKL22UXKVD7MXP"; -var destinationAsset = StellarSdk.Asset.native(); -var destinationAmount = "20"; - -server.paths(sourceAccount, destinationAsset, destinationAmount) - .call() - .then(function (pathResult) { - console.log(pathResult.records); - }) - .catch(function (err) { - console.log(err) - }) -``` - -## Response - -This endpoint responds with a page of path resources. See [path resource](../resources/path.md) for reference. - -### Example Response - -```json -{ - "_embedded": { - "records": [ - { - "source_asset_type": "credit_alphanum4", - "source_asset_code": "FOO", - "source_asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR", - "source_amount": "100.0000000", - "destination_asset_type": "credit_alphanum4", - "destination_asset_code": "FOO", - "destination_asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR", - "destination_amount": "100.0000000", - "path": [] - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if no paths could be found to fulfill this payment request diff --git a/services/horizon/internal/docs/reference/endpoints/path-finding-strict-send.md b/services/horizon/internal/docs/reference/endpoints/path-finding-strict-send.md deleted file mode 100644 index ff2ad9d444..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/path-finding-strict-send.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: Strict Send Payment Paths -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=paths&endpoint=all -replacement: https://developers.stellar.org/api/aggregations/paths/strict-send/ ---- - -The Stellar Network allows payments to be made across assets through _path payments_. A path -payment specifies a series of assets to route a payment through, from source asset (the asset -debited from the payer) to destination asset (the asset credited to the payee). - -A [Path Payment Strict Send](../../../guides/concepts/list-of-operations.html#path-payment-strict-send) allows a user to specify the amount of the asset to send. The amount received will vary based on offers in the order books. - - -A path payment strict send search is specified using: - -- The destination account id or destination assets. -- The source asset. -- The source amount. - -As part of the search, horizon will load a list of assets available to the source account id or use the assets passed in the request and will find any payment paths from those source assets to the desired destination asset. The source's amount parameter will be used to determine if a given path can satisfy a payment of the desired amount. - -## Request - -``` -https://horizon-testnet.stellar.org/paths/strict-send?&source_amount={sa}&source_asset_type={at}&source_asset_code={ac}&source_asset_issuer={ai}&destination_account={da} -``` - -## Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `?source_amount` | string | The amount, denominated in the source asset, that any returned path should be able to satisfy | `10.1` | -| `?source_asset_type` | string | The type of the source asset | `credit_alphanum4` | -| `?source_asset_code` | string, required if `source_asset_type` is not `native`, string | The source asset code, if source_asset_type is not "native" | `USD` | -| `?source_asset_issuer` | string, required if `source_asset_type` is not `native`, string | The issuer for the source asset, if source_asset_type is not "native" | `GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V` | -| `?destination_account` | string optional | The destination account that any returned path should use | `GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V` | -| `?destination_assets` | string optional | A comma separated list of assets. Any returned path must use an asset included in this list | `USD:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V,native` | - -The endpoint will not allow requests which provide both a `destination_account` and `destination_assets` parameter. All requests must provide one or the other. -The assets in `destination_assets` are expected to be encoded using the following format: - -XLM should be represented as `"native"`. Issued assets should be represented as `"Code:IssuerAccountID"`. `"Code"` must consist of alphanumeric ASCII characters. - - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/paths/strict-send?&source_amount=10&source_asset_type=native&destination_assets=MXN:GC2GFGZ5CZCFCDJSQF3YYEAYBOS3ZREXJSPU7LUJ7JU3LP3BQNHY7YKS" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var sourceAsset = StellarSdk.Asset.native(); -var sourceAmount = "20"; -var destinationAsset = new StellarSdk.Asset( - 'USD', - 'GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN' -) - - -server.strictSendPaths(sourceAsset, sourceAmount, [destinationAsset]) - .call() - .then(function (pathResult) { - console.log(pathResult.records); - }) - .catch(function (err) { - console.log(err) - }) -``` - -## Response - -This endpoint responds with a page of path resources. See [path resource](../resources/path.md) for reference. - -### Example Response - -```json -{ - "_embedded": { - "records": [ - { - "source_asset_type": "credit_alphanum4", - "source_asset_code": "FOO", - "source_asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR", - "source_amount": "100.0000000", - "destination_asset_type": "credit_alphanum4", - "destination_asset_code": "FOO", - "destination_asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR", - "destination_amount": "100.0000000", - "path": [] - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if no paths could be found to fulfill this payment request diff --git a/services/horizon/internal/docs/reference/endpoints/path-finding.md b/services/horizon/internal/docs/reference/endpoints/path-finding.md deleted file mode 100644 index 356ec40f0f..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/path-finding.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: Find Payment Paths -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=paths&endpoint=all -replacement: https://developers.stellar.org/api/aggregations/paths/ ---- - -**Note**: This endpoint will be deprecated, use [/path/strict-receive](./path-finding-strict-receive.md) instead. There are no differences between both endpoints, `/paths` is an alias for `/path/strict-receive`. - - -The Stellar Network allows payments to be made across assets through _path payments_. A path -payment specifies a series of assets to route a payment through, from source asset (the asset -debited from the payer) to destination asset (the asset credited to the payee). - -A path search is specified using: - -- The destination account id -- The source account id -- The asset and amount that the destination account should receive - -As part of the search, horizon will load a list of assets available to the source account id and -will find any payment paths from those source assets to the desired destination asset. The search's -amount parameter will be used to determine if there a given path can satisfy a payment of the -desired amount. - -## Request - -``` -GET /paths?destination_account={da}&source_account={sa}&destination_asset_type={at}&destination_asset_code={ac}&destination_asset_issuer={di}&destination_amount={amount} -``` - -## Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `?destination_account` | string | The destination account that any returned path should use | `GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V` | -| `?destination_asset_type` | string | The type of the destination asset | `credit_alphanum4` | -| `?destination_asset_code` | string | The destination asset code, if destination_asset_type is not "native" | `USD` | -| `?destination_asset_issuer` | string | The issuer for the destination, if destination_asset_type is not "native" | `GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V` | -| `?destination_amount` | string | The amount, denominated in the destination asset, that any returned path should be able to satisfy | `10.1` | -| `?source_account` | string | The sender's account id. Any returned path must use a source that the sender can hold | `GARSFJNXJIHO6ULUBK3DBYKVSIZE7SC72S5DYBCHU7DKL22UXKVD7MXP` | -| `?source_assets` | string | A comma separated list of assets. Any returned path must use a source included in this list | `USD:GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V,native` | - -The endpoint will not allow requests which provide both a `source_account` and a `source_assets` parameter. All requests must provide one or the other. -The assets in `source_assets` are expected to be encoded using the following format: - -The native asset should be represented as `"native"`. Issued assets should be represented as `"Code:IssuerAccountID"`. `"Code"` must consist of alphanumeric ASCII characters. - - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/paths?destination_account=GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V&source_account=GARSFJNXJIHO6ULUBK3DBYKVSIZE7SC72S5DYBCHU7DKL22UXKVD7MXP&destination_asset_type=native&destination_amount=20" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var source_account = "GARSFJNXJIHO6ULUBK3DBYKVSIZE7SC72S5DYBCHU7DKL22UXKVD7MXP"; -var destination_account = "GAEDTJ4PPEFVW5XV2S7LUXBEHNQMX5Q2GM562RJGOQG7GVCE5H3HIB4V"; -var destination_asset = StellarSdk.Asset.native(); -var destination_amount = "20"; - -server.paths(source_account, destination_account, destination_asset, destination_amount) - .call() - .then(function (pathResult) { - console.log(pathResult.records); - }) - .catch(function (err) { - console.log(err) - }) -``` - -## Response - -This endpoint responds with a page of path resources. See [path resource](../resources/path.md) for reference. - -### Example Response - -```json -{ - "_embedded": { - "records": [ - { - "source_asset_type": "credit_alphanum4", - "source_asset_code": "FOO", - "source_asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR", - "source_amount": "100.0000000", - "destination_asset_type": "credit_alphanum4", - "destination_asset_code": "FOO", - "destination_asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR", - "destination_amount": "100.0000000", - "path": [] - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if no paths could be found to fulfill this payment request diff --git a/services/horizon/internal/docs/reference/endpoints/payments-all.md b/services/horizon/internal/docs/reference/endpoints/payments-all.md deleted file mode 100644 index 4df10b3c81..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/payments-all.md +++ /dev/null @@ -1,198 +0,0 @@ ---- -title: All Payments -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=payments&endpoint=all ---- - -This endpoint represents all payment-related [operations](../resources/operation.md) that are part -of validated [transactions](../resources/transaction.md). This endpoint can also be used in -[streaming](../streaming.md) mode so it is possible to use it to listen for new payments as they -get made in the Stellar network. - -If called in streaming mode Horizon will start at the earliest known payment unless a `cursor` is -set. In that case it will start from the `cursor`. You can also set `cursor` value to `now` to only -stream payments created since your request time. - -The operations that can be returned in by this endpoint are: -- `create_account` -- `payment` -- `path_payment` -- `account_merge` - -## Request - -``` -GET /payments{?cursor,limit,order,include_failed} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `?cursor` | optional, any, default _null_ | A paging token, specifying where to start returning records from. When streaming this can be set to `now` to stream object created since your request time. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default: `10` | Maximum number of records to return. | `200` | -| `?include_failed` | optional, bool, default: `false` | Set to `true` to include payments of failed transactions in results. | `true` | -| `?join` | optional, string, default: _null_ | Set to `transactions` to include the transactions which created each of the payments in the response. | `transactions` | - -### curl Example Request - -```sh -# Retrieve the first 200 payments, ordered chronologically. -curl "https://horizon-testnet.stellar.org/payments?limit=200" -``` - -```sh -# Retrieve a page of payments to occur immediately before the transaction -# specified by the paging token "1234". -curl "https://horizon-testnet.stellar.org/payments?cursor=1234&order=desc" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.payments() - .call() - .then(function (paymentResults) { - console.log(paymentResults.records) - }) - .catch(function (err) { - console.log(err) - }) -``` - -### JavaScript Streaming Example - -```javascript -var StellarSdk = require('stellar-sdk') -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var paymentHandler = function (paymentResponse) { - console.log(paymentResponse); -}; - -var es = server.payments() - .cursor('now') - .stream({ - onmessage: paymentHandler - }) -``` - -## Response - -This endpoint responds with a list of payments. See [operation resource](../resources/operation.md) for more information about operations (and payment operations). - -### Example Response - -```json -{ - "_embedded": { - "records": [ - { - "_links": { - "effects": { - "href": "/operations/77309415424/effects/{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=77309415424&order=asc" - }, - "self": { - "href": "/operations/77309415424" - }, - "succeeds": { - "href": "/operations?cursor=77309415424&order=desc" - }, - "transactions": { - "href": "/transactions/77309415424" - } - }, - "account": "GBIA4FH6TV64KSPDAJCNUQSM7PFL4ILGUVJDPCLUOPJ7ONMKBBVUQHRO", - "funder": "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ", - "id": 77309415424, - "paging_token": "77309415424", - "starting_balance": 1e+14, - "type_i": 0, - "type": "create_account" - }, - { - "_links": { - "effects": { - "href": "/operations/463856472064/effects/{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=463856472064&order=asc" - }, - "self": { - "href": "/operations/463856472064" - }, - "succeeds": { - "href": "/operations?cursor=463856472064&order=desc" - }, - "transactions": { - "href": "/transactions/463856472064" - } - }, - "account": "GC2ADYAIPKYQRGGUFYBV2ODJ54PY6VZUPKNCWWNX2C7FCJYKU4ZZNKVL", - "funder": "GBIA4FH6TV64KSPDAJCNUQSM7PFL4ILGUVJDPCLUOPJ7ONMKBBVUQHRO", - "id": 463856472064, - "paging_token": "463856472064", - "starting_balance": 1e+09, - "type_i": 0, - "type": "create_account" - } - ] - }, - "_links": { - "next": { - "href": "?order=asc&limit=2&cursor=463856472064" - }, - "prev": { - "href": "?order=desc&limit=2&cursor=77309415424" - }, - "self": { - "href": "?order=asc&limit=2&cursor=" - } - } -} -``` - -### Example Streaming Event - -```json -{ - "_links": { - "effects": { - "href": "/operations/77309415424/effects/{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=77309415424&order=asc" - }, - "self": { - "href": "/operations/77309415424" - }, - "succeeds": { - "href": "/operations?cursor=77309415424&order=desc" - }, - "transactions": { - "href": "/transactions/77309415424" - } - }, - "account": "GBIA4FH6TV64KSPDAJCNUQSM7PFL4ILGUVJDPCLUOPJ7ONMKBBVUQHRO", - "funder": "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ", - "id": 77309415424, - "paging_token": "77309415424", - "starting_balance": 1e+14, - "type_i": 0, - "type": "create_account" -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#standard-errors). diff --git a/services/horizon/internal/docs/reference/endpoints/payments-for-account.md b/services/horizon/internal/docs/reference/endpoints/payments-for-account.md deleted file mode 100644 index 3bd6bd4bb6..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/payments-for-account.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: Payments for Account -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=payments&endpoint=for_account -replacement: https://developers.stellar.org/api/resources/accounts/payments/ ---- - -This endpoint responds with a collection of payment-related operations where the given -[account](../resources/account.md) was either the sender or receiver. - -This endpoint can also be used in [streaming](../streaming.md) mode so it is possible to use it to -listen for new payments to or from an account as they get made in the Stellar network. -If called in streaming mode Horizon will start at the earliest known payment unless a `cursor` is -set. In that case it will start from the `cursor`. You can also set `cursor` value to `now` to only -stream payments created since your request time. - -The operations that can be returned in by this endpoint are: -- `create_account` -- `payment` -- `path_payment` -- `account_merge` - -## Request - -``` -GET /accounts/{id}/payments{?cursor,limit,order} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `id` | required, string | The account id of the account used to constrain results. | `GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ` | -| `?cursor` | optional, default _null_ | A payment paging token specifying from where to begin results. When streaming this can be set to `now` to stream object created since your request time. | `8589934592` | -| `?limit` | optional, number, default `10` | Specifies the count of records at most to return. | `200` | -| `?order` | optional, string, default `asc` | Specifies order of returned results. `asc` means older payments first, `desc` mean newer payments first. | `desc` | -| `?include_failed` | optional, bool, default: `false` | Set to `true` to include payments of failed transactions in results. | `true` | -| `?join` | optional, string, default: _null_ | Set to `transactions` to include the transactions which created each of the payments in the response. | `transactions` | - -### curl Example Request - -```bash -# Retrieve the 25 latest payments for a specific account. -curl "https://horizon-testnet.stellar.org/accounts/GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ/payments?limit=25&order=desc" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.payments() - .forAccount("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ") - .call() - .then(function (accountResult) { - console.log(accountResult); - }) - .catch(function (err) { - console.error(err); - }) -``` - -### JavaScript Streaming Example - -```javascript -var StellarSdk = require('stellar-sdk') -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var paymentHandler = function (paymentResponse) { - console.log(paymentResponse); -}; - -var es = server.payments() - .forAccount("GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ") - .cursor('now') - .stream({ - onmessage: paymentHandler - }) -``` - -## Response - -This endpoint responds with a [page](../resources/page.md) of [payment operations](../resources/operation.md). - -### Example Response - -```json -{"_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "/operations/12884905984" - }, - "transaction": { - "href": "/transaction/6391dd190f15f7d1665ba53c63842e368f485651a53d8d852ed442a446d1c69a" - }, - "precedes": { - "href": "/accounts/GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ/payments?cursor=12884905984&order=asc{?limit}", - "templated": true - }, - "succeeds": { - "href": "/accounts/GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ/payments?cursor=12884905984&order=desc{?limit}", - "templated": true - } - }, - "id": 12884905984, - "paging_token": "12884905984", - "type_i": 0, - "type": "payment", - "sender": "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ", - "receiver": "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", - "asset": { - "code": "XLM" - }, - "amount": 1000000000, - "amount_f": 100.00 - } - ] -}, -"_links": { - "next": { - "href": "/accounts/GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ/payments?cursor=12884905984&order=asc{?limit}", - "templated": true - }, - "self": { - "href": "/accounts/GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ/payments" - } -} -} -``` - -### Example Streaming Event - -```json -{ - "_links": { - "effects": { - "href": "/operations/77309415424/effects/{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=77309415424&order=asc" - }, - "self": { - "href": "/operations/77309415424" - }, - "succeeds": { - "href": "/operations?cursor=77309415424&order=desc" - }, - "transactions": { - "href": "/transactions/77309415424" - } - }, - "account": "GBIA4FH6TV64KSPDAJCNUQSM7PFL4ILGUVJDPCLUOPJ7ONMKBBVUQHRO", - "funder": "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ", - "id": 77309415424, - "paging_token": "77309415424", - "starting_balance": 1e+14, - "type_i": 0, - "type": "create_account" -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#standard-errors). diff --git a/services/horizon/internal/docs/reference/endpoints/payments-for-ledger.md b/services/horizon/internal/docs/reference/endpoints/payments-for-ledger.md deleted file mode 100644 index 52cc95ffb6..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/payments-for-ledger.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: Payments for Ledger -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=payments&endpoint=for_ledger -replacement: https://developers.stellar.org/api/resources/ledgers/payments/ ---- - -This endpoint represents all payment-related [operations](../resources/operation.md) that are part -of a valid [transactions](../resources/transaction.md) in a given [ledger](../resources/ledger.md). - -The operations that can be returned in by this endpoint are: -- `create_account` -- `payment` -- `path_payment` -- `account_merge` - -## Request - -``` -GET /ledgers/{id}/payments{?cursor,limit,order,include_failed} -``` - -### Arguments - -| name | notes | description | example | -| ------ | ------- | ----------- | ------- | -| `id` | required, number | Ledger ID | `696960` | -| `?cursor` | optional, default _null_ | A paging token, specifying where to start returning records from. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default `10` | Maximum number of records to return. | `200` | -| `?include_failed` | optional, bool, default: `false` | Set to `true` to include payments of failed transactions in results. | `true` | -| `?join` | optional, string, default: _null_ | Set to `transactions` to include the transactions which created each of the payments in the response. | `transactions` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/ledgers/696960/payments?limit=1" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk') -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.payments() - .forLedger("696960") - .call() - .then(function (paymentResult) { - console.log(paymentResult) - }) - .catch(function (err) { - console.log(err) - }) -``` - -## Response - -This endpoint responds with a list of payment operations in a given ledger. See [operation -resource](../resources/operation.md) for more information about operations (and payment -operations). - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/ledgers/696960/payments?cursor=&limit=1&order=asc" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/ledgers/696960/payments?cursor=2993420406628353&limit=1&order=asc" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/ledgers/696960/payments?cursor=2993420406628353&limit=1&order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/operations/2993420406628353" - }, - "transaction": { - "href": "https://horizon-testnet.stellar.org/transactions/f65278b36875d170e865853838da400515f59ca23836f072e8d62cac18b803e5" - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/operations/2993420406628353/effects" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=2993420406628353" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=2993420406628353" - } - }, - "id": "2993420406628353", - "paging_token": "2993420406628353", - "transaction_successful": true, - "source_account": "GAYB4GWPX2HUWR5QE7YX77QY6TSNFZIJZTYX2TDRW6YX6332BGD5SEAK", - "type": "payment", - "type_i": 1, - "created_at": "2019-04-09T20:00:54Z", - "transaction_hash": "f65278b36875d170e865853838da400515f59ca23836f072e8d62cac18b803e5", - "asset_type": "native", - "from": "GAYB4GWPX2HUWR5QE7YX77QY6TSNFZIJZTYX2TDRW6YX6332BGD5SEAK", - "to": "GDGEQS64ISS6Y2KDM5V67B6LXALJX4E7VE4MIA54NANSUX5MKGKBZM5G", - "amount": "293.0000000" - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no ledger whose ID matches the `id` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/payments-for-transaction.md b/services/horizon/internal/docs/reference/endpoints/payments-for-transaction.md deleted file mode 100644 index 29b557eb4c..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/payments-for-transaction.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: Payments for Transaction -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=payments&endpoint=for_transaction ---- - -This endpoint represents all payment-related [operations](../resources/operation.md) that are part -of a given [transaction](../resources/transaction.md). - -The operations that can be returned in by this endpoint are: -- `create_account` -- `payment` -- `path_payment` -- `account_merge` - -### Warning - failed transactions - -"Payments for Transaction" endpoint returns list of payments of successful or failed transactions -(that are also included in Stellar ledger). Always check the payment status in this endpoint using -`transaction_successful` field! - -## Request - -``` -GET /transactions/{hash}/payments{?cursor,limit,order} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `hash` | required, string | A transaction hash, hex-encoded, lowercase. | `f65278b36875d170e865853838da400515f59ca23836f072e8d62cac18b803e5` | -| `?cursor` | optional, default _null_ | A paging token, specifying where to start returning records from. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default `10` | Maximum number of records to return. | `200` | -| `?join` | optional, string, default: _null_ | Set to `transactions` to include the transactions which created each of the payments in the response. | `transactions` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/transactions/f65278b36875d170e865853838da400515f59ca23836f072e8d62cac18b803e5/payments" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.payments() - .forTransaction("f65278b36875d170e865853838da400515f59ca23836f072e8d62cac18b803e5") - .call() - .then(function (paymentResult) { - console.log(paymentResult.records); - }) - .catch(function (err) { - console.log(err); - }) -``` - -## Response - -This endpoint responds with a list of payments operations that are part of a given transaction. See -[operation resource](../resources/operation.md) for more information about operations (and payment -operations). - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/transactions/f65278b36875d170e865853838da400515f59ca23836f072e8d62cac18b803e5/payments?cursor=&limit=10&order=asc" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/transactions/f65278b36875d170e865853838da400515f59ca23836f072e8d62cac18b803e5/payments?cursor=2993420406628353&limit=10&order=asc" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/transactions/f65278b36875d170e865853838da400515f59ca23836f072e8d62cac18b803e5/payments?cursor=2993420406628353&limit=10&order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/operations/2993420406628353" - }, - "transaction": { - "href": "https://horizon-testnet.stellar.org/transactions/f65278b36875d170e865853838da400515f59ca23836f072e8d62cac18b803e5" - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/operations/2993420406628353/effects" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=2993420406628353" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=2993420406628353" - } - }, - "id": "2993420406628353", - "paging_token": "2993420406628353", - "transaction_successful": true, - "source_account": "GAYB4GWPX2HUWR5QE7YX77QY6TSNFZIJZTYX2TDRW6YX6332BGD5SEAK", - "type": "payment", - "type_i": 1, - "created_at": "2019-04-09T20:00:54Z", - "transaction_hash": "f65278b36875d170e865853838da400515f59ca23836f072e8d62cac18b803e5", - "asset_type": "native", - "from": "GAYB4GWPX2HUWR5QE7YX77QY6TSNFZIJZTYX2TDRW6YX6332BGD5SEAK", - "to": "GDGEQS64ISS6Y2KDM5V67B6LXALJX4E7VE4MIA54NANSUX5MKGKBZM5G", - "amount": "293.0000000" - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no - transaction whose ID matches the `hash` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/trade_aggregations.md b/services/horizon/internal/docs/reference/endpoints/trade_aggregations.md deleted file mode 100644 index 47a4c3fef8..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/trade_aggregations.md +++ /dev/null @@ -1,155 +0,0 @@ ---- -title: Trade Aggregations -replacement: https://developers.stellar.org/api/aggregations/trade-aggregations/ ---- - -Trade Aggregations are catered specifically for developers of trading clients. They facilitate -efficient gathering of historical trade data. This is done by dividing a given time range into -segments and aggregating statistics, for a given asset pair (`base`, `counter`) over each of these -segments. - -The duration of the segments is specified with the `resolution` parameter. The start and end of the -time range are given by `startTime` and `endTime` respectively, which are both rounded to the -nearest multiple of `resolution` since epoch. - -The individual segments are also aligned with multiples of `resolution` since epoch. If you want to -change this alignment, the segments can be offset by specifying the `offset` parameter. - - -## Request - -``` -GET /trade_aggregations?base_asset_type={base_asset_type}&base_asset_code={base_asset_code}&base_asset_issuer={base_asset_issuer}&counter_asset_type={counter_asset_type}&counter_asset_code={counter_asset_code}&counter_asset_issuer={counter_asset_issuer} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `start_time` | long | lower time boundary represented as millis since epoch | 1512689100000 | -| `end_time` | long | upper time boundary represented as millis since epoch | 1512775500000 | -| `resolution` | long | segment duration as millis. *Supported values are 1 minute (60000), 5 minutes (300000), 15 minutes (900000), 1 hour (3600000), 1 day (86400000) and 1 week (604800000).* | 300000 | -| `offset` | long | segments can be offset using this parameter. Expressed in milliseconds. Can only be used if the resolution is greater than 1 hour. *Value must be in whole hours, less than the provided resolution, and less than 24 hours.* | 3600000 (1 hour) | -| `base_asset_type` | string | Type of base asset | `native` | -| `base_asset_code` | string | Code of base asset, not required if type is `native` | `USD` | -| `base_asset_issuer` | string | Issuer of base asset, not required if type is `native` | 'GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36' | -| `counter_asset_type` | string | Type of counter asset | `credit_alphanum4` | -| `counter_asset_code` | string | Code of counter asset, not required if type is `native` | `BTC` | -| `counter_asset_issuer` | string | Issuer of counter asset, not required if type is `native` | 'GATEMHCCKCY67ZUCKTROYN24ZYT5GK4EQZ65JJLDHKHRUZI3EUEKMTCH' | -| `?order` | optional, string, default `asc` | The order, in terms of timeline, in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default: `10` | Maximum number of records to return. | `200` | - -### curl Example Request -```sh -curl https://horizon.stellar.org/trade_aggregations?base_asset_type=native&counter_asset_code=SLT&counter_asset_issuer=GCKA6K5PCQ6PNF5RQBF7PQDJWRHO6UOGFMRLK3DYHDOI244V47XKQ4GP&counter_asset_type=credit_alphanum4&limit=200&order=asc&resolution=3600000&start_time=1517521726000&end_time=1517532526000 -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon.stellar.org'); - -var base = new StellarSdk.Asset.native(); -var counter = new StellarSdk.Asset("SLT", "GCKA6K5PCQ6PNF5RQBF7PQDJWRHO6UOGFMRLK3DYHDOI244V47XKQ4GP"); -var startTime = 1517521726000; -var endTime = 1517532526000; -var resolution = 3600000; -var offset = 0; - -server.tradeAggregation(base, counter, startTime, endTime, resolution, offset) - .call() - .then(function (tradeAggregation) { - console.log(tradeAggregation); - }) - .catch(function (err) { - console.log(err); - }) -``` - -## Response - -A list of collected trade aggregations. - -Note -- Segments that fit into the time range but have 0 trades in them, will not be included. -- Partial segments, in the beginning and end of the time range, will not be included. Thus if your - start time is noon Wednesday, your end time is noon Thursday, and your resolution is one day, you - will not receive back any data. Instead, you would want to either start at midnight Wednesday and - midnight Thursday, or shorten the resolution interval to better cover your time frame. - -### Example Response -```json -{ - "_links": { - "self": { - "href": "https://horizon.stellar.org/trade_aggregations?base_asset_type=native\u0026counter_asset_code=SLT\u0026counter_asset_issuer=GCKA6K5PCQ6PNF5RQBF7PQDJWRHO6UOGFMRLK3DYHDOI244V47XKQ4GP\u0026counter_asset_type=credit_alphanum4\u0026limit=200\u0026order=asc\u0026resolution=3600000\u0026start_time=1517521726000\u0026end_time=1517532526000" - }, - "next": { - "href": "https://horizon.stellar.org/trade_aggregations?base_asset_type=native\u0026counter_asset_code=SLT\u0026counter_asset_issuer=GCKA6K5PCQ6PNF5RQBF7PQDJWRHO6UOGFMRLK3DYHDOI244V47XKQ4GP\u0026counter_asset_type=credit_alphanum4\u0026end_time=1517532526000\u0026limit=200\u0026order=asc\u0026resolution=3600000\u0026start_time=1517529600000" - } - }, - "_embedded": { - "records": [ - { - "timestamp": 1517522400000, - "trade_count": 26, - "base_volume": "27575.0201596", - "counter_volume": "5085.6410385", - "avg": "0.1844293", - "high": "0.1915709", - "high_r": { - "N": 50, - "D": 261 - }, - "low": "0.1506024", - "low_r": { - "N": 25, - "D": 166 - }, - "open": "0.1724138", - "open_r": { - "N": 5, - "D": 29 - }, - "close": "0.1506024", - "close_r": { - "N": 25, - "D": 166 - } - }, - { - "timestamp": 1517526000000, - "trade_count": 15, - "base_volume": "3913.8224543", - "counter_volume": "719.4993608", - "avg": "0.1838355", - "high": "0.1960784", - "high_r": { - "N": 10, - "D": 51 - }, - "low": "0.1506024", - "low_r": { - "N": 25, - "D": 166 - }, - "open": "0.1869159", - "open_r": { - "N": 20, - "D": 107 - }, - "close": "0.1515152", - "close_r": { - "N": 5, - "D": 33 - } - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#standard-errors). diff --git a/services/horizon/internal/docs/reference/endpoints/trades-for-account.md b/services/horizon/internal/docs/reference/endpoints/trades-for-account.md deleted file mode 100644 index 1290ac68ab..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/trades-for-account.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -title: Trades for Account -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=trades&endpoint=for_account -replacement: https://developers.stellar.org/api/resources/accounts/trades/ ---- - -This endpoint represents all [trades](../resources/trade.md) that affect a given [account](../resources/account.md). - -This endpoint can also be used in [streaming](../streaming.md) mode, making it possible to listen for new trades that affect the given account as they occur on the Stellar network. -If called in streaming mode Horizon will start at the earliest known trade unless a `cursor` is set. In that case it will start from the `cursor`. You can also set `cursor` value to `now` to only stream trades created since your request time. - -## Request - -``` -GET /accounts/{account_id}/trades{?cursor,limit,order} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `account_id` | required, string | ID of an account | GBYTR4MC5JAX4ALGUBJD7EIKZVM7CUGWKXIUJMRSMK573XH2O7VAK3SR | -| `?cursor` | optional, any, default _null_ | A paging token, specifying where to start returning records from. When streaming this can be set to `now` to stream object created since your request time. | 12884905984 | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default: `10` | Maximum number of records to return. | `200` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/accounts/GBYTR4MC5JAX4ALGUBJD7EIKZVM7CUGWKXIUJMRSMK573XH2O7VAK3SR/trades?limit=1" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.trades() - .forAccount("GBYTR4MC5JAX4ALGUBJD7EIKZVM7CUGWKXIUJMRSMK573XH2O7VAK3SR") - .call() - .then(function (accountResult) { - console.log(accountResult); - }) - .catch(function (err) { - console.error(err); - }) -``` - - -## Response - -This endpoint responds with a list of trades that changed a given account's state. See the [trade resource](../resources/trade.md) for reference. - -### Example Response -```json -{ - "_links": { - "self": { - "href": "/accounts/GBYTR4MC5JAX4ALGUBJD7EIKZVM7CUGWKXIUJMRSMK573XH2O7VAK3SR/trades?cursor=\u0026limit=1\u0026order=asc" - }, - "next": { - "href": "/accounts/GBYTR4MC5JAX4ALGUBJD7EIKZVM7CUGWKXIUJMRSMK573XH2O7VAK3SR/trades?cursor=940258535411713-0\u0026limit=1\u0026order=asc" - }, - "prev": { - "href": "/accounts/GBYTR4MC5JAX4ALGUBJD7EIKZVM7CUGWKXIUJMRSMK573XH2O7VAK3SR/trades?cursor=940258535411713-0\u0026limit=1\u0026order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "" - }, - "base": { - "href": "/accounts/GBYTR4MC5JAX4ALGUBJD7EIKZVM7CUGWKXIUJMRSMK573XH2O7VAK3SR" - }, - "counter": { - "href": "/accounts/GBOOAYCAJIN7YCUUAHEQJJARNQMRUP4P2WXVO6P4KAMAB27NGA3CYTZU" - }, - "operation": { - "href": "/operations/940258535411713" - } - }, - "id": "940258535411713-0", - "paging_token": "940258535411713-0", - "ledger_close_time": "2017-03-30T13:20:41Z", - "offer_id": "8", - "base_offer_id": "8", - "base_account": "GBYTR4MC5JAX4ALGUBJD7EIKZVM7CUGWKXIUJMRSMK573XH2O7VAK3SR", - "base_amount": "1.0000000", - "base_asset_type": "credit_alphanum4", - "base_asset_code": "BTC", - "base_asset_issuer": "GB6FN4C7ZLWKENAOZDLZOQHNIOK4RDMV6EKLR53LWCHEBR6LVXOEKDZH", - "counter_offer_id": "4611686044197195777", - "counter_account": "GBOOAYCAJIN7YCUUAHEQJJARNQMRUP4P2WXVO6P4KAMAB27NGA3CYTZU", - "counter_amount": "1.0000000", - "counter_asset_type": "native", - "base_is_seller": true, - "price": { - "n": 1, - "d": 1 - } - } - ] - } -} -``` - -## Example Streaming Event -``` -{ - _links: - { self: { href: '' }, - base: { href: '/accounts/GDICGE2CFCNM3ZWRUVOWDJB2RAO667UE7WOSJJ2Z3IMISUA7CJZCE3KO' }, - counter: { href: '/accounts/GBILENMVJPVPEPXUPUPRBUEAME5OUQWAHIGZAX7TQX65NIQW3G3DGUYX' }, - operation: { href: '/operations/47274327069954049' } }, - id: '47274327069954049-0', - paging_token: '47274327069954049-0', - ledger_close_time: '2018-09-12T00:00:34Z', - offer_id: '711437', - base_account: 'GDICGE2CFCNM3ZWRUVOWDJB2RAO667UE7WOSJJ2Z3IMISUA7CJZCE3KO', - base_amount: '13.0000000', - base_asset_type: 'native', - counter_account: 'GBILENMVJPVPEPXUPUPRBUEAME5OUQWAHIGZAX7TQX65NIQW3G3DGUYX', - counter_amount: '13.0000000', - counter_asset_type: 'credit_alphanum4', - counter_asset_code: 'CNY', - counter_asset_issuer: 'GAREELUB43IRHWEASCFBLKHURCGMHE5IF6XSE7EXDLACYHGRHM43RFOX', - base_is_seller: true, - price: { n: 1, d: 1 } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no account whose ID matches the `account_id` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/trades-for-offer.md b/services/horizon/internal/docs/reference/endpoints/trades-for-offer.md deleted file mode 100644 index e6533e1cc1..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/trades-for-offer.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: Trades for Offer -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=trades&endpoint=for_offer ---- - -This endpoint represents all [trades](../resources/trade.md) for a given [offer](../resources/offer.md). - -This endpoint can also be used in [streaming](../streaming.md) mode, making it possible to listen for new trades for the given offer as they occur on the Stellar network. -If called in streaming mode Horizon will start at the earliest known trade unless a `cursor` is set. In that case it will start from the `cursor`. You can also set `cursor` value to `now` to only stream trades created since your request time. -## Request - -``` -GET /offers/{offer_id}/trades{?cursor,limit,order} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `offer_id` | required, number | ID of an offer | 323223 | -| `?cursor` | optional, any, default _null_ | A paging token, specifying where to start returning records from. | 12884905984 | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default: `10` | Maximum number of records to return. | `200` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/offers/323223/trades" -``` - -### JavaScript Example Request - -```js -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.trades() - .forOffer(323223) - .call() - .then(function (tradesResult) { - console.log(tradesResult); - }) - .catch(function (err) { - console.error(err); - }) -``` - - -## Response - -This endpoint responds with a list of trades that consumed a given offer. See the [trade resource](../resources/trade.md) for reference. - -### Example Response -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/offers/323223/trades?cursor=\u0026limit=10\u0026order=asc" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/offers/323223/trades?cursor=35789107779080193-0\u0026limit=10\u0026order=asc" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/offers/323223/trades?cursor=35789107779080193-0\u0026limit=10\u0026order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "" - }, - "base": { - "href": "https://horizon-testnet.stellar.org/accounts/GDRCFIQAUEFUQ6GXF5DPRO2M77E4UB7RW7EWI2FTKOW7CWYKZCHSI75K" - }, - "counter": { - "href": "https://horizon-testnet.stellar.org/accounts/GCUD7CBKTQI4D7ZR7IKHMGXZKKVABML7XFBHV4AIYBOEN5UQFZ5DSPPT" - }, - "operation": { - "href": "https://horizon-testnet.stellar.org/operations/35789107779080193" - } - }, - "id": "35789107779080193-0", - "paging_token": "35789107779080193-0", - "ledger_close_time": "2018-04-08T05:58:37Z", - "base_offer_id": "323223", - "base_account": "GDRCFIQAUEFUQ6GXF5DPRO2M77E4UB7RW7EWI2FTKOW7CWYKZCHSI75K", - "base_amount": "912.6607285", - "base_asset_type": "native", - "counter_offer_id": "4611686044197195777", - "counter_account": "GCUD7CBKTQI4D7ZR7IKHMGXZKKVABML7XFBHV4AIYBOEN5UQFZ5DSPPT", - "counter_amount": "16.5200719", - "counter_asset_type": "credit_alphanum4", - "counter_asset_code": "CM10", - "counter_asset_issuer": "GBUJJAYHS64L4RDHPLURQJUKSHHPINSAYXYVMWPEF4LECHDKB2EFMKBX", - "base_is_seller": true, - "price": { - "n": 18101, - "d": 1000000 - } - } - ] - } -} -``` - -## Example Streaming Event -```cgo -{ _links: - { self: { href: '' }, - base: - { href: '/accounts/GDJNMHET4DTS7HUHU7IG5DB274OSMHUYA7TRRKOD6ZABHPUW5YWJ4SUD' }, - counter: - { href: '/accounts/GCALYDRCCJEUPMV24TAX2N2N3IBX7NUUYZNM7I5FQS5GIEQ4A7EVKUOP' }, - operation: { href: '/operations/47261068505915393' } }, - id: '47261068505915393-0', - paging_token: '47261068505915393-0', - ledger_close_time: '2018-09-11T19:42:04Z', - offer_id: '734529', - base_account: 'GDJNMHET4DTS7HUHU7IG5DB274OSMHUYA7TRRKOD6ZABHPUW5YWJ4SUD', - base_amount: '0.0175999', - base_asset_type: 'credit_alphanum4', - base_asset_code: 'BOC', - base_asset_issuer: 'GCTS32RGWRH6RJM62UVZ4UT5ZN5L6B2D3LPGO6Z2NM2EOGVQA7TA6SKO', - counter_account: 'GCALYDRCCJEUPMV24TAX2N2N3IBX7NUUYZNM7I5FQS5GIEQ4A7EVKUOP', - counter_amount: '0.0199998', - counter_asset_type: 'credit_alphanum4', - counter_asset_code: 'ABC', - counter_asset_issuer: 'GCTS32RGWRH6RJM62UVZ4UT5ZN5L6B2D3LPGO6Z2NM2EOGVQA7TA6SKO', - base_is_seller: true, - price: { n: 2840909, d: 2500000 } -} -``` -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no offer whose ID matches the `offer_id` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/trades.md b/services/horizon/internal/docs/reference/endpoints/trades.md deleted file mode 100644 index ab7760f3e9..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/trades.md +++ /dev/null @@ -1,178 +0,0 @@ ---- -title: Trades -replacement: https://developers.stellar.org/api/resources/trades/ ---- - -People on the Stellar network can make [offers](../resources/offer.md) to buy or sell assets. When -an offer is fully or partially fulfilled, a [trade](../resources/trade.md) happens. - -Trades can be filtered for a specific orderbook, defined by an asset pair: `base` and `counter`. - -This endpoint can also be used in [streaming](../streaming.md) mode, making it possible to listen -for new trades as they occur on the Stellar network. - -If called in streaming mode Horizon will start at the earliest known trade unless a `cursor` is -set. In that case it will start from the `cursor`. You can also set `cursor` value to `now` to only -stream trades created since your request time. - -## Request - -``` -GET /trades?base_asset_type={base_asset_type}&base_asset_code={base_asset_code}&base_asset_issuer={base_asset_issuer}&counter_asset_type={counter_asset_type}&counter_asset_code={counter_asset_code}&counter_asset_issuer={counter_asset_issuer} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `base_asset_type` | optional, string | Type of base asset | `native` | -| `base_asset_code` | optional, string | Code of base asset, not required if type is `native` | `USD` | -| `base_asset_issuer` | optional, string | Issuer of base asset, not required if type is `native` | 'GA2HGBJIJKI6O4XEM7CZWY5PS6GKSXL6D34ERAJYQSPYA6X6AI7HYW36' | -| `counter_asset_type` | optional, string | Type of counter asset | `credit_alphanum4` | -| `counter_asset_code` | optional, string | Code of counter asset, not required if type is `native` | `BTC` | -| `counter_asset_issuer` | optional, string | Issuer of counter asset, not required if type is `native` | 'GD6VWBXI6NY3AOOR55RLVQ4MNIDSXE5JSAVXUTF35FRRI72LYPI3WL6Z' | -| `offer_id` | optional, string | filter for by a specific offer id | `283606` | -| `?cursor` | optional, any, default _null_ | A paging token, specifying where to start returning records from. | `12884905984` | -| `?order` | optional, string, default `asc` | The order, in terms of timeline, in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default: `10` | Maximum number of records to return. | `200` | - -### curl Example Request -```sh -curl https://horizon.stellar.org/trades?base_asset_type=native&counter_asset_code=SLT&counter_asset_issuer=GCKA6K5PCQ6PNF5RQBF7PQDJWRHO6UOGFMRLK3DYHDOI244V47XKQ4GP&counter_asset_type=credit_alphanum4&limit=2&order=desc -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.trades() - .call() - .then(function (tradesResult) { - console.log(tradesResult.records); - }) - .catch(function (err) { - console.log(err) - }) -``` - -### JavaScript Example Streaming Request - -```javascript -var StellarSdk = require('stellar-sdk') -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var tradesHandler = function (tradeResponse) { - console.log(tradeResponse); -}; - -var es = server.trades() - .cursor('now') - .stream({ - onmessage: tradesHandler -}) -``` - -## Response - -The list of trades. `base` and `counter` in the records will match the asset pair filter order. If an asset pair is not specified, the order is arbitrary. - -### Example Response -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/trades?cursor=&limit=10&order=asc" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/trades?cursor=6025839120434-0&limit=10&order=asc" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/trades?cursor=6012954218535-0&limit=10&order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "" - }, - "base": { - "href": "https://horizon-testnet.stellar.org/accounts/GAQHWQYBBW272OOXNQMMLCA5WY2XAZPODGB7Q3S5OKKIXVESKO55ZQ7C" - }, - "counter": { - "href": "https://horizon-testnet.stellar.org/accounts/GCYN7MI6VXVRP74KR6MKBAW2ELLCXL6QCY5H4YQ62HVWZWMCE6Y232UC" - }, - "operation": { - "href": "https://horizon-testnet.stellar.org/operations/6012954218535" - } - }, - "id": "6012954218535-0", - "paging_token": "6012954218535-0", - "ledger_close_time": "2019-02-27T11:54:53Z", - "offer_id": "37", - "base_offer_id": "4611692031381606439", - "base_account": "GAQHWQYBBW272OOXNQMMLCA5WY2XAZPODGB7Q3S5OKKIXVESKO55ZQ7C", - "base_amount": "25.6687300", - "base_asset_type": "credit_alphanum4", - "base_asset_code": "DSQ", - "base_asset_issuer": "GBDQPTQJDATT7Z7EO4COS4IMYXH44RDLLI6N6WIL5BZABGMUOVMLWMQF", - "counter_offer_id": "37", - "counter_account": "GCYN7MI6VXVRP74KR6MKBAW2ELLCXL6QCY5H4YQ62HVWZWMCE6Y232UC", - "counter_amount": "1.0265563", - "counter_asset_type": "credit_alphanum4", - "counter_asset_code": "USD", - "counter_asset_issuer": "GAA4MFNZGUPJAVLWWG6G5XZJFZDHLKQNG3Q6KB24BAD6JHNNVXDCF4XG", - "base_is_seller": false, - "price": { - "n": 10000000, - "d": 250046977 - } - }, - { - "_links": { - "self": { - "href": "" - }, - "base": { - "href": "https://horizon-testnet.stellar.org/accounts/GAQHWQYBBW272OOXNQMMLCA5WY2XAZPODGB7Q3S5OKKIXVESKO55ZQ7C" - }, - "counter": { - "href": "https://horizon-testnet.stellar.org/accounts/GCYN7MI6VXVRP74KR6MKBAW2ELLCXL6QCY5H4YQ62HVWZWMCE6Y232UC" - }, - "operation": { - "href": "https://horizon-testnet.stellar.org/operations/6025839120385" - } - }, - "id": "6025839120385-0", - "paging_token": "6025839120385-0", - "ledger_close_time": "2019-02-27T11:55:09Z", - "offer_id": "1", - "base_offer_id": "4611692044266508289", - "base_account": "GAQHWQYBBW272OOXNQMMLCA5WY2XAZPODGB7Q3S5OKKIXVESKO55ZQ7C", - "base_amount": "1434.4442973", - "base_asset_type": "credit_alphanum4", - "base_asset_code": "DSQ", - "base_asset_issuer": "GBDQPTQJDATT7Z7EO4COS4IMYXH44RDLLI6N6WIL5BZABGMUOVMLWMQF", - "counter_offer_id": "1", - "counter_account": "GCYN7MI6VXVRP74KR6MKBAW2ELLCXL6QCY5H4YQ62HVWZWMCE6Y232UC", - "counter_amount": "0.5622050", - "counter_asset_type": "credit_alphanum4", - "counter_asset_code": "SXRT", - "counter_asset_issuer": "GAIOQ3UYK5NYIZY5ZFAG4JBN4O37NAVFKZM5YDYEB6YEFBZSZ5KDCUFO", - "base_is_seller": false, - "price": { - "n": 642706, - "d": 1639839483 - } - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#standard-errors). diff --git a/services/horizon/internal/docs/reference/endpoints/transactions-all.md b/services/horizon/internal/docs/reference/endpoints/transactions-all.md deleted file mode 100644 index aac65c4950..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/transactions-all.md +++ /dev/null @@ -1,189 +0,0 @@ ---- -title: All Transactions -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=transactions&endpoint=all -replacement: https://developers.stellar.org/api/resources/transactions/single/ ---- - -This endpoint represents all successful [transactions](../resources/transaction.md). -Please note that this endpoint returns failed transactions that are included in the ledger if -`include_failed` parameter is `true` and Horizon is ingesting failed transactions. -This endpoint can also be used in [streaming](../streaming.md) mode. This makes it possible to use -it to listen for new transactions as they get made in the Stellar network. If called in streaming -mode Horizon will start at the earliest known transaction unless a `cursor` is set. In that case it -will start from the `cursor`. You can also set `cursor` value to `now` to only stream transaction -created since your request time. - -## Request - -``` -GET /transactions{?cursor,limit,order,include_failed} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `?cursor` | optional, any, default _null_ | A paging token, specifying where to start returning records from. When streaming this can be set to `now` to stream object created since your request time. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default: `10` | Maximum number of records to return. | `200` | -| `?include_failed` | optional, bool, default: `false` | Set to `true` to include failed transactions in results. | `true` | - -### curl Example Request - -```sh -# Retrieve the 200 latest transactions, ordered chronologically: -curl "https://horizon-testnet.stellar.org/transactions?limit=200&order=desc" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.transactions() - .call() - .then(function (transactionResult) { - //page 1 - console.log(transactionResult.records); - return transactionResult.next(); - }) - .then(function (transactionResult) { - console.log(transactionResult.records); - }) - .catch(function (err) { - console.log(err) - }) -``` - -### JavaScript Streaming Example - -```javascript -var StellarSdk = require('stellar-sdk') -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var txHandler = function (txResponse) { - console.log(txResponse); -}; - -var es = server.transactions() - .cursor('now') - .stream({ - onmessage: txHandler - }) -``` - -## Response - -If called normally this endpoint responds with a [page](../resources/page.md) of transactions. -If called in streaming mode the transaction resources are returned individually. -See [transaction resource](../resources/transaction.md) for reference. - -### Example Response - -```json -{ - "_embedded": { - "records": [ - { - "_links": { - "account": { - "href": "/accounts/GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K" - }, - "effects": { - "href": "/transactions/fa78cb43d72171fdb2c6376be12d57daa787b1fa1a9fdd0e9453e1f41ee5f15a/effects{?cursor,limit,order}", - "templated": true - }, - "ledger": { - "href": "/ledgers/146970" - }, - "operations": { - "href": "/transactions/fa78cb43d72171fdb2c6376be12d57daa787b1fa1a9fdd0e9453e1f41ee5f15a/operations{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/transactions?cursor=631231343497216\u0026order=asc" - }, - "self": { - "href": "/transactions/fa78cb43d72171fdb2c6376be12d57daa787b1fa1a9fdd0e9453e1f41ee5f15a" - }, - "succeeds": { - "href": "/transactions?cursor=631231343497216\u0026order=desc" - } - }, - "id": "fa78cb43d72171fdb2c6376be12d57daa787b1fa1a9fdd0e9453e1f41ee5f15a", - "paging_token": "631231343497216", - "successful": true, - "hash": "fa78cb43d72171fdb2c6376be12d57daa787b1fa1a9fdd0e9453e1f41ee5f15a", - "ledger": 146970, - "created_at": "2015-09-24T10:07:09Z", - "account": "GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K", - "account_sequence": 279172874343, - "max_fee": 100, - "fee_charged": 100, - "operation_count": 1, - "envelope_xdr": "AAAAAGXNhLrhGtltTwCpmqlarh7s1DB2hIkbP//jgzn4Fos/AAAACgAAAEEAAABnAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA2ddmTOFAgr21Crs2RXRGLhiAKxicZb/IERyEZL/Y2kUAAAAXSHboAAAAAAAAAAAB+BaLPwAAAECDEEZmzbgBr5fc3mfJsCjWPDtL6H8/vf16me121CC09ONyWJZnw0PUvp4qusmRwC6ZKfLDdk8F3Rq41s+yOgQD", - "result_xdr": "AAAAAAAAAAoAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", - "result_meta_xdr": "AAAAAAAAAAEAAAACAAAAAAACPhoAAAAAAAAAANnXZkzhQIK9tQq7NkV0Ri4YgCsYnGW/yBEchGS/2NpFAAAAF0h26AAAAj4aAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAQACPhoAAAAAAAAAAGXNhLrhGtltTwCpmqlarh7s1DB2hIkbP//jgzn4Fos/AABT8kS2c/oAAABBAAAAZwAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA" - }, - { - "_links": { - "account": { - "href": "/accounts/GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K" - }, - "effects": { - "href": "/transactions/90ad6cfc9b0911bdbf202cace78ae7ecf50989c424288670dadb69bf8237c1b3/effects{?cursor,limit,order}", - "templated": true - }, - "ledger": { - "href": "/ledgers/144798" - }, - "operations": { - "href": "/transactions/90ad6cfc9b0911bdbf202cace78ae7ecf50989c424288670dadb69bf8237c1b3/operations{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/transactions?cursor=621902674530304\u0026order=asc" - }, - "self": { - "href": "/transactions/90ad6cfc9b0911bdbf202cace78ae7ecf50989c424288670dadb69bf8237c1b3" - }, - "succeeds": { - "href": "/transactions?cursor=621902674530304\u0026order=desc" - } - }, - "id": "90ad6cfc9b0911bdbf202cace78ae7ecf50989c424288670dadb69bf8237c1b3", - "paging_token": "621902674530304", - "successful": false, - "hash": "90ad6cfc9b0911bdbf202cace78ae7ecf50989c424288670dadb69bf8237c1b3", - "ledger": 144798, - "created_at": "2015-09-24T07:49:38Z", - "account": "GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K", - "account_sequence": 279172874342, - "max_fee": 100, - "fee_charged": 100, - "operation_count": 1, - "envelope_xdr": "AAAAAGXNhLrhGtltTwCpmqlarh7s1DB2hIkbP//jgzn4Fos/AAAACgAAAEEAAABmAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAMPT7P7buwqnMueFS4NV10vE2q3C/mcAy4jx03/RdSGsAAAAXSHboAAAAAAAAAAAB+BaLPwAAAEBPWWMNSWyPBbQlhRheXyvAFDVx1rnf68fdDOUHPdDIkHdUczBpzvCjpdgwhQ2NYOX5ga1ZgOIWLy789YNnuIcL", - "result_xdr": "AAAAAAAAAAoAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", - "result_meta_xdr": "AAAAAAAAAAEAAAACAAAAAAACNZ4AAAAAAAAAADD0+z+27sKpzLnhUuDVddLxNqtwv5nAMuI8dN/0XUhrAAAAF0h26AAAAjWeAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAQACNZ4AAAAAAAAAAGXNhLrhGtltTwCpmqlarh7s1DB2hIkbP//jgzn4Fos/AABUCY0tXAQAAABBAAAAZgAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA" - } - ] - }, - "_links": { - "next": { - "href": "/transactions?order=desc\u0026limit=2\u0026cursor=621902674530304" - }, - "prev": { - "href": "/transactions?order=asc\u0026limit=2\u0026cursor=631231343497216" - }, - "self": { - "href": "/transactions?order=desc\u0026limit=2\u0026cursor=" - } - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#standard-errors). diff --git a/services/horizon/internal/docs/reference/endpoints/transactions-create.md b/services/horizon/internal/docs/reference/endpoints/transactions-create.md deleted file mode 100644 index 1801bb5c12..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/transactions-create.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: Post Transaction -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=transactions&endpoint=create -replacement: https://developers.stellar.org/api/resources/transactions/post/ ---- - -Posts a new [transaction](../resources/transaction.md) to the Stellar Network. -Note that creating a valid transaction and signing it properly is the -responsibility of your client library. - -Transaction submission and the subsequent validation and inclusion into the -Stellar Network's ledger is a [complicated and asynchronous -process](https://www.stellar.org/developers/learn/concepts/transactions.html#life-cycle). -To reduce the complexity, horizon manages these asynchronous processes for the -client and will wait to hear results from the Stellar Network before returning -an HTTP response to a client. - -Transaction submission to horizon aims to be -[idempotent](https://en.wikipedia.org/wiki/Idempotence#Computer_science_meaning): -a client can submit a given transaction to horizon more than once and horizon -will behave the same each time. If the transaction has already been -successfully applied to the ledger, horizon will simply return the saved result -and not attempt to submit the transaction again. Only in cases where a -transaction's status is unknown (and thus will have a chance of being included -into a ledger) will a resubmission to the network occur. - -Information about [building transactions](https://www.stellar.org/developers/js-stellar-base/reference/building-transactions.html) in JavaScript. - -### Timeout - -If you are encountering this error it means that either: - -* Horizon has not received a confirmation from the Core server that the transaction you are trying to submit to the network was included in a ledger in a timely manner or: -* Horizon has not sent a response to a reverse-proxy before in a specified time. - -The former case may happen because there was no room for your transaction in the 3 consecutive ledgers. In such case, Core server removes a transaction from a queue. To solve this you can either: - -* Keep resubmitting the same transaction (with the same sequence number) and wait until it finally is added to a new ledger or: -* Increase the [fee](../../../guides/concepts/fees.html). - -## Request - -``` -POST /transactions -``` - -### Arguments - -| name | loc | notes | example | description | -| ---- | ---- | -------- | ---------------------- | ----------- | -| `tx` | body | required | `AAAAAO`....`f4yDBA==` | Base64 representation of transaction envelope [XDR](../xdr.md) | - - -### curl Example Request - -```sh -curl -X POST \ - -F "tx=AAAAAOo1QK/3upA74NLkdq4Io3DQAQZPi4TVhuDnvCYQTKIVAAAACgAAH8AAAAABAAAAAAAAAAAAAAABAAAAAQAAAADqNUCv97qQO+DS5HauCKNw0AEGT4uE1Ybg57wmEEyiFQAAAAEAAAAAZc2EuuEa2W1PAKmaqVquHuzUMHaEiRs//+ODOfgWiz8AAAAAAAAAAAAAA+gAAAAAAAAAARBMohUAAABAPnnZL8uPlS+c/AM02r4EbxnZuXmP6pQHvSGmxdOb0SzyfDB2jUKjDtL+NC7zcMIyw4NjTa9Ebp4lvONEf4yDBA==" \ - "https://horizon-testnet.stellar.org/transactions" -``` - -## Response - -A successful response (i.e. any response with a successful HTTP response code) -indicates that the transaction was successful and has been included into the -ledger. - -If the transaction failed or errored, then an error response will be returned. Please see the errors section below. - -### Attributes - -The response will include all fields from the [transaction resource](../resources/transaction.md). - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/transactions/264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c" - }, - "account": { - "href": "https://horizon-testnet.stellar.org/accounts/GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR" - }, - "ledger": { - "href": "https://horizon-testnet.stellar.org/ledgers/697121" - }, - "operations": { - "href": "https://horizon-testnet.stellar.org/transactions/264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c/operations{?cursor,limit,order}", - "templated": true - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/transactions/264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c/effects{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/transactions?order=asc&cursor=2994111896358912" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/transactions?order=desc&cursor=2994111896358912" - } - }, - "id": "264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c", - "paging_token": "2994111896358912", - "successful": true, - "hash": "264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c", - "ledger": 697121, - "created_at": "2019-04-09T20:14:25Z", - "source_account": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", - "fee_account": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", - "source_account_sequence": "4660039994869", - "fee_charged": 100, - "max_fee": 100, - "operation_count": 1, - "envelope_xdr": "AAAAABB90WssODNIgi6BHveqzxTRmIpvAFRyVNM+Hm2GVuCcAAAAZAAABD0AB031AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAFIMRkFZ9gZifhRSlklQpsz/9P04Earv0dzS3MkIM1cYAAAAXSHboAAAAAAAAAAABhlbgnAAAAEA+biIjrDy8yi+SvhFElIdWGBRYlDscnSSHkPchePy2JYDJn4wvJYDBumXI7/NmttUey3+cGWbBFfnnWh1H5EoD", - "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", - "result_meta_xdr": "AAAAAQAAAAIAAAADAAqjIQAAAAAAAAAAEH3Rayw4M0iCLoEe96rPFNGYim8AVHJU0z4ebYZW4JwBOLmYhGq/IAAABD0AB030AAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAqjIQAAAAAAAAAAEH3Rayw4M0iCLoEe96rPFNGYim8AVHJU0z4ebYZW4JwBOLmYhGq/IAAABD0AB031AAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAMACqMhAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnAE4uZiEar8gAAAEPQAHTfUAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEACqMhAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnAE4uYE789cgAAAEPQAHTfUAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAACqMhAAAAAAAAAAAUgxGQVn2BmJ+FFKWSVCmzP/0/TgRqu/R3NLcyQgzVxgAAABdIdugAAAqjIQAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", - "fee_meta_xdr": "AAAAAgAAAAMACqMgAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnAE4uZiEar+EAAAEPQAHTfQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEACqMhAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnAE4uZiEar8gAAAEPQAHTfQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", - "memo_type": "none", - "signatures": [ - "Pm4iI6w8vMovkr4RRJSHVhgUWJQ7HJ0kh5D3IXj8tiWAyZ+MLyWAwbplyO/zZrbVHst/nBlmwRX551odR+RKAw==" - ] -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard_Errors). -- [transaction_failed](../errors/transaction-failed.md): The transaction failed and could not be applied to the ledger. -- [transaction_malformed](../errors/transaction-malformed.md): The transaction could not be decoded and was not submitted to the network. -- [timeout](../errors/timeout.md): No response from the Core server in a timely manner. Please check "Timeout" section above. diff --git a/services/horizon/internal/docs/reference/endpoints/transactions-for-account.md b/services/horizon/internal/docs/reference/endpoints/transactions-for-account.md deleted file mode 100644 index 375c281698..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/transactions-for-account.md +++ /dev/null @@ -1,166 +0,0 @@ ---- -title: Transactions for Account -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=transactions&endpoint=for_account -replacement: https://developers.stellar.org/api/resources/accounts/transactions/ ---- - -This endpoint represents successful [transactions](../resources/transaction.md) that affected a -given [account](../resources/account.md). This endpoint can also be used in -[streaming](../streaming.md) mode so it is possible to use it to listen for new transactions that -affect a given account as they get made in the Stellar network. - -If called in streaming mode Horizon will start at the earliest known transaction unless a `cursor` -is set. In that case it will start from the `cursor`. You can also set `cursor` value to `now` to -only stream transaction created since your request time. - -## Request - -``` -GET /accounts/{account_id}/transactions{?cursor,limit,order,include_failed} -``` - -### Arguments - -| name | notes | description | example | -| ---- | ----- | ----------- | ------- | -| `account_id` | required, string | ID of an account | GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K | -| `?cursor` | optional, any, default _null_ | A paging token, specifying where to start returning records from. When streaming this can be set to `now` to stream object created since your request time. | 12884905984 | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default: `10` | Maximum number of records to return. | `200` | -| `?include_failed` | optional, bool, default: `false` | Set to `true` to include failed transactions in results. | `true` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/accounts/GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K/transactions?limit=1" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.transactions() - .forAccount("GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K") - .call() - .then(function (accountResult) { - console.log(accountResult); - }) - .catch(function (err) { - console.error(err); - }) -``` - -### JavaScript Streaming Example - -```javascript -var StellarSdk = require('stellar-sdk') -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -var txHandler = function (txResponse) { - console.log(txResponse); -}; - -var es = server.transactions() - .forAccount("GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K") - .cursor('now') - .stream({ - onmessage: txHandler - }) -``` - -## Response - -This endpoint responds with a list of transactions that changed a given account's state. See -[transaction resource](../resources/transaction.md) for reference. - -### Example Response -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/accounts/GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF/payments?cursor=&limit=10&order=asc" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/accounts/GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF/payments?cursor=2714719978786817&limit=10&order=asc" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/accounts/GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF/payments?cursor=1919197546291201&limit=10&order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/operations/1919197546291201" - }, - "transaction": { - "href": "https://horizon-testnet.stellar.org/transactions/7e2050abc676003efc3eaadd623c927f753b7a6c37f50864bf284f4e1510d088" - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/operations/1919197546291201/effects" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=1919197546291201" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=1919197546291201" - } - }, - "id": "1919197546291201", - "paging_token": "1919197546291201", - "transaction_successful": true, - "source_account": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", - "type": "create_account", - "type_i": 0, - "created_at": "2019-03-25T22:43:38Z", - "transaction_hash": "7e2050abc676003efc3eaadd623c927f753b7a6c37f50864bf284f4e1510d088", - "starting_balance": "10000.0000000", - "funder": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", - "account": "GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF" - }, - { - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/operations/2714719978786817" - }, - "transaction": { - "href": "https://horizon-testnet.stellar.org/transactions/7cea6abe90654578b42ee696e823187d89d91daa157a1077b542ee7c77413ce3" - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/operations/2714719978786817/effects" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/effects?order=desc&cursor=2714719978786817" - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/effects?order=asc&cursor=2714719978786817" - } - }, - "id": "2714719978786817", - "paging_token": "2714719978786817", - "transaction_successful": true, - "source_account": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR", - "type": "payment", - "type_i": 1, - "created_at": "2019-04-05T23:07:42Z", - "transaction_hash": "7cea6abe90654578b42ee696e823187d89d91daa157a1077b542ee7c77413ce3", - "asset_type": "credit_alphanum4", - "asset_code": "FOO", - "asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR", - "from": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR", - "to": "GBYUUJHG6F4EPJGNLERINATVQLNDOFRUD7SGJZ26YZLG5PAYLG7XUSGF", - "amount": "1000000.0000000" - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no account whose ID matches the `account_id` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/transactions-for-ledger.md b/services/horizon/internal/docs/reference/endpoints/transactions-for-ledger.md deleted file mode 100644 index 7f1f5e4693..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/transactions-for-ledger.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: Transactions for Ledger -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=transactions&endpoint=for_ledger -replacement: https://developers.stellar.org/api/resources/ledgers/transactions/ ---- - -This endpoint represents successful [transactions](../resources/transaction.md) in a given [ledger](../resources/ledger.md). - -## Request - -``` -GET /ledgers/{id}/transactions{?cursor,limit,order,include_failed} -``` - -### Arguments - -| name | notes | description | example | -| ------ | ------- | ----------- | ------- | -| `id` | required, number | Ledger ID | `697121` | -| `?cursor` | optional, default _null_ | A paging token, specifying where to start returning records from. | `12884905984` | -| `?order` | optional, string, default `asc` | The order in which to return rows, "asc" or "desc". | `asc` | -| `?limit` | optional, number, default `10` | Maximum number of records to return. | `200` | -| `?include_failed` | optional, bool, default: `false` | Set to `true` to include failed transactions in results. | `true` | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/ledgers/697121/transactions?limit=1" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.transactions() - .forLedger("697121") - .limit("1") - .call() - .then(function (accountResults) { - console.log(accountResults.records) - }) - .catch(function (err) { - console.log(err) - }) -``` - -## Response - -This endpoint responds with a list of transactions in a given ledger. See [transaction -resource](../resources/transaction.md) for reference. - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/ledgers/697121/transactions?cursor=&limit=10&order=asc" - }, - "next": { - "href": "https://horizon-testnet.stellar.org/ledgers/697121/transactions?cursor=2994111896367104&limit=10&order=asc" - }, - "prev": { - "href": "https://horizon-testnet.stellar.org/ledgers/697121/transactions?cursor=2994111896358912&limit=10&order=desc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/transactions/264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c" - }, - "account": { - "href": "https://horizon-testnet.stellar.org/accounts/GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR" - }, - "ledger": { - "href": "https://horizon-testnet.stellar.org/ledgers/697121" - }, - "operations": { - "href": "https://horizon-testnet.stellar.org/transactions/264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c/operations{?cursor,limit,order}", - "templated": true - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/transactions/264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c/effects{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/transactions?order=asc&cursor=2994111896358912" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/transactions?order=desc&cursor=2994111896358912" - } - }, - "id": "264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c", - "paging_token": "2994111896358912", - "successful": true, - "hash": "264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c", - "ledger": 697121, - "created_at": "2019-04-09T20:14:25Z", - "source_account": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", - "source_account_sequence": "4660039994869", - "fee_charged": 100, - "max_fee": 100, - "operation_count": 1, - "envelope_xdr": "AAAAABB90WssODNIgi6BHveqzxTRmIpvAFRyVNM+Hm2GVuCcAAAAZAAABD0AB031AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAFIMRkFZ9gZifhRSlklQpsz/9P04Earv0dzS3MkIM1cYAAAAXSHboAAAAAAAAAAABhlbgnAAAAEA+biIjrDy8yi+SvhFElIdWGBRYlDscnSSHkPchePy2JYDJn4wvJYDBumXI7/NmttUey3+cGWbBFfnnWh1H5EoD", - "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", - "result_meta_xdr": "AAAAAQAAAAIAAAADAAqjIQAAAAAAAAAAEH3Rayw4M0iCLoEe96rPFNGYim8AVHJU0z4ebYZW4JwBOLmYhGq/IAAABD0AB030AAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAqjIQAAAAAAAAAAEH3Rayw4M0iCLoEe96rPFNGYim8AVHJU0z4ebYZW4JwBOLmYhGq/IAAABD0AB031AAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAMACqMhAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnAE4uZiEar8gAAAEPQAHTfUAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEACqMhAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnAE4uYE789cgAAAEPQAHTfUAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAACqMhAAAAAAAAAAAUgxGQVn2BmJ+FFKWSVCmzP/0/TgRqu/R3NLcyQgzVxgAAABdIdugAAAqjIQAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", - "fee_meta_xdr": "AAAAAgAAAAMACqMgAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnAE4uZiEar+EAAAEPQAHTfQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEACqMhAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnAE4uZiEar8gAAAEPQAHTfQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", - "memo_type": "none", - "signatures": [ - "Pm4iI6w8vMovkr4RRJSHVhgUWJQ7HJ0kh5D3IXj8tiWAyZ+MLyWAwbplyO/zZrbVHst/nBlmwRX551odR+RKAw==" - ] - }, - { - "memo": "2A1V6J5703G47XHY", - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/transactions/f175108e5c64619705b112a99fa32884dfa0511d9a8986aade87905b08eabe5b" - }, - "account": { - "href": "https://horizon-testnet.stellar.org/accounts/GAZ4A54KE6MTMXYEPM7T3IDLZWGNCCKB5ME422NZ3MAMTHWWP37RPEBW" - }, - "ledger": { - "href": "https://horizon-testnet.stellar.org/ledgers/697121" - }, - "operations": { - "href": "https://horizon-testnet.stellar.org/transactions/f175108e5c64619705b112a99fa32884dfa0511d9a8986aade87905b08eabe5b/operations{?cursor,limit,order}", - "templated": true - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/transactions/f175108e5c64619705b112a99fa32884dfa0511d9a8986aade87905b08eabe5b/effects{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/transactions?order=asc&cursor=2994111896363008" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/transactions?order=desc&cursor=2994111896363008" - } - }, - "id": "f175108e5c64619705b112a99fa32884dfa0511d9a8986aade87905b08eabe5b", - "paging_token": "2994111896363008", - "successful": true, - "hash": "f175108e5c64619705b112a99fa32884dfa0511d9a8986aade87905b08eabe5b", - "ledger": 697121, - "created_at": "2019-04-09T20:14:25Z", - "source_account": "GAZ4A54KE6MTMXYEPM7T3IDLZWGNCCKB5ME422NZ3MAMTHWWP37RPEBW", - "source_account_sequence": "2994107601387521", - "fee_charged": 100, - "max_fee": 100, - "operation_count": 1, - "envelope_xdr": "AAAAADPAd4onmTZfBHs/PaBrzYzRCUHrCc1pudsAyZ7Wfv8XAAAAZAAKoyAAAAABAAAAAAAAAAEAAAAQMkExVjZKNTcwM0c0N1hIWQAAAAEAAAABAAAAADPAd4onmTZfBHs/PaBrzYzRCUHrCc1pudsAyZ7Wfv8XAAAAAQAAAADMSEvcRKXsaUNna++Hy7gWm/CfqTjEA7xoGypfrFGUHAAAAAAAAAAAhFKDAAAAAAAAAAAB1n7/FwAAAEBJdXuYg13Glzx1RinVCXd/cc1usrhU/0f5HFZ7lyIR8kS3T6PRrW78TQDNqXz+ukUiPwlB1A8MqxoW/SAL5FIB", - "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=", - "result_meta_xdr": "AAAAAQAAAAIAAAADAAqjIQAAAAAAAAAAM8B3iieZNl8Eez89oGvNjNEJQesJzWm52wDJntZ+/xcAAAAXSHbnnAAKoyAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAqjIQAAAAAAAAAAM8B3iieZNl8Eez89oGvNjNEJQesJzWm52wDJntZ+/xcAAAAXSHbnnAAKoyAAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAABAAAAAMACqMgAAAAAAAAAADMSEvcRKXsaUNna++Hy7gWm/CfqTjEA7xoGypfrFGUHAAAAANViducAABeBgAAoRQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEACqMhAAAAAAAAAADMSEvcRKXsaUNna++Hy7gWm/CfqTjEA7xoGypfrFGUHAAAAAPZ3F6cAABeBgAAoRQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAMACqMhAAAAAAAAAAAzwHeKJ5k2XwR7Pz2ga82M0QlB6wnNabnbAMme1n7/FwAAABdIduecAAqjIAAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEACqMhAAAAAAAAAAAzwHeKJ5k2XwR7Pz2ga82M0QlB6wnNabnbAMme1n7/FwAAABbEJGScAAqjIAAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", - "fee_meta_xdr": "AAAAAgAAAAMACqMgAAAAAAAAAAAzwHeKJ5k2XwR7Pz2ga82M0QlB6wnNabnbAMme1n7/FwAAABdIdugAAAqjIAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEACqMhAAAAAAAAAAAzwHeKJ5k2XwR7Pz2ga82M0QlB6wnNabnbAMme1n7/FwAAABdIduecAAqjIAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", - "memo_type": "text", - "signatures": [ - "SXV7mINdxpc8dUYp1Ql3f3HNbrK4VP9H+RxWe5ciEfJEt0+j0a1u/E0Azal8/rpFIj8JQdQPDKsaFv0gC+RSAQ==" - ] - }, - { - "memo": "WHALE", - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/transactions/83b6ebf4b3aec5b36cab14ae0f438a23487746857903a9e0bb002564b4641e25" - }, - "account": { - "href": "https://horizon-testnet.stellar.org/accounts/GABRMXDIJCTDSMPC67J64NSAMWRSYXVCXYTXVFC73DTHBKELHNKWANXP" - }, - "ledger": { - "href": "https://horizon-testnet.stellar.org/ledgers/697121" - }, - "operations": { - "href": "https://horizon-testnet.stellar.org/transactions/83b6ebf4b3aec5b36cab14ae0f438a23487746857903a9e0bb002564b4641e25/operations{?cursor,limit,order}", - "templated": true - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/transactions/83b6ebf4b3aec5b36cab14ae0f438a23487746857903a9e0bb002564b4641e25/effects{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/transactions?order=asc&cursor=2994111896367104" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/transactions?order=desc&cursor=2994111896367104" - } - }, - "id": "83b6ebf4b3aec5b36cab14ae0f438a23487746857903a9e0bb002564b4641e25", - "paging_token": "2994111896367104", - "successful": true, - "hash": "83b6ebf4b3aec5b36cab14ae0f438a23487746857903a9e0bb002564b4641e25", - "ledger": 697121, - "created_at": "2019-04-09T20:14:25Z", - "source_account": "GABRMXDIJCTDSMPC67J64NSAMWRSYXVCXYTXVFC73DTHBKELHNKWANXP", - "source_account_sequence": "122518237256298", - "fee_charged": 100, - "max_fee": 100, - "operation_count": 1, - "envelope_xdr": "AAAAAAMWXGhIpjkx4vfT7jZAZaMsXqK+J3qUX9jmcKiLO1VgAAAAZAAAb24AAppqAAAAAQAAAAAAAAAAAAAAAFys/kkAAAABAAAABVdIQUxFAAAAAAAAAQAAAAAAAAAAAAAAAKrN4k6edFMb0WEyPzEEjWUAji0pvvALw+BAH4OnekA5AAAAAAcnDgAAAAAAAAAAAYs7VWAAAABAYd9uIm+TjIcAjTU90YJoNg/r+6PU3Uss7ewUb1w3yMa+HyoSvDq8sDz/SYmDBH7F+0ACIeBF4kkVEKVBJMh0AQ==", - "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", - "result_meta_xdr": "AAAAAQAAAAIAAAADAAqjIQAAAAAAAAAAAxZcaEimOTHi99PuNkBloyxeor4nepRf2OZwqIs7VWAAJBMYWVFGqAAAb24AApppAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAqjIQAAAAAAAAAAAxZcaEimOTHi99PuNkBloyxeor4nepRf2OZwqIs7VWAAJBMYWVFGqAAAb24AAppqAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAMACqMhAAAAAAAAAAADFlxoSKY5MeL30+42QGWjLF6ivid6lF/Y5nCoiztVYAAkExhZUUaoAABvbgACmmoAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEACqMhAAAAAAAAAAADFlxoSKY5MeL30+42QGWjLF6ivid6lF/Y5nCoiztVYAAkExhSKjioAABvbgACmmoAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAACqMhAAAAAAAAAACqzeJOnnRTG9FhMj8xBI1lAI4tKb7wC8PgQB+Dp3pAOQAAAAAHJw4AAAqjIQAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", - "fee_meta_xdr": "AAAAAgAAAAMACqMfAAAAAAAAAAADFlxoSKY5MeL30+42QGWjLF6ivid6lF/Y5nCoiztVYAAkExhZUUcMAABvbgACmmkAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEACqMhAAAAAAAAAAADFlxoSKY5MeL30+42QGWjLF6ivid6lF/Y5nCoiztVYAAkExhZUUaoAABvbgACmmkAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", - "memo_type": "text", - "signatures": [ - "Yd9uIm+TjIcAjTU90YJoNg/r+6PU3Uss7ewUb1w3yMa+HyoSvDq8sDz/SYmDBH7F+0ACIeBF4kkVEKVBJMh0AQ==" - ], - "valid_after": "1970-01-01T00:00:00Z", - "valid_before": "2019-04-09T20:19:21Z" - } - ] - } -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no ledgers whose sequence matches the `id` argument. diff --git a/services/horizon/internal/docs/reference/endpoints/transactions-single.md b/services/horizon/internal/docs/reference/endpoints/transactions-single.md deleted file mode 100644 index 99aa29e376..0000000000 --- a/services/horizon/internal/docs/reference/endpoints/transactions-single.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -title: Transaction Details -clientData: - laboratoryUrl: https://www.stellar.org/laboratory/#explorer?resource=transactions&endpoint=single -replacement: https://developers.stellar.org/api/resources/transactions/single/ ---- - -The transaction details endpoint provides information on a single -[transaction](../resources/transaction.md). The transaction hash provided in the `hash` argument -specifies which transaction to load. - -### Warning - failed transactions - -Transaction can be successful or failed (failed transactions are also included in Stellar ledger). -Always check it's status using `successful` field! - -## Request - -``` -GET /transactions/{hash} -``` - -### Arguments - -| name | notes | description | example | -| ------ | ------- | ----------- | ------- | -| `hash` | required, string | A transaction hash, hex-encoded, lowercase. | 264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c | - -### curl Example Request - -```sh -curl "https://horizon-testnet.stellar.org/transactions/264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c" -``` - -### JavaScript Example Request - -```javascript -var StellarSdk = require('stellar-sdk'); -var server = new StellarSdk.Server('https://horizon-testnet.stellar.org'); - -server.transactions() - .transaction("264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c") - .call() - .then(function (transactionResult) { - console.log(transactionResult) - }) - .catch(function (err) { - console.log(err) - }) -``` - -## Response - -This endpoint responds with a single Transaction. See [transaction resource](../resources/transaction.md) for reference. - -### Example Response - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/transactions/264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c" - }, - "account": { - "href": "https://horizon-testnet.stellar.org/accounts/GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR" - }, - "ledger": { - "href": "https://horizon-testnet.stellar.org/ledgers/697121" - }, - "operations": { - "href": "https://horizon-testnet.stellar.org/transactions/264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c/operations{?cursor,limit,order}", - "templated": true - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/transactions/264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c/effects{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/transactions?order=asc&cursor=2994111896358912" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/transactions?order=desc&cursor=2994111896358912" - } - }, - "id": "264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c", - "paging_token": "2994111896358912", - "successful": true, - "hash": "264226cb06af3b86299031884175155e67a02e0a8ad0b3ab3a88b409a8c09d5c", - "ledger": 697121, - "created_at": "2019-04-09T20:14:25Z", - "source_account": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", - "fee_account": "GAIH3ULLFQ4DGSECF2AR555KZ4KNDGEKN4AFI4SU2M7B43MGK3QJZNSR", - "source_account_sequence": "4660039994869", - "fee_charged": 100, - "max_fee": 100, - "operation_count": 1, - "envelope_xdr": "AAAAABB90WssODNIgi6BHveqzxTRmIpvAFRyVNM+Hm2GVuCcAAAAZAAABD0AB031AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAFIMRkFZ9gZifhRSlklQpsz/9P04Earv0dzS3MkIM1cYAAAAXSHboAAAAAAAAAAABhlbgnAAAAEA+biIjrDy8yi+SvhFElIdWGBRYlDscnSSHkPchePy2JYDJn4wvJYDBumXI7/NmttUey3+cGWbBFfnnWh1H5EoD", - "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAA=", - "result_meta_xdr": "AAAAAQAAAAIAAAADAAqjIQAAAAAAAAAAEH3Rayw4M0iCLoEe96rPFNGYim8AVHJU0z4ebYZW4JwBOLmYhGq/IAAABD0AB030AAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAqjIQAAAAAAAAAAEH3Rayw4M0iCLoEe96rPFNGYim8AVHJU0z4ebYZW4JwBOLmYhGq/IAAABD0AB031AAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAwAAAAMACqMhAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnAE4uZiEar8gAAAEPQAHTfUAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEACqMhAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnAE4uYE789cgAAAEPQAHTfUAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAACqMhAAAAAAAAAAAUgxGQVn2BmJ+FFKWSVCmzP/0/TgRqu/R3NLcyQgzVxgAAABdIdugAAAqjIQAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", - "fee_meta_xdr": "AAAAAgAAAAMACqMgAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnAE4uZiEar+EAAAEPQAHTfQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEACqMhAAAAAAAAAAAQfdFrLDgzSIIugR73qs8U0ZiKbwBUclTTPh5thlbgnAE4uZiEar8gAAAEPQAHTfQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", - "memo_type": "none", - "signatures": [ - "Pm4iI6w8vMovkr4RRJSHVhgUWJQ7HJ0kh5D3IXj8tiWAyZ+MLyWAwbplyO/zZrbVHst/nBlmwRX551odR+RKAw==" - ] -} -``` - -## Possible Errors - -- The [standard errors](../errors.md#Standard-Errors). -- [not_found](../errors/not-found.md): A `not_found` error will be returned if there is no - transaction whose ID matches the `hash` argument. diff --git a/services/horizon/internal/docs/reference/errors.md b/services/horizon/internal/docs/reference/errors.md deleted file mode 100644 index f95a922a4e..0000000000 --- a/services/horizon/internal/docs/reference/errors.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -title: Errors -replacement: https://developers.stellar.org/api/errors/ ---- - -In the event that an error occurs while processing a request to horizon, an -**error** response will be returned to the client. This error response will -contain information detailing why the request couldn't complete successfully. - -Like HAL for successful responses, horizon uses a standard to specify how we -communicate errors to the client. Specifically, horizon uses the [Problem -Details for HTTP APIs](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00) draft specification. The specification is short, so we recommend -you read it. In summary, when an error occurs on the server we respond with a -json document with the following attributes: - -| name | type | description | -| -------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| type | url | The identifier for the error, expressed as a url. Visiting the url in a web browser will redirect you to the additional documentation for the problem. | -| title | string | A short title describing the error. | -| status | number | An HTTP status code that maps to the error. An error that is triggered due to client input will be in the 400-499 range of status code, for example. | -| detail | string | A longer description of the error meant the further explain the error to developers. | -| instance | string | A token that uniquely identifies this request. Allows server administrators to correlate a client report with server log files | - - -## Standard Errors - -There are a set of errors that can occur in any request to horizon which we -call **standard errors**. These errors are: - -- [Server Error](../reference/errors/server-error.md) -- [Rate Limit Exceeded](../reference/errors/rate-limit-exceeded.md) -- [Forbidden](../reference/errors/forbidden.md) diff --git a/services/horizon/internal/docs/reference/errors/bad-request.md b/services/horizon/internal/docs/reference/errors/bad-request.md deleted file mode 100644 index 56cf369136..0000000000 --- a/services/horizon/internal/docs/reference/errors/bad-request.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Bad Request -replacement: https://developers.stellar.org/api/errors/http-status-codes/standard/ ---- - -If Horizon cannot understand a request due to invalid parameters, it will return a `bad_request` -error. This is analogous to the -[HTTP 400 Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes). - -If you are encountering this error, check the `invalid_field` attribute on the `extras` object to -see what field is triggering the error. - -## Attributes - -As with all errors Horizon returns, `bad_request` follows the -[Problem Details for HTTP APIs](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00) -draft specification guide and thus has the following attributes: - -| Attribute | Type | Description | -| ----------- | ------ | ------------------------------------------------------------------------------- | -| `type` | URL | The identifier for the error. This is a URL that can be visited in the browser.| -| `title` | String | A short title describing the error. | -| `status` | Number | An HTTP status code that maps to the error. | -| `detail` | String | A more detailed description of the error. | - -## Example - -```shell -$ curl -X GET "https://horizon-testnet.stellar.org/ledgers?limit=invalidlimit" -{ - "type": "https://stellar.org/horizon-errors/bad_request", - "title": "Bad Request", - "status": 400, - "detail": "The request you sent was invalid in some way", - "extras": { - "invalid_field": "limit", - "reason": "unparseable value" - } -} -``` - -## Related - -- [Malformed Transaction](./transaction-malformed.md) diff --git a/services/horizon/internal/docs/reference/errors/before-history.md b/services/horizon/internal/docs/reference/errors/before-history.md deleted file mode 100644 index 2e1dd7cc68..0000000000 --- a/services/horizon/internal/docs/reference/errors/before-history.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Before History -replacement: https://developers.stellar.org/api/errors/http-status-codes/horizon-specific/ ---- - -A horizon server may be configured to only keep a portion of the stellar network's history stored -within its database. This error will be returned when a client requests a piece of information -(such as a page of transactions or a single operation) that the server can positively identify as -falling outside the range of recorded history. - -This error returns a -[HTTP 410 Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes). - -## Attributes - -As with all errors Horizon returns, `before_history` follows the -[Problem Details for HTTP APIs](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00) -draft specification guide and thus has the following attributes: - -| Attribute | Type | Description | -| ----------- | ------ | ------------------------------------------------------------------------------- | -| `type` | URL | The identifier for the error. This is a URL that can be visited in the browser.| -| `title` | String | A short title describing the error. | -| `status` | Number | An HTTP status code that maps to the error. | -| `detail` | String | A more detailed description of the error. | - -## Example - -```shell -$ curl -X GET "https://horizon-testnet.stellar.org/transactions?cursor=1&order=desc" -{ - "type": "https://stellar.org/horizon-errors/before_history", - "title": "Data Requested Is Before Recorded History", - "status": 410, - "detail": "This horizon instance is configured to only track a portion of the stellar network's latest history. This request is asking for results prior to the recorded history known to this horizon instance." -} -``` - -## Related - -- [Not Found](./not-found.md) diff --git a/services/horizon/internal/docs/reference/errors/not-acceptable.md b/services/horizon/internal/docs/reference/errors/not-acceptable.md deleted file mode 100644 index d58305492a..0000000000 --- a/services/horizon/internal/docs/reference/errors/not-acceptable.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Not Acceptable -replacement: https://developers.stellar.org/api/errors/http-status-codes/standard/ ---- - -When your client only accepts certain formats of data from Horizon and Horizon cannot fulfill that -request, Horizon will return a `not_acceptable` error. This is analogous to a -[HTTP 406 Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes). - -For example, if your client only accepts an XML response (`Accept: application/xml`), Horizon will -respond with a `not_acceptable` error. - -If you are encountering this error, please check to make sure the criteria for content you’ll -accept is correct. - -## Attributes - -As with all errors Horizon returns, `not_acceptable` follows the -[Problem Details for HTTP APIs](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00) -draft specification guide and thus has the following attributes: - -| Attribute | Type | Description | -| ----------- | ------ | ------------------------------------------------------------------------------- | -| `type` | URL | The identifier for the error. This is a URL that can be visited in the browser.| -| `title` | String | A short title describing the error. | -| `status` | Number | An HTTP status code that maps to the error. | -| `detail` | String | A more detailed description of the error. | - -## Example - -```shell -$ curl -X GET -H "Accept: application/xml" "https://horizon-testnet.stellar.org/accounts/GALWEV6GY73RJ255JC7XUOZ2L7WZ5JJDTKATB2MUK7F3S67DVT2A6R5G" -{ - "type": "https://stellar.org/horizon-errors/not_acceptable", - "title": "An acceptable response content-type could not be provided for this request", - "status": 406 -} -``` - -## Related - -- [Not Found](./not-found.md) diff --git a/services/horizon/internal/docs/reference/errors/not-found.md b/services/horizon/internal/docs/reference/errors/not-found.md deleted file mode 100644 index f431b5eef2..0000000000 --- a/services/horizon/internal/docs/reference/errors/not-found.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Not Found -replacement: https://developers.stellar.org/api/errors/http-status-codes/standard/ ---- - -When Horizon can't find whatever data you are requesting, it will return a `not_found` error. This -is similar to a -[HTTP 404 Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes) error -response. - -Incorrect URL path parameters or missing data are the common reasons for this error. If you -navigate using a link from a valid response, you should never receive this error message. - -## Attributes - -As with all errors Horizon returns, `not_found` follows the -[Problem Details for HTTP APIs](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00) -draft specification guide and thus has the following attributes: - -| Attribute | Type | Description | -| ----------- | ------ | ------------------------------------------------------------------------------- | -| `type` | URL | The identifier for the error. This is a URL that can be visited in the browser.| -| `title` | String | A short title describing the error. | -| `status` | Number | An HTTP status code that maps to the error. | -| `detail` | String | A more detailed description of the error. | - -## Example - -```shell -$ curl -X GET "https://horizon-testnet.stellar.org/accounts/accountthatdoesntexist" -{ - "type": "https://stellar.org/horizon-errors/bad_request", - "title": "Bad Request", - "status": 400, - "detail": "The request you sent was invalid in some way", - "extras": { - "invalid_field": "account_id", - "reason": "invalid address" - } -} -``` - -## Related - -- [Not Acceptable](./not-acceptable.md) diff --git a/services/horizon/internal/docs/reference/errors/not-implemented.md b/services/horizon/internal/docs/reference/errors/not-implemented.md deleted file mode 100644 index 41d1852ede..0000000000 --- a/services/horizon/internal/docs/reference/errors/not-implemented.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Not Implemented -replacement: https://developers.stellar.org/api/errors/http-status-codes/standard/ ---- - -If your [request method](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html) is not supported by -Horizon, Horizon will return a `not_implemented` error. Likewise, if functionality that is intended -but does not exist (thus reserving the endpoint for future use), it will also return a -`not_implemented` error. This is analogous to a -[HTTP 501 Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes). - -If you are encountering this error, Horizon does not have the functionality you are requesting. - -## Attributes - -As with all errors Horizon returns, `not_implemented` follows the -[Problem Details for HTTP APIs](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00) -draft specification guide and thus has the following attributes: - -| Attribute | Type | Description | -| ----------- | ------ | ------------------------------------------------------------------------------- | -| `type` | URL | The identifier for the error. This is a URL that can be visited in the browser.| -| `title` | String | A short title describing the error. | -| `status` | Number | An HTTP status code that maps to the error. | -| `detail` | String | A more detailed description of the error. | - -## Example - -```shell -$ curl -X GET "https://horizon-testnet.stellar.org/offers/1234" -{ - "type": "https://stellar.org/horizon-errors/not_implemented", - "title": "Resource Not Yet Implemented", - "status": 404, - "detail": "While the requested URL is expected to eventually point to a valid resource, the work to implement the resource has not yet been completed." -} -``` - -## Related - -- [Server Error](./server-error.md) diff --git a/services/horizon/internal/docs/reference/errors/rate-limit-exceeded.md b/services/horizon/internal/docs/reference/errors/rate-limit-exceeded.md deleted file mode 100644 index 1a6399466e..0000000000 --- a/services/horizon/internal/docs/reference/errors/rate-limit-exceeded.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Rate Limit Exceeded -replacement: https://developers.stellar.org/api/errors/http-status-codes/standard/ ---- - -When a single user makes too many requests to Horizon in a one hour time frame, Horizon returns a -`rate_limit_exceeded` error. By default, Horizon allows 3600 requests per hour -- an average of one -request per second. This is analogous to a -[HTTP 429 Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes). - -If you are encountering this error, please reduce your request speed. Here are some strategies for -doing so: -* For collection endpoints, try specifying larger page sizes. -* Try streaming responses to watch for new data instead of pulling data every time. -* Cache immutable data, such as transaction details, locally. - -See the [Rate Limiting Guide](../../reference/rate-limiting.md) for more info. - -## Attributes - -As with all errors Horizon returns, `rate_limit_exceeded` follows the -[Problem Details for HTTP APIs](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00) -draft specification guide and thus has the following attributes: - -| Attribute | Type | Description | -| ----------- | ------ | ------------------------------------------------------------------------------- | -| `type` | URL | The identifier for the error. This is a URL that can be visited in the browser.| -| `title` | String | A short title describing the error. | -| `status` | Number | An HTTP status code that maps to the error. | -| `detail` | String | A more detailed description of the error. | - -## Example - -```json -{ - "type": "https://stellar.org/horizon-errors/rate_limit_exceeded", - "title": "Rate Limit Exceeded", - "status": 429, - "details": "The rate limit for the requesting IP address is over its alloted limit. The allowed limit and requests left per time period are communicated to clients via the http response headers 'X-RateLimit-*' headers." -} -``` diff --git a/services/horizon/internal/docs/reference/errors/server-error.md b/services/horizon/internal/docs/reference/errors/server-error.md deleted file mode 100644 index 7fe88bef91..0000000000 --- a/services/horizon/internal/docs/reference/errors/server-error.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: Internal Server Error -replacement: https://developers.stellar.org/api/errors/http-status-codes/standard/ ---- - -If there's an internal error within Horizon, Horizon will return a -`server_error` response. This response is a catch-all, and can refer to many -possible errors in the Horizon server: a configuration mistake, a database -connection error, etc. This is analogous to a -[HTTP 500 Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes). - -Horizon does not expose information such as stack traces or raw error messages -to a client, as doing so may reveal sensitive configuration data such as secret -keys. If you are encountering this error on a server you control, please check the -Horizon log files for more details. The logs should contain detailed -information to help you discover the root issue. - -If you are encountering this error on the public Stellar infrastructure, please -report an error on [Horizon's issue tracker](https://github.com/stellar/go/issues) -and include as much information about the request that triggered the response -as you can (especially the time of the request). - -## Attributes - -As with all errors Horizon returns, `server_error` follows the -[Problem Details for HTTP APIs](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00) -draft specification guide and thus has the following attributes: - -| Attribute | Type | Description | -| ----------- | ------ | ------------------------------------------------------------------------------- | -| `type` | URL | The identifier for the error. This is a URL that can be visited in the browser.| -| `title` | String | A short title describing the error. | -| `status` | Number | An HTTP status code that maps to the error. | -| `detail` | String | A more detailed description of the error. | - -## Examples -```json -{ - "type": "https://stellar.org/horizon-errors/server_error", - "title": "Internal Server Error", - "status": 500, - "details": "An error occurred while processing this request. This is usually due to a bug within the server software. Trying this request again may succeed if the bug is transient, otherwise please report this issue to the issue tracker at: https://github.com/stellar/go/issues. Please include this response in your issue." -} -``` - -## Related - -- [Not Implemented](./not-implemented.md) diff --git a/services/horizon/internal/docs/reference/errors/stale-history.md b/services/horizon/internal/docs/reference/errors/stale-history.md deleted file mode 100644 index 041ef11723..0000000000 --- a/services/horizon/internal/docs/reference/errors/stale-history.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: Stale History -replacement: https://developers.stellar.org/api/errors/http-status-codes/horizon-specific/ ---- - -A horizon server may be configured to reject historical requests when the history is known to be -further out of date than the configured threshold. In such cases, this error is returned. To -resolve this error (provided you are the horizon instance's operator) please ensure that the -ingestion system is running correctly and importing new ledgers. This error returns a -[HTTP 503 Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes). - -## Attributes - -As with all errors Horizon returns, `stale_history` follows the -[Problem Details for HTTP APIs](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00) -draft specification guide and thus has the following attributes: - -| Attribute | Type | Description | -| ----------- | ------ | ------------------------------------------------------------------------------- | -| `type` | URL | The identifier for the error. This is a URL that can be visited in the browser.| -| `title` | String | A short title describing the error. | -| `status` | Number | An HTTP status code that maps to the error. | -| `detail` | String | A more detailed description of the error. | - -## Example - -```json -{ - - "type": "https://stellar.org/horizon-errors/stale_history", - "title": "Historical DB Is Too Stale", - "status": 503, - "detail": "This horizon instance is configured to reject client requests when it can determine that the history database is lagging too far behind the connected instance of stellar-core. If you operate this server, please ensure that the ingestion system is properly running." -} -``` - -## Related - -- [Internal Server Error](./server-error.md) diff --git a/services/horizon/internal/docs/reference/errors/timeout.md b/services/horizon/internal/docs/reference/errors/timeout.md deleted file mode 100644 index dca8cf4d64..0000000000 --- a/services/horizon/internal/docs/reference/errors/timeout.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Timeout -replacement: https://developers.stellar.org/api/errors/http-status-codes/horizon-specific/ ---- - -If you are encountering this error it means that either: - -* Horizon has not received a confirmation from the Stellar Core server that the transaction you are - trying to submit to the network was included in a ledger in a timely manner. -* Horizon has not sent a response to a reverse-proxy before a specified amount of time has elapsed. - -The former case may happen because there was no room for your transaction for 3 consecutive -ledgers. This is because Stellar Core removes each submitted transaction from a queue. To solve -this you can: - -* Keep resubmitting the same transaction (with the same sequence number) and wait until it finally - is added to a new ledger. -* Increase the [fee](../../../guides/concepts/fees.md) in order to prioritize the transaction. - -This error returns a -[HTTP 504 Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes). - -## Attributes - -As with all errors Horizon returns, `timeout` follows the -[Problem Details for HTTP APIs](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00) -draft specification guide and thus has the following attributes: - -| Attribute | Type | Description | -| ----------- | ------ | ------------------------------------------------------------------------------- | -| `type` | URL | The identifier for the error. This is a URL that can be visited in the browser.| -| `title` | String | A short title describing the error. | -| `status` | Number | An HTTP status code that maps to the error. | -| `detail` | String | A more detailed description of the error. | - -## Example -```json -{ - "type": "https://stellar.org/horizon-errors/timeout", - "title": "Timeout", - "status": 504, - "detail": "Your request timed out before completing. Please try your request again. If you are submitting a transaction make sure you are sending exactly the same transaction (with the same sequence number)." -} -``` - -## Related - -- [Not Acceptable](./not-acceptable.md) -- [Transaction Failed](./transaction-failed.md) diff --git a/services/horizon/internal/docs/reference/errors/transaction-failed.md b/services/horizon/internal/docs/reference/errors/transaction-failed.md deleted file mode 100644 index fa84178d3a..0000000000 --- a/services/horizon/internal/docs/reference/errors/transaction-failed.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Transaction Failed -replacement: https://developers.stellar.org/api/errors/http-status-codes/horizon-specific/ ---- - -The `transaction_failed` error occurs when a client submits a transaction that was well-formed but -was not included into the ledger due to some other failure. For example, a transaction may fail if: - -- The source account for transaction cannot pay the minimum fee. -- The sequence number is incorrect. -- One of the contained operations has failed such as a payment operation that overdraws on the - paying account. - -In almost every case, this error indicates that the transaction submitted in the initial request -will never succeed. There is one exception: a transaction that fails with the `tx_bad_seq` result -code (as expressed in the `result_code` field of the error) may become valid in the future if the -sequence number it used was too high. - -This error returns a -[HTTP 400 Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes). - -## Attributes - -As with all errors Horizon returns, `transaction_failed` follows the -[Problem Details for HTTP APIs](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00) -draft specification guide and thus has the following attributes: - -| Attribute | Type | Description | -| ----------- | ------ | ------------------------------------------------------------------------------- | -| `type` | URL | The identifier for the error. This is a URL that can be visited in the browser.| -| `title` | String | A short title describing the error. | -| `status` | Number | An HTTP status code that maps to the error. | -| `detail` | String | A more detailed description of the error. | - -In addition, the following additional data is provided in the `extras` field of the error: - -| Attribute | Type | Description | -|----------------------------|--------|-----------------------------------------------------------------------------------------------------------------------------| -| `envelope_xdr` | String | A base64-encoded representation of the TransactionEnvelope XDR whose failure triggered this response. | -| `result_xdr` | String | A base64-encoded representation of the TransactionResult XDR returned by stellar-core when submitting this transaction. | -| `result_codes.transaction` | String | The transaction result code returned by Stellar Core. | -| `result_codes.operations` | Array | An array of strings, representing the operation result codes for each operation in the submitted transaction, if available. | - - -## Examples - -### No Source Account -```json -{ - "type": "https://stellar.org/horizon-errors/transaction_failed", - "title": "Transaction Failed", - "status": 400, - "detail": "The transaction failed when submitted to the stellar network. The `extras.result_codes` field on this response contains further details. Descriptions of each code can be found at: https://www.stellar.org/developers/learn/concepts/list-of-operations.html", - "extras": { - "envelope_xdr": "AAAAANNVpdQ9vctZdAJ67sFmNe1KDzaj51dAdkW3vKKM51H3AAAAZAAAAABJlgLSAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAA01Wl1D29y1l0AnruwWY17UoPNqPnV0B2Rbe8ooznUfcAAAAAAAAAAAL68IAAAAAAAAAAAA==", - "result_codes": { - "transaction": "tx_no_source_account" - }, - "result_xdr": "AAAAAAAAAAD////4AAAAAA==" - } -} -``` - -### Bad Authentication -```json -{ - "type": "https://stellar.org/horizon-errors/transaction_failed", - "title": "Transaction Failed", - "status": 400, - "detail": "The transaction failed when submitted to the stellar network. The `extras.result_codes` field on this response contains further details. Descriptions of each code can be found at: https://www.stellar.org/developers/learn/concepts/list-of-operations.html", - "extras": { - "envelope_xdr": "AAAAAPORy3CoX6ox2ilbeiVjBA5WlpCSZRcjZ7VE9Wf4QVk7AAAAZAAAQz0AAAACAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAA85HLcKhfqjHaKVt6JWMEDlaWkJJlFyNntUT1Z/hBWTsAAAAAAAAAAAL68IAAAAAAAAAAARN17BEAAABAA9Ad7OKc7y60NT/JuobaHOfmuq8KbZqcV6G/es94u9yT84fi0aI7tJsFMOyy8cZ4meY3Nn908OU+KfRWV40UCw==", - "result_codes": { - "transaction": "tx_bad_auth" - }, - "result_xdr": "AAAAAAAAAGT////6AAAAAA==" - } -} -``` - -## Related - -- [Transaction Malformed](./transaction-malformed.md) diff --git a/services/horizon/internal/docs/reference/errors/transaction-malformed.md b/services/horizon/internal/docs/reference/errors/transaction-malformed.md deleted file mode 100644 index 24983047c0..0000000000 --- a/services/horizon/internal/docs/reference/errors/transaction-malformed.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Transaction Malformed -replacement: https://developers.stellar.org/api/errors/http-status-codes/horizon-specific/ ---- - -When you submit a malformed transaction to Horizon, Horizon will return a `transaction_malformed` -error. There are many ways in which a transaction could be malformed, including: - -- You submitted an empty string. -- Your base64-encoded string is invalid. -- Your [XDR](../xdr.md) structure is invalid. -- You have leftover bytes in your [XDR](../xdr.md) structure. - -If you are encountering this error, please check the contents of the transaction you are -submitting. This error returns a -[HTTP 400 Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Response_codes). - -## Attributes - -As with all errors Horizon returns, `transaction_malformed` follows the -[Problem Details for HTTP APIs](https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00) -draft specification guide and thus has the following attributes: - -| Attribute | Type | Description | -| ----------- | ------ | ------------------------------------------------------------------------------- | -| `type` | URL | The identifier for the error. This is a URL that can be visited in the browser.| -| `title` | String | A short title describing the error. | -| `status` | Number | An HTTP status code that maps to the error. | -| `detail` | String | A more detailed description of the error. | - -In addition, the following additional data is provided in the `extras` field of the error: - -| Attribute | Type | Description | -|----------------|--------|----------------------------------------------------| -| `envelope_xdr` | String | The submitted data that was malformed in some way. | - -## Example - -```json -{ - "type": "https://stellar.org/horizon-errors/transaction_malformed", - "title": "Transaction Malformed", - "status": 400, - "detail": "Horizon could not decode the transaction envelope in this request. A transaction should be an XDR TransactionEnvelope struct encoded using base64. The envelope read from this request is echoed in the `extras.envelope_xdr` field of this response for your convenience.", - "extras": { - "envelope_xdr": "BBBBBPORy3CoX6ox2ilbeiVjBA5WlpCSZRcjZ7VE9Wf4QVk7AAAAZAAAQz0AAAACAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAA85HLcKhfqjHaKVt6JWMEDlaWkJJlFyNntUT1Z/hBWTsAAAAAAAAAAAL68IAAAAAAAAAAARN17BEAAABAA9Ad7OKc7y60NT/JuobaHOfmuq8KbZqcV6G/es94u9yT84fi0aI7tJsFMOyy8cZ4meY3Nn908OU+KfRWV40UCw==" - } -} -``` - -## Related - -- [Bad Request](./bad-request.md) diff --git a/services/horizon/internal/docs/reference/paging.md b/services/horizon/internal/docs/reference/paging.md deleted file mode 100644 index d2c8db0dda..0000000000 --- a/services/horizon/internal/docs/reference/paging.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Paging -replacement: https://developers.stellar.org/api/introduction/pagination/ ---- - -The Stellar network contains a lot of data and it would be infeasible to return it all at once. The paging system allows -a user to request a "page" of data containing only a limited number of results. Then, the user can use the paging system -to request results adjacent to the current page where they left off at. - -Read about the [page resource](../reference/resources/page.md) for information on the paging system's usage and representation. - - diff --git a/services/horizon/internal/docs/reference/rate-limiting.md b/services/horizon/internal/docs/reference/rate-limiting.md deleted file mode 100644 index 642412fbed..0000000000 --- a/services/horizon/internal/docs/reference/rate-limiting.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Rate Limiting -replacement: https://developers.stellar.org/api/introduction/rate-limiting/ ---- - -In order to provide service stability, Horizon limits the number of requests a -client (single IP) can perform within a one hour window. By default this is set to 3600 -requests per hour—an average of one request per second. Also, while streaming -every update of the stream (what happens every time there's a new ledger) is -counted. Ex. if there were 12 new ledgers in a minute, 12 requests will be -subtracted from the limit. - -Horizon is using [GCRA](https://brandur.org/rate-limiting#gcra) algorithm. - -## Response headers for rate limiting - -Every response from Horizon sets advisory headers to inform clients of their -standing with rate limiting system: - -| Header | Description | -| ----------------------- | ------------------------------------------------------------------------ | -| `X-RateLimit-Limit` | The maximum number of requests that the current client can make in one hour. | -| `X-RateLimit-Remaining` | The number of remaining requests for the current window. | -| `X-RateLimit-Reset` | Seconds until a new window starts. | - -In addition, a `Retry-After` header will be set when the current client is being -throttled. diff --git a/services/horizon/internal/docs/reference/readme.md b/services/horizon/internal/docs/reference/readme.md deleted file mode 100644 index 0e4f4cbc32..0000000000 --- a/services/horizon/internal/docs/reference/readme.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Overview ---- - -Horizon is an API server for the Stellar ecosystem. It acts as the interface between [stellar-core](https://github.com/stellar/stellar-core) and applications that want to access the Stellar network. It allows you to submit transactions to the network, check the status of accounts, subscribe to event streams, etc. See [an overview of the Stellar ecosystem](https://www.stellar.org/developers/guides/) for details of where Horizon fits in. - -Horizon provides a RESTful API to allow client applications to interact with the Stellar network. You can communicate with Horizon using cURL or just your web browser. However, if you're building a client application, you'll likely want to use a Stellar SDK in the language of your client. -SDF provides a [JavaScript SDK](https://www.stellar.org/developers/js-stellar-sdk/reference/index.html) for clients to use to interact with Horizon. - -SDF runs a instance of Horizon that is connected to the test net: [https://horizon-testnet.stellar.org/](https://horizon-testnet.stellar.org/) and one that is connected to the public Stellar network: -[https://horizon.stellar.org/](https://horizon.stellar.org/). - -## Libraries - -SDF maintained libraries:
-- [JavaScript](https://github.com/stellar/js-stellar-sdk) -- [Go](https://github.com/stellar/go/tree/master/clients/horizonclient) -- [Java](https://github.com/stellar/java-stellar-sdk) - -Community maintained libraries for interacting with Horizon in other languages:
-- [Python](https://github.com/StellarCN/py-stellar-base) -- [C# .NET Core 2.x](https://github.com/elucidsoft/dotnetcore-stellar-sdk) -- [Ruby](https://github.com/astroband/ruby-stellar-sdk) -- [iOS and macOS](https://github.com/Soneso/stellar-ios-mac-sdk) -- [Scala SDK](https://github.com/synesso/scala-stellar-sdk) -- [C++ SDK](https://github.com/bnogalm/StellarQtSDK) diff --git a/services/horizon/internal/docs/reference/resources/account.md b/services/horizon/internal/docs/reference/resources/account.md deleted file mode 100644 index 8e90e78f77..0000000000 --- a/services/horizon/internal/docs/reference/resources/account.md +++ /dev/null @@ -1,199 +0,0 @@ ---- -title: Account -replacement: https://developers.stellar.org/api/resources/accounts/ ---- - -In the Stellar network, users interact using **accounts** which can be controlled by a corresponding keypair that can authorize transactions. One can create a new account with the [Create Account](./operation.md#create-account) operation. - -To learn more about the concept of accounts in the Stellar network, take a look at the [Stellar account concept guide](https://www.stellar.org/developers/learn/concepts/accounts.html). - -When horizon returns information about an account it uses the following format: - -## Attributes -| Attribute | Type | Description | -|----------------|------------------|------------------------------------------------------------------------------------------------------------------------ | -| id | string | The canonical id of this account, suitable for use as the :id parameter for url templates that require an account's ID. | -| account_id | string | The account's public key encoded into a base32 string representation. | -| sequence | number | The current sequence number that can be used when submitting a transaction from this account. | -| subentry_count | number | The number of [account subentries](https://www.stellar.org/developers/guides/concepts/ledger.html#ledger-entries). | -| balances | array of objects | An array of the native asset or credits this account holds. | -| thresholds | object | An object of account thresholds. | -| flags | object | The flags denote the enabling/disabling of certain asset issuer privileges. | -| signers | array of objects | An array of [account signers](https://www.stellar.org/developers/guides/concepts/multi-sig.html#additional-signing-keys) with their weights. | -| data | object | An array of account [data](./data.md) fields. | - -### Signer Object -| Attribute | Type | Description | -|------------|--------|------------------------------------------------------------------------------------------------------------------| -| weight | number | The numerical weight of a signer, necessary to determine whether a transaction meets the threshold requirements. | -| key | string | Different depending on the type of the signer. | -| type | string | See below. | - -### Possible Signer Types -| Type | Description | -|--------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ed25519_public_key | A normal Stellar public key. | -| sha256_hash | The SHA256 hash of some arbitrary `x`. Adding a signature of this type allows anyone who knows `x` to sign a transaction from this account. *Note: Once this transaction is broadcast, `x` will be known publicly.* | -| preauth_tx | The hash of a pre-authorized transaction. This signer is automatically removed from the account when a matching transaction is properly applied. | -### Balances Object -| Attribute | Type | | -|---------------------|------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| balance | string | How much of an asset is owned. | -| buying_liabilities | string | The total amount of an asset offered to buy aggregated over all offers owned by this account. | -| selling_liabilities | string | The total amount of an asset offered to sell aggregated over all offers owned by this account. | -| limit | optional, number | The maximum amount of an asset that this account is willing to accept (this is specified when an account opens a trustline). | -| asset_type | string | Either native, credit_alphanum4, or credit_alphanum12. | -| asset_code | optional, string | The code for the asset. | -| asset_issuer | optional, string | The stellar address of the given asset's issuer. | -| is_authorized | optional, bool | The trustline status for an `auth_required` asset. If true, the issuer of the asset has granted the account permission to send, receive, buy, or sell the asset. If false, the issuer has not, so the account cannot send, receive, buy, or sell the asset. | - -### Flag Object -| Attribute | Type | | -|----------------|------|--------------------------------------------------------------------------------------------------------------------------------| -| auth_immutable | bool | With this setting, none of the following authorization flags can be changed. | -| auth_required | bool | With this setting, an anchor must approve anyone who wants to hold its asset. | -| auth_revocable | bool | With this setting, an anchor can set the authorize flag of an existing trustline to freeze the assets held by an asset holder. | - -### Threshold Object -| Attribute | Type | | -|----------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| low_threshold | number | The weight required for a valid transaction including the [Allow Trust][allow_trust] and [Bump Sequence][bump_seq] operations. | -| med_threshold | number | The weight required for a valid transaction including the [Create Account][create_acc], [Payment][payment], [Path Payment Strict Send][path_payment_send], [Path Payment Strict Receive][path_payment_receive], [Manage Buy Offer][manage_buy_offer], [Manage Sell Offer][manage_sell_offer], [Create Passive Sell Offer][passive_sell_offer], [Change Trust][change_trust], [Inflation][inflation], and [Manage Data][manage_data] operations. | -| high_threshold | number | The weight required for a valid transaction including the [Account Merge][account_merge] and [Set Options]() operations. | - -[account_merge]: https://www.stellar.org/developers/guides/concepts/list-of-operations.html#account-merge -[allow_trust]: https://www.stellar.org/developers/guides/concepts/list-of-operations.html#allow-trust -[bump_seq]: https://www.stellar.org/developers/guides/concepts/list-of-operations.html#bump-sequence -[change_trust]: https://www.stellar.org/developers/guides/concepts/list-of-operations.html#change-trust -[create_acc]: https://www.stellar.org/developers/guides/concepts/list-of-operations.html#create-account -[inflation]: https://www.stellar.org/developers/guides/concepts/list-of-operations.html#inflation -[manage_data]: https://www.stellar.org/developers/guides/concepts/list-of-operations.html#manage-data -[manage_buy_offer]: https://www.stellar.org/developers/guides/concepts/list-of-operations.html#manage-buy-offer -[manage_sell_offer]: https://www.stellar.org/developers/guides/concepts/list-of-operations.html#manage-sell-offer -[passive_sell_offer]: https://www.stellar.org/developers/guides/concepts/list-of-operations.html#create-passive-sell-offer -[path_payment_receive]: https://www.stellar.org/developers/guides/concepts/list-of-operations.html#path-payment-strict-receive -[path_payment_send]: https://www.stellar.org/developers/guides/concepts/list-of-operations.html#path-payment-strict-send -[payment]: https://www.stellar.org/developers/guides/concepts/list-of-operations.html#payment -[set_options]: https://www.stellar.org/developers/guides/concepts/list-of-operations.html#set-options - -## Links -| rel | Example | Description | `templated` | -|--------------|---------------------------------------------------------------------------------------------------------|--------------------------------------------------------------|-------------| -| data | `/accounts/GAOEWNUEKXKNGB2AAOX6S6FEP6QKCFTU7KJH647XTXQXTMOAUATX2VF5/data/{key}` | [Data fields](./data.md) related to this account | true | -| effects | `/accounts/GAOEWNUEKXKNGB2AAOX6S6FEP6QKCFTU7KJH647XTXQXTMOAUATX2VF5/effects/{?cursor,limit,order}` | The [effects](./effect.md) related to this account | true | -| offers | `/accounts/GAOEWNUEKXKNGB2AAOX6S6FEP6QKCFTU7KJH647XTXQXTMOAUATX2VF5/offers/{?cursor,limit,order}` | The [offers](./offer.md) related to this account | true | -| operations | `/accounts/GAOEWNUEKXKNGB2AAOX6S6FEP6QKCFTU7KJH647XTXQXTMOAUATX2VF5/operations/{?cursor,limit,order}` | The [operations](./operation.md) related to this account | true | -| payments | `/accounts/GAOEWNUEKXKNGB2AAOX6S6FEP6QKCFTU7KJH647XTXQXTMOAUATX2VF5/payments/{?cursor,limit,order}` | The [payments](./payment.md) related to this account | true | -| trades | `/accounts/GAOEWNUEKXKNGB2AAOX6S6FEP6QKCFTU7KJH647XTXQXTMOAUATX2VF5/trades/{?cursor,limit,order}` | The [trades](./trade.md) related to this account | true | -| transactions | `/accounts/GAOEWNUEKXKNGB2AAOX6S6FEP6QKCFTU7KJH647XTXQXTMOAUATX2VF5/transactions/{?cursor,limit,order}` | The [transactions](./transaction.md) related to this account | true | - -## Example - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/accounts/GBWRID7MPYUDBTNQPEHUN4XOBVVDPJOHYXAVW3UTOD2RG7BDAY6O3PHW" - }, - "transactions": { - "href": "https://horizon-testnet.stellar.org/accounts/GBWRID7MPYUDBTNQPEHUN4XOBVVDPJOHYXAVW3UTOD2RG7BDAY6O3PHW/transactions{?cursor,limit,order}", - "templated": true - }, - "operations": { - "href": "https://horizon-testnet.stellar.org/accounts/GBWRID7MPYUDBTNQPEHUN4XOBVVDPJOHYXAVW3UTOD2RG7BDAY6O3PHW/operations{?cursor,limit,order}", - "templated": true - }, - "payments": { - "href": "https://horizon-testnet.stellar.org/accounts/GBWRID7MPYUDBTNQPEHUN4XOBVVDPJOHYXAVW3UTOD2RG7BDAY6O3PHW/payments{?cursor,limit,order}", - "templated": true - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/accounts/GBWRID7MPYUDBTNQPEHUN4XOBVVDPJOHYXAVW3UTOD2RG7BDAY6O3PHW/effects{?cursor,limit,order}", - "templated": true - }, - "offers": { - "href": "https://horizon-testnet.stellar.org/accounts/GBWRID7MPYUDBTNQPEHUN4XOBVVDPJOHYXAVW3UTOD2RG7BDAY6O3PHW/offers{?cursor,limit,order}", - "templated": true - }, - "trades": { - "href": "https://horizon-testnet.stellar.org/accounts/GBWRID7MPYUDBTNQPEHUN4XOBVVDPJOHYXAVW3UTOD2RG7BDAY6O3PHW/trades{?cursor,limit,order}", - "templated": true - }, - "data": { - "href": "https://horizon-testnet.stellar.org/accounts/GBWRID7MPYUDBTNQPEHUN4XOBVVDPJOHYXAVW3UTOD2RG7BDAY6O3PHW/data/{key}", - "templated": true - } - }, - "id": "GBWRID7MPYUDBTNQPEHUN4XOBVVDPJOHYXAVW3UTOD2RG7BDAY6O3PHW", - "paging_token": "", - "account_id": "GBWRID7MPYUDBTNQPEHUN4XOBVVDPJOHYXAVW3UTOD2RG7BDAY6O3PHW", - "sequence": "43692723777044483", - "subentry_count": 3, - "thresholds": { - "low_threshold": 0, - "med_threshold": 0, - "high_threshold": 0 - }, - "flags": { - "auth_required": false, - "auth_revocable": false, - "auth_immutable": false - }, - "balances": [ - { - "balance": "1000000.0000000", - "limit": "922337203685.4775807", - "buying_liabilities": "0.0000000", - "selling_liabilities": "0.0000000", - "last_modified_ledger": 632070, - "asset_type": "credit_alphanum4", - "asset_code": "FOO", - "asset_issuer": "GAGLYFZJMN5HEULSTH5CIGPOPAVUYPG5YSWIYDJMAPIECYEBPM2TA3QR" - }, - { - "balance": "10000.0000000", - "buying_liabilities": "0.0000000", - "selling_liabilities": "0.0000000", - "asset_type": "native" - } - ], - "signers": [ - { - "public_key": "GDLEPBJBC2VSKJCLJB264F2WDK63X4NKOG774A3QWVH2U6PERGDPUCS4", - "weight": 1, - "key": "GDLEPBJBC2VSKJCLJB264F2WDK63X4NKOG774A3QWVH2U6PERGDPUCS4", - "type": "ed25519_public_key" - }, - { - "public_key": "XCPNCUKYDHPMMH6TMHK73K5VP5A6ZTQ2L7Q74JR3TDANNFB3TMRS5OKG", - "weight": 1, - "key": "XCPNCUKYDHPMMH6TMHK73K5VP5A6ZTQ2L7Q74JR3TDANNFB3TMRS5OKG", - "type": "sha256_hash" - }, - { - "public_key": "TABGGIW6EXOVOSNJ2O27U2DUX7RWHSRBGOKQLGYDTOXPANEX6LXBX7O7", - "weight": 1, - "key": "TABGGIW6EXOVOSNJ2O27U2DUX7RWHSRBGOKQLGYDTOXPANEX6LXBX7O7", - "type": "preauth_tx" - }, - { - "public_key": "GBWRID7MPYUDBTNQPEHUN4XOBVVDPJOHYXAVW3UTOD2RG7BDAY6O3PHW", - "weight": 1, - "key": "GBWRID7MPYUDBTNQPEHUN4XOBVVDPJOHYXAVW3UTOD2RG7BDAY6O3PHW", - "type": "ed25519_public_key" - } - ], - "data": {} -} -``` - -## Endpoints -| Resource | Type | Resource URI Template | -|------------------------------------------------------------------|------------|--------------------------------------| -| [Account Details](../endpoints/accounts-single.md) | Single | `/accounts/:id` | -| [Account Data](../endpoints/data-for-account.md) | Single | `/accounts/:id/data/:key` | -| [Account Transactions](../endpoints/transactions-for-account.md) | Collection | `/accounts/:account_id/transactions` | -| [Account Operations](../endpoints/operations-for-account.md) | Collection | `/accounts/:account_id/operations` | -| [Account Payments](../endpoints/payments-for-account.md) | Collection | `/accounts/:account_id/payments` | -| [Account Effects](../endpoints/effects-for-account.md) | Collection | `/accounts/:account_id/effects` | -| [Account Offers](../endpoints/offers-for-account.md) | Collection | `/accounts/:account_id/offers` | diff --git a/services/horizon/internal/docs/reference/resources/asset.md b/services/horizon/internal/docs/reference/resources/asset.md deleted file mode 100644 index 730b0de73a..0000000000 --- a/services/horizon/internal/docs/reference/resources/asset.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Asset -replacement: https://developers.stellar.org/api/resources/assets/ ---- - -**Assets** are the units that are traded on the Stellar Network. - -An asset consists of an type, code, and issuer. - -To learn more about the concept of assets in the Stellar network, take a look at the [Stellar assets concept guide](https://www.stellar.org/developers/guides/concepts/assets.html). - -## Attributes - -| Attribute | Type | | -| ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | -| asset_type | string | The type of this asset: "credit_alphanum4", or "credit_alphanum12". | -| asset_code | string | The code of this asset. | -| asset_issuer | string | The issuer of this asset. | -| accounts | object | The number of accounts and claimable balances holding this asset. Accounts are summarized by each state of the trust line flags. | -| balances | object | The number of units of credit issued, summarized by each state of the trust line flags, or if they are in a claimable balance. | -| flags | object | The flags denote the enabling/disabling of certain asset issuer privileges. | -| paging_token | string | A [paging token](./page.md) suitable for use as the `cursor` parameter to transaction collection resources. | - -#### Flag Object -| Attribute | Type | | -| ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | -| auth_immutable | bool | With this setting, none of the following authorization flags can be changed. | -| auth_required | bool | With this setting, an anchor must approve anyone who wants to hold its asset. | -| auth_revocable | bool | With this setting, an anchor can set the authorize flag of an existing trustline to freeze the assets held by an asset holder. | - -## Links -| rel | Example | Description -|--------------|---------------------------------------------------------------------------------------------------|------------------------------------------------------------ -| toml | `https://www.stellar.org/.well-known/stellar.toml`| Link to the TOML file for this issuer | - -## Example - -```json -{ - "_links": { - "toml": { - "href": "https://www.stellar.org/.well-known/stellar.toml" - } - }, - "asset_type": "credit_alphanum4", - "asset_code": "USD", - "asset_issuer": "GBAUUA74H4XOQYRSOW2RZUA4QL5PB37U3JS5NE3RTB2ELJVMIF5RLMAG", - "paging_token": "USD_GBAUUA74H4XOQYRSOW2RZUA4QL5PB37U3JS5NE3RTB2ELJVMIF5RLMAG_credit_alphanum4", - "accounts": { - "authorized": 91547871, - "authorized_to_maintain_liabilities": 45773935, - "unauthorized": 22886967, - "claimable_balances": 11443483 - }, - "balances": { - "authorized": "100.0000000", - "authorized_to_maintain_liabilities": "50.0000000", - "unauthorized": "25.0000000", - "claimable_balances": "12.5000000" - }, - "flags": { - "auth_required": false, - "auth_revocable": false - } -} -``` - -## Endpoints - -| Resource | Type | Resource URI Template | -| ---------------------------------------- | ---------- | ---------------------------- | -| [All Assets](../endpoints/assets-all.md) | Collection | `/assets` (`GET`) | diff --git a/services/horizon/internal/docs/reference/resources/data.md b/services/horizon/internal/docs/reference/resources/data.md deleted file mode 100644 index fb79d26d7b..0000000000 --- a/services/horizon/internal/docs/reference/resources/data.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Data -replacement: https://developers.stellar.org/api/resources/accounts/data/ ---- - -Each account in Stellar network can contain multiple key/value pairs associated with it. Horizon can be used to retrieve value of each data key. - -When horizon returns information about a single account data key it uses the following format: - -## Attributes - -| Attribute | Type | | -| --- | --- | --- | -| value | base64-encoded string | The base64-encoded value for the key | - -## Example - -```json -{ - "value": "MTAw" -} -``` diff --git a/services/horizon/internal/docs/reference/resources/effect.md b/services/horizon/internal/docs/reference/resources/effect.md deleted file mode 100644 index 8eb2c29452..0000000000 --- a/services/horizon/internal/docs/reference/resources/effect.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: Effect -replacement: https://developers.stellar.org/api/resources/effects/ ---- - -A successful operation will yield zero or more **effects**. These effects -represent specific changes that occur in the ledger, but are not necessarily -directly reflected in the [ledger](https://www.stellar.org/developers/learn/concepts/ledger.html) or [history](https://github.com/stellar/stellar-core/blob/master/docs/history.md), as [transactions](https://www.stellar.org/developers/learn/concepts/transactions.html) and [operations](https://www.stellar.org/developers/learn/concepts/operations.html) are. - -## Effect types - -We can distinguish 6 effect groups: -- Account effects -- Signer effects -- Trustline effects -- Trading effects -- Data effects -- Misc effects - -### Account effects - -| Type | Operation | -|---------------------------------------|------------------------------------------------------| -| Account Created | create_account | -| Account Removed | merge_account | -| Account Credited | create_account, payment, path_payment, merge_account | -| Account Debited | create_account, payment, path_payment, merge_account | -| Account Thresholds Updated | set_options | -| Account Home Domain Updated | set_options | -| Account Flags Updated | set_options | -| Account Inflation Destination Updated | set_options | - -### Signer effects - -| Type | Operation | -|----------------|-------------| -| Signer Created | set_options | -| Signer Removed | set_options | -| Signer Updated | set_options | - -### Trustline effects - -| Type | Operation | -|------------------------|---------------------------| -| Trustline Created | change_trust | -| Trustline Removed | change_trust | -| Trustline Updated | change_trust, allow_trust | -| Trustline Authorized | allow_trust | -| Trustline Deauthorized | allow_trust | - -### Trading effects - -| Type | Operation | -|---------------|------------------------------------------------------------------------------| -| Offer Created | manage_buy_offer, manage_sell_offer, create_passive_sell_offer | -| Offer Removed | manage_buy_offer, manage_sell_offer, create_passive_sell_offer, path_payment | -| Offer Updated | manage_buy_offer, manage_sell_offer, create_passive_sell_offer, path_payment | -| Trade | manage_buy_offer, manage_sell_offer, create_passive_sell_offer, path_payment | -### Data effects - -| Type | Operation | -|--------------|-------------| -| Data Created | manage_data | -| Data Removed | manage_data | -| Data Updated | manage_data | -### Misc effects - -| Type | Operation | -|-----------------|---------------| -| Sequence Bumped | bump_sequence | - -## Attributes - -Attributes depend on effect type. - -## Links - -| rel | Example | Relation | -|-----------|---------------------------------------------------------------|-----------------------------------| -| self | `/effects?order=asc\u0026limit=1` | | -| prev | `/effects?order=desc\u0026limit=1\u0026cursor=141733924865-1` | | -| next | `/effects?order=asc\u0026limit=1\u0026cursor=141733924865-1` | | -| operation | `/operations/141733924865` | Operation that created the effect | - -## Example - -```json -{ - "_embedded": { - "records": [ - { - "_links": { - "operation": { - "href": "/operations/141733924865" - }, - "precedes": { - "href": "/effects?cursor=141733924865-1\u0026order=asc" - }, - "succeeds": { - "href": "/effects?cursor=141733924865-1\u0026order=desc" - } - }, - "account": "GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K", - "paging_token": "141733924865-1", - "starting_balance": "10000000.0", - "type_i": 0, - "type": "account_created" - } - ] - }, - "_links": { - "next": { - "href": "/effects?order=asc\u0026limit=1\u0026cursor=141733924865-1" - }, - "prev": { - "href": "/effects?order=desc\u0026limit=1\u0026cursor=141733924865-1" - }, - "self": { - "href": "/effects?order=asc\u0026limit=1\u0026cursor=" - } - } -} -``` - -## Endpoints - -| Resource | Type | Resource URI Template | -|--------------------------------------------------------------------------------------------------------------------------------------------|------------|---------------------------------| -| [All Effects](https://github.com/stellar/go/blob/master/services/horizon/internal/docs/reference/endpoints/effects-all.md) | Collection | `/effects` | -| [Operation Effects](https://github.com/stellar/go/blob/master/services/horizon/internal/docs/reference/endpoints/effects-for-operation.md) | Collection | `/operations/:id/effects` | -| [Account Effects](https://github.com/stellar/go/blob/master/services/horizon/internal/docs/reference/endpoints/effects-for-account.md) | Collection | `/accounts/:account_id/effects` | -| [Ledger Effects](https://github.com/stellar/go/blob/master/services/horizon/internal/docs/reference/endpoints/effects-for-ledger.md) | Collection | `/ledgers/:ledger_id/effects` | diff --git a/services/horizon/internal/docs/reference/resources/ledger.md b/services/horizon/internal/docs/reference/resources/ledger.md deleted file mode 100644 index 23632cc1da..0000000000 --- a/services/horizon/internal/docs/reference/resources/ledger.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: Ledger -replacement: https://developers.stellar.org/api/resources/ledgers/ ---- - -A **ledger** resource contains information about a given ledger. - -To learn more about the concept of ledgers in the Stellar network, take a look at the [Stellar ledger concept guide](https://www.stellar.org/developers/learn/concepts/ledger.html). - -## Attributes - -| Attribute | Type | | -|------------------------------|--------|------------------------------------------------------------------------------------------------------------------------------| -| id | string | The id is a unique identifier for this ledger. | -| paging_token | number | A [paging token](./page.md) suitable for use as a `cursor` parameter. | -| hash | string | A hex-encoded, lowercase SHA-256 hash of the ledger's [XDR](../../learn/xdr.md)-encoded form. | -| prev_hash | string | The hash of the ledger that chronologically came before this one. | -| sequence | number | Sequence number of this ledger, suitable for use as the as the :id parameter for url templates that require a ledger number. | -| successful_transaction_count | number | The number of successful transactions in this ledger. | -| failed_transaction_count | number | The number of failed transactions in this ledger. | -| operation_count | number | The number of operations applied in this ledger. | -| tx_set_operation_count | number | The number of operations in this ledger. This number includes operations from failed and successful transactions. | -| closed_at | string | An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) formatted string of when this ledger was closed. | -| total_coins | string | The total number of lumens in circulation. | -| fee_pool | string | The sum of all transaction fees *(in lumens)* since the last inflation operation. They are redistributed during [inflation]. | -| base_fee | number | The [fee] the network charges per operation in a transaction. | -| base_reserve | string | The [reserve][fee] the network uses when calculating an account's minimum balance. | -| max_tx_set_size | number | The maximum number of transactions validators have agreed to process in a given ledger. | -| protocol_version | number | The protocol version that the stellar network was running when this ledger was committed. | -| header_xdr | string | A base64 encoded string of the raw `LedgerHeader` xdr struct for this ledger. | -| base_fee_in_stroops | number | The [fee] the network charges per operation in a transaction. Expressed in stroops. | -| base_reserve_in_stroops | number | The [reserve][fee] the network uses when calculating an account's minimum balance. Expressed in stroops. | - -## Links -| | Example | Relation | templated | -|--------------|---------------------------------------------------|---------------------------------|-----------| -| self | `/ledgers/500` | | | -| effects | `/ledgers/500/effects/{?cursor,limit,order}` | The effects in this transaction | true | -| operations | `/ledgers/500/operations/{?cursor,limit,order}` | The operations in this ledger | true | -| transactions | `/ledgers/500/transactions/{?cursor,limit,order}` | The transactions in this ledger | true | - - -## Example - -```json -{ - "_links": { - "effects": { - "href": "/ledgers/500/effects/{?cursor,limit,order}", - "templated": true - }, - "operations": { - "href": "/ledgers/500/operations/{?cursor,limit,order}", - "templated": true - }, - "self": { - "href": "/ledgers/500" - }, - "transactions": { - "href": "/ledgers/500/transactions/{?cursor,limit,order}", - "templated": true - } - }, - "id": "689f00d4824b8e69330bf4ad7eb10092ff2f8fdb76d4668a41eebb9469ef7f30", - "paging_token": "2147483648000", - "hash": "689f00d4824b8e69330bf4ad7eb10092ff2f8fdb76d4668a41eebb9469ef7f30", - "prev_hash": "b608e110c7cc58200c912140f121af50dc5ef407aabd53b76e1741080aca1cf0", - "sequence": 500, - "transaction_count": 0, - "successful_transaction_count": 0, - "failed_transaction_count": 0, - "operation_count": 0, - "tx_set_operation_count": 0, - "closed_at": "2015-07-09T21:39:28Z", - "total_coins": "100000000000.0000000", - "fee_pool": "0.0025600", - "base_fee": 100, - "base_reserve": "10.0000000", - "max_tx_set_size": 50, - "protocol_version": 8, - "header_xdr": "...", - "base_fee_in_stroops": 100, - "base_reserve_in_stroops": 100000000 -} -``` - -## Endpoints -| Resource | Type | Resource URI Template | -|-------------------------|------------|------------------------------------| -| [All ledgers](../endpoints/ledgers-all.md) | Collection | `/ledgers` | -| [Single Ledger](../endpoints/ledgers-single.md) | Single | `/ledgers/:id` | -| [Ledger Transactions](../endpoints/transactions-for-ledger.md) | Collection | `/ledgers/:ledger_id/transactions` | -| [Ledger Operations](../endpoints/operations-for-ledger.md) | Collection | `/ledgers/:ledger_id/operations` | -| [Ledger Payments](../endpoints/payments-for-ledger.md) | Collection | `/ledgers/:ledger_id/payments` | -| [Ledger Effects](../endpoints/effects-for-ledger.md) | Collection | `/ledgers/:ledger_id/effects` | - - - -[inflation]: https://www.stellar.org/developers/learn/concepts/inflation.html -[fee]: https://www.stellar.org/developers/learn/concepts/fees.html diff --git a/services/horizon/internal/docs/reference/resources/offer.md b/services/horizon/internal/docs/reference/resources/offer.md deleted file mode 100644 index 14c2a8cb96..0000000000 --- a/services/horizon/internal/docs/reference/resources/offer.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -title: Offer -replacement: https://developers.stellar.org/api/resources/offers/ ---- - -Accounts on the Stellar network can make [offers](http://stellar.org/developers/learn/concepts/exchange.html) to buy or sell assets. Users can create offers with the [Manage Offer](http://stellar.org/developers/learn/concepts/list-of-operations.html) operation. - -Horizon only returns offers that belong to a particular account. When it does, it uses the following format: - -## Attributes -| Attribute | Type | | -|----------------------|-------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| -| id | string | The ID of this offer. | -| paging_token | string | A [paging token](./page.md) suitable for use as a `cursor` parameter. | -| seller | string | Account id of the account making this offer. | -| selling | [Asset](http://stellar.org/developers/learn/concepts/assets.html) | The Asset this offer wants to sell. | -| buying | [Asset](http://stellar.org/developers/learn/concepts/assets.html) | The Asset this offer wants to buy. | -| amount | string | The amount of `selling` the account making this offer is willing to sell. | -| price_r | object | An object of a number numerator and number denominator that represent the buy and sell price of the currencies on offer. | -| price | string | How many units of `buying` it takes to get 1 unit of `selling`. A number representing the decimal form of `price_r`. | -| last_modified_ledger | integer | sequence number for the latest ledger in which this offer was modified. | -| last_modified_time | string | An ISO 8601 formatted string of last modification time. | - -#### Price_r Object -Price_r is a more precise representation of a bid/ask offer. - -| Attribute | Type | | -|-----------|--------|------------------| -| n | number | The numerator. | -| d | number | The denominator. | - -Thus to get price you would take n / d. - - - -## Links -| rel | Example | Description | `templated` | -|--------|------------------------------------------|---------------------------------------------------------|-------------| -| seller | `/accounts/{seller}?cursor,limit,order}` | Link to details about the account that made this offer. | true | - -## Example - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/offers/2611" - }, - "offer_maker": { - "href": "https://horizon-testnet.stellar.org/accounts/GDG3NOK5YI7A4FCBHE6SKI4L65R7UPRBZUZVBT44IBTQBWGUSTJDDKBQ" - } - }, - "id": "2611", - "paging_token": "2611", - "seller": "GDG3NOK5YI7A4FCBHE6SKI4L65R7UPRBZUZVBT44IBTQBWGUSTJDDKBQ", - "selling": { - "asset_type": "credit_alphanum12", - "asset_code": "USD", - "asset_issuer": "GCL3BJDFYQ2KAV7ARC4YCTERNJFOBOBQXSG556TX4YMOPKGEDV5K6LCQ" - }, - "buying": { - "asset_type": "native" - }, - "amount": "1.0000000", - "price_r": { - "n": 1463518003, - "d": 25041627 - }, - "price": "58.4434072", - "last_modified_ledger": 196458, - "last_modified_time": "2020-02-10T18:51:42Z" -} -``` - -## Endpoints - -| Resource | Type | Resource URI Template | -|------------------------------------------------------|------------|--------------------------------| -| [Offers](../endpoints/offers.md) | Collection | `/offers` | -| [Account Offers](../endpoints/offers-for-account.md) | Collection | `/accounts/:account_id/offers` | -| [Offers Details](../endpoints/offer-details.md) | Single | `/offers/:offer_id` | diff --git a/services/horizon/internal/docs/reference/resources/operation.md b/services/horizon/internal/docs/reference/resources/operation.md deleted file mode 100644 index 887bd277c2..0000000000 --- a/services/horizon/internal/docs/reference/resources/operation.md +++ /dev/null @@ -1,840 +0,0 @@ ---- -title: Operation -replacement: https://developers.stellar.org/api/resources/operations/ ---- - -[Operations](https://www.stellar.org/developers/learn/concepts/operations.html) are objects that represent a desired change to the ledger: payments, -offers to exchange currency, changes made to account options, etc. Operations -are submitted to the Stellar network grouped in a [Transaction](./transaction.md). - -To learn more about the concept of operations in the Stellar network, take a look at the [Stellar operations concept guide](https://www.stellar.org/developers/learn/concepts/operations.html). - -## Operation Types - -| type | type_i | description | -|---------------------------------------------------------|--------|------------------------------------------------------------------------------------------------------------| -| [CREATE_ACCOUNT](#create-account) | 0 | Creates a new account in Stellar network. | -| [PAYMENT](#payment) | 1 | Sends a simple payment between two accounts in Stellar network. | -| [PATH_PAYMENT_STRICT_RECEIVE](#path-payment) | 2 | Sends a path payment strict receive between two accounts in the Stellar network. | -| [PATH_PAYMENT_STRICT_SEND](#path-payment-strict-send) | 13 | Sends a path payment strict send between two accounts in the Stellar network. | -| [MANAGE_SELL_OFFER](#manage-sell-offer) | 3 | Creates, updates or deletes a sell offer in the Stellar network. | -| [MANAGE_BUY_OFFER](#manage-buy-offer) | 12 | Creates, updates or deletes a buy offer in the Stellar network. | -| [CREATE_PASSIVE_SELL_OFFER](#create-passive-sell-offer) | 4 | Creates an offer that won't consume a counter offer that exactly matches this offer. | -| [SET_OPTIONS](#set-options) | 5 | Sets account options (inflation destination, adding signers, etc.) | -| [CHANGE_TRUST](#change-trust) | 6 | Creates, updates or deletes a trust line. | -| [ALLOW_TRUST](#allow-trust) | 7 | Updates the "authorized" flag of an existing trust line this is called by the issuer of the related asset. | -| [ACCOUNT_MERGE](#account-merge) | 8 | Deletes account and transfers remaining balance to destination account. | -| [INFLATION](#inflation) | 9 | Runs inflation. | -| [MANAGE_DATA](#manage-data) | 10 | Set, modify or delete a Data Entry (name/value pair) for an account. | -| [BUMP_SEQUENCE](#bump-sequence) | 11 | Bumps forward the sequence number of an account. | - - -Every operation type shares a set of common attributes and links, some operations also contain -additional attributes and links specific to that operation type. - - - -## Common Attributes - -| | Type | | -|------------------------|--------|-----------------------------------------------------------------------------------------------------------------------------| -| id | number | The canonical id of this operation, suitable for use as the :id parameter for url templates that require an operation's ID. | -| paging_token | any | A [paging token](./page.md) suitable for use as a `cursor` parameter. | -| transaction_successful | bool | Indicates if this operation is part of successful transaction. | -| type | string | A string representation of the type of operation. | -| type_i | number | Specifies the type of operation, See "Types" section below for reference. | - -## Common Links - -| | Relation | -|-------------|---------------------------------------------------------------------------| -| self | Relative link to the current operation | -| succeeds | Relative link to the list of operations succeeding the current operation. | -| precedes | Relative link to the list of operations preceding the current operation. | -| effects | The effects this operation triggered | -| transaction | The transaction this operation is part of | - - -Each operation type will have a different set of attributes, in addition to the -common attributes listed above. - -
-### Create Account - -Create Account operation represents a new account creation. - -#### Attributes - -| Field | Type | Description | -|------------------|--------|------------------------------------| -| account | string | A new account that was funded. | -| funder | string | Account that funded a new account. | -| starting_balance | string | Amount the account was funded. | - - -#### Example -```json -{ - "_links": { - "effects": { - "href": "/operations/402494270214144/effects/{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=402494270214144&order=asc" - }, - "self": { - "href": "/operations/402494270214144" - }, - "succeeds": { - "href": "/operations?cursor=402494270214144&order=desc" - }, - "transactions": { - "href": "/transactions/402494270214144" - } - }, - "account": "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ", - "funder": "GBIA4FH6TV64KSPDAJCNUQSM7PFL4ILGUVJDPCLUOPJ7ONMKBBVUQHRO", - "id": "402494270214144", - "paging_token": "402494270214144", - "starting_balance": "10000.0", - "type_i": 0, - "type": "create_account" -} -``` - - -### Payment - -A payment operation represents a payment from one account to another. This payment -can be either a simple native asset payment or a fiat asset payment. - -#### Attributes - -| Field | Type | Description | -|--------------|--------|----------------------------------------------| -| from | string | Sender of a payment. | -| to | string | Destination of a payment. | -| asset_type | string | Asset type (native / alphanum4 / alphanum12) | -| asset_code | string | Code of the destination asset. | -| asset_issuer | string | Asset issuer. | -| amount | string | Amount sent. | - -#### Links - -| | Example | Relation | -|----------|--------------------------------------------------------------------|-------------------| -| sender | /accounts/GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2 | Sending account | -| receiver | /accounts/GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ | Receiving account | - -#### Example - -```json -{ - "_links": { - "effects": { - "href": "/operations/58402965295104/effects/{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=58402965295104&order=asc" - }, - "self": { - "href": "/operations/58402965295104" - }, - "succeeds": { - "href": "/operations?cursor=58402965295104&order=desc" - }, - "transactions": { - "href": "/transactions/58402965295104" - } - }, - "amount": "200.0", - "asset_type": "native", - "from": "GAKLBGHNHFQ3BMUYG5KU4BEWO6EYQHZHAXEWC33W34PH2RBHZDSQBD75", - "id": "58402965295104", - "paging_token": "58402965295104", - "to": "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ", - "transaction_successful": true, - "type_i": 1, - "type": "payment" -} -``` - - -### Path Payment Strict Receive - -A path payment strict receive operation represents a payment from one account to another through a path. This type of payment starts as one type of asset and ends as another type of asset. There can be other assets that are traded into and out of along the path. - - -#### Attributes - -| Field | Type | Description | -|---------------------|-------------------------------|-----------------------------------------------------------------------------| -| from | string | Sender of a payment. | -| to | string | Destination of a payment. | -| asset_code | string | Code of the destination asset. | -| asset_issuer | string | Destination asset issuer. | -| asset_type | string | Destination asset type (native / alphanum4 / alphanum12) | -| amount | string | Amount received. | -| source_asset_code | string | Code of the source asset. | -| source_asset_issuer | string | Source asset issuer. | -| source_asset_type | string | Source asset type (native / alphanum4 / alphanum12) | -| source_max | string | Max send amount. | -| source_amount | string | Amount sent. | -| path | array of [Assets](./asset.md) | Additional hops the operation went through to get to the destination asset. | - -#### Example - -```json -{ - "_links": { - "effects": { - "href": "/operations/25769807873/effects/{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=25769807873\u0026order=asc" - }, - "self": { - "href": "/operations/25769807873" - }, - "succeeds": { - "href": "/operations?cursor=25769807873\u0026order=desc" - }, - "transaction": { - "href": "/transactions/25769807872" - } - }, - "amount": "10.0", - "asset_code": "EUR", - "asset_issuer": "GCQPYGH4K57XBDENKKX55KDTWOTK5WDWRQOH2LHEDX3EKVIQRLMESGBG", - "asset_type": "credit_alphanum4", - "from": "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", - "id": "25769807873", - "paging_token": "25769807873", - "source_asset_code": "USD", - "source_asset_issuer": "GC23QF2HUE52AMXUFUH3AYJAXXGXXV2VHXYYR6EYXETPKDXZSAW67XO4", - "source_asset_type": "credit_alphanum4", - "source_amount": "10.0", - "source_max": "10.0", - "to": "GA5WBPYA5Y4WAEHXWR2UKO2UO4BUGHUQ74EUPKON2QHV4WRHOIRNKKH2", - "transaction_successful": true, - "type_i": 2, - "type": "path_payment_strict_receive" -} -``` - - -### Path Payment Strict Send - -A path payment strict send operation represents a payment from one account to another through a path. This type of payment starts as one type of asset and ends as another type of asset. There can be other assets that are traded into and out of along the path. - -Unlike [path payment strict receive](#path-payment), this operation sends precisely the source amount, ensuring that the destination account receives at least the minimum specified amount (the amount received will vary based on offers in the order books). - - -#### Attributes - -| Field | Type | Description | -|---------------------|-------------------------------|-----------------------------------------------------------------------------| -| from | string | Sender of a payment. | -| to | string | Destination of a payment. | -| asset_type | string | Destination asset type (native / alphanum4 / alphanum12) | -| asset_code | string | Code of the destination asset. | -| asset_issuer | string | Destination asset issuer. | -| amount | string | Amount received. | -| source_asset_type | string | Source asset type (native / alphanum4 / alphanum12) | -| source_asset_code | string | Source asset code. | -| source_asset_issuer | string | Source asset issuer. | -| source_amount | string | Amount sent. | -| destination_min | string | The minimum amount of destination asset expected to be received. | -| path | array of [Assets](./asset.md) | Additional hops the operation went through to get to the destination asset. | - - -#### Example - -```json -{ - "_links": { - "self": { - "href": "/operations/120903307907612673" - }, - "transaction": { - "href": "/transactions/f60f32eff7f1dd0649cfe2986955d12f6ff45288357fe1526600642ea1b418aa" - }, - "effects": { - "href": "/operations/120903307907612673/effects" - }, - "succeeds": { - "href": "/effects?order=desc&cursor=120903307907612673" - }, - "precedes": { - "href": "/effects?order=asc&cursor=120903307907612673" - } - }, - "id": "120903307907612673", - "paging_token": "120903307907612673", - "transaction_successful": true, - "source_account": "GCXVEEBWI4YMRK6AFJQSEUBYDQL4PZ24ECAPJE2ZIAPIQZLZIBAX3LIF", - "type": "path_payment_strict_send", - "type_i": 13, - "created_at": "2020-02-09T20:32:53Z", - "transaction_hash": "f60f32eff7f1dd0649cfe2986955d12f6ff45288357fe1526600642ea1b418aa", - "asset_type": "native", - "from": "GCXVEEBWI4YMRK6AFJQSEUBYDQL4PZ24ECAPJE2ZIAPIQZLZIBAX3LIF", - "to": "GCXVEEBWI4YMRK6AFJQSEUBYDQL4PZ24ECAPJE2ZIAPIQZLZIBAX3LIF", - "amount": "0.0859000", - "path": [ - - ], - "source_amount": "1000.0000000", - "destination_min": "0.0859000", - "source_asset_type": "credit_alphanum4", - "source_asset_code": "KIN", - "source_asset_issuer": "GBDEVU63Y6NTHJQQZIKVTC23NWLQVP3WJ2RI2OTSJTNYOIGICST6DUXR" -} -``` - - -### Manage Sell Offer - -A "Manage Sell Offer" operation can create, update or delete a sell -offer to trade assets in the Stellar network. -It specifies an issuer, a price and amount of a given asset to -buy or sell. - -When this operation is applied to the ledger, trades can potentially be executed if -this offer crosses others that already exist in the ledger. - -In the event that there are not enough crossing orders to fill the order completely -a new "Offer" object will be created in the ledger. As other accounts make -offers or payments, this offer can potentially be filled. - -#### Sell Offer vs. Buy Offer - -A [sell offer](#manage-sell-offer) specifies a certain amount of the `selling` asset that should be sold in exchange for the maximum quantity of the `buying` asset. It additionally only crosses offers where the price is higher than `price`. - -A [buy offer](#manage-buy-offer) specifies a certain amount of the `buying` asset that should be bought in exchange for the minimum quantity of the `selling` asset. It additionally only crosses offers where the price is lower than `price`. - -Both will fill only partially (or not at all) if there are few (or no) offers that cross them. - -#### Attributes - -| Field | Type | Description | -|----------------------|--------|---------------------------------------------------------| -| offer_id | string | Offer ID. | -| amount | string | Amount of asset to be sold. | -| buying_asset_code | string | The code of asset to buy. | -| buying_asset_issuer | string | The issuer of asset to buy. | -| buying_asset_type | string | Type of asset to buy (native / alphanum4 / alphanum12) | -| price | string | Price of selling_asset in units of buying_asset | -| price_r | Object | n: price numerator, d: price denominator | -| selling_asset_code | string | The code of asset to sell. | -| selling_asset_issuer | string | The issuer of asset to sell. | -| selling_asset_type | string | Type of asset to sell (native / alphanum4 / alphanum12) | - -#### Example - -```json -{ - "_links": { - "effects": { - "href": "/operations/592323234762753/effects{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=592323234762753\u0026order=asc" - }, - "self": { - "href": "/operations/592323234762753" - }, - "succeeds": { - "href": "/operations?cursor=592323234762753\u0026order=desc" - }, - "transaction": { - "href": "/transactions/592323234762752" - } - }, - "amount": "100.0", - "buying_asset_code": "CHP", - "buying_asset_issuer": "GAC2ZUXVI5266NMMGDPBMXHH4BTZKJ7MMTGXRZGX2R5YLMFRYLJ7U5EA", - "buying_asset_type": "credit_alphanum4", - "id": "592323234762753", - "offer_id": "8", - "paging_token": "592323234762753", - "price": "2.0", - "price_r": { - "d": 1, - "n": 2 - }, - "selling_asset_code": "YEN", - "selling_asset_issuer": "GDVXG2FMFFSUMMMBIUEMWPZAIU2FNCH7QNGJMWRXRD6K5FZK5KJS4DDR", - "selling_asset_type": "credit_alphanum4", - "transaction_successful": true, - "type_i": 3, - "type": "manage_sell_offer" -} -``` - - -### Manage Buy Offer - -A "Manage Buy Offer" operation can create, update or delete a buy -offer to trade assets in the Stellar network. -It specifies an issuer, a price and amount of a given asset to -buy or sell. - -When this operation is applied to the ledger, trades can potentially be executed if -this offer crosses others that already exist in the ledger. - -In the event that there are not enough crossing orders to fill the order completely -a new "Offer" object will be created in the ledger. As other accounts make -offers or payments, this offer can potentially be filled. - -#### Attributes - -| Field | Type | Description | -|----------------------|--------|---------------------------------------------------------------| -| offer_id | string | Offer ID. | -| buy_amount | string | Amount of asset to be bought. | -| buying_asset_code | string | The code of asset to buy. | -| buying_asset_issuer | string | The issuer of asset to buy. | -| buying_asset_type | string | Type of asset to buy (native / alphanum4 / alphanum12) | -| price | string | Price of thing being bought in terms of what you are selling. | -| price_r | Object | n: price numerator, d: price denominator | -| selling_asset_code | string | The code of asset to sell. | -| selling_asset_issuer | string | The issuer of asset to sell. | -| selling_asset_type | string | Type of asset to sell (native / alphanum4 / alphanum12) | -#### Example - -```json -{ - "_links": { - "effects": { - "href": "/operations/592323234762753/effects{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=592323234762753\u0026order=asc" - }, - "self": { - "href": "/operations/592323234762753" - }, - "succeeds": { - "href": "/operations?cursor=592323234762753\u0026order=desc" - }, - "transaction": { - "href": "/transactions/592323234762752" - } - }, - "amount": "100.0", - "buying_asset_code": "CHP", - "buying_asset_issuer": "GAC2ZUXVI5266NMMGDPBMXHH4BTZKJ7MMTGXRZGX2R5YLMFRYLJ7U5EA", - "buying_asset_type": "credit_alphanum4", - "id": "592323234762753", - "offer_id": "8", - "paging_token": "592323234762753", - "price": "2.0", - "price_r": { - "d": 1, - "n": 2 - }, - "selling_asset_code": "YEN", - "selling_asset_issuer": "GDVXG2FMFFSUMMMBIUEMWPZAIU2FNCH7QNGJMWRXRD6K5FZK5KJS4DDR", - "selling_asset_type": "credit_alphanum4", - "transaction_successful": true, - "type_i": 12, - "type": "manage_buy_offer" -} -``` - - -### Create Passive Sell Offer - -“Create Passive Sell Offer” operation creates an offer that won't consume a counter offer that exactly matches this offer. This is useful for offers just used as 1:1 exchanges for path payments. Use Manage Sell Offer to manage this offer after using this operation to create it. - -#### Attributes - -As in [Manage Sell Offer](#manage-sell-offer) operation. - -#### Example - -```json -{ - "_links": { - "effects": { - "href": "/operations/1127729562914817/effects{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=1127729562914817\u0026order=asc" - }, - "self": { - "href": "/operations/1127729562914817" - }, - "succeeds": { - "href": "/operations?cursor=1127729562914817\u0026order=desc" - }, - "transaction": { - "href": "/transactions/1127729562914816" - } - }, - "amount": "11.27827", - "buying_asset_code": "USD", - "buying_asset_issuer": "GDS5JW5E6DRSSN5XK4LW7E6VUMFKKE2HU5WCOVFTO7P2RP7OXVCBLJ3Y", - "buying_asset_type": "credit_alphanum4", - "id": "1127729562914817", - "offer_id": "9", - "paging_token": "1127729562914817", - "price": "1.0", - "price_r": { - "d": 1, - "n": 1 - }, - "selling_asset_type": "native", - "transaction_successful": true, - "type_i": 4, - "type": "create_passive_sell_offer" -} -``` - - - -### Set Options - -Use “Set Options” operation to set following options to your account: -* Set/clear account flags: - * AUTH_REQUIRED_FLAG (0x1) - if set, TrustLines are created with authorized set to `false` requiring the issuer to set it for each TrustLine. - * AUTH_REVOCABLE_FLAG (0x2) - if set, the authorized flag in TrustLines can be cleared. Otherwise, authorization cannot be revoked. -* Set the account’s inflation destination. -* Add new signers to the account. -* Set home domain. - - -#### Attributes - -| Field | Type | Description | -|-------------------|--------|------------------------------------------------------------------------------| -| signer_key | string | The public key of the new signer. | -| signer_weight | int | The weight of the new signer (1-255). | -| master_key_weight | int | The weight of the master key (1-255). | -| low_threshold | int | The sum weight for the low threshold. | -| med_threshold | int | The sum weight for the medium threshold. | -| high_threshold | int | The sum weight for the high threshold. | -| home_domain | string | The home domain used for reverse federation lookup | -| set_flags | array | The array of numeric values of flags that has been set in this operation | -| set_flags_s | array | The array of string values of flags that has been set in this operation | -| clear_flags | array | The array of numeric values of flags that has been cleared in this operation | -| clear_flags_s | array | The array of string values of flags that has been cleared in this operation | - - -#### Example - -```json -{ - "_links": { - "effects": { - "href": "/operations/696867033714691/effects{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=696867033714691\u0026order=asc" - }, - "self": { - "href": "/operations/696867033714691" - }, - "succeeds": { - "href": "/operations?cursor=696867033714691\u0026order=desc" - }, - "transaction": { - "href": "/transactions/696867033714688" - } - }, - "high_threshold": 3, - "home_domain": "stellar.org", - "id": "696867033714691", - "low_threshold": 0, - "med_threshold": 3, - "paging_token": "696867033714691", - "set_flags": [ - 1 - ], - "set_flags_s": [ - "auth_required_flag" - ], - "transaction_successful": true, - "type_i": 5, - "type": "set_options" -} -``` - - -### Change Trust - -Use “Change Trust” operation to create/update/delete a trust line from the source account to another. The issuer being trusted and the asset code are in the given Asset object. - -#### Attributes - -| Field | Type | Description | -|--------------|--------|----------------------------------------------| -| asset_code | string | Asset code. | -| asset_issuer | string | Asset issuer. | -| asset_type | string | Asset type (native / alphanum4 / alphanum12) | -| trustee | string | Trustee account. | -| trustor | string | Trustor account. | -| limit | string | The limit for the asset. | - -#### Example - -```json -{ - "_links": { - "effects": { - "href": "/operations/574731048718337/effects{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=574731048718337\u0026order=asc" - }, - "self": { - "href": "/operations/574731048718337" - }, - "succeeds": { - "href": "/operations?cursor=574731048718337\u0026order=desc" - }, - "transaction": { - "href": "/transactions/574731048718336" - } - }, - "asset_code": "CHP", - "asset_issuer": "GAC2ZUXVI5266NMMGDPBMXHH4BTZKJ7MMTGXRZGX2R5YLMFRYLJ7U5EA", - "asset_type": "credit_alphanum4", - "id": "574731048718337", - "limit": "5.0", - "paging_token": "574731048718337", - "trustee": "GAC2ZUXVI5266NMMGDPBMXHH4BTZKJ7MMTGXRZGX2R5YLMFRYLJ7U5EA", - "trustor": "GDVXG2FMFFSUMMMBIUEMWPZAIU2FNCH7QNGJMWRXRD6K5FZK5KJS4DDR", - "transaction_successful": true, - "type_i": 6, - "type": "change_trust" -} -``` - - -### Allow Trust - -Updates the "authorized" flag of an existing trust line this is called by the issuer of the asset. - -Heads up! Unless the issuing account has `AUTH_REVOCABLE_FLAG` set than the "authorized" flag can only be set and never cleared. - -#### Attributes - -| Field | Type | Description | -|--------------|--------|---------------------------------------------------------| -| asset_code | string | Asset code. | -| asset_issuer | string | Asset issuer. | -| asset_type | string | Asset type (native / alphanum4 / alphanum12) | -| authorize | bool | `true` when allowing trust, `false` when revoking trust | -| trustee | string | Trustee account. | -| trustor | string | Trustor account. | - -#### Example - -```json -{ - "_links": { - "effects": { - "href": "/operations/34359742465/effects/{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=34359742465\u0026order=asc" - }, - "self": { - "href": "/operations/34359742465" - }, - "succeeds": { - "href": "/operations?cursor=34359742465\u0026order=desc" - }, - "transaction": { - "href": "/transactions/34359742464" - } - }, - "asset_code": "USD", - "asset_issuer": "GC23QF2HUE52AMXUFUH3AYJAXXGXXV2VHXYYR6EYXETPKDXZSAW67XO4", - "asset_type": "credit_alphanum4", - "authorize": true, - "id": "34359742465", - "paging_token": "34359742465", - "trustee": "GC23QF2HUE52AMXUFUH3AYJAXXGXXV2VHXYYR6EYXETPKDXZSAW67XO4", - "trustor": "GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON", - "transaction_successful": true, - "type_i": 7, - "type": "allow_trust" -} -``` - - -### Account Merge - -Removes the account and transfers all remaining XLM to the destination account. - -#### Attributes - -| Field | Type | Description | -|-------|--------|-------------------------------------------------------------| -| into | string | Account ID where funds of deleted account were transferred. | - -#### Example -```json -{ - "_links": { - "effects": { - "href": "/operations/799357838299137/effects{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=799357838299137\u0026order=asc" - }, - "self": { - "href": "/operations/799357838299137" - }, - "succeeds": { - "href": "/operations?cursor=799357838299137\u0026order=desc" - }, - "transaction": { - "href": "/transactions/799357838299136" - } - }, - "account": "GBCR5OVQ54S2EKHLBZMK6VYMTXZHXN3T45Y6PRX4PX4FXDMJJGY4FD42", - "id": "799357838299137", - "into": "GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K", - "paging_token": "799357838299137", - "transaction_successful": true, - "type_i": 8, - "type": "account_merge" -} -``` - - -### Inflation - -Runs inflation. - -#### Example - -```json -{ - "_links": { - "effects": { - "href": "/operations/12884914177/effects/{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "/operations?cursor=12884914177\u0026order=asc" - }, - "self": { - "href": "/operations/12884914177" - }, - "succeeds": { - "href": "/operations?cursor=12884914177\u0026order=desc" - }, - "transaction": { - "href": "/transactions/12884914176" - } - }, - "id": "12884914177", - "paging_token": "12884914177", - "transaction_successful": true, - "type_i": 9, - "type": "inflation" -} -``` - - -### Manage Data - -Set, modify or delete a Data Entry (name/value pair) for an account. - -#### Example - -```json -{ - "_links": { - "self": { - "href": "/operations/5250180907536385" - }, - "transaction": { - "href": "/transactions/e0710d3e410fe6b1ba77fcfec9e3789e94ff29b2424f1f4bf51e530dbbdf221c" - }, - "effects": { - "href": "/operations/5250180907536385/effects" - }, - "succeeds": { - "href": "/effects?order=desc&cursor=5250180907536385" - }, - "precedes": { - "href": "/effects?order=asc&cursor=5250180907536385" - } - }, - "id": "5250180907536385", - "paging_token": "5250180907536385", - "source_account": "GCGG3CIRBG2TTBR4HYZJ7JLDRFKZIYOAHFXRWLU62CA2QN52P2SUQNPJ", - "type": "manage_data", - "type_i": 10, - "transaction_successful": true, - "name": "lang", - "value": "aW5kb25lc2lhbg==" -} -``` - - -### Bump Sequence - -Bumps forward the sequence number of the source account of the operation, allowing it to invalidate any transactions with a smaller sequence number. - -#### Attributes - -| Field | Type | Description | -|--------|--------|-------------------------------------------------------------------| -| bumpTo | number | Desired value for the operation’s source account sequence number. | - -#### Example -```json -{ - "_links": { - "self": { - "href": "/operations/1743756726273" - }, - "transaction": { - "href": "/transactions/328436a8dffaf6ca33c08a93279234c7d3eaf1c028804152614187dc76b7168d" - }, - "effects": { - "href": "/operations/1743756726273/effects" - }, - "succeeds": { - "href": "/effects?order=desc&cursor=1743756726273" - }, - "precedes": { - "href": "/effects?order=asc&cursor=1743756726273" - } - }, - "id": "1743756726273", - "paging_token": "1743756726273", - "source_account": "GBHPJ3VMVT3X7Y6HIIAPK7YPTZCF3CWO4557BKGX2GVO4O7EZHIBELLH", - "type": "bump_sequence", - "type_i": 11, - "transaction_hash": "328436a8dffaf6ca33c08a93279234c7d3eaf1c028804152614187dc76b7168d", - "bump_to": "1273737228" -} -``` - -## Endpoints - -| Resource | Type | Resource URI Template | -|----------------------------------------------------|------------|-------------------------------------------------| -| [All Operations](../endpoints/operations-all.md) | Collection | `/operations` | -| [Operations Details](../endpoints/operations-single.md) | Single | `/operations/:id` | -| [Ledger Operations](../endpoints/operations-for-ledger.md) | Collection | `/ledgers/{id}/operations{?cursor,limit,order}` | -| [Account Operations](../endpoints/operations-for-account.md) | Collection | `/accounts/:account_id/operations` | -| [Account Payments](../endpoints/payments-for-account.md) | Collection | `/accounts/:account_id/payments` | diff --git a/services/horizon/internal/docs/reference/resources/orderbook.md b/services/horizon/internal/docs/reference/resources/orderbook.md deleted file mode 100644 index c7e1e78b30..0000000000 --- a/services/horizon/internal/docs/reference/resources/orderbook.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: Orderbook -replacement: https://developers.stellar.org/api/aggregations/order-books/ ---- - -[Orderbooks](https://www.stellar.org/developers/learn/concepts/exchange.html) are collections of offers for each issuer and currency pairs. Let's say you wanted to exchange EUR issued by a particular bank for BTC issued by a particular exchange. You would look at the orderbook and see who is buying `foo_bank/EUR` and selling `baz_exchange/BTC` and at what prices. - -## Attributes -| Attribute | Type | | -|--------------|------------------|------------------------------------------------------------------------------------------------------------------------| -| bids | object | Array of {`price_r`, `price`, `amount`} objects (see [offers](./offer.md)). These represent prices and amounts accounts are willing to buy for the given `selling` and `buying` pair. | -| asks | object | Array of {`price_r`, `price`, `amount`} objects (see [offers](./offer.md)). These represent prices and amounts accounts are willing to sell for the given `selling` and `buying` pair.| -| base | [Asset](http://stellar.org/developers/learn/concepts/assets.html) | The Asset this offer wants to sell.| -| counter | [Asset](http://stellar.org/developers/learn/concepts/assets.html) | The Asset this offer wants to buy.| - -#### Bid Object -| Attribute | Type | | -| ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | -| price_r | object | An object of a number numerator and number denominator that represents the bid price. | -| price | string | The bid price of the asset. A number representing the decimal form of price_r | -| amount | string | The amount of asset bid offer. | - -#### Ask Object -| Attribute | Type | | -| ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | -| price_r | object | An object of a number numerator and number denominator that represents the ask price. | -| price | string | The ask price of the asset. A number representing the decimal form of price_r | -| amount | string | The amount of asset ask offer. | - -#### Price_r Object -Price_r is a more precise representation of a bid/ask offer. - -| Attribute | Type | | -| ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | -| n | number | The numerator. | -| d | number | The denominator. | - -Thus to get price you would take n / d. - -## Links - -This resource has no links. - - -## Endpoints - -| Resource | Type | Resource URI Template | -|--------------------------|------------|--------------------------------------| -| [Orderbook Details](../endpoints/orderbook-details.md) | Single | `/orderbook?{orderbook_params}` | -| [Trades](../endpoints/trades.md) | Collection | `/trades?{orderbook_params}` | diff --git a/services/horizon/internal/docs/reference/resources/page.md b/services/horizon/internal/docs/reference/resources/page.md deleted file mode 100644 index 6c90db63bf..0000000000 --- a/services/horizon/internal/docs/reference/resources/page.md +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: Page -replacement: https://developers.stellar.org/api/introduction/pagination/page-arguments/ ---- - -Pages represent a subset of a larger collection of objects. -As an example, it would be unfeasible to provide the -[All Transactions](../endpoints/transactions-all.md) endpoint without paging. Over time there -will be millions of transactions in the Stellar network's ledger and returning -them all over a single request would be unfeasible. - -## Attributes - -A page itself exposes no attributes. It is merely a container for embedded -records and some links to aid in iterating the entire collection the page is -part of. - -## Cursor -A `cursor` is a number that points to a specific location in a collection of resources. - -The `cursor` attribute itself is an opaque value meaning that users should not try to parse it. - -## Embedded Resources - -A page contains an embedded set of `records`, regardless of the contained resource. - -## Links - -A page provides a couple of links to ease in iteration. - -| | Example | Relation | -| ---- | ------------------------------------------------------ | ---------------------------- | -| self | `/transactions` | | -| prev | `/transactions?cursor=12884905984&order=desc&limit=10` | The previous page of results | -| next | `/transactions?cursor=12884905984&order=asc&limit=10` | The next page of results | - -## Example - -```json -{ - "_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "/operations/12884905984" - }, - "transaction": { - "href": "/transaction/6391dd190f15f7d1665ba53c63842e368f485651a53d8d852ed442a446d1c69a" - }, - "precedes": { - "href": "/account/GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ/payments?cursor=12884905984&order=asc{?limit}", - "templated": true - }, - "succeeds": { - "href": "/account/GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ/payments?cursor=12884905984&order=desc{?limit}", - "templated": true - } - }, - "id": 12884905984, - "paging_token": "12884905984", - "type_i": 0, - "type": "payment", - "sender": "GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ", - "receiver": "GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU", - "asset": { - "code": "XLM" - }, - "amount": 1000000000, - "amount_f": 100.00 - } - ] - }, - "_links": { - "next": { - "href": "/account/GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ/payments?cursor=12884905984&order=asc&limit=100" - }, - "prev": { - "href": "/account/GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ/payments?cursor=12884905984&order=desc&limit=100" - }, - "self": { - "href": "/account/GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ/payments?limit=100" - } - } -} - -``` - -## Endpoints - -Any endpoint that provides a collection of resources will represent them as pages. - diff --git a/services/horizon/internal/docs/reference/resources/path.md b/services/horizon/internal/docs/reference/resources/path.md deleted file mode 100644 index 20c87b2bc9..0000000000 --- a/services/horizon/internal/docs/reference/resources/path.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Payment Path -replacement: https://developers.stellar.org/api/aggregations/paths/ ---- - -A **path** resource contains information about a payment path. A path can be used by code to populate necessary fields on path payment operation, such as `path` and `sendMax`. - - -## Attributes -| Attribute | Type | | -|--------------------------|------------------|--------------------------------------------------------------------------------------------------------------------------------| -| path | array of objects | An array of assets that represents the intermediary assets this path hops through | -| source_amount | string | An estimated cost for making a payment of destination_amount on this path. Suitable for use in a path payments `sendMax` field | -| destination_amount | string | The destination amount specified in the search that found this path | -| destination_asset_type | string | The type for the destination asset specified in the search that found this path | -| destination_asset_code | optional, string | The code for the destination asset specified in the search that found this path | -| destination_asset_issuer | optional, string | The issuer for the destination asset specified in the search that found this path | -| source_asset_type | string | The type for the source asset specified in the search that found this path | -| source_asset_code | optional, string | The code for the source asset specified in the search that found this path | -| source_asset_issuer | optional, string | The issuer for the source asset specified in the search that found this path | - -#### Asset Object -| Attribute | Type | | -|--------------|------------------|------------------------------------------------------------------------------------------------------------------------ -| asset_code | optional, string | The code for the asset. | -| asset_type | string | Either native, credit_alphanum4, or credit_alphanum12. | -| asset_issuer | optional, string | The stellar address of the given asset's issuer. | - -## Example - -```json -{ - "destination_amount": "20.0000000", - "destination_asset_code": "EUR", - "destination_asset_issuer": "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", - "destination_asset_type": "credit_alphanum4", - "path": [ - { - "asset_code": "1", - "asset_issuer": "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", - "asset_type": "credit_alphanum4" - } - ], - "source_amount": "20.0000000", - "source_asset_code": "USD", - "source_asset_issuer": "GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN", - "source_asset_type": "credit_alphanum4" -} -``` - -## Endpoints -| Resource | Type | Resource URI Template | -|------------------------------------------|------------|-----------------------| -| [Find Payment Paths](../endpoints/path-finding.md) | Collection | `/paths` | diff --git a/services/horizon/internal/docs/reference/resources/trade.md b/services/horizon/internal/docs/reference/resources/trade.md deleted file mode 100644 index 08434dcd63..0000000000 --- a/services/horizon/internal/docs/reference/resources/trade.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Trade ---- - -A trade represents a fulfilled offer. For example, let's say that there exists an offer to sell 9 `foo_bank/EUR` for 3 `baz_exchange/BTC` and you make an offer to buy 3 `foo_bank/EUR` for 1 `baz_exchange/BTC`. Since your offer and the existing one cross, a trade happens. After the trade completes: - -- you are 3 `foo_bank/EUR` richer and 1 `baz_exchange/BTC` poorer -- the maker of the other offer is 1 `baz_exchange/BTC` richer and 3 `foo_bank/EUR` poorer -- your offer is completely fulfilled and no longer exists -- the other offer is partially fulfilled and becomes an offer to sell 6 `foo_bank/EUR` for 2 `baz_exchange/BTC`. The price of that offer doesn't change, but the amount does. - -Trades can also be caused by successful [path payments](https://www.stellar.org/developers/learn/concepts/exchange.html), because path payments involve fulfilling offers. - -Payments are one-way in that afterwards, the source account has a smaller balance and the destination account of the payment has a bigger one. Trades are two-way; both accounts increase and decrease their balances. - -A trade occurs between two parties - `base` and `counter`. Which is either arbitrary or determined by the calling query. - -## Attributes -| Attribute | Type | | -|--------------|------------------|------------------------------------------------------------------------------------------------------------------------| -| id | string | The ID of this trade. | -| paging_token | string | A [paging token](./page.md) suitable for use as a `cursor` parameter.| -| ledger_close_time | string | An ISO 8601 formatted string of when the ledger with this trade was closed.| -| offer_id | string | DEPRECATED. the sell offer id. -| base_account | string | base party of this trade| -| base_offer_id | string | the base offer id. If this offer was immediately fully consumed this will be a synthetic id -| base_amount | string | amount of base asset that was moved from `base_account` to `counter_account`| -| base_asset_type | string | type of base asset| -| base_asset_code | string | code of base asset| -| base_asset_issuer | string | issuer of base asset| -| counter_offer_id | string | the counter offer id. If this offer was immediately fully consumed this will be a synthetic id -| counter_account | string | counter party of this trade| -| counter_amount | string | amount of counter asset that was moved from `counter_account` to `base_account`| -| counter_asset_type | string | type of counter asset| -| counter_asset_code | string | code of counter asset| -| counter_asset_issuer | string | issuer of counter asset| -| price | object | original offer price, expressed as a rational number. example: {n:7, d:3} -| base_is_seller | boolean | indicates which party of the trade made the sell offer| - -#### Price Object -Price is a precise representation of a bid/ask offer. - -| Attribute | Type | | -| ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | -| n | number | The numerator. | -| d | number | The denominator. | - -Thus to get price you would take n / d. - -#### Synthetic Offer Ids -Offer ids in the horizon trade resource (base_offer_id, counter_offer_id) are synthetic and don't always reflect the respective stellar-core offer ids. This is due to the fact that stellar-core does not assign offer ids when an offer gets filled immediately. In these cases, Horizon synthetically generates an offer id for the buying offer, based on the total order id of the offer operation. This allows wallets to aggregate historical trades based on offer ids without adding special handling for edge cases. The exact encoding can be found [here](https://github.com/stellar/go/blob/master/services/horizon/internal/db2/history/synt_offer_id.go). - -## Links - -| rel | Example | Description | `templated` | -|--------------|---------------------------------------------------------------------------------------------------|------------------------------------------------------------|-------------| -| base | `/accounts/{base_account}` | Link to details about the base account| true | -| counter | `/accounts/{counter_account}` | Link to details about the counter account | true | -| operation | `/operation/{operation_id}` | Link to the operation of the assets bought and sold. | true | - -## Endpoints - -| Resource | Type | Resource URI Template | -|--------------------------|------------|--------------------------------------| -| [Trades](../endpoints/trades.md) | Collection | `/trades` | -| [Account Trades](../endpoints/trades-for-account.md) | Collection | `/accounts/:account_id/trades` | diff --git a/services/horizon/internal/docs/reference/resources/trade_aggregation.md b/services/horizon/internal/docs/reference/resources/trade_aggregation.md deleted file mode 100644 index d1019e7f0c..0000000000 --- a/services/horizon/internal/docs/reference/resources/trade_aggregation.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -title: Trade Aggregation -replacement: https://developers.stellar.org/api/aggregations/trade-aggregations/ ---- - -A Trade Aggregation represents aggregated statistics on an asset pair (`base` and `counter`) for a specific time period. - -## Attributes -| Attribute | Type | | -|--------------|------------------|------------------------------------------------------------------------------------------------------------------------| -| timestamp | string | start time for this trade_aggregation. Represented as milliseconds since epoch.| -| trade_count | int | total number of trades aggregated.| -| base_volume | string | total volume of `base` asset.| -| counter_volume | string | total volume of `counter` asset.| -| avg | string | weighted average price of `counter` asset in terms of `base` asset.| -| high | string | highest price for this time period.| -| high_r | object | highest price for this time period as a rational number.| -| low | string | lowest price for this time period.| -| low_r | object | lowest price for this time period as a rational number.| -| open | string | price as seen on first trade aggregated.| -| open_r | object | price as seen on first trade aggregated as a rational number.| -| close | string | price as seen on last trade aggregated.| -| close_r | object | price as seen on last trade aggregated as a rational number.| - -#### Price_r Object -Price_r (high_r, low_r, open_r, close_r) is a more precise representation of a bid/ask offer. - -| Attribute | Type | | -| ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | -| n | number | The numerator. | -| d | number | The denominator. | - -Thus to get price you would take n / d. - -## Endpoints - -| Resource | Type | Resource URI Template | -|--------------------------|------------|--------------------------------------| -| [Trade Aggregations](../endpoints/trade_aggregations.md) | Collection | `/trade_aggregations?{orderbook_params}` | diff --git a/services/horizon/internal/docs/reference/resources/transaction.md b/services/horizon/internal/docs/reference/resources/transaction.md deleted file mode 100644 index ac4c5b488b..0000000000 --- a/services/horizon/internal/docs/reference/resources/transaction.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: Transaction -replacement: https://developers.stellar.org/api/resources/transactions/ ---- - -**Transactions** are the basic unit of change in the Stellar Network. - -A transaction is a grouping of [operations](./operation.md). - -To learn more about the concept of transactions in the Stellar network, take a look at the [Stellar transactions concept guide](https://www.stellar.org/developers/learn/concepts/transactions.html). - -## Attributes - -| Attribute | Type | | -|-------------------------|--------------------------|--------------------------------------------------------------------------------------------------------------------------------| -| id | string | The canonical id of this transaction, suitable for use as the :id parameter for url templates that require a transaction's ID. | -| paging_token | string | A [paging token](./page.md) suitable for use as the `cursor` parameter to transaction collection resources. | -| successful | bool | Indicates if transaction was successful or not. | -| hash | string | A hex-encoded, lowercase SHA-256 hash of the transaction's [XDR](../../learn/xdr.md)-encoded form. | -| ledger | number | Sequence number of the ledger in which this transaction was applied. | -| created_at | ISO8601 string | | -| fee_account | string | The account which paid for the transaction fees | -| source_account | string | | -| source_account_sequence | string | | -| max_fee | number | The the maximum fee the fee account was willing to pay. | -| fee_charged | number | The fee paid by the fee account of this transaction when the transaction was applied to the ledger. | -| operation_count | number | The number of operations that are contained within this transaction. | -| envelope_xdr | string | A base64 encoded string of the raw `TransactionEnvelope` xdr struct for this transaction | -| result_xdr | string | A base64 encoded string of the raw `TransactionResult` xdr struct for this transaction | -| result_meta_xdr | string | A base64 encoded string of the raw `TransactionMeta` xdr struct for this transaction | -| fee_meta_xdr | string | A base64 encoded string of the raw `LedgerEntryChanges` xdr struct produced by taking fees for this transaction. | -| memo_type | string | The type of memo set in the transaction. Possible values are `none`, `text`, `id`, `hash`, and `return`. | -| memo | string | The string representation of the memo set in the transaction. When `memo_type` is `id`, the `memo` is a decimal string representation of an unsigned 64 bit integer. When `memo_type` is `hash` or `return`, the `memo` is a base64 encoded string. When `memo_type` is `text`, the `memo` is a unicode string. However, if the original memo byte sequence in the transaction XDR is not valid unicode, Horizon will replace any invalid byte sequences with the utf-8 replacement character. Note this field is only present when `memo_type` is not `none`. | -| memo_bytes | string | A base64 encoded string of the memo bytes set in the transaction's xdr envelope. Note this field is only present when `memo_type` is `text`. | -| signatures | string[] | An array of signatures used to sign this transaction | -| valid_after | RFC3339 date-time string | | -| valid_before | RFC3339 date-time string | | -| fee_bump_transaction | object | This object is only present if the transaction is a fee bump transaction or is wrapped by a fee bump transaction. The object has two fields: `hash` (the hash of the fee bump transaction) and `signatures` (the signatures present in the fee bump transaction envelope) | -| inner_transaction | object | This object is only present if the transaction is a fee bump transaction or is wrapped by a fee bump transaction. The object has three fields: `hash` (the hash of the inner transaction wrapped by the fee bump transaction), `max_fee` (the max fee set in the inner transaction), and `signatures` (the signatures present in the inner transaction envelope) | - -## Links - -| rel | Example | Description | -|------------|------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------| -| self | `https://horizon-testnet.stellar.org/transactions/cb9a25394acb6fe0d1d9bdea5afc01cafe2c6fde59a96ddceb2564a65780a81f` | | -| account | `https://horizon-testnet.stellar.org/accounts/GCDLRUXOD6KA53G5ILL435TZAISNLPS4EKIHSOVY3MVD3DVJ333NO4DT` | The source [account](../endpoints/accounts-single.md) for this transaction. | -| ledger | `https://horizon-testnet.stellar.org/ledgers/2352988` | The [ledger](../endpoints/ledgers-single.md) in which this transaction was applied. | -| operations | `https://horizon-testnet.stellar.org/transactions/cb9a25394acb6fe0d1d9bdea5afc01cafe2c6fde59a96ddceb2564a65780a81f/operations{?cursor,limit,order}"` | [Operations](../endpoints/operations-for-transaction.md) included in this transaction. | -| effects | `https://horizon-testnet.stellar.org/transactions/cb9a25394acb6fe0d1d9bdea5afc01cafe2c6fde59a96ddceb2564a65780a81f/effects{?cursor,limit,order}"` | [Effects](../endpoints/effects-for-transaction.md) which resulted by operations in this transaction. | -| precedes | `https://horizon-testnet.stellar.org/transactions?order=asc&cursor=10106006507900928` | A collection of transactions that occur after this transaction. | -| succeeds | `https://horizon-testnet.stellar.org/transactions?order=desc&cursor=10106006507900928` | A collection of transactions that occur before this transaction. | - -## Example - -```json -{ - "_links": { - "self": { - "href": "https://horizon-testnet.stellar.org/transactions/cb9a25394acb6fe0d1d9bdea5afc01cafe2c6fde59a96ddceb2564a65780a81f" - }, - "account": { - "href": "https://horizon-testnet.stellar.org/accounts/GCDLRUXOD6KA53G5ILL435TZAISNLPS4EKIHSOVY3MVD3DVJ333NO4DT" - }, - "ledger": { - "href": "https://horizon-testnet.stellar.org/ledgers/2352988" - }, - "operations": { - "href": "https://horizon-testnet.stellar.org/transactions/cb9a25394acb6fe0d1d9bdea5afc01cafe2c6fde59a96ddceb2564a65780a81f/operations{?cursor,limit,order}", - "templated": true - }, - "effects": { - "href": "https://horizon-testnet.stellar.org/transactions/cb9a25394acb6fe0d1d9bdea5afc01cafe2c6fde59a96ddceb2564a65780a81f/effects{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "https://horizon-testnet.stellar.org/transactions?order=asc&cursor=10106006507900928" - }, - "succeeds": { - "href": "https://horizon-testnet.stellar.org/transactions?order=desc&cursor=10106006507900928" - } - }, - "id": "cb9a25394acb6fe0d1d9bdea5afc01cafe2c6fde59a96ddceb2564a65780a81f", - "paging_token": "10106006507900928", - "successful": true, - "hash": "cb9a25394acb6fe0d1d9bdea5afc01cafe2c6fde59a96ddceb2564a65780a81f", - "ledger": 2352988, - "created_at": "2019-02-21T21:44:13Z", - "source_account": "GCDLRUXOD6KA53G5ILL435TZAISNLPS4EKIHSOVY3MVD3DVJ333NO4DT", - "fee_account": "GCDLRUXOD6KA53G5ILL435TZAISNLPS4EKIHSOVY3MVD3DVJ333NO4DT", - "source_account_sequence": "10105916313567234", - "max_fee": 100, - "fee_charged":100, - "operation_count": 1, - "envelope_xdr": "AAAAAIa40u4flA7s3ULXzfZ5AiTVvlwikHk6uNsqPY6p3vbXAAAAZAAj50cAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAAB2Fmc2RmYXMAAAAAAQAAAAAAAAABAAAAAIa40u4flA7s3ULXzfZ5AiTVvlwikHk6uNsqPY6p3vbXAAAAAAAAAAEqBfIAAAAAAAAAAAGp3vbXAAAAQKElK3CoNo1f8fWIGeJm98lw2AaFiyVVFhx3uFok0XVW3MHV9MubtEhfA+n1iLPrxmzHtHfmZsumWk+sOEQlSwI=", - "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAA=", - "result_meta_xdr": "AAAAAQAAAAIAAAADACPnXAAAAAAAAAAAhrjS7h+UDuzdQtfN9nkCJNW+XCKQeTq42yo9jqne9tcAAAAXSHbnOAAj50cAAAABAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABACPnXAAAAAAAAAAAhrjS7h+UDuzdQtfN9nkCJNW+XCKQeTq42yo9jqne9tcAAAAXSHbnOAAj50cAAAACAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA==", - "fee_meta_xdr": "AAAAAgAAAAMAI+dTAAAAAAAAAACGuNLuH5QO7N1C1832eQIk1b5cIpB5OrjbKj2Oqd721wAAABdIduecACPnRwAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAEAI+dcAAAAAAAAAACGuNLuH5QO7N1C1832eQIk1b5cIpB5OrjbKj2Oqd721wAAABdIduc4ACPnRwAAAAEAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==", - "memo_type": "text", - "memo": "afsdfas", - "valid_after": "1970-01-01T00:00:00Z", - "signatures": [ - "oSUrcKg2jV/x9YgZ4mb3yXDYBoWLJVUWHHe4WiTRdVbcwdX0y5u0SF8D6fWIs+vGbMe0d+Zmy6ZaT6w4RCVLAg==" - ] -} -``` - -## Endpoints - -| Resource | Type | Resource URI Template | -|--------------------------------------------------------|------------|--------------------------------------| -| [All Transactions](../endpoints/transactions-all.md) | Collection | `/transactions` (`GET`) | -| [Post Transaction](../endpoints/transactions-create.md) | Action | `/transactions` (`POST`) | -| [Transaction Details](../endpoints/transactions-single.md) | Single | `/transactions/:id` | -| [Account Transactions](../endpoints/transactions-for-account.md) | Collection | `/accounts/:account_id/transactions` | -| [Ledger Transactions](../endpoints/transactions-for-ledger.md) | Collection | `/ledgers/:ledger_id/transactions` | - - -## Submitting transactions -To submit a new transaction to Stellar network, it must first be built and signed locally. Then you can submit a hex representation of your transaction’s [XDR](../xdr.md) to the `/transactions` endpoint. Read more about submitting transactions in [Post Transaction](../endpoints/transactions-create.md) doc. diff --git a/services/horizon/internal/docs/reference/responses.md b/services/horizon/internal/docs/reference/responses.md deleted file mode 100644 index 151e11ca57..0000000000 --- a/services/horizon/internal/docs/reference/responses.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: Response Format -replacement: https://developers.stellar.org/api/introduction/response-format/ ---- - -Rather than using a fully custom way of representing the resources we expose in -Horizon, we use [HAL](http://stateless.co/hal_specification.html). HAL is a -hypermedia format in JSON that remains simple while giving us a couple of -benefits such as simpler client integration for several languages. See [this -wiki page](https://github.com/mikekelly/hal_specification/wiki/Libraries) for a -list of libraries. - -## Attributes, Links, Embedded Resources - -At its simplest, a HAL response is just a JSON object with a couple of reserved -property names: `_links` is used for expressing links and `_embedded` is used -for bundling other HAL objects with the response. Other than links and embedded -objects, **HAL is just JSON**. - -### Links - -HAL is a hypermedia format, like HTML, in that it has a mechanism to express -links between documents. Let's look at a simple example: - -```json -{ - "_links": { - "self": { - "href": "/ledgers/1" - }, - "transactions": { - "href": "/ledgers/1/transactions{?cursor,limit,order}", - "templated": true - } - }, - "id": "43cf4db3741a7d6c2322e7b646320ce9d7b099a0b3501734dcf70e74a8a4e637", - "hash": "43cf4db3741a7d6c2322e7b646320ce9d7b099a0b3501734dcf70e74a8a4e637", - "prev_hash": "", - "sequence": 1, - "transaction_count": 0, - "operation_count": 0, - "closed_at": "0001-01-01T00:00:00Z", - "total_coins": "100000000000.0000000", - "fee_pool": "0.0000000", - "base_fee_in_stroops": 100, - "base_reserve_in_stroops": 100000000, - "max_tx_set_size": 50 -} -``` - -The above response is for the genesis ledger of the Stellar test network, and -the links in the `_links` attribute provide links to other relavant resources in -Horizon. Notice the object beneath the `transactions` key. The key of each -link specifies that links relation to the current resource, and in this case -`transactions` means "Transactions that occurred in this ledger". Logically, -you should expect that resource to respond with a collection of transactions -with all of the results having a `ledger_sequence` attribute equal to 1. - -The `transactions` link is also _templated_, which means that the `href` -attribute of the link is actually a URI template, as specified by [RFC -6570](https://tools.ietf.org/html/rfc6570). We use URI templates to show you -what parameters a given resource can take. You must evaluate the template to a -valid URI before navigating to it. - -## Pages - -Pages represent a subset of a larger collection of objects. -As an example, it would be unfeasible to provide the -[All Transactions](../reference/endpoints/transactions-all.md) endpoint without paging. -Over time there will be millions of transactions in the Stellar network's ledger -and returning them all over a single request would be unfeasible. - -Read more about paging in following docs: -- [Page](../reference/resources/page.md) -- [Paging](./paging.md) diff --git a/services/horizon/internal/docs/reference/streaming.md b/services/horizon/internal/docs/reference/streaming.md deleted file mode 100644 index 4cc4aee979..0000000000 --- a/services/horizon/internal/docs/reference/streaming.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Streaming -replacement: https://developers.stellar.org/api/introduction/streaming/ ---- - -## Streaming - -Certain endpoints in Horizon can be called in streaming mode using Server-Sent Events. This mode will keep the connection to Horizon open and Horizon will continue to return responses as ledgers close. All parameters for the endpoints that allow this mode are the same. The way a caller initiates this mode is by setting `Accept: text/event-stream` in the HTTP header when you make the request. -You can read an example of using the streaming mode in the [Follow Received Payments](./tutorials/follow-received-payments.md) tutorial. - -Endpoints that currently support streaming: -* [Account](./endpoints/accounts-single.md) -* [Effects](./endpoints/effects-all.md) -* [Ledgers](./endpoints/ledgers-all.md) -* [Offers](./endpoints/offers-for-account.md) -* [Operations](./endpoints/operations-all.md) -* [Orderbook](./endpoints/orderbook-details.md) -* [Payments](./endpoints/payments-all.md) -* [Transactions](./endpoints/transactions-all.md) -* [Trades](./endpoints/trades.md) diff --git a/services/horizon/internal/docs/reference/tutorials/follow-received-payments.md b/services/horizon/internal/docs/reference/tutorials/follow-received-payments.md deleted file mode 100644 index 667384ae68..0000000000 --- a/services/horizon/internal/docs/reference/tutorials/follow-received-payments.md +++ /dev/null @@ -1,213 +0,0 @@ ---- -title: Follow Received Payments ---- - -This tutorial shows how easy it is to use Horizon to watch for incoming payments on an [account](../../reference/resources/account.md) -using JavaScript and `EventSource`. We will eschew using [`js-stellar-sdk`](https://github.com/stellar/js-stellar-sdk), the -high-level helper library, to show that it is possible for you to perform this -task on your own, with whatever programming language you would like to use. - -This tutorial assumes that you: - -- Have node.js installed locally on your machine. -- Have curl installed locally on your machine. -- Are running on Linux, OS X, or any other system that has access to a bash-like - shell. -- Are familiar with launching and running commands in a terminal. - -In this tutorial we will learn: - -- How to create a new account. -- How to fund your account using friendbot. -- How to follow payments to your account using curl and EventSource. - -## Project Skeleton - -Let's get started by building our project skeleton: - -```bash -$ mkdir follow_tutorial -$ cd follow_tutorial -$ npm install --save stellar-base -$ npm install --save eventsource -``` - -This should have created a `package.json` in the `follow_tutorial` directory. -You can check that everything went well by running the following command: - -```bash -$ node -e "require('stellar-base')" -``` - -Everything was successful if no output it generated from the above command. Now -let's write a script to create a new account. - -## Creating an account - -Create a new file named `make_account.js` and paste the following text into it: - -```javascript -var Keypair = require("stellar-base").Keypair; - -var newAccount = Keypair.random(); - -console.log("New key pair created!"); -console.log(" Account ID: " + newAccount.publicKey()); -console.log(" Secret: " + newAccount.secret()); -``` - -Save the file and run it: - -```bash -$ node make_account.js -New key pair created! - Account ID: GB7JFK56QXQ4DVJRNPDBXABNG3IVKIXWWJJRJICHRU22Z5R5PI65GAK3 - Secret: SCU36VV2OYTUMDSSU4EIVX4UUHY3XC7N44VL4IJ26IOG6HVNC7DY5UJO -$ -``` - -Before our account can do anything it must be funded. Indeed, before an account -is funded it does not truly exist! - -## Funding your account - -The Stellar test network provides the Friendbot, a tool that developers -can use to get testnet lumens for testing purposes. To fund your account, simply -execute the following curl command: - -```bash -$ curl "https://friendbot.stellar.org/?addr=GB7JFK56QXQ4DVJRNPDBXABNG3IVKIXWWJJRJICHRU22Z5R5PI65GAK3" -``` - -Don't forget to replace the account id above with your own. If the request -succeeds, you should see a response like: - -```json -{ - "hash": "ed9e96e136915103f5d8978cbb2036628e811f2c59c4c3d88534444cf504e360", - "result": "received", - "submission_result": "000000000000000a0000000000000001000000000000000000000000" -} -``` - -After a few seconds, the Stellar network will perform consensus, close the -ledger, and your account will have been created. Next up we will write a command -that watches for new payments to your account and outputs a message to the -terminal. - -## Following payments using `curl` - -To follow new payments connected to your account you simply need to send `Accept: text/event-stream` header to the [/payments](../../reference/endpoints/payments-all.md) endpoint. - -```bash -$ curl -H "Accept: text/event-stream" "https://horizon-testnet.stellar.org/accounts/GB7JFK56QXQ4DVJRNPDBXABNG3IVKIXWWJJRJICHRU22Z5R5PI65GAK3/payments" -``` - -As a result you will see something like: - -```bash -retry: 1000 -event: open -data: "hello" - -id: 713226564145153 -data: {"_links":{"effects":{"href":"/operations/713226564145153/effects/{?cursor,limit,order}","templated":true}, - "precedes":{"href":"/operations?cursor=713226564145153\u0026order=asc"}, - "self":{"href":"/operations/713226564145153"}, - "succeeds":{"href":"/operations?cursor=713226564145153\u0026order=desc"}, - "transactions":{"href":"/transactions/713226564145152"}}, - "account":"GB7JFK56QXQ4DVJRNPDBXABNG3IVKIXWWJJRJICHRU22Z5R5PI65GAK3", - "funder":"GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K", - "id":713226564145153, - "paging_token":"713226564145153", - "starting_balance":"10000", - "type_i":0, - "type":"create_account"} -``` - -Every time you receive a new payment you will get a new row of data. Payments is not the only endpoint that supports streaming. You can also stream transactions [/transactions](../../reference/endpoints/transactions-all.md) and operations [/operations](../../reference/endpoints/operations-all.md). - -## Following payments using `EventStream` - -> **Warning!** `EventSource` object does not reconnect for certain error types so it can stop working. -> If you need a reliable streaming connection please use our [SDK](https://github.com/stellar/js-stellar-sdk). - -Another way to follow payments is writing a simple JS script that will stream payments and print them to console. Create `stream_payments.js` file and paste the following code into it: - -```js -var EventSource = require('eventsource'); -var es = new EventSource('https://horizon-testnet.stellar.org/accounts/GB7JFK56QXQ4DVJRNPDBXABNG3IVKIXWWJJRJICHRU22Z5R5PI65GAK3/payments'); -es.onmessage = function(message) { - var result = message.data ? JSON.parse(message.data) : message; - console.log('New payment:'); - console.log(result); -}; -es.onerror = function(error) { - console.log('An error occurred!'); -} -``` -Now, run our script: `node stream_payments.js`. You should see following output: -```bash -New payment: -{ _links: - { effects: - { href: '/operations/713226564145153/effects/{?cursor,limit,order}', - templated: true }, - precedes: { href: '/operations?cursor=713226564145153&order=asc' }, - self: { href: '/operations/713226564145153' }, - succeeds: { href: '/operations?cursor=713226564145153&order=desc' }, - transactions: { href: '/transactions/713226564145152' } }, - account: 'GB7JFK56QXQ4DVJRNPDBXABNG3IVKIXWWJJRJICHRU22Z5R5PI65GAK3', - funder: 'GBS43BF24ENNS3KPACUZVKK2VYPOZVBQO2CISGZ777RYGOPYC2FT6S3K', - id: 713226564145153, - paging_token: '713226564145153', - starting_balance: '10000', - type_i: 0, - type: 'create_account' } -``` - -## Testing it out - -We now know how to get a stream of transactions to an account. Let's check if our solution actually works and if new payments appear. Let's watch as we send a payment ([`create_account` operation](../../../guides/concepts/list-of-operations.html#create-account)) from our account to another account. - -We use the `create_account` operation because we are sending payment to a new, unfunded account. If we were sending payment to an account that is already funded, we would use the [`payment` operation](../../../guides/concepts/list-of-operations.html#payment). - -First, let's check our account sequence number so we can create a payment transaction. To do this we send a request to horizon: - -```bash -$ curl "https://horizon-testnet.stellar.org/accounts/GB7JFK56QXQ4DVJRNPDBXABNG3IVKIXWWJJRJICHRU22Z5R5PI65GAK3" -``` - -Sequence number can be found under the `sequence` field. The current sequence number is `713226564141056`. Save this value somewhere. - -Now, create `make_payment.js` file and paste the following code into it: - -```js -var StellarBase = require("stellar-base"); -StellarBase.Network.useTestNetwork(); - -var keypair = StellarBase.Keypair.fromSecret('SCU36VV2OYTUMDSSU4EIVX4UUHY3XC7N44VL4IJ26IOG6HVNC7DY5UJO'); -var account = new StellarBase.Account(keypair.publicKey(), "713226564141056"); - -var amount = "100"; -var transaction = new StellarBase.TransactionBuilder(account) - .addOperation(StellarBase.Operation.createAccount({ - destination: StellarBase.Keypair.random().publicKey(), - startingBalance: amount - })) - .build(); - -transaction.sign(keypair); - -console.log(transaction.toEnvelope().toXDR().toString("base64")); -``` - -After running this script you should see a signed transaction blob. To submit this transaction we send it to horizon or stellar-core. But before we do, let's open a new console and start our previous script by `node stream_payments.js`. - -Now to send a transaction just use horizon: - -```bash -curl -H "Content-Type: application/json" -X POST -d '{"tx":"AAAAAH6Sq76F4cHVMWvGG4AtNtFVIvayUxSgR401rPY9ej3TAAAD6AACiK0AAAABAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAKc1j3y10+nI+sxuXlmFz71JS35mp/RcPCP45Gw0obdAAAAAAAAAAAAExLQAAAAAAAAAAAT16PdMAAABAsJTBC5N5B9Q/9+ZKS7qkMd/wZHWlP6uCCFLzeD+JWT60/VgGFCpzQhZmMg2k4Vg+AwKJTwko3d7Jt3Y6WhjLCg=="}' "https://horizon-testnet.stellar.org/transactions" -``` - -You should see a new payment in a window running `stream_payments.js` script. diff --git a/services/horizon/internal/docs/reference/xdr.md b/services/horizon/internal/docs/reference/xdr.md deleted file mode 100644 index c80063394c..0000000000 --- a/services/horizon/internal/docs/reference/xdr.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: XDR -replacement: https://developers.stellar.org/api/introduction/xdr/ ---- - -**XDR**, also known as _External Data Representation_, is used extensively in -the Stellar Network, especially in the core protocol. The ledger, transactions, results, -history, and even the messages passed between computers running stellar-core -are encoded using XDR. - -XDR is specified in [RFC 4506](http://tools.ietf.org/html/rfc4506.html). - -Since XDR is a binary format and not known as widely as JSON for example, we try -to hide most of it from Horizon. Instead, we opt to interpret the XDR for you -and present the values as JSON attributes. That said, we also expose the XDR -to you so you can get access to the raw, canonical data. - -In general, Horizon will encode the XDR structures in base64 so that they can be -transmitted within a json body. You should decode the base64 string -into a byte stream, then decode the XDR into an in-memory data structure. - -## .X files - -Data structures in XDR are specified in an _interface definition file_ (IDL). -The IDL files used for the Stellar Network are available -[on GitHub](https://github.com/stellar/stellar-core/tree/master/src/xdr). From a9391b03909ecbad827c24e55b323f8bc61dd948 Mon Sep 17 00:00:00 2001 From: tamirms Date: Tue, 11 Jun 2024 16:43:36 +0100 Subject: [PATCH 187/234] services/horizon: Update guide for developers (#5337) --- .../docker/docker-compose.standalone.yml | 2 +- services/horizon/docker/start.sh | 14 ++- .../internal/docs/GUIDE_FOR_DEVELOPERS.md | 99 ++++++++++--------- .../horizon/internal/docs/TESTING_NOTES.md | 4 +- 4 files changed, 65 insertions(+), 54 deletions(-) diff --git a/services/horizon/docker/docker-compose.standalone.yml b/services/horizon/docker/docker-compose.standalone.yml index a537be0fb0..7d909dd6e0 100644 --- a/services/horizon/docker/docker-compose.standalone.yml +++ b/services/horizon/docker/docker-compose.standalone.yml @@ -14,7 +14,7 @@ services: core: platform: linux/amd64 - image: ${CORE_IMAGE:-stellar/stellar-core:19.11.0-1323.7fb6d5e88.focal} + image: ${CORE_IMAGE:-stellar/stellar-core:21} depends_on: - core-postgres - core-upgrade diff --git a/services/horizon/docker/start.sh b/services/horizon/docker/start.sh index 824fa19a0a..af098dda4f 100755 --- a/services/horizon/docker/start.sh +++ b/services/horizon/docker/start.sh @@ -2,20 +2,30 @@ set -e +# Use the dirname directly, without changing directories +if [[ $BASH_SOURCE = */* ]]; then + DOCKER_DIR=${BASH_SOURCE%/*}/ +else + DOCKER_DIR=./ +fi + +echo "Docker dir is $DOCKER_DIR" + NETWORK=${1:-testnet} case $NETWORK in standalone) - DOCKER_FLAGS="-f docker-compose.yml -f docker-compose.standalone.yml" + DOCKER_FLAGS="-f ${DOCKER_DIR}docker-compose.yml -f ${DOCKER_DIR}docker-compose.standalone.yml" echo "running on standalone network" ;; pubnet) - DOCKER_FLAGS="-f docker-compose.yml -f docker-compose.pubnet.yml" + DOCKER_FLAGS="-f ${DOCKER_DIR}docker-compose.yml -f ${DOCKER_DIR}docker-compose.pubnet.yml" echo "running on public network" ;; testnet) + DOCKER_FLAGS="-f ${DOCKER_DIR}docker-compose.yml" echo "running on test network" ;; diff --git a/services/horizon/internal/docs/GUIDE_FOR_DEVELOPERS.md b/services/horizon/internal/docs/GUIDE_FOR_DEVELOPERS.md index b076dc4dd4..cdbc1b97c7 100644 --- a/services/horizon/internal/docs/GUIDE_FOR_DEVELOPERS.md +++ b/services/horizon/internal/docs/GUIDE_FOR_DEVELOPERS.md @@ -6,13 +6,13 @@ This document describes how to build Horizon from source, so that you can test a - A [Unix-like](https://en.wikipedia.org/wiki/Unix-like) operating system with the common core commands (cp, tar, mkdir, bash, etc.) - Go (this repository is officially supported on the last [two releases of Go](https://go.dev/doc/devel/release)) - [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) (to check out Horizon's source code) -- [mercurial](https://www.mercurial-scm.org/) (needed for `go-dep`) - [Docker](https://www.docker.com/products/docker-desktop) +- [stellar-core](#building-stellar-core) ## The Go Monorepo All the code for Horizon resides in our Go monorepo. ```bash -git clone https://github.com/go.git +git clone https://github.com/stellar/go.git ``` If you want to contribute to the project, consider forking the repository and cloning the fork instead. @@ -20,11 +20,11 @@ If you want to contribute to the project, consider forking the repository and cl The [start.sh](/services/horizon/docker/start.sh) script builds horizon from current source, and then runs docker-compose to start the docker containers with runtime configs for horizon, postgres, and optionally core if the optional `standalone` network parameter was included. The script takes one optional parameter which configures the Stellar network used by the docker containers. If no parameter is supplied, the containers will run on the Stellar test network. Read more about the public and private networks in the [public documentation](https://developers.stellar.org/docs/fundamentals-and-concepts/testnet-and-pubnet#testnet) -`./start.sh pubnet` will run the containers on the Stellar public network. +`./services/horizon/docker/start.sh pubnet` will run the containers on the Stellar public network. -`./start.sh standalone` will run the containers on a private standalone Stellar network. +`./services/horizon/docker/start.sh standalone` will run the containers on a private standalone Stellar network. -`./start.sh testnet` will run the containers on the Stellar test network. +`./services/horizon/docker/start.sh testnet` will run the containers on the Stellar test network. The following ports will be exposed: - Horizon: **8000** @@ -42,47 +42,21 @@ We will now configure a development environment to run Horizon service locally w ### Building Stellar Core Horizon requires an instance of stellar-core binary on the same host. This is referred to as the `Captive Core`. Since, we are running horizon for dev purposes, we recommend considering two approaches to get the stellar-core binary, if saving time is top priority and your development machine is on a linux debian o/s, then consider installing the debian package, otherwise the next option available is to compile the core source directly to binary on your machine, refer to [INSTALL.md](https://github.com/stellar/stellar-core/blob/master/INSTALL.md) file for the instructions on both approaches. -### Building Horizon - -1. Change to the horizon services directory - `cd go/services/horizon/`. -2. Compile the Horizon binary: `go build -o stellar-horizon && go install`. You should see the resulting `stellar-horizon` executable in `go/services/horizon`. -3. Add the executable to your PATH in your `~/.bashrc` or equivalent, for easy access: `export PATH=$PATH:{absolute-path-to-horizon-folder}` - -Open a new terminal. Confirm everything worked by running `stellar-horizon --help` successfully. You should see an informative message listing the command line options supported by Horizon. - ### Database Setup -Horizon uses a Postgres database backend to record information ingested from an associated Stellar Core. The unit and integration tests will also attempt to reference a Postgres db server at ``localhost:5432`` with trust auth method enabled by default for ``postgres`` user. You can either install the server locally or run any type of docker container that hosts the database server. We recommend using the [docker-compose.yml](/services/horizon/docker/docker-compose.yml) file in the ``docker`` folder: +Horizon uses a Postgres database to record information ingested from Stellar Core. The unit and integration tests also expect a Postgres DB to be running at ``localhost:5432`` with trust auth method enabled by default for the ``postgres`` user. You can run the following command to spin up a Horizon database as a docker container: ```bash -docker-compose -f ./docker/docker-compose.yml up horizon-postgres +docker-compose -f ./services/horizon/docker/docker-compose.yml up -d horizon-postgres ``` -This starts a Horizon Postgres docker container and exposes it on the port 5432. Note that while Horizon will run locally, it's PostgresQL db will run in docker. +The docker container will accept database connections on port 5432. Note that while Horizon will run locally, it's PostgresQL db will run in docker. To shut down all docker containers and free up resources, run the following command: ```bash -docker-compose -f ./docker/docker-compose.yml down -``` - -### Run tests -At this point you should be able to run Horizon's unit tests: -```bash -cd go/services/horizon/ -go test ./... +docker-compose -f ./services/horizon/docker/docker-compose.yml down --remove-orphans -v ``` -To run the integration tests, you need to set some environment variables: +### Running Horizon in an IDE -```bash -export HORIZON_INTEGRATION_TESTS_ENABLED=true -export HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL=19 -export HORIZON_INTEGRATION_TESTS_DOCKER_IMG=stellar/stellar-core:19.11.0-1323.7fb6d5e88.focal -go test -race -timeout 25m -v ./services/horizon/internal/integration/... -``` -Note that this will also require a Postgres instance running on port 5432 either locally or exposed through a docker container. Also note that the ``POSTGRES_HOST_AUTH_METHOD`` has been enabled. - -### Setup Debug Configuration in IDE - -#### Code Debug Add a debug configuration in your IDE to attach a debugger to the local Horizon process and set breakpoints in your code. Here is an example configuration for VS Code: ```json @@ -94,9 +68,8 @@ Add a debug configuration in your IDE to attach a debugger to the local Horizon "program": "${workspaceRoot}/services/horizon/main.go", "env": { "DATABASE_URL": "postgres://postgres@localhost:5432/horizon?sslmode=disable", - "CAPTIVE_CORE_CONFIG_APPEND_PATH": "./ingest/ledgerbackend/configs/captive-core-testnet.cfg", - "HISTORY_ARCHIVE_URLS": "https://history.stellar.org/prd/core-testnet/core_testnet_001,https://history.stellar.org/prd/core-testnet/core_testnet_002", - "NETWORK_PASSPHRASE": "Test SDF Network ; September 2015", + "APPLY_MIGRATIONS": "true", + "NETWORK": "testnet", "PER_HOUR_RATE_LIMIT": "0" }, "args": [] @@ -104,8 +77,36 @@ Add a debug configuration in your IDE to attach a debugger to the local Horizon ``` If all is well, you should see ingest logs written to standard out. You can read more about configuring the different environment variables in [Configuring](https://developers.stellar.org/docs/run-api-server/configuring) section of our public documentation. -#### Test Debug -You can also use a similar configuration to debug the integration and unit tests. For e.g. here is a configuration for debugging the ```TestFilteringAccountWhiteList``` integration test. +### Building Horizon Binary + +1. Change to the horizon services directory - `cd go/services/horizon/`. +2. Compile the Horizon binary: `go build -o stellar-horizon && go install`. You should see the resulting `stellar-horizon` executable in `go/services/horizon`. +3. Add the executable to your PATH in your `~/.bashrc` or equivalent, for easy access: `export PATH=$PATH:{absolute-path-to-horizon-folder}` + +Open a new terminal. Confirm everything worked by running `stellar-horizon --help` successfully. You should see an informative message listing the command line options supported by Horizon. + +### Run tests + +Once you have [installed stellar-core](#building-stellar-core) on your machine and have the +[Horizon database](#database-setup) up and running, you should be able to run Horizon's unit tests: + +```bash +cd go/services/horizon/ +go test ./... +``` + +To run the integration tests, you need to set some environment variables: + +```bash +export HORIZON_INTEGRATION_TESTS_ENABLED=true +export HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL=21 +export HORIZON_INTEGRATION_TESTS_DOCKER_IMG=stellar/stellar-core:21 +go test -race -timeout 25m -v ./services/horizon/internal/integration/... +``` + +#### Running tests in IDE + +You can debug integration and unit tests in your IDE. For example, here is a VS Code configuration for debugging the ```TestFilteringAccountWhiteList``` integration test. ```json { "name": "Debug Test Function", @@ -115,8 +116,8 @@ You can also use a similar configuration to debug the integration and unit tests "program": "${workspaceRoot}/services/horizon/internal/integration", "env": { "HORIZON_INTEGRATION_TESTS_ENABLED": "true", - "HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL": "19", - "HORIZON_INTEGRATION_TESTS_DOCKER_IMG": "stellar/stellar-core:19.11.0-1323.7fb6d5e88.focal" + "HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL": "21", + "HORIZON_INTEGRATION_TESTS_DOCKER_IMG": "stellar/stellar-core:21" }, "args": [ "-test.run", @@ -166,18 +167,18 @@ this customization is only applicable when running a standalone network, as it r ## Using a specific version of Stellar Core -By default, the Docker Compose file is configured to use version 19 of Protocol and Stellar Core. You can specify optional environment variables from the command shell for stating version overrides for either the docker-compose or start.sh invocations. +By default, the Docker Compose file is configured to use version 21 of Protocol and Stellar Core. You can specify optional environment variables from the command shell for stating version overrides for either the docker-compose or start.sh invocations. ```bash -export PROTOCOL_VERSION="19" -export CORE_IMAGE="stellar/stellar-core:19.11.0-1323.7fb6d5e88.focal" -export STELLAR_CORE_VERSION="19.11.0-1323.7fb6d5e88.focal" +export PROTOCOL_VERSION="21" +export CORE_IMAGE="stellar/stellar-core:21" +export STELLAR_CORE_VERSION="21.0.0-1872.c6f474133.focal" ``` Example: -Runs Stellar Protocol and Core version 19, for any mode of testnet, standalone, pubnet +Runs Stellar Protocol and Core version 21, for any mode of testnet, standalone, pubnet ```bash -PROTOCOL_VERSION=19 CORE_IMAGE=stellar/stellar-core:19.11.0-1323.7fb6d5e88.focal STELLAR_CORE_VERSION=19.11.0-1323.7fb6d5e88.focal ./start.sh [standalone|pubnet] +PROTOCOL_VERSION=21 CORE_IMAGE=stellar/stellar-core:21 STELLAR_CORE_VERSION=21.0.0-1872.c6f474133.focal ./start.sh [standalone|pubnet] ``` ## **Logging** diff --git a/services/horizon/internal/docs/TESTING_NOTES.md b/services/horizon/internal/docs/TESTING_NOTES.md index 42577173ce..c7db9cd465 100644 --- a/services/horizon/internal/docs/TESTING_NOTES.md +++ b/services/horizon/internal/docs/TESTING_NOTES.md @@ -15,8 +15,8 @@ go test github.com/stellar/go/services/horizon/... Before running integration tests, you also need to set some environment variables: ```bash export HORIZON_INTEGRATION_TESTS_ENABLED=true -export HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL=19 -export HORIZON_INTEGRATION_TESTS_DOCKER_IMG=stellar/stellar-core:19.11.0-1323.7fb6d5e88.focal +export HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL=21 +export HORIZON_INTEGRATION_TESTS_DOCKER_IMG=stellar/stellar-core:21.0.0-1872.c6f474133.focal ``` Make sure to check [horizon.yml](/.github/workflows/horizon.yml) for the latest core image version. From 4579697cf3bfeacfd8bc460df7f5f02b3cfd16fb Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 13 Jun 2024 12:17:05 -0700 Subject: [PATCH 188/234] exp/services/ledgerexporter: refactor ledgerexporter cli into sub-commands (#5335) #hubble-271: create append and scan-and-fill sub-commands, allow captive core config override --- exp/services/ledgerexporter/README.md | 51 ++-- exp/services/ledgerexporter/config.toml | 11 +- exp/services/ledgerexporter/docker/start | 10 +- exp/services/ledgerexporter/internal/app.go | 50 +-- .../ledgerexporter/internal/app_test.go | 36 +-- .../ledgerexporter/internal/config.go | 209 +++++++++---- .../ledgerexporter/internal/config_test.go | 288 ++++++++++++++---- .../ledgerexporter/internal/exportmanager.go | 30 +- .../internal/exportmanager_test.go | 36 ++- .../internal/ledger_meta_archive.go | 6 +- .../internal/ledger_meta_archive_test.go | 12 +- exp/services/ledgerexporter/internal/main.go | 94 ++++++ .../ledgerexporter/internal/main_test.go | 116 +++++++ .../internal/test/10perfile.toml | 9 +- .../internal/test/15perfile.toml | 9 +- .../internal/test/1perfile.toml | 9 +- .../internal/test/64perfile.toml | 9 +- .../internal/test/captive-core-test.cfg | 12 + .../test/invalid_captive_core_toml_path.toml | 5 + .../internal/test/invalid_empty.toml | 1 + .../test/invalid_preconfigured_network.toml | 4 + .../internal/test/no_core_bin.toml | 3 + .../internal/test/no_network.toml | 11 - .../ledgerexporter/internal/test/test.toml | 11 +- .../internal/test/useragent.toml | 7 + .../test/valid_captive_core_manual.toml | 5 + .../test/valid_captive_core_override.toml | 4 + .../valid_captive_core_override_archives.toml | 4 + .../valid_captive_core_preconfigured.toml | 4 + .../internal/test/validate_start_end.toml | 9 +- .../ledgerexporter/internal/uploader_test.go | 2 +- exp/services/ledgerexporter/main.go | 23 +- .../ledgerbackend/buffered_storage_backend.go | 2 +- .../buffered_storage_backend_test.go | 2 +- support/datastore/datastore.go | 5 +- support/datastore/datastore_test.go | 2 +- support/datastore/gcs_datastore.go | 8 +- support/datastore/gcs_test.go | 24 +- support/datastore/history_archive.go | 10 +- support/datastore/ledgerbatch_config.go | 8 +- support/datastore/ledgerbatch_config_test.go | 4 +- support/datastore/metadata.go | 6 +- support/datastore/metadata_test.go | 10 +- support/datastore/resumablemanager_test.go | 53 ++-- support/datastore/resumeablemanager.go | 11 +- 45 files changed, 874 insertions(+), 361 deletions(-) create mode 100644 exp/services/ledgerexporter/internal/main.go create mode 100644 exp/services/ledgerexporter/internal/main_test.go create mode 100644 exp/services/ledgerexporter/internal/test/captive-core-test.cfg create mode 100644 exp/services/ledgerexporter/internal/test/invalid_captive_core_toml_path.toml create mode 100644 exp/services/ledgerexporter/internal/test/invalid_empty.toml create mode 100644 exp/services/ledgerexporter/internal/test/invalid_preconfigured_network.toml create mode 100644 exp/services/ledgerexporter/internal/test/no_core_bin.toml delete mode 100644 exp/services/ledgerexporter/internal/test/no_network.toml create mode 100644 exp/services/ledgerexporter/internal/test/useragent.toml create mode 100644 exp/services/ledgerexporter/internal/test/valid_captive_core_manual.toml create mode 100644 exp/services/ledgerexporter/internal/test/valid_captive_core_override.toml create mode 100644 exp/services/ledgerexporter/internal/test/valid_captive_core_override_archives.toml create mode 100644 exp/services/ledgerexporter/internal/test/valid_captive_core_preconfigured.toml diff --git a/exp/services/ledgerexporter/README.md b/exp/services/ledgerexporter/README.md index 967bd1dc9e..57757508e1 100644 --- a/exp/services/ledgerexporter/README.md +++ b/exp/services/ledgerexporter/README.md @@ -21,45 +21,56 @@ type LedgerCloseMetaBatch struct { ### Command Line Options -#### Bounded Mode: -Exports a specific range of ledgers, defined by --start and --end. +#### Scan and Fill Mode: +Exports a specific range of ledgers, defined by --start and --end. Will only export to remote datastore if data is absent. ```bash -ledgerexporter --start --end --config-file +ledgerexporter scan-and-fill --start --end --config-file ``` -#### Unbounded Mode: -Exports ledgers continuously starting from --start. In this mode, the end ledger is either not provided or set to 0. +#### Append Mode: +Exports ledgers initially searching from --start, looking for the next absent ledger sequence number proceeding --start on the data store. If abscence is detected, the export range is narrowed to `--start `. +This feature requires ledgers to be present on the remote data store for some (possibly empty) prefix of the requested range and then absent for the (possibly empty) remainder. + +In this mode, the --end ledger can be provided to stop the process once export has reached that ledger, or if absent or 0 it will result in continous exporting of new ledgers emitted from the network. + + It’s guaranteed that ledgers exported during `append` mode from `start` and up to the last logged ledger file `Uploaded {ledger file name}` were contiguous, meaning all ledgers within that range were exported to the data lake with no gaps or missing ledgers in between. ```bash -ledgerexporter --start --config-file +ledgerexporter append --start --config-file ``` -#### Resumability: -Exporting a ledger range can be optimized further by enabling resumability if the remote data store supports it. +### Configuration (toml): +The `stellar_core_config` supports two ways for configuring captive core: + - use prebuilt captive core config toml, archive urls, and passphrase based on `stellar_core_config.network = testnet|pubnet`. + - manually set the the captive core confg by supplying these core parameters which will override any defaults when `stellar_core_config.network` is present also: + `stellar_core_config.captive_core_toml_path` + `stellar_core_config.history_archive_urls` + `stellar_core_config.network_passphrase` -By default, resumability is disabled, `--resume false` +Ensure you have stellar-core installed and set `stellar_core_config.stellar_core_binary_path` to it's path on o/s. -When enabled, `--resume true`, ledgerexporter will search the remote data store within the requested range, looking for the oldest absent ledger sequence number within range. If abscence is detected, the export range is narrowed to `--start `. -This feature requires all ledgers to be present on the remote data store for some (possibly empty) prefix of the requested range and then absent for the (possibly empty) remainder. - -### Configuration (toml): +Enable web service that will be bound to localhost post and publishes metrics by including `admin_port = {port}` +An example config, demonstrating preconfigured captive core settings and gcs data store config. ```toml -network = "testnet" # Options: `testnet` or `pubnet` +admin_port = 6061 [datastore_config] type = "GCS" [datastore_config.params] -destination_bucket_path = "your-bucket-name/" +destination_bucket_path = "your-bucket-name///" -[exporter_config] +[datastore_config.schema] ledgers_per_file = 64 files_per_partition = 10 -``` -#### Stellar-core configuration: -- The exporter automatically configures stellar-core based on the network specified in the config. -- Ensure you have stellar-core installed and accessible in your system's $PATH. +[stellar_core_config] + network = "testnet" + stellar_core_binary_path = "/my/path/to/stellar-core" + captive_core_toml_path = "my-captive-core.cfg" + history_archive_urls = ["http://testarchiveurl1", "http://testarchiveurl2"] + network_passphrase = "test" +``` ### Exported Files diff --git a/exp/services/ledgerexporter/config.toml b/exp/services/ledgerexporter/config.toml index a56087427c..c41d9376ac 100644 --- a/exp/services/ledgerexporter/config.toml +++ b/exp/services/ledgerexporter/config.toml @@ -1,15 +1,14 @@ -network = "testnet" - [datastore_config] type = "GCS" [datastore_config.params] -destination_bucket_path = "exporter-test/ledgers" +destination_bucket_path = "exporter-test/ledgers/testnet" -[exporter_config] - ledgers_per_file = 1 - files_per_partition = 64000 +[datastore_config.schema] +ledgers_per_file = 1 +files_per_partition = 64000 [stellar_core_config] + network = "testnet" stellar_core_binary_path = "/usr/local/bin/stellar-core" diff --git a/exp/services/ledgerexporter/docker/start b/exp/services/ledgerexporter/docker/start index eca213de0c..3b61c74eee 100644 --- a/exp/services/ledgerexporter/docker/start +++ b/exp/services/ledgerexporter/docker/start @@ -17,23 +17,25 @@ files_per_partition="${FILES_PER_PARTITION:-64000}" # Generate TOML configuration cat < config.toml -network = "${NETWORK}" [datastore_config] type = "GCS" [datastore_config.params] -destination_bucket_path = "${ARCHIVE_TARGET}" +destination_bucket_path = "${ARCHIVE_TARGET}/${NETWORK}" -[exporter_config] +[datastore_config.schema] ledgers_per_file = $ledgers_per_file files_per_partition = $files_per_partition + +[stellar_core_config] + network = "${NETWORK}" EOF # Check if START or END variables are set if [[ -n "$START" || -n "$END" ]]; then echo "START: $START END: $END" - /usr/bin/ledgerexporter --config-file config.toml --start $START --end $END + /usr/bin/ledgerexporter scan-and-fill --config-file config.toml --start $START --end $END else echo "Error: No ledger range provided." exit 1 diff --git a/exp/services/ledgerexporter/internal/app.go b/exp/services/ledgerexporter/internal/app.go index c6adcd41a0..eb0f544ec3 100644 --- a/exp/services/ledgerexporter/internal/app.go +++ b/exp/services/ledgerexporter/internal/app.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "os" + "os/exec" "os/signal" "sync" "syscall" @@ -72,7 +73,7 @@ type InvalidDataStoreError struct { func (m InvalidDataStoreError) Error() string { return fmt.Sprintf("The remote data store has inconsistent data, "+ "a resumable starting ledger of %v was identified, "+ - "but that is not aligned to expected ledgers-per-file of %v. use '--resume false' to bypass", + "but that is not aligned to expected ledgers-per-file of %v. use 'scan-and-fill' sub-command to bypass", m.LedgerSequence, m.LedgersPerFile) } @@ -82,17 +83,16 @@ type App struct { dataStore datastore.DataStore exportManager *ExportManager uploader Uploader - flags Flags adminServer *http.Server } -func NewApp(flags Flags) *App { +func NewApp() *App { logger.SetLevel(log.DebugLevel) - app := &App{flags: flags} + app := &App{} return app } -func (a *App) init(ctx context.Context) error { +func (a *App) init(ctx context.Context, runtimeSettings RuntimeSettings) error { var err error var archive historyarchive.ArchiveInterface @@ -104,20 +104,22 @@ func (a *App) init(ctx context.Context) error { collectors.NewGoCollector(), ) - if a.config, err = NewConfig(a.flags); err != nil { + if a.config, err = NewConfig(runtimeSettings); err != nil { return errors.Wrap(err, "Could not load configuration") } - if archive, err = datastore.CreateHistoryArchiveFromNetworkName(ctx, a.config.Network); err != nil { + if archive, err = a.config.GenerateHistoryArchive(ctx); err != nil { + return err + } + if err = a.config.ValidateAndSetLedgerRange(ctx, archive); err != nil { return err } - a.config.ValidateAndSetLedgerRange(ctx, archive) - if a.dataStore, err = datastore.NewDataStore(ctx, a.config.DataStoreConfig, a.config.Network); err != nil { + if a.dataStore, err = datastore.NewDataStore(ctx, a.config.DataStoreConfig); err != nil { return errors.Wrap(err, "Could not connect to destination data store") } - if a.config.Resume { + if a.config.Resumable() { if err = a.applyResumability(ctx, - datastore.NewResumableManager(a.dataStore, a.config.Network, a.config.LedgerBatchConfig, archive)); err != nil { + datastore.NewResumableManager(a.dataStore, a.config.DataStoreConfig.Schema, archive)); err != nil { return err } } @@ -129,7 +131,10 @@ func (a *App) init(ctx context.Context) error { } queue := NewUploadQueue(uploadQueueCapacity, registry) - if a.exportManager, err = NewExportManager(a.config, a.ledgerBackend, queue, registry); err != nil { + if a.exportManager, err = NewExportManager(a.config.DataStoreConfig.Schema, + a.ledgerBackend, queue, registry, + a.config.StellarCoreConfig.NetworkPassphrase, + a.config.CoreVersion); err != nil { return err } a.uploader = NewUploader(a.dataStore, queue, registry) @@ -151,8 +156,8 @@ func (a *App) applyResumability(ctx context.Context, resumableManager datastore. // TODO - evaluate a more robust validation of remote data for ledgers-per-file consistency // this assumes ValidateAndSetLedgerRange() has conditioned the a.config.StartLedger to be at least > 1 - if absentLedger > 2 && absentLedger != a.config.LedgerBatchConfig.GetSequenceNumberStartBoundary(absentLedger) { - return NewInvalidDataStoreError(absentLedger, a.config.LedgerBatchConfig.LedgersPerFile) + if absentLedger > 2 && absentLedger != a.config.DataStoreConfig.Schema.GetSequenceNumberStartBoundary(absentLedger) { + return NewInvalidDataStoreError(absentLedger, a.config.DataStoreConfig.Schema.LedgersPerFile) } logger.Infof("For export ledger range start=%d, end=%d, the remote storage has some of this data already, will resume at later start ledger of %d", a.config.StartLedger, a.config.EndLedger, absentLedger) a.config.StartLedger = absentLedger @@ -180,18 +185,19 @@ func newAdminServer(adminPort int, prometheusRegistry *prometheus.Registry) *htt } } -func (a *App) Run() { +func (a *App) Run(runtimeSettings RuntimeSettings) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - if err := a.init(ctx); err != nil { - var dataAlreadyExported DataAlreadyExportedError + if err := a.init(ctx, runtimeSettings); err != nil { + var dataAlreadyExported *DataAlreadyExportedError if errors.As(err, &dataAlreadyExported) { logger.Info(err.Error()) logger.Info("Shutting down ledger-exporter") - return + return nil } - logger.WithError(err).Fatal("Stopping ledger-exporter") + logger.WithError(err).Error("Stopping ledger-exporter") + return err } defer a.close() @@ -254,12 +260,16 @@ func (a *App) Run() { logger.WithError(err).Warn("error in internalServer.Shutdown") } } + return nil } // newLedgerBackend Creates and initializes captive core ledger backend // Currently, only supports captive-core as ledger backend func newLedgerBackend(config *Config, prometheusRegistry *prometheus.Registry) (ledgerbackend.LedgerBackend, error) { - captiveConfig, err := config.generateCaptiveCoreConfig() + // best effort check on a core bin available from PATH to provide as default if + // no core bin is provided from config. + coreBinFromPath, _ := exec.LookPath("stellar-core") + captiveConfig, err := config.GenerateCaptiveCoreConfig(coreBinFromPath) if err != nil { return nil, err } diff --git a/exp/services/ledgerexporter/internal/app_test.go b/exp/services/ledgerexporter/internal/app_test.go index 2063fd21a2..bb715baa6f 100644 --- a/exp/services/ledgerexporter/internal/app_test.go +++ b/exp/services/ledgerexporter/internal/app_test.go @@ -12,7 +12,7 @@ import ( func TestApplyResumeHasStartError(t *testing.T) { ctx := context.Background() app := &App{} - app.config = &Config{StartLedger: 10, EndLedger: 19, Resume: true} + app.config = &Config{StartLedger: 10, EndLedger: 19, Mode: Append} mockResumableManager := &datastore.MockResumableManager{} mockResumableManager.On("FindStart", ctx, uint32(10), uint32(19)).Return(uint32(0), false, errors.New("start error")).Once() @@ -24,7 +24,7 @@ func TestApplyResumeHasStartError(t *testing.T) { func TestApplyResumeDatastoreComplete(t *testing.T) { ctx := context.Background() app := &App{} - app.config = &Config{StartLedger: 10, EndLedger: 19, Resume: true} + app.config = &Config{StartLedger: 10, EndLedger: 19, Mode: Append} mockResumableManager := &datastore.MockResumableManager{} mockResumableManager.On("FindStart", ctx, uint32(10), uint32(19)).Return(uint32(0), false, nil).Once() @@ -38,10 +38,10 @@ func TestApplyResumeInvalidDataStoreLedgersPerFileBoundary(t *testing.T) { ctx := context.Background() app := &App{} app.config = &Config{ - StartLedger: 3, - EndLedger: 9, - Resume: true, - LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + StartLedger: 3, + EndLedger: 9, + Mode: Append, + DataStoreConfig: datastore.DataStoreConfig{Schema: datastore.DataStoreSchema{LedgersPerFile: 10, FilesPerPartition: 50}}, } mockResumableManager := &datastore.MockResumableManager{} // simulate the datastore has inconsistent data, @@ -58,10 +58,10 @@ func TestApplyResumeWithPartialRemoteDataPresent(t *testing.T) { ctx := context.Background() app := &App{} app.config = &Config{ - StartLedger: 10, - EndLedger: 99, - Resume: true, - LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + StartLedger: 10, + EndLedger: 99, + Mode: Append, + DataStoreConfig: datastore.DataStoreConfig{Schema: datastore.DataStoreSchema{LedgersPerFile: 10, FilesPerPartition: 50}}, } mockResumableManager := &datastore.MockResumableManager{} // simulates a data store that had ledger files populated up to seq=49, so the first absent ledger would be 50 @@ -77,10 +77,10 @@ func TestApplyResumeWithNoRemoteDataPresent(t *testing.T) { ctx := context.Background() app := &App{} app.config = &Config{ - StartLedger: 10, - EndLedger: 99, - Resume: true, - LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + StartLedger: 10, + EndLedger: 99, + Mode: Append, + DataStoreConfig: datastore.DataStoreConfig{Schema: datastore.DataStoreSchema{LedgersPerFile: 10, FilesPerPartition: 50}}, } mockResumableManager := &datastore.MockResumableManager{} // simulates a data store that had no data in the requested range @@ -99,10 +99,10 @@ func TestApplyResumeWithNoRemoteDataAndRequestFromGenesis(t *testing.T) { ctx := context.Background() app := &App{} app.config = &Config{ - StartLedger: 2, - EndLedger: 99, - Resume: true, - LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 10, FilesPerPartition: 50}, + StartLedger: 2, + EndLedger: 99, + Mode: Append, + DataStoreConfig: datastore.DataStoreConfig{Schema: datastore.DataStoreSchema{LedgersPerFile: 10, FilesPerPartition: 50}}, } mockResumableManager := &datastore.MockResumableManager{} // simulates a data store that had no data in the requested range diff --git a/exp/services/ledgerexporter/internal/config.go b/exp/services/ledgerexporter/internal/config.go index ef062867f4..ff0ffbe2bf 100644 --- a/exp/services/ledgerexporter/internal/config.go +++ b/exp/services/ledgerexporter/internal/config.go @@ -4,6 +4,7 @@ import ( "context" _ "embed" "fmt" + "os" "os/exec" "strings" @@ -16,20 +17,42 @@ import ( "github.com/stellar/go/support/datastore" "github.com/stellar/go/support/errors" "github.com/stellar/go/support/ordered" + "github.com/stellar/go/support/storage" ) -const Pubnet = "pubnet" -const Testnet = "testnet" +const ( + Pubnet = "pubnet" + Testnet = "testnet" + UserAgent = "ledgerexporter" +) + +type Mode int + +const ( + _ Mode = iota + ScanFill Mode = iota + Append +) + +func (mode Mode) Name() string { + switch mode { + case ScanFill: + return "Scan and Fill" + case Append: + return "Append" + } + return "none" +} -type Flags struct { +type RuntimeSettings struct { StartLedger uint32 EndLedger uint32 ConfigFilePath string - Resume bool - AdminPort uint + Mode Mode } type StellarCoreConfig struct { + Network string `toml:"network"` NetworkPassphrase string `toml:"network_passphrase"` HistoryArchiveUrls []string `toml:"history_archive_urls"` StellarCoreBinaryPath string `toml:"stellar_core_binary_path"` @@ -39,115 +62,126 @@ type StellarCoreConfig struct { type Config struct { AdminPort int `toml:"admin_port"` - Network string `toml:"network"` - DataStoreConfig datastore.DataStoreConfig `toml:"datastore_config"` - LedgerBatchConfig datastore.LedgerBatchConfig `toml:"exporter_config"` - StellarCoreConfig StellarCoreConfig `toml:"stellar_core_config"` + DataStoreConfig datastore.DataStoreConfig `toml:"datastore_config"` + StellarCoreConfig StellarCoreConfig `toml:"stellar_core_config"` + UserAgent string `toml:"user_agent"` StartLedger uint32 EndLedger uint32 - Resume bool + Mode Mode - CoreVersion string + CoreVersion string + SerializedCaptiveCoreToml []byte } -// This will generate the config based on commandline flags and toml +// This will generate the config based on settings // -// flags - command line flags +// settings - requested settings // // return - *Config or an error if any range validation failed. -func NewConfig(flags Flags) (*Config, error) { +func NewConfig(settings RuntimeSettings) (*Config, error) { config := &Config{} - config.StartLedger = uint32(flags.StartLedger) - config.EndLedger = uint32(flags.EndLedger) - config.Resume = flags.Resume + config.StartLedger = uint32(settings.StartLedger) + config.EndLedger = uint32(settings.EndLedger) + config.Mode = settings.Mode - logger.Infof("Requested ledger range start=%d, end=%d, resume=%v", config.StartLedger, config.EndLedger, config.Resume) + logger.Infof("Requested export mode of %v with start=%d, end=%d", settings.Mode.Name(), config.StartLedger, config.EndLedger) var err error - if err = config.processToml(flags.ConfigFilePath); err != nil { + if err = config.processToml(settings.ConfigFilePath); err != nil { return nil, err } - logger.Infof("Config: %v", *config) + logger.Infof("Network Config Archive URLs: %v", config.StellarCoreConfig.HistoryArchiveUrls) + logger.Infof("Network Config Archive Passphrase: %v", config.StellarCoreConfig.NetworkPassphrase) + logger.Infof("Network Config Archive Stellar Core Binary Path: %v", config.StellarCoreConfig.StellarCoreBinaryPath) + logger.Infof("Network Config Archive Stellar Core Toml Config: %v", string(config.SerializedCaptiveCoreToml)) return config, nil } +func (config *Config) Resumable() bool { + return config.Mode == Append +} + // Validates requested ledger range, and will automatically adjust it // to be ledgers-per-file boundary aligned func (config *Config) ValidateAndSetLedgerRange(ctx context.Context, archive historyarchive.ArchiveInterface) error { + + if config.StartLedger < 2 { + return errors.New("invalid start value, must be greater than one.") + } + + if config.Mode == ScanFill && config.EndLedger == 0 { + return errors.New("invalid end value, unbounded mode not supported, end must be greater than start.") + } + + if config.EndLedger != 0 && config.EndLedger <= config.StartLedger { + return errors.New("invalid end value, must be greater than start") + } + latestNetworkLedger, err := datastore.GetLatestLedgerSequenceFromHistoryArchives(archive) + latestNetworkLedger = latestNetworkLedger + (datastore.GetHistoryArchivesCheckPointFrequency() * 2) if err != nil { return errors.Wrap(err, "Failed to retrieve the latest ledger sequence from history archives.") } - logger.Infof("Latest %v ledger sequence was detected as %d", config.Network, latestNetworkLedger) + logger.Infof("Latest ledger sequence was detected as %d", latestNetworkLedger) if config.StartLedger > latestNetworkLedger { - return errors.Errorf("--start %d exceeds latest network ledger %d", + return errors.Errorf("start %d exceeds latest network ledger %d", config.StartLedger, latestNetworkLedger) } - if config.EndLedger != 0 { // Bounded mode - if config.EndLedger < config.StartLedger { - return errors.New("invalid --end value, must be >= --start") - } - if config.EndLedger > latestNetworkLedger { - return errors.Errorf("--end %d exceeds latest network ledger %d", - config.EndLedger, latestNetworkLedger) - } + if config.EndLedger > latestNetworkLedger { + return errors.Errorf("end %d exceeds latest network ledger %d", + config.EndLedger, latestNetworkLedger) } config.adjustLedgerRange() return nil } -func (config *Config) generateCaptiveCoreConfig() (ledgerbackend.CaptiveCoreConfig, error) { - coreConfig := &config.StellarCoreConfig +func (config *Config) GenerateHistoryArchive(ctx context.Context) (historyarchive.ArchiveInterface, error) { + return historyarchive.NewArchivePool(config.StellarCoreConfig.HistoryArchiveUrls, historyarchive.ArchiveOptions{ + ConnectOptions: storage.ConnectOptions{ + UserAgent: config.UserAgent, + Context: ctx, + }, + }) +} - // Look for stellar-core binary in $PATH, if not supplied - if config.StellarCoreConfig.StellarCoreBinaryPath == "" { - var err error - if config.StellarCoreConfig.StellarCoreBinaryPath, err = exec.LookPath("stellar-core"); err != nil { - return ledgerbackend.CaptiveCoreConfig{}, errors.Wrap(err, "Failed to find stellar-core binary") - } - } +// coreBinDefaultPath - a default value to use for core binary path on system. +// +// this will be used if StellarCoreConfig.StellarCoreBinaryPath is not specified +func (config *Config) GenerateCaptiveCoreConfig(coreBinFromPath string) (ledgerbackend.CaptiveCoreConfig, error) { + var err error - if err := config.setCoreVersionInfo(); err != nil { - return ledgerbackend.CaptiveCoreConfig{}, fmt.Errorf("failed to set stellar-core version info: %w", err) + if config.StellarCoreConfig.StellarCoreBinaryPath == "" && coreBinFromPath == "" { + return ledgerbackend.CaptiveCoreConfig{}, errors.New("Invalid captive core config, no stellar-core binary path was provided.") } - var captiveCoreConfig []byte - // Default network config - switch config.Network { - case Pubnet: - coreConfig.NetworkPassphrase = network.PublicNetworkPassphrase - coreConfig.HistoryArchiveUrls = network.PublicNetworkhistoryArchiveURLs - captiveCoreConfig = ledgerbackend.PubnetDefaultConfig - - case Testnet: - coreConfig.NetworkPassphrase = network.TestNetworkPassphrase - coreConfig.HistoryArchiveUrls = network.TestNetworkhistoryArchiveURLs - captiveCoreConfig = ledgerbackend.TestnetDefaultConfig + if config.StellarCoreConfig.StellarCoreBinaryPath == "" { + config.StellarCoreConfig.StellarCoreBinaryPath = coreBinFromPath + } - default: - logger.Fatalf("Invalid network %s", config.Network) + if err = config.setCoreVersionInfo(); err != nil { + return ledgerbackend.CaptiveCoreConfig{}, fmt.Errorf("failed to set stellar-core version info: %w", err) } params := ledgerbackend.CaptiveCoreTomlParams{ - NetworkPassphrase: coreConfig.NetworkPassphrase, - HistoryArchiveURLs: coreConfig.HistoryArchiveUrls, + NetworkPassphrase: config.StellarCoreConfig.NetworkPassphrase, + HistoryArchiveURLs: config.StellarCoreConfig.HistoryArchiveUrls, UseDB: true, } - captiveCoreToml, err := ledgerbackend.NewCaptiveCoreTomlFromData(captiveCoreConfig, params) + captiveCoreToml, err := ledgerbackend.NewCaptiveCoreTomlFromData(config.SerializedCaptiveCoreToml, params) if err != nil { return ledgerbackend.CaptiveCoreConfig{}, errors.Wrap(err, "Failed to create captive-core toml") } return ledgerbackend.CaptiveCoreConfig{ - BinaryPath: coreConfig.StellarCoreBinaryPath, + BinaryPath: config.StellarCoreConfig.StellarCoreBinaryPath, NetworkPassphrase: params.NetworkPassphrase, HistoryArchiveURLs: params.HistoryArchiveURLs, CheckpointFrequency: datastore.GetHistoryArchivesCheckPointFrequency(), @@ -186,34 +220,75 @@ func (config *Config) processToml(tomlPath string) error { // Load config TOML file cfg, err := toml.LoadFile(tomlPath) if err != nil { - return err + return errors.Wrapf(err, "config file %v was not found", tomlPath) } // Unmarshal TOML data into the Config struct - if err := cfg.Unmarshal(config); err != nil { + if err = cfg.Unmarshal(config); err != nil { return errors.Wrap(err, "Error unmarshalling TOML config.") } - // validate TOML data - if config.Network == "" { - return errors.New("Invalid TOML config, 'network' must be set, supported values are 'testnet' or 'pubnet'") + if config.UserAgent == "" { + config.UserAgent = UserAgent } + + if config.StellarCoreConfig.Network == "" && (len(config.StellarCoreConfig.HistoryArchiveUrls) == 0 || config.StellarCoreConfig.NetworkPassphrase == "" || config.StellarCoreConfig.CaptiveCoreTomlPath == "") { + return errors.New("Invalid captive core config, the 'network' parameter must be set to pubnet or testnet or " + + "'stellar_core_config.history_archive_urls' and 'stellar_core_config.network_passphrase' and 'stellar_core_config.captive_core_toml_path' must be set.") + } + + // network config values are an overlay, with network preconfigured values being first if network is present + // and then toml settings specific for passphrase, archiveurls, core toml file can override lastly. + var networkPassPhrase string + var networkArchiveUrls []string + switch config.StellarCoreConfig.Network { + case "": + + case Pubnet: + networkPassPhrase = network.PublicNetworkPassphrase + networkArchiveUrls = network.PublicNetworkhistoryArchiveURLs + config.SerializedCaptiveCoreToml = ledgerbackend.PubnetDefaultConfig + + case Testnet: + networkPassPhrase = network.TestNetworkPassphrase + networkArchiveUrls = network.TestNetworkhistoryArchiveURLs + config.SerializedCaptiveCoreToml = ledgerbackend.TestnetDefaultConfig + + default: + return errors.New("invalid captive core config, " + + "preconfigured_network must be set to 'pubnet' or 'testnet' or network_passphrase, history_archive_urls," + + " and captive_core_toml_path must be set") + } + + if config.StellarCoreConfig.NetworkPassphrase == "" { + config.StellarCoreConfig.NetworkPassphrase = networkPassPhrase + } + + if len(config.StellarCoreConfig.HistoryArchiveUrls) < 1 { + config.StellarCoreConfig.HistoryArchiveUrls = networkArchiveUrls + } + + if config.StellarCoreConfig.CaptiveCoreTomlPath != "" { + if config.SerializedCaptiveCoreToml, err = os.ReadFile(config.StellarCoreConfig.CaptiveCoreTomlPath); err != nil { + return errors.Wrap(err, "Failed to load captive-core-toml-path file") + } + } + return nil } func (config *Config) adjustLedgerRange() { - // Check if either the start or end ledger does not fall on the "LedgersPerFile" boundary // and adjust the start and end ledger accordingly. // Align the start ledger to the nearest "LedgersPerFile" boundary. - config.StartLedger = config.LedgerBatchConfig.GetSequenceNumberStartBoundary(config.StartLedger) + config.StartLedger = config.DataStoreConfig.Schema.GetSequenceNumberStartBoundary(config.StartLedger) // Ensure that the adjusted start ledger is at least 2. config.StartLedger = ordered.Max(2, config.StartLedger) // Align the end ledger (for bounded cases) to the nearest "LedgersPerFile" boundary. if config.EndLedger != 0 { - config.EndLedger = config.LedgerBatchConfig.GetSequenceNumberEndBoundary(config.EndLedger) + config.EndLedger = config.DataStoreConfig.Schema.GetSequenceNumberEndBoundary(config.EndLedger) } logger.Infof("Computed effective export boundary ledger range: start=%d, end=%d", config.StartLedger, config.EndLedger) diff --git a/exp/services/ledgerexporter/internal/config_test.go b/exp/services/ledgerexporter/internal/config_test.go index a4ce76cfe5..37dc574012 100644 --- a/exp/services/ledgerexporter/internal/config_test.go +++ b/exp/services/ledgerexporter/internal/config_test.go @@ -7,118 +7,304 @@ import ( "os/exec" "testing" + "github.com/stellar/go/network" + "github.com/stellar/go/support/datastore" + "github.com/stretchr/testify/require" "github.com/stellar/go/historyarchive" "github.com/stellar/go/support/errors" ) -func TestNewConfigResumeEnabled(t *testing.T) { +func TestNewConfig(t *testing.T) { ctx := context.Background() mockArchive := &historyarchive.MockArchive{} mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: 5}, nil).Once() - config, err := NewConfig(Flags{StartLedger: 1, EndLedger: 2, ConfigFilePath: "test/test.toml", Resume: true}) - config.ValidateAndSetLedgerRange(ctx, mockArchive) + config, err := NewConfig( + RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/test.toml", Mode: Append}) + + require.NoError(t, err) + err = config.ValidateAndSetLedgerRange(ctx, mockArchive) require.NoError(t, err) + require.Equal(t, config.StellarCoreConfig.Network, "pubnet") require.Equal(t, config.DataStoreConfig.Type, "ABC") - require.Equal(t, config.LedgerBatchConfig.FilesPerPartition, uint32(1)) - require.Equal(t, config.LedgerBatchConfig.LedgersPerFile, uint32(3)) - require.True(t, config.Resume) + require.Equal(t, config.DataStoreConfig.Schema.FilesPerPartition, uint32(1)) + require.Equal(t, config.DataStoreConfig.Schema.LedgersPerFile, uint32(3)) + require.Equal(t, config.UserAgent, "ledgerexporter") + require.True(t, config.Resumable()) url, ok := config.DataStoreConfig.Params["destination_bucket_path"] require.True(t, ok) - require.Equal(t, url, "your-bucket-name/subpath") + require.Equal(t, url, "your-bucket-name/subpath/testnet") + mockArchive.AssertExpectations(t) } -func TestNewConfigResumeDisabled(t *testing.T) { +func TestGenerateHistoryArchiveFromPreconfiguredNetwork(t *testing.T) { + ctx := context.Background() + config, err := NewConfig( + RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/valid_captive_core_preconfigured.toml", Mode: Append}) + require.NoError(t, err) - mockArchive := &historyarchive.MockArchive{} - mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: 5}, nil).Once() + _, err = config.GenerateHistoryArchive(ctx) + require.NoError(t, err) +} + +func TestGenerateHistoryArchiveFromManulConfiguredNetwork(t *testing.T) { + ctx := context.Background() + config, err := NewConfig( + RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/valid_captive_core_manual.toml", Mode: Append}) + require.NoError(t, err) + + _, err = config.GenerateHistoryArchive(ctx) + require.NoError(t, err) +} + +func TestNewConfigUserAgent(t *testing.T) { + config, err := NewConfig( + RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/useragent.toml"}) + require.NoError(t, err) + require.Equal(t, config.UserAgent, "useragent_x") +} + +func TestResumeDisabled(t *testing.T) { + // resumable is only enabled when mode is Append + config, err := NewConfig( + RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/test.toml", Mode: ScanFill}) + require.NoError(t, err) + require.False(t, config.Resumable()) +} + +func TestInvalidConfigFilePath(t *testing.T) { + _, err := NewConfig( + RuntimeSettings{ConfigFilePath: "test/notfound.toml"}) + require.ErrorContains(t, err, "config file test/notfound.toml was not found") +} + +func TestNoCaptiveCoreBin(t *testing.T) { + cfg, err := NewConfig( + RuntimeSettings{ConfigFilePath: "test/no_core_bin.toml"}) + require.NoError(t, err) + + _, err = cfg.GenerateCaptiveCoreConfig("") + require.ErrorContains(t, err, "Invalid captive core config, no stellar-core binary path was provided.") +} + +func TestDefaultCaptiveCoreBin(t *testing.T) { + cfg, err := NewConfig( + RuntimeSettings{ConfigFilePath: "test/no_core_bin.toml"}) + require.NoError(t, err) + + cmdOut = "v20.2.0-2-g6e73c0a88\n" + ccConfig, err := cfg.GenerateCaptiveCoreConfig("/test/default/stellar-core") + require.NoError(t, err) + require.Equal(t, ccConfig.BinaryPath, "/test/default/stellar-core") +} + +func TestInvalidCaptiveCorePreconfiguredNetwork(t *testing.T) { + _, err := NewConfig( + RuntimeSettings{ConfigFilePath: "test/invalid_preconfigured_network.toml"}) + + require.ErrorContains(t, err, "invalid captive core config") +} + +func TestValidCaptiveCorePreconfiguredNetwork(t *testing.T) { + cfg, err := NewConfig( + RuntimeSettings{ConfigFilePath: "test/valid_captive_core_preconfigured.toml"}) + require.NoError(t, err) + + require.Equal(t, cfg.StellarCoreConfig.NetworkPassphrase, network.PublicNetworkPassphrase) + require.Equal(t, cfg.StellarCoreConfig.HistoryArchiveUrls, network.PublicNetworkhistoryArchiveURLs) + + cmdOut = "v20.2.0-2-g6e73c0a88\n" + ccConfig, err := cfg.GenerateCaptiveCoreConfig("") + require.NoError(t, err) + + // validates that ingest/ledgerbackend/configs/captive-core-pubnet.cfg was loaded + require.Equal(t, ccConfig.BinaryPath, "test/stellar-core") + require.Equal(t, ccConfig.NetworkPassphrase, network.PublicNetworkPassphrase) + require.Equal(t, ccConfig.HistoryArchiveURLs, network.PublicNetworkhistoryArchiveURLs) + require.Empty(t, ccConfig.Toml.HistoryEntries) + require.Len(t, ccConfig.Toml.Validators, 23) + require.Equal(t, ccConfig.Toml.Validators[0].Name, "Boötes") +} - // resume disabled by default - config, err := NewConfig(Flags{StartLedger: 1, EndLedger: 2, ConfigFilePath: "test/test.toml"}) +func TestValidCaptiveCoreManualNetwork(t *testing.T) { + cfg, err := NewConfig( + RuntimeSettings{ConfigFilePath: "test/valid_captive_core_manual.toml"}) require.NoError(t, err) - require.False(t, config.Resume) + require.Equal(t, cfg.CoreVersion, "") + require.Equal(t, cfg.StellarCoreConfig.NetworkPassphrase, "test") + require.Equal(t, cfg.StellarCoreConfig.HistoryArchiveUrls, []string{"http://testarchive"}) + + cmdOut = "v20.2.0-2-g6e73c0a88\n" + ccConfig, err := cfg.GenerateCaptiveCoreConfig("") + require.NoError(t, err) + + require.Equal(t, ccConfig.BinaryPath, "test/stellar-core") + require.Equal(t, ccConfig.NetworkPassphrase, "test") + require.Equal(t, ccConfig.HistoryArchiveURLs, []string{"http://testarchive"}) + require.Empty(t, ccConfig.Toml.HistoryEntries) + require.Len(t, ccConfig.Toml.Validators, 1) + require.Equal(t, ccConfig.Toml.Validators[0].Name, "local_core") + require.Equal(t, cfg.CoreVersion, "v20.2.0-2-g6e73c0a88") } -func TestInvalidTomlConfig(t *testing.T) { +func TestValidCaptiveCoreOverridenToml(t *testing.T) { + cfg, err := NewConfig( + RuntimeSettings{ConfigFilePath: "test/valid_captive_core_override.toml"}) + require.NoError(t, err) + require.Equal(t, cfg.StellarCoreConfig.NetworkPassphrase, network.PublicNetworkPassphrase) + require.Equal(t, cfg.StellarCoreConfig.HistoryArchiveUrls, network.PublicNetworkhistoryArchiveURLs) + + cmdOut = "v20.2.0-2-g6e73c0a88\n" + ccConfig, err := cfg.GenerateCaptiveCoreConfig("") + require.NoError(t, err) - _, err := NewConfig(Flags{StartLedger: 1, EndLedger: 2, ConfigFilePath: "test/no_network.toml", Resume: true}) - require.ErrorContains(t, err, "Invalid TOML config") + // the external core cfg file should have applied over the preconf'd network config + require.Equal(t, ccConfig.BinaryPath, "test/stellar-core") + require.Equal(t, ccConfig.NetworkPassphrase, network.PublicNetworkPassphrase) + require.Equal(t, ccConfig.HistoryArchiveURLs, network.PublicNetworkhistoryArchiveURLs) + require.Empty(t, ccConfig.Toml.HistoryEntries) + require.Len(t, ccConfig.Toml.Validators, 1) + require.Equal(t, ccConfig.Toml.Validators[0].Name, "local_core") + require.Equal(t, cfg.CoreVersion, "v20.2.0-2-g6e73c0a88") +} + +func TestValidCaptiveCoreOverridenArchiveUrls(t *testing.T) { + cfg, err := NewConfig( + RuntimeSettings{ConfigFilePath: "test/valid_captive_core_override_archives.toml"}) + require.NoError(t, err) + + require.Equal(t, cfg.StellarCoreConfig.NetworkPassphrase, network.PublicNetworkPassphrase) + require.Equal(t, cfg.StellarCoreConfig.HistoryArchiveUrls, []string{"http://testarchive"}) + + cmdOut = "v20.2.0-2-g6e73c0a88\n" + ccConfig, err := cfg.GenerateCaptiveCoreConfig("") + require.NoError(t, err) + + // validates that ingest/ledgerbackend/configs/captive-core-pubnet.cfg was loaded + require.Equal(t, ccConfig.BinaryPath, "test/stellar-core") + require.Equal(t, ccConfig.NetworkPassphrase, network.PublicNetworkPassphrase) + require.Equal(t, ccConfig.HistoryArchiveURLs, []string{"http://testarchive"}) + require.Empty(t, ccConfig.Toml.HistoryEntries) + require.Len(t, ccConfig.Toml.Validators, 23) + require.Equal(t, ccConfig.Toml.Validators[0].Name, "Boötes") +} + +func TestInvalidCaptiveCoreTomlPath(t *testing.T) { + _, err := NewConfig( + RuntimeSettings{ConfigFilePath: "test/invalid_captive_core_toml_path.toml"}) + require.ErrorContains(t, err, "Failed to load captive-core-toml-path file") } func TestValidateStartAndEndLedger(t *testing.T) { - const latestNetworkLedger = uint32(20000) + latestNetworkLedger := uint32(20000) + latestNetworkLedgerPadding := datastore.GetHistoryArchivesCheckPointFrequency() * 2 tests := []struct { name string startLedger uint32 endLedger uint32 errMsg string + mode Mode + mockHas bool }{ { name: "End ledger same as latest ledger", startLedger: 512, endLedger: 512, - errMsg: "", + mode: ScanFill, + errMsg: "invalid end value, must be greater than start", + mockHas: false, }, { name: "End ledger greater than start ledger", startLedger: 512, endLedger: 600, + mode: ScanFill, errMsg: "", + mockHas: true, }, { - name: "No end ledger provided, unbounded mode", + name: "No end ledger provided, append mode, no error", startLedger: 512, endLedger: 0, + mode: Append, errMsg: "", + mockHas: true, + }, + { + name: "No end ledger provided, scan-and-fill error", + startLedger: 512, + endLedger: 0, + mode: ScanFill, + errMsg: "invalid end value, unbounded mode not supported, end must be greater than start.", }, { name: "End ledger before start ledger", startLedger: 512, endLedger: 2, - errMsg: "invalid --end value, must be >= --start", + mode: ScanFill, + errMsg: "invalid end value, must be greater than start", }, { name: "End ledger exceeds latest ledger", startLedger: 512, - endLedger: latestNetworkLedger + 1, - errMsg: fmt.Sprintf("--end %d exceeds latest network ledger %d", - latestNetworkLedger+1, latestNetworkLedger), + endLedger: latestNetworkLedger + latestNetworkLedgerPadding + 1, + mode: ScanFill, + mockHas: true, + errMsg: fmt.Sprintf("end %d exceeds latest network ledger %d", + latestNetworkLedger+latestNetworkLedgerPadding+1, latestNetworkLedger+latestNetworkLedgerPadding), }, { name: "Start ledger 0", startLedger: 0, endLedger: 2, - errMsg: "", + mode: ScanFill, + errMsg: "invalid start value, must be greater than one.", + }, + { + name: "Start ledger 1", + startLedger: 1, + endLedger: 2, + mode: ScanFill, + errMsg: "invalid start value, must be greater than one.", }, { name: "Start ledger exceeds latest ledger", - startLedger: latestNetworkLedger + 1, - endLedger: 0, - errMsg: fmt.Sprintf("--start %d exceeds latest network ledger %d", - latestNetworkLedger+1, latestNetworkLedger), + startLedger: latestNetworkLedger + latestNetworkLedgerPadding + 1, + endLedger: latestNetworkLedger + latestNetworkLedgerPadding + 2, + mode: ScanFill, + mockHas: true, + errMsg: fmt.Sprintf("start %d exceeds latest network ledger %d", + latestNetworkLedger+latestNetworkLedgerPadding+1, latestNetworkLedger+latestNetworkLedgerPadding), }, } ctx := context.Background() mockArchive := &historyarchive.MockArchive{} - mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: latestNetworkLedger}, nil).Times(len(tests)) + mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: latestNetworkLedger}, nil) + mockedHasCtr := 0 for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - config, err := NewConfig(Flags{StartLedger: tt.startLedger, EndLedger: tt.endLedger, ConfigFilePath: "test/validate_start_end.toml"}) + if tt.mockHas { + mockedHasCtr++ + } + config, err := NewConfig( + RuntimeSettings{StartLedger: tt.startLedger, EndLedger: tt.endLedger, ConfigFilePath: "test/validate_start_end.toml", Mode: tt.mode}) require.NoError(t, err) err = config.ValidateAndSetLedgerRange(ctx, mockArchive) if tt.errMsg != "" { + require.Error(t, err) require.Equal(t, tt.errMsg, err.Error()) } else { require.NoError(t, err) } }) } + mockArchive.AssertNumberOfCalls(t, "GetRootHAS", mockedHasCtr) } func TestAdjustedLedgerRangeBoundedMode(t *testing.T) { @@ -130,27 +316,19 @@ func TestAdjustedLedgerRangeBoundedMode(t *testing.T) { expectedStart uint32 expectedEnd uint32 }{ - { - name: "Min start ledger 2", - configFile: "test/1perfile.toml", - start: 0, - end: 10, - expectedStart: 2, - expectedEnd: 10, - }, { name: "No change, 1 ledger per file", configFile: "test/1perfile.toml", start: 2, - end: 2, + end: 3, expectedStart: 2, - expectedEnd: 2, + expectedEnd: 3, }, { name: "Min start ledger2, round up end ledger, 10 ledgers per file", configFile: "test/10perfile.toml", - start: 0, - end: 1, + start: 2, + end: 3, expectedStart: 2, expectedEnd: 9, }, @@ -186,7 +364,9 @@ func TestAdjustedLedgerRangeBoundedMode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - config, err := NewConfig(Flags{StartLedger: tt.start, EndLedger: tt.end, ConfigFilePath: tt.configFile}) + config, err := NewConfig( + RuntimeSettings{StartLedger: tt.start, EndLedger: tt.end, ConfigFilePath: tt.configFile, Mode: ScanFill}) + require.NoError(t, err) err = config.ValidateAndSetLedgerRange(ctx, mockArchive) require.NoError(t, err) @@ -194,6 +374,7 @@ func TestAdjustedLedgerRangeBoundedMode(t *testing.T) { require.EqualValues(t, tt.expectedEnd, config.EndLedger) }) } + mockArchive.AssertExpectations(t) } func TestAdjustedLedgerRangeUnBoundedMode(t *testing.T) { @@ -205,14 +386,6 @@ func TestAdjustedLedgerRangeUnBoundedMode(t *testing.T) { expectedStart uint32 expectedEnd uint32 }{ - { - name: "Min start ledger 2", - configFile: "test/1perfile.toml", - start: 0, - end: 0, - expectedStart: 2, - expectedEnd: 0, - }, { name: "No change, 1 ledger per file", configFile: "test/1perfile.toml", @@ -254,7 +427,8 @@ func TestAdjustedLedgerRangeUnBoundedMode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - config, err := NewConfig(Flags{StartLedger: tt.start, EndLedger: tt.end, ConfigFilePath: tt.configFile}) + config, err := NewConfig( + RuntimeSettings{StartLedger: tt.start, EndLedger: tt.end, ConfigFilePath: tt.configFile, Mode: Append}) require.NoError(t, err) err = config.ValidateAndSetLedgerRange(ctx, mockArchive) require.NoError(t, err) @@ -262,6 +436,7 @@ func TestAdjustedLedgerRangeUnBoundedMode(t *testing.T) { require.EqualValues(t, tt.expectedEnd, config.EndLedger) }) } + mockArchive.AssertExpectations(t) } var cmdOut = "" @@ -273,15 +448,20 @@ func fakeExecCommand(command string, args ...string) *exec.Cmd { return cmd } +func init() { + execCommand = fakeExecCommand +} + func TestExecCmdHelperProcess(t *testing.T) { if os.Getenv("GO_EXEC_CMD_HELPER_PROCESS") != "1" { return } - fmt.Fprintf(os.Stdout, os.Getenv("CMD_OUT")) + fmt.Fprint(os.Stdout, os.Getenv("CMD_OUT")) os.Exit(0) } func TestSetCoreVersionInfo(t *testing.T) { + execCommand = fakeExecCommand tests := []struct { name string commandOutput string @@ -315,9 +495,7 @@ func TestSetCoreVersionInfo(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config := Config{} - cmdOut = tt.commandOutput - execCommand = fakeExecCommand err := config.setCoreVersionInfo() if tt.expectedError != nil { diff --git a/exp/services/ledgerexporter/internal/exportmanager.go b/exp/services/ledgerexporter/internal/exportmanager.go index ae85c072d6..af2633d6be 100644 --- a/exp/services/ledgerexporter/internal/exportmanager.go +++ b/exp/services/ledgerexporter/internal/exportmanager.go @@ -8,21 +8,29 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/stellar/go/ingest/ledgerbackend" + "github.com/stellar/go/support/datastore" "github.com/stellar/go/xdr" ) type ExportManager struct { - config *Config + dataStoreSchema datastore.DataStoreSchema ledgerBackend ledgerbackend.LedgerBackend currentMetaArchive *xdr.LedgerCloseMetaBatch queue UploadQueue latestLedgerMetric *prometheus.GaugeVec + networkPassPhrase string + coreVersion string } // NewExportManager creates a new ExportManager with the provided configuration. -func NewExportManager(config *Config, backend ledgerbackend.LedgerBackend, queue UploadQueue, prometheusRegistry *prometheus.Registry) (*ExportManager, error) { - if config.LedgerBatchConfig.LedgersPerFile < 1 { - return nil, errors.Errorf("Invalid ledgers per file (%d): must be at least 1", config.LedgerBatchConfig.LedgersPerFile) +func NewExportManager(dataStoreSchema datastore.DataStoreSchema, + backend ledgerbackend.LedgerBackend, + queue UploadQueue, + prometheusRegistry *prometheus.Registry, + networkPassPhrase string, + coreVersion string) (*ExportManager, error) { + if dataStoreSchema.LedgersPerFile < 1 { + return nil, errors.Errorf("Invalid ledgers per file (%d): must be at least 1", dataStoreSchema.LedgersPerFile) } latestLedgerMetric := prometheus.NewGaugeVec(prometheus.GaugeOpts{ @@ -32,10 +40,12 @@ func NewExportManager(config *Config, backend ledgerbackend.LedgerBackend, queue prometheusRegistry.MustRegister(latestLedgerMetric) return &ExportManager{ - config: config, + dataStoreSchema: dataStoreSchema, ledgerBackend: backend, queue: queue, latestLedgerMetric: latestLedgerMetric, + networkPassPhrase: networkPassPhrase, + coreVersion: coreVersion, }, nil } @@ -44,16 +54,16 @@ func (e *ExportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta ledgerSeq := ledgerCloseMeta.LedgerSequence() // Determine the object key for the given ledger sequence - objectKey := e.config.LedgerBatchConfig.GetObjectKeyFromSequenceNumber(ledgerSeq) + objectKey := e.dataStoreSchema.GetObjectKeyFromSequenceNumber(ledgerSeq) if e.currentMetaArchive == nil { - endSeq := ledgerSeq + e.config.LedgerBatchConfig.LedgersPerFile - 1 - if ledgerSeq < e.config.LedgerBatchConfig.LedgersPerFile { + endSeq := ledgerSeq + e.dataStoreSchema.LedgersPerFile - 1 + if ledgerSeq < e.dataStoreSchema.LedgersPerFile { // Special case: Adjust the end ledger sequence for the first batch. // Since the start ledger is 2 instead of 0, we want to ensure that the end ledger sequence // does not exceed LedgersPerFile. // For example, if LedgersPerFile is 64, the file name for the first batch should be 0-63, not 2-66. - endSeq = e.config.LedgerBatchConfig.LedgersPerFile - 1 + endSeq = e.dataStoreSchema.LedgersPerFile - 1 } // Create a new LedgerCloseMetaBatch @@ -65,7 +75,7 @@ func (e *ExportManager) AddLedgerCloseMeta(ctx context.Context, ledgerCloseMeta } if ledgerSeq >= uint32(e.currentMetaArchive.EndSequence) { - ledgerMetaArchive, err := NewLedgerMetaArchiveFromXDR(e.config, objectKey, *e.currentMetaArchive) + ledgerMetaArchive, err := NewLedgerMetaArchiveFromXDR(e.networkPassPhrase, e.coreVersion, objectKey, *e.currentMetaArchive) if err != nil { return err } diff --git a/exp/services/ledgerexporter/internal/exportmanager_test.go b/exp/services/ledgerexporter/internal/exportmanager_test.go index b74af2e19e..7845186c06 100644 --- a/exp/services/ledgerexporter/internal/exportmanager_test.go +++ b/exp/services/ledgerexporter/internal/exportmanager_test.go @@ -8,6 +8,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -58,18 +59,18 @@ func (s *ExportManagerSuite) TearDownTest() { } func (s *ExportManagerSuite) TestInvalidExportConfig() { - config := &Config{LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 0, FilesPerPartition: 10}} + config := datastore.DataStoreSchema{LedgersPerFile: 0, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - _, err := NewExportManager(config, &s.mockBackend, queue, registry) + _, err := NewExportManager(config, &s.mockBackend, queue, registry, "passphrase", "coreversion") s.Require().Error(err) } func (s *ExportManagerSuite) TestRun() { - config := &Config{LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 64, FilesPerPartition: 10}} + config := datastore.DataStoreSchema{LedgersPerFile: 64, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) + exporter, err := NewExportManager(config, &s.mockBackend, queue, registry, "passphrase", "coreversion") s.Require().NoError(err) start := uint32(0) @@ -79,7 +80,7 @@ func (s *ExportManagerSuite) TestRun() { for i := start; i <= end; i++ { s.mockBackend.On("GetLedger", s.ctx, i). Return(createLedgerCloseMeta(i), nil) - key := config.LedgerBatchConfig.GetObjectKeyFromSequenceNumber(i) + key := config.GetObjectKeyFromSequenceNumber(i) expectedKeys.Add(key) } @@ -116,10 +117,11 @@ func (s *ExportManagerSuite) TestRun() { } func (s *ExportManagerSuite) TestRunContextCancel() { - config := &Config{LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 1}} + config := datastore.DataStoreSchema{LedgersPerFile: 1, FilesPerPartition: 1} + registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) + exporter, err := NewExportManager(config, &s.mockBackend, queue, registry, "passphrase", "coreversion") s.Require().NoError(err) ctx, cancel := context.WithCancel(context.Background()) @@ -146,10 +148,11 @@ func (s *ExportManagerSuite) TestRunContextCancel() { } func (s *ExportManagerSuite) TestRunWithCanceledContext() { - config := &Config{LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10}} + config := datastore.DataStoreSchema{LedgersPerFile: 1, FilesPerPartition: 10} + registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) + exporter, err := NewExportManager(config, &s.mockBackend, queue, registry, "passphrase", "coreversion") s.Require().NoError(err) ctx, cancel := context.WithCancel(context.Background()) @@ -165,10 +168,11 @@ func (s *ExportManagerSuite) TestRunWithCanceledContext() { } func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { - config := &Config{LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10}} + config := datastore.DataStoreSchema{LedgersPerFile: 1, FilesPerPartition: 10} + registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) + exporter, err := NewExportManager(config, &s.mockBackend, queue, registry, "passphrase", "coreversion") s.Require().NoError(err) expectedKeys := set.NewSet[string](10) @@ -193,7 +197,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { for i := start; i <= end; i++ { s.Require().NoError(exporter.AddLedgerCloseMeta(context.Background(), createLedgerCloseMeta(i))) - key := config.LedgerBatchConfig.GetObjectKeyFromSequenceNumber(i) + key := config.GetObjectKeyFromSequenceNumber(i) expectedKeys.Add(key) } @@ -203,10 +207,10 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMeta() { } func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { - config := &Config{LedgerBatchConfig: datastore.LedgerBatchConfig{LedgersPerFile: 1, FilesPerPartition: 10}} + config := datastore.DataStoreSchema{LedgersPerFile: 1, FilesPerPartition: 10} registry := prometheus.NewRegistry() queue := NewUploadQueue(1, registry) - exporter, err := NewExportManager(config, &s.mockBackend, queue, registry) + exporter, err := NewExportManager(config, &s.mockBackend, queue, registry, "passphrase", "coreversion") s.Require().NoError(err) ctx, cancel := context.WithCancel(context.Background()) @@ -215,7 +219,7 @@ func (s *ExportManagerSuite) TestAddLedgerCloseMetaContextCancel() { cancel() }() - s.Require().NoError(exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(1))) + require.NoError(s.T(), exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(1))) err = exporter.AddLedgerCloseMeta(ctx, createLedgerCloseMeta(2)) - s.Require().EqualError(err, "context canceled") + require.EqualError(s.T(), err, "context canceled") } diff --git a/exp/services/ledgerexporter/internal/ledger_meta_archive.go b/exp/services/ledgerexporter/internal/ledger_meta_archive.go index 583f6a3ec4..f63a77f28b 100644 --- a/exp/services/ledgerexporter/internal/ledger_meta_archive.go +++ b/exp/services/ledgerexporter/internal/ledger_meta_archive.go @@ -14,7 +14,7 @@ type LedgerMetaArchive struct { } // NewLedgerMetaArchiveFromXDR creates a new LedgerMetaArchive instance. -func NewLedgerMetaArchiveFromXDR(config *Config, key string, data xdr.LedgerCloseMetaBatch) (*LedgerMetaArchive, error) { +func NewLedgerMetaArchiveFromXDR(networkPassPhrase string, coreVersion string, key string, data xdr.LedgerCloseMetaBatch) (*LedgerMetaArchive, error) { startLedger, err := data.GetLedger(uint32(data.StartSequence)) if err != nil { return &LedgerMetaArchive{}, err @@ -33,10 +33,10 @@ func NewLedgerMetaArchiveFromXDR(config *Config, key string, data xdr.LedgerClos EndLedger: endLedger.LedgerSequence(), StartLedgerCloseTime: startLedger.LedgerCloseTime(), EndLedgerCloseTime: endLedger.LedgerCloseTime(), - Network: config.Network, + NetworkPassPhrase: networkPassPhrase, CompressionType: compressxdr.DefaultCompressor.Name(), ProtocolVersion: endLedger.ProtocolVersion(), - CoreVersion: config.CoreVersion, + CoreVersion: coreVersion, Version: version, }, }, nil diff --git a/exp/services/ledgerexporter/internal/ledger_meta_archive_test.go b/exp/services/ledgerexporter/internal/ledger_meta_archive_test.go index 3cf263d71b..152f2c6496 100644 --- a/exp/services/ledgerexporter/internal/ledger_meta_archive_test.go +++ b/exp/services/ledgerexporter/internal/ledger_meta_archive_test.go @@ -10,10 +10,6 @@ import ( ) func TestNewLedgerMetaArchiveFromXDR(t *testing.T) { - config := &Config{ - Network: "testnet", - CoreVersion: "v1.2.3", - } data := xdr.LedgerCloseMetaBatch{ StartSequence: 1234, EndSequence: 1234, @@ -22,7 +18,7 @@ func TestNewLedgerMetaArchiveFromXDR(t *testing.T) { }, } - archive, err := NewLedgerMetaArchiveFromXDR(config, "key", data) + archive, err := NewLedgerMetaArchiveFromXDR("testnet", "v1.2.3", "key", data) require.NoError(t, err) require.NotNil(t, archive) @@ -33,7 +29,7 @@ func TestNewLedgerMetaArchiveFromXDR(t *testing.T) { EndLedger: 1234, StartLedgerCloseTime: 1234 * 100, EndLedgerCloseTime: 1234 * 100, - Network: "testnet", + NetworkPassPhrase: "testnet", CompressionType: "zstd", ProtocolVersion: 21, CoreVersion: "v1.2.3", @@ -53,7 +49,7 @@ func TestNewLedgerMetaArchiveFromXDR(t *testing.T) { }, } - archive, err = NewLedgerMetaArchiveFromXDR(config, "key", data) + archive, err = NewLedgerMetaArchiveFromXDR("testnet", "v1.2.3", "key", data) require.NoError(t, err) require.NotNil(t, archive) @@ -64,7 +60,7 @@ func TestNewLedgerMetaArchiveFromXDR(t *testing.T) { EndLedger: 1237, StartLedgerCloseTime: 1234 * 100, EndLedgerCloseTime: 1237 * 100, - Network: "testnet", + NetworkPassPhrase: "testnet", CompressionType: "zstd", ProtocolVersion: 21, CoreVersion: "v1.2.3", diff --git a/exp/services/ledgerexporter/internal/main.go b/exp/services/ledgerexporter/internal/main.go new file mode 100644 index 0000000000..d1409eb89c --- /dev/null +++ b/exp/services/ledgerexporter/internal/main.go @@ -0,0 +1,94 @@ +package ledgerexporter + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stellar/go/support/strutils" +) + +var ( + ledgerExporterCmdRunner = func(runtimeSettings RuntimeSettings) error { + app := NewApp() + return app.Run(runtimeSettings) + } + rootCmd, scanAndFillCmd, appendCmd *cobra.Command +) + +func Execute() error { + defineCommands() + return rootCmd.Execute() +} + +func defineCommands() { + rootCmd = &cobra.Command{ + Use: "ledgerexporter", + Short: "Export Stellar network ledger data to a remote data store", + Long: "Converts ledger meta data from Stellar network into static data and exports it remote data storage.", + + RunE: func(cmd *cobra.Command, args []string) error { + return fmt.Errorf("please specify one of the availble sub-commands to initiate export") + }, + } + scanAndFillCmd = &cobra.Command{ + Use: "scan-and-fill", + Short: "scans the entire bounded requested range between 'start' and 'end' flags and exports only the ledgers which are missing from the data lake.", + Long: "scans the entire bounded requested range between 'start' and 'end' flags and exports only the ledgers which are missing from the data lake.", + RunE: func(cmd *cobra.Command, args []string) error { + settings := bindCliParameters(cmd.PersistentFlags().Lookup("start"), + cmd.PersistentFlags().Lookup("end"), + cmd.PersistentFlags().Lookup("config-file"), + ) + settings.Mode = ScanFill + return ledgerExporterCmdRunner(settings) + }, + } + appendCmd = &cobra.Command{ + Use: "append", + Short: "export ledgers beginning with the first missing ledger after the specified 'start' ledger and resumes exporting from there", + Long: "export ledgers beginning with the first missing ledger after the specified 'start' ledger and resumes exporting from there", + RunE: func(cmd *cobra.Command, args []string) error { + settings := bindCliParameters(cmd.PersistentFlags().Lookup("start"), + cmd.PersistentFlags().Lookup("end"), + cmd.PersistentFlags().Lookup("config-file"), + ) + settings.Mode = Append + return ledgerExporterCmdRunner(settings) + }, + } + + rootCmd.AddCommand(scanAndFillCmd) + rootCmd.AddCommand(appendCmd) + + scanAndFillCmd.PersistentFlags().Uint32P("start", "s", 0, "Starting ledger (inclusive), must be set to a value greater than 1") + scanAndFillCmd.PersistentFlags().Uint32P("end", "e", 0, "Ending ledger (inclusive), must be set to value greater than 'start' and less than the network's current ledger") + scanAndFillCmd.PersistentFlags().String("config-file", "config.toml", "Path to the TOML config file. Defaults to 'config.toml' on runtime working directory path.") + viper.BindPFlags(scanAndFillCmd.PersistentFlags()) + + appendCmd.PersistentFlags().Uint32P("start", "s", 0, "Starting ledger (inclusive), must be set to a value greater than 1") + appendCmd.PersistentFlags().Uint32P("end", "e", 0, "Ending ledger (inclusive), optional, setting to non-zero means bounded mode, "+ + "only export ledgers from 'start' up to 'end' value which must be greater than 'start' and less than the network's current ledger. "+ + "If 'end' is absent or '0' means unbounded mode, exporter will continue to run indefintely and export the latest closed ledgers from network as they are generated in real time.") + appendCmd.PersistentFlags().String("config-file", "config.toml", "Path to the TOML config file. Defaults to 'config.toml' on runtime working directory path.") + viper.BindPFlags(appendCmd.PersistentFlags()) +} + +func bindCliParameters(startFlag *pflag.Flag, endFlag *pflag.Flag, configFileFlag *pflag.Flag) RuntimeSettings { + settings := RuntimeSettings{} + + viper.BindPFlag(startFlag.Name, startFlag) + viper.BindEnv(startFlag.Name, strutils.KebabToConstantCase(startFlag.Name)) + settings.StartLedger = viper.GetUint32(startFlag.Name) + + viper.BindPFlag(endFlag.Name, endFlag) + viper.BindEnv(endFlag.Name, strutils.KebabToConstantCase(endFlag.Name)) + settings.EndLedger = viper.GetUint32(endFlag.Name) + + viper.BindPFlag(configFileFlag.Name, configFileFlag) + viper.BindEnv(configFileFlag.Name, strutils.KebabToConstantCase(configFileFlag.Name)) + settings.ConfigFilePath = viper.GetString(configFileFlag.Name) + + return settings +} diff --git a/exp/services/ledgerexporter/internal/main_test.go b/exp/services/ledgerexporter/internal/main_test.go new file mode 100644 index 0000000000..4c9e5412f3 --- /dev/null +++ b/exp/services/ledgerexporter/internal/main_test.go @@ -0,0 +1,116 @@ +package ledgerexporter + +import ( + "bytes" + "io" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +func TestFlagsOutput(t *testing.T) { + var testResultSettings RuntimeSettings + appRunnerSuccess := func(runtimeSettings RuntimeSettings) error { + testResultSettings = runtimeSettings + return nil + } + + appRunnerError := func(runtimeSettings RuntimeSettings) error { + return errors.New("test error") + } + + testCases := []struct { + name string + commandArgs []string + expectedErrOutput string + appRunner func(runtimeSettings RuntimeSettings) error + expectedSettings RuntimeSettings + }{ + { + name: "no sub-command", + commandArgs: []string{"--start", "4", "--end", "5", "--config-file", "myfile"}, + expectedErrOutput: "Error: ", + }, + { + name: "append sub-command with start and end present", + commandArgs: []string{"append", "--start", "4", "--end", "5", "--config-file", "myfile"}, + expectedErrOutput: "", + appRunner: appRunnerSuccess, + expectedSettings: RuntimeSettings{ + StartLedger: 4, + EndLedger: 5, + ConfigFilePath: "myfile", + Mode: Append, + }, + }, + { + name: "append sub-command with start and end absent", + commandArgs: []string{"append", "--config-file", "myfile"}, + expectedErrOutput: "", + appRunner: appRunnerSuccess, + expectedSettings: RuntimeSettings{ + StartLedger: 0, + EndLedger: 0, + ConfigFilePath: "myfile", + Mode: Append, + }, + }, + { + name: "append sub-command prints app error", + commandArgs: []string{"append", "--start", "4", "--end", "5", "--config-file", "myfile"}, + expectedErrOutput: "test error", + appRunner: appRunnerError, + }, + { + name: "scanfill sub-command with start and end present", + commandArgs: []string{"scan-and-fill", "--start", "4", "--end", "5", "--config-file", "myfile"}, + expectedErrOutput: "", + appRunner: appRunnerSuccess, + expectedSettings: RuntimeSettings{ + StartLedger: 4, + EndLedger: 5, + ConfigFilePath: "myfile", + Mode: ScanFill, + }, + }, + { + name: "scanfill sub-command with start and end absent", + commandArgs: []string{"scan-and-fill", "--config-file", "myfile"}, + expectedErrOutput: "", + appRunner: appRunnerSuccess, + expectedSettings: RuntimeSettings{ + StartLedger: 0, + EndLedger: 0, + ConfigFilePath: "myfile", + Mode: ScanFill, + }, + }, + { + name: "scanfill sub-command prints app error", + commandArgs: []string{"scan-and-fill", "--start", "4", "--end", "5", "--config-file", "myfile"}, + expectedErrOutput: "test error", + appRunner: appRunnerError, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + // mock the ledger exporter's cmd runner to be this test's mock routine instead of real app + ledgerExporterCmdRunner = testCase.appRunner + defineCommands() + rootCmd.SetArgs(testCase.commandArgs) + var errWriter io.Writer = &bytes.Buffer{} + var outWriter io.Writer = &bytes.Buffer{} + rootCmd.SetErr(errWriter) + rootCmd.SetOut(outWriter) + rootCmd.Execute() + + errOutput := errWriter.(*bytes.Buffer).String() + if testCase.expectedErrOutput != "" { + assert.Contains(t, errOutput, testCase.expectedErrOutput) + } else { + assert.Equal(t, testCase.expectedSettings, testResultSettings) + } + }) + } +} diff --git a/exp/services/ledgerexporter/internal/test/10perfile.toml b/exp/services/ledgerexporter/internal/test/10perfile.toml index 9b96927804..cae3d9f9ea 100644 --- a/exp/services/ledgerexporter/internal/test/10perfile.toml +++ b/exp/services/ledgerexporter/internal/test/10perfile.toml @@ -1,5 +1,6 @@ -network = "test" +[stellar_core_config] +stellar_core_binary_path = "test/stellar-core" +network = "pubnet" -[exporter_config] -ledgers_per_file = 10 -file_suffix = ".xdr.gz" \ No newline at end of file +[datastore_config.schema] +ledgers_per_file = 10 \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/15perfile.toml b/exp/services/ledgerexporter/internal/test/15perfile.toml index 94df87a2e2..e3e4ee9d44 100644 --- a/exp/services/ledgerexporter/internal/test/15perfile.toml +++ b/exp/services/ledgerexporter/internal/test/15perfile.toml @@ -1,5 +1,6 @@ -network = "test" +[stellar_core_config] +stellar_core_binary_path = "test/stellar-core" +network = "pubnet" -[exporter_config] -ledgers_per_file = 15 -file_suffix = ".xdr.gz" \ No newline at end of file +[datastore_config.schema] +ledgers_per_file = 15 \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/1perfile.toml b/exp/services/ledgerexporter/internal/test/1perfile.toml index a15dc9bf41..f8e88de478 100644 --- a/exp/services/ledgerexporter/internal/test/1perfile.toml +++ b/exp/services/ledgerexporter/internal/test/1perfile.toml @@ -1,5 +1,6 @@ -network = "test" +[stellar_core_config] +stellar_core_binary_path = "test/stellar-core" +network = "pubnet" -[exporter_config] -ledgers_per_file = 1 -file_suffix = ".xdr.gz" \ No newline at end of file +[datastore_config.schema] +ledgers_per_file = 1 \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/64perfile.toml b/exp/services/ledgerexporter/internal/test/64perfile.toml index 8e8d122fcc..769546afaf 100644 --- a/exp/services/ledgerexporter/internal/test/64perfile.toml +++ b/exp/services/ledgerexporter/internal/test/64perfile.toml @@ -1,5 +1,6 @@ -network = "test" +[stellar_core_config] +stellar_core_binary_path = "test/stellar-core" +network = "pubnet" -[exporter_config] -ledgers_per_file = 64 -file_suffix = ".xdr.gz" \ No newline at end of file +[datastore_config.schema] +ledgers_per_file = 64 \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/captive-core-test.cfg b/exp/services/ledgerexporter/internal/test/captive-core-test.cfg new file mode 100644 index 0000000000..dac9369464 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/captive-core-test.cfg @@ -0,0 +1,12 @@ +PEER_PORT=11725 + +UNSAFE_QUORUM=true +FAILURE_SAFETY=0 + +[[VALIDATORS]] +NAME="local_core" +HOME_DOMAIN="core.local" +PUBLIC_KEY="GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS" +ADDRESS="localhost" +HISTORY="curl -sf https://localhost/{0} -o {1}" +QUALITY="MEDIUM" \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/invalid_captive_core_toml_path.toml b/exp/services/ledgerexporter/internal/test/invalid_captive_core_toml_path.toml new file mode 100644 index 0000000000..580590b667 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/invalid_captive_core_toml_path.toml @@ -0,0 +1,5 @@ +[stellar_core_config] +captive_core_toml_path = "test/notfound.cfg" +stellar_core_binary_path = "test/stellar-core" +history_archive_urls = ["http://testarchive"] +network_passphrase = "test" diff --git a/exp/services/ledgerexporter/internal/test/invalid_empty.toml b/exp/services/ledgerexporter/internal/test/invalid_empty.toml new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/invalid_empty.toml @@ -0,0 +1 @@ + diff --git a/exp/services/ledgerexporter/internal/test/invalid_preconfigured_network.toml b/exp/services/ledgerexporter/internal/test/invalid_preconfigured_network.toml new file mode 100644 index 0000000000..8e09f05e55 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/invalid_preconfigured_network.toml @@ -0,0 +1,4 @@ +[stellar_core_config] +stellar_core_binary_path = "test/stellar-core" +network = "invalid" + diff --git a/exp/services/ledgerexporter/internal/test/no_core_bin.toml b/exp/services/ledgerexporter/internal/test/no_core_bin.toml new file mode 100644 index 0000000000..abbd1d9972 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/no_core_bin.toml @@ -0,0 +1,3 @@ +[stellar_core_config] +network = "testnet" + \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/no_network.toml b/exp/services/ledgerexporter/internal/test/no_network.toml deleted file mode 100644 index 1cb591cdd4..0000000000 --- a/exp/services/ledgerexporter/internal/test/no_network.toml +++ /dev/null @@ -1,11 +0,0 @@ - -[datastore_config] -type = "ABC" - -[datastore_config.params] -destination_bucket_path = "your-bucket-name/subpath" - -[exporter_config] -ledgers_per_file = 3 -files_per_partition = 1 -file_suffix = ".xdr.gz" \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/test.toml b/exp/services/ledgerexporter/internal/test/test.toml index 58b5fc6df6..6a6804c70f 100644 --- a/exp/services/ledgerexporter/internal/test/test.toml +++ b/exp/services/ledgerexporter/internal/test/test.toml @@ -1,12 +1,13 @@ -network = "test" +[stellar_core_config] +stellar_core_binary_path = "test/stellar-core" +network = "pubnet" [datastore_config] type = "ABC" [datastore_config.params] -destination_bucket_path = "your-bucket-name/subpath" +destination_bucket_path = "your-bucket-name/subpath/testnet" -[exporter_config] +[datastore_config.schema] ledgers_per_file = 3 -files_per_partition = 1 -file_suffix = ".xdr.gz" \ No newline at end of file +files_per_partition = 1 \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/useragent.toml b/exp/services/ledgerexporter/internal/test/useragent.toml new file mode 100644 index 0000000000..209c04155b --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/useragent.toml @@ -0,0 +1,7 @@ +user_agent = "useragent_x" + +[stellar_core_config] +stellar_core_binary_path = "test/stellar-core" +network = "pubnet" + + diff --git a/exp/services/ledgerexporter/internal/test/valid_captive_core_manual.toml b/exp/services/ledgerexporter/internal/test/valid_captive_core_manual.toml new file mode 100644 index 0000000000..24a4304844 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/valid_captive_core_manual.toml @@ -0,0 +1,5 @@ +[stellar_core_config] +captive_core_toml_path = "test/captive-core-test.cfg" +stellar_core_binary_path = "test/stellar-core" +history_archive_urls = ["http://testarchive"] +network_passphrase = "test" \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/valid_captive_core_override.toml b/exp/services/ledgerexporter/internal/test/valid_captive_core_override.toml new file mode 100644 index 0000000000..312ccb29bb --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/valid_captive_core_override.toml @@ -0,0 +1,4 @@ +[stellar_core_config] +network="pubnet" +captive_core_toml_path = "test/captive-core-test.cfg" +stellar_core_binary_path = "test/stellar-core" diff --git a/exp/services/ledgerexporter/internal/test/valid_captive_core_override_archives.toml b/exp/services/ledgerexporter/internal/test/valid_captive_core_override_archives.toml new file mode 100644 index 0000000000..9aebc153c2 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/valid_captive_core_override_archives.toml @@ -0,0 +1,4 @@ +[stellar_core_config] +network="pubnet" +history_archive_urls = ["http://testarchive"] +stellar_core_binary_path = "test/stellar-core" diff --git a/exp/services/ledgerexporter/internal/test/valid_captive_core_preconfigured.toml b/exp/services/ledgerexporter/internal/test/valid_captive_core_preconfigured.toml new file mode 100644 index 0000000000..cb1c3c5c52 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/valid_captive_core_preconfigured.toml @@ -0,0 +1,4 @@ +[stellar_core_config] +stellar_core_binary_path = "test/stellar-core" +network = "pubnet" + diff --git a/exp/services/ledgerexporter/internal/test/validate_start_end.toml b/exp/services/ledgerexporter/internal/test/validate_start_end.toml index a15dc9bf41..8cbd9f37a9 100644 --- a/exp/services/ledgerexporter/internal/test/validate_start_end.toml +++ b/exp/services/ledgerexporter/internal/test/validate_start_end.toml @@ -1,5 +1,6 @@ -network = "test" - -[exporter_config] +[datastore_config.schema] ledgers_per_file = 1 -file_suffix = ".xdr.gz" \ No newline at end of file + +[stellar_core_config] +stellar_core_binary_path = "test/stellar-core" +network = "pubnet" \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/uploader_test.go b/exp/services/ledgerexporter/internal/uploader_test.go index bb3ca1e29a..612c9e8740 100644 --- a/exp/services/ledgerexporter/internal/uploader_test.go +++ b/exp/services/ledgerexporter/internal/uploader_test.go @@ -59,7 +59,7 @@ func (s *UploaderSuite) TestUploadWithMetadata() { EndLedgerCloseTime: 987654321, ProtocolVersion: 3, CoreVersion: "v1.2.3", - Network: "testnet", + NetworkPassPhrase: "testnet", CompressionType: "gzip", Version: "1.0.0", } diff --git a/exp/services/ledgerexporter/main.go b/exp/services/ledgerexporter/main.go index 63e094980f..6d38c69df9 100644 --- a/exp/services/ledgerexporter/main.go +++ b/exp/services/ledgerexporter/main.go @@ -1,25 +1,16 @@ package main import ( - "flag" + "fmt" + "os" exporter "github.com/stellar/go/exp/services/ledgerexporter/internal" ) func main() { - flags := exporter.Flags{} - startLedger := uint(0) - endLedger := uint(0) - flag.UintVar(&startLedger, "start", 0, "Starting ledger") - flag.UintVar(&endLedger, "end", 0, "Ending ledger (inclusive)") - flag.StringVar(&flags.ConfigFilePath, "config-file", "config.toml", "Path to the TOML config file") - flag.BoolVar(&flags.Resume, "resume", false, "Attempt to find a resumable starting point on remote data store") - flag.UintVar(&flags.AdminPort, "admin-port", 0, "Admin HTTP port for prometheus metrics") - - flag.Parse() - flags.StartLedger = uint32(startLedger) - flags.EndLedger = uint32(endLedger) - - app := exporter.NewApp(flags) - app.Run() + err := exporter.Execute() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } } diff --git a/ingest/ledgerbackend/buffered_storage_backend.go b/ingest/ledgerbackend/buffered_storage_backend.go index 41ff4a942e..4a353bfe22 100644 --- a/ingest/ledgerbackend/buffered_storage_backend.go +++ b/ingest/ledgerbackend/buffered_storage_backend.go @@ -18,7 +18,7 @@ import ( var _ LedgerBackend = (*BufferedStorageBackend)(nil) type BufferedStorageBackendConfig struct { - LedgerBatchConfig datastore.LedgerBatchConfig + LedgerBatchConfig datastore.DataStoreSchema DataStore datastore.DataStore BufferSize uint32 NumWorkers uint32 diff --git a/ingest/ledgerbackend/buffered_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go index 1aca8306c8..f18329fffa 100644 --- a/ingest/ledgerbackend/buffered_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -44,7 +44,7 @@ func createBufferedStorageBackendConfigForTesting() BufferedStorageBackendConfig param := make(map[string]string) param["destination_bucket_path"] = "testURL" - ledgerBatchConfig := datastore.LedgerBatchConfig{ + ledgerBatchConfig := datastore.DataStoreSchema{ LedgersPerFile: 1, FilesPerPartition: 64000, } diff --git a/support/datastore/datastore.go b/support/datastore/datastore.go index 0a92857b5c..e7e999345d 100644 --- a/support/datastore/datastore.go +++ b/support/datastore/datastore.go @@ -10,6 +10,7 @@ import ( type DataStoreConfig struct { Type string `toml:"type"` Params map[string]string `toml:"params"` + Schema DataStoreSchema `toml:"schema"` } // DataStore defines an interface for interacting with data storage @@ -24,14 +25,14 @@ type DataStore interface { } // NewDataStore factory, it creates a new DataStore based on the config type -func NewDataStore(ctx context.Context, datastoreConfig DataStoreConfig, network string) (DataStore, error) { +func NewDataStore(ctx context.Context, datastoreConfig DataStoreConfig) (DataStore, error) { switch datastoreConfig.Type { case "GCS": destinationBucketPath, ok := datastoreConfig.Params["destination_bucket_path"] if !ok { return nil, errors.Errorf("Invalid GCS config, no destination_bucket_path") } - return NewGCSDataStore(ctx, destinationBucketPath, network) + return NewGCSDataStore(ctx, destinationBucketPath) default: return nil, errors.Errorf("Invalid datastore type %v, not supported", datastoreConfig.Type) } diff --git a/support/datastore/datastore_test.go b/support/datastore/datastore_test.go index 12041729be..3482ef3204 100644 --- a/support/datastore/datastore_test.go +++ b/support/datastore/datastore_test.go @@ -8,6 +8,6 @@ import ( ) func TestInvalidStore(t *testing.T) { - _, err := NewDataStore(context.Background(), DataStoreConfig{Type: "unknown"}, "test") + _, err := NewDataStore(context.Background(), DataStoreConfig{Type: "unknown"}) require.Error(t, err) } diff --git a/support/datastore/gcs_datastore.go b/support/datastore/gcs_datastore.go index c6b22b0fca..cdedea086d 100644 --- a/support/datastore/gcs_datastore.go +++ b/support/datastore/gcs_datastore.go @@ -26,19 +26,19 @@ type GCSDataStore struct { prefix string } -func NewGCSDataStore(ctx context.Context, bucketPath string, network string) (DataStore, error) { +func NewGCSDataStore(ctx context.Context, bucketPath string) (DataStore, error) { client, err := storage.NewClient(ctx) if err != nil { return nil, err } - return FromGCSClient(ctx, client, bucketPath, network) + return FromGCSClient(ctx, client, bucketPath) } -func FromGCSClient(ctx context.Context, client *storage.Client, bucketPath string, network string) (DataStore, error) { +func FromGCSClient(ctx context.Context, client *storage.Client, bucketPath string) (DataStore, error) { // append the gcs:// scheme to enable usage of the url package reliably to // get parse bucket name which is first path segment as URL.Host - gcsBucketURL := fmt.Sprintf("gcs://%s/%s", bucketPath, network) + gcsBucketURL := fmt.Sprintf("gcs://%s", bucketPath) parsed, err := url.Parse(gcsBucketURL) if err != nil { return nil, err diff --git a/support/datastore/gcs_test.go b/support/datastore/gcs_test.go index 0ddf60ac99..8838e8dadb 100644 --- a/support/datastore/gcs_test.go +++ b/support/datastore/gcs_test.go @@ -24,7 +24,7 @@ func TestGCSExists(t *testing.T) { }) defer server.Stop() - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) @@ -52,7 +52,7 @@ func TestGCSSize(t *testing.T) { }) defer server.Stop() - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) @@ -86,7 +86,7 @@ func TestGCSPutFile(t *testing.T) { DefaultEventBasedHold: false, }) - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) @@ -138,7 +138,7 @@ func TestGCSPutFileIfNotExists(t *testing.T) { }) defer server.Stop() - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) @@ -187,7 +187,7 @@ func TestGCSPutFileWithMetadata(t *testing.T) { DefaultEventBasedHold: false, }) - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) @@ -198,7 +198,7 @@ func TestGCSPutFileWithMetadata(t *testing.T) { EndLedger: 1234, StartLedgerCloseTime: 1234, EndLedgerCloseTime: 1234, - Network: "testnet", + NetworkPassPhrase: "testnet", CompressionType: "zstd", ProtocolVersion: 21, CoreVersion: "v1.2.3", @@ -223,7 +223,7 @@ func TestGCSPutFileWithMetadata(t *testing.T) { EndLedger: 6789, StartLedgerCloseTime: 1622547800, EndLedgerCloseTime: 1622548900, - Network: "mainnet", + NetworkPassPhrase: "mainnet", CompressionType: "gzip", ProtocolVersion: 23, CoreVersion: "v1.4.0", @@ -255,7 +255,7 @@ func TestGCSPutFileIfNotExistsWithMetadata(t *testing.T) { }) defer server.Stop() - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) @@ -265,7 +265,7 @@ func TestGCSPutFileIfNotExistsWithMetadata(t *testing.T) { EndLedger: 1234, StartLedgerCloseTime: 1234, EndLedgerCloseTime: 1234, - Network: "testnet", + NetworkPassPhrase: "testnet", CompressionType: "zstd", ProtocolVersion: 21, CoreVersion: "v1.2.3", @@ -290,7 +290,7 @@ func TestGCSPutFileIfNotExistsWithMetadata(t *testing.T) { EndLedger: 6789, StartLedgerCloseTime: 1622547800, EndLedgerCloseTime: 1622548900, - Network: "mainnet", + NetworkPassPhrase: "mainnet", CompressionType: "gzip", ProtocolVersion: 23, CoreVersion: "v1.4.0", @@ -323,7 +323,7 @@ func TestGCSGetNonExistentFile(t *testing.T) { }) defer server.Stop() - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) @@ -365,7 +365,7 @@ func TestGCSGetFileValidatesCRC32C(t *testing.T) { }) defer server.Stop() - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects", "testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) diff --git a/support/datastore/history_archive.go b/support/datastore/history_archive.go index bc692fb77e..123240dc6f 100644 --- a/support/datastore/history_archive.go +++ b/support/datastore/history_archive.go @@ -10,10 +10,12 @@ import ( "github.com/stellar/go/support/storage" ) -const Pubnet = "pubnet" -const Testnet = "testnet" +const ( + Pubnet = "pubnet" + Testnet = "testnet" +) -func CreateHistoryArchiveFromNetworkName(ctx context.Context, networkName string) (historyarchive.ArchiveInterface, error) { +func CreateHistoryArchiveFromNetworkName(ctx context.Context, networkName string, userAgent string) (historyarchive.ArchiveInterface, error) { var historyArchiveUrls []string switch networkName { case Pubnet: @@ -26,7 +28,7 @@ func CreateHistoryArchiveFromNetworkName(ctx context.Context, networkName string return historyarchive.NewArchivePool(historyArchiveUrls, historyarchive.ArchiveOptions{ ConnectOptions: storage.ConnectOptions{ - UserAgent: "ledger-exporter", + UserAgent: userAgent, Context: ctx, }, }) diff --git a/support/datastore/ledgerbatch_config.go b/support/datastore/ledgerbatch_config.go index 4ebac0d110..e70bfb5fb0 100644 --- a/support/datastore/ledgerbatch_config.go +++ b/support/datastore/ledgerbatch_config.go @@ -7,24 +7,24 @@ import ( "github.com/stellar/go/support/compressxdr" ) -type LedgerBatchConfig struct { +type DataStoreSchema struct { LedgersPerFile uint32 `toml:"ledgers_per_file"` FilesPerPartition uint32 `toml:"files_per_partition"` } -func (ec LedgerBatchConfig) GetSequenceNumberStartBoundary(ledgerSeq uint32) uint32 { +func (ec DataStoreSchema) GetSequenceNumberStartBoundary(ledgerSeq uint32) uint32 { if ec.LedgersPerFile == 0 { return 0 } return (ledgerSeq / ec.LedgersPerFile) * ec.LedgersPerFile } -func (ec LedgerBatchConfig) GetSequenceNumberEndBoundary(ledgerSeq uint32) uint32 { +func (ec DataStoreSchema) GetSequenceNumberEndBoundary(ledgerSeq uint32) uint32 { return ec.GetSequenceNumberStartBoundary(ledgerSeq) + ec.LedgersPerFile - 1 } // GetObjectKeyFromSequenceNumber generates the object key name from the ledger sequence number based on configuration. -func (ec LedgerBatchConfig) GetObjectKeyFromSequenceNumber(ledgerSeq uint32) string { +func (ec DataStoreSchema) GetObjectKeyFromSequenceNumber(ledgerSeq uint32) string { var objectKey string if ec.FilesPerPartition > 1 { diff --git a/support/datastore/ledgerbatch_config_test.go b/support/datastore/ledgerbatch_config_test.go index bd79dd265d..f95fef168e 100644 --- a/support/datastore/ledgerbatch_config_test.go +++ b/support/datastore/ledgerbatch_config_test.go @@ -31,7 +31,7 @@ func TestGetObjectKeyFromSequenceNumber(t *testing.T) { for _, tc := range testCases { t.Run(fmt.Sprintf("LedgerSeq-%d-LedgersPerFile-%d", tc.ledgerSeq, tc.ledgersPerFile), func(t *testing.T) { - config := LedgerBatchConfig{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile} + config := DataStoreSchema{FilesPerPartition: tc.filesPerPartition, LedgersPerFile: tc.ledgersPerFile} key := config.GetObjectKeyFromSequenceNumber(tc.ledgerSeq) require.Equal(t, tc.expectedKey, key) }) @@ -39,7 +39,7 @@ func TestGetObjectKeyFromSequenceNumber(t *testing.T) { } func TestGetObjectKeyFromSequenceNumber_ObjectKeyDescOrder(t *testing.T) { - config := LedgerBatchConfig{ + config := DataStoreSchema{ LedgersPerFile: 1, FilesPerPartition: 10, } diff --git a/support/datastore/metadata.go b/support/datastore/metadata.go index db734bbd4d..bd943b1987 100644 --- a/support/datastore/metadata.go +++ b/support/datastore/metadata.go @@ -9,7 +9,7 @@ type MetaData struct { EndLedgerCloseTime int64 ProtocolVersion uint32 CoreVersion string - Network string + NetworkPassPhrase string CompressionType string Version string } @@ -22,7 +22,7 @@ func (m MetaData) ToMap() map[string]string { "end-ledger-close-time": strconv.FormatInt(m.EndLedgerCloseTime, 10), "protocol-version": strconv.FormatInt(int64(m.ProtocolVersion), 10), "core-version": m.CoreVersion, - "network": m.Network, + "network-passphrase": m.NetworkPassPhrase, "compression-type": m.CompressionType, "version": m.Version, } @@ -71,7 +71,7 @@ func NewMetaDataFromMap(data map[string]string) (MetaData, error) { } metaData.CoreVersion = data["core-version"] - metaData.Network = data["network"] + metaData.NetworkPassPhrase = data["network-passphrase"] metaData.CompressionType = data["compression-type"] metaData.Version = data["version"] diff --git a/support/datastore/metadata_test.go b/support/datastore/metadata_test.go index 6ec6a176b8..ffc92f4e51 100644 --- a/support/datastore/metadata_test.go +++ b/support/datastore/metadata_test.go @@ -22,7 +22,7 @@ func TestMetaDataToMap(t *testing.T) { EndLedgerCloseTime: 987654321, ProtocolVersion: 3, CoreVersion: "v1.2.3", - Network: "testnet", + NetworkPassPhrase: "testnet passphrase", CompressionType: "gzip", Version: "1.0.0", }, @@ -33,7 +33,7 @@ func TestMetaDataToMap(t *testing.T) { "end-ledger-close-time": "987654321", "protocol-version": "3", "core-version": "v1.2.3", - "network": "testnet", + "network-passphrase": "testnet passphrase", "compression-type": "gzip", "version": "1.0.0", }, @@ -48,7 +48,7 @@ func TestMetaDataToMap(t *testing.T) { EndLedgerCloseTime: tt.metaData.EndLedgerCloseTime, ProtocolVersion: tt.metaData.ProtocolVersion, CoreVersion: tt.metaData.CoreVersion, - Network: tt.metaData.Network, + NetworkPassPhrase: tt.metaData.NetworkPassPhrase, CompressionType: tt.metaData.CompressionType, Version: tt.metaData.Version, } @@ -66,7 +66,7 @@ func TestNewMetaDataFromMap(t *testing.T) { "end-ledger-close-time": "987654321", "protocol-version": "3", "core-version": "v1.2.3", - "network": "testnet", + "network-passphrase": "testnet passphrase", "compression-type": "gzip", "version": "1.0.0", } @@ -78,7 +78,7 @@ func TestNewMetaDataFromMap(t *testing.T) { EndLedgerCloseTime: 987654321, ProtocolVersion: 3, CoreVersion: "v1.2.3", - Network: "testnet", + NetworkPassPhrase: "testnet passphrase", CompressionType: "gzip", Version: "1.0.0", } diff --git a/support/datastore/resumablemanager_test.go b/support/datastore/resumablemanager_test.go index 2649a26033..4616f9e4ae 100644 --- a/support/datastore/resumablemanager_test.go +++ b/support/datastore/resumablemanager_test.go @@ -16,10 +16,9 @@ func TestResumability(t *testing.T) { name string startLedger uint32 endLedger uint32 - ledgerBatchConfig LedgerBatchConfig + dataStoreSchema DataStoreSchema absentLedger uint32 findStartOk bool - networkName string latestLedger uint32 errorSnippet string archiveError error @@ -31,11 +30,10 @@ func TestResumability(t *testing.T) { endLedger: 0, absentLedger: 0, findStartOk: false, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test", errorSnippet: "archive error", archiveError: errors.New("archive error"), registerMockCalls: func(store *MockDataStore) {}, @@ -46,11 +44,10 @@ func TestResumability(t *testing.T) { endLedger: 4, absentLedger: 0, findStartOk: false, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test", registerMockCalls: func(mockDataStore *MockDataStore) { mockDataStore.On("Exists", ctx, "FFFFFFFF--0-9.xdr.zstd").Return(true, nil).Once() }, @@ -61,11 +58,10 @@ func TestResumability(t *testing.T) { endLedger: 14, absentLedger: 14, findStartOk: true, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test", registerMockCalls: func(mockDataStore *MockDataStore) { mockDataStore.On("Exists", ctx, "FFFFFFF5--10-19.xdr.zstd").Return(false, nil).Twice() }, @@ -76,11 +72,10 @@ func TestResumability(t *testing.T) { endLedger: 68, absentLedger: 64, findStartOk: true, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(100), LedgersPerFile: uint32(64), }, - networkName: "test", registerMockCalls: func(mockDataStore *MockDataStore) { mockDataStore.On("Exists", ctx, "FFFFFFFF--0-6399/FFFFFFBF--64-127.xdr.zstd").Return(false, nil).Twice() }, @@ -91,11 +86,10 @@ func TestResumability(t *testing.T) { endLedger: 130, absentLedger: 0, findStartOk: false, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(100), LedgersPerFile: uint32(64), }, - networkName: "test", registerMockCalls: func(mockDataStore *MockDataStore) { mockDataStore.On("Exists", ctx, "FFFFFFFF--0-6399/FFFFFF7F--128-191.xdr.zstd").Return(true, nil).Once() }, @@ -106,11 +100,10 @@ func TestResumability(t *testing.T) { endLedger: 127, absentLedger: 2, findStartOk: true, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(100), LedgersPerFile: uint32(64), }, - networkName: "test", registerMockCalls: func(mockDataStore *MockDataStore) { mockDataStore.On("Exists", ctx, "FFFFFFFF--0-6399/FFFFFFBF--64-127.xdr.zstd").Return(true, nil).Once() mockDataStore.On("Exists", ctx, "FFFFFFFF--0-6399/FFFFFFFF--0-63.xdr.zstd").Return(false, nil).Once() @@ -122,11 +115,10 @@ func TestResumability(t *testing.T) { endLedger: 24, absentLedger: 0, findStartOk: false, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test", errorSnippet: "datastore error happened", registerMockCalls: func(mockDataStore *MockDataStore) { mockDataStore.On("Exists", ctx, "FFFFFFEB--20-29.xdr.zstd").Return(false, errors.New("datastore error happened")).Once() @@ -138,11 +130,10 @@ func TestResumability(t *testing.T) { endLedger: 50, absentLedger: 40, findStartOk: true, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test", registerMockCalls: func(mockDataStore *MockDataStore) { mockDataStore.On("Exists", ctx, "FFFFFFCD--50-59.xdr.zstd").Return(false, nil).Once() mockDataStore.On("Exists", ctx, "FFFFFFE1--30-39.xdr.zstd").Return(true, nil).Once() @@ -155,11 +146,10 @@ func TestResumability(t *testing.T) { endLedger: 85, absentLedger: 80, findStartOk: true, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test", registerMockCalls: func(mockDataStore *MockDataStore) { mockDataStore.On("Exists", ctx, "FFFFFFB9--70-79.xdr.zstd").Return(true, nil).Once() mockDataStore.On("Exists", ctx, "FFFFFFAF--80-89.xdr.zstd").Return(false, nil).Twice() @@ -171,11 +161,10 @@ func TestResumability(t *testing.T) { endLedger: 275, absentLedger: 0, findStartOk: false, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test", registerMockCalls: func(mockDataStore *MockDataStore) { mockDataStore.On("Exists", ctx, "FFFFFEFB--260-269.xdr.zstd").Return(true, nil).Once() mockDataStore.On("Exists", ctx, "FFFFFEF1--270-279.xdr.zstd").Return(true, nil).Once() @@ -187,11 +176,10 @@ func TestResumability(t *testing.T) { endLedger: 125, absentLedger: 95, findStartOk: true, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test", registerMockCalls: func(mockDataStore *MockDataStore) { mockDataStore.On("Exists", ctx, "FFFFFF87--120-129.xdr.zstd").Return(false, nil).Once() mockDataStore.On("Exists", ctx, "FFFFFF91--110-119.xdr.zstd").Return(false, nil).Once() @@ -205,11 +193,10 @@ func TestResumability(t *testing.T) { endLedger: 10, absentLedger: 0, findStartOk: false, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test", errorSnippet: "Invalid start value", registerMockCalls: func(store *MockDataStore) {}, }, @@ -219,11 +206,10 @@ func TestResumability(t *testing.T) { endLedger: 0, absentLedger: 1145, findStartOk: true, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test2", latestLedger: uint32(2000), registerMockCalls: func(mockDataStore *MockDataStore) { mockDataStore.On("Exists", ctx, "FFFFF9A1--1630-1639.xdr.zstd").Return(false, nil).Once() @@ -242,11 +228,10 @@ func TestResumability(t *testing.T) { endLedger: 0, absentLedger: 2250, findStartOk: true, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test3", latestLedger: uint32(3000), registerMockCalls: func(mockDataStore *MockDataStore) { mockDataStore.On("Exists", ctx, "FFFFF5B9--2630-2639.xdr.zstd").Return(false, nil).Once() @@ -264,11 +249,10 @@ func TestResumability(t *testing.T) { endLedger: 0, absentLedger: 4070, findStartOk: true, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test4", latestLedger: uint32(4000), registerMockCalls: func(mockDataStore *MockDataStore) { mockDataStore.On("Exists", ctx, "FFFFF1D1--3630-3639.xdr.zstd").Return(true, nil).Once() @@ -286,11 +270,10 @@ func TestResumability(t *testing.T) { endLedger: 0, absentLedger: 0, findStartOk: false, - ledgerBatchConfig: LedgerBatchConfig{ + dataStoreSchema: DataStoreSchema{ FilesPerPartition: uint32(1), LedgersPerFile: uint32(10), }, - networkName: "test5", latestLedger: uint32(5000), errorSnippet: "Invalid start value of 5129, it is greater than network's latest ledger of 5128", registerMockCalls: func(store *MockDataStore) {}, @@ -304,7 +287,7 @@ func TestResumability(t *testing.T) { mockDataStore := &MockDataStore{} tt.registerMockCalls(mockDataStore) - resumableManager := NewResumableManager(mockDataStore, tt.networkName, tt.ledgerBatchConfig, mockArchive) + resumableManager := NewResumableManager(mockDataStore, tt.dataStoreSchema, mockArchive) absentLedger, ok, err := resumableManager.FindStart(ctx, tt.startLedger, tt.endLedger) if tt.errorSnippet != "" { require.ErrorContains(t, err, tt.errorSnippet) diff --git a/support/datastore/resumeablemanager.go b/support/datastore/resumeablemanager.go index f552dcf106..35031d73f6 100644 --- a/support/datastore/resumeablemanager.go +++ b/support/datastore/resumeablemanager.go @@ -37,19 +37,16 @@ type ResumableManager interface { } type resumableManagerService struct { - network string - ledgerBatchConfig LedgerBatchConfig + ledgerBatchConfig DataStoreSchema dataStore DataStore archive historyarchive.ArchiveInterface } func NewResumableManager(dataStore DataStore, - network string, - ledgerBatchConfig LedgerBatchConfig, + ledgerBatchConfig DataStoreSchema, archive historyarchive.ArchiveInterface) ResumableManager { return &resumableManagerService{ ledgerBatchConfig: ledgerBatchConfig, - network: network, dataStore: dataStore, archive: archive, } @@ -60,7 +57,7 @@ func (rm resumableManagerService) FindStart(ctx context.Context, start, end uint return 0, false, errors.New("Invalid start value, must be greater than zero") } - log.WithField("start", start).WithField("end", end).WithField("network", rm.network) + log.WithField("start", start).WithField("end", end) networkLatest := uint32(0) if end < 1 { @@ -71,7 +68,7 @@ func (rm resumableManagerService) FindStart(ctx context.Context, start, end uint return 0, false, err } networkLatest = networkLatest + (GetHistoryArchivesCheckPointFrequency() * 2) - log.Infof("Resumability computed effective latest network ledger including padding of checkpoint frequency to be %d + for network=%v", networkLatest, rm.network) + log.Infof("Resumability computed effective latest network ledger including padding of checkpoint frequency to be %d", networkLatest) if start > networkLatest { // requested to start at a point beyond the latest network, resume not applicable. From d62fa1d945820409313a388590c6998fb2f2eb1f Mon Sep 17 00:00:00 2001 From: urvisavla Date: Fri, 14 Jun 2024 14:14:39 -0700 Subject: [PATCH 189/234] exp/services/ledgerexporter: Streamline docker setup (#5348) --- exp/services/ledgerexporter/Makefile | 8 ++-- exp/services/ledgerexporter/docker/Dockerfile | 7 +--- .../ledgerexporter/docker/config.test.toml | 13 ++++++ exp/services/ledgerexporter/docker/start | 42 ------------------- 4 files changed, 18 insertions(+), 52 deletions(-) create mode 100644 exp/services/ledgerexporter/docker/config.test.toml delete mode 100644 exp/services/ledgerexporter/docker/start diff --git a/exp/services/ledgerexporter/Makefile b/exp/services/ledgerexporter/Makefile index 10bf16e9dd..971fc3eb25 100644 --- a/exp/services/ledgerexporter/Makefile +++ b/exp/services/ledgerexporter/Makefile @@ -33,12 +33,10 @@ docker-test: docker-clean # Run the ledger-exporter $(SUDO) docker run --platform linux/amd64 -t --network test-network\ - -e NETWORK=pubnet \ - -e ARCHIVE_TARGET=exporter-test/test-subpath \ - -e START=1000 \ - -e END=2000 \ + -v ${PWD}/exp/services/ledgerexporter/docker/config.test.toml:/config.toml \ -e STORAGE_EMULATOR_HOST=http://fake-gcs-server:4443 \ - $(DOCKER_IMAGE):$(VERSION) + $(DOCKER_IMAGE):$(VERSION) \ + scan-and-fill --start 1000 --end 2000 $(MAKE) docker-clean diff --git a/exp/services/ledgerexporter/docker/Dockerfile b/exp/services/ledgerexporter/docker/Dockerfile index 59e57030f3..7144800d87 100644 --- a/exp/services/ledgerexporter/docker/Dockerfile +++ b/exp/services/ledgerexporter/docker/Dockerfile @@ -26,12 +26,9 @@ RUN echo "deb https://apt.stellar.org focal unstable" >/etc/apt/sources.list.d/S RUN apt-get update && apt-get install -y stellar-core=${STELLAR_CORE_VERSION} RUN apt-get clean -COPY exp/services/ledgerexporter/docker/start / - -RUN ["chmod", "+x", "/start"] - COPY --from=builder /go/bin/ledgerexporter /usr/bin/ledgerexporter -ENTRYPOINT ["/start"] +ENTRYPOINT ["/usr/bin/ledgerexporter"] +CMD ["--help"] diff --git a/exp/services/ledgerexporter/docker/config.test.toml b/exp/services/ledgerexporter/docker/config.test.toml new file mode 100644 index 0000000000..c5c4519f0b --- /dev/null +++ b/exp/services/ledgerexporter/docker/config.test.toml @@ -0,0 +1,13 @@ +[datastore_config] +type = "GCS" + +[datastore_config.params] +destination_bucket_path = "exporter-test/ledgers/testnet" + +[datastore_config.schema] +ledgers_per_file = 1 +files_per_partition = 64000 + +[stellar_core_config] + network = "testnet" + diff --git a/exp/services/ledgerexporter/docker/start b/exp/services/ledgerexporter/docker/start deleted file mode 100644 index 3b61c74eee..0000000000 --- a/exp/services/ledgerexporter/docker/start +++ /dev/null @@ -1,42 +0,0 @@ -#! /usr/bin/env bash -set -e - -# Validation -if [ -z "$ARCHIVE_TARGET" ]; then - echo "error: undefined ARCHIVE_TARGET env variable" - exit 1 -fi - -if [ -z "$NETWORK" ]; then - echo "error: undefined NETWORK env variable" - exit 1 -fi - -ledgers_per_file="${LEDGERS_PER_FILE:-1}" -files_per_partition="${FILES_PER_PARTITION:-64000}" - -# Generate TOML configuration -cat < config.toml - -[datastore_config] -type = "GCS" - -[datastore_config.params] -destination_bucket_path = "${ARCHIVE_TARGET}/${NETWORK}" - -[datastore_config.schema] - ledgers_per_file = $ledgers_per_file - files_per_partition = $files_per_partition - -[stellar_core_config] - network = "${NETWORK}" -EOF - -# Check if START or END variables are set -if [[ -n "$START" || -n "$END" ]]; then - echo "START: $START END: $END" - /usr/bin/ledgerexporter scan-and-fill --config-file config.toml --start $START --end $END -else - echo "Error: No ledger range provided." - exit 1 -fi From 3a31ed780c580e4d9419ebda5532c1c16748f432 Mon Sep 17 00:00:00 2001 From: Prit Sheth <124409873+psheth9@users.noreply.github.com> Date: Fri, 14 Jun 2024 16:40:01 -0700 Subject: [PATCH 190/234] Expose captive core version details (#5332) * Expose captive core version details * Log error instead of terminating the process * Fetch core-version using stellar-core version * fix formatting * Fix review comments * Use log from config --- ingest/ledgerbackend/captive_core_backend.go | 39 ++++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/ingest/ledgerbackend/captive_core_backend.go b/ingest/ledgerbackend/captive_core_backend.go index 25b22d01d5..9dfc82ac02 100644 --- a/ingest/ledgerbackend/captive_core_backend.go +++ b/ingest/ledgerbackend/captive_core_backend.go @@ -6,6 +6,8 @@ import ( "fmt" "net/http" "os" + "os/exec" + "strings" "sync" "time" @@ -110,8 +112,9 @@ type CaptiveStellarCore struct { lastLedger *uint32 // end of current segment if offline, nil if online previousLedgerHash *string - config CaptiveCoreConfig - stellarCoreClient *stellarcore.Client + config CaptiveCoreConfig + stellarCoreClient *stellarcore.Client + captiveCoreVersion string // Updates when captive-core restarts } // CaptiveCoreConfig contains all the parameters required to create a CaptiveStellarCore instance @@ -200,6 +203,7 @@ func NewCaptive(config CaptiveCoreConfig) (*CaptiveStellarCore, error) { } c.stellarCoreRunnerFactory = func() stellarCoreRunnerInterface { + c.setCoreVersion() return newStellarCoreRunner(config) } @@ -211,7 +215,7 @@ func NewCaptive(config CaptiveCoreConfig) (*CaptiveStellarCore, error) { URL: fmt.Sprintf("http://localhost:%d", config.Toml.HTTPPort), } } - + c.setCoreVersion() return c, nil } @@ -245,6 +249,35 @@ func (c *CaptiveStellarCore) coreVersionMetric() float64 { return float64(info.Info.ProtocolVersion) } +// By default, it points to exec.Command, overridden for testing purpose +var execCommand = exec.Command + +// Executes the "stellar-core version" command and parses its output to extract +// the core version +// The output of the "version" command is expected to be a multi-line string where the +// first line is the core version in format "vX.Y.Z-*". +func (c *CaptiveStellarCore) setCoreVersion() { + versionCmd := execCommand(c.config.BinaryPath, "version") + versionOutput, err := versionCmd.Output() + if err != nil { + c.config.Log.Errorf("failed to execute stellar-core version command: %s", err) + } + + // Split the output into lines + rows := strings.Split(string(versionOutput), "\n") + if len(rows) == 0 || len(rows[0]) == 0 { + c.config.Log.Error("stellar-core version not found") + return + } + + c.captiveCoreVersion = rows[0] + c.config.Log.Infof("stellar-core version: %s", c.captiveCoreVersion) +} + +func (c *CaptiveStellarCore) GetCoreVersion() string { + return c.captiveCoreVersion +} + func (c *CaptiveStellarCore) registerMetrics(registry *prometheus.Registry, namespace string) { coreSynced := prometheus.NewGaugeFunc( prometheus.GaugeOpts{ From 100dc4fa6043cf65dd17f53f352cbf6752823847 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Mon, 17 Jun 2024 20:35:18 +0200 Subject: [PATCH 191/234] Misc fixes (#5352) --- exp/services/ledgerexporter/internal/app.go | 2 +- .../ledgerexporter/internal/config.go | 4 ++- .../ledgerexporter/internal/config_test.go | 4 +-- historyarchive/archive.go | 2 ++ historyarchive/archive_pool.go | 7 +++-- ingest/ledgerbackend/captive_core_backend.go | 7 +++-- ingest/ledgerbackend/main.go | 26 +++++++++-------- ingest/ledgerbackend/mock_cmd_test.go | 17 ++++++----- ingest/ledgerbackend/stellar_core_runner.go | 29 +++++++++++++++---- services/horizon/internal/ingest/main.go | 1 + support/datastore/history_archive.go | 7 +++-- support/db/session.go | 24 +-------------- 12 files changed, 71 insertions(+), 59 deletions(-) diff --git a/exp/services/ledgerexporter/internal/app.go b/exp/services/ledgerexporter/internal/app.go index eb0f544ec3..54c7a72096 100644 --- a/exp/services/ledgerexporter/internal/app.go +++ b/exp/services/ledgerexporter/internal/app.go @@ -107,7 +107,7 @@ func (a *App) init(ctx context.Context, runtimeSettings RuntimeSettings) error { if a.config, err = NewConfig(runtimeSettings); err != nil { return errors.Wrap(err, "Could not load configuration") } - if archive, err = a.config.GenerateHistoryArchive(ctx); err != nil { + if archive, err = a.config.GenerateHistoryArchive(ctx, logger); err != nil { return err } if err = a.config.ValidateAndSetLedgerRange(ctx, archive); err != nil { diff --git a/exp/services/ledgerexporter/internal/config.go b/exp/services/ledgerexporter/internal/config.go index ff0ffbe2bf..b040e2dcc9 100644 --- a/exp/services/ledgerexporter/internal/config.go +++ b/exp/services/ledgerexporter/internal/config.go @@ -11,6 +11,7 @@ import ( "github.com/stellar/go/historyarchive" "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/network" + "github.com/stellar/go/support/log" "github.com/pelletier/go-toml" @@ -142,12 +143,13 @@ func (config *Config) ValidateAndSetLedgerRange(ctx context.Context, archive his return nil } -func (config *Config) GenerateHistoryArchive(ctx context.Context) (historyarchive.ArchiveInterface, error) { +func (config *Config) GenerateHistoryArchive(ctx context.Context, entry *log.Entry) (historyarchive.ArchiveInterface, error) { return historyarchive.NewArchivePool(config.StellarCoreConfig.HistoryArchiveUrls, historyarchive.ArchiveOptions{ ConnectOptions: storage.ConnectOptions{ UserAgent: config.UserAgent, Context: ctx, }, + Logger: logger, }) } diff --git a/exp/services/ledgerexporter/internal/config_test.go b/exp/services/ledgerexporter/internal/config_test.go index 37dc574012..6523df7605 100644 --- a/exp/services/ledgerexporter/internal/config_test.go +++ b/exp/services/ledgerexporter/internal/config_test.go @@ -46,7 +46,7 @@ func TestGenerateHistoryArchiveFromPreconfiguredNetwork(t *testing.T) { RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/valid_captive_core_preconfigured.toml", Mode: Append}) require.NoError(t, err) - _, err = config.GenerateHistoryArchive(ctx) + _, err = config.GenerateHistoryArchive(ctx, nil) require.NoError(t, err) } @@ -56,7 +56,7 @@ func TestGenerateHistoryArchiveFromManulConfiguredNetwork(t *testing.T) { RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/valid_captive_core_manual.toml", Mode: Append}) require.NoError(t, err) - _, err = config.GenerateHistoryArchive(ctx) + _, err = config.GenerateHistoryArchive(ctx, nil) require.NoError(t, err) } diff --git a/historyarchive/archive.go b/historyarchive/archive.go index d52c41ec43..4f9e14380f 100644 --- a/historyarchive/archive.go +++ b/historyarchive/archive.go @@ -23,6 +23,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/stellar/go/support/errors" + supportlog "github.com/stellar/go/support/log" "github.com/stellar/go/support/storage" "github.com/stellar/go/xdr" ) @@ -43,6 +44,7 @@ type CommandOptions struct { type ArchiveOptions struct { storage.ConnectOptions + Logger *supportlog.Entry // NetworkPassphrase defines the expected network of history archive. It is // checked when getting HAS. If network passphrase does not match, error is // returned. diff --git a/historyarchive/archive_pool.go b/historyarchive/archive_pool.go index 259f8ff48a..48178ade26 100644 --- a/historyarchive/archive_pool.go +++ b/historyarchive/archive_pool.go @@ -10,6 +10,7 @@ import ( "time" "github.com/pkg/errors" + log "github.com/stellar/go/support/log" "github.com/stellar/go/xdr" @@ -19,6 +20,7 @@ import ( // An ArchivePool is just a collection of `ArchiveInterface`s so that we can // distribute requests fairly throughout the pool. type ArchivePool struct { + logger *log.Entry backoff backoff.BackOff pool []ArchiveInterface curr int @@ -46,6 +48,7 @@ func NewArchivePoolWithBackoff(archiveURLs []string, opts ArchiveOptions, strate ap := ArchivePool{ pool: make([]ArchiveInterface, 0, len(archiveURLs)), backoff: strategy, + logger: opts.Logger, } var lastErr error @@ -107,8 +110,8 @@ func (pa *ArchivePool) runRoundRobin(runner func(ai ArchiveInterface) error) err } // Intentionally avoid logging context errors - if stats := ai.GetStats(); len(stats) > 0 { - log.WithField("error", err).Warnf( + if stats := ai.GetStats(); len(stats) > 0 && pa.logger != nil { + pa.logger.WithField("error", err).Warnf( "Encountered an error with archive '%s'", stats[0].GetBackendName()) } diff --git a/ingest/ledgerbackend/captive_core_backend.go b/ingest/ledgerbackend/captive_core_backend.go index 9dfc82ac02..879049f873 100644 --- a/ingest/ledgerbackend/captive_core_backend.go +++ b/ingest/ledgerbackend/captive_core_backend.go @@ -179,6 +179,7 @@ func NewCaptive(config CaptiveCoreConfig) (*CaptiveStellarCore, error) { archivePool, err := historyarchive.NewArchivePool( config.HistoryArchiveURLs, historyarchive.ArchiveOptions{ + Logger: config.Log, NetworkPassphrase: config.NetworkPassphrase, CheckpointFrequency: config.CheckpointFrequency, ConnectOptions: storage.ConnectOptions{ @@ -780,14 +781,14 @@ func (c *CaptiveStellarCore) GetLatestLedgerSequence(ctx context.Context) (uint3 // all subsequent calls to PrepareRange(), GetLedger(), etc will fail. // Close is thread-safe and can be called from another go routine. func (c *CaptiveStellarCore) Close() error { + // after the CaptiveStellarCore context is canceled all subsequent calls to PrepareRange() will fail + c.cancel() + c.stellarCoreLock.RLock() defer c.stellarCoreLock.RUnlock() c.closed = true - // after the CaptiveStellarCore context is canceled all subsequent calls to PrepareRange() will fail - c.cancel() - // TODO: Sucks to ignore the error here, but no worse than it was before, // so... if c.ledgerHashStore != nil { diff --git a/ingest/ledgerbackend/main.go b/ingest/ledgerbackend/main.go index 4a5d119de2..6029b8a1f6 100644 --- a/ingest/ledgerbackend/main.go +++ b/ingest/ledgerbackend/main.go @@ -1,7 +1,6 @@ package ledgerbackend import ( - "io" "io/fs" "io/ioutil" "os" @@ -40,7 +39,7 @@ func (realSystemCaller) stat(name string) (isDir, error) { func (realSystemCaller) command(name string, arg ...string) cmdI { cmd := exec.Command(name, arg...) - return &realCmd{cmd} + return &realCmd{Cmd: cmd} } type cmdI interface { @@ -49,36 +48,39 @@ type cmdI interface { Start() error Run() error setDir(dir string) - setStdout(stdout io.Writer) - getStdout() io.Writer - setStderr(stderr io.Writer) - getStderr() io.Writer + setStdout(stdout *logLineWriter) + getStdout() *logLineWriter + setStderr(stderr *logLineWriter) + getStderr() *logLineWriter getProcess() *os.Process setExtraFiles([]*os.File) } type realCmd struct { *exec.Cmd + stdout, stderr *logLineWriter } func (r *realCmd) setDir(dir string) { r.Cmd.Dir = dir } -func (r *realCmd) setStdout(stdout io.Writer) { +func (r *realCmd) setStdout(stdout *logLineWriter) { + r.stdout = stdout r.Cmd.Stdout = stdout } -func (r *realCmd) getStdout() io.Writer { - return r.Cmd.Stdout +func (r *realCmd) getStdout() *logLineWriter { + return r.stdout } -func (r *realCmd) setStderr(stderr io.Writer) { +func (r *realCmd) setStderr(stderr *logLineWriter) { + r.stderr = stderr r.Cmd.Stderr = stderr } -func (r *realCmd) getStderr() io.Writer { - return r.Cmd.Stderr +func (r *realCmd) getStderr() *logLineWriter { + return r.stderr } func (r *realCmd) getProcess() *os.Process { diff --git a/ingest/ledgerbackend/mock_cmd_test.go b/ingest/ledgerbackend/mock_cmd_test.go index a28b6c8d01..bf06a9ae86 100644 --- a/ingest/ledgerbackend/mock_cmd_test.go +++ b/ingest/ledgerbackend/mock_cmd_test.go @@ -35,22 +35,22 @@ func (m *mockCmd) setDir(dir string) { m.Called(dir) } -func (m *mockCmd) setStdout(stdout io.Writer) { +func (m *mockCmd) setStdout(stdout *logLineWriter) { m.Called(stdout) } -func (m *mockCmd) getStdout() io.Writer { +func (m *mockCmd) getStdout() *logLineWriter { args := m.Called() - return args.Get(0).(io.Writer) + return args.Get(0).(*logLineWriter) } -func (m *mockCmd) setStderr(stderr io.Writer) { +func (m *mockCmd) setStderr(stderr *logLineWriter) { m.Called(stderr) } -func (m *mockCmd) getStderr() io.Writer { +func (m *mockCmd) getStderr() *logLineWriter { args := m.Called() - return args.Get(0).(io.Writer) + return args.Get(0).(*logLineWriter) } func (m *mockCmd) getProcess() *os.Process { @@ -64,12 +64,13 @@ func (m *mockCmd) setExtraFiles(files []*os.File) { func simpleCommandMock() *mockCmd { _, writer := io.Pipe() + llw := logLineWriter{pipeWriter: writer} cmdMock := &mockCmd{} cmdMock.On("setDir", mock.Anything) cmdMock.On("setStdout", mock.Anything) - cmdMock.On("getStdout").Return(writer) + cmdMock.On("getStdout").Return(&llw) cmdMock.On("setStderr", mock.Anything) - cmdMock.On("getStderr").Return(writer) + cmdMock.On("getStderr").Return(&llw) cmdMock.On("getProcess").Return(&os.Process{}).Maybe() cmdMock.On("setExtraFiles", mock.Anything) cmdMock.On("Start").Return(nil) diff --git a/ingest/ledgerbackend/stellar_core_runner.go b/ingest/ledgerbackend/stellar_core_runner.go index 7f883b69c5..57e8c1c0f9 100644 --- a/ingest/ledgerbackend/stellar_core_runner.go +++ b/ingest/ledgerbackend/stellar_core_runner.go @@ -194,13 +194,32 @@ func (r *stellarCoreRunner) getConfFileName() string { return path } -func (r *stellarCoreRunner) getLogLineWriter() io.Writer { +type logLineWriter struct { + pipeWriter *io.PipeWriter + wg sync.WaitGroup +} + +func (l *logLineWriter) Write(p []byte) (n int, err error) { + return l.pipeWriter.Write(p) +} + +func (l *logLineWriter) Close() error { + err := l.pipeWriter.Close() + l.wg.Wait() + return err +} + +func (r *stellarCoreRunner) getLogLineWriter() *logLineWriter { rd, wr := io.Pipe() br := bufio.NewReader(rd) - + result := &logLineWriter{ + pipeWriter: wr, + } // Strip timestamps from log lines from captive stellar-core. We emit our own. dateRx := regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3} `) + result.wg.Add(1) go func() { + defer result.wg.Done() levelRx := regexp.MustCompile(`\[(\w+) ([A-Z]+)\] (.*)`) for { line, err := br.ReadString('\n') @@ -238,7 +257,7 @@ func (r *stellarCoreRunner) getLogLineWriter() io.Writer { } } }() - return wr + return result } func (r *stellarCoreRunner) offlineInfo() (stellarcore.InfoResponse, error) { @@ -526,8 +545,8 @@ func (r *stellarCoreRunner) handleExit() { // closeLogLineWriters closes the go routines created by getLogLineWriter() func (r *stellarCoreRunner) closeLogLineWriters(cmd cmdI) { - cmd.getStdout().(*io.PipeWriter).Close() - cmd.getStderr().(*io.PipeWriter).Close() + cmd.getStdout().Close() + cmd.getStderr().Close() } // getMetaPipe returns a channel which contains ledgers streamed from the captive core subprocess diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 7e109c391a..0769faee4f 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -240,6 +240,7 @@ func NewSystem(config Config) (System, error) { archive, err := historyarchive.NewArchivePool( config.HistoryArchiveURLs, historyarchive.ArchiveOptions{ + Logger: log.WithField("subservice", "archive"), NetworkPassphrase: config.NetworkPassphrase, CheckpointFrequency: config.CheckpointFrequency, ConnectOptions: storage.ConnectOptions{ diff --git a/support/datastore/history_archive.go b/support/datastore/history_archive.go index 123240dc6f..9fd291bac7 100644 --- a/support/datastore/history_archive.go +++ b/support/datastore/history_archive.go @@ -3,10 +3,12 @@ package datastore import ( "context" + log "github.com/sirupsen/logrus" + "github.com/stellar/go/historyarchive" "github.com/stellar/go/network" "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/log" + supportlog "github.com/stellar/go/support/log" "github.com/stellar/go/support/storage" ) @@ -15,7 +17,7 @@ const ( Testnet = "testnet" ) -func CreateHistoryArchiveFromNetworkName(ctx context.Context, networkName string, userAgent string) (historyarchive.ArchiveInterface, error) { +func CreateHistoryArchiveFromNetworkName(ctx context.Context, networkName string, userAgent string, logger *supportlog.Entry) (historyarchive.ArchiveInterface, error) { var historyArchiveUrls []string switch networkName { case Pubnet: @@ -27,6 +29,7 @@ func CreateHistoryArchiveFromNetworkName(ctx context.Context, networkName string } return historyarchive.NewArchivePool(historyArchiveUrls, historyarchive.ArchiveOptions{ + Logger: logger, ConnectOptions: storage.ConnectOptions{ UserAgent: userAgent, Context: ctx, diff --git a/support/db/session.go b/support/db/session.go index 6b5c2b18c0..dc3c98cafb 100644 --- a/support/db/session.go +++ b/support/db/session.go @@ -55,29 +55,7 @@ func (s *Session) context(requestCtx context.Context) (context.Context, context. // Begin binds this session to a new transaction. func (s *Session) Begin(ctx context.Context) error { - if s.tx != nil { - return errors.New("already in transaction") - } - ctx, cancel, err := s.context(ctx) - if err != nil { - return err - } - - tx, err := s.DB.BeginTxx(ctx, nil) - if err != nil { - if knownErr := s.handleError(err, ctx); knownErr != nil { - cancel() - return knownErr - } - - cancel() - return errors.Wrap(err, "beginx failed") - } - log.Debug("sql: begin") - s.tx = tx - s.txOptions = nil - s.txCancel = cancel - return nil + return s.BeginTx(ctx, nil) } // BeginTx binds this session to a new transaction which is configured with the From 0b7df85d46ae850fb7758ad08944b0ae97bb0d82 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Thu, 20 Jun 2024 15:53:23 -0700 Subject: [PATCH 192/234] ingest/ledgerbackend: Add version check for protocol 21 (#5346) * Remove protocol 20 from integration tests * Implement a version check for protocol 21 when creating a new captive-core instance. * Add common functions to get the stellar-core build version and protocol version and update ledgerexporter and captivecorebackend to use these functions. --- .github/workflows/horizon.yml | 7 +- exp/services/ledgerexporter/internal/app.go | 2 +- .../ledgerexporter/internal/config.go | 28 ++-- .../ledgerexporter/internal/config_test.go | 117 ++++------------- ingest/ledgerbackend/captive_core_backend.go | 51 +++++--- .../captive_core_backend_test.go | 50 +++++-- ingest/ledgerbackend/stellar_core_version.go | 64 +++++++++ .../stellar_core_version_test.go | 123 ++++++++++++++++++ ingest/ledgerbackend/toml.go | 107 +-------------- ingest/ledgerbackend/toml_test.go | 17 --- ...captive-core-classic-integration-tests.cfg | 15 --- ...ingest-range-classic-integration-tests.cfg | 11 -- services/horizon/internal/flags_test.go | 4 +- .../internal/ingest/db_integration_test.go | 2 + services/horizon/internal/ingest/main.go | 27 ++-- services/horizon/internal/ingest/main_test.go | 24 +++- .../horizon/internal/integration/db_test.go | 13 +- .../internal/test/integration/integration.go | 14 +- 18 files changed, 340 insertions(+), 336 deletions(-) create mode 100644 ingest/ledgerbackend/stellar_core_version.go create mode 100644 ingest/ledgerbackend/stellar_core_version_test.go delete mode 100644 services/horizon/docker/captive-core-classic-integration-tests.cfg delete mode 100644 services/horizon/docker/captive-core-reingest-range-classic-integration-tests.cfg diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index fe7b2df7a8..3ea92c8b17 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -13,7 +13,7 @@ jobs: os: [ubuntu-20.04, ubuntu-22.04] go: ["1.21", "1.22"] pg: [12, 16] - protocol-version: [20, 21] + protocol-version: [21] runs-on: ${{ matrix.os }} services: postgres: @@ -36,9 +36,6 @@ jobs: PROTOCOL_21_CORE_DEBIAN_PKG_VERSION: 21.0.0-1872.c6f474133.focal PROTOCOL_21_CORE_DOCKER_IMG: stellar/stellar-core:21 PROTOCOL_21_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:21.0.0-rc2-73 - PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 21.0.0-1872.c6f474133.focal - PROTOCOL_20_CORE_DOCKER_IMG: stellar/stellar-core:21 - PROTOCOL_20_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:21.0.0-rc2-73 PGHOST: localhost PGPORT: 5432 PGUSER: postgres @@ -101,7 +98,7 @@ jobs: - name: Calculate the source hash id: calculate_source_hash run: | - combined_hash=$(echo "horizon-hash-${{ hashFiles('./horizon') }}-${{ hashFiles('./clients/horizonclient/**') }}-${{ hashFiles('./protocols/horizon/**') }}-${{ hashFiles('./txnbuild/**') }}-${{ hashFiles('./ingest/**') }}-${{ hashFiles('./xdr/**') }}-${{ hashFiles('./services/**') }}-${{ env.PROTOCOL_20_CORE_DOCKER_IMG }}-${{ env.PROTOCOL_19_CORE_DOCKER_IMG }}-${{ env.PREFIX }}" | sha256sum | cut -d ' ' -f 1) + combined_hash=$(echo "horizon-hash-${{ hashFiles('./horizon') }}-${{ hashFiles('./clients/horizonclient/**') }}-${{ hashFiles('./protocols/horizon/**') }}-${{ hashFiles('./txnbuild/**') }}-${{ hashFiles('./ingest/**') }}-${{ hashFiles('./xdr/**') }}-${{ hashFiles('./services/**') }}-${{ env.PROTOCOL_21_CORE_DOCKER_IMG }}-${{ env.PREFIX }}" | sha256sum | cut -d ' ' -f 1) echo "COMBINED_SOURCE_HASH=$combined_hash" >> "$GITHUB_ENV" - name: Restore Horizon binary and integration tests source hash to cache diff --git a/exp/services/ledgerexporter/internal/app.go b/exp/services/ledgerexporter/internal/app.go index 54c7a72096..40cdf90f0e 100644 --- a/exp/services/ledgerexporter/internal/app.go +++ b/exp/services/ledgerexporter/internal/app.go @@ -104,7 +104,7 @@ func (a *App) init(ctx context.Context, runtimeSettings RuntimeSettings) error { collectors.NewGoCollector(), ) - if a.config, err = NewConfig(runtimeSettings); err != nil { + if a.config, err = NewConfig(runtimeSettings, nil); err != nil { return errors.Wrap(err, "Could not load configuration") } if archive, err = a.config.GenerateHistoryArchive(ctx, logger); err != nil { diff --git a/exp/services/ledgerexporter/internal/config.go b/exp/services/ledgerexporter/internal/config.go index b040e2dcc9..d5aad53256 100644 --- a/exp/services/ledgerexporter/internal/config.go +++ b/exp/services/ledgerexporter/internal/config.go @@ -5,8 +5,6 @@ import ( _ "embed" "fmt" "os" - "os/exec" - "strings" "github.com/stellar/go/historyarchive" "github.com/stellar/go/ingest/ledgerbackend" @@ -73,6 +71,7 @@ type Config struct { CoreVersion string SerializedCaptiveCoreToml []byte + CoreBuildVersionFn ledgerbackend.CoreBuildVersionFunc } // This will generate the config based on settings @@ -80,12 +79,16 @@ type Config struct { // settings - requested settings // // return - *Config or an error if any range validation failed. -func NewConfig(settings RuntimeSettings) (*Config, error) { +func NewConfig(settings RuntimeSettings, getCoreVersionFn ledgerbackend.CoreBuildVersionFunc) (*Config, error) { config := &Config{} config.StartLedger = uint32(settings.StartLedger) config.EndLedger = uint32(settings.EndLedger) config.Mode = settings.Mode + config.CoreBuildVersionFn = ledgerbackend.CoreBuildVersion + if getCoreVersionFn != nil { + config.CoreBuildVersionFn = getCoreVersionFn + } logger.Infof("Requested export mode of %v with start=%d, end=%d", settings.Mode.Name(), config.StartLedger, config.EndLedger) @@ -194,26 +197,11 @@ func (config *Config) GenerateCaptiveCoreConfig(coreBinFromPath string) (ledgerb }, nil } -// By default, it points to exec.Command, overridden for testing purpose -var execCommand = exec.Command - -// Executes the "stellar-core version" command and parses its output to extract -// the core version -// The output of the "version" command is expected to be a multi-line string where the -// first line is the core version in format "vX.Y.Z-*". func (c *Config) setCoreVersionInfo() (err error) { - versionCmd := execCommand(c.StellarCoreConfig.StellarCoreBinaryPath, "version") - versionOutput, err := versionCmd.Output() + c.CoreVersion, err = c.CoreBuildVersionFn(c.StellarCoreConfig.StellarCoreBinaryPath) if err != nil { - return fmt.Errorf("failed to execute stellar-core version command: %w", err) - } - - // Split the output into lines - rows := strings.Split(string(versionOutput), "\n") - if len(rows) == 0 || len(rows[0]) == 0 { - return fmt.Errorf("stellar-core version not found") + return fmt.Errorf("failed to set stellar-core version: %w", err) } - c.CoreVersion = rows[0] logger.Infof("stellar-core version: %s", c.CoreVersion) return nil } diff --git a/exp/services/ledgerexporter/internal/config_test.go b/exp/services/ledgerexporter/internal/config_test.go index 6523df7605..f782de5ea4 100644 --- a/exp/services/ledgerexporter/internal/config_test.go +++ b/exp/services/ledgerexporter/internal/config_test.go @@ -3,8 +3,6 @@ package ledgerexporter import ( "context" "fmt" - "os" - "os/exec" "testing" "github.com/stellar/go/network" @@ -13,7 +11,6 @@ import ( "github.com/stretchr/testify/require" "github.com/stellar/go/historyarchive" - "github.com/stellar/go/support/errors" ) func TestNewConfig(t *testing.T) { @@ -23,7 +20,7 @@ func TestNewConfig(t *testing.T) { mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: 5}, nil).Once() config, err := NewConfig( - RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/test.toml", Mode: Append}) + RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/test.toml", Mode: Append}, nil) require.NoError(t, err) err = config.ValidateAndSetLedgerRange(ctx, mockArchive) @@ -43,7 +40,7 @@ func TestNewConfig(t *testing.T) { func TestGenerateHistoryArchiveFromPreconfiguredNetwork(t *testing.T) { ctx := context.Background() config, err := NewConfig( - RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/valid_captive_core_preconfigured.toml", Mode: Append}) + RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/valid_captive_core_preconfigured.toml", Mode: Append}, nil) require.NoError(t, err) _, err = config.GenerateHistoryArchive(ctx, nil) @@ -53,7 +50,7 @@ func TestGenerateHistoryArchiveFromPreconfiguredNetwork(t *testing.T) { func TestGenerateHistoryArchiveFromManulConfiguredNetwork(t *testing.T) { ctx := context.Background() config, err := NewConfig( - RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/valid_captive_core_manual.toml", Mode: Append}) + RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/valid_captive_core_manual.toml", Mode: Append}, nil) require.NoError(t, err) _, err = config.GenerateHistoryArchive(ctx, nil) @@ -62,7 +59,7 @@ func TestGenerateHistoryArchiveFromManulConfiguredNetwork(t *testing.T) { func TestNewConfigUserAgent(t *testing.T) { config, err := NewConfig( - RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/useragent.toml"}) + RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/useragent.toml"}, nil) require.NoError(t, err) require.Equal(t, config.UserAgent, "useragent_x") } @@ -70,20 +67,20 @@ func TestNewConfigUserAgent(t *testing.T) { func TestResumeDisabled(t *testing.T) { // resumable is only enabled when mode is Append config, err := NewConfig( - RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/test.toml", Mode: ScanFill}) + RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/test.toml", Mode: ScanFill}, nil) require.NoError(t, err) require.False(t, config.Resumable()) } func TestInvalidConfigFilePath(t *testing.T) { _, err := NewConfig( - RuntimeSettings{ConfigFilePath: "test/notfound.toml"}) + RuntimeSettings{ConfigFilePath: "test/notfound.toml"}, nil) require.ErrorContains(t, err, "config file test/notfound.toml was not found") } func TestNoCaptiveCoreBin(t *testing.T) { cfg, err := NewConfig( - RuntimeSettings{ConfigFilePath: "test/no_core_bin.toml"}) + RuntimeSettings{ConfigFilePath: "test/no_core_bin.toml"}, nil) require.NoError(t, err) _, err = cfg.GenerateCaptiveCoreConfig("") @@ -92,10 +89,10 @@ func TestNoCaptiveCoreBin(t *testing.T) { func TestDefaultCaptiveCoreBin(t *testing.T) { cfg, err := NewConfig( - RuntimeSettings{ConfigFilePath: "test/no_core_bin.toml"}) + RuntimeSettings{ConfigFilePath: "test/no_core_bin.toml"}, + func(string) (string, error) { return "v20.2.0-2-g6e73c0a88", nil }) require.NoError(t, err) - cmdOut = "v20.2.0-2-g6e73c0a88\n" ccConfig, err := cfg.GenerateCaptiveCoreConfig("/test/default/stellar-core") require.NoError(t, err) require.Equal(t, ccConfig.BinaryPath, "/test/default/stellar-core") @@ -103,20 +100,20 @@ func TestDefaultCaptiveCoreBin(t *testing.T) { func TestInvalidCaptiveCorePreconfiguredNetwork(t *testing.T) { _, err := NewConfig( - RuntimeSettings{ConfigFilePath: "test/invalid_preconfigured_network.toml"}) + RuntimeSettings{ConfigFilePath: "test/invalid_preconfigured_network.toml"}, nil) require.ErrorContains(t, err, "invalid captive core config") } func TestValidCaptiveCorePreconfiguredNetwork(t *testing.T) { cfg, err := NewConfig( - RuntimeSettings{ConfigFilePath: "test/valid_captive_core_preconfigured.toml"}) + RuntimeSettings{ConfigFilePath: "test/valid_captive_core_preconfigured.toml"}, + func(string) (string, error) { return "v20.2.0-2-g6e73c0a88", nil }) require.NoError(t, err) require.Equal(t, cfg.StellarCoreConfig.NetworkPassphrase, network.PublicNetworkPassphrase) require.Equal(t, cfg.StellarCoreConfig.HistoryArchiveUrls, network.PublicNetworkhistoryArchiveURLs) - cmdOut = "v20.2.0-2-g6e73c0a88\n" ccConfig, err := cfg.GenerateCaptiveCoreConfig("") require.NoError(t, err) @@ -131,13 +128,13 @@ func TestValidCaptiveCorePreconfiguredNetwork(t *testing.T) { func TestValidCaptiveCoreManualNetwork(t *testing.T) { cfg, err := NewConfig( - RuntimeSettings{ConfigFilePath: "test/valid_captive_core_manual.toml"}) + RuntimeSettings{ConfigFilePath: "test/valid_captive_core_manual.toml"}, + func(string) (string, error) { return "v20.2.0-2-g6e73c0a88", nil }) require.NoError(t, err) require.Equal(t, cfg.CoreVersion, "") require.Equal(t, cfg.StellarCoreConfig.NetworkPassphrase, "test") require.Equal(t, cfg.StellarCoreConfig.HistoryArchiveUrls, []string{"http://testarchive"}) - cmdOut = "v20.2.0-2-g6e73c0a88\n" ccConfig, err := cfg.GenerateCaptiveCoreConfig("") require.NoError(t, err) @@ -152,12 +149,12 @@ func TestValidCaptiveCoreManualNetwork(t *testing.T) { func TestValidCaptiveCoreOverridenToml(t *testing.T) { cfg, err := NewConfig( - RuntimeSettings{ConfigFilePath: "test/valid_captive_core_override.toml"}) + RuntimeSettings{ConfigFilePath: "test/valid_captive_core_override.toml"}, + func(string) (string, error) { return "v20.2.0-2-g6e73c0a88", nil }) require.NoError(t, err) require.Equal(t, cfg.StellarCoreConfig.NetworkPassphrase, network.PublicNetworkPassphrase) require.Equal(t, cfg.StellarCoreConfig.HistoryArchiveUrls, network.PublicNetworkhistoryArchiveURLs) - cmdOut = "v20.2.0-2-g6e73c0a88\n" ccConfig, err := cfg.GenerateCaptiveCoreConfig("") require.NoError(t, err) @@ -173,13 +170,13 @@ func TestValidCaptiveCoreOverridenToml(t *testing.T) { func TestValidCaptiveCoreOverridenArchiveUrls(t *testing.T) { cfg, err := NewConfig( - RuntimeSettings{ConfigFilePath: "test/valid_captive_core_override_archives.toml"}) + RuntimeSettings{ConfigFilePath: "test/valid_captive_core_override_archives.toml"}, + func(string) (string, error) { return "v20.2.0-2-g6e73c0a88\n", nil }) require.NoError(t, err) require.Equal(t, cfg.StellarCoreConfig.NetworkPassphrase, network.PublicNetworkPassphrase) require.Equal(t, cfg.StellarCoreConfig.HistoryArchiveUrls, []string{"http://testarchive"}) - cmdOut = "v20.2.0-2-g6e73c0a88\n" ccConfig, err := cfg.GenerateCaptiveCoreConfig("") require.NoError(t, err) @@ -194,7 +191,8 @@ func TestValidCaptiveCoreOverridenArchiveUrls(t *testing.T) { func TestInvalidCaptiveCoreTomlPath(t *testing.T) { _, err := NewConfig( - RuntimeSettings{ConfigFilePath: "test/invalid_captive_core_toml_path.toml"}) + RuntimeSettings{ConfigFilePath: "test/invalid_captive_core_toml_path.toml"}, + nil) require.ErrorContains(t, err, "Failed to load captive-core-toml-path file") } @@ -293,7 +291,7 @@ func TestValidateStartAndEndLedger(t *testing.T) { mockedHasCtr++ } config, err := NewConfig( - RuntimeSettings{StartLedger: tt.startLedger, EndLedger: tt.endLedger, ConfigFilePath: "test/validate_start_end.toml", Mode: tt.mode}) + RuntimeSettings{StartLedger: tt.startLedger, EndLedger: tt.endLedger, ConfigFilePath: "test/validate_start_end.toml", Mode: tt.mode}, nil) require.NoError(t, err) err = config.ValidateAndSetLedgerRange(ctx, mockArchive) if tt.errMsg != "" { @@ -365,7 +363,7 @@ func TestAdjustedLedgerRangeBoundedMode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config, err := NewConfig( - RuntimeSettings{StartLedger: tt.start, EndLedger: tt.end, ConfigFilePath: tt.configFile, Mode: ScanFill}) + RuntimeSettings{StartLedger: tt.start, EndLedger: tt.end, ConfigFilePath: tt.configFile, Mode: ScanFill}, nil) require.NoError(t, err) err = config.ValidateAndSetLedgerRange(ctx, mockArchive) @@ -428,7 +426,7 @@ func TestAdjustedLedgerRangeUnBoundedMode(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config, err := NewConfig( - RuntimeSettings{StartLedger: tt.start, EndLedger: tt.end, ConfigFilePath: tt.configFile, Mode: Append}) + RuntimeSettings{StartLedger: tt.start, EndLedger: tt.end, ConfigFilePath: tt.configFile, Mode: Append}, nil) require.NoError(t, err) err = config.ValidateAndSetLedgerRange(ctx, mockArchive) require.NoError(t, err) @@ -438,72 +436,3 @@ func TestAdjustedLedgerRangeUnBoundedMode(t *testing.T) { } mockArchive.AssertExpectations(t) } - -var cmdOut = "" - -func fakeExecCommand(command string, args ...string) *exec.Cmd { - cs := append([]string{"-test.run=TestExecCmdHelperProcess", "--", command}, args...) - cmd := exec.Command(os.Args[0], cs...) - cmd.Env = append(os.Environ(), "GO_EXEC_CMD_HELPER_PROCESS=1", "CMD_OUT="+cmdOut) - return cmd -} - -func init() { - execCommand = fakeExecCommand -} - -func TestExecCmdHelperProcess(t *testing.T) { - if os.Getenv("GO_EXEC_CMD_HELPER_PROCESS") != "1" { - return - } - fmt.Fprint(os.Stdout, os.Getenv("CMD_OUT")) - os.Exit(0) -} - -func TestSetCoreVersionInfo(t *testing.T) { - execCommand = fakeExecCommand - tests := []struct { - name string - commandOutput string - expectedError error - expectedCoreVer string - }{ - { - name: "version found", - commandOutput: "v20.2.0-2-g6e73c0a88\n" + - "rust version: rustc 1.74.1 (a28077b28 2023-12-04)\n" + - "soroban-env-host: \n" + - " curr:\n" + - " package version: 20.2.0\n" + - " git version: 1bfc0f2a2ee134efc1e1b0d5270281d0cba61c2e\n" + - " ledger protocol version: 20\n" + - " pre-release version: 0\n" + - " rs-stellar-xdr:\n" + - " package version: 20.1.0\n" + - " git version: 8b9d623ef40423a8462442b86997155f2c04d3a1\n" + - " base XDR git version: b96148cd4acc372cc9af17b909ffe4b12c43ecb6\n", - expectedError: nil, - expectedCoreVer: "v20.2.0-2-g6e73c0a88", - }, - { - name: "core version not found", - commandOutput: "", - expectedError: errors.New("stellar-core version not found"), - expectedCoreVer: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - config := Config{} - cmdOut = tt.commandOutput - err := config.setCoreVersionInfo() - - if tt.expectedError != nil { - require.EqualError(t, err, tt.expectedError.Error()) - } else { - require.NoError(t, err) - require.Equal(t, tt.expectedCoreVer, config.CoreVersion) - } - }) - } -} diff --git a/ingest/ledgerbackend/captive_core_backend.go b/ingest/ledgerbackend/captive_core_backend.go index 879049f873..9d1f7f7fc0 100644 --- a/ingest/ledgerbackend/captive_core_backend.go +++ b/ingest/ledgerbackend/captive_core_backend.go @@ -6,8 +6,6 @@ import ( "fmt" "net/http" "os" - "os/exec" - "strings" "sync" "time" @@ -22,6 +20,8 @@ import ( "github.com/stellar/go/xdr" ) +const minProtocolVersionSupported uint = 21 + // Ensure CaptiveStellarCore implements LedgerBackend var _ LedgerBackend = (*CaptiveStellarCore)(nil) @@ -154,6 +154,12 @@ type CaptiveCoreConfig struct { // of DATABASE parameter in the captive-core-config-path or if absent, the db will default to sqlite // and the db file will be stored at location derived from StoragePath parameter. UseDB bool + + // CoreProtocolVersionFn is a function that returns the protocol version of the stellar-core binary. + CoreProtocolVersionFn CoreProtocolVersionFunc + + // CoreBuildVersionFn is a function that returns the build version of the stellar-core binary. + CoreBuildVersionFn CoreBuildVersionFunc } // NewCaptive returns a new CaptiveStellarCore instance. @@ -168,6 +174,25 @@ func NewCaptive(config CaptiveCoreConfig) (*CaptiveStellarCore, error) { config.Log.SetLevel(logrus.InfoLevel) } + if config.CoreProtocolVersionFn == nil { + config.CoreProtocolVersionFn = CoreProtocolVersion + } + + if config.CoreBuildVersionFn == nil { + config.CoreBuildVersionFn = CoreBuildVersion + } + + protocolVersion, err := config.CoreProtocolVersionFn(config.BinaryPath) + if err != nil { + return nil, fmt.Errorf("error determining stellar-core protocol version: %w", err) + } + + if protocolVersion < minProtocolVersionSupported { + return nil, fmt.Errorf("stellar-core version not supported. Installed stellar-core version is at protocol %d, but minimum "+ + "required version is %d. Please upgrade stellar-core to a version that supports protocol version %d or higher", + protocolVersion, minProtocolVersionSupported, minProtocolVersionSupported) + } + parentCtx := config.Context if parentCtx == nil { parentCtx = context.Background() @@ -250,28 +275,12 @@ func (c *CaptiveStellarCore) coreVersionMetric() float64 { return float64(info.Info.ProtocolVersion) } -// By default, it points to exec.Command, overridden for testing purpose -var execCommand = exec.Command - -// Executes the "stellar-core version" command and parses its output to extract -// the core version -// The output of the "version" command is expected to be a multi-line string where the -// first line is the core version in format "vX.Y.Z-*". func (c *CaptiveStellarCore) setCoreVersion() { - versionCmd := execCommand(c.config.BinaryPath, "version") - versionOutput, err := versionCmd.Output() + var err error + c.captiveCoreVersion, err = c.config.CoreBuildVersionFn(c.config.BinaryPath) if err != nil { - c.config.Log.Errorf("failed to execute stellar-core version command: %s", err) - } - - // Split the output into lines - rows := strings.Split(string(versionOutput), "\n") - if len(rows) == 0 || len(rows[0]) == 0 { - c.config.Log.Error("stellar-core version not found") - return + c.config.Log.Errorf("Failed to set stellar-core version: %s", err) } - - c.captiveCoreVersion = rows[0] c.config.Log.Infof("stellar-core version: %s", c.captiveCoreVersion) } diff --git a/ingest/ledgerbackend/captive_core_backend_test.go b/ingest/ledgerbackend/captive_core_backend_test.go index a367f560f1..76319c2f77 100644 --- a/ingest/ledgerbackend/captive_core_backend_test.go +++ b/ingest/ledgerbackend/captive_core_backend_test.go @@ -153,11 +153,12 @@ func TestCaptiveNew(t *testing.T) { captiveStellarCore, err := NewCaptive( CaptiveCoreConfig{ - BinaryPath: executablePath, - NetworkPassphrase: networkPassphrase, - HistoryArchiveURLs: historyURLs, - StoragePath: storagePath, - UserAgent: "uatest", + BinaryPath: executablePath, + NetworkPassphrase: networkPassphrase, + HistoryArchiveURLs: historyURLs, + StoragePath: storagePath, + UserAgent: "uatest", + CoreProtocolVersionFn: func(string) (uint, error) { return 21, nil }, }, ) @@ -169,6 +170,34 @@ func TestCaptiveNew(t *testing.T) { assert.Equal(t, "uatest", userAgent) } +func TestCaptiveNewUnsupportedProtocolVersion(t *testing.T) { + storagePath, err := os.MkdirTemp("", "captive-core-*") + require.NoError(t, err) + defer os.RemoveAll(storagePath) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + executablePath := "/etc/stellar-core" + networkPassphrase := network.PublicNetworkPassphrase + historyURLs := []string{server.URL} + + _, err = NewCaptive( + CaptiveCoreConfig{ + BinaryPath: executablePath, + NetworkPassphrase: networkPassphrase, + HistoryArchiveURLs: historyURLs, + StoragePath: storagePath, + UserAgent: "uatest", + CoreProtocolVersionFn: func(string) (uint, error) { return 20, nil }, + }, + ) + + assert.EqualError(t, err, "stellar-core version not supported. Installed stellar-core version is at protocol 20, but minimum required version is 21. Please upgrade stellar-core to a version that supports protocol version 21 or higher") +} + func TestCaptivePrepareRange(t *testing.T) { metaChan := make(chan metaResult, 100) @@ -986,11 +1015,12 @@ func TestCaptiveStellarCore_PrepareRangeAfterClose(t *testing.T) { captiveStellarCore, err := NewCaptive( CaptiveCoreConfig{ - BinaryPath: executablePath, - NetworkPassphrase: networkPassphrase, - HistoryArchiveURLs: historyURLs, - Toml: captiveCoreToml, - StoragePath: storagePath, + BinaryPath: executablePath, + NetworkPassphrase: networkPassphrase, + HistoryArchiveURLs: historyURLs, + Toml: captiveCoreToml, + StoragePath: storagePath, + CoreProtocolVersionFn: func(string) (uint, error) { return 21, nil }, }, ) assert.NoError(t, err) diff --git a/ingest/ledgerbackend/stellar_core_version.go b/ingest/ledgerbackend/stellar_core_version.go new file mode 100644 index 0000000000..d5b085d934 --- /dev/null +++ b/ingest/ledgerbackend/stellar_core_version.go @@ -0,0 +1,64 @@ +package ledgerbackend + +import ( + "fmt" + "os/exec" + "regexp" + "strconv" + "strings" +) + +// By default, it points to exec.Command, overridden for testing purpose +var execCommand = exec.Command + +type CoreBuildVersionFunc func(coreBinaryPath string) (string, error) +type CoreProtocolVersionFunc func(coreBinaryPath string) (uint, error) + +// CoreBuildVersion executes the "stellar-core version" command and parses its output to extract +// the core version +// The output of the "version" command is expected to be a multi-line string where the +// first line is the core version in format "vX.Y.Z-*". +func CoreBuildVersion(coreBinaryPath string) (string, error) { + versionCmd := execCommand(coreBinaryPath, "version") + versionOutput, err := versionCmd.Output() + if err != nil { + return "", fmt.Errorf("failed to execute stellar-core version command: %w", err) + } + + // Split the output into lines + rows := strings.Split(string(versionOutput), "\n") + if len(rows) == 0 || len(rows[0]) == 0 { + return "", fmt.Errorf("stellar-core version not found") + } + + return rows[0], nil +} + +// CoreProtocolVersion retrieves the ledger protocol version from the specified stellar-core binary. +// It executes the "stellar-core version" command and parses the output to extract the protocol version. +func CoreProtocolVersion(coreBinaryPath string) (uint, error) { + if coreBinaryPath == "" { + return 0, fmt.Errorf("stellar-core binary path is empty") + } + + versionBytes, err := execCommand(coreBinaryPath, "version").Output() + if err != nil { + return 0, fmt.Errorf("error executing stellar-core version command (%s): %w", coreBinaryPath, err) + } + + versionRows := strings.Split(string(versionBytes), "\n") + re := regexp.MustCompile(`^\s*ledger protocol version: (\d*)`) + var ledgerProtocolStrings []string + for _, line := range versionRows { + ledgerProtocolStrings = re.FindStringSubmatch(line) + if len(ledgerProtocolStrings) == 2 { + val, err := strconv.Atoi(ledgerProtocolStrings[1]) + if err != nil { + return 0, fmt.Errorf("error parsing protocol version from stellar-core output: %w", err) + } + return uint(val), nil + } + } + + return 0, fmt.Errorf("error parsing protocol version from stellar-core output") +} diff --git a/ingest/ledgerbackend/stellar_core_version_test.go b/ingest/ledgerbackend/stellar_core_version_test.go new file mode 100644 index 0000000000..4d58b35875 --- /dev/null +++ b/ingest/ledgerbackend/stellar_core_version_test.go @@ -0,0 +1,123 @@ +package ledgerbackend + +import ( + "fmt" + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/require" +) + +var fakeExecCmdOut = "" + +func fakeExecCommand(command string, args ...string) *exec.Cmd { + cs := append([]string{"-test.run=TestExecCmdHelperProcess", "--", command}, args...) + cmd := exec.Command(os.Args[0], cs...) + cmd.Env = append(os.Environ(), "GO_EXEC_CMD_HELPER_PROCESS=1", "CMD_OUT="+fakeExecCmdOut) + return cmd +} + +func init() { + execCommand = fakeExecCommand +} + +func TestExecCmdHelperProcess(t *testing.T) { + if os.Getenv("GO_EXEC_CMD_HELPER_PROCESS") != "1" { + return + } + fmt.Fprint(os.Stdout, os.Getenv("CMD_OUT")) + os.Exit(0) +} + +func TestGetCoreBuildVersion(t *testing.T) { + tests := []struct { + name string + commandOutput string + expectedError error + expectedCoreVer string + }{ + { + name: "core build version found", + commandOutput: "v20.2.0-2-g6e73c0a88\n" + + "rust version: rustc 1.74.1 (a28077b28 2023-12-04)\n" + + "soroban-env-host: \n" + + " curr:\n" + + " package version: 20.2.0\n" + + " git version: 1bfc0f2a2ee134efc1e1b0d5270281d0cba61c2e\n" + + " ledger protocol version: 20\n" + + " pre-release version: 0\n" + + " rs-stellar-xdr:\n" + + " package version: 20.1.0\n" + + " git version: 8b9d623ef40423a8462442b86997155f2c04d3a1\n" + + " base XDR git version: b96148cd4acc372cc9af17b909ffe4b12c43ecb6\n", + expectedError: nil, + expectedCoreVer: "v20.2.0-2-g6e73c0a88", + }, + { + name: "core build version not found", + commandOutput: "", + expectedError: fmt.Errorf("stellar-core version not found"), + expectedCoreVer: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeExecCmdOut = tt.commandOutput + coreVersion, err := CoreBuildVersion("") + + if tt.expectedError != nil { + require.EqualError(t, err, tt.expectedError.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tt.expectedCoreVer, coreVersion) + } + }) + } +} + +func TestGetCoreProtcolVersion(t *testing.T) { + tests := []struct { + name string + commandOutput string + expectedError error + expectedProtocolVersion uint + }{ + { + name: "core protocol version found", + commandOutput: "v20.2.0-2-g6e73c0a88\n" + + "rust version: rustc 1.74.1 (a28077b28 2023-12-04)\n" + + "soroban-env-host: \n" + + " curr:\n" + + " package version: 20.2.0\n" + + " git version: 1bfc0f2a2ee134efc1e1b0d5270281d0cba61c2e\n" + + " ledger protocol version: 21\n" + + " pre-release version: 0\n" + + " rs-stellar-xdr:\n" + + " package version: 20.1.0\n" + + " git version: 8b9d623ef40423a8462442b86997155f2c04d3a1\n" + + " base XDR git version: b96148cd4acc372cc9af17b909ffe4b12c43ecb6\n", + expectedError: nil, + expectedProtocolVersion: 21, + }, + { + name: "core protocol version not found", + commandOutput: "", + expectedError: fmt.Errorf("error parsing protocol version from stellar-core output"), + expectedProtocolVersion: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeExecCmdOut = tt.commandOutput + coreVersion, err := CoreProtocolVersion("/usr/bin/stellar-core") + + if tt.expectedError != nil { + require.EqualError(t, err, tt.expectedError.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tt.expectedProtocolVersion, coreVersion) + } + }) + } +} diff --git a/ingest/ledgerbackend/toml.go b/ingest/ledgerbackend/toml.go index a4f61a454d..b8b6634a78 100644 --- a/ingest/ledgerbackend/toml.go +++ b/ingest/ledgerbackend/toml.go @@ -5,9 +5,7 @@ import ( _ "embed" "fmt" "os" - "os/exec" "regexp" - "strconv" "strings" "github.com/stellar/go/support/errors" @@ -346,8 +344,6 @@ type CaptiveCoreTomlParams struct { EnforceSorobanDiagnosticEvents bool // Enfore EnableSorobanTransactionMetaExtV1 when not disabled explicitly EnforceSorobanTransactionMetaExtV1 bool - // used for testing - checkCoreVersion func(coreBinaryPath string) coreVersion } // NewCaptiveCoreTomlFromFile constructs a new CaptiveCoreToml instance by merging configuration @@ -445,104 +441,13 @@ func (c *CaptiveCoreToml) CatchupToml() (*CaptiveCoreToml, error) { return offline, nil } -// coreVersion helper struct identify a core version and provides the -// utilities to compare the version ( i.e. minor + major pair ) to a predefined -// version. -type coreVersion struct { - major int - minor int - ledgerProtocolVersion int -} - -// IsEqualOrAbove compares the core version to a version specific. If unable -// to make the decision, the result is always "false", leaning toward the -// common denominator. -func (c *coreVersion) IsEqualOrAbove(major, minor int) bool { - if c.major == 0 && c.minor == 0 { - return false - } - return (c.major == major && c.minor >= minor) || (c.major > major) -} - -// IsEqualOrAbove compares the core version to a version specific. If unable -// to make the decision, the result is always "false", leaning toward the -// common denominator. -func (c *coreVersion) IsProtocolVersionEqualOrAbove(protocolVer int) bool { - if c.ledgerProtocolVersion == 0 { - return false - } - return c.ledgerProtocolVersion >= protocolVer -} - -func checkCoreVersion(coreBinaryPath string) coreVersion { - if coreBinaryPath == "" { - return coreVersion{} - } - - versionBytes, err := exec.Command(coreBinaryPath, "version").Output() - if err != nil { - return coreVersion{} - } - - // starting soroban, we want to use only the first row for the version. - versionRows := strings.Split(string(versionBytes), "\n") - versionRaw := versionRows[0] - - var version [2]int - - re := regexp.MustCompile(`\D*(\d*)\.(\d*).*`) - versionStr := re.FindStringSubmatch(versionRaw) - if len(versionStr) == 3 { - for i := 1; i < len(versionStr); i++ { - val, err := strconv.Atoi((versionStr[i])) - if err != nil { - break - } - version[i-1] = val - } - } - - re = regexp.MustCompile(`^\s*ledger protocol version: (\d*)`) - var ledgerProtocol int - var ledgerProtocolStrings []string - for _, line := range versionRows { - ledgerProtocolStrings = re.FindStringSubmatch(line) - if len(ledgerProtocolStrings) > 0 { - break - } - } - if len(ledgerProtocolStrings) == 2 { - if val, err := strconv.Atoi(ledgerProtocolStrings[1]); err == nil { - ledgerProtocol = val - } - } - - return coreVersion{ - major: version[0], - minor: version[1], - ledgerProtocolVersion: ledgerProtocol, - } -} - -const MinimalBucketListDBCoreSupportVersionMajor = 19 -const MinimalBucketListDBCoreSupportVersionMinor = 6 -const MinimalSorobanProtocolSupport = 20 - func (c *CaptiveCoreToml) setDefaults(params CaptiveCoreTomlParams) { if params.UseDB && !c.tree.Has("DATABASE") { c.Database = "sqlite3://stellar.db" } - checkCoreVersionF := params.checkCoreVersion - if checkCoreVersionF == nil { - checkCoreVersionF = checkCoreVersion - } - currentCoreVersion := checkCoreVersionF(params.CoreBinaryPath) if def := c.tree.Has("EXPERIMENTAL_BUCKETLIST_DB"); !def && params.UseDB { - // Supports version 19.6 and above - if currentCoreVersion.IsEqualOrAbove(MinimalBucketListDBCoreSupportVersionMajor, MinimalBucketListDBCoreSupportVersionMinor) { - c.UseBucketListDB = true - } + c.UseBucketListDB = true } if c.UseBucketListDB && !c.tree.Has("EXPERIMENTAL_BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT") { @@ -584,14 +489,10 @@ func (c *CaptiveCoreToml) setDefaults(params CaptiveCoreTomlParams) { } if params.EnforceSorobanDiagnosticEvents { - if currentCoreVersion.IsEqualOrAbove(20, 0) { - enforceOption(&c.EnableSorobanDiagnosticEvents) - } - if currentCoreVersion.IsEqualOrAbove(20, 1) { - enforceOption(&c.EnableDiagnosticsForTxSubmission) - } + enforceOption(&c.EnableSorobanDiagnosticEvents) + enforceOption(&c.EnableDiagnosticsForTxSubmission) } - if params.EnforceSorobanTransactionMetaExtV1 && currentCoreVersion.IsEqualOrAbove(20, 4) { + if params.EnforceSorobanTransactionMetaExtV1 { enforceOption(&c.EnableEmitSorobanTransactionMetaExtV1) } } diff --git a/ingest/ledgerbackend/toml_test.go b/ingest/ledgerbackend/toml_test.go index fecf50c8d9..39b8473d4a 100644 --- a/ingest/ledgerbackend/toml_test.go +++ b/ingest/ledgerbackend/toml_test.go @@ -366,13 +366,6 @@ func TestGenerateConfig(t *testing.T) { UseDB: testCase.useDB, EnforceSorobanDiagnosticEvents: testCase.enforceSorobanDiagnosticEvents, EnforceSorobanTransactionMetaExtV1: testCase.enforceEmitMetaV1, - checkCoreVersion: func(coreBinaryPath string) coreVersion { - return coreVersion{ - major: 21, - minor: 0, - ledgerProtocolVersion: 21, - } - }, } if testCase.appendPath != "" { captiveCoreToml, err = NewCaptiveCoreTomlFromFile(testCase.appendPath, params) @@ -497,13 +490,3 @@ func TestNonDBConfigDoesNotUpdateDatabase(t *testing.T) { require.NoError(t, toml.unmarshal(configBytes, true)) assert.Equal(t, toml.Database, "") } - -func TestCheckCoreVersion(t *testing.T) { - coreBin := os.Getenv("HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") - if coreBin == "" { - t.SkipNow() - return - } - version := checkCoreVersion(coreBin) - require.True(t, version.IsEqualOrAbove(20, 0)) -} diff --git a/services/horizon/docker/captive-core-classic-integration-tests.cfg b/services/horizon/docker/captive-core-classic-integration-tests.cfg deleted file mode 100644 index 2f95a0ee54..0000000000 --- a/services/horizon/docker/captive-core-classic-integration-tests.cfg +++ /dev/null @@ -1,15 +0,0 @@ -PEER_PORT=11725 -ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true - -UNSAFE_QUORUM=true -FAILURE_SAFETY=0 - -EXPERIMENTAL_BUCKETLIST_DB=true - -[[VALIDATORS]] -NAME="local_core" -HOME_DOMAIN="core.local" -# From "SACJC372QBSSKJYTV5A7LWT4NXWHTQO6GHG4QDAVC2XDPX6CNNXFZ4JK" -PUBLIC_KEY="GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS" -ADDRESS="localhost" -QUALITY="MEDIUM" diff --git a/services/horizon/docker/captive-core-reingest-range-classic-integration-tests.cfg b/services/horizon/docker/captive-core-reingest-range-classic-integration-tests.cfg deleted file mode 100644 index 735f58b739..0000000000 --- a/services/horizon/docker/captive-core-reingest-range-classic-integration-tests.cfg +++ /dev/null @@ -1,11 +0,0 @@ -ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true - -EXPERIMENTAL_BUCKETLIST_DB=true - -[[VALIDATORS]] -NAME="local_core" -HOME_DOMAIN="core.local" -# From "SACJC372QBSSKJYTV5A7LWT4NXWHTQO6GHG4QDAVC2XDPX6CNNXFZ4JK" -PUBLIC_KEY="GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS" -ADDRESS="localhost" -QUALITY="MEDIUM" diff --git a/services/horizon/internal/flags_test.go b/services/horizon/internal/flags_test.go index 65d1da524c..76ec1ffd8d 100644 --- a/services/horizon/internal/flags_test.go +++ b/services/horizon/internal/flags_test.go @@ -307,7 +307,7 @@ func TestEnvironmentVariables(t *testing.T) { assert.Equal(t, config.AdminPort, uint(6060)) assert.Equal(t, config.Port, uint(8001)) assert.Equal(t, config.CaptiveCoreBinaryPath, os.Getenv("HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_BIN")) - assert.Equal(t, config.CaptiveCoreConfigPath, "../docker/captive-core-classic-integration-tests.cfg") + assert.Equal(t, config.CaptiveCoreConfigPath, "../docker/captive-core-integration-tests.cfg") assert.Equal(t, config.CaptiveCoreConfigUseDB, true) } @@ -324,7 +324,7 @@ func horizonEnvVars() map[string]string { "ADMIN_PORT": "6060", "PORT": "8001", "CAPTIVE_CORE_BINARY_PATH": os.Getenv("HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_BIN"), - "CAPTIVE_CORE_CONFIG_PATH": "../docker/captive-core-classic-integration-tests.cfg", + "CAPTIVE_CORE_CONFIG_PATH": "../docker/captive-core-integration-tests.cfg", "CAPTIVE_CORE_USE_DB": "true", } } diff --git a/services/horizon/internal/ingest/db_integration_test.go b/services/horizon/internal/ingest/db_integration_test.go index 606cd9fb2b..0ac6e9d796 100644 --- a/services/horizon/internal/ingest/db_integration_test.go +++ b/services/horizon/internal/ingest/db_integration_test.go @@ -100,12 +100,14 @@ func (s *DBTestSuite) SetupTest() { s.checkpointHash = xdr.Hash{1, 2, 3} s.ledgerBackend = &ledgerbackend.MockDatabaseBackend{} s.historyAdapter = &mockHistoryArchiveAdapter{} + var err error sIface, err := NewSystem(Config{ HistorySession: s.tt.HorizonSession(), HistoryArchiveURLs: []string{"http://ignore.test"}, DisableStateVerification: false, CheckpointFrequency: 64, + CoreProtocolVersionFn: func(string) (uint, error) { return 21, nil }, }) s.Assert().NoError(err) s.system = sIface.(*system) diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 0769faee4f..98dcad34f4 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -109,6 +109,9 @@ type Config struct { MaxLedgerPerFlush uint32 SkipTxmeta bool + + CoreProtocolVersionFn ledgerbackend.CoreProtocolVersionFunc + CoreBuildVersionFn ledgerbackend.CoreBuildVersionFunc } const ( @@ -259,17 +262,19 @@ func NewSystem(config Config) (System, error) { logger := log.WithField("subservice", "stellar-core") ledgerBackend, err := ledgerbackend.NewCaptive( ledgerbackend.CaptiveCoreConfig{ - BinaryPath: config.CaptiveCoreBinaryPath, - StoragePath: config.CaptiveCoreStoragePath, - UseDB: config.CaptiveCoreConfigUseDB, - Toml: config.CaptiveCoreToml, - NetworkPassphrase: config.NetworkPassphrase, - HistoryArchiveURLs: config.HistoryArchiveURLs, - CheckpointFrequency: config.CheckpointFrequency, - LedgerHashStore: ledgerbackend.NewHorizonDBLedgerHashStore(config.HistorySession), - Log: logger, - Context: ctx, - UserAgent: fmt.Sprintf("captivecore horizon/%s golang/%s", apkg.Version(), runtime.Version()), + BinaryPath: config.CaptiveCoreBinaryPath, + StoragePath: config.CaptiveCoreStoragePath, + UseDB: config.CaptiveCoreConfigUseDB, + Toml: config.CaptiveCoreToml, + NetworkPassphrase: config.NetworkPassphrase, + HistoryArchiveURLs: config.HistoryArchiveURLs, + CheckpointFrequency: config.CheckpointFrequency, + LedgerHashStore: ledgerbackend.NewHorizonDBLedgerHashStore(config.HistorySession), + Log: logger, + Context: ctx, + UserAgent: fmt.Sprintf("captivecore horizon/%s golang/%s", apkg.Version(), runtime.Version()), + CoreProtocolVersionFn: config.CoreProtocolVersionFn, + CoreBuildVersionFn: config.CoreBuildVersionFn, }, ) if err != nil { diff --git a/services/horizon/internal/ingest/main_test.go b/services/horizon/internal/ingest/main_test.go index 3c7c587aa2..40a3d48cce 100644 --- a/services/horizon/internal/ingest/main_test.go +++ b/services/horizon/internal/ingest/main_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "database/sql" + "reflect" "sync" "testing" "time" @@ -94,20 +95,39 @@ func TestNewSystem(t *testing.T) { DisableStateVerification: true, HistoryArchiveURLs: []string{"https://history.stellar.org/prd/core-live/core_live_001"}, CheckpointFrequency: 64, + CoreProtocolVersionFn: func(string) (uint, error) { return 21, nil }, } sIface, err := NewSystem(config) assert.NoError(t, err) system := sIface.(*system) - assert.Equal(t, config, system.config) + CompareConfigs(t, config, system.config) assert.Equal(t, config.DisableStateVerification, system.disableStateVerification) - assert.Equal(t, config, system.runner.(*ProcessorRunner).config) + CompareConfigs(t, config, system.runner.(*ProcessorRunner).config) assert.Equal(t, system.ctx, system.runner.(*ProcessorRunner).ctx) assert.Equal(t, system.maxLedgerPerFlush, MaxLedgersPerFlush) } +// Custom comparator function.This function is needed because structs in Go that contain function fields +// cannot be directly compared using assert.Equal, so here we compare each individual field, skipping the function fields. +func CompareConfigs(t *testing.T, expected, actual Config) bool { + fields := reflect.TypeOf(expected) + for i := 0; i < fields.NumField(); i++ { + field := fields.Field(i) + if field.Name == "CoreProtocolVersionFn" || field.Name == "CoreBuildVersionFn" { + continue + } + expectedValue := reflect.ValueOf(expected).Field(i).Interface() + actualValue := reflect.ValueOf(actual).Field(i).Interface() + if !assert.Equal(t, expectedValue, actualValue, field.Name) { + return false + } + } + return true +} + func TestStateMachineRunReturnsUnexpectedTransaction(t *testing.T) { historyQ := &mockDBQ{} system := &system{ diff --git a/services/horizon/internal/integration/db_test.go b/services/horizon/internal/integration/db_test.go index 98d584c8e6..331f99ee27 100644 --- a/services/horizon/internal/integration/db_test.go +++ b/services/horizon/internal/integration/db_test.go @@ -12,7 +12,6 @@ import ( "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/historyarchive" - "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/keypair" hProtocol "github.com/stellar/go/protocols/horizon" horizoncmd "github.com/stellar/go/services/horizon/cmd" @@ -535,7 +534,7 @@ func TestReingestDB(t *testing.T) { horizonConfig.CaptiveCoreConfigPath = filepath.Join( filepath.Dir(horizonConfig.CaptiveCoreConfigPath), - getCoreConfigFile(itest), + "captive-core-reingest-range-integration-tests.cfg", ) horizoncmd.RootCmd.SetArgs(command(t, horizonConfig, "db", @@ -703,14 +702,6 @@ func TestReingestDBWithFilterRules(t *testing.T) { }, 30*time.Second, time.Second) } -func getCoreConfigFile(itest *integration.Test) string { - coreConfigFile := "captive-core-reingest-range-classic-integration-tests.cfg" - if itest.Config().ProtocolVersion >= ledgerbackend.MinimalSorobanProtocolSupport { - coreConfigFile = "captive-core-reingest-range-integration-tests.cfg" - } - return coreConfigFile -} - func command(t *testing.T, horizonConfig horizon.Config, args ...string) []string { return append([]string{ "--stellar-core-url", @@ -848,7 +839,7 @@ func TestFillGaps(t *testing.T) { horizonConfig.CaptiveCoreConfigPath = filepath.Join( filepath.Dir(horizonConfig.CaptiveCoreConfigPath), - getCoreConfigFile(itest), + "captive-core-reingest-range-integration-tests.cfg", ) horizoncmd.RootCmd.SetArgs(command(t, horizonConfig, "db", "fill-gaps", "--parallel-workers=1")) tt.NoError(horizoncmd.RootCmd.Execute()) diff --git a/services/horizon/internal/test/integration/integration.go b/services/horizon/internal/test/integration/integration.go index e12f77f693..0402301a44 100644 --- a/services/horizon/internal/test/integration/integration.go +++ b/services/horizon/internal/test/integration/integration.go @@ -26,7 +26,6 @@ import ( sdk "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/clients/stellarcore" - "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/keypair" proto "github.com/stellar/go/protocols/horizon" horizon "github.com/stellar/go/services/horizon/internal" @@ -184,11 +183,7 @@ func NewTest(t *testing.T, config Config) *Test { func (i *Test) configureCaptiveCore() { composePath := findDockerComposePath() i.coreConfig.binaryPath = os.Getenv("HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") - coreConfigFile := "captive-core-classic-integration-tests.cfg" - if i.config.ProtocolVersion >= ledgerbackend.MinimalSorobanProtocolSupport { - coreConfigFile = "captive-core-integration-tests.cfg" - } - i.coreConfig.configPath = filepath.Join(composePath, coreConfigFile) + i.coreConfig.configPath = filepath.Join(composePath, "captive-core-integration-tests.cfg") i.coreConfig.storagePath = i.CurrentTest().TempDir() i.coreConfig.useDB = true @@ -254,13 +249,6 @@ func (i *Test) runComposeCommand(args ...string) { ) } - if i.config.ProtocolVersion < ledgerbackend.MinimalSorobanProtocolSupport { - cmd.Env = append( - cmd.Environ(), - "CORE_CONFIG_FILE=stellar-core-classic-integration-tests.cfg", - ) - } - i.t.Log("Running", cmd.Args) out, innerErr := cmd.Output() if len(out) > 0 { From 90c18e2719795c5d91ebea982c0080326c2c5a42 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Sun, 23 Jun 2024 22:27:54 -0700 Subject: [PATCH 193/234] ingest/ledgerbackend: Update captive-core config with new BucketlistDB parameters (#5333) * Update bucketlistdb config parameters * Remove EXPERIMENTAL_BUCKETLIST_DB flag * Add DEPRECATED_SQL_LEDGER_STATE param. Add unit tests for DEPRECATED_SQL_LEDGER_STATE. * Update changelog * Update Horizon changelog --- ingest/CHANGELOG.md | 7 +- .../configs/captive-core-pubnet.cfg | 2 - .../configs/captive-core-testnet.cfg | 2 - .../testdata/expected-bucketlistdb-core.cfg | 22 ++++++ .../expected-default-bucketlistdb-core.cfg | 21 ++++++ .../testdata/expected-in-mem-core.cfg | 19 +++++ .../testdata/expected-offline-core.cfg | 4 +- ...offline-enforce-diag-events-and-metav1.cfg | 1 + ...ine-enforce-disabled-diagnostic-events.cfg | 1 + .../expected-offline-with-appendix-core.cfg | 1 + .../expected-offline-with-extra-fields.cfg | 1 + .../expected-offline-with-no-peer-port.cfg | 1 + .../testdata/expected-online-core.cfg | 1 + ...with-appendix-minimum-persistent-entry.cfg | 1 + ...e-with-no-http-port-diag-events-metav1.cfg | 1 + .../expected-online-with-no-http-port.cfg | 1 + .../expected-online-with-no-peer-port.cfg | 1 + .../testdata/sample-appendix-bucketlistdb.cfg | 12 ++++ .../testdata/sample-appendix-in-memory.cfg | 11 +++ .../testdata/sample-appendix-on-disk.cfg | 11 +++ ingest/ledgerbackend/toml.go | 35 +++++---- ingest/ledgerbackend/toml_test.go | 71 +++++++++++++++---- services/horizon/CHANGELOG.md | 6 +- .../docker/captive-core-integration-tests.cfg | 2 - ...ive-core-integration-tests.soroban-rpc.cfg | 3 +- ...-core-reingest-range-integration-tests.cfg | 1 - .../docker/captive-core-standalone.cfg | 2 - ...stellar-core-classic-integration-tests.cfg | 2 +- .../docker/stellar-core-integration-tests.cfg | 2 +- .../horizon/docker/stellar-core-pubnet.cfg | 2 - .../docker/stellar-core-standalone.cfg | 1 - .../horizon/docker/stellar-core-testnet.cfg | 2 - .../verify-range/captive-core-pubnet.cfg | 2 - .../docker/verify-range/stellar-core.cfg | 2 - 34 files changed, 199 insertions(+), 55 deletions(-) create mode 100644 ingest/ledgerbackend/testdata/expected-bucketlistdb-core.cfg create mode 100644 ingest/ledgerbackend/testdata/expected-default-bucketlistdb-core.cfg create mode 100644 ingest/ledgerbackend/testdata/expected-in-mem-core.cfg create mode 100644 ingest/ledgerbackend/testdata/sample-appendix-bucketlistdb.cfg create mode 100644 ingest/ledgerbackend/testdata/sample-appendix-in-memory.cfg create mode 100644 ingest/ledgerbackend/testdata/sample-appendix-on-disk.cfg diff --git a/ingest/CHANGELOG.md b/ingest/CHANGELOG.md index 1aab4ae542..ed168de74e 100644 --- a/ingest/CHANGELOG.md +++ b/ingest/CHANGELOG.md @@ -2,8 +2,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). - -## Unreleased +### Stellar Core Protocol 21 Configuration Update: +* BucketlistDB is now the default database for stellar-core, replacing the experimental option. As a result, the `EXPERIMENTAL_BUCKETLIST_DB` configuration parameter has been deprecated. +* A new mandatory parameter, `DEPRECATED_SQL_LEDGER_STATE`, has been added with a default value of false which equivalent to `EXPERIMENTAL_BUCKETLIST_DB` being set to true. +* The parameter `EXPERIMENTAL_BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT` has been renamed to `BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT`. +* The parameter `EXPERIMENTAL_BUCKETLIST_DB_INDEX_CUTOFF` has been renamed to `BUCKETLIST_DB_INDEX_CUTOFF`. ### New Features * Support for Soroban and Protocol 20! diff --git a/ingest/ledgerbackend/configs/captive-core-pubnet.cfg b/ingest/ledgerbackend/configs/captive-core-pubnet.cfg index 6d23046f56..5af59efaf9 100644 --- a/ingest/ledgerbackend/configs/captive-core-pubnet.cfg +++ b/ingest/ledgerbackend/configs/captive-core-pubnet.cfg @@ -5,8 +5,6 @@ FAILURE_SAFETY=1 HTTP_PORT=11626 PEER_PORT=11725 -EXPERIMENTAL_BUCKETLIST_DB=true - [[HOME_DOMAINS]] HOME_DOMAIN="publicnode.org" QUALITY="HIGH" diff --git a/ingest/ledgerbackend/configs/captive-core-testnet.cfg b/ingest/ledgerbackend/configs/captive-core-testnet.cfg index 3f0dec5095..9abeecc8f5 100644 --- a/ingest/ledgerbackend/configs/captive-core-testnet.cfg +++ b/ingest/ledgerbackend/configs/captive-core-testnet.cfg @@ -2,8 +2,6 @@ NETWORK_PASSPHRASE="Test SDF Network ; September 2015" UNSAFE_QUORUM=true FAILURE_SAFETY=1 -EXPERIMENTAL_BUCKETLIST_DB=true - [[HOME_DOMAINS]] HOME_DOMAIN="testnet.stellar.org" QUALITY="HIGH" diff --git a/ingest/ledgerbackend/testdata/expected-bucketlistdb-core.cfg b/ingest/ledgerbackend/testdata/expected-bucketlistdb-core.cfg new file mode 100644 index 0000000000..9040d6dc57 --- /dev/null +++ b/ingest/ledgerbackend/testdata/expected-bucketlistdb-core.cfg @@ -0,0 +1,22 @@ +# Generated file, do not edit +BUCKETLIST_DB_INDEX_CUTOFF = 20 +BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT = 15 +DATABASE = "sqlite3://stellar.db" +DEPRECATED_SQL_LEDGER_STATE = false +FAILURE_SAFETY = -1 +HTTP_PORT = 11626 +LOG_FILE_PATH = "" +NETWORK_PASSPHRASE = "Public Global Stellar Network ; September 2015" + +[[HOME_DOMAINS]] + HOME_DOMAIN = "testnet.stellar.org" + QUALITY = "MEDIUM" + +[[VALIDATORS]] + ADDRESS = "localhost:123" + HOME_DOMAIN = "testnet.stellar.org" + NAME = "sdf_testnet_1" + PUBLIC_KEY = "GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" + +[HISTORY.h0] + get = "curl -sf http://localhost:1170/{0} -o {1}" diff --git a/ingest/ledgerbackend/testdata/expected-default-bucketlistdb-core.cfg b/ingest/ledgerbackend/testdata/expected-default-bucketlistdb-core.cfg new file mode 100644 index 0000000000..12fe5e36a5 --- /dev/null +++ b/ingest/ledgerbackend/testdata/expected-default-bucketlistdb-core.cfg @@ -0,0 +1,21 @@ +# Generated file, do not edit +BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT = 12 +DATABASE = "sqlite3://stellar.db" +DEPRECATED_SQL_LEDGER_STATE = false +FAILURE_SAFETY = -1 +HTTP_PORT = 11626 +LOG_FILE_PATH = "" +NETWORK_PASSPHRASE = "Public Global Stellar Network ; September 2015" + +[[HOME_DOMAINS]] + HOME_DOMAIN = "testnet.stellar.org" + QUALITY = "MEDIUM" + +[[VALIDATORS]] + ADDRESS = "localhost:123" + HOME_DOMAIN = "testnet.stellar.org" + NAME = "sdf_testnet_1" + PUBLIC_KEY = "GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" + +[HISTORY.h0] + get = "curl -sf http://localhost:1170/{0} -o {1}" diff --git a/ingest/ledgerbackend/testdata/expected-in-mem-core.cfg b/ingest/ledgerbackend/testdata/expected-in-mem-core.cfg new file mode 100644 index 0000000000..965fa5aaa6 --- /dev/null +++ b/ingest/ledgerbackend/testdata/expected-in-mem-core.cfg @@ -0,0 +1,19 @@ +# Generated file, do not edit +DEPRECATED_SQL_LEDGER_STATE = true +FAILURE_SAFETY = -1 +HTTP_PORT = 11626 +LOG_FILE_PATH = "" +NETWORK_PASSPHRASE = "Public Global Stellar Network ; September 2015" + +[[HOME_DOMAINS]] + HOME_DOMAIN = "testnet.stellar.org" + QUALITY = "MEDIUM" + +[[VALIDATORS]] + ADDRESS = "localhost:123" + HOME_DOMAIN = "testnet.stellar.org" + NAME = "sdf_testnet_1" + PUBLIC_KEY = "GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" + +[HISTORY.h0] + get = "curl -sf http://localhost:1170/{0} -o {1}" diff --git a/ingest/ledgerbackend/testdata/expected-offline-core.cfg b/ingest/ledgerbackend/testdata/expected-offline-core.cfg index 53838f6165..ec37e504fc 100644 --- a/ingest/ledgerbackend/testdata/expected-offline-core.cfg +++ b/ingest/ledgerbackend/testdata/expected-offline-core.cfg @@ -1,7 +1,7 @@ # Generated file, do not edit +BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT = 12 DATABASE = "sqlite3://stellar.db" -EXPERIMENTAL_BUCKETLIST_DB = true -EXPERIMENTAL_BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT = 12 +DEPRECATED_SQL_LEDGER_STATE = false FAILURE_SAFETY = 0 HTTP_PORT = 0 LOG_FILE_PATH = "" diff --git a/ingest/ledgerbackend/testdata/expected-offline-enforce-diag-events-and-metav1.cfg b/ingest/ledgerbackend/testdata/expected-offline-enforce-diag-events-and-metav1.cfg index 8c66b4a5ad..fa25d69b98 100644 --- a/ingest/ledgerbackend/testdata/expected-offline-enforce-diag-events-and-metav1.cfg +++ b/ingest/ledgerbackend/testdata/expected-offline-enforce-diag-events-and-metav1.cfg @@ -1,4 +1,5 @@ # Generated file, do not edit +DEPRECATED_SQL_LEDGER_STATE = true EMIT_SOROBAN_TRANSACTION_META_EXT_V1 = true ENABLE_DIAGNOSTICS_FOR_TX_SUBMISSION = true ENABLE_SOROBAN_DIAGNOSTIC_EVENTS = true diff --git a/ingest/ledgerbackend/testdata/expected-offline-enforce-disabled-diagnostic-events.cfg b/ingest/ledgerbackend/testdata/expected-offline-enforce-disabled-diagnostic-events.cfg index df307a3a00..4a971a19d2 100644 --- a/ingest/ledgerbackend/testdata/expected-offline-enforce-disabled-diagnostic-events.cfg +++ b/ingest/ledgerbackend/testdata/expected-offline-enforce-disabled-diagnostic-events.cfg @@ -1,4 +1,5 @@ # Generated file, do not edit +DEPRECATED_SQL_LEDGER_STATE = true FAILURE_SAFETY = 0 HTTP_PORT = 0 LOG_FILE_PATH = "" diff --git a/ingest/ledgerbackend/testdata/expected-offline-with-appendix-core.cfg b/ingest/ledgerbackend/testdata/expected-offline-with-appendix-core.cfg index 124abc435b..30159c150a 100644 --- a/ingest/ledgerbackend/testdata/expected-offline-with-appendix-core.cfg +++ b/ingest/ledgerbackend/testdata/expected-offline-with-appendix-core.cfg @@ -1,4 +1,5 @@ # Generated file, do not edit +DEPRECATED_SQL_LEDGER_STATE = true FAILURE_SAFETY = 0 HTTP_PORT = 0 LOG_FILE_PATH = "" diff --git a/ingest/ledgerbackend/testdata/expected-offline-with-extra-fields.cfg b/ingest/ledgerbackend/testdata/expected-offline-with-extra-fields.cfg index d7b41a0b81..eab3b67bf8 100644 --- a/ingest/ledgerbackend/testdata/expected-offline-with-extra-fields.cfg +++ b/ingest/ledgerbackend/testdata/expected-offline-with-extra-fields.cfg @@ -1,4 +1,5 @@ # Generated file, do not edit +DEPRECATED_SQL_LEDGER_STATE = true FAILURE_SAFETY = 0 HTTP_PORT = 0 LOG_FILE_PATH = "" diff --git a/ingest/ledgerbackend/testdata/expected-offline-with-no-peer-port.cfg b/ingest/ledgerbackend/testdata/expected-offline-with-no-peer-port.cfg index 9eca1ccad1..9c5fd26769 100644 --- a/ingest/ledgerbackend/testdata/expected-offline-with-no-peer-port.cfg +++ b/ingest/ledgerbackend/testdata/expected-offline-with-no-peer-port.cfg @@ -1,4 +1,5 @@ # Generated file, do not edit +DEPRECATED_SQL_LEDGER_STATE = true FAILURE_SAFETY = 0 HTTP_PORT = 0 LOG_FILE_PATH = "/var/stellar-core/test.log" diff --git a/ingest/ledgerbackend/testdata/expected-online-core.cfg b/ingest/ledgerbackend/testdata/expected-online-core.cfg index 57a5e7ff2c..b8aeff746c 100644 --- a/ingest/ledgerbackend/testdata/expected-online-core.cfg +++ b/ingest/ledgerbackend/testdata/expected-online-core.cfg @@ -1,4 +1,5 @@ # Generated file, do not edit +DEPRECATED_SQL_LEDGER_STATE = true FAILURE_SAFETY = -1 HTTP_PORT = 6789 LOG_FILE_PATH = "" diff --git a/ingest/ledgerbackend/testdata/expected-online-with-appendix-minimum-persistent-entry.cfg b/ingest/ledgerbackend/testdata/expected-online-with-appendix-minimum-persistent-entry.cfg index 278c681595..8ecc0fe958 100644 --- a/ingest/ledgerbackend/testdata/expected-online-with-appendix-minimum-persistent-entry.cfg +++ b/ingest/ledgerbackend/testdata/expected-online-with-appendix-minimum-persistent-entry.cfg @@ -1,4 +1,5 @@ # Generated file, do not edit +DEPRECATED_SQL_LEDGER_STATE = true FAILURE_SAFETY = -1 HTTP_PORT = 11626 LOG_FILE_PATH = "" diff --git a/ingest/ledgerbackend/testdata/expected-online-with-no-http-port-diag-events-metav1.cfg b/ingest/ledgerbackend/testdata/expected-online-with-no-http-port-diag-events-metav1.cfg index f2227376a9..3960c1b546 100644 --- a/ingest/ledgerbackend/testdata/expected-online-with-no-http-port-diag-events-metav1.cfg +++ b/ingest/ledgerbackend/testdata/expected-online-with-no-http-port-diag-events-metav1.cfg @@ -1,4 +1,5 @@ # Generated file, do not edit +DEPRECATED_SQL_LEDGER_STATE = true EMIT_SOROBAN_TRANSACTION_META_EXT_V1 = true ENABLE_DIAGNOSTICS_FOR_TX_SUBMISSION = true ENABLE_SOROBAN_DIAGNOSTIC_EVENTS = true diff --git a/ingest/ledgerbackend/testdata/expected-online-with-no-http-port.cfg b/ingest/ledgerbackend/testdata/expected-online-with-no-http-port.cfg index 89e1762757..6b3830e6ff 100644 --- a/ingest/ledgerbackend/testdata/expected-online-with-no-http-port.cfg +++ b/ingest/ledgerbackend/testdata/expected-online-with-no-http-port.cfg @@ -1,4 +1,5 @@ # Generated file, do not edit +DEPRECATED_SQL_LEDGER_STATE = true FAILURE_SAFETY = -1 HTTP_PORT = 11626 LOG_FILE_PATH = "" diff --git a/ingest/ledgerbackend/testdata/expected-online-with-no-peer-port.cfg b/ingest/ledgerbackend/testdata/expected-online-with-no-peer-port.cfg index 1b65c5f318..93bb61b8be 100644 --- a/ingest/ledgerbackend/testdata/expected-online-with-no-peer-port.cfg +++ b/ingest/ledgerbackend/testdata/expected-online-with-no-peer-port.cfg @@ -1,4 +1,5 @@ # Generated file, do not edit +DEPRECATED_SQL_LEDGER_STATE = true FAILURE_SAFETY = -1 HTTP_PORT = 6789 LOG_FILE_PATH = "/var/stellar-core/test.log" diff --git a/ingest/ledgerbackend/testdata/sample-appendix-bucketlistdb.cfg b/ingest/ledgerbackend/testdata/sample-appendix-bucketlistdb.cfg new file mode 100644 index 0000000000..e01a92cc61 --- /dev/null +++ b/ingest/ledgerbackend/testdata/sample-appendix-bucketlistdb.cfg @@ -0,0 +1,12 @@ +BUCKETLIST_DB_INDEX_CUTOFF = 20 +BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT = 15 + +[[HOME_DOMAINS]] +HOME_DOMAIN="testnet.stellar.org" +QUALITY="MEDIUM" + +[[VALIDATORS]] +NAME="sdf_testnet_1" +HOME_DOMAIN="testnet.stellar.org" +PUBLIC_KEY="GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" +ADDRESS="localhost:123" diff --git a/ingest/ledgerbackend/testdata/sample-appendix-in-memory.cfg b/ingest/ledgerbackend/testdata/sample-appendix-in-memory.cfg new file mode 100644 index 0000000000..958019998c --- /dev/null +++ b/ingest/ledgerbackend/testdata/sample-appendix-in-memory.cfg @@ -0,0 +1,11 @@ +DEPRECATED_SQL_LEDGER_STATE = false + +[[HOME_DOMAINS]] +HOME_DOMAIN="testnet.stellar.org" +QUALITY="MEDIUM" + +[[VALIDATORS]] +NAME="sdf_testnet_1" +HOME_DOMAIN="testnet.stellar.org" +PUBLIC_KEY="GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" +ADDRESS="localhost:123" diff --git a/ingest/ledgerbackend/testdata/sample-appendix-on-disk.cfg b/ingest/ledgerbackend/testdata/sample-appendix-on-disk.cfg new file mode 100644 index 0000000000..3ca36b223b --- /dev/null +++ b/ingest/ledgerbackend/testdata/sample-appendix-on-disk.cfg @@ -0,0 +1,11 @@ +DEPRECATED_SQL_LEDGER_STATE = true + +[[HOME_DOMAINS]] +HOME_DOMAIN="testnet.stellar.org" +QUALITY="MEDIUM" + +[[VALIDATORS]] +NAME="sdf_testnet_1" +HOME_DOMAIN="testnet.stellar.org" +PUBLIC_KEY="GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" +ADDRESS="localhost:123" diff --git a/ingest/ledgerbackend/toml.go b/ingest/ledgerbackend/toml.go index b8b6634a78..b7f41b03a7 100644 --- a/ingest/ledgerbackend/toml.go +++ b/ingest/ledgerbackend/toml.go @@ -21,6 +21,8 @@ var ( //go:embed configs/captive-core-testnet.cfg TestnetDefaultConfig []byte + + defaultBucketListDBPageSize uint = 12 ) const ( @@ -95,9 +97,9 @@ type captiveCoreTomlValues struct { Validators []Validator `toml:"VALIDATORS,omitempty"` HistoryEntries map[string]History `toml:"-"` QuorumSetEntries map[string]QuorumSet `toml:"-"` - UseBucketListDB bool `toml:"EXPERIMENTAL_BUCKETLIST_DB,omitempty"` - BucketListDBPageSizeExp *uint `toml:"EXPERIMENTAL_BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT,omitempty"` - BucketListDBCutoff *uint `toml:"EXPERIMENTAL_BUCKETLIST_DB_INDEX_CUTOFF,omitempty"` + BucketListDBPageSizeExp *uint `toml:"BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT,omitempty"` + BucketListDBCutoff *uint `toml:"BUCKETLIST_DB_INDEX_CUTOFF,omitempty"` + DeprecatedSqlLedgerState *bool `toml:"DEPRECATED_SQL_LEDGER_STATE,omitempty"` EnableSorobanDiagnosticEvents *bool `toml:"ENABLE_SOROBAN_DIAGNOSTIC_EVENTS,omitempty"` TestingMinimumPersistentEntryLifetime *uint `toml:"TESTING_MINIMUM_PERSISTENT_ENTRY_LIFETIME,omitempty"` TestingSorobanHighLimitOverride *bool `toml:"TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE,omitempty"` @@ -446,14 +448,15 @@ func (c *CaptiveCoreToml) setDefaults(params CaptiveCoreTomlParams) { c.Database = "sqlite3://stellar.db" } - if def := c.tree.Has("EXPERIMENTAL_BUCKETLIST_DB"); !def && params.UseDB { - c.UseBucketListDB = true - } - - if c.UseBucketListDB && !c.tree.Has("EXPERIMENTAL_BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT") { - n := uint(12) - c.BucketListDBPageSizeExp = &n // Set default page size to 4KB + deprecatedSqlLedgerState := false + if !params.UseDB { + deprecatedSqlLedgerState = true + } else { + if !c.tree.Has("BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT") { + c.BucketListDBPageSizeExp = &defaultBucketListDBPageSize + } } + c.DeprecatedSqlLedgerState = &deprecatedSqlLedgerState if !c.tree.Has("NETWORK_PASSPHRASE") { c.NetworkPassphrase = params.NetworkPassphrase @@ -543,10 +546,14 @@ func (c *CaptiveCoreToml) validate(params CaptiveCoreTomlParams) error { ) } - if def := c.tree.Has("EXPERIMENTAL_BUCKETLIST_DB"); def && !params.UseDB { - return fmt.Errorf( - "BucketListDB enabled in captive core config file, requires Horizon flag --captive-core-use-db", - ) + if c.tree.Has("DEPRECATED_SQL_LEDGER_STATE") { + if params.UseDB && *c.DeprecatedSqlLedgerState { + return fmt.Errorf("CAPTIVE_CORE_USE_DB parameter is set to true, indicating stellar-core on-disk mode," + + " in which DEPRECATED_SQL_LEDGER_STATE must be set to false") + } else if !params.UseDB && !*c.DeprecatedSqlLedgerState { + return fmt.Errorf("CAPTIVE_CORE_USE_DB parameter is set to false, indicating stellar-core in-memory mode," + + " in which DEPRECATED_SQL_LEDGER_STATE must be set to true") + } } homeDomainSet := map[string]HomeDomain{} diff --git a/ingest/ledgerbackend/toml_test.go b/ingest/ledgerbackend/toml_test.go index 39b8473d4a..42412b85f1 100644 --- a/ingest/ledgerbackend/toml_test.go +++ b/ingest/ledgerbackend/toml_test.go @@ -2,9 +2,7 @@ package ledgerbackend import ( "io/ioutil" - "os" "path/filepath" - "strconv" "testing" "github.com/stretchr/testify/assert" @@ -28,6 +26,7 @@ func TestCaptiveCoreTomlValidation(t *testing.T) { peerPort *uint logPath *string expectedError string + inMemory bool }{ { name: "mismatching NETWORK_PASSPHRASE", @@ -203,6 +202,19 @@ func TestCaptiveCoreTomlValidation(t *testing.T) { appendPath: filepath.Join("testdata", "appendix-with-bucket-dir-path.cfg"), expectedError: "could not unmarshal captive core toml: setting BUCKET_DIR_PATH is disallowed for Captive Core, use CAPTIVE_CORE_STORAGE_PATH instead", }, + { + name: "invalid DEPRECATED_SQL_LEDGER_STATE on-disk", + appendPath: filepath.Join("testdata", "sample-appendix-on-disk.cfg"), + expectedError: "invalid captive core toml: CAPTIVE_CORE_USE_DB parameter is set to true, indicating " + + "stellar-core on-disk mode, in which DEPRECATED_SQL_LEDGER_STATE must be set to false", + }, + { + name: "invalid DEPRECATED_SQL_LEDGER_STATE in-memory", + appendPath: filepath.Join("testdata", "sample-appendix-in-memory.cfg"), + expectedError: "invalid captive core toml: CAPTIVE_CORE_USE_DB parameter is set to false, indicating " + + "stellar-core in-memory mode, in which DEPRECATED_SQL_LEDGER_STATE must be set to true", + inMemory: true, + }, } { t.Run(testCase.name, func(t *testing.T) { params := CaptiveCoreTomlParams{ @@ -212,6 +224,7 @@ func TestCaptiveCoreTomlValidation(t *testing.T) { PeerPort: testCase.peerPort, LogPath: testCase.logPath, Strict: true, + UseDB: !testCase.inMemory, } _, err := NewCaptiveCoreTomlFromFile(testCase.appendPath, params) assert.EqualError(t, err, testCase.expectedError) @@ -219,18 +232,6 @@ func TestCaptiveCoreTomlValidation(t *testing.T) { } } -func checkTestingAboveProtocol19() bool { - str := os.Getenv("HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL") - if str == "" { - return false - } - version, err := strconv.ParseUint(str, 10, 32) - if err != nil { - return false - } - return uint32(version) > 19 -} - func TestGenerateConfig(t *testing.T) { for _, testCase := range []struct { name string @@ -352,6 +353,22 @@ func TestGenerateConfig(t *testing.T) { expectedPath: filepath.Join("testdata", "expected-online-with-appendix-minimum-persistent-entry.cfg"), logPath: nil, }, + { + name: "default BucketlistDB config", + mode: stellarCoreRunnerModeOnline, + appendPath: filepath.Join("testdata", "sample-appendix.cfg"), + expectedPath: filepath.Join("testdata", "expected-default-bucketlistdb-core.cfg"), + useDB: true, + logPath: nil, + }, + { + name: "BucketlistDB config in appendix", + mode: stellarCoreRunnerModeOnline, + appendPath: filepath.Join("testdata", "sample-appendix-bucketlistdb.cfg"), + expectedPath: filepath.Join("testdata", "expected-bucketlistdb-core.cfg"), + useDB: true, + logPath: nil, + }, } { t.Run(testCase.name, func(t *testing.T) { var err error @@ -385,6 +402,29 @@ func TestGenerateConfig(t *testing.T) { } } +func TestGenerateCoreConfigInMemory(t *testing.T) { + appendPath := filepath.Join("testdata", "sample-appendix.cfg") + expectedPath := filepath.Join("testdata", "expected-in-mem-core.cfg") + var err error + var captiveCoreToml *CaptiveCoreToml + params := CaptiveCoreTomlParams{ + NetworkPassphrase: "Public Global Stellar Network ; September 2015", + HistoryArchiveURLs: []string{"http://localhost:1170"}, + Strict: false, + UseDB: false, + } + captiveCoreToml, err = NewCaptiveCoreTomlFromFile(appendPath, params) + assert.NoError(t, err) + + configBytes, err := generateConfig(captiveCoreToml, stellarCoreRunnerModeOnline) + assert.NoError(t, err) + + expectedByte, err := ioutil.ReadFile(expectedPath) + assert.NoError(t, err) + + assert.Equal(t, string(expectedByte), string(configBytes)) +} + func TestHistoryArchiveURLTrailingSlash(t *testing.T) { httpPort := uint(8000) peerPort := uint(8000) @@ -461,6 +501,9 @@ func TestDBConfigDefaultsToSqlite(t *testing.T) { toml := CaptiveCoreToml{} require.NoError(t, toml.unmarshal(configBytes, true)) assert.Equal(t, toml.Database, "sqlite3://stellar.db") + assert.Equal(t, *toml.DeprecatedSqlLedgerState, false) + assert.Equal(t, *toml.BucketListDBPageSizeExp, defaultBucketListDBPageSize) + assert.Equal(t, toml.BucketListDBCutoff, (*uint)(nil)) } func TestNonDBConfigDoesNotUpdateDatabase(t *testing.T) { diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 7a89844062..6f127b9968 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -10,8 +10,12 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). - Change ingestion filtering logic to store transactions if any filter matches on it. ([5303](https://github.com/stellar/go/pull/5303)) - The previous behaviour was to store a tx only if both asset and account filters match together. So even if a tx matched an account filter but failed to match an asset filter, it would not be stored by Horizon. -## 2.30.0 +- Captive-core configuration parameters updated to align with [stellar-core v21](https://github.com/stellar/stellar-core/issues/3811) ([5333](https://github.com/stellar/go/pull/5333)) + - BucketlistDB is now the default database for stellar-core, deprecating `EXPERIMENTAL_BUCKETLIST_DB`. + - A new mandatory parameter `DEPRECATED_SQL_LEDGER_STATE` (default: false) is required by stellar-core on its configuration toml file. if the toml provided by `CAPTIVE_CORE_CONFIG_PATH` does not have this new setting, Horizon will add it automatically, therefore, no action required. + - If using `EXPERIMENTAL_BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT` or `EXPERIMENTAL_BUCKETLIST_DB_INDEX_CUTOFF`, they have been renamed to `BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT` and `BUCKETLIST_DB_INDEX_CUTOFF` respectively. +## 2.30.0 **This release adds support for Protocol 21** ### Added diff --git a/services/horizon/docker/captive-core-integration-tests.cfg b/services/horizon/docker/captive-core-integration-tests.cfg index 02c59d7057..275599bacd 100644 --- a/services/horizon/docker/captive-core-integration-tests.cfg +++ b/services/horizon/docker/captive-core-integration-tests.cfg @@ -4,8 +4,6 @@ ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true UNSAFE_QUORUM=true FAILURE_SAFETY=0 -EXPERIMENTAL_BUCKETLIST_DB=true - ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true # Lower the TTL of persistent ledger entries # so that ledger entry extension/restoring becomes testeable diff --git a/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg b/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg index cf4d514975..8d76504de2 100644 --- a/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg +++ b/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg @@ -1,11 +1,10 @@ +EXPERIMENTAL_BUCKETLIST_DB = true PEER_PORT=11725 ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true UNSAFE_QUORUM=true FAILURE_SAFETY=0 -EXPERIMENTAL_BUCKETLIST_DB=true - ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true # Lower the TTL of persistent ledger entries # so that ledger entry extension/restoring becomes testeable diff --git a/services/horizon/docker/captive-core-reingest-range-integration-tests.cfg b/services/horizon/docker/captive-core-reingest-range-integration-tests.cfg index 4744fd390e..44820f5933 100644 --- a/services/horizon/docker/captive-core-reingest-range-integration-tests.cfg +++ b/services/horizon/docker/captive-core-reingest-range-integration-tests.cfg @@ -2,7 +2,6 @@ ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE=true TESTING_MINIMUM_PERSISTENT_ENTRY_LIFETIME=10 ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true -EXPERIMENTAL_BUCKETLIST_DB=true [[VALIDATORS]] NAME="local_core" diff --git a/services/horizon/docker/captive-core-standalone.cfg b/services/horizon/docker/captive-core-standalone.cfg index f5042a14df..d54b0ecae1 100644 --- a/services/horizon/docker/captive-core-standalone.cfg +++ b/services/horizon/docker/captive-core-standalone.cfg @@ -3,8 +3,6 @@ PEER_PORT=11725 UNSAFE_QUORUM=true FAILURE_SAFETY=0 -EXPERIMENTAL_BUCKETLIST_DB=true - [[VALIDATORS]] NAME="local_core" HOME_DOMAIN="core.local" diff --git a/services/horizon/docker/stellar-core-classic-integration-tests.cfg b/services/horizon/docker/stellar-core-classic-integration-tests.cfg index f2b20b4927..fe23e94e8d 100644 --- a/services/horizon/docker/stellar-core-classic-integration-tests.cfg +++ b/services/horizon/docker/stellar-core-classic-integration-tests.cfg @@ -1,3 +1,4 @@ +DEPRECATED_SQL_LEDGER_STATE=false ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true NETWORK_PASSPHRASE="Standalone Network ; February 2017" @@ -13,7 +14,6 @@ UNSAFE_QUORUM=true FAILURE_SAFETY=0 DATABASE="postgresql://user=postgres password=mysecretpassword host=core-postgres port=5641 dbname=stellar" -EXPERIMENTAL_BUCKETLIST_DB=true [QUORUM_SET] THRESHOLD_PERCENT=100 diff --git a/services/horizon/docker/stellar-core-integration-tests.cfg b/services/horizon/docker/stellar-core-integration-tests.cfg index 0d5ec5cc43..414ad9c1eb 100644 --- a/services/horizon/docker/stellar-core-integration-tests.cfg +++ b/services/horizon/docker/stellar-core-integration-tests.cfg @@ -1,3 +1,4 @@ +DEPRECATED_SQL_LEDGER_STATE=false ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true NETWORK_PASSPHRASE="Standalone Network ; February 2017" @@ -13,7 +14,6 @@ UNSAFE_QUORUM=true FAILURE_SAFETY=0 DATABASE="postgresql://user=postgres password=mysecretpassword host=core-postgres port=5641 dbname=stellar" -EXPERIMENTAL_BUCKETLIST_DB=true # Lower the TTL of persistent ledger entries # so that ledger entry extension/restoring becomes testeable diff --git a/services/horizon/docker/stellar-core-pubnet.cfg b/services/horizon/docker/stellar-core-pubnet.cfg index 7f250b10c7..94b29c4b4e 100644 --- a/services/horizon/docker/stellar-core-pubnet.cfg +++ b/services/horizon/docker/stellar-core-pubnet.cfg @@ -9,8 +9,6 @@ DATABASE="postgresql://user=postgres password=mysecretpassword host=host.docker. NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" CATCHUP_RECENT=100 -EXPERIMENTAL_BUCKETLIST_DB=true - [HISTORY.cache] get="cp /opt/stellar/history-cache/{0} {1}" diff --git a/services/horizon/docker/stellar-core-standalone.cfg b/services/horizon/docker/stellar-core-standalone.cfg index 41c8a377aa..a2b7e806c9 100644 --- a/services/horizon/docker/stellar-core-standalone.cfg +++ b/services/horizon/docker/stellar-core-standalone.cfg @@ -14,7 +14,6 @@ UNSAFE_QUORUM=true FAILURE_SAFETY=0 DATABASE="postgresql://user=postgres password=mysecretpassword host=host.docker.internal port=5641 dbname=stellar" -EXPERIMENTAL_BUCKETLIST_DB=true [QUORUM_SET] THRESHOLD_PERCENT=100 diff --git a/services/horizon/docker/stellar-core-testnet.cfg b/services/horizon/docker/stellar-core-testnet.cfg index 55c5cea462..cf8546a3e9 100644 --- a/services/horizon/docker/stellar-core-testnet.cfg +++ b/services/horizon/docker/stellar-core-testnet.cfg @@ -12,8 +12,6 @@ UNSAFE_QUORUM=true FAILURE_SAFETY=1 CATCHUP_RECENT=100 -EXPERIMENTAL_BUCKETLIST_DB=true - [HISTORY.cache] get="cp /opt/stellar/history-cache/{0} {1}" diff --git a/services/horizon/docker/verify-range/captive-core-pubnet.cfg b/services/horizon/docker/verify-range/captive-core-pubnet.cfg index cedbbc65ab..5a702711fe 100644 --- a/services/horizon/docker/verify-range/captive-core-pubnet.cfg +++ b/services/horizon/docker/verify-range/captive-core-pubnet.cfg @@ -2,8 +2,6 @@ PEER_PORT=11725 FAILURE_SAFETY=1 -EXPERIMENTAL_BUCKETLIST_DB=true - [[HOME_DOMAINS]] HOME_DOMAIN="stellar.org" QUALITY="HIGH" diff --git a/services/horizon/docker/verify-range/stellar-core.cfg b/services/horizon/docker/verify-range/stellar-core.cfg index 91bb190131..6139f9f528 100644 --- a/services/horizon/docker/verify-range/stellar-core.cfg +++ b/services/horizon/docker/verify-range/stellar-core.cfg @@ -4,8 +4,6 @@ LOG_FILE_PATH="" NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" CATCHUP_RECENT=100 -EXPERIMENTAL_BUCKETLIST_DB=true - AUTOMATIC_MAINTENANCE_COUNT=0 NODE_NAMES=[ From b5c5b62ded30c1eb44b968017f7c109fb4be42a3 Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 24 Jun 2024 17:58:55 +0100 Subject: [PATCH 194/234] services/horizon/internal: Improve horizon history reaper (#5331) Improve horizon history reaper --- services/horizon/cmd/db.go | 30 +- services/horizon/internal/app.go | 26 -- services/horizon/internal/config.go | 5 + .../horizon/internal/db2/history/ledger.go | 14 + .../internal/db2/history/ledger_test.go | 51 +++ services/horizon/internal/db2/history/main.go | 17 +- .../horizon/internal/db2/history/reap_test.go | 20 +- .../internal/db2/history/verify_lock.go | 28 +- services/horizon/internal/flags.go | 18 + services/horizon/internal/httpt_test.go | 16 +- services/horizon/internal/ingest/fsm.go | 1 + .../fsm_reingest_history_range_state.go | 2 +- .../ingest/ingest_history_range_state_test.go | 18 +- services/horizon/internal/ingest/main.go | 43 ++- services/horizon/internal/ingest/main_test.go | 19 +- services/horizon/internal/ingest/reap.go | 245 ++++++++++++ services/horizon/internal/ingest/reap_test.go | 365 ++++++++++++++++++ .../internal/ingest/resume_state_test.go | 1 + services/horizon/internal/init.go | 5 + .../horizon/internal/integration/db_test.go | 3 +- services/horizon/internal/reap/main.go | 41 -- services/horizon/internal/reap/system.go | 125 ------ services/horizon/internal/reap/system_test.go | 54 --- support/db/main.go | 2 +- support/db/metrics.go | 6 +- support/db/mock_session.go | 5 +- support/db/session.go | 9 +- 27 files changed, 853 insertions(+), 316 deletions(-) create mode 100644 services/horizon/internal/ingest/reap.go create mode 100644 services/horizon/internal/ingest/reap_test.go delete mode 100644 services/horizon/internal/reap/main.go delete mode 100644 services/horizon/internal/reap/system.go delete mode 100644 services/horizon/internal/reap/system_test.go diff --git a/services/horizon/cmd/db.go b/services/horizon/cmd/db.go index cec51543c9..e2589a7385 100644 --- a/services/horizon/cmd/db.go +++ b/services/horizon/cmd/db.go @@ -7,14 +7,15 @@ import ( "go/types" "log" "os" + "os/signal" "strconv" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/stellar/go/services/horizon/internal/db2/history" horizon "github.com/stellar/go/services/horizon/internal" + "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/services/horizon/internal/db2/schema" "github.com/stellar/go/services/horizon/internal/ingest" support "github.com/stellar/go/support/config" @@ -220,17 +221,28 @@ var dbReapCmd = &cobra.Command{ Short: "reaps (i.e. removes) any reapable history data", Long: "reap removes any historical data that is earlier than the configured retention cutoff", RunE: func(cmd *cobra.Command, args []string) error { - app, err := horizon.NewAppFromFlags(globalConfig, globalFlags) + + err := horizon.ApplyFlags(globalConfig, globalFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false, AlwaysIngest: false}) if err != nil { return err } - defer func() { - app.Shutdown() - app.CloseDB() - }() - ctx := context.Background() - app.UpdateHorizonLedgerState(ctx) - return app.DeleteUnretainedHistory(ctx) + + session, err := db.Open("postgres", globalConfig.DatabaseURL) + if err != nil { + return fmt.Errorf("cannot open Horizon DB: %v", err) + } + defer session.Close() + + reaper := ingest.NewReaper( + ingest.ReapConfig{ + RetentionCount: uint32(globalConfig.HistoryRetentionCount), + BatchSize: uint32(globalConfig.HistoryRetentionReapCount), + }, + session, + ) + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) + defer cancel() + return reaper.DeleteUnretainedHistory(ctx) }, } diff --git a/services/horizon/internal/app.go b/services/horizon/internal/app.go index 843232beba..f99e4e9159 100644 --- a/services/horizon/internal/app.go +++ b/services/horizon/internal/app.go @@ -22,7 +22,6 @@ import ( "github.com/stellar/go/services/horizon/internal/ledger" "github.com/stellar/go/services/horizon/internal/operationfeestats" "github.com/stellar/go/services/horizon/internal/paths" - "github.com/stellar/go/services/horizon/internal/reap" "github.com/stellar/go/services/horizon/internal/txsub" "github.com/stellar/go/support/app" "github.com/stellar/go/support/db" @@ -47,7 +46,6 @@ type App struct { submitter *txsub.System paths paths.Finder ingester ingest.System - reaper *reap.System ticks *time.Ticker ledgerState *ledger.State @@ -107,14 +105,6 @@ func (a *App) Serve() error { }() } - if a.reaper != nil { - wg.Add(1) - go func() { - a.reaper.Run() - wg.Done() - }() - } - // configure shutdown signal handler signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) @@ -169,9 +159,6 @@ func (a *App) Shutdown() { if a.ingester != nil { a.ingester.Shutdown() } - if a.reaper != nil { - a.reaper.Shutdown() - } a.ticks.Stop() } @@ -441,12 +428,6 @@ func (a *App) UpdateStellarCoreInfo(ctx context.Context) error { return nil } -// DeleteUnretainedHistory forwards to the app's reaper. See -// `reap.DeleteUnretainedHistory` for details -func (a *App) DeleteUnretainedHistory(ctx context.Context) error { - return a.reaper.DeleteUnretainedHistory(ctx) -} - // Tick triggers horizon to update all of it's background processes such as // transaction submission, metrics, ingestion and reaping. func (a *App) Tick(ctx context.Context) error { @@ -511,13 +492,6 @@ func (a *App) init() error { // txsub initSubmissionSystem(a) - // reaper - a.reaper = reap.New( - a.config.HistoryRetentionCount, - a.config.HistoryRetentionReapCount, - a.HorizonSession(), - a.ledgerState) - // go metrics initGoMetrics(a) diff --git a/services/horizon/internal/config.go b/services/horizon/internal/config.go index 2e4192a1c9..3414b1aaea 100644 --- a/services/horizon/internal/config.go +++ b/services/horizon/internal/config.go @@ -80,6 +80,11 @@ type Config struct { // especially if enabling reaping for the first time or in times of // increased ledger load. HistoryRetentionReapCount uint + // ReapFrequency configures how often (in units of ledgers) history is reaped. + // If ReapFrequency is set to 1 history is reaped after ingesting every ledger. + // If ReapFrequency is set to 2 history is reaped after ingesting every two ledgers. + // etc... + ReapFrequency uint // StaleThreshold represents the number of ledgers a history database may be // out-of-date by before horizon begins to respond with an error to history // requests. diff --git a/services/horizon/internal/db2/history/ledger.go b/services/horizon/internal/db2/history/ledger.go index ca89534702..c2d1f6b3c9 100644 --- a/services/horizon/internal/db2/history/ledger.go +++ b/services/horizon/internal/db2/history/ledger.go @@ -2,6 +2,7 @@ package history import ( "context" + "database/sql" "encoding/hex" "fmt" "sort" @@ -9,6 +10,7 @@ import ( sq "github.com/Masterminds/squirrel" "github.com/guregu/null" + "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" @@ -150,6 +152,18 @@ func (i *ledgerBatchInsertBuilder) Exec(ctx context.Context, session db.SessionI return i.builder.Exec(ctx, session, i.table) } +func (q *Q) GetNextLedgerSequence(ctx context.Context, start uint32) (uint32, bool, error) { + var value uint32 + err := q.GetRaw(ctx, &value, `SELECT sequence FROM history_ledgers WHERE sequence > ?`, start) + if err == sql.ErrNoRows { + return 0, false, nil + } + if err != nil { + return 0, false, err + } + return value, true, nil +} + // GetLedgerGaps obtains ingestion gaps in the history_ledgers table. // Returns the gaps and error. func (q *Q) GetLedgerGaps(ctx context.Context) ([]LedgerRange, error) { diff --git a/services/horizon/internal/db2/history/ledger_test.go b/services/horizon/internal/db2/history/ledger_test.go index 4fe9125fbe..bcff8530b4 100644 --- a/services/horizon/internal/db2/history/ledger_test.go +++ b/services/horizon/internal/db2/history/ledger_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/guregu/null" + "github.com/stellar/go/services/horizon/internal/test" "github.com/stellar/go/toid" "github.com/stellar/go/xdr" @@ -337,3 +338,53 @@ func TestGetLedgerGaps(t *testing.T) { expectedGaps = append(expectedGaps, LedgerRange{1001, 1001}) tt.Assert.Equal(expectedGaps, gaps) } + +func TestGetNextLedgerSequence(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + + q := &Q{tt.HorizonSession()} + + _, ok, err := q.GetNextLedgerSequence(context.Background(), 0) + tt.Assert.NoError(err) + tt.Assert.False(ok) + + insertLedgerWithSequence(tt, q, 4) + insertLedgerWithSequence(tt, q, 5) + insertLedgerWithSequence(tt, q, 6) + insertLedgerWithSequence(tt, q, 7) + + insertLedgerWithSequence(tt, q, 99) + insertLedgerWithSequence(tt, q, 100) + insertLedgerWithSequence(tt, q, 101) + insertLedgerWithSequence(tt, q, 102) + + seq, ok, err := q.GetNextLedgerSequence(context.Background(), 0) + tt.Assert.NoError(err) + tt.Assert.True(ok) + tt.Assert.Equal(uint32(4), seq) + + seq, ok, err = q.GetNextLedgerSequence(context.Background(), 4) + tt.Assert.NoError(err) + tt.Assert.True(ok) + tt.Assert.Equal(uint32(5), seq) + + seq, ok, err = q.GetNextLedgerSequence(context.Background(), 10) + tt.Assert.NoError(err) + tt.Assert.True(ok) + tt.Assert.Equal(uint32(99), seq) + + seq, ok, err = q.GetNextLedgerSequence(context.Background(), 101) + tt.Assert.NoError(err) + tt.Assert.True(ok) + tt.Assert.Equal(uint32(102), seq) + + _, ok, err = q.GetNextLedgerSequence(context.Background(), 102) + tt.Assert.NoError(err) + tt.Assert.False(ok) + + _, ok, err = q.GetNextLedgerSequence(context.Background(), 110) + tt.Assert.NoError(err) + tt.Assert.False(ok) +} diff --git a/services/horizon/internal/db2/history/main.go b/services/horizon/internal/db2/history/main.go index 08c80a0385..35f32f5434 100644 --- a/services/horizon/internal/db2/history/main.go +++ b/services/horizon/internal/db2/history/main.go @@ -302,9 +302,12 @@ type IngestionQ interface { GetOfferCompactionSequence(context.Context) (uint32, error) GetLiquidityPoolCompactionSequence(context.Context) (uint32, error) TruncateIngestStateTables(context.Context) error - DeleteRangeAll(ctx context.Context, start, end int64) error + DeleteRangeAll(ctx context.Context, start, end int64) (int64, error) DeleteTransactionsFilteredTmpOlderThan(ctx context.Context, howOldInSeconds uint64) (int64, error) - TryStateVerificationLock(ctx context.Context) (bool, error) + GetNextLedgerSequence(context.Context, uint32) (uint32, bool, error) + TryStateVerificationLock(context.Context) (bool, error) + TryReaperLock(context.Context) (bool, error) + ElderLedger(context.Context, interface{}) error } // QAccounts defines account related queries. @@ -1154,7 +1157,8 @@ func constructReapLookupTablesQuery(table string, historyTables []tableObjectFie // DeleteRangeAll deletes a range of rows from all history tables between // `start` and `end` (exclusive). -func (q *Q) DeleteRangeAll(ctx context.Context, start, end int64) error { +func (q *Q) DeleteRangeAll(ctx context.Context, start, end int64) (int64, error) { + var total int64 for table, column := range map[string]string{ "history_effects": "history_operation_id", "history_ledgers": "id", @@ -1169,12 +1173,13 @@ func (q *Q) DeleteRangeAll(ctx context.Context, start, end int64) error { "history_transaction_liquidity_pools": "history_transaction_id", "history_transactions": "id", } { - err := q.DeleteRange(ctx, start, end, table, column) + count, err := q.DeleteRange(ctx, start, end, table, column) if err != nil { - return errors.Wrapf(err, "Error clearing %s", table) + return 0, errors.Wrapf(err, "Error clearing %s", table) } + total += count } - return nil + return total, nil } // upsertRows builds and executes an upsert query that allows very fast upserts diff --git a/services/horizon/internal/db2/history/reap_test.go b/services/horizon/internal/db2/history/reap_test.go index 6aa271e483..508041eb22 100644 --- a/services/horizon/internal/db2/history/reap_test.go +++ b/services/horizon/internal/db2/history/reap_test.go @@ -4,20 +4,23 @@ import ( "testing" "github.com/stellar/go/services/horizon/internal/db2/history" - "github.com/stellar/go/services/horizon/internal/ledger" - "github.com/stellar/go/services/horizon/internal/reap" + "github.com/stellar/go/services/horizon/internal/ingest" "github.com/stellar/go/services/horizon/internal/test" ) func TestReapLookupTables(t *testing.T) { tt := test.Start(t) defer tt.Finish() - ledgerState := &ledger.State{} - ledgerState.SetStatus(tt.Scenario("kahuna")) + tt.Scenario("kahuna") db := tt.HorizonSession() - - sys := reap.New(0, 0, db, ledgerState) + reaper := ingest.NewReaper( + ingest.ReapConfig{ + RetentionCount: 1, + BatchSize: 50, + }, + db, + ) var ( prevLedgers, curLedgers int @@ -41,10 +44,7 @@ func TestReapLookupTables(t *testing.T) { tt.Require.NoError(err) } - ledgerState.SetStatus(tt.LoadLedgerStatus()) - sys.RetentionCount = 1 - sys.RetentionBatch = 50 - err := sys.DeleteUnretainedHistory(tt.Ctx) + err := reaper.DeleteUnretainedHistory(tt.Ctx) tt.Require.NoError(err) q := &history.Q{tt.HorizonSession()} diff --git a/services/horizon/internal/db2/history/verify_lock.go b/services/horizon/internal/db2/history/verify_lock.go index f56045e9b1..29bc11a473 100644 --- a/services/horizon/internal/db2/history/verify_lock.go +++ b/services/horizon/internal/db2/history/verify_lock.go @@ -7,16 +7,34 @@ import ( "github.com/stellar/go/support/errors" ) -// stateVerificationLockId is the objid for the advisory lock acquired during -// state verification. The value is arbitrary. The only requirement is that -// all ingesting nodes use the same value which is why it's hard coded here. -const stateVerificationLockId = 73897213 +const ( + // stateVerificationLockId is the objid for the advisory lock acquired during + // state verification. The value is arbitrary. The only requirement is that + // all ingesting nodes use the same value which is why it's hard coded here.`1 + stateVerificationLockId = 73897213 + // reaperLockId is the objid for the advisory lock acquired during + // reaping. The value is arbitrary. The only requirement is that + // all ingesting nodes use the same value which is why it's hard coded here. + reaperLockId = 944670730 +) // TryStateVerificationLock attempts to acquire the state verification lock // which gives the ingesting node exclusive access to perform state verification. // TryStateVerificationLock returns true if the lock was acquired or false if the // lock could not be acquired because it is held by another node. func (q *Q) TryStateVerificationLock(ctx context.Context) (bool, error) { + return q.tryAdvisoryLock(ctx, stateVerificationLockId) +} + +// TryReaperLock attempts to acquire the reaper lock +// which gives the ingesting node exclusive access to perform reaping. +// TryReaperLock returns true if the lock was acquired or false if the +// lock could not be acquired because it is held by another node. +func (q *Q) TryReaperLock(ctx context.Context) (bool, error) { + return q.tryAdvisoryLock(ctx, reaperLockId) +} + +func (q *Q) tryAdvisoryLock(ctx context.Context, lockId int) (bool, error) { if tx := q.GetTx(); tx == nil { return false, errors.New("cannot be called outside of a transaction") } @@ -26,7 +44,7 @@ func (q *Q) TryStateVerificationLock(ctx context.Context) (bool, error) { context.WithValue(ctx, &db.QueryTypeContextKey, db.AdvisoryLockQueryType), &acquired, "SELECT pg_try_advisory_xact_lock(?)", - stateVerificationLockId, + lockId, ) if err != nil { return false, errors.Wrap(err, "error acquiring advisory lock for state verification") diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index 8c6b122dcb..7321d07fb2 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -669,6 +669,24 @@ func Flags() (*Config, support.ConfigOptions) { return nil }, }, + &support.ConfigOption{ + Name: "reap-frequency", + ConfigKey: &config.ReapFrequency, + OptType: types.Uint, + FlagDefault: uint(720), + Usage: "the frequency in units of ledgers for how often history is reaped. " + + "A value of 1 implies history is trimmed after every ledger. " + + "A value of 2 implies history is trimmed on every second ledger.", + UsedInCommands: IngestionCommands, + CustomSetValue: func(opt *support.ConfigOption) error { + val := viper.GetUint(opt.Name) + if val <= 0 { + return fmt.Errorf("flag --reap-frequency must be positive") + } + *(opt.ConfigKey.(*uint)) = val + return nil + }, + }, &support.ConfigOption{ Name: "history-stale-threshold", ConfigKey: &config.StaleThreshold, diff --git a/services/horizon/internal/httpt_test.go b/services/horizon/internal/httpt_test.go index 587d401d8a..8dfae08979 100644 --- a/services/horizon/internal/httpt_test.go +++ b/services/horizon/internal/httpt_test.go @@ -7,7 +7,7 @@ import ( "net/url" "testing" - "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/services/horizon/internal/ingest" "github.com/stellar/go/services/horizon/internal/test" tdb "github.com/stellar/go/services/horizon/internal/test/db" ) @@ -102,12 +102,14 @@ func (ht *HTTPT) Post( // ReapHistory causes the test server to run `DeleteUnretainedHistory`, after // setting the retention count to the provided number. -func (ht *HTTPT) ReapHistory(retention uint) { - ht.App.reaper.RetentionCount = retention - ht.App.reaper.RetentionBatch = 50_000 - ht.App.reaper.HistoryQ = &history.Q{ht.HorizonSession()} - err := ht.App.DeleteUnretainedHistory(context.Background()) - ht.Require.NoError(err) +func (ht *HTTPT) ReapHistory(retention uint32) { + reaper := ingest.NewReaper( + ingest.ReapConfig{ + RetentionCount: retention, + BatchSize: 50_000, + }, ht.HorizonSession()) + + ht.Require.NoError(reaper.DeleteUnretainedHistory(context.Background())) ht.App.UpdateCoreLedgerState(context.Background()) ht.App.UpdateHorizonLedgerState(context.Background()) } diff --git a/services/horizon/internal/ingest/fsm.go b/services/horizon/internal/ingest/fsm.go index 1b51518391..283cd3347a 100644 --- a/services/horizon/internal/ingest/fsm.go +++ b/services/horizon/internal/ingest/fsm.go @@ -578,6 +578,7 @@ func (r resumeState) run(s *system) (transition, error) { localLog.Info("Processed ledger") s.maybeVerifyState(ingestLedger, ledgerCloseMeta.BucketListHash()) + s.maybeReapHistory(ingestLedger) s.maybeReapLookupTables(ingestLedger) return resumeImmediately(ingestLedger), nil diff --git a/services/horizon/internal/ingest/fsm_reingest_history_range_state.go b/services/horizon/internal/ingest/fsm_reingest_history_range_state.go index 499d15871c..5d10ece6d9 100644 --- a/services/horizon/internal/ingest/fsm_reingest_history_range_state.go +++ b/services/horizon/internal/ingest/fsm_reingest_history_range_state.go @@ -44,7 +44,7 @@ func (h reingestHistoryRangeState) ingestRange(s *system, fromLedger, toLedger u return errors.Wrap(err, "Invalid range") } - err = s.historyQ.DeleteRangeAll(s.ctx, start, end) + _, err = s.historyQ.DeleteRangeAll(s.ctx, start, end) if err != nil { return errors.Wrap(err, "error in DeleteRangeAll") } diff --git a/services/horizon/internal/ingest/ingest_history_range_state_test.go b/services/horizon/internal/ingest/ingest_history_range_state_test.go index cf89ce4ab2..ce63151fb4 100644 --- a/services/horizon/internal/ingest/ingest_history_range_state_test.go +++ b/services/horizon/internal/ingest/ingest_history_range_state_test.go @@ -352,7 +352,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateClearH toidTo := toid.New(201, 0, 0) s.historyQ.On( "DeleteRangeAll", s.ctx, toidFrom.ToInt64(), toidTo.ToInt64(), - ).Return(errors.New("my error")).Once() + ).Return(int64(0), errors.New("my error")).Once() err := s.system.ReingestRange([]history.LedgerRange{{100, 200}}, false, true) s.Assert().EqualError(err, "error in DeleteRangeAll: my error") @@ -364,7 +364,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateRunTra toidTo := toid.New(201, 0, 0) s.historyQ.On( "DeleteRangeAll", s.ctx, toidFrom.ToInt64(), toidTo.ToInt64(), - ).Return(nil).Once() + ).Return(int64(100), nil).Once() meta := xdr.LedgerCloseMeta{ V0: &xdr.LedgerCloseMetaV0{ @@ -389,7 +389,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateSucces toidTo := toid.New(201, 0, 0) s.historyQ.On( "DeleteRangeAll", s.ctx, toidFrom.ToInt64(), toidTo.ToInt64(), - ).Return(nil).Once() + ).Return(int64(100), nil).Once() s.historyQ.On("RebuildTradeAggregationBuckets", s.ctx, uint32(100), uint32(200), 0).Return(nil).Once() for i := uint32(100); i <= uint32(200); i++ { @@ -417,7 +417,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateSucces toidTo := toid.New(201, 0, 0) s.historyQ.On( "DeleteRangeAll", s.ctx, toidFrom.ToInt64(), toidTo.ToInt64(), - ).Return(nil).Once() + ).Return(int64(100), nil).Once() s.historyQ.On("RebuildTradeAggregationBuckets", s.ctx, uint32(100), uint32(200), 0).Return(nil).Once() firstLedgersBatch := []xdr.LedgerCloseMeta{} @@ -458,7 +458,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateSucces toidTo := toid.New(101, 0, 0) s.historyQ.On( "DeleteRangeAll", s.ctx, toidFrom.ToInt64(), toidTo.ToInt64(), - ).Return(nil).Once() + ).Return(int64(100), nil).Once() meta := xdr.LedgerCloseMeta{ V0: &xdr.LedgerCloseMetaV0{ @@ -497,7 +497,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForce( toidTo := toid.New(201, 0, 0) s.historyQ.On( "DeleteRangeAll", s.ctx, toidFrom.ToInt64(), toidTo.ToInt64(), - ).Return(nil).Once() + ).Return(int64(100), nil).Once() for i := 100; i <= 200; i++ { meta := xdr.LedgerCloseMeta{ @@ -528,7 +528,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceL toidTo := toid.New(201, 0, 0) s.historyQ.On( "DeleteRangeAll", s.ctx, toidFrom.ToInt64(), toidTo.ToInt64(), - ).Return(nil).Once() + ).Return(int64(100), nil).Once() for i := 100; i <= 105; i++ { meta := xdr.LedgerCloseMeta{ @@ -561,7 +561,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceL toidTo := toid.New(201, 0, 0) s.historyQ.On( "DeleteRangeAll", s.ctx, toidFrom.ToInt64(), toidTo.ToInt64(), - ).Return(nil).Once() + ).Return(int64(100), nil).Once() for i := 100; i <= 105; i++ { meta := xdr.LedgerCloseMeta{ @@ -595,7 +595,7 @@ func (s *ReingestHistoryRangeStateTestSuite) TestReingestHistoryRangeStateForceW toidTo := toid.New(201, 0, 0) s.historyQ.On( "DeleteRangeAll", s.ctx, toidFrom.ToInt64(), toidTo.ToInt64(), - ).Return(nil).Once() + ).Return(int64(100), nil).Once() firstLedgersBatch := []xdr.LedgerCloseMeta{} secondLedgersBatch := []xdr.LedgerCloseMeta{} diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 98dcad34f4..5bff414ac3 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -69,7 +69,8 @@ const ( // * Ledger ingestion, // * State verifications, // * Metrics updates. - MaxDBConnections = 3 + // * Reaping (requires 2 connections, the extra connection is used for holding the advisory lock) + MaxDBConnections = 5 defaultCoreCursorName = "HORIZON" stateVerificationErrorThreshold = 3 @@ -112,6 +113,8 @@ type Config struct { CoreProtocolVersionFn ledgerbackend.CoreProtocolVersionFunc CoreBuildVersionFn ledgerbackend.CoreBuildVersionFunc + + ReapConfig ReapConfig } const ( @@ -228,6 +231,8 @@ type system struct { reapOffsets map[string]int64 maxLedgerPerFlush uint32 + reaper *Reaper + currentStateMutex sync.Mutex currentState State } @@ -318,6 +323,10 @@ func NewSystem(config Config) (System, error) { config.StateVerificationCheckpointFrequency, ), maxLedgerPerFlush: maxLedgersPerFlush, + reaper: NewReaper( + config.ReapConfig, + config.HistorySession, + ), } system.initMetrics() @@ -493,6 +502,7 @@ func (s *system) RegisterMetrics(registry *prometheus.Registry) { registry.MustRegister(s.metrics.HistoryArchiveStatsCounter) registry.MustRegister(s.metrics.IngestionErrorCounter) s.ledgerBackend = ledgerbackend.WithMetrics(s.ledgerBackend, registry, "horizon") + s.reaper.RegisterMetrics(registry) } // Run starts ingestion system. Ingestion system supports distributed ingestion @@ -694,11 +704,22 @@ func (s *system) runStateMachine(cur stateMachineNode) error { } } +func (s *system) maybeReapHistory(lastIngestedLedger uint32) { + if s.reaper.config.Frequency == 0 || lastIngestedLedger%uint32(s.reaper.config.Frequency) != 0 { + return + } + s.wg.Add(1) + go func() { + defer s.wg.Done() + s.reaper.DeleteUnretainedHistory(s.ctx) + }() +} + func (s *system) maybeVerifyState(lastIngestedLedger uint32, expectedBucketListHash xdr.Hash) { stateInvalid, err := s.historyQ.GetExpStateInvalid(s.ctx) if err != nil { if !isCancelledError(s.ctx, err) { - log.WithField("err", err).Error("Error getting state invalid value") + log.WithError(err).Error("Error getting state invalid value") } return } @@ -722,9 +743,9 @@ func (s *system) maybeVerifyState(lastIngestedLedger uint32, expectedBucketListH case ingest.StateError: markStateInvalid(s.ctx, s.historyQ, err) default: - logger := log.WithField("err", err).Warn + logger := log.WithError(err).Warn if errorCount >= stateVerificationErrorThreshold { - logger = log.WithField("err", err).Error + logger = log.WithError(err).Error } logger("State verification errored") } @@ -743,7 +764,7 @@ func (s *system) maybeReapLookupTables(lastIngestedLedger uint32) { // Check if lastIngestedLedger is the last one available in the backend sequence, err := s.ledgerBackend.GetLatestLedgerSequence(s.ctx) if err != nil { - log.WithField("err", err).Error("Error getting latest ledger sequence from backend") + log.WithError(err).Error("Error getting latest ledger sequence from backend") return } @@ -754,7 +775,7 @@ func (s *system) maybeReapLookupTables(lastIngestedLedger uint32) { err = s.historyQ.Begin(s.ctx) if err != nil { - log.WithField("err", err).Error("Error starting a transaction") + log.WithError(err).Error("Error starting a transaction") return } defer s.historyQ.Rollback() @@ -762,7 +783,7 @@ func (s *system) maybeReapLookupTables(lastIngestedLedger uint32) { // If so block ingestion in the cluster to reap tables _, err = s.historyQ.GetLastLedgerIngest(s.ctx) if err != nil { - log.WithField("err", err).Error(getLastIngestedErrMsg) + log.WithError(err).Error(getLastIngestedErrMsg) return } @@ -774,13 +795,13 @@ func (s *system) maybeReapLookupTables(lastIngestedLedger uint32) { reapStart := time.Now() deletedCount, newOffsets, err := s.historyQ.ReapLookupTables(ctx, s.reapOffsets) if err != nil { - log.WithField("err", err).Warn("Error reaping lookup tables") + log.WithError(err).Warn("Error reaping lookup tables") return } err = s.historyQ.Commit() if err != nil { - log.WithField("err", err).Error("Error committing a transaction") + log.WithError(err).Error("Error committing a transaction") return } @@ -830,10 +851,10 @@ func (s *system) Shutdown() { } func markStateInvalid(ctx context.Context, historyQ history.IngestionQ, err error) { - log.WithField("err", err).Error("STATE IS INVALID!") + log.WithError(err).Error("STATE IS INVALID!") q := historyQ.CloneIngestionQ() if err := q.UpdateExpStateInvalid(ctx, true); err != nil { - log.WithField("err", err).Error(updateExpStateInvalidErrMsg) + log.WithError(err).Error(updateExpStateInvalidErrMsg) } } diff --git a/services/horizon/internal/ingest/main_test.go b/services/horizon/internal/ingest/main_test.go index 40a3d48cce..4f0e220ebe 100644 --- a/services/horizon/internal/ingest/main_test.go +++ b/services/horizon/internal/ingest/main_test.go @@ -457,6 +457,21 @@ func (m *mockDBQ) TryStateVerificationLock(ctx context.Context) (bool, error) { return args.Get(0).(bool), args.Error(1) } +func (m *mockDBQ) TryReaperLock(ctx context.Context) (bool, error) { + args := m.Called(ctx) + return args.Get(0).(bool), args.Error(1) +} + +func (m *mockDBQ) GetNextLedgerSequence(ctx context.Context, start uint32) (uint32, bool, error) { + args := m.Called(ctx, start) + return args.Get(0).(uint32), args.Get(1).(bool), args.Error(2) +} + +func (m *mockDBQ) ElderLedger(ctx context.Context, dest interface{}) error { + args := m.Called(ctx, dest) + return args.Error(0) +} + func (m *mockDBQ) GetTx() *sqlx.Tx { args := m.Called() if args.Get(0) == nil { @@ -525,9 +540,9 @@ func (m *mockDBQ) TruncateIngestStateTables(ctx context.Context) error { return args.Error(0) } -func (m *mockDBQ) DeleteRangeAll(ctx context.Context, start, end int64) error { +func (m *mockDBQ) DeleteRangeAll(ctx context.Context, start, end int64) (int64, error) { args := m.Called(ctx, start, end) - return args.Error(0) + return args.Get(0).(int64), args.Error(1) } // Methods from interfaces duplicating methods: diff --git a/services/horizon/internal/ingest/reap.go b/services/horizon/internal/ingest/reap.go new file mode 100644 index 0000000000..07a61a4cde --- /dev/null +++ b/services/horizon/internal/ingest/reap.go @@ -0,0 +1,245 @@ +package ingest + +import ( + "context" + "fmt" + "strconv" + "sync/atomic" + "time" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/support/db" + "github.com/stellar/go/support/errors" + logpkg "github.com/stellar/go/support/log" + "github.com/stellar/go/toid" +) + +// Reaper represents the history reaping subsystem of horizon. +type Reaper struct { + historyQ history.IngestionQ + reapLockQ history.IngestionQ + pending atomic.Bool + config ReapConfig + logger *logpkg.Entry + + totalDuration *prometheus.SummaryVec + totalDeleted *prometheus.SummaryVec + deleteBatchDuration prometheus.Summary + rowsInBatchDeleted prometheus.Summary +} + +type ReapConfig struct { + Frequency uint + RetentionCount uint32 + BatchSize uint32 +} + +// NewReaper creates a new Reaper instance +func NewReaper(config ReapConfig, dbSession db.SessionInterface) *Reaper { + return newReaper(config, &history.Q{dbSession.Clone()}, &history.Q{dbSession.Clone()}) +} + +func newReaper(config ReapConfig, historyQ, reapLockQ history.IngestionQ) *Reaper { + return &Reaper{ + historyQ: historyQ, + reapLockQ: reapLockQ, + config: config, + deleteBatchDuration: prometheus.NewSummary(prometheus.SummaryOpts{ + Namespace: "horizon", Subsystem: "reap", Name: "batch_duration", + Help: "reap batch duration in seconds, sliding window = 10m", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }), + rowsInBatchDeleted: prometheus.NewSummary(prometheus.SummaryOpts{ + Namespace: "horizon", Subsystem: "reap", Name: "batch_rows_deleted", + Help: "rows deleted during reap batch, sliding window = 10m", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }), + totalDuration: prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Namespace: "horizon", Subsystem: "reap", Name: "duration", + Help: "reap invocation duration in seconds, sliding window = 10m", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, []string{"complete"}), + totalDeleted: prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Namespace: "horizon", Subsystem: "reap", Name: "rows_deleted", + Help: "rows deleted during reap invocation, sliding window = 10m", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, []string{"complete"}), + logger: log.WithField("subservice", "reaper"), + } +} + +// DeleteUnretainedHistory removes all data associated with unretained ledgers. +func (r *Reaper) DeleteUnretainedHistory(ctx context.Context) error { + // RetentionCount of 0 indicates "keep all history" + if r.config.RetentionCount == 0 { + return nil + } + + // check if reap is already in progress on this horizon node + if !r.pending.CompareAndSwap(false, true) { + r.logger.Infof("existing reap already in progress, skipping request to start a new one") + return nil + } + defer r.pending.Store(false) + + if err := r.reapLockQ.Begin(ctx); err != nil { + return errors.Wrap(err, "error while starting reaper lock transaction") + } + defer func() { + if err := r.reapLockQ.Rollback(); err != nil { + r.logger.WithField("error", err).Error("failed to release reaper lock") + } + }() + // check if reap is already in progress on another horizon node + if acquired, err := r.reapLockQ.TryReaperLock(ctx); err != nil { + return errors.Wrap(err, "error while acquiring reaper database lock") + } else if !acquired { + r.logger.Info("reap already in progress on another node") + return nil + } + + latest, err := r.historyQ.GetLatestHistoryLedger(ctx) + if err != nil { + return errors.Wrap(err, "error fetching latest history ledger") + } + var oldest uint32 + err = r.historyQ.ElderLedger(ctx, &oldest) + if err != nil { + return errors.Wrap(err, "error fetching elder ledger") + } + + targetElder := latest - r.config.RetentionCount + 1 + if latest <= r.config.RetentionCount || targetElder < oldest { + r.logger. + WithField("latest", latest). + WithField("oldest", oldest). + WithField("retention_count", r.config.RetentionCount). + Info("not enough history to reap") + return nil + } + + startTime := time.Now() + var totalDeleted int64 + var complete bool + totalDeleted, err = r.clearBefore(ctx, oldest, targetElder) + elapsedSeconds := time.Since(startTime).Seconds() + logger := r.logger. + WithField("duration", elapsedSeconds). + WithField("rows_deleted", totalDeleted) + + if err != nil { + logger.WithError(err).Warn("reaper failed") + } else { + complete = true + logger. + WithField("new_elder", targetElder). + Info("reaper succeeded") + } + + labels := prometheus.Labels{ + "complete": strconv.FormatBool(complete), + } + r.totalDeleted.With(labels).Observe(float64(totalDeleted)) + r.totalDuration.With(labels).Observe(elapsedSeconds) + return err +} + +// RegisterMetrics registers the prometheus metrics +func (s *Reaper) RegisterMetrics(registry *prometheus.Registry) { + registry.MustRegister( + s.deleteBatchDuration, + s.rowsInBatchDeleted, + s.totalDuration, + s.totalDeleted, + ) +} + +// Work in 50k (by default, otherwise configurable via the CLI) ledger +// blocks to prevent using all the CPU. +// +// By default, this runs every 720 ledgers (approximately 1 hour), so we +// need to make sure it doesn't run for longer than +// an hour. +// +// Current ledger at 2024-04-04s is 51,092,283, so 50k means 1021 batches. At 1 +// batch/second, that seems like a reasonable balance between running under an +// hour, and slowing it down enough to leave some CPU for other processes. +var sleep = 1 * time.Second + +func (r *Reaper) clearBefore(ctx context.Context, startSeq, endSeq uint32) (int64, error) { + batchSize := r.config.BatchSize + var sum int64 + if batchSize <= 0 { + return sum, fmt.Errorf("invalid batch size for reaping (%d)", batchSize) + } + + r.logger.WithField("start_ledger", startSeq). + WithField("end_ledger", endSeq). + WithField("batch_size", batchSize). + Info("deleting history outside retention window") + + for batchStartSeq := startSeq; batchStartSeq < endSeq; { + batchEndSeq := batchStartSeq + batchSize + if batchEndSeq >= endSeq { + batchEndSeq = endSeq - 1 + } + + count, err := r.deleteBatch(ctx, batchStartSeq, batchEndSeq) + if err != nil { + return sum, err + } + sum += count + if count == 0 { + next, ok, err := r.historyQ.GetNextLedgerSequence(ctx, batchStartSeq) + if err != nil { + return sum, errors.Wrapf(err, "could not find next ledger sequence after %d", batchStartSeq) + } + if !ok { + break + } + batchStartSeq = next + } else { + batchStartSeq += batchSize + 1 + } + time.Sleep(sleep) + } + + return sum, nil +} + +func (r *Reaper) deleteBatch(ctx context.Context, batchStartSeq, batchEndSeq uint32) (int64, error) { + batchStart, batchEnd, err := toid.LedgerRangeInclusive(int32(batchStartSeq), int32(batchEndSeq)) + if err != nil { + return 0, err + } + + startTime := time.Now() + err = r.historyQ.Begin(ctx) + if err != nil { + return 0, errors.Wrap(err, "Error in begin") + } + defer r.historyQ.Rollback() + + count, err := r.historyQ.DeleteRangeAll(ctx, batchStart, batchEnd) + if err != nil { + return 0, errors.Wrap(err, "Error in DeleteRangeAll") + } + + err = r.historyQ.Commit() + if err != nil { + return 0, errors.Wrap(err, "Error in commit") + } + + elapsedSeconds := time.Since(startTime).Seconds() + r.logger.WithField("start_ledger", batchStartSeq). + WithField("end_ledger", batchEndSeq). + WithField("rows_deleted", strconv.FormatInt(count, 10)). + WithField("duration", elapsedSeconds). + Info("successfully deleted batch") + + r.rowsInBatchDeleted.Observe(float64(count)) + r.deleteBatchDuration.Observe(elapsedSeconds) + return count, nil +} diff --git a/services/horizon/internal/ingest/reap_test.go b/services/horizon/internal/ingest/reap_test.go new file mode 100644 index 0000000000..c34e94c543 --- /dev/null +++ b/services/horizon/internal/ingest/reap_test.go @@ -0,0 +1,365 @@ +package ingest + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + + "github.com/stellar/go/services/horizon/internal/test" + "github.com/stellar/go/toid" +) + +func TestDeleteUnretainedHistory(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + tt.Scenario("kahuna") + + db := tt.HorizonSession() + + reaper := NewReaper(ReapConfig{ + RetentionCount: 0, + BatchSize: 50, + }, db) + + // Disable sleeps for this. + prevSleep := sleep + sleep = 0 + t.Cleanup(func() { + sleep = prevSleep + }) + + var ( + prev int + cur int + ) + err := db.GetRaw(tt.Ctx, &prev, `SELECT COUNT(*) FROM history_ledgers`) + tt.Require.NoError(err) + + err = reaper.DeleteUnretainedHistory(tt.Ctx) + if tt.Assert.NoError(err) { + err = db.GetRaw(tt.Ctx, &cur, `SELECT COUNT(*) FROM history_ledgers`) + tt.Require.NoError(err) + tt.Assert.Equal(prev, cur, "Ledgers deleted when RetentionCount == 0") + } + + reaper.config.RetentionCount = 10 + err = reaper.DeleteUnretainedHistory(tt.Ctx) + if tt.Assert.NoError(err) { + err = db.GetRaw(tt.Ctx, &cur, `SELECT COUNT(*) FROM history_ledgers`) + tt.Require.NoError(err) + tt.Assert.Equal(10, cur) + } + + reaper.config.RetentionCount = 1 + err = reaper.DeleteUnretainedHistory(tt.Ctx) + if tt.Assert.NoError(err) { + err = db.GetRaw(tt.Ctx, &cur, `SELECT COUNT(*) FROM history_ledgers`) + tt.Require.NoError(err) + tt.Assert.Equal(1, cur) + } +} + +type ReaperTestSuite struct { + suite.Suite + ctx context.Context + historyQ *mockDBQ + reapLockQ *mockDBQ + reaper *Reaper + prevSleep time.Duration +} + +func TestReaper(t *testing.T) { + suite.Run(t, new(ReaperTestSuite)) +} + +func (t *ReaperTestSuite) SetupTest() { + t.ctx = context.Background() + t.historyQ = &mockDBQ{} + t.reapLockQ = &mockDBQ{} + t.reaper = newReaper(ReapConfig{ + RetentionCount: 30, + BatchSize: 10, + Frequency: 7, + }, t.historyQ, t.reapLockQ) + t.prevSleep = sleep + sleep = 0 +} + +func (t *ReaperTestSuite) TearDownTest() { + t.historyQ.AssertExpectations(t.T()) + t.reapLockQ.AssertExpectations(t.T()) + sleep = t.prevSleep +} + +func (t *ReaperTestSuite) TestDisabled() { + t.reaper.config.RetentionCount = 0 + t.Assert().NoError(t.reaper.DeleteUnretainedHistory(t.ctx)) +} + +func assertMocksInOrder(calls ...*mock.Call) { + for i := len(calls) - 1; i > 0; i-- { + calls[i].NotBefore(calls[i-1]) + } +} + +func (t *ReaperTestSuite) TestInProgressOnOtherNode() { + assertMocksInOrder( + t.reapLockQ.On("Begin", t.ctx).Return(nil).Once(), + t.reapLockQ.On("TryReaperLock", t.ctx).Return(false, nil).Once(), + t.reapLockQ.On("Rollback").Return(nil).Once(), + ) + t.Assert().NoError(t.reaper.DeleteUnretainedHistory(t.ctx)) +} + +func (t *ReaperTestSuite) TestInProgress() { + t.reapLockQ.On("Begin", t.ctx).Return(fmt.Errorf("transient error")).Once().Run( + func(args mock.Arguments) { + t.Assert().NoError(t.reaper.DeleteUnretainedHistory(t.ctx)) + }, + ) + t.Assert().EqualError( + t.reaper.DeleteUnretainedHistory(t.ctx), + "error while starting reaper lock transaction: transient error", + ) +} + +func (t *ReaperTestSuite) TestReaperInvokedOnMatchingLedger() { + s := &system{ + ctx: t.ctx, + reaper: t.reaper, + } + assertMocksInOrder( + t.reapLockQ.On("Begin", t.ctx).Return(nil).Once(), + t.reapLockQ.On("TryReaperLock", t.ctx).Return(false, nil).Once(), + t.reapLockQ.On("Rollback").Return(nil).Once(), + ) + s.maybeReapHistory(49) + s.wg.Wait() +} + +func (t *ReaperTestSuite) TestReaperIgnoredOnMismatchingLedger() { + s := &system{ + ctx: t.ctx, + reaper: t.reaper, + } + s.maybeReapHistory(48) + s.wg.Wait() +} + +func (t *ReaperTestSuite) TestLatestLedgerTooSmall() { + assertMocksInOrder( + t.reapLockQ.On("Begin", t.ctx).Return(nil).Once(), + t.reapLockQ.On("TryReaperLock", t.ctx).Return(true, nil).Once(), + t.historyQ.On("GetLatestHistoryLedger", t.ctx).Return(uint32(30), nil).Once(), + t.historyQ.On("ElderLedger", t.ctx, mock.AnythingOfType("*uint32")). + Return(nil).Once().Run( + func(args mock.Arguments) { + ledger := args.Get(1).(*uint32) + *ledger = 1 + }), + t.reapLockQ.On("Rollback").Return(nil).Once(), + ) + t.Assert().NoError(t.reaper.DeleteUnretainedHistory(t.ctx)) +} + +func (t *ReaperTestSuite) TestNotEnoughHistory() { + assertMocksInOrder( + t.reapLockQ.On("Begin", t.ctx).Return(nil).Once(), + t.reapLockQ.On("TryReaperLock", t.ctx).Return(true, nil).Once(), + t.historyQ.On("GetLatestHistoryLedger", t.ctx).Return(uint32(90), nil).Once(), + t.historyQ.On("ElderLedger", t.ctx, mock.AnythingOfType("*uint32")). + Return(nil).Once().Run( + func(args mock.Arguments) { + ledger := args.Get(1).(*uint32) + *ledger = 85 + }), + t.reapLockQ.On("Rollback").Return(nil).Once(), + ) + t.Assert().NoError(t.reaper.DeleteUnretainedHistory(t.ctx)) +} + +func (t *ReaperTestSuite) TestSucceeds() { + assertMocksInOrder( + t.reapLockQ.On("Begin", t.ctx).Return(nil).Once(), + t.reapLockQ.On("TryReaperLock", t.ctx).Return(true, nil).Once(), + t.historyQ.On("GetLatestHistoryLedger", t.ctx).Return(uint32(90), nil).Once(), + t.historyQ.On("ElderLedger", t.ctx, mock.AnythingOfType("*uint32")). + Return(nil).Once().Run( + func(args mock.Arguments) { + ledger := args.Get(1).(*uint32) + *ledger = 55 + }), + t.historyQ.On("Begin", t.ctx).Return(nil).Once(), + t.historyQ.On("DeleteRangeAll", t.ctx, + toid.New(55, 0, 0).ToInt64(), toid.New(61, 0, 0).ToInt64(), + ).Return(int64(400), nil).Once(), + t.historyQ.On("Commit").Return(nil).Once(), + t.historyQ.On("Rollback").Return(nil).Once(), + t.reapLockQ.On("Rollback").Return(nil).Once(), + ) + t.Assert().NoError(t.reaper.DeleteUnretainedHistory(t.ctx)) +} + +func (t *ReaperTestSuite) TestFails() { + assertMocksInOrder( + t.reapLockQ.On("Begin", t.ctx).Return(nil).Once(), + t.reapLockQ.On("TryReaperLock", t.ctx).Return(true, nil).Once(), + t.historyQ.On("GetLatestHistoryLedger", t.ctx).Return(uint32(90), nil).Once(), + t.historyQ.On("ElderLedger", t.ctx, mock.AnythingOfType("*uint32")). + Return(nil).Once().Run( + func(args mock.Arguments) { + ledger := args.Get(1).(*uint32) + *ledger = 2 + }), + t.historyQ.On("Begin", t.ctx).Return(nil).Once(), + t.historyQ.On("DeleteRangeAll", t.ctx, + toid.New(2, 0, 0).ToInt64(), toid.New(13, 0, 0).ToInt64(), + ).Return(int64(0), fmt.Errorf("transient error")).Once(), + t.historyQ.On("Rollback").Return(nil).Once(), + t.reapLockQ.On("Rollback").Return(nil).Once(), + ) + t.Assert().EqualError(t.reaper.DeleteUnretainedHistory(t.ctx), "Error in DeleteRangeAll: transient error") +} + +func (t *ReaperTestSuite) TestPartiallySucceeds() { + assertMocksInOrder( + t.reapLockQ.On("Begin", t.ctx).Return(nil).Once(), + t.reapLockQ.On("TryReaperLock", t.ctx).Return(true, nil).Once(), + t.historyQ.On("GetLatestHistoryLedger", t.ctx).Return(uint32(90), nil).Once(), + t.historyQ.On("ElderLedger", t.ctx, mock.AnythingOfType("*uint32")). + Return(nil).Once().Run( + func(args mock.Arguments) { + ledger := args.Get(1).(*uint32) + *ledger = 30 + }), + + t.historyQ.On("Begin", t.ctx).Return(nil).Once(), + t.historyQ.On("DeleteRangeAll", t.ctx, + toid.New(30, 0, 0).ToInt64(), toid.New(41, 0, 0).ToInt64(), + ).Return(int64(200), nil).Once(), + t.historyQ.On("Commit").Return(nil).Once(), + t.historyQ.On("Rollback").Return(nil).Once(), + + t.historyQ.On("Begin", t.ctx).Return(nil).Once(), + t.historyQ.On("DeleteRangeAll", t.ctx, + toid.New(41, 0, 0).ToInt64(), toid.New(52, 0, 0).ToInt64(), + ).Return(int64(0), fmt.Errorf("transient error")).Once(), + t.historyQ.On("Rollback").Return(nil).Once(), + + t.reapLockQ.On("Rollback").Return(nil).Once(), + ) + t.Assert().EqualError(t.reaper.DeleteUnretainedHistory(t.ctx), "Error in DeleteRangeAll: transient error") +} + +func (t *ReaperTestSuite) TestSucceedsOnMultipleBatches() { + assertMocksInOrder( + t.reapLockQ.On("Begin", t.ctx).Return(nil).Once(), + t.reapLockQ.On("TryReaperLock", t.ctx).Return(true, nil).Once(), + t.historyQ.On("GetLatestHistoryLedger", t.ctx).Return(uint32(90), nil).Once(), + t.historyQ.On("ElderLedger", t.ctx, mock.AnythingOfType("*uint32")). + Return(nil).Once().Run( + func(args mock.Arguments) { + ledger := args.Get(1).(*uint32) + *ledger = 35 + }), + + t.historyQ.On("Begin", t.ctx).Return(nil).Once(), + t.historyQ.On("DeleteRangeAll", t.ctx, + toid.New(35, 0, 0).ToInt64(), toid.New(46, 0, 0).ToInt64(), + ).Return(int64(200), nil).Once(), + t.historyQ.On("Commit").Return(nil).Once(), + t.historyQ.On("Rollback").Return(nil).Once(), + + t.historyQ.On("Begin", t.ctx).Return(nil).Once(), + t.historyQ.On("DeleteRangeAll", t.ctx, + toid.New(46, 0, 0).ToInt64(), toid.New(57, 0, 0).ToInt64(), + ).Return(int64(150), nil).Once(), + t.historyQ.On("Commit").Return(nil).Once(), + t.historyQ.On("Rollback").Return(nil).Once(), + + t.historyQ.On("Begin", t.ctx).Return(nil).Once(), + t.historyQ.On("DeleteRangeAll", t.ctx, + toid.New(57, 0, 0).ToInt64(), toid.New(61, 0, 0).ToInt64(), + ).Return(int64(80), nil).Once(), + t.historyQ.On("Commit").Return(nil).Once(), + t.historyQ.On("Rollback").Return(nil).Once(), + + t.reapLockQ.On("Rollback").Return(nil).Once(), + ) + t.Assert().NoError(t.reaper.DeleteUnretainedHistory(t.ctx)) +} + +func (t *ReaperTestSuite) TestSkipGap() { + assertMocksInOrder( + t.reapLockQ.On("Begin", t.ctx).Return(nil).Once(), + t.reapLockQ.On("TryReaperLock", t.ctx).Return(true, nil).Once(), + t.historyQ.On("GetLatestHistoryLedger", t.ctx).Return(uint32(90), nil).Once(), + t.historyQ.On("ElderLedger", t.ctx, mock.AnythingOfType("*uint32")). + Return(nil).Once().Run( + func(args mock.Arguments) { + ledger := args.Get(1).(*uint32) + *ledger = 2 + }), + + t.historyQ.On("Begin", t.ctx).Return(nil).Once(), + t.historyQ.On("DeleteRangeAll", t.ctx, + toid.New(2, 0, 0).ToInt64(), toid.New(13, 0, 0).ToInt64(), + ).Return(int64(200), nil).Once(), + t.historyQ.On("Commit").Return(nil).Once(), + t.historyQ.On("Rollback").Return(nil).Once(), + + t.historyQ.On("Begin", t.ctx).Return(nil).Once(), + t.historyQ.On("DeleteRangeAll", t.ctx, + toid.New(13, 0, 0).ToInt64(), toid.New(24, 0, 0).ToInt64(), + ).Return(int64(0), nil).Once(), + t.historyQ.On("Commit").Return(nil).Once(), + t.historyQ.On("Rollback").Return(nil).Once(), + t.historyQ.On("GetNextLedgerSequence", t.ctx, uint32(13)).Return(uint32(55), true, nil).Once(), + + t.historyQ.On("Begin", t.ctx).Return(nil).Once(), + t.historyQ.On("DeleteRangeAll", t.ctx, + toid.New(55, 0, 0).ToInt64(), toid.New(61, 0, 0).ToInt64(), + ).Return(int64(20), nil).Once(), + t.historyQ.On("Commit").Return(nil).Once(), + t.historyQ.On("Rollback").Return(nil).Once(), + + t.reapLockQ.On("Rollback").Return(nil).Once(), + ) + t.Assert().NoError(t.reaper.DeleteUnretainedHistory(t.ctx)) +} + +func (t *ReaperTestSuite) TestSkipGapTerminatesEarly() { + assertMocksInOrder( + t.reapLockQ.On("Begin", t.ctx).Return(nil).Once(), + t.reapLockQ.On("TryReaperLock", t.ctx).Return(true, nil).Once(), + t.historyQ.On("GetLatestHistoryLedger", t.ctx).Return(uint32(90), nil).Once(), + t.historyQ.On("ElderLedger", t.ctx, mock.AnythingOfType("*uint32")). + Return(nil).Once().Run( + func(args mock.Arguments) { + ledger := args.Get(1).(*uint32) + *ledger = 2 + }), + + t.historyQ.On("Begin", t.ctx).Return(nil).Once(), + t.historyQ.On("DeleteRangeAll", t.ctx, + toid.New(2, 0, 0).ToInt64(), toid.New(13, 0, 0).ToInt64(), + ).Return(int64(200), nil).Once(), + t.historyQ.On("Commit").Return(nil).Once(), + t.historyQ.On("Rollback").Return(nil).Once(), + + t.historyQ.On("Begin", t.ctx).Return(nil).Once(), + t.historyQ.On("DeleteRangeAll", t.ctx, + toid.New(13, 0, 0).ToInt64(), toid.New(24, 0, 0).ToInt64(), + ).Return(int64(0), nil).Once(), + t.historyQ.On("Commit").Return(nil).Once(), + t.historyQ.On("Rollback").Return(nil).Once(), + t.historyQ.On("GetNextLedgerSequence", t.ctx, uint32(13)).Return(uint32(65), true, nil).Once(), + + t.reapLockQ.On("Rollback").Return(nil).Once(), + ) + t.Assert().NoError(t.reaper.DeleteUnretainedHistory(t.ctx)) +} diff --git a/services/horizon/internal/ingest/resume_state_test.go b/services/horizon/internal/ingest/resume_state_test.go index 985391883f..5167eb195a 100644 --- a/services/horizon/internal/ingest/resume_state_test.go +++ b/services/horizon/internal/ingest/resume_state_test.go @@ -45,6 +45,7 @@ func (s *ResumeTestTestSuite) SetupTest() { ledgerBackend: s.ledgerBackend, stellarCoreClient: s.stellarCoreClient, runStateVerificationOnLedger: ledgerEligibleForStateVerification(64, 1), + reaper: &Reaper{}, } s.system.initMetrics() diff --git a/services/horizon/internal/init.go b/services/horizon/internal/init.go index a221c00682..137ff26aff 100644 --- a/services/horizon/internal/init.go +++ b/services/horizon/internal/init.go @@ -103,6 +103,11 @@ func initIngester(app *App) { EnableExtendedLogLedgerStats: app.config.IngestEnableExtendedLogLedgerStats, RoundingSlippageFilter: app.config.RoundingSlippageFilter, SkipTxmeta: app.config.SkipTxmeta, + ReapConfig: ingest.ReapConfig{ + Frequency: app.config.ReapFrequency, + RetentionCount: uint32(app.config.HistoryRetentionCount), + BatchSize: uint32(app.config.HistoryRetentionReapCount), + }, }) if err != nil { diff --git a/services/horizon/internal/integration/db_test.go b/services/horizon/internal/integration/db_test.go index 331f99ee27..1f1d2277ec 100644 --- a/services/horizon/internal/integration/db_test.go +++ b/services/horizon/internal/integration/db_test.go @@ -835,7 +835,8 @@ func TestFillGaps(t *testing.T) { var oldestLedger, latestLedger int64 tt.NoError(historyQ.ElderLedger(context.Background(), &oldestLedger)) tt.NoError(historyQ.LatestLedger(context.Background(), &latestLedger)) - tt.NoError(historyQ.DeleteRangeAll(context.Background(), oldestLedger, latestLedger)) + _, err = historyQ.DeleteRangeAll(context.Background(), oldestLedger, latestLedger) + tt.NoError(err) horizonConfig.CaptiveCoreConfigPath = filepath.Join( filepath.Dir(horizonConfig.CaptiveCoreConfigPath), diff --git a/services/horizon/internal/reap/main.go b/services/horizon/internal/reap/main.go deleted file mode 100644 index 7d462e7e52..0000000000 --- a/services/horizon/internal/reap/main.go +++ /dev/null @@ -1,41 +0,0 @@ -// Package reap contains the history reaping subsystem for horizon. This system -// is designed to remove data from the history database such that it does not -// grow indefinitely. The system can be configured with a number of ledgers to -// maintain at a minimum. -package reap - -import ( - "context" - - "github.com/stellar/go/services/horizon/internal/db2/history" - "github.com/stellar/go/services/horizon/internal/ledger" - "github.com/stellar/go/support/db" -) - -// System represents the history reaping subsystem of horizon. -type System struct { - HistoryQ *history.Q - RetentionCount uint - RetentionBatch uint - - ledgerState *ledger.State - ctx context.Context - cancel context.CancelFunc -} - -// New initializes the reaper, causing it to begin polling the stellar-core -// database for now ledgers and ingesting data into the horizon database. -func New(retention, retentionBatchSize uint, dbSession db.SessionInterface, ledgerState *ledger.State) *System { - ctx, cancel := context.WithCancel(context.Background()) - - r := &System{ - HistoryQ: &history.Q{dbSession.Clone()}, - RetentionCount: retention, - RetentionBatch: retentionBatchSize, - ledgerState: ledgerState, - ctx: ctx, - cancel: cancel, - } - - return r -} diff --git a/services/horizon/internal/reap/system.go b/services/horizon/internal/reap/system.go deleted file mode 100644 index f08dae37a7..0000000000 --- a/services/horizon/internal/reap/system.go +++ /dev/null @@ -1,125 +0,0 @@ -package reap - -import ( - "context" - "fmt" - "time" - - herrors "github.com/stellar/go/services/horizon/internal/errors" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/log" - "github.com/stellar/go/toid" -) - -// DeleteUnretainedHistory removes all data associated with unretained ledgers. -func (r *System) DeleteUnretainedHistory(ctx context.Context) error { - // RetentionCount of 0 indicates "keep all history" - if r.RetentionCount == 0 { - return nil - } - - var ( - latest = r.ledgerState.CurrentStatus() - targetElder = (latest.HistoryLatest - int32(r.RetentionCount)) + 1 - ) - - if targetElder < latest.HistoryElder { - return nil - } - - err := r.clearBefore(ctx, latest.HistoryElder, targetElder) - if err != nil { - return err - } - - log. - WithField("new_elder", targetElder). - Info("reaper succeeded") - - return nil -} - -// Run triggers the reaper system to update itself, deleted unretained history -// if it is the appropriate time. -func (r *System) Run() { - for { - select { - case <-time.After(1 * time.Hour): - r.runOnce(r.ctx) - case <-r.ctx.Done(): - return - } - } -} - -func (r *System) Shutdown() { - r.cancel() -} - -func (r *System) runOnce(ctx context.Context) { - defer func() { - if rec := recover(); rec != nil { - err := herrors.FromPanic(rec) - log.Errorf("reaper panicked: %s", err) - herrors.ReportToSentry(err, nil) - } - }() - - err := r.DeleteUnretainedHistory(ctx) - if err != nil { - log.Errorf("reaper failed: %s", err) - } -} - -// Work backwards in 50k (by default, otherwise configurable via the CLI) ledger -// blocks to prevent using all the CPU. -// -// This runs every hour, so we need to make sure it doesn't run for longer than -// an hour. -// -// Current ledger at 2024-04-04s is 51,092,283, so 50k means 1021 batches. At 1 -// batch/second, that seems like a reasonable balance between running under an -// hour, and slowing it down enough to leave some CPU for other processes. -var sleep = 1 * time.Second - -func (r *System) clearBefore(ctx context.Context, startSeq, endSeq int32) error { - batchSize := int32(r.RetentionBatch) - if batchSize <= 0 { - return fmt.Errorf("invalid batch size for reaping (%d)", batchSize) - } - - for batchEndSeq := endSeq - 1; batchEndSeq >= startSeq; batchEndSeq -= batchSize { - batchStartSeq := batchEndSeq - batchSize - if batchStartSeq < startSeq { - batchStartSeq = startSeq - } - log.WithField("start_ledger", batchStartSeq). - WithField("end_ledger", batchEndSeq). - Info("reaper: clearing") - - batchStart, batchEnd, err := toid.LedgerRangeInclusive(batchStartSeq, batchEndSeq) - if err != nil { - return err - } - - err = r.HistoryQ.Begin(ctx) - if err != nil { - return errors.Wrap(err, "Error in begin") - } - defer r.HistoryQ.Rollback() - - err = r.HistoryQ.DeleteRangeAll(ctx, batchStart, batchEnd) - if err != nil { - return errors.Wrap(err, "Error in DeleteRangeAll") - } - - err = r.HistoryQ.Commit() - if err != nil { - return errors.Wrap(err, "Error in commit") - } - - time.Sleep(sleep) - } - - return nil -} diff --git a/services/horizon/internal/reap/system_test.go b/services/horizon/internal/reap/system_test.go deleted file mode 100644 index 1595171d89..0000000000 --- a/services/horizon/internal/reap/system_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package reap - -import ( - "testing" - - "github.com/stellar/go/services/horizon/internal/ledger" - "github.com/stellar/go/services/horizon/internal/test" -) - -func TestDeleteUnretainedHistory(t *testing.T) { - tt := test.Start(t) - defer tt.Finish() - ledgerState := &ledger.State{} - ledgerState.SetStatus(tt.Scenario("kahuna")) - - db := tt.HorizonSession() - - sys := New(0, 50, db, ledgerState) - - // Disable sleeps for this. - sleep = 0 - - var ( - prev int - cur int - ) - err := db.GetRaw(tt.Ctx, &prev, `SELECT COUNT(*) FROM history_ledgers`) - tt.Require.NoError(err) - - err = sys.DeleteUnretainedHistory(tt.Ctx) - if tt.Assert.NoError(err) { - err = db.GetRaw(tt.Ctx, &cur, `SELECT COUNT(*) FROM history_ledgers`) - tt.Require.NoError(err) - tt.Assert.Equal(prev, cur, "Ledgers deleted when RetentionCount == 0") - } - - ledgerState.SetStatus(tt.LoadLedgerStatus()) - sys.RetentionCount = 10 - err = sys.DeleteUnretainedHistory(tt.Ctx) - if tt.Assert.NoError(err) { - err = db.GetRaw(tt.Ctx, &cur, `SELECT COUNT(*) FROM history_ledgers`) - tt.Require.NoError(err) - tt.Assert.Equal(10, cur) - } - - ledgerState.SetStatus(tt.LoadLedgerStatus()) - sys.RetentionCount = 1 - err = sys.DeleteUnretainedHistory(tt.Ctx) - if tt.Assert.NoError(err) { - err = db.GetRaw(tt.Ctx, &cur, `SELECT COUNT(*) FROM history_ledgers`) - tt.Require.NoError(err) - tt.Assert.Equal(1, cur) - } -} diff --git a/support/db/main.go b/support/db/main.go index 4b0b4c8b84..06cdacee5a 100644 --- a/support/db/main.go +++ b/support/db/main.go @@ -154,7 +154,7 @@ type SessionInterface interface { start, end int64, table string, idCol string, - ) error + ) (int64, error) } // Table helps to build sql queries against a given table. It logically diff --git a/support/db/metrics.go b/support/db/metrics.go index 5abfe3013a..70baed3a80 100644 --- a/support/db/metrics.go +++ b/support/db/metrics.go @@ -520,7 +520,7 @@ func (s *SessionWithMetrics) DeleteRange( start, end int64, table string, idCol string, -) (err error) { +) (count int64, err error) { queryType := "delete" timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) { s.queryDurationSummary.With(prometheus.Labels{ @@ -538,6 +538,6 @@ func (s *SessionWithMetrics) DeleteRange( }).Inc() }() - err = s.SessionInterface.DeleteRange(ctx, start, end, table, idCol) - return err + count, err = s.SessionInterface.DeleteRange(ctx, start, end, table, idCol) + return count, err } diff --git a/support/db/mock_session.go b/support/db/mock_session.go index e6cb58c987..889e454a34 100644 --- a/support/db/mock_session.go +++ b/support/db/mock_session.go @@ -125,6 +125,7 @@ func (m *MockSession) DeleteRange( start, end int64, table string, idCol string, -) (err error) { - return m.Called(ctx, start, end, table, idCol).Error(0) +) (int64, error) { + args := m.Called(ctx, start, end, table, idCol) + return args.Get(0).(int64), args.Error(1) } diff --git a/support/db/session.go b/support/db/session.go index dc3c98cafb..1ace39c941 100644 --- a/support/db/session.go +++ b/support/db/session.go @@ -142,14 +142,17 @@ func (s *Session) DeleteRange( start, end int64, table string, idCol string, -) error { +) (int64, error) { del := sq.Delete(table).Where( fmt.Sprintf("%s >= ? AND %s < ?", idCol, idCol), start, end, ) - _, err := s.Exec(ctx, del) - return err + result, err := s.Exec(ctx, del) + if err != nil { + return 0, err + } + return result.RowsAffected() } // Get runs `query`, setting the first result found on `dest`, if From 132b84c7a5f927035f6a7a0e0eab996d6c8271ca Mon Sep 17 00:00:00 2001 From: urvisavla Date: Mon, 24 Jun 2024 11:17:16 -0700 Subject: [PATCH 195/234] exp/lighthorizon: delete lighthorizon (#5334) --- Makefile | 3 +- exp/lighthorizon/actions/accounts.go | 142 ------ exp/lighthorizon/actions/accounts_test.go | 191 ------- exp/lighthorizon/actions/apidocs.go | 26 - exp/lighthorizon/actions/main.go | 124 ----- exp/lighthorizon/actions/problem.go | 94 ---- exp/lighthorizon/actions/root.go | 29 -- exp/lighthorizon/actions/static/api_docs.yml | 228 --------- exp/lighthorizon/adapters/account_merge.go | 21 - exp/lighthorizon/adapters/allow_trust.go | 43 -- .../begin_sponsoring_future_reserves.go | 15 - exp/lighthorizon/adapters/bump_sequence.go | 17 - exp/lighthorizon/adapters/change_trust.go | 63 --- .../adapters/claim_claimable_balance.go | 26 - exp/lighthorizon/adapters/clawback.go | 37 -- .../adapters/clawback_claimable_balance.go | 22 - exp/lighthorizon/adapters/create_account.go | 18 - .../adapters/create_claimable_balance.go | 27 - .../adapters/create_passive_sell_offer.go | 51 -- .../end_sponsoring_future_reserves.go | 38 -- exp/lighthorizon/adapters/inflation.go | 12 - .../adapters/liquidity_pool_deposit.go | 33 -- .../adapters/liquidity_pool_withdraw.go | 24 - exp/lighthorizon/adapters/manage_buy_offer.go | 52 -- exp/lighthorizon/adapters/manage_data.go | 23 - .../adapters/manage_sell_offer.go | 52 -- exp/lighthorizon/adapters/operation.go | 93 ---- .../adapters/path_payment_strict_receive.go | 78 --- .../adapters/path_payment_strict_send.go | 78 --- exp/lighthorizon/adapters/payment.go | 35 -- .../adapters/revoke_sponsorship.go | 66 --- exp/lighthorizon/adapters/set_options.go | 122 ----- .../adapters/set_trust_line_flags.go | 83 --- .../adapters/testdata/transactions.json | 67 --- exp/lighthorizon/adapters/transaction.go | 295 ----------- exp/lighthorizon/adapters/transaction_test.go | 81 --- exp/lighthorizon/build/README.md | 24 - exp/lighthorizon/build/build.sh | 56 -- exp/lighthorizon/build/index-batch/Dockerfile | 20 - exp/lighthorizon/build/index-batch/README.md | 7 - exp/lighthorizon/build/index-batch/start | 17 - .../build/index-single/Dockerfile | 25 - exp/lighthorizon/build/k8s/ledgerexporter.yml | 125 ----- .../build/k8s/lighthorizon_batch_map_job.yml | 43 -- .../k8s/lighthorizon_batch_reduce_job.yml | 42 -- .../build/k8s/lighthorizon_index.yml | 74 --- .../build/k8s/lighthorizon_web.yml | 133 ----- .../build/ledgerexporter/Dockerfile | 33 -- .../build/ledgerexporter/README.md | 42 -- .../ledgerexporter/captive-core-pubnet.cfg | 200 -------- .../ledgerexporter/captive-core-testnet.cfg | 32 -- exp/lighthorizon/build/ledgerexporter/start | 55 -- exp/lighthorizon/build/web/Dockerfile | 24 - exp/lighthorizon/common/operation.go | 52 -- exp/lighthorizon/common/transaction.go | 70 --- exp/lighthorizon/http.go | 78 --- exp/lighthorizon/http_test.go | 64 --- exp/lighthorizon/index/Makefile | 24 - exp/lighthorizon/index/backend/backend.go | 14 - exp/lighthorizon/index/backend/file.go | 214 -------- exp/lighthorizon/index/backend/file_test.go | 43 -- exp/lighthorizon/index/backend/gzip.go | 74 --- exp/lighthorizon/index/backend/gzip_test.go | 61 --- .../index/backend/parallel_flush.go | 73 --- exp/lighthorizon/index/backend/s3.go | 220 -------- exp/lighthorizon/index/builder.go | 366 -------------- exp/lighthorizon/index/cmd/batch/doc.go | 52 -- exp/lighthorizon/index/cmd/batch/map/main.go | 144 ------ .../index/cmd/batch/reduce/main.go | 389 -------------- exp/lighthorizon/index/cmd/map.sh | 96 ---- exp/lighthorizon/index/cmd/mapreduce_test.go | 232 --------- exp/lighthorizon/index/cmd/reduce.sh | 75 --- exp/lighthorizon/index/cmd/single/main.go | 59 --- exp/lighthorizon/index/cmd/single_test.go | 279 ---------- exp/lighthorizon/index/cmd/testdata/latest | 1 - .../index/cmd/testdata/ledgers/1410048 | Bin 4160 -> 0 bytes .../index/cmd/testdata/ledgers/1410049 | Bin 5340 -> 0 bytes .../index/cmd/testdata/ledgers/1410050 | Bin 5136 -> 0 bytes .../index/cmd/testdata/ledgers/1410051 | Bin 4872 -> 0 bytes .../index/cmd/testdata/ledgers/1410052 | Bin 5052 -> 0 bytes .../index/cmd/testdata/ledgers/1410053 | Bin 3896 -> 0 bytes .../index/cmd/testdata/ledgers/1410054 | Bin 2820 -> 0 bytes .../index/cmd/testdata/ledgers/1410055 | Bin 2968 -> 0 bytes .../index/cmd/testdata/ledgers/1410056 | Bin 6084 -> 0 bytes .../index/cmd/testdata/ledgers/1410057 | Bin 4876 -> 0 bytes .../index/cmd/testdata/ledgers/1410058 | Bin 4876 -> 0 bytes .../index/cmd/testdata/ledgers/1410059 | Bin 4876 -> 0 bytes .../index/cmd/testdata/ledgers/1410060 | Bin 4084 -> 0 bytes .../index/cmd/testdata/ledgers/1410061 | Bin 5944 -> 0 bytes .../index/cmd/testdata/ledgers/1410062 | Bin 6732 -> 0 bytes .../index/cmd/testdata/ledgers/1410063 | Bin 6004 -> 0 bytes .../index/cmd/testdata/ledgers/1410064 | Bin 5940 -> 0 bytes .../index/cmd/testdata/ledgers/1410065 | Bin 4992 -> 0 bytes .../index/cmd/testdata/ledgers/1410066 | Bin 4004 -> 0 bytes .../index/cmd/testdata/ledgers/1410067 | Bin 6092 -> 0 bytes .../index/cmd/testdata/ledgers/1410068 | Bin 4864 -> 0 bytes .../index/cmd/testdata/ledgers/1410069 | Bin 4084 -> 0 bytes .../index/cmd/testdata/ledgers/1410070 | Bin 3928 -> 0 bytes .../index/cmd/testdata/ledgers/1410071 | Bin 2900 -> 0 bytes .../index/cmd/testdata/ledgers/1410072 | Bin 5316 -> 0 bytes .../index/cmd/testdata/ledgers/1410073 | Bin 5080 -> 0 bytes .../index/cmd/testdata/ledgers/1410074 | Bin 5928 -> 0 bytes .../index/cmd/testdata/ledgers/1410075 | Bin 6060 -> 0 bytes .../index/cmd/testdata/ledgers/1410076 | Bin 3876 -> 0 bytes .../index/cmd/testdata/ledgers/1410077 | Bin 3700 -> 0 bytes .../index/cmd/testdata/ledgers/1410078 | Bin 5132 -> 0 bytes .../index/cmd/testdata/ledgers/1410079 | Bin 4852 -> 0 bytes .../index/cmd/testdata/ledgers/1410080 | Bin 4704 -> 0 bytes .../index/cmd/testdata/ledgers/1410081 | Bin 6180 -> 0 bytes .../index/cmd/testdata/ledgers/1410082 | Bin 4884 -> 0 bytes .../index/cmd/testdata/ledgers/1410083 | Bin 5916 -> 0 bytes .../index/cmd/testdata/ledgers/1410084 | Bin 6220 -> 0 bytes .../index/cmd/testdata/ledgers/1410085 | Bin 5972 -> 0 bytes .../index/cmd/testdata/ledgers/1410086 | Bin 4528 -> 0 bytes .../index/cmd/testdata/ledgers/1410087 | Bin 3704 -> 0 bytes .../index/cmd/testdata/ledgers/1410088 | Bin 4048 -> 0 bytes .../index/cmd/testdata/ledgers/1410089 | Bin 5080 -> 0 bytes .../index/cmd/testdata/ledgers/1410090 | Bin 3696 -> 0 bytes .../index/cmd/testdata/ledgers/1410091 | Bin 2680 -> 0 bytes .../index/cmd/testdata/ledgers/1410092 | Bin 2904 -> 0 bytes .../index/cmd/testdata/ledgers/1410093 | Bin 4696 -> 0 bytes .../index/cmd/testdata/ledgers/1410094 | Bin 4628 -> 0 bytes .../index/cmd/testdata/ledgers/1410095 | Bin 5132 -> 0 bytes .../index/cmd/testdata/ledgers/1410096 | Bin 7196 -> 0 bytes .../index/cmd/testdata/ledgers/1410097 | Bin 6016 -> 0 bytes .../index/cmd/testdata/ledgers/1410098 | Bin 7080 -> 0 bytes .../index/cmd/testdata/ledgers/1410099 | Bin 4708 -> 0 bytes .../index/cmd/testdata/ledgers/1410100 | Bin 4844 -> 0 bytes .../index/cmd/testdata/ledgers/1410101 | Bin 3860 -> 0 bytes .../index/cmd/testdata/ledgers/1410102 | Bin 5768 -> 0 bytes .../index/cmd/testdata/ledgers/1410103 | Bin 5580 -> 0 bytes .../index/cmd/testdata/ledgers/1410104 | Bin 4964 -> 0 bytes .../index/cmd/testdata/ledgers/1410105 | Bin 4984 -> 0 bytes .../index/cmd/testdata/ledgers/1410106 | Bin 5080 -> 0 bytes .../index/cmd/testdata/ledgers/1410107 | Bin 4032 -> 0 bytes .../index/cmd/testdata/ledgers/1410108 | Bin 2968 -> 0 bytes .../index/cmd/testdata/ledgers/1410109 | Bin 5084 -> 0 bytes .../index/cmd/testdata/ledgers/1410110 | Bin 2740 -> 0 bytes .../index/cmd/testdata/ledgers/1410111 | Bin 5212 -> 0 bytes .../index/cmd/testdata/ledgers/1410112 | Bin 4884 -> 0 bytes .../index/cmd/testdata/ledgers/1410113 | Bin 4708 -> 0 bytes .../index/cmd/testdata/ledgers/1410114 | Bin 4644 -> 0 bytes .../index/cmd/testdata/ledgers/1410115 | Bin 4868 -> 0 bytes .../index/cmd/testdata/ledgers/1410116 | Bin 4696 -> 0 bytes .../index/cmd/testdata/ledgers/1410117 | Bin 5836 -> 0 bytes .../index/cmd/testdata/ledgers/1410118 | Bin 4876 -> 0 bytes .../index/cmd/testdata/ledgers/1410119 | Bin 7452 -> 0 bytes .../index/cmd/testdata/ledgers/1410120 | Bin 6060 -> 0 bytes .../index/cmd/testdata/ledgers/1410121 | Bin 5948 -> 0 bytes .../index/cmd/testdata/ledgers/1410122 | Bin 4908 -> 0 bytes .../index/cmd/testdata/ledgers/1410123 | Bin 3924 -> 0 bytes .../index/cmd/testdata/ledgers/1410124 | Bin 3844 -> 0 bytes .../index/cmd/testdata/ledgers/1410125 | Bin 2496 -> 0 bytes .../index/cmd/testdata/ledgers/1410126 | Bin 2752 -> 0 bytes .../index/cmd/testdata/ledgers/1410127 | Bin 3928 -> 0 bytes .../index/cmd/testdata/ledgers/1410128 | Bin 4960 -> 0 bytes .../index/cmd/testdata/ledgers/1410129 | Bin 3976 -> 0 bytes .../index/cmd/testdata/ledgers/1410130 | Bin 5184 -> 0 bytes .../index/cmd/testdata/ledgers/1410131 | Bin 5880 -> 0 bytes .../index/cmd/testdata/ledgers/1410132 | Bin 6120 -> 0 bytes .../index/cmd/testdata/ledgers/1410133 | Bin 5292 -> 0 bytes .../index/cmd/testdata/ledgers/1410134 | Bin 5124 -> 0 bytes .../index/cmd/testdata/ledgers/1410135 | Bin 4876 -> 0 bytes .../index/cmd/testdata/ledgers/1410136 | Bin 5036 -> 0 bytes .../index/cmd/testdata/ledgers/1410137 | Bin 5144 -> 0 bytes .../index/cmd/testdata/ledgers/1410138 | Bin 3876 -> 0 bytes .../index/cmd/testdata/ledgers/1410139 | Bin 4908 -> 0 bytes .../index/cmd/testdata/ledgers/1410140 | Bin 3924 -> 0 bytes .../index/cmd/testdata/ledgers/1410141 | Bin 3844 -> 0 bytes .../index/cmd/testdata/ledgers/1410142 | Bin 6092 -> 0 bytes .../index/cmd/testdata/ledgers/1410143 | Bin 5960 -> 0 bytes .../index/cmd/testdata/ledgers/1410144 | Bin 6080 -> 0 bytes .../index/cmd/testdata/ledgers/1410145 | Bin 3976 -> 0 bytes .../index/cmd/testdata/ledgers/1410146 | Bin 3896 -> 0 bytes .../index/cmd/testdata/ledgers/1410147 | Bin 2840 -> 0 bytes .../index/cmd/testdata/ledgers/1410148 | Bin 2920 -> 0 bytes .../index/cmd/testdata/ledgers/1410149 | Bin 2744 -> 0 bytes .../index/cmd/testdata/ledgers/1410150 | Bin 6084 -> 0 bytes .../index/cmd/testdata/ledgers/1410151 | Bin 4796 -> 0 bytes .../index/cmd/testdata/ledgers/1410152 | Bin 4748 -> 0 bytes .../index/cmd/testdata/ledgers/1410153 | Bin 6116 -> 0 bytes .../index/cmd/testdata/ledgers/1410154 | Bin 5896 -> 0 bytes .../index/cmd/testdata/ledgers/1410155 | Bin 5132 -> 0 bytes .../index/cmd/testdata/ledgers/1410156 | Bin 3844 -> 0 bytes .../index/cmd/testdata/ledgers/1410157 | Bin 3796 -> 0 bytes .../index/cmd/testdata/ledgers/1410158 | Bin 4884 -> 0 bytes .../index/cmd/testdata/ledgers/1410159 | Bin 4708 -> 0 bytes .../index/cmd/testdata/ledgers/1410160 | Bin 4644 -> 0 bytes .../index/cmd/testdata/ledgers/1410161 | Bin 7296 -> 0 bytes .../index/cmd/testdata/ledgers/1410162 | Bin 7176 -> 0 bytes .../index/cmd/testdata/ledgers/1410163 | Bin 4700 -> 0 bytes .../index/cmd/testdata/ledgers/1410164 | Bin 2920 -> 0 bytes .../index/cmd/testdata/ledgers/1410165 | Bin 4032 -> 0 bytes .../index/cmd/testdata/ledgers/1410166 | Bin 7036 -> 0 bytes .../index/cmd/testdata/ledgers/1410167 | Bin 3856 -> 0 bytes .../index/cmd/testdata/ledgers/1410168 | Bin 5648 -> 0 bytes .../index/cmd/testdata/ledgers/1410169 | Bin 5600 -> 0 bytes .../index/cmd/testdata/ledgers/1410170 | Bin 3876 -> 0 bytes .../index/cmd/testdata/ledgers/1410171 | Bin 4908 -> 0 bytes .../index/cmd/testdata/ledgers/1410172 | Bin 3924 -> 0 bytes .../index/cmd/testdata/ledgers/1410173 | Bin 3844 -> 0 bytes .../index/cmd/testdata/ledgers/1410174 | Bin 4704 -> 0 bytes .../index/cmd/testdata/ledgers/1410175 | Bin 4860 -> 0 bytes .../index/cmd/testdata/ledgers/1410176 | Bin 6248 -> 0 bytes .../index/cmd/testdata/ledgers/1410177 | Bin 7168 -> 0 bytes .../index/cmd/testdata/ledgers/1410178 | Bin 5828 -> 0 bytes .../index/cmd/testdata/ledgers/1410179 | Bin 4932 -> 0 bytes .../index/cmd/testdata/ledgers/1410180 | Bin 3844 -> 0 bytes .../index/cmd/testdata/ledgers/1410181 | Bin 2840 -> 0 bytes .../index/cmd/testdata/ledgers/1410182 | Bin 3872 -> 0 bytes .../index/cmd/testdata/ledgers/1410183 | Bin 4904 -> 0 bytes .../index/cmd/testdata/ledgers/1410184 | Bin 3920 -> 0 bytes .../index/cmd/testdata/ledgers/1410185 | Bin 3840 -> 0 bytes .../index/cmd/testdata/ledgers/1410186 | Bin 2840 -> 0 bytes .../index/cmd/testdata/ledgers/1410187 | Bin 5164 -> 0 bytes .../index/cmd/testdata/ledgers/1410188 | Bin 4908 -> 0 bytes .../index/cmd/testdata/ledgers/1410189 | Bin 7128 -> 0 bytes .../index/cmd/testdata/ledgers/1410190 | Bin 5108 -> 0 bytes .../index/cmd/testdata/ledgers/1410191 | Bin 4884 -> 0 bytes .../index/cmd/testdata/ledgers/1410192 | Bin 4708 -> 0 bytes .../index/cmd/testdata/ledgers/1410193 | Bin 4884 -> 0 bytes .../index/cmd/testdata/ledgers/1410194 | Bin 6092 -> 0 bytes .../index/cmd/testdata/ledgers/1410195 | Bin 4864 -> 0 bytes .../index/cmd/testdata/ledgers/1410196 | Bin 4992 -> 0 bytes .../index/cmd/testdata/ledgers/1410197 | Bin 4004 -> 0 bytes .../index/cmd/testdata/ledgers/1410198 | Bin 4828 -> 0 bytes .../index/cmd/testdata/ledgers/1410199 | Bin 4828 -> 0 bytes .../index/cmd/testdata/ledgers/1410200 | Bin 6368 -> 0 bytes .../index/cmd/testdata/ledgers/1410201 | Bin 4928 -> 0 bytes .../index/cmd/testdata/ledgers/1410202 | Bin 4612 -> 0 bytes .../index/cmd/testdata/ledgers/1410203 | Bin 4168 -> 0 bytes .../index/cmd/testdata/ledgers/1410204 | Bin 3992 -> 0 bytes .../index/cmd/testdata/ledgers/1410205 | Bin 6092 -> 0 bytes .../index/cmd/testdata/ledgers/1410206 | Bin 4884 -> 0 bytes .../index/cmd/testdata/ledgers/1410207 | Bin 4864 -> 0 bytes .../index/cmd/testdata/ledgers/1410208 | Bin 4992 -> 0 bytes .../index/cmd/testdata/ledgers/1410209 | Bin 5012 -> 0 bytes .../index/cmd/testdata/ledgers/1410210 | Bin 4884 -> 0 bytes .../index/cmd/testdata/ledgers/1410211 | Bin 11720 -> 0 bytes .../index/cmd/testdata/ledgers/1410212 | Bin 6068 -> 0 bytes .../index/cmd/testdata/ledgers/1410213 | Bin 6184 -> 0 bytes .../index/cmd/testdata/ledgers/1410214 | Bin 5112 -> 0 bytes .../index/cmd/testdata/ledgers/1410215 | Bin 6116 -> 0 bytes .../index/cmd/testdata/ledgers/1410216 | Bin 6016 -> 0 bytes .../index/cmd/testdata/ledgers/1410217 | Bin 3984 -> 0 bytes .../index/cmd/testdata/ledgers/1410218 | Bin 4336 -> 0 bytes .../index/cmd/testdata/ledgers/1410219 | Bin 2920 -> 0 bytes .../index/cmd/testdata/ledgers/1410220 | Bin 2900 -> 0 bytes .../index/cmd/testdata/ledgers/1410221 | Bin 3028 -> 0 bytes .../index/cmd/testdata/ledgers/1410222 | Bin 5372 -> 0 bytes .../index/cmd/testdata/ledgers/1410223 | Bin 5500 -> 0 bytes .../index/cmd/testdata/ledgers/1410224 | Bin 6136 -> 0 bytes .../index/cmd/testdata/ledgers/1410225 | Bin 6004 -> 0 bytes .../index/cmd/testdata/ledgers/1410226 | Bin 5920 -> 0 bytes .../index/cmd/testdata/ledgers/1410227 | Bin 6140 -> 0 bytes .../index/cmd/testdata/ledgers/1410228 | Bin 3844 -> 0 bytes .../index/cmd/testdata/ledgers/1410229 | Bin 4728 -> 0 bytes .../index/cmd/testdata/ledgers/1410230 | Bin 4808 -> 0 bytes .../index/cmd/testdata/ledgers/1410231 | Bin 4876 -> 0 bytes .../index/cmd/testdata/ledgers/1410232 | Bin 4796 -> 0 bytes .../index/cmd/testdata/ledgers/1410233 | Bin 5052 -> 0 bytes .../index/cmd/testdata/ledgers/1410234 | Bin 5436 -> 0 bytes .../index/cmd/testdata/ledgers/1410235 | Bin 5156 -> 0 bytes .../index/cmd/testdata/ledgers/1410236 | Bin 5044 -> 0 bytes .../index/cmd/testdata/ledgers/1410237 | Bin 3036 -> 0 bytes .../index/cmd/testdata/ledgers/1410238 | Bin 5196 -> 0 bytes .../index/cmd/testdata/ledgers/1410239 | Bin 5412 -> 0 bytes .../index/cmd/testdata/ledgers/1410240 | Bin 3280 -> 0 bytes .../index/cmd/testdata/ledgers/1410241 | Bin 5268 -> 0 bytes .../index/cmd/testdata/ledgers/1410242 | Bin 6556 -> 0 bytes .../index/cmd/testdata/ledgers/1410243 | Bin 4884 -> 0 bytes .../index/cmd/testdata/ledgers/1410244 | Bin 7236 -> 0 bytes .../index/cmd/testdata/ledgers/1410245 | Bin 7088 -> 0 bytes .../index/cmd/testdata/ledgers/1410246 | Bin 7160 -> 0 bytes .../index/cmd/testdata/ledgers/1410247 | Bin 4728 -> 0 bytes .../index/cmd/testdata/ledgers/1410248 | Bin 5928 -> 0 bytes .../index/cmd/testdata/ledgers/1410249 | Bin 5132 -> 0 bytes .../index/cmd/testdata/ledgers/1410250 | Bin 3844 -> 0 bytes .../index/cmd/testdata/ledgers/1410251 | Bin 3844 -> 0 bytes .../index/cmd/testdata/ledgers/1410252 | Bin 4148 -> 0 bytes .../index/cmd/testdata/ledgers/1410253 | Bin 5088 -> 0 bytes .../index/cmd/testdata/ledgers/1410254 | Bin 4932 -> 0 bytes .../index/cmd/testdata/ledgers/1410255 | Bin 6208 -> 0 bytes .../index/cmd/testdata/ledgers/1410256 | Bin 2864 -> 0 bytes .../index/cmd/testdata/ledgers/1410257 | Bin 3892 -> 0 bytes .../index/cmd/testdata/ledgers/1410258 | Bin 1528 -> 0 bytes .../index/cmd/testdata/ledgers/1410259 | Bin 2648 -> 0 bytes .../index/cmd/testdata/ledgers/1410260 | Bin 2736 -> 0 bytes .../index/cmd/testdata/ledgers/1410261 | Bin 2428 -> 0 bytes .../index/cmd/testdata/ledgers/1410262 | Bin 2428 -> 0 bytes .../index/cmd/testdata/ledgers/1410263 | Bin 2428 -> 0 bytes .../index/cmd/testdata/ledgers/1410264 | Bin 3588 -> 0 bytes .../index/cmd/testdata/ledgers/1410265 | Bin 1476 -> 0 bytes .../index/cmd/testdata/ledgers/1410266 | Bin 2684 -> 0 bytes .../index/cmd/testdata/ledgers/1410267 | Bin 2764 -> 0 bytes .../index/cmd/testdata/ledgers/1410268 | Bin 3876 -> 0 bytes .../index/cmd/testdata/ledgers/1410269 | Bin 7072 -> 0 bytes .../index/cmd/testdata/ledgers/1410270 | Bin 6052 -> 0 bytes .../index/cmd/testdata/ledgers/1410271 | Bin 6060 -> 0 bytes .../index/cmd/testdata/ledgers/1410272 | Bin 6276 -> 0 bytes .../index/cmd/testdata/ledgers/1410273 | Bin 4864 -> 0 bytes .../index/cmd/testdata/ledgers/1410274 | Bin 3976 -> 0 bytes .../index/cmd/testdata/ledgers/1410275 | Bin 3896 -> 0 bytes .../index/cmd/testdata/ledgers/1410276 | Bin 3896 -> 0 bytes .../index/cmd/testdata/ledgers/1410277 | Bin 5352 -> 0 bytes .../index/cmd/testdata/ledgers/1410278 | Bin 4076 -> 0 bytes .../index/cmd/testdata/ledgers/1410279 | Bin 4876 -> 0 bytes .../index/cmd/testdata/ledgers/1410280 | Bin 6020 -> 0 bytes .../index/cmd/testdata/ledgers/1410281 | Bin 4100 -> 0 bytes .../index/cmd/testdata/ledgers/1410282 | Bin 2684 -> 0 bytes .../index/cmd/testdata/ledgers/1410283 | Bin 2596 -> 0 bytes .../index/cmd/testdata/ledgers/1410284 | Bin 1476 -> 0 bytes .../index/cmd/testdata/ledgers/1410285 | Bin 2484 -> 0 bytes .../index/cmd/testdata/ledgers/1410286 | Bin 2484 -> 0 bytes .../index/cmd/testdata/ledgers/1410287 | Bin 2484 -> 0 bytes .../index/cmd/testdata/ledgers/1410288 | Bin 3692 -> 0 bytes .../index/cmd/testdata/ledgers/1410289 | Bin 2484 -> 0 bytes .../index/cmd/testdata/ledgers/1410290 | Bin 2484 -> 0 bytes .../index/cmd/testdata/ledgers/1410291 | Bin 3772 -> 0 bytes .../index/cmd/testdata/ledgers/1410292 | Bin 2708 -> 0 bytes .../index/cmd/testdata/ledgers/1410293 | Bin 6368 -> 0 bytes .../index/cmd/testdata/ledgers/1410294 | Bin 3920 -> 0 bytes .../index/cmd/testdata/ledgers/1410295 | Bin 4736 -> 0 bytes .../index/cmd/testdata/ledgers/1410296 | Bin 4076 -> 0 bytes .../index/cmd/testdata/ledgers/1410297 | Bin 2664 -> 0 bytes .../index/cmd/testdata/ledgers/1410298 | Bin 4080 -> 0 bytes .../index/cmd/testdata/ledgers/1410299 | Bin 4828 -> 0 bytes .../index/cmd/testdata/ledgers/1410300 | Bin 4148 -> 0 bytes .../index/cmd/testdata/ledgers/1410301 | Bin 3844 -> 0 bytes .../index/cmd/testdata/ledgers/1410302 | Bin 5088 -> 0 bytes .../index/cmd/testdata/ledgers/1410303 | Bin 6076 -> 0 bytes .../index/cmd/testdata/ledgers/1410304 | Bin 6376 -> 0 bytes .../index/cmd/testdata/ledgers/1410305 | Bin 8292 -> 0 bytes .../index/cmd/testdata/ledgers/1410306 | Bin 6692 -> 0 bytes .../index/cmd/testdata/ledgers/1410307 | Bin 5688 -> 0 bytes .../index/cmd/testdata/ledgers/1410308 | Bin 3228 -> 0 bytes .../index/cmd/testdata/ledgers/1410309 | Bin 2428 -> 0 bytes .../index/cmd/testdata/ledgers/1410310 | Bin 3636 -> 0 bytes .../index/cmd/testdata/ledgers/1410311 | Bin 1472 -> 0 bytes .../index/cmd/testdata/ledgers/1410312 | Bin 1472 -> 0 bytes .../index/cmd/testdata/ledgers/1410313 | Bin 520 -> 0 bytes .../index/cmd/testdata/ledgers/1410314 | Bin 1596 -> 0 bytes .../index/cmd/testdata/ledgers/1410315 | Bin 1728 -> 0 bytes .../index/cmd/testdata/ledgers/1410316 | Bin 2764 -> 0 bytes .../index/cmd/testdata/ledgers/1410317 | Bin 1476 -> 0 bytes .../index/cmd/testdata/ledgers/1410318 | Bin 4892 -> 0 bytes .../index/cmd/testdata/ledgers/1410319 | Bin 3596 -> 0 bytes .../index/cmd/testdata/ledgers/1410320 | Bin 4884 -> 0 bytes .../index/cmd/testdata/ledgers/1410321 | Bin 6140 -> 0 bytes .../index/cmd/testdata/ledgers/1410322 | Bin 4628 -> 0 bytes .../index/cmd/testdata/ledgers/1410323 | Bin 5088 -> 0 bytes .../index/cmd/testdata/ledgers/1410324 | Bin 3620 -> 0 bytes .../index/cmd/testdata/ledgers/1410325 | Bin 5032 -> 0 bytes .../index/cmd/testdata/ledgers/1410326 | Bin 5648 -> 0 bytes .../index/cmd/testdata/ledgers/1410327 | Bin 7596 -> 0 bytes .../index/cmd/testdata/ledgers/1410328 | Bin 4796 -> 0 bytes .../index/cmd/testdata/ledgers/1410329 | Bin 4244 -> 0 bytes .../index/cmd/testdata/ledgers/1410330 | Bin 3036 -> 0 bytes .../index/cmd/testdata/ledgers/1410331 | Bin 3124 -> 0 bytes .../index/cmd/testdata/ledgers/1410332 | Bin 5040 -> 0 bytes .../index/cmd/testdata/ledgers/1410333 | Bin 3608 -> 0 bytes .../index/cmd/testdata/ledgers/1410334 | Bin 3660 -> 0 bytes .../index/cmd/testdata/ledgers/1410335 | Bin 4236 -> 0 bytes .../index/cmd/testdata/ledgers/1410336 | Bin 2484 -> 0 bytes .../index/cmd/testdata/ledgers/1410337 | Bin 4768 -> 0 bytes .../index/cmd/testdata/ledgers/1410338 | Bin 2484 -> 0 bytes .../index/cmd/testdata/ledgers/1410339 | Bin 3772 -> 0 bytes .../index/cmd/testdata/ledgers/1410340 | Bin 1476 -> 0 bytes .../index/cmd/testdata/ledgers/1410341 | Bin 3548 -> 0 bytes .../index/cmd/testdata/ledgers/1410342 | Bin 2428 -> 0 bytes .../index/cmd/testdata/ledgers/1410343 | Bin 3636 -> 0 bytes .../index/cmd/testdata/ledgers/1410344 | Bin 2428 -> 0 bytes .../index/cmd/testdata/ledgers/1410345 | Bin 2764 -> 0 bytes .../index/cmd/testdata/ledgers/1410346 | Bin 3876 -> 0 bytes .../index/cmd/testdata/ledgers/1410347 | Bin 3700 -> 0 bytes .../index/cmd/testdata/ledgers/1410348 | Bin 5252 -> 0 bytes .../index/cmd/testdata/ledgers/1410349 | Bin 2888 -> 0 bytes .../index/cmd/testdata/ledgers/1410350 | Bin 5600 -> 0 bytes .../index/cmd/testdata/ledgers/1410351 | Bin 3280 -> 0 bytes .../index/cmd/testdata/ledgers/1410352 | Bin 3936 -> 0 bytes .../index/cmd/testdata/ledgers/1410353 | Bin 6092 -> 0 bytes .../index/cmd/testdata/ledgers/1410354 | Bin 4708 -> 0 bytes .../index/cmd/testdata/ledgers/1410355 | Bin 6060 -> 0 bytes .../index/cmd/testdata/ledgers/1410356 | Bin 5804 -> 0 bytes .../index/cmd/testdata/ledgers/1410357 | Bin 4728 -> 0 bytes .../index/cmd/testdata/ledgers/1410358 | Bin 4808 -> 0 bytes .../index/cmd/testdata/ledgers/1410359 | Bin 6084 -> 0 bytes .../index/cmd/testdata/ledgers/1410360 | Bin 4920 -> 0 bytes .../index/cmd/testdata/ledgers/1410361 | Bin 3844 -> 0 bytes .../index/cmd/testdata/ledgers/1410362 | Bin 5436 -> 0 bytes .../index/cmd/testdata/ledgers/1410363 | Bin 4080 -> 0 bytes .../index/cmd/testdata/ledgers/1410364 | Bin 6252 -> 0 bytes .../index/cmd/testdata/ledgers/1410365 | Bin 5000 -> 0 bytes .../index/cmd/testdata/ledgers/1410366 | Bin 4996 -> 0 bytes .../index/cmd/testdata/ledgers/1410367 | Bin 6884 -> 0 bytes .../index/cmd/testdata/regenerate.sh | 3 - exp/lighthorizon/index/connect.go | 68 --- exp/lighthorizon/index/mock_store.go | 78 --- exp/lighthorizon/index/modules.go | 314 ------------ exp/lighthorizon/index/store.go | 377 -------------- exp/lighthorizon/index/types/bitmap.go | 367 -------------- exp/lighthorizon/index/types/bitmap_test.go | 382 -------------- exp/lighthorizon/index/types/trie.go | 345 ------------- exp/lighthorizon/index/types/trie_test.go | 297 ----------- exp/lighthorizon/ingester/ingester.go | 55 -- exp/lighthorizon/ingester/main.go | 87 ---- exp/lighthorizon/ingester/mock_ingester.go | 44 -- .../ingester/parallel_ingester.go | 141 ------ exp/lighthorizon/ingester/participants.go | 35 -- exp/lighthorizon/main.go | 183 ------- exp/lighthorizon/services/cursor.go | 102 ---- exp/lighthorizon/services/cursor_test.go | 96 ---- exp/lighthorizon/services/main.go | 216 -------- exp/lighthorizon/services/main_test.go | 250 --------- exp/lighthorizon/services/mock_services.go | 32 -- exp/lighthorizon/services/operations.go | 90 ---- exp/lighthorizon/services/transactions.go | 76 --- exp/lighthorizon/tools/cache.go | 270 ---------- exp/lighthorizon/tools/index.go | 356 ------------- exp/lighthorizon/tools/index_test.go | 58 --- go.mod | 2 +- gxdr/xdr_generated.go | 238 +-------- .../ledgerbackend/history_archive_backend.go | 51 -- metaarchive/main.go | 62 --- xdr/Stellar-lighthorizon.x | 39 -- xdr/xdr_generated.go | 478 ------------------ 426 files changed, 3 insertions(+), 11230 deletions(-) delete mode 100644 exp/lighthorizon/actions/accounts.go delete mode 100644 exp/lighthorizon/actions/accounts_test.go delete mode 100644 exp/lighthorizon/actions/apidocs.go delete mode 100644 exp/lighthorizon/actions/main.go delete mode 100644 exp/lighthorizon/actions/problem.go delete mode 100644 exp/lighthorizon/actions/root.go delete mode 100644 exp/lighthorizon/actions/static/api_docs.yml delete mode 100644 exp/lighthorizon/adapters/account_merge.go delete mode 100644 exp/lighthorizon/adapters/allow_trust.go delete mode 100644 exp/lighthorizon/adapters/begin_sponsoring_future_reserves.go delete mode 100644 exp/lighthorizon/adapters/bump_sequence.go delete mode 100644 exp/lighthorizon/adapters/change_trust.go delete mode 100644 exp/lighthorizon/adapters/claim_claimable_balance.go delete mode 100644 exp/lighthorizon/adapters/clawback.go delete mode 100644 exp/lighthorizon/adapters/clawback_claimable_balance.go delete mode 100644 exp/lighthorizon/adapters/create_account.go delete mode 100644 exp/lighthorizon/adapters/create_claimable_balance.go delete mode 100644 exp/lighthorizon/adapters/create_passive_sell_offer.go delete mode 100644 exp/lighthorizon/adapters/end_sponsoring_future_reserves.go delete mode 100644 exp/lighthorizon/adapters/inflation.go delete mode 100644 exp/lighthorizon/adapters/liquidity_pool_deposit.go delete mode 100644 exp/lighthorizon/adapters/liquidity_pool_withdraw.go delete mode 100644 exp/lighthorizon/adapters/manage_buy_offer.go delete mode 100644 exp/lighthorizon/adapters/manage_data.go delete mode 100644 exp/lighthorizon/adapters/manage_sell_offer.go delete mode 100644 exp/lighthorizon/adapters/operation.go delete mode 100644 exp/lighthorizon/adapters/path_payment_strict_receive.go delete mode 100644 exp/lighthorizon/adapters/path_payment_strict_send.go delete mode 100644 exp/lighthorizon/adapters/payment.go delete mode 100644 exp/lighthorizon/adapters/revoke_sponsorship.go delete mode 100644 exp/lighthorizon/adapters/set_options.go delete mode 100644 exp/lighthorizon/adapters/set_trust_line_flags.go delete mode 100644 exp/lighthorizon/adapters/testdata/transactions.json delete mode 100644 exp/lighthorizon/adapters/transaction.go delete mode 100644 exp/lighthorizon/adapters/transaction_test.go delete mode 100644 exp/lighthorizon/build/README.md delete mode 100755 exp/lighthorizon/build/build.sh delete mode 100644 exp/lighthorizon/build/index-batch/Dockerfile delete mode 100644 exp/lighthorizon/build/index-batch/README.md delete mode 100644 exp/lighthorizon/build/index-batch/start delete mode 100644 exp/lighthorizon/build/index-single/Dockerfile delete mode 100644 exp/lighthorizon/build/k8s/ledgerexporter.yml delete mode 100644 exp/lighthorizon/build/k8s/lighthorizon_batch_map_job.yml delete mode 100644 exp/lighthorizon/build/k8s/lighthorizon_batch_reduce_job.yml delete mode 100644 exp/lighthorizon/build/k8s/lighthorizon_index.yml delete mode 100644 exp/lighthorizon/build/k8s/lighthorizon_web.yml delete mode 100644 exp/lighthorizon/build/ledgerexporter/Dockerfile delete mode 100644 exp/lighthorizon/build/ledgerexporter/README.md delete mode 100644 exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg delete mode 100644 exp/lighthorizon/build/ledgerexporter/captive-core-testnet.cfg delete mode 100644 exp/lighthorizon/build/ledgerexporter/start delete mode 100644 exp/lighthorizon/build/web/Dockerfile delete mode 100644 exp/lighthorizon/common/operation.go delete mode 100644 exp/lighthorizon/common/transaction.go delete mode 100644 exp/lighthorizon/http.go delete mode 100644 exp/lighthorizon/http_test.go delete mode 100644 exp/lighthorizon/index/Makefile delete mode 100644 exp/lighthorizon/index/backend/backend.go delete mode 100644 exp/lighthorizon/index/backend/file.go delete mode 100644 exp/lighthorizon/index/backend/file_test.go delete mode 100644 exp/lighthorizon/index/backend/gzip.go delete mode 100644 exp/lighthorizon/index/backend/gzip_test.go delete mode 100644 exp/lighthorizon/index/backend/parallel_flush.go delete mode 100644 exp/lighthorizon/index/backend/s3.go delete mode 100644 exp/lighthorizon/index/builder.go delete mode 100644 exp/lighthorizon/index/cmd/batch/doc.go delete mode 100644 exp/lighthorizon/index/cmd/batch/map/main.go delete mode 100644 exp/lighthorizon/index/cmd/batch/reduce/main.go delete mode 100755 exp/lighthorizon/index/cmd/map.sh delete mode 100644 exp/lighthorizon/index/cmd/mapreduce_test.go delete mode 100755 exp/lighthorizon/index/cmd/reduce.sh delete mode 100644 exp/lighthorizon/index/cmd/single/main.go delete mode 100644 exp/lighthorizon/index/cmd/single_test.go delete mode 100644 exp/lighthorizon/index/cmd/testdata/latest delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410048 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410049 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410050 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410051 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410052 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410053 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410054 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410055 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410056 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410057 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410058 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410059 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410060 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410061 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410062 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410063 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410064 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410065 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410066 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410067 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410068 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410069 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410070 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410071 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410072 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410073 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410074 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410075 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410076 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410077 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410078 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410079 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410080 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410081 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410082 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410083 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410084 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410085 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410086 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410087 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410088 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410089 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410090 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410091 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410092 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410093 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410094 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410095 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410096 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410097 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410098 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410099 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410100 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410101 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410102 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410103 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410104 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410105 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410106 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410107 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410108 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410109 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410110 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410111 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410112 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410113 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410114 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410115 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410116 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410117 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410118 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410119 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410120 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410121 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410122 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410123 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410124 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410125 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410126 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410127 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410128 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410129 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410130 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410131 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410132 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410133 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410134 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410135 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410136 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410137 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410138 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410139 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410140 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410141 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410142 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410143 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410144 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410145 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410146 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410147 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410148 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410149 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410150 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410151 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410152 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410153 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410154 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410155 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410156 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410157 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410158 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410159 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410160 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410161 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410162 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410163 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410164 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410165 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410166 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410167 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410168 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410169 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410170 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410171 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410172 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410173 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410174 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410175 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410176 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410177 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410178 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410179 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410180 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410181 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410182 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410183 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410184 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410185 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410186 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410187 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410188 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410189 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410190 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410191 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410192 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410193 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410194 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410195 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410196 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410197 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410198 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410199 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410200 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410201 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410202 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410203 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410204 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410205 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410206 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410207 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410208 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410209 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410210 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410211 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410212 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410213 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410214 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410215 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410216 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410217 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410218 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410219 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410220 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410221 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410222 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410223 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410224 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410225 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410226 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410227 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410228 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410229 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410230 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410231 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410232 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410233 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410234 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410235 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410236 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410237 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410238 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410239 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410240 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410241 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410242 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410243 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410244 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410245 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410246 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410247 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410248 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410249 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410250 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410251 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410252 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410253 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410254 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410255 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410256 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410257 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410258 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410259 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410260 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410261 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410262 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410263 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410264 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410265 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410266 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410267 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410268 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410269 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410270 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410271 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410272 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410273 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410274 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410275 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410276 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410277 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410278 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410279 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410280 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410281 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410282 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410283 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410284 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410285 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410286 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410287 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410288 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410289 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410290 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410291 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410292 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410293 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410294 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410295 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410296 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410297 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410298 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410299 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410300 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410301 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410302 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410303 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410304 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410305 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410306 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410307 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410308 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410309 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410310 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410311 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410312 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410313 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410314 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410315 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410316 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410317 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410318 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410319 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410320 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410321 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410322 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410323 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410324 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410325 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410326 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410327 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410328 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410329 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410330 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410331 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410332 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410333 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410334 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410335 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410336 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410337 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410338 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410339 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410340 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410341 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410342 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410343 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410344 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410345 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410346 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410347 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410348 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410349 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410350 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410351 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410352 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410353 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410354 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410355 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410356 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410357 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410358 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410359 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410360 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410361 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410362 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410363 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410364 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410365 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410366 delete mode 100644 exp/lighthorizon/index/cmd/testdata/ledgers/1410367 delete mode 100644 exp/lighthorizon/index/cmd/testdata/regenerate.sh delete mode 100644 exp/lighthorizon/index/connect.go delete mode 100644 exp/lighthorizon/index/mock_store.go delete mode 100644 exp/lighthorizon/index/modules.go delete mode 100644 exp/lighthorizon/index/store.go delete mode 100644 exp/lighthorizon/index/types/bitmap.go delete mode 100644 exp/lighthorizon/index/types/bitmap_test.go delete mode 100644 exp/lighthorizon/index/types/trie.go delete mode 100644 exp/lighthorizon/index/types/trie_test.go delete mode 100644 exp/lighthorizon/ingester/ingester.go delete mode 100644 exp/lighthorizon/ingester/main.go delete mode 100644 exp/lighthorizon/ingester/mock_ingester.go delete mode 100644 exp/lighthorizon/ingester/parallel_ingester.go delete mode 100644 exp/lighthorizon/ingester/participants.go delete mode 100644 exp/lighthorizon/main.go delete mode 100644 exp/lighthorizon/services/cursor.go delete mode 100644 exp/lighthorizon/services/cursor_test.go delete mode 100644 exp/lighthorizon/services/main.go delete mode 100644 exp/lighthorizon/services/main_test.go delete mode 100644 exp/lighthorizon/services/mock_services.go delete mode 100644 exp/lighthorizon/services/operations.go delete mode 100644 exp/lighthorizon/services/transactions.go delete mode 100644 exp/lighthorizon/tools/cache.go delete mode 100644 exp/lighthorizon/tools/index.go delete mode 100644 exp/lighthorizon/tools/index_test.go delete mode 100644 ingest/ledgerbackend/history_archive_backend.go delete mode 100644 metaarchive/main.go delete mode 100644 xdr/Stellar-lighthorizon.x diff --git a/Makefile b/Makefile index 07037315e4..69d46f68c3 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,7 @@ xdr/Stellar-contract.x \ xdr/Stellar-internal.x \ xdr/Stellar-contract-config-setting.x -XDRS = $(DOWNLOADABLE_XDRS) xdr/Stellar-lighthorizon.x \ - xdr/Stellar-exporter.x +XDRS = $(DOWNLOADABLE_XDRS) xdr/Stellar-exporter.x XDRGEN_COMMIT=e2cac557162d99b12ae73b846cf3d5bfe16636de diff --git a/exp/lighthorizon/actions/accounts.go b/exp/lighthorizon/actions/accounts.go deleted file mode 100644 index 86673afa68..0000000000 --- a/exp/lighthorizon/actions/accounts.go +++ /dev/null @@ -1,142 +0,0 @@ -package actions - -import ( - "errors" - "net/http" - "os" - "strconv" - - "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" - - "github.com/stellar/go/exp/lighthorizon/adapters" - "github.com/stellar/go/exp/lighthorizon/services" - hProtocol "github.com/stellar/go/protocols/horizon" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/render/hal" - supportProblem "github.com/stellar/go/support/render/problem" - "github.com/stellar/go/toid" -) - -const ( - urlAccountId = "account_id" -) - -func accountRequestParams(w http.ResponseWriter, r *http.Request) (string, pagination, error) { - var accountId string - var accountErr bool - - if accountId, accountErr = getURLParam(r, urlAccountId); !accountErr { - return "", pagination{}, errors.New("unable to find account_id in url path") - } - - paginate, err := paging(r) - if err != nil { - return "", pagination{}, err - } - - if paginate.Cursor < 1 { - paginate.Cursor = toid.New(1, 1, 1).ToInt64() - } - - if paginate.Limit == 0 { - paginate.Limit = 10 - } - - return accountId, paginate, nil -} - -func NewTXByAccountHandler(lightHorizon services.LightHorizon) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - var accountId string - var paginate pagination - var err error - - if accountId, paginate, err = accountRequestParams(w, r); err != nil { - errorMsg := supportProblem.MakeInvalidFieldProblem("account_id", err) - sendErrorResponse(r.Context(), w, *errorMsg) - return - } - - page := hal.Page{ - Cursor: strconv.FormatInt(paginate.Cursor, 10), - Order: string(paginate.Order), - Limit: uint64(paginate.Limit), - } - page.Init() - page.FullURL = r.URL - - txns, err := lightHorizon.Transactions.GetTransactionsByAccount(ctx, paginate.Cursor, paginate.Limit, accountId) - if err != nil { - log.Error(err) - if os.IsNotExist(err) { - sendErrorResponse(r.Context(), w, supportProblem.NotFound) - } else if err != nil { - sendErrorResponse(r.Context(), w, supportProblem.ServerError) - } - return - } - - encoder := xdr.NewEncodingBuffer() - for _, txn := range txns { - var response hProtocol.Transaction - response, err = adapters.PopulateTransaction(r.URL, &txn, encoder) - if err != nil { - log.Error(err) - sendErrorResponse(r.Context(), w, supportProblem.ServerError) - return - } - - page.Add(response) - } - - page.PopulateLinks() - sendPageResponse(r.Context(), w, page) - } -} - -func NewOpsByAccountHandler(lightHorizon services.LightHorizon) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - var accountId string - var paginate pagination - var err error - - if accountId, paginate, err = accountRequestParams(w, r); err != nil { - errorMsg := supportProblem.MakeInvalidFieldProblem("account_id", err) - sendErrorResponse(r.Context(), w, *errorMsg) - return - } - - page := hal.Page{ - Cursor: strconv.FormatInt(paginate.Cursor, 10), - Order: string(paginate.Order), - Limit: uint64(paginate.Limit), - } - page.Init() - page.FullURL = r.URL - - ops, err := lightHorizon.Operations.GetOperationsByAccount(ctx, paginate.Cursor, paginate.Limit, accountId) - if err != nil { - log.Error(err) - sendErrorResponse(r.Context(), w, supportProblem.ServerError) - return - } - - for _, op := range ops { - var response operations.Operation - response, err = adapters.PopulateOperation(r, &op) - if err != nil { - log.Error(err) - sendErrorResponse(r.Context(), w, supportProblem.ServerError) - return - } - - page.Add(response) - } - - page.PopulateLinks() - sendPageResponse(r.Context(), w, page) - } -} diff --git a/exp/lighthorizon/actions/accounts_test.go b/exp/lighthorizon/actions/accounts_test.go deleted file mode 100644 index 40576fb7e4..0000000000 --- a/exp/lighthorizon/actions/accounts_test.go +++ /dev/null @@ -1,191 +0,0 @@ -package actions - -import ( - "context" - "encoding/json" - "errors" - "io/ioutil" - "net/http" - "net/http/httptest" - "net/url" - "testing" - - "github.com/go-chi/chi" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/exp/lighthorizon/services" - "github.com/stellar/go/support/render/problem" -) - -func setupTest() { - problem.RegisterHost("") -} - -func TestTxByAccountMissingParamError(t *testing.T) { - setupTest() - recorder := httptest.NewRecorder() - request := buildHttpRequest( - t, - map[string]string{}, - map[string]string{}, - ) - - mockOperationService := &services.MockOperationService{} - mockTransactionService := &services.MockTransactionService{} - - lh := services.LightHorizon{ - Operations: mockOperationService, - Transactions: mockTransactionService, - } - - handler := NewTXByAccountHandler(lh) - handler(recorder, request) - - resp := recorder.Result() - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) - - raw, err := ioutil.ReadAll(resp.Body) - assert.NoError(t, err) - - var problem problem.P - err = json.Unmarshal(raw, &problem) - assert.NoError(t, err) - assert.Equal(t, "Bad Request", problem.Title) - assert.Equal(t, "bad_request", problem.Type) - assert.Equal(t, "account_id", problem.Extras["invalid_field"]) - assert.Equal(t, "The request you sent was invalid in some way.", problem.Detail) - assert.Equal(t, "unable to find account_id in url path", problem.Extras["reason"]) -} - -func TestTxByAccountServerError(t *testing.T) { - setupTest() - recorder := httptest.NewRecorder() - pathParams := make(map[string]string) - pathParams["account_id"] = "G1234" - request := buildHttpRequest( - t, - map[string]string{}, - pathParams, - ) - - mockOperationService := &services.MockOperationService{} - mockTransactionService := &services.MockTransactionService{} - mockTransactionService.On("GetTransactionsByAccount", mock.Anything, mock.Anything, mock.Anything, "G1234").Return([]common.Transaction{}, errors.New("not good")) - - lh := services.LightHorizon{ - Operations: mockOperationService, - Transactions: mockTransactionService, - } - - handler := NewTXByAccountHandler(lh) - handler(recorder, request) - - resp := recorder.Result() - assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) - - raw, err := ioutil.ReadAll(resp.Body) - assert.NoError(t, err) - - var problem problem.P - err = json.Unmarshal(raw, &problem) - assert.NoError(t, err) - assert.Equal(t, "Internal Server Error", problem.Title) - assert.Equal(t, "server_error", problem.Type) -} - -func TestOpsByAccountMissingParamError(t *testing.T) { - setupTest() - recorder := httptest.NewRecorder() - request := buildHttpRequest( - t, - map[string]string{}, - map[string]string{}, - ) - - mockOperationService := &services.MockOperationService{} - mockTransactionService := &services.MockTransactionService{} - - lh := services.LightHorizon{ - Operations: mockOperationService, - Transactions: mockTransactionService, - } - - handler := NewOpsByAccountHandler(lh) - handler(recorder, request) - - resp := recorder.Result() - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) - - raw, err := ioutil.ReadAll(resp.Body) - assert.NoError(t, err) - - var problem problem.P - err = json.Unmarshal(raw, &problem) - assert.NoError(t, err) - assert.Equal(t, "Bad Request", problem.Title) - assert.Equal(t, "bad_request", problem.Type) - assert.Equal(t, "account_id", problem.Extras["invalid_field"]) - assert.Equal(t, "The request you sent was invalid in some way.", problem.Detail) - assert.Equal(t, "unable to find account_id in url path", problem.Extras["reason"]) -} - -func TestOpsByAccountServerError(t *testing.T) { - setupTest() - recorder := httptest.NewRecorder() - pathParams := make(map[string]string) - pathParams["account_id"] = "G1234" - request := buildHttpRequest( - t, - map[string]string{}, - pathParams, - ) - - mockOperationService := &services.MockOperationService{} - mockTransactionService := &services.MockTransactionService{} - mockOperationService.On("GetOperationsByAccount", mock.Anything, mock.Anything, mock.Anything, "G1234").Return([]common.Operation{}, errors.New("not good")) - - lh := services.LightHorizon{ - Operations: mockOperationService, - Transactions: mockTransactionService, - } - - handler := NewOpsByAccountHandler(lh) - handler(recorder, request) - - resp := recorder.Result() - assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) - - raw, err := ioutil.ReadAll(resp.Body) - assert.NoError(t, err) - - var problem problem.P - err = json.Unmarshal(raw, &problem) - assert.NoError(t, err) - assert.Equal(t, "Internal Server Error", problem.Title) - assert.Equal(t, "server_error", problem.Type) -} - -func buildHttpRequest( - t *testing.T, - queryParams map[string]string, - routeParams map[string]string, -) *http.Request { - request, err := http.NewRequest("GET", "/", nil) - require.NoError(t, err) - - query := url.Values{} - for key, value := range queryParams { - query.Set(key, value) - } - request.URL.RawQuery = query.Encode() - - chiRouteContext := chi.NewRouteContext() - for key, value := range routeParams { - chiRouteContext.URLParams.Add(key, value) - } - ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiRouteContext) - return request.WithContext(ctx) -} diff --git a/exp/lighthorizon/actions/apidocs.go b/exp/lighthorizon/actions/apidocs.go deleted file mode 100644 index 713c4054fa..0000000000 --- a/exp/lighthorizon/actions/apidocs.go +++ /dev/null @@ -1,26 +0,0 @@ -package actions - -import ( - supportProblem "github.com/stellar/go/support/render/problem" - "net/http" -) - -func ApiDocs() func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - r.URL.Scheme = "http" - r.URL.Host = "localhost:8080" - - if r.Method != "GET" { - sendErrorResponse(r.Context(), w, supportProblem.BadRequest) - return - } - - p, err := staticFiles.ReadFile("static/api_docs.yml") - if err != nil { - w.WriteHeader(http.StatusNotFound) - return - } - w.Header().Set("Content-Type", "application/openapi+yaml") - w.Write(p) - } -} diff --git a/exp/lighthorizon/actions/main.go b/exp/lighthorizon/actions/main.go deleted file mode 100644 index 01769682b5..0000000000 --- a/exp/lighthorizon/actions/main.go +++ /dev/null @@ -1,124 +0,0 @@ -package actions - -import ( - "context" - "embed" - "encoding/json" - "net/http" - "net/url" - "strconv" - - "github.com/go-chi/chi" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - "github.com/stellar/go/support/log" - "github.com/stellar/go/support/render/hal" - supportProblem "github.com/stellar/go/support/render/problem" -) - -var ( - //go:embed static - staticFiles embed.FS - //lint:ignore U1000 temporary - requestCount = promauto.NewCounter(prometheus.CounterOpts{ - Name: "horizon_lite_request_count", - Help: "How many requests have occurred?", - }) - //lint:ignore U1000 temporary - requestTime = promauto.NewHistogram(prometheus.HistogramOpts{ - Name: "horizon_lite_request_duration", - Help: "How long do requests take?", - Buckets: append( - prometheus.LinearBuckets(0, 50, 20), - prometheus.LinearBuckets(1000, 1000, 8)..., - ), - }) -) - -type order string - -const ( - orderAsc order = "asc" - orderDesc order = "desc" -) - -type pagination struct { - Limit uint64 - Cursor int64 - Order order -} - -func sendPageResponse(ctx context.Context, w http.ResponseWriter, page hal.Page) { - w.Header().Set("Content-Type", "application/hal+json; charset=utf-8") - encoder := json.NewEncoder(w) - encoder.SetIndent("", " ") - err := encoder.Encode(page) - if err != nil { - log.Error(err) - sendErrorResponse(ctx, w, supportProblem.ServerError) - } -} - -func sendErrorResponse(ctx context.Context, w http.ResponseWriter, problem supportProblem.P) { - supportProblem.Render(ctx, w, problem) -} - -func requestUnaryParam(r *http.Request, paramName string) (string, error) { - query, err := url.ParseQuery(r.URL.RawQuery) - if err != nil { - return "", err - } - return query.Get(paramName), nil -} - -func paging(r *http.Request) (pagination, error) { - paginate := pagination{ - Order: orderAsc, - } - - if cursorRequested, err := requestUnaryParam(r, "cursor"); err != nil { - return pagination{}, err - } else if cursorRequested != "" { - paginate.Cursor, err = strconv.ParseInt(cursorRequested, 10, 64) - if err != nil { - return pagination{}, err - } - } - - if limitRequested, err := requestUnaryParam(r, "limit"); err != nil { - return pagination{}, err - } else if limitRequested != "" { - paginate.Limit, err = strconv.ParseUint(limitRequested, 10, 64) - if err != nil { - return pagination{}, err - } - } - - if orderRequested, err := requestUnaryParam(r, "order"); err != nil { - return pagination{}, err - } else if orderRequested != "" && orderRequested == string(orderDesc) { - paginate.Order = orderDesc - } - - return paginate, nil -} - -func getURLParam(r *http.Request, key string) (string, bool) { - rctx := chi.RouteContext(r.Context()) - - if rctx == nil { - return "", false - } - - if len(rctx.URLParams.Keys) != len(rctx.URLParams.Values) { - return "", false - } - - for k := len(rctx.URLParams.Keys) - 1; k >= 0; k-- { - if rctx.URLParams.Keys[k] == key { - return rctx.URLParams.Values[k], true - } - } - - return "", false -} diff --git a/exp/lighthorizon/actions/problem.go b/exp/lighthorizon/actions/problem.go deleted file mode 100644 index cd82cfb1e8..0000000000 --- a/exp/lighthorizon/actions/problem.go +++ /dev/null @@ -1,94 +0,0 @@ -package actions - -import ( - "net/http" - - "github.com/stellar/go/support/render/problem" -) - -// Well-known and reused problems below: -// inspired by similar default established in horizon - services/horizon/internal/render/problem/problem.go -var ( - - // ClientDisconnected, represented by a non-standard HTTP status code of 499, which was introduced by - // nginix.org(https://www.nginx.com/resources/wiki/extending/api/http/) as a way to capture this state. Use it as a shortcut - // in your actions. - ClientDisconnected = problem.P{ - Type: "client_disconnected", - Title: "Client Disconnected", - Status: 499, - Detail: "The client has closed the connection.", - } - - // ServiceUnavailable is a well-known problem type. Use it as a shortcut - // in your actions. - ServiceUnavailable = problem.P{ - Type: "service_unavailable", - Title: "Service Unavailable", - Status: http.StatusServiceUnavailable, - Detail: "The request cannot be serviced at this time.", - } - - // RateLimitExceeded is a well-known problem type. Use it as a shortcut - // in your actions. - RateLimitExceeded = problem.P{ - Type: "rate_limit_exceeded", - Title: "Rate Limit Exceeded", - Status: 429, - Detail: "The rate limit for the requesting IP address is over its alloted " + - "limit. The allowed limit and requests left per time period are " + - "communicated to clients via the http response headers 'X-RateLimit-*' " + - "headers.", - } - - // NotImplemented is a well-known problem type. Use it as a shortcut - // in your actions. - NotImplemented = problem.P{ - Type: "not_implemented", - Title: "Resource Not Yet Implemented", - Status: http.StatusNotFound, - Detail: "While the requested URL is expected to eventually point to a " + - "valid resource, the work to implement the resource has not yet " + - "been completed.", - } - - // NotAcceptable is a well-known problem type. Use it as a shortcut - // in your actions. - NotAcceptable = problem.P{ - Type: "not_acceptable", - Title: "An acceptable response content-type could not be provided for " + - "this request", - Status: http.StatusNotAcceptable, - } - - // ServerOverCapacity is a well-known problem type. Use it as a shortcut - // in your actions. - ServerOverCapacity = problem.P{ - Type: "server_over_capacity", - Title: "Server Over Capacity", - Status: http.StatusServiceUnavailable, - Detail: "This horizon server is currently overloaded. Please wait for " + - "several minutes before trying your request again.", - } - - // Timeout is a well-known problem type. Use it as a shortcut - // in your actions. - Timeout = problem.P{ - Type: "timeout", - Title: "Timeout", - Status: http.StatusGatewayTimeout, - Detail: "Your request timed out before completing. Please try your " + - "request again. If you are submitting a transaction make sure you are " + - "sending exactly the same transaction (with the same sequence number).", - } - - // UnsupportedMediaType is a well-known problem type. Use it as a shortcut - // in your actions. - UnsupportedMediaType = problem.P{ - Type: "unsupported_media_type", - Title: "Unsupported Media Type", - Status: http.StatusUnsupportedMediaType, - Detail: "The request has an unsupported content type. Presently, the " + - "only supported content type is application/x-www-form-urlencoded.", - } -) diff --git a/exp/lighthorizon/actions/root.go b/exp/lighthorizon/actions/root.go deleted file mode 100644 index 3dfa4341a0..0000000000 --- a/exp/lighthorizon/actions/root.go +++ /dev/null @@ -1,29 +0,0 @@ -package actions - -import ( - "encoding/json" - "net/http" - - "github.com/stellar/go/support/log" - supportProblem "github.com/stellar/go/support/render/problem" -) - -type RootResponse struct { - Version string `json:"version"` - LedgerSource string `json:"ledger_source"` - IndexSource string `json:"index_source"` - LatestLedger uint32 `json:"latest_indexed_ledger"` -} - -func Root(config RootResponse) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/hal+json; charset=utf-8") - encoder := json.NewEncoder(w) - encoder.SetIndent("", " ") - err := encoder.Encode(config) - if err != nil { - log.Error(err) - sendErrorResponse(r.Context(), w, supportProblem.ServerError) - } - } -} diff --git a/exp/lighthorizon/actions/static/api_docs.yml b/exp/lighthorizon/actions/static/api_docs.yml deleted file mode 100644 index 281cf2b605..0000000000 --- a/exp/lighthorizon/actions/static/api_docs.yml +++ /dev/null @@ -1,228 +0,0 @@ -openapi: 3.1.0 -info: - title: Horizon Lite API - version: 0.0.1 - description: |- - The Horizon Lite API is a published web service on port 8080. It's considered - extremely experimental and only provides a minimal subset of endpoints. -servers: - - url: http://localhost:8080/ -paths: - /accounts/{account_id}/operations: - get: - operationId: GetOperationsByAccountId - parameters: - - $ref: '#/components/parameters/CursorParam' - - $ref: '#/components/parameters/LimitParam' - - $ref: '#/components/parameters/AccountIDParam' - responses: - '200': - description: OK - headers: {} - content: - application/json: - schema: - $ref: '#/components/schemas/CollectionModel_Operation' - example: - _links: - self: - href: http://localhost:8080/accounts/GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ/operations?cursor=6606617478959105&limit=1&order=asc - next: - href: http://localhost:8080/accounts/GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ/operations?cursor=6606621773926401&limit=1&order=asc - prev: - href: http://localhost:8080/accounts/GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ/operations?cursor=6606621773926401&limit=1&order=desc - _embedded: - records: - - _links: - self: - href: http://localhost:8080/operations/6606621773926401 - id: '6606621773926401' - paging_token: '6606621773926401' - transaction_successful: true - source_account: GBGTCH47BOEEKLPHHMR2GOK6KQFGL3O7Q53FIZTJ7S7YEDWYJ5IUDJDJ - type: manage_sell_offer - type_i: 3 - created_at: '2022-06-17T23:29:42Z' - transaction_hash: 544469b76cd90978345a4734a0ce69a9d0ddb4a6595a7afc503225a77826722a - amount: '0.0000000' - price: '0.0000001' - price_r: - n: 1 - d: 10000000 - buying_asset_type: credit_alphanum4 - buying_asset_code: USDV - buying_asset_issuer: GAXXMQMTDUQ4YEPXJMKFBGN3GETPJNEXEUHFCQJKGJDVI3XQCNBU3OZI - selling_asset_type: credit_alphanum4 - selling_asset_code: EURV - selling_asset_issuer: GAXXMQMTDUQ4YEPXJMKFBGN3GETPJNEXEUHFCQJKGJDVI3XQCNBU3OZI - offer_id: '425531' - summary: Get Operations by Account ID and Paged list - description: Get Operations by Account ID and Paged list - tags: [] - /accounts/{account_id}/transactions: - get: - operationId: GetTransactionsByAccountId - parameters: - - $ref: '#/components/parameters/CursorParam' - - $ref: '#/components/parameters/LimitParam' - - $ref: '#/components/parameters/AccountIDParam' - responses: - '200': - description: OK - headers: {} - content: - application/json: - schema: - $ref: '#/components/schemas/CollectionModel_Tx' - example: - _links: - self: - href: http://localhost:8080/accounts/GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ/transactions?cursor=&limit=0&order= - next: - href: http://localhost:8080/accounts/GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ/transactions?cursor=6606621773930497&limit=0&order= - prev: - href: http://localhost:8080/accounts/GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ/transactions?cursor=6606621773930497&limit=0&order=asc - _embedded: - records: - - memo: xdr.MemoText("psp:1405") - _links: - self: - href: http://localhost:8080/transactions/5fef21d5ef75ecf18d65a160cfab17dca8dbf6dbc4e2fd66a510719ad8dddb09 - id: 5fef21d5ef75ecf18d65a160cfab17dca8dbf6dbc4e2fd66a510719ad8dddb09 - paging_token: '6606621773930497' - successful: false - hash: 5fef21d5ef75ecf18d65a160cfab17dca8dbf6dbc4e2fd66a510719ad8dddb09 - ledger: 1538224 - created_at: '2022-06-17T23:29:42Z' - source_account: GCFJN22UG6IZHXKDVAJWAVEQ3NERGCRCURR2FHARNRBNLYFEQZGML4PW - source_account_sequence: '' - fee_account: '' - fee_charged: '3000' - max_fee: '0' - operation_count: 1 - envelope_xdr: AAAAAgAAAACKlutUN5GT3UOoE2BUkNtJEwoipGOinBFsQtXgpIZMxQAAJxAAE05oAAHUKAAAAAEAAAAAAAAAAAAAAABirQ6AAAAAAQAAAAhwc3A6MTQwNQAAAAEAAAAAAAAAAQAAAADpPdN37FA9KVcJfmMBuD8pPcaT5jqlrMeYEOTP36Zo2AAAAAJBVE1ZUgAAAAAAAAAAAAAAZ8rWY3iaDnWNtfpvLpNaCEbKdDjrd2gQODOuKpmj1vMAAAAAGHAagAAAAAAAAAABpIZMxQAAAEDNJwYToiBR6bzElRL4ORJdXXZYO9cE3-ishQLC_fWGrPGhWrW7_UkPJWvxWdQDJBjVOHuA1Jjc94NSe91hSwEL - result_xdr: AAAAAAAAC7j_____AAAAAQAAAAAAAAAB____-gAAAAA= - result_meta_xdr: '' - fee_meta_xdr: '' - memo_type: MemoTypeMemoText - signatures: - - pIZMxQAAAEDNJwYToiBR6bzElRL4ORJdXXZYO9cE3-ishQLC_fWGrPGhWrW7_UkPJWvxWdQDJBjVOHuA1Jjc94NSe91hSwEL - summary: Get Transactions by Account ID and Paged list - description: Get Transactions by Account ID and Paged list - tags: [] -components: - parameters: - CursorParam: - name: cursor - in: query - required: false - schema: - type: integer - example: 6606617478959105 - description: The packed order id consisting of Ledger Num, TX Order Num, Operation Order Num - LimitParam: - in: query - name: limit - required: false - schema: - type: integer - default: 10 - description: The numbers of items to return - AccountIDParam: - name: account_id - in: path - required: true - description: The strkey encoded Account ID - schema: - type: string - example: GDMQQNJM4UL7QIA66P7R2PZHMQINWZBM77BEBMHLFXD5JEUAHGJ7R4JZ - TransactionIDParam: - name: tx_id - in: path - required: true - description: The Transaction hash, it's id. - schema: - type: string - example: a221f4743450736cba4a78940f22b01e1f64568eec8cb04c2ae37874d86cee3d - schemas: - CollectionModelItem: - type: object - properties: - _embedded: - type: object - properties: - records: - type: array - items: - "$ref": "#/components/schemas/Item" - _links: - "$ref": "#/components/schemas/Links" - Item: - type: object - properties: - id: - type: string - _links: - "$ref": "#/components/schemas/Links" - CollectionModel_Tx: - type: object - allOf: - - $ref: "#/components/schemas/CollectionModelItem" - properties: - _embedded: - type: object - properties: - records: - type: array - items: - $ref: "#/components/schemas/EntityModel_Tx" - EntityModel_Tx: - type: object - allOf: - - $ref: "#/components/schemas/Tx" - - $ref: "#/components/schemas/Links" - Tx: - type: object - properties: - id: - type: string - hash: - type: string - ledger: - type: integer - CollectionModel_Operation: - type: object - allOf: - - $ref: "#/components/schemas/CollectionModelItem" - properties: - _embedded: - type: object - properties: - records: - type: array - items: - $ref: "#/components/schemas/EntityModel_Operation" - EntityModel_Operation: - type: object - allOf: - - $ref: "#/components/schemas/Operation" - - $ref: "#/components/schemas/Links" - Operation: - type: object - properties: - id: - type: string - type: - type: string - source_account: - type: string - Links: - type: object - additionalProperties: - "$ref": "#/components/schemas/Link" - Link: - type: object - properties: - href: - type: string -tags: [] diff --git a/exp/lighthorizon/adapters/account_merge.go b/exp/lighthorizon/adapters/account_merge.go deleted file mode 100644 index 1fa6934638..0000000000 --- a/exp/lighthorizon/adapters/account_merge.go +++ /dev/null @@ -1,21 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/operations" -) - -func populateAccountMergeOperation(op *common.Operation, baseOp operations.Base) (operations.AccountMerge, error) { - destination := op.Get().Body.MustDestination() - - return operations.AccountMerge{ - Base: baseOp, - Account: op.SourceAccount().Address(), - Into: destination.Address(), - // TODO: - AccountMuxed: "", - AccountMuxedID: 0, - IntoMuxed: "", - IntoMuxedID: 0, - }, nil -} diff --git a/exp/lighthorizon/adapters/allow_trust.go b/exp/lighthorizon/adapters/allow_trust.go deleted file mode 100644 index 2e3fea2188..0000000000 --- a/exp/lighthorizon/adapters/allow_trust.go +++ /dev/null @@ -1,43 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/base" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/xdr" -) - -func populateAllowTrustOperation(op *common.Operation, baseOp operations.Base) (operations.AllowTrust, error) { - allowTrust := op.Get().Body.MustAllowTrustOp() - - var ( - assetType string - code string - issuer string - ) - - err := allowTrust.Asset.ToAsset(op.SourceAccount()).Extract(&assetType, &code, &issuer) - if err != nil { - return operations.AllowTrust{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - flags := xdr.TrustLineFlags(allowTrust.Authorize) - - return operations.AllowTrust{ - Base: baseOp, - Asset: base.Asset{ - Type: assetType, - Code: code, - Issuer: issuer, - }, - - Trustee: op.SourceAccount().Address(), - Trustor: allowTrust.Trustor.Address(), - Authorize: flags.IsAuthorized(), - AuthorizeToMaintainLiabilities: flags.IsAuthorizedToMaintainLiabilitiesFlag(), - // TODO: - TrusteeMuxed: "", - TrusteeMuxedID: 0, - }, nil -} diff --git a/exp/lighthorizon/adapters/begin_sponsoring_future_reserves.go b/exp/lighthorizon/adapters/begin_sponsoring_future_reserves.go deleted file mode 100644 index a5fe86a3ce..0000000000 --- a/exp/lighthorizon/adapters/begin_sponsoring_future_reserves.go +++ /dev/null @@ -1,15 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/operations" -) - -func populateBeginSponsoringFutureReservesOperation(op *common.Operation, baseOp operations.Base) (operations.BeginSponsoringFutureReserves, error) { - beginSponsoringFutureReserves := op.Get().Body.MustBeginSponsoringFutureReservesOp() - - return operations.BeginSponsoringFutureReserves{ - Base: baseOp, - SponsoredID: beginSponsoringFutureReserves.SponsoredId.Address(), - }, nil -} diff --git a/exp/lighthorizon/adapters/bump_sequence.go b/exp/lighthorizon/adapters/bump_sequence.go deleted file mode 100644 index 53fe0125a2..0000000000 --- a/exp/lighthorizon/adapters/bump_sequence.go +++ /dev/null @@ -1,17 +0,0 @@ -package adapters - -import ( - "strconv" - - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/operations" -) - -func populateBumpSequenceOperation(op *common.Operation, baseOp operations.Base) (operations.BumpSequence, error) { - bumpSequence := op.Get().Body.MustBumpSequenceOp() - - return operations.BumpSequence{ - Base: baseOp, - BumpTo: strconv.FormatInt(int64(bumpSequence.BumpTo), 10), - }, nil -} diff --git a/exp/lighthorizon/adapters/change_trust.go b/exp/lighthorizon/adapters/change_trust.go deleted file mode 100644 index e06dbcfb39..0000000000 --- a/exp/lighthorizon/adapters/change_trust.go +++ /dev/null @@ -1,63 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/amount" - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/base" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/xdr" -) - -func populateChangeTrustOperation(op *common.Operation, baseOp operations.Base) (operations.ChangeTrust, error) { - changeTrust := op.Get().Body.MustChangeTrustOp() - - var ( - assetType string - code string - issuer string - - liquidityPoolID string - ) - - switch changeTrust.Line.Type { - case xdr.AssetTypeAssetTypeCreditAlphanum4, xdr.AssetTypeAssetTypeCreditAlphanum12: - err := changeTrust.Line.ToAsset().Extract(&assetType, &code, &issuer) - if err != nil { - return operations.ChangeTrust{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - case xdr.AssetTypeAssetTypePoolShare: - assetType = "liquidity_pool_shares" - - if changeTrust.Line.LiquidityPool.Type != xdr.LiquidityPoolTypeLiquidityPoolConstantProduct { - return operations.ChangeTrust{}, errors.Errorf("unkown liquidity pool type %d", changeTrust.Line.LiquidityPool.Type) - } - - cp := changeTrust.Line.LiquidityPool.ConstantProduct - poolID, err := xdr.NewPoolId(cp.AssetA, cp.AssetB, cp.Fee) - if err != nil { - return operations.ChangeTrust{}, errors.Wrap(err, "error generating pool id") - } - liquidityPoolID = xdr.Hash(poolID).HexString() - default: - return operations.ChangeTrust{}, errors.Errorf("unknown asset type %d", changeTrust.Line.Type) - } - - return operations.ChangeTrust{ - Base: baseOp, - LiquidityPoolOrAsset: base.LiquidityPoolOrAsset{ - Asset: base.Asset{ - Type: assetType, - Code: code, - Issuer: issuer, - }, - LiquidityPoolID: liquidityPoolID, - }, - Limit: amount.String(changeTrust.Limit), - Trustee: issuer, - Trustor: op.SourceAccount().Address(), - // TODO: - TrustorMuxed: "", - TrustorMuxedID: 0, - }, nil -} diff --git a/exp/lighthorizon/adapters/claim_claimable_balance.go b/exp/lighthorizon/adapters/claim_claimable_balance.go deleted file mode 100644 index 7dffe49d13..0000000000 --- a/exp/lighthorizon/adapters/claim_claimable_balance.go +++ /dev/null @@ -1,26 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/xdr" -) - -func populateClaimClaimableBalanceOperation(op *common.Operation, baseOp operations.Base) (operations.ClaimClaimableBalance, error) { - claimClaimableBalance := op.Get().Body.MustClaimClaimableBalanceOp() - - balanceID, err := xdr.MarshalHex(claimClaimableBalance.BalanceId) - if err != nil { - return operations.ClaimClaimableBalance{}, errors.New("invalid balanceId") - } - - return operations.ClaimClaimableBalance{ - Base: baseOp, - BalanceID: balanceID, - Claimant: op.SourceAccount().Address(), - // TODO - ClaimantMuxed: "", - ClaimantMuxedID: 0, - }, nil -} diff --git a/exp/lighthorizon/adapters/clawback.go b/exp/lighthorizon/adapters/clawback.go deleted file mode 100644 index 32f6ed7401..0000000000 --- a/exp/lighthorizon/adapters/clawback.go +++ /dev/null @@ -1,37 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/amount" - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/base" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/errors" -) - -func populateClawbackOperation(op *common.Operation, baseOp operations.Base) (operations.Clawback, error) { - clawback := op.Get().Body.MustClawbackOp() - - var ( - assetType string - code string - issuer string - ) - err := clawback.Asset.Extract(&assetType, &code, &issuer) - if err != nil { - return operations.Clawback{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - return operations.Clawback{ - Base: baseOp, - Asset: base.Asset{ - Type: assetType, - Code: code, - Issuer: issuer, - }, - Amount: amount.String(clawback.Amount), - From: clawback.From.Address(), - // TODO: - FromMuxed: "", - FromMuxedID: 0, - }, nil -} diff --git a/exp/lighthorizon/adapters/clawback_claimable_balance.go b/exp/lighthorizon/adapters/clawback_claimable_balance.go deleted file mode 100644 index a24d4828b0..0000000000 --- a/exp/lighthorizon/adapters/clawback_claimable_balance.go +++ /dev/null @@ -1,22 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/xdr" -) - -func populateClawbackClaimableBalanceOperation(op *common.Operation, baseOp operations.Base) (operations.ClawbackClaimableBalance, error) { - clawbackClaimableBalance := op.Get().Body.MustClawbackClaimableBalanceOp() - - balanceID, err := xdr.MarshalHex(clawbackClaimableBalance.BalanceId) - if err != nil { - return operations.ClawbackClaimableBalance{}, errors.Wrap(err, "invalid balanceId") - } - - return operations.ClawbackClaimableBalance{ - Base: baseOp, - BalanceID: balanceID, - }, nil -} diff --git a/exp/lighthorizon/adapters/create_account.go b/exp/lighthorizon/adapters/create_account.go deleted file mode 100644 index d9a7c678a1..0000000000 --- a/exp/lighthorizon/adapters/create_account.go +++ /dev/null @@ -1,18 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/amount" - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/operations" -) - -func populateCreateAccountOperation(op *common.Operation, baseOp operations.Base) (operations.CreateAccount, error) { - createAccount := op.Get().Body.MustCreateAccountOp() - - return operations.CreateAccount{ - Base: baseOp, - StartingBalance: amount.String(createAccount.StartingBalance), - Funder: op.SourceAccount().Address(), - Account: createAccount.Destination.Address(), - }, nil -} diff --git a/exp/lighthorizon/adapters/create_claimable_balance.go b/exp/lighthorizon/adapters/create_claimable_balance.go deleted file mode 100644 index 472e43b30c..0000000000 --- a/exp/lighthorizon/adapters/create_claimable_balance.go +++ /dev/null @@ -1,27 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/amount" - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon" - "github.com/stellar/go/protocols/horizon/operations" -) - -func populateCreateClaimableBalanceOperation(op *common.Operation, baseOp operations.Base) (operations.CreateClaimableBalance, error) { - createClaimableBalance := op.Get().Body.MustCreateClaimableBalanceOp() - - claimants := make([]horizon.Claimant, len(createClaimableBalance.Claimants)) - for i, claimant := range createClaimableBalance.Claimants { - claimants[i] = horizon.Claimant{ - Destination: claimant.MustV0().Destination.Address(), - Predicate: claimant.MustV0().Predicate, - } - } - - return operations.CreateClaimableBalance{ - Base: baseOp, - Asset: createClaimableBalance.Asset.StringCanonical(), - Amount: amount.String(createClaimableBalance.Amount), - Claimants: claimants, - }, nil -} diff --git a/exp/lighthorizon/adapters/create_passive_sell_offer.go b/exp/lighthorizon/adapters/create_passive_sell_offer.go deleted file mode 100644 index 89b2b29e97..0000000000 --- a/exp/lighthorizon/adapters/create_passive_sell_offer.go +++ /dev/null @@ -1,51 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/amount" - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/base" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/errors" -) - -func populateCreatePassiveSellOfferOperation(op *common.Operation, baseOp operations.Base) (operations.CreatePassiveSellOffer, error) { - createPassiveSellOffer := op.Get().Body.MustCreatePassiveSellOfferOp() - - var ( - buyingAssetType string - buyingCode string - buyingIssuer string - ) - err := createPassiveSellOffer.Buying.Extract(&buyingAssetType, &buyingCode, &buyingIssuer) - if err != nil { - return operations.CreatePassiveSellOffer{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - var ( - sellingAssetType string - sellingCode string - sellingIssuer string - ) - err = createPassiveSellOffer.Selling.Extract(&sellingAssetType, &sellingCode, &sellingIssuer) - if err != nil { - return operations.CreatePassiveSellOffer{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - return operations.CreatePassiveSellOffer{ - Offer: operations.Offer{ - Base: baseOp, - Amount: amount.String(createPassiveSellOffer.Amount), - Price: createPassiveSellOffer.Price.String(), - PriceR: base.Price{ - N: int32(createPassiveSellOffer.Price.N), - D: int32(createPassiveSellOffer.Price.D), - }, - BuyingAssetType: buyingAssetType, - BuyingAssetCode: buyingCode, - BuyingAssetIssuer: buyingIssuer, - SellingAssetType: sellingAssetType, - SellingAssetCode: sellingCode, - SellingAssetIssuer: sellingIssuer, - }, - }, nil -} diff --git a/exp/lighthorizon/adapters/end_sponsoring_future_reserves.go b/exp/lighthorizon/adapters/end_sponsoring_future_reserves.go deleted file mode 100644 index b6ca7a1742..0000000000 --- a/exp/lighthorizon/adapters/end_sponsoring_future_reserves.go +++ /dev/null @@ -1,38 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/operations" -) - -func populateEndSponsoringFutureReservesOperation(op *common.Operation, baseOp operations.Base) (operations.EndSponsoringFutureReserves, error) { - return operations.EndSponsoringFutureReserves{ - Base: baseOp, - BeginSponsor: findInitatingSandwichSponsor(op), - // TODO - BeginSponsorMuxed: "", - BeginSponsorMuxedID: 0, - }, nil -} - -func findInitatingSandwichSponsor(op *common.Operation) string { - if !op.TransactionResult.Successful() { - // Failed transactions may not have a compliant sandwich structure - // we can rely on (e.g. invalid nesting or a being operation with the wrong sponsoree ID) - // and thus we bail out since we could return incorrect information. - return "" - } - sponsoree := op.SourceAccount() - operations := op.TransactionEnvelope.Operations() - for i := int(op.OpIndex) - 1; i >= 0; i-- { - if beginOp, ok := operations[i].Body.GetBeginSponsoringFutureReservesOp(); ok && - beginOp.SponsoredId.Address() == sponsoree.Address() { - if operations[i].SourceAccount != nil { - return operations[i].SourceAccount.Address() - } else { - return op.TransactionEnvelope.SourceAccount().ToAccountId().Address() - } - } - } - return "" -} diff --git a/exp/lighthorizon/adapters/inflation.go b/exp/lighthorizon/adapters/inflation.go deleted file mode 100644 index 57c927263d..0000000000 --- a/exp/lighthorizon/adapters/inflation.go +++ /dev/null @@ -1,12 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/operations" -) - -func populateInflationOperation(op *common.Operation, baseOp operations.Base) (operations.Inflation, error) { - return operations.Inflation{ - Base: baseOp, - }, nil -} diff --git a/exp/lighthorizon/adapters/liquidity_pool_deposit.go b/exp/lighthorizon/adapters/liquidity_pool_deposit.go deleted file mode 100644 index f0b4384009..0000000000 --- a/exp/lighthorizon/adapters/liquidity_pool_deposit.go +++ /dev/null @@ -1,33 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/amount" - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/base" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/xdr" -) - -func populateLiquidityPoolDepositOperation(op *common.Operation, baseOp operations.Base) (operations.LiquidityPoolDeposit, error) { - liquidityPoolDeposit := op.Get().Body.MustLiquidityPoolDepositOp() - - return operations.LiquidityPoolDeposit{ - Base: baseOp, - // TODO: some fields missing because derived from meta - LiquidityPoolID: xdr.Hash(liquidityPoolDeposit.LiquidityPoolId).HexString(), - ReservesMax: []base.AssetAmount{ - {Amount: amount.String(liquidityPoolDeposit.MaxAmountA)}, - {Amount: amount.String(liquidityPoolDeposit.MaxAmountB)}, - }, - MinPrice: liquidityPoolDeposit.MinPrice.String(), - MinPriceR: base.Price{ - N: int32(liquidityPoolDeposit.MinPrice.N), - D: int32(liquidityPoolDeposit.MinPrice.D), - }, - MaxPrice: liquidityPoolDeposit.MaxPrice.String(), - MaxPriceR: base.Price{ - N: int32(liquidityPoolDeposit.MaxPrice.N), - D: int32(liquidityPoolDeposit.MaxPrice.D), - }, - }, nil -} diff --git a/exp/lighthorizon/adapters/liquidity_pool_withdraw.go b/exp/lighthorizon/adapters/liquidity_pool_withdraw.go deleted file mode 100644 index c618baf2de..0000000000 --- a/exp/lighthorizon/adapters/liquidity_pool_withdraw.go +++ /dev/null @@ -1,24 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/amount" - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/base" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/xdr" -) - -func populateLiquidityPoolWithdrawOperation(op *common.Operation, baseOp operations.Base) (operations.LiquidityPoolWithdraw, error) { - liquidityPoolWithdraw := op.Get().Body.MustLiquidityPoolWithdrawOp() - - return operations.LiquidityPoolWithdraw{ - Base: baseOp, - // TODO: some fields missing because derived from meta - LiquidityPoolID: xdr.Hash(liquidityPoolWithdraw.LiquidityPoolId).HexString(), - ReservesMin: []base.AssetAmount{ - {Amount: amount.String(liquidityPoolWithdraw.MinAmountA)}, - {Amount: amount.String(liquidityPoolWithdraw.MinAmountB)}, - }, - Shares: amount.String(liquidityPoolWithdraw.Amount), - }, nil -} diff --git a/exp/lighthorizon/adapters/manage_buy_offer.go b/exp/lighthorizon/adapters/manage_buy_offer.go deleted file mode 100644 index ccdd66bc69..0000000000 --- a/exp/lighthorizon/adapters/manage_buy_offer.go +++ /dev/null @@ -1,52 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/amount" - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/base" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/errors" -) - -func populateManageBuyOfferOperation(op *common.Operation, baseOp operations.Base) (operations.ManageBuyOffer, error) { - manageBuyOffer := op.Get().Body.MustManageBuyOfferOp() - - var ( - buyingAssetType string - buyingCode string - buyingIssuer string - ) - err := manageBuyOffer.Buying.Extract(&buyingAssetType, &buyingCode, &buyingIssuer) - if err != nil { - return operations.ManageBuyOffer{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - var ( - sellingAssetType string - sellingCode string - sellingIssuer string - ) - err = manageBuyOffer.Selling.Extract(&sellingAssetType, &sellingCode, &sellingIssuer) - if err != nil { - return operations.ManageBuyOffer{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - return operations.ManageBuyOffer{ - Offer: operations.Offer{ - Base: baseOp, - Amount: amount.String(manageBuyOffer.BuyAmount), - Price: manageBuyOffer.Price.String(), - PriceR: base.Price{ - N: int32(manageBuyOffer.Price.N), - D: int32(manageBuyOffer.Price.D), - }, - BuyingAssetType: buyingAssetType, - BuyingAssetCode: buyingCode, - BuyingAssetIssuer: buyingIssuer, - SellingAssetType: sellingAssetType, - SellingAssetCode: sellingCode, - SellingAssetIssuer: sellingIssuer, - }, - OfferID: int64(manageBuyOffer.OfferId), - }, nil -} diff --git a/exp/lighthorizon/adapters/manage_data.go b/exp/lighthorizon/adapters/manage_data.go deleted file mode 100644 index dd66ed2ae4..0000000000 --- a/exp/lighthorizon/adapters/manage_data.go +++ /dev/null @@ -1,23 +0,0 @@ -package adapters - -import ( - "encoding/base64" - - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/operations" -) - -func populateManageDataOperation(op *common.Operation, baseOp operations.Base) (operations.ManageData, error) { - manageData := op.Get().Body.MustManageDataOp() - - dataValue := "" - if manageData.DataValue != nil { - dataValue = base64.StdEncoding.EncodeToString(*manageData.DataValue) - } - - return operations.ManageData{ - Base: baseOp, - Name: string(manageData.DataName), - Value: dataValue, - }, nil -} diff --git a/exp/lighthorizon/adapters/manage_sell_offer.go b/exp/lighthorizon/adapters/manage_sell_offer.go deleted file mode 100644 index 56893cc1ab..0000000000 --- a/exp/lighthorizon/adapters/manage_sell_offer.go +++ /dev/null @@ -1,52 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/amount" - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/base" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/errors" -) - -func populateManageSellOfferOperation(op *common.Operation, baseOp operations.Base) (operations.ManageSellOffer, error) { - manageSellOffer := op.Get().Body.MustManageSellOfferOp() - - var ( - buyingAssetType string - buyingCode string - buyingIssuer string - ) - err := manageSellOffer.Buying.Extract(&buyingAssetType, &buyingCode, &buyingIssuer) - if err != nil { - return operations.ManageSellOffer{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - var ( - sellingAssetType string - sellingCode string - sellingIssuer string - ) - err = manageSellOffer.Selling.Extract(&sellingAssetType, &sellingCode, &sellingIssuer) - if err != nil { - return operations.ManageSellOffer{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - return operations.ManageSellOffer{ - Offer: operations.Offer{ - Base: baseOp, - Amount: amount.String(manageSellOffer.Amount), - Price: manageSellOffer.Price.String(), - PriceR: base.Price{ - N: int32(manageSellOffer.Price.N), - D: int32(manageSellOffer.Price.D), - }, - BuyingAssetType: buyingAssetType, - BuyingAssetCode: buyingCode, - BuyingAssetIssuer: buyingIssuer, - SellingAssetType: sellingAssetType, - SellingAssetCode: sellingCode, - SellingAssetIssuer: sellingIssuer, - }, - OfferID: int64(manageSellOffer.OfferId), - }, nil -} diff --git a/exp/lighthorizon/adapters/operation.go b/exp/lighthorizon/adapters/operation.go deleted file mode 100644 index a2448c8c58..0000000000 --- a/exp/lighthorizon/adapters/operation.go +++ /dev/null @@ -1,93 +0,0 @@ -package adapters - -import ( - "fmt" - "net/http" - "strconv" - "time" - - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/render/hal" - "github.com/stellar/go/xdr" -) - -func PopulateOperation(r *http.Request, op *common.Operation) (operations.Operation, error) { - hash, err := op.TransactionHash() - if err != nil { - return nil, err - } - - toid := strconv.FormatInt(op.TOID(), 10) - baseOp := operations.Base{ - ID: toid, - PT: toid, - TransactionSuccessful: op.TransactionResult.Successful(), - SourceAccount: op.SourceAccount().Address(), - LedgerCloseTime: time.Unix(int64(op.LedgerHeader.ScpValue.CloseTime), 0).UTC(), - TransactionHash: hash, - Type: operations.TypeNames[op.Get().Body.Type], - TypeI: int32(op.Get().Body.Type), - } - - lb := hal.LinkBuilder{Base: r.URL} - self := fmt.Sprintf("/operations/%s", toid) - baseOp.Links.Self = lb.Link(self) - baseOp.Links.Succeeds = lb.Linkf("/effects?order=desc&cursor=%s", baseOp.PT) - baseOp.Links.Precedes = lb.Linkf("/effects?order=asc&cursor=%s", baseOp.PT) - baseOp.Links.Transaction = lb.Linkf("/transactions/%s", hash) - baseOp.Links.Effects = lb.Link(self, "effects") - - switch op.Get().Body.Type { - case xdr.OperationTypeCreateAccount: - return populateCreateAccountOperation(op, baseOp) - case xdr.OperationTypePayment: - return populatePaymentOperation(op, baseOp) - case xdr.OperationTypePathPaymentStrictReceive: - return populatePathPaymentStrictReceiveOperation(op, baseOp) - case xdr.OperationTypePathPaymentStrictSend: - return populatePathPaymentStrictSendOperation(op, baseOp) - case xdr.OperationTypeManageBuyOffer: - return populateManageBuyOfferOperation(op, baseOp) - case xdr.OperationTypeManageSellOffer: - return populateManageSellOfferOperation(op, baseOp) - case xdr.OperationTypeCreatePassiveSellOffer: - return populateCreatePassiveSellOfferOperation(op, baseOp) - case xdr.OperationTypeSetOptions: - return populateSetOptionsOperation(op, baseOp) - case xdr.OperationTypeChangeTrust: - return populateChangeTrustOperation(op, baseOp) - case xdr.OperationTypeAllowTrust: - return populateAllowTrustOperation(op, baseOp) - case xdr.OperationTypeAccountMerge: - return populateAccountMergeOperation(op, baseOp) - case xdr.OperationTypeInflation: - return populateInflationOperation(op, baseOp) - case xdr.OperationTypeManageData: - return populateManageDataOperation(op, baseOp) - case xdr.OperationTypeBumpSequence: - return populateBumpSequenceOperation(op, baseOp) - case xdr.OperationTypeCreateClaimableBalance: - return populateCreateClaimableBalanceOperation(op, baseOp) - case xdr.OperationTypeClaimClaimableBalance: - return populateClaimClaimableBalanceOperation(op, baseOp) - case xdr.OperationTypeBeginSponsoringFutureReserves: - return populateBeginSponsoringFutureReservesOperation(op, baseOp) - case xdr.OperationTypeEndSponsoringFutureReserves: - return populateEndSponsoringFutureReservesOperation(op, baseOp) - case xdr.OperationTypeRevokeSponsorship: - return populateRevokeSponsorshipOperation(op, baseOp) - case xdr.OperationTypeClawback: - return populateClawbackOperation(op, baseOp) - case xdr.OperationTypeClawbackClaimableBalance: - return populateClawbackClaimableBalanceOperation(op, baseOp) - case xdr.OperationTypeSetTrustLineFlags: - return populateSetTrustLineFlagsOperation(op, baseOp) - case xdr.OperationTypeLiquidityPoolDeposit: - return populateLiquidityPoolDepositOperation(op, baseOp) - case xdr.OperationTypeLiquidityPoolWithdraw: - return populateLiquidityPoolWithdrawOperation(op, baseOp) - default: - return nil, fmt.Errorf("unknown operation type: %s", op.Get().Body.Type) - } -} diff --git a/exp/lighthorizon/adapters/path_payment_strict_receive.go b/exp/lighthorizon/adapters/path_payment_strict_receive.go deleted file mode 100644 index eeaabad969..0000000000 --- a/exp/lighthorizon/adapters/path_payment_strict_receive.go +++ /dev/null @@ -1,78 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/amount" - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/base" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/errors" -) - -func populatePathPaymentStrictReceiveOperation(op *common.Operation, baseOp operations.Base) (operations.PathPayment, error) { - payment := op.Get().Body.MustPathPaymentStrictReceiveOp() - - var ( - sendAssetType string - sendCode string - sendIssuer string - ) - err := payment.SendAsset.Extract(&sendAssetType, &sendCode, &sendIssuer) - if err != nil { - return operations.PathPayment{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - var ( - destAssetType string - destCode string - destIssuer string - ) - err = payment.DestAsset.Extract(&destAssetType, &destCode, &destIssuer) - if err != nil { - return operations.PathPayment{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - sourceAmount := amount.String(0) - if op.TransactionResult.Successful() { - result := op.OperationResult().MustPathPaymentStrictReceiveResult() - sourceAmount = amount.String(result.SendAmount()) - } - - var path = make([]base.Asset, len(payment.Path)) - for i := range payment.Path { - var ( - assetType string - code string - issuer string - ) - err = payment.Path[i].Extract(&assetType, &code, &issuer) - if err != nil { - return operations.PathPayment{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - path[i] = base.Asset{ - Type: assetType, - Code: code, - Issuer: issuer, - } - } - - return operations.PathPayment{ - Payment: operations.Payment{ - Base: baseOp, - From: op.SourceAccount().Address(), - To: payment.Destination.Address(), - Asset: base.Asset{ - Type: destAssetType, - Code: destCode, - Issuer: destIssuer, - }, - Amount: amount.String(payment.DestAmount), - }, - Path: path, - SourceAmount: sourceAmount, - SourceMax: amount.String(payment.SendMax), - SourceAssetType: sendAssetType, - SourceAssetCode: sendCode, - SourceAssetIssuer: sendIssuer, - }, nil -} diff --git a/exp/lighthorizon/adapters/path_payment_strict_send.go b/exp/lighthorizon/adapters/path_payment_strict_send.go deleted file mode 100644 index 0068db30b5..0000000000 --- a/exp/lighthorizon/adapters/path_payment_strict_send.go +++ /dev/null @@ -1,78 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/amount" - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/base" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/errors" -) - -func populatePathPaymentStrictSendOperation(op *common.Operation, baseOp operations.Base) (operations.PathPaymentStrictSend, error) { - payment := op.Get().Body.MustPathPaymentStrictSendOp() - - var ( - sendAssetType string - sendCode string - sendIssuer string - ) - err := payment.SendAsset.Extract(&sendAssetType, &sendCode, &sendIssuer) - if err != nil { - return operations.PathPaymentStrictSend{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - var ( - destAssetType string - destCode string - destIssuer string - ) - err = payment.DestAsset.Extract(&destAssetType, &destCode, &destIssuer) - if err != nil { - return operations.PathPaymentStrictSend{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - destAmount := amount.String(0) - if op.TransactionResult.Successful() { - result := op.OperationResult().MustPathPaymentStrictSendResult() - destAmount = amount.String(result.DestAmount()) - } - - var path = make([]base.Asset, len(payment.Path)) - for i := range payment.Path { - var ( - assetType string - code string - issuer string - ) - err = payment.Path[i].Extract(&assetType, &code, &issuer) - if err != nil { - return operations.PathPaymentStrictSend{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - path[i] = base.Asset{ - Type: assetType, - Code: code, - Issuer: issuer, - } - } - - return operations.PathPaymentStrictSend{ - Payment: operations.Payment{ - Base: baseOp, - From: op.SourceAccount().Address(), - To: payment.Destination.Address(), - Asset: base.Asset{ - Type: destAssetType, - Code: destCode, - Issuer: destIssuer, - }, - Amount: destAmount, - }, - Path: path, - SourceAmount: amount.String(payment.SendAmount), - DestinationMin: amount.String(payment.DestMin), - SourceAssetType: sendAssetType, - SourceAssetCode: sendCode, - SourceAssetIssuer: sendIssuer, - }, nil -} diff --git a/exp/lighthorizon/adapters/payment.go b/exp/lighthorizon/adapters/payment.go deleted file mode 100644 index 97af5f6120..0000000000 --- a/exp/lighthorizon/adapters/payment.go +++ /dev/null @@ -1,35 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/amount" - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/base" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/errors" -) - -func populatePaymentOperation(op *common.Operation, baseOp operations.Base) (operations.Payment, error) { - payment := op.Get().Body.MustPaymentOp() - - var ( - assetType string - code string - issuer string - ) - err := payment.Asset.Extract(&assetType, &code, &issuer) - if err != nil { - return operations.Payment{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - return operations.Payment{ - Base: baseOp, - To: payment.Destination.Address(), - From: op.SourceAccount().Address(), - Asset: base.Asset{ - Type: assetType, - Code: code, - Issuer: issuer, - }, - Amount: amount.StringFromInt64(int64(payment.Amount)), - }, nil -} diff --git a/exp/lighthorizon/adapters/revoke_sponsorship.go b/exp/lighthorizon/adapters/revoke_sponsorship.go deleted file mode 100644 index cb19decc5c..0000000000 --- a/exp/lighthorizon/adapters/revoke_sponsorship.go +++ /dev/null @@ -1,66 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/xdr" -) - -func populateRevokeSponsorshipOperation(op *common.Operation, baseOp operations.Base) (operations.RevokeSponsorship, error) { - revokeSponsorship := op.Get().Body.MustRevokeSponsorshipOp() - - switch revokeSponsorship.Type { - case xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry: - ret := operations.RevokeSponsorship{ - Base: baseOp, - } - - ledgerKey := revokeSponsorship.LedgerKey - - switch ledgerKey.Type { - case xdr.LedgerEntryTypeAccount: - accountID := ledgerKey.Account.AccountId.Address() - ret.AccountID = &accountID - case xdr.LedgerEntryTypeClaimableBalance: - marshalHex, err := xdr.MarshalHex(ledgerKey.ClaimableBalance.BalanceId) - if err != nil { - return operations.RevokeSponsorship{}, err - } - ret.ClaimableBalanceID = &marshalHex - case xdr.LedgerEntryTypeData: - accountID := ledgerKey.Data.AccountId.Address() - dataName := string(ledgerKey.Data.DataName) - ret.DataAccountID = &accountID - ret.DataName = &dataName - case xdr.LedgerEntryTypeOffer: - offerID := int64(ledgerKey.Offer.OfferId) - ret.OfferID = &offerID - case xdr.LedgerEntryTypeTrustline: - trustlineAccountID := ledgerKey.TrustLine.AccountId.Address() - ret.TrustlineAccountID = &trustlineAccountID - if ledgerKey.TrustLine.Asset.Type == xdr.AssetTypeAssetTypePoolShare { - trustlineLiquidityPoolID := xdr.Hash(*ledgerKey.TrustLine.Asset.LiquidityPoolId).HexString() - ret.TrustlineLiquidityPoolID = &trustlineLiquidityPoolID - } else { - trustlineAsset := ledgerKey.TrustLine.Asset.ToAsset().StringCanonical() - ret.TrustlineAsset = &trustlineAsset - } - default: - return operations.RevokeSponsorship{}, errors.Errorf("invalid ledger key type: %d", ledgerKey.Type) - } - - return ret, nil - case xdr.RevokeSponsorshipTypeRevokeSponsorshipSigner: - signerAccountID := revokeSponsorship.Signer.AccountId.Address() - signerKey := revokeSponsorship.Signer.SignerKey.Address() - - return operations.RevokeSponsorship{ - Base: baseOp, - SignerAccountID: &signerAccountID, - SignerKey: &signerKey, - }, nil - } - - return operations.RevokeSponsorship{}, errors.Errorf("invalid revoke type: %d", revokeSponsorship.Type) -} diff --git a/exp/lighthorizon/adapters/set_options.go b/exp/lighthorizon/adapters/set_options.go deleted file mode 100644 index cf2cdeb20f..0000000000 --- a/exp/lighthorizon/adapters/set_options.go +++ /dev/null @@ -1,122 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/xdr" -) - -func populateSetOptionsOperation(op *common.Operation, baseOp operations.Base) (operations.SetOptions, error) { - setOptions := op.Get().Body.MustSetOptionsOp() - - homeDomain := "" - if setOptions.HomeDomain != nil { - homeDomain = string(*setOptions.HomeDomain) - } - - inflationDest := "" - if setOptions.InflationDest != nil { - inflationDest = setOptions.InflationDest.Address() - } - - var signerKey string - var signerWeight *int - if setOptions.Signer != nil { - signerKey = setOptions.Signer.Key.Address() - signerWeightInt := int(setOptions.Signer.Weight) - signerWeight = &signerWeightInt - } - - var masterKeyWeight, lowThreshold, medThreshold, highThreshold *int - if setOptions.MasterWeight != nil { - masterKeyWeightInt := int(*setOptions.MasterWeight) - masterKeyWeight = &masterKeyWeightInt - } - if setOptions.LowThreshold != nil { - lowThresholdInt := int(*setOptions.LowThreshold) - lowThreshold = &lowThresholdInt - } - if setOptions.MedThreshold != nil { - medThresholdInt := int(*setOptions.MedThreshold) - medThreshold = &medThresholdInt - } - if setOptions.HighThreshold != nil { - highThresholdInt := int(*setOptions.HighThreshold) - highThreshold = &highThresholdInt - } - - var ( - setFlags []int - setFlagsS []string - - clearFlags []int - clearFlagsS []string - ) - - if setOptions.SetFlags != nil && *setOptions.SetFlags > 0 { - f := xdr.AccountFlags(*setOptions.SetFlags) - - if f.IsAuthRequired() { - setFlags = append(setFlags, int(xdr.AccountFlagsAuthRequiredFlag)) - setFlagsS = append(setFlagsS, "auth_required") - } - - if f.IsAuthRevocable() { - setFlags = append(setFlags, int(xdr.AccountFlagsAuthRevocableFlag)) - setFlagsS = append(setFlagsS, "auth_revocable") - } - - if f.IsAuthImmutable() { - setFlags = append(setFlags, int(xdr.AccountFlagsAuthImmutableFlag)) - setFlagsS = append(setFlagsS, "auth_immutable") - } - - if f.IsAuthClawbackEnabled() { - setFlags = append(setFlags, int(xdr.AccountFlagsAuthClawbackEnabledFlag)) - setFlagsS = append(setFlagsS, "auth_clawback_enabled") - } - } - - if setOptions.ClearFlags != nil && *setOptions.ClearFlags > 0 { - f := xdr.AccountFlags(*setOptions.ClearFlags) - - if f.IsAuthRequired() { - clearFlags = append(clearFlags, int(xdr.AccountFlagsAuthRequiredFlag)) - clearFlagsS = append(clearFlagsS, "auth_required") - } - - if f.IsAuthRevocable() { - clearFlags = append(clearFlags, int(xdr.AccountFlagsAuthRevocableFlag)) - clearFlagsS = append(clearFlagsS, "auth_revocable") - } - - if f.IsAuthImmutable() { - clearFlags = append(clearFlags, int(xdr.AccountFlagsAuthImmutableFlag)) - clearFlagsS = append(clearFlagsS, "auth_immutable") - } - - if f.IsAuthClawbackEnabled() { - clearFlags = append(clearFlags, int(xdr.AccountFlagsAuthClawbackEnabledFlag)) - clearFlagsS = append(clearFlagsS, "auth_clawback_enabled") - } - } - - return operations.SetOptions{ - Base: baseOp, - HomeDomain: homeDomain, - InflationDest: inflationDest, - - MasterKeyWeight: masterKeyWeight, - SignerKey: signerKey, - SignerWeight: signerWeight, - - SetFlags: setFlags, - SetFlagsS: setFlagsS, - ClearFlags: clearFlags, - ClearFlagsS: clearFlagsS, - - LowThreshold: lowThreshold, - MedThreshold: medThreshold, - HighThreshold: highThreshold, - }, nil -} diff --git a/exp/lighthorizon/adapters/set_trust_line_flags.go b/exp/lighthorizon/adapters/set_trust_line_flags.go deleted file mode 100644 index 2969dcb2b5..0000000000 --- a/exp/lighthorizon/adapters/set_trust_line_flags.go +++ /dev/null @@ -1,83 +0,0 @@ -package adapters - -import ( - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/protocols/horizon/base" - "github.com/stellar/go/protocols/horizon/operations" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/xdr" -) - -func populateSetTrustLineFlagsOperation(op *common.Operation, baseOp operations.Base) (operations.SetTrustLineFlags, error) { - setTrustLineFlags := op.Get().Body.MustSetTrustLineFlagsOp() - - var ( - assetType string - code string - issuer string - ) - err := setTrustLineFlags.Asset.Extract(&assetType, &code, &issuer) - if err != nil { - return operations.SetTrustLineFlags{}, errors.Wrap(err, "xdr.Asset.Extract error") - } - - var ( - setFlags []int - setFlagsS []string - - clearFlags []int - clearFlagsS []string - ) - - if setTrustLineFlags.SetFlags > 0 { - f := xdr.TrustLineFlags(setTrustLineFlags.SetFlags) - - if f.IsAuthorized() { - setFlags = append(setFlags, int(xdr.TrustLineFlagsAuthorizedFlag)) - setFlagsS = append(setFlagsS, "authorized") - } - - if f.IsAuthorizedToMaintainLiabilitiesFlag() { - setFlags = append(setFlags, int(xdr.TrustLineFlagsAuthorizedToMaintainLiabilitiesFlag)) - setFlagsS = append(setFlagsS, "authorized_to_maintain_liabilites") - } - - if f.IsClawbackEnabledFlag() { - setFlags = append(setFlags, int(xdr.TrustLineFlagsTrustlineClawbackEnabledFlag)) - setFlagsS = append(setFlagsS, "clawback_enabled") - } - } - - if setTrustLineFlags.ClearFlags > 0 { - f := xdr.TrustLineFlags(setTrustLineFlags.ClearFlags) - - if f.IsAuthorized() { - clearFlags = append(clearFlags, int(xdr.TrustLineFlagsAuthorizedFlag)) - clearFlagsS = append(clearFlagsS, "authorized") - } - - if f.IsAuthorizedToMaintainLiabilitiesFlag() { - clearFlags = append(clearFlags, int(xdr.TrustLineFlagsAuthorizedToMaintainLiabilitiesFlag)) - clearFlagsS = append(clearFlagsS, "authorized_to_maintain_liabilites") - } - - if f.IsClawbackEnabledFlag() { - clearFlags = append(clearFlags, int(xdr.TrustLineFlagsTrustlineClawbackEnabledFlag)) - clearFlagsS = append(clearFlagsS, "clawback_enabled") - } - } - - return operations.SetTrustLineFlags{ - Base: baseOp, - Asset: base.Asset{ - Type: assetType, - Code: code, - Issuer: issuer, - }, - Trustor: setTrustLineFlags.Trustor.Address(), - SetFlags: setFlags, - SetFlagsS: setFlagsS, - ClearFlags: clearFlags, - ClearFlagsS: clearFlagsS, - }, nil -} diff --git a/exp/lighthorizon/adapters/testdata/transactions.json b/exp/lighthorizon/adapters/testdata/transactions.json deleted file mode 100644 index 6128801533..0000000000 --- a/exp/lighthorizon/adapters/testdata/transactions.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "_links": { - "self": { - "href": "https://horizon.stellar.org/accounts/GBFHFINUD6NVGSX33PY25DDRCABN3H2JTDMLUEXAUEJVV22HTXVGLEZD/transactions?cursor=179530990183178241\u0026limit=1\u0026order=desc" - }, - "next": { - "href": "https://horizon.stellar.org/accounts/GBFHFINUD6NVGSX33PY25DDRCABN3H2JTDMLUEXAUEJVV22HTXVGLEZD/transactions?cursor=179530990183174144\u0026limit=1\u0026order=desc" - }, - "prev": { - "href": "https://horizon.stellar.org/accounts/GBFHFINUD6NVGSX33PY25DDRCABN3H2JTDMLUEXAUEJVV22HTXVGLEZD/transactions?cursor=179530990183174144\u0026limit=1\u0026order=asc" - } - }, - "_embedded": { - "records": [ - { - "_links": { - "self": { - "href": "https://horizon.stellar.org/transactions/55d8aa3693489ffc1d70b8ba33b8b5c012ec098f6f104383e3f090048488febd" - }, - "account": { - "href": "https://horizon.stellar.org/accounts/GBFHFINUD6NVGSX33PY25DDRCABN3H2JTDMLUEXAUEJVV22HTXVGLEZD" - }, - "ledger": { - "href": "https://horizon.stellar.org/ledgers/41800316" - }, - "operations": { - "href": "https://horizon.stellar.org/transactions/55d8aa3693489ffc1d70b8ba33b8b5c012ec098f6f104383e3f090048488febd/operations{?cursor,limit,order}", - "templated": true - }, - "effects": { - "href": "https://horizon.stellar.org/transactions/55d8aa3693489ffc1d70b8ba33b8b5c012ec098f6f104383e3f090048488febd/effects{?cursor,limit,order}", - "templated": true - }, - "precedes": { - "href": "https://horizon.stellar.org/transactions?order=asc\u0026cursor=179530990183174144" - }, - "succeeds": { - "href": "https://horizon.stellar.org/transactions?order=desc\u0026cursor=179530990183174144" - }, - "transaction": { - "href": "https://horizon.stellar.org/transactions/55d8aa3693489ffc1d70b8ba33b8b5c012ec098f6f104383e3f090048488febd" - } - }, - "id": "55d8aa3693489ffc1d70b8ba33b8b5c012ec098f6f104383e3f090048488febd", - "paging_token": "179530990183174144", - "successful": true, - "hash": "55d8aa3693489ffc1d70b8ba33b8b5c012ec098f6f104383e3f090048488febd", - "ledger": 41800316, - "created_at": "2022-07-17T13:08:41Z", - "source_account": "GBFHFINUD6NVGSX33PY25DDRCABN3H2JTDMLUEXAUEJVV22HTXVGLEZD", - "source_account_sequence": "172589382434294350", - "fee_account": "GBFHFINUD6NVGSX33PY25DDRCABN3H2JTDMLUEXAUEJVV22HTXVGLEZD", - "fee_charged": "100", - "max_fee": "100000", - "operation_count": 1, - "envelope_xdr": "AAAAAgAAAABKcqG0H5tTSvvb8a6McRAC3Z9JmNi6EuChE1rrR53qZQABhqACZSkhAAAKTgAAAAAAAAAAAAAAAQAAAAEAAAAASnKhtB+bU0r72/GujHEQAt2fSZjYuhLgoRNa60ed6mUAAAANAAAAAXlYTE0AAAAAIjbXcP4NPgFSGXXVz3rEhCtwldaxqddo0+mmMumZBr4AAAACVAvkAAAAAABKcqG0H5tTSvvb8a6McRAC3Z9JmNi6EuChE1rrR53qZQAAAAJEUklGVAAAAAAAAAAAAAAAvSOzPqUOGnDIcJOm7T85qDFRM0wfOVoubgkEPk95DZ0AAAEQvqAGdQAAAAEAAAAAAAAAAAAAAAFHneplAAAAQAVm9muIrK31Z+m2ZvhDYhtuoHcc/n+MO0DOaiQjfW+tsUNVCOw7foHiDRVLBdAHBZT+xxa3F+Ek9wQiKzxtQQM=", - "result_xdr": "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAANAAAAAAAAAAIAAAABAAAAAPaTW9sBV2ja6yDUtPcpGpUrnVEHaTHC4I065TklIsguAAAAAD1H0goAAAAAAAAAAlQE8wsAAAABeVhMTQAAAAAiNtdw/g0+AVIZddXPesSEK3CV1rGp12jT6aYy6ZkGvgAAAAJUC+QAAAAAAgRnzllf8Sas0MUQlkxROsBgUzEoIN2XrYP9tlH5SINjAAAAAkRSSUZUAAAAAAAAAAAAAAC9I7M+pQ4acMhwk6btPzmoMVEzTB85Wi5uCQQ+T3kNnQAAARN/56NFAAAAAAAAAAJUBPMLAAAAAEpyobQfm1NK+9vxroxxEALdn0mY2LoS4KETWutHneplAAAAAkRSSUZUAAAAAAAAAAAAAAC9I7M+pQ4acMhwk6btPzmoMVEzTB85Wi5uCQQ+T3kNnQAAARN/56NFAAAAAA==", - "result_meta_xdr": "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", - "fee_meta_xdr": "AAAAAgAAAAMCfdJjAAAAAAAAAABKcqG0H5tTSvvb8a6McRAC3Z9JmNi6EuChE1rrR53qZQAAAAAQ2K4DAmUpIQAACk0AAAAKAAAAAQAAAADEccZDcGLJUGqJNC5TihraQE0vQc8dOiVfQyH3xuDhdQAAAAAAAAAJbG9ic3RyLmNvAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAJ90mMAAAAAYtQJSQAAAAAAAAABAn3SfAAAAAAAAAAASnKhtB+bU0r72/GujHEQAt2fSZjYuhLgoRNa60ed6mUAAAAAENitnwJlKSEAAApNAAAACgAAAAEAAAAAxHHGQ3BiyVBqiTQuU4oa2kBNL0HPHTolX0Mh98bg4XUAAAAAAAAACWxvYnN0ci5jbwAAAAEAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAwAAAAACfdJjAAAAAGLUCUkAAAAA", - "memo_type": "none", - "signatures": [ - "BWb2a4isrfVn6bZm+ENiG26gdxz+f4w7QM5qJCN9b62xQ1UI7Dt+geINFUsF0AcFlP7HFrcX4ST3BCIrPG1BAw==" - ] - } - ] - } -} \ No newline at end of file diff --git a/exp/lighthorizon/adapters/transaction.go b/exp/lighthorizon/adapters/transaction.go deleted file mode 100644 index 6942668c8d..0000000000 --- a/exp/lighthorizon/adapters/transaction.go +++ /dev/null @@ -1,295 +0,0 @@ -package adapters - -import ( - "bytes" - "encoding/base64" - "encoding/hex" - "fmt" - "net/url" - "strconv" - "strings" - "time" - "unicode/utf8" - - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/exp/lighthorizon/ingester" - "github.com/stellar/go/network" - protocol "github.com/stellar/go/protocols/horizon" - "github.com/stellar/go/support/render/hal" - "github.com/stellar/go/xdr" - "golang.org/x/exp/constraints" -) - -// PopulateTransaction converts between ingested XDR and RESTful JSON. In -// Horizon Classic, the data goes from Captive Core -> DB -> JSON. In our case, -// there's no DB intermediary, so we need to directly translate. -func PopulateTransaction( - baseUrl *url.URL, - tx *common.Transaction, - encoder *xdr.EncodingBuffer, -) (dest protocol.Transaction, err error) { - txHash, err := tx.TransactionHash() - if err != nil { - return - } - - dest.ID = txHash - dest.Successful = tx.Result.Successful() - dest.Hash = txHash - dest.Ledger = int32(tx.LedgerHeader.LedgerSeq) - dest.LedgerCloseTime = time.Unix(int64(tx.LedgerHeader.ScpValue.CloseTime), 0).UTC() - - source := tx.SourceAccount() - dest.Account = source.ToAccountId().Address() - if _, ok := source.GetMed25519(); ok { - dest.AccountMuxed, err = source.GetAddress() - if err != nil { - return - } - dest.AccountMuxedID, err = source.GetId() - if err != nil { - return - } - } - dest.AccountSequence = tx.Envelope.SeqNum() - - envelopeBase64, err := encoder.MarshalBase64(tx.Envelope) - if err != nil { - return - } - resultBase64, err := encoder.MarshalBase64(&tx.Result.Result) - if err != nil { - return - } - metaBase64, err := encoder.MarshalBase64(tx.UnsafeMeta) - if err != nil { - return - } - feeMetaBase64, err := encoder.MarshalBase64(tx.FeeChanges) - if err != nil { - return - } - - dest.OperationCount = int32(len(tx.Envelope.Operations())) - dest.EnvelopeXdr = envelopeBase64 - dest.ResultXdr = resultBase64 - dest.ResultMetaXdr = metaBase64 - dest.FeeMetaXdr = feeMetaBase64 - dest.MemoType = memoType(*tx.LedgerTransaction) - if m, ok := memo(*tx.LedgerTransaction); ok { - dest.Memo = m - if dest.MemoType == "text" { - var mb string - if mb, err = memoBytes(envelopeBase64); err != nil { - return - } else { - dest.MemoBytes = mb - } - } - } - - dest.Signatures = signatures(tx.Envelope.Signatures()) - - // If we never use this, we'll remove it later. This just defends us against - // nil dereferences. - dest.Preconditions = &protocol.TransactionPreconditions{} - - if tb := tx.Envelope.Preconditions().TimeBounds; tb != nil { - dest.Preconditions.TimeBounds = &protocol.TransactionPreconditionsTimebounds{ - MaxTime: formatTime(tb.MaxTime), - MinTime: formatTime(tb.MinTime), - } - } - - if lb := tx.Envelope.LedgerBounds(); lb != nil { - dest.Preconditions.LedgerBounds = &protocol.TransactionPreconditionsLedgerbounds{ - MinLedger: uint32(lb.MinLedger), - MaxLedger: uint32(lb.MaxLedger), - } - } - - if minSeq := tx.Envelope.MinSeqNum(); minSeq != nil { - dest.Preconditions.MinAccountSequence = fmt.Sprint(*minSeq) - } - - if minSeqAge := tx.Envelope.MinSeqAge(); minSeqAge != nil && *minSeqAge > 0 { - dest.Preconditions.MinAccountSequenceAge = formatTime(*minSeqAge) - } - - if minSeqGap := tx.Envelope.MinSeqLedgerGap(); minSeqGap != nil { - dest.Preconditions.MinAccountSequenceLedgerGap = uint32(*minSeqGap) - } - - if signers := tx.Envelope.ExtraSigners(); len(signers) > 0 { - dest.Preconditions.ExtraSigners = formatSigners(signers) - } - - if tx.Envelope.IsFeeBump() { - innerTx, ok := tx.Envelope.FeeBump.Tx.InnerTx.GetV1() - if !ok { - panic("Failed to parse inner transaction from fee-bump tx.") - } - - var rawInnerHash [32]byte - rawInnerHash, err = network.HashTransaction(innerTx.Tx, tx.NetworkPassphrase) - if err != nil { - return - } - innerHash := hex.EncodeToString(rawInnerHash[:]) - - feeAccountMuxed := tx.Envelope.FeeBumpAccount() - dest.FeeAccount = feeAccountMuxed.ToAccountId().Address() - if _, ok := feeAccountMuxed.GetMed25519(); ok { - dest.FeeAccountMuxed, err = feeAccountMuxed.GetAddress() - if err != nil { - return - } - dest.FeeAccountMuxedID, err = feeAccountMuxed.GetId() - if err != nil { - return - } - } - - dest.MaxFee = tx.Envelope.FeeBumpFee() - dest.FeeBumpTransaction = &protocol.FeeBumpTransaction{ - Hash: txHash, - Signatures: signatures(tx.Envelope.FeeBumpSignatures()), - } - dest.InnerTransaction = &protocol.InnerTransaction{ - Hash: innerHash, - MaxFee: int64(innerTx.Tx.Fee), - Signatures: signatures(tx.Envelope.Signatures()), - } - // TODO: Figure out what this means? Maybe @tamirms knows. - // if transactionHash != row.TransactionHash { - // dest.Signatures = dest.InnerTransaction.Signatures - // } - } else { - dest.FeeAccount = dest.Account - dest.FeeAccountMuxed = dest.AccountMuxed - dest.FeeAccountMuxedID = dest.AccountMuxedID - dest.MaxFee = int64(tx.Envelope.Fee()) - } - dest.FeeCharged = int64(tx.Result.Result.FeeCharged) - - lb := hal.LinkBuilder{Base: baseUrl} - dest.PT = strconv.FormatUint(uint64(tx.TOID()), 10) - dest.Links.Account = lb.Link("/accounts", dest.Account) - dest.Links.Ledger = lb.Link("/ledgers", fmt.Sprint(dest.Ledger)) - dest.Links.Operations = lb.PagedLink("/transactions", dest.ID, "operations") - dest.Links.Effects = lb.PagedLink("/transactions", dest.ID, "effects") - dest.Links.Self = lb.Link("/transactions", dest.ID) - dest.Links.Transaction = dest.Links.Self - dest.Links.Succeeds = lb.Linkf("/transactions?order=desc&cursor=%s", dest.PT) - dest.Links.Precedes = lb.Linkf("/transactions?order=asc&cursor=%s", dest.PT) - - // If we didn't need the structure, drop it. - if !tx.HasPreconditions() { - dest.Preconditions = nil - } - - return -} - -func formatSigners(s []xdr.SignerKey) []string { - if s == nil { - return nil - } - - signers := make([]string, len(s)) - for i, key := range s { - signers[i] = key.Address() - } - return signers -} - -func signatures(xdrSignatures []xdr.DecoratedSignature) []string { - signatures := make([]string, len(xdrSignatures)) - for i, sig := range xdrSignatures { - signatures[i] = base64.StdEncoding.EncodeToString(sig.Signature) - } - return signatures -} - -func memoType(tx ingester.LedgerTransaction) string { - switch tx.Envelope.Memo().Type { - case xdr.MemoTypeMemoNone: - return "none" - case xdr.MemoTypeMemoText: - return "text" - case xdr.MemoTypeMemoId: - return "id" - case xdr.MemoTypeMemoHash: - return "hash" - case xdr.MemoTypeMemoReturn: - return "return" - default: - panic(fmt.Errorf("invalid memo type: %v", tx.Envelope.Memo().Type)) - } -} - -func memo(tx ingester.LedgerTransaction) (value string, valid bool) { - valid = true - memo := tx.Envelope.Memo() - - switch memo.Type { - case xdr.MemoTypeMemoNone: - value, valid = "", false - - case xdr.MemoTypeMemoText: - scrubbed := scrub(memo.MustText()) - notnull := strings.Join(strings.Split(scrubbed, "\x00"), "") - value = notnull - - case xdr.MemoTypeMemoId: - value = fmt.Sprintf("%d", memo.MustId()) - - case xdr.MemoTypeMemoHash: - hash := memo.MustHash() - value = base64.StdEncoding.EncodeToString(hash[:]) - - case xdr.MemoTypeMemoReturn: - hash := memo.MustRetHash() - value = base64.StdEncoding.EncodeToString(hash[:]) - - default: - panic(fmt.Errorf("invalid memo type: %v", memo.Type)) - } - - return -} - -func memoBytes(envelopeXDR string) (string, error) { - var parsedEnvelope xdr.TransactionEnvelope - if err := xdr.SafeUnmarshalBase64(envelopeXDR, &parsedEnvelope); err != nil { - return "", err - } - - memo := *parsedEnvelope.Memo().Text - return base64.StdEncoding.EncodeToString([]byte(memo)), nil -} - -// scrub ensures that a given string is valid utf-8, replacing any invalid byte -// sequences with the utf-8 replacement character. -func scrub(in string) string { - // First check validity using the stdlib, returning if the string is already - // valid - if utf8.ValidString(in) { - return in - } - - left := []byte(in) - var result bytes.Buffer - - for len(left) > 0 { - r, n := utf8.DecodeRune(left) - result.WriteRune(r) // never errors, only panics - left = left[n:] - } - - return result.String() -} - -func formatTime[T constraints.Integer](t T) string { - return strconv.FormatUint(uint64(t), 10) -} diff --git a/exp/lighthorizon/adapters/transaction_test.go b/exp/lighthorizon/adapters/transaction_test.go deleted file mode 100644 index 5a8ba4ab80..0000000000 --- a/exp/lighthorizon/adapters/transaction_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package adapters - -import ( - "encoding/json" - "net/url" - "os" - "path/filepath" - "strconv" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/exp/lighthorizon/ingester" - "github.com/stellar/go/ingest" - "github.com/stellar/go/network" - protocol "github.com/stellar/go/protocols/horizon" - "github.com/stellar/go/toid" - "github.com/stellar/go/xdr" -) - -// TestTransactionAdapter confirms that the adapter correctly serializes a -// transaction to JSON by actually pulling a transaction from the -// known-to-be-true horizon.stellar.org, turning it into an "ingested" -// transaction, and serializing it. -func TestTransactionAdapter(t *testing.T) { - f, err := os.Open(filepath.Join("./testdata", "transactions.json")) - require.NoErrorf(t, err, "are fixtures missing?") - - page := protocol.TransactionsPage{} - decoder := json.NewDecoder(f) - require.NoError(t, decoder.Decode(&page)) - require.Len(t, page.Embedded.Records, 1) - expectedTx := page.Embedded.Records[0] - - parsedUrl, err := url.Parse(page.Links.Self.Href) - require.NoError(t, err) - parsedToid, err := strconv.ParseInt(expectedTx.PagingToken(), 10, 64) - require.NoError(t, err) - expectedTxIndex := toid.Parse(parsedToid).TransactionOrder - - txEnv := xdr.TransactionEnvelope{} - txResult := xdr.TransactionResult{} - txMeta := xdr.TransactionMeta{} - txFeeMeta := xdr.LedgerEntryChanges{} - - require.NoError(t, xdr.SafeUnmarshalBase64(expectedTx.EnvelopeXdr, &txEnv)) - require.NoError(t, xdr.SafeUnmarshalBase64(expectedTx.ResultMetaXdr, &txMeta)) - require.NoError(t, xdr.SafeUnmarshalBase64(expectedTx.ResultXdr, &txResult)) - require.NoError(t, xdr.SafeUnmarshalBase64(expectedTx.FeeMetaXdr, &txFeeMeta)) - - closeTimestamp := expectedTx.LedgerCloseTime.UTC().Unix() - - tx := common.Transaction{ - LedgerTransaction: &ingester.LedgerTransaction{ - LedgerTransaction: &ingest.LedgerTransaction{ - Index: 0, - Envelope: txEnv, - Result: xdr.TransactionResultPair{ - TransactionHash: xdr.Hash{}, - Result: txResult, - }, - FeeChanges: txFeeMeta, - UnsafeMeta: txMeta, - }, - }, - LedgerHeader: &xdr.LedgerHeader{ - LedgerSeq: xdr.Uint32(expectedTx.Ledger), - ScpValue: xdr.StellarValue{ - CloseTime: xdr.TimePoint(closeTimestamp), - }, - }, - TxIndex: expectedTxIndex - 1, // TOIDs have a 1-based index - NetworkPassphrase: network.PublicNetworkPassphrase, - } - - result, err := PopulateTransaction(parsedUrl, &tx, xdr.NewEncodingBuffer()) - require.NoError(t, err) - assert.Equal(t, expectedTx, result) -} diff --git a/exp/lighthorizon/build/README.md b/exp/lighthorizon/build/README.md deleted file mode 100644 index d9dcf4556d..0000000000 --- a/exp/lighthorizon/build/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Light Horizon services deployment - -Light Horizon is composed of a few micro services: -* index-batch - contains map and reduce binaries to parallize tx-meta reads and index writes. -* index-single - contains single binary that reads tx-meta and writes indexes. -* ledgerexporter - contains single binary that reads from captive core and writes tx-meta -* web - contains single binary that runs web api which reads from tx-meta and index. - -See [godoc](https://godoc.org/github.com/stellar/go/exp/lighthorizon) for details on each service. - -## Buiding docker images of each service -Each service is packaged into a Docker image, use the helper script included here to build: -`./build.sh ` - -example to build just the mydockerhubname/lighthorizon-index-single:latest image to docker local images, no push to registry: -`./build.sh index-single mydockerhubname latest false` - -example to build images for all the services and push them to mydockerhubname/lighthorizon-:testversion: -`./build.sh all mydockerhubname testversion true` - -## Deploy service images on kubernetes(k8s) -* `k8s/ledgerexporter.yml` - creates a deployment with ledgerexporter image and supporting resources, such as configmap, secret, pvc for captive core on-disk storage. Review the settings to confirm they work in your environment before deployment. -* `k8s/lighthorizon_index.yml` - creates a deployment with index-single image and supporting resources, such as configmap, secret. Review the settings to confirm they work in your environment before deployment. -* `k8s/lighthorizon_web.yml` - creates a deployment with the web image and supporting resources, such as configmap, ingress rule. Review the settings to confirm they work in your environment before deployment. diff --git a/exp/lighthorizon/build/build.sh b/exp/lighthorizon/build/build.sh deleted file mode 100755 index e884fc4914..0000000000 --- a/exp/lighthorizon/build/build.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -e - -# Move to repo root -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd "$DIR/../../.." -# module name is the sub-folder name under ./build -MODULE=$1 -DOCKER_REPO_PREFIX=$2 -DOCKER_TAG=$3 -DOCKER_PUSH=$4 - -if [ -z "$MODULE" ] ||\ - [ -z "$DOCKER_REPO_PREFIX" ] ||\ - [ -z "$DOCKER_TAG" ] ||\ - [ -z "$DOCKER_PUSH" ]; then - echo "invalid parameters, requires './build.sh '" - exit 1 -fi - -build_target () { - DOCKER_LABEL="$DOCKER_REPO_PREFIX"/lighthorizon-"$MODULE":"$DOCKER_TAG" - docker build --tag $DOCKER_LABEL --platform linux/amd64 -f "exp/lighthorizon/build/$MODULE/Dockerfile" . - if [ "$DOCKER_PUSH" == "true" ]; then - docker push $DOCKER_LABEL - fi -} - -case $MODULE in -index-batch) - build_target - ;; -ledgerexporter) - build_target - ;; -index-single) - build_target - ;; -web) - build_target - ;; -all) - MODULE=index-batch - build_target - MODULE=web - build_target - MODULE=index-single - build_target - MODULE=ledgerexporter - build_target - ;; -*) - echo "unknown MODULE build parameter ('$MODULE'), must be one of all|index-batch|web|index-single|ledgerexporter" - exit 1 - ;; -esac - diff --git a/exp/lighthorizon/build/index-batch/Dockerfile b/exp/lighthorizon/build/index-batch/Dockerfile deleted file mode 100644 index 1780df682f..0000000000 --- a/exp/lighthorizon/build/index-batch/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM golang:1.20 AS builder - -WORKDIR /go/src/github.com/stellar/go -COPY . ./ -RUN go mod download -RUN go install github.com/stellar/go/exp/lighthorizon/index/cmd/batch/map -RUN go install github.com/stellar/go/exp/lighthorizon/index/cmd/batch/reduce - -FROM ubuntu:22.04 -ENV DEBIAN_FRONTEND=noninteractive -# ca-certificates are required to make tls connections -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl wget gnupg apt-utils -RUN apt-get clean - -COPY --from=builder /go/src/github.com/stellar/go/exp/lighthorizon/build/index-batch/start ./ -COPY --from=builder /go/bin/map ./ -COPY --from=builder /go/bin/reduce ./ -RUN ["chmod", "+x", "/start"] - -ENTRYPOINT ["/start"] diff --git a/exp/lighthorizon/build/index-batch/README.md b/exp/lighthorizon/build/index-batch/README.md deleted file mode 100644 index c300066536..0000000000 --- a/exp/lighthorizon/build/index-batch/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# `stellar/lighthorizon-index-batch` - -This docker image contains the ledger/checkpoint indexing executables. It allows running multiple instances of `map`/`reduce` on a single machine or running it in [AWS Batch](https://aws.amazon.com/batch/). - -## Env variables - -See the [package documentation](../../index/cmd/batch/doc.go) for more details diff --git a/exp/lighthorizon/build/index-batch/start b/exp/lighthorizon/build/index-batch/start deleted file mode 100644 index 88fb5335fb..0000000000 --- a/exp/lighthorizon/build/index-batch/start +++ /dev/null @@ -1,17 +0,0 @@ -#! /usr/bin/env bash -set -e - -# RUN_MODE must be set to 'map' or 'reduce' - -export TRACY_NO_INVARIANT_CHECK=1 -NETWORK_PASSPHRASE="${NETWORK_PASSPHRASE:=Public Global Stellar Network ; September 2015}" -if [ "$RUN_MODE" == "reduce" ]; then - echo "Running Reduce, REDUCE JOBS: $REDUCE_JOB_COUNT MAP JOBS: $MAP_JOB_COUNT TARGET INDEX: $INDEX_TARGET" - /reduce -elif [ "$RUN_MODE" == "map" ]; then - echo "Running Map, TARGET INDEX: $INDEX_TARGET FIRST CHECKPOINT: $FIRST_CHECKPOINT" - /map -else - echo "error: undefined RUN_MODE env variable ('$RUN_MODE'), must be 'map' or 'reduce'" - exit 1 -fi diff --git a/exp/lighthorizon/build/index-single/Dockerfile b/exp/lighthorizon/build/index-single/Dockerfile deleted file mode 100644 index 1473f59f5c..0000000000 --- a/exp/lighthorizon/build/index-single/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM golang:1.20 AS builder - -WORKDIR /go/src/github.com/stellar/go -COPY . ./ -RUN go mod download -RUN go install github.com/stellar/go/exp/lighthorizon/index/cmd/single - -FROM ubuntu:22.04 - -ENV DEBIAN_FRONTEND=noninteractive -# ca-certificates are required to make tls connections -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl wget gnupg apt-utils -RUN apt-get clean - -COPY --from=builder /go/bin/single ./ - -ENTRYPOINT ./single \ - -source "$TXMETA_SOURCE" \ - -target "$INDEXES_SOURCE" \ - -network-passphrase "$NETWORK_PASSPHRASE" \ - -start "$START" \ - -end "$END" \ - -modules "$MODULES" \ - -watch="$WATCH" \ - -workers "$WORKERS" diff --git a/exp/lighthorizon/build/k8s/ledgerexporter.yml b/exp/lighthorizon/build/k8s/ledgerexporter.yml deleted file mode 100644 index 290dd85c63..0000000000 --- a/exp/lighthorizon/build/k8s/ledgerexporter.yml +++ /dev/null @@ -1,125 +0,0 @@ -# this file contains the ledgerexporter deployment and it's config artifacts. -# -# when applying the manifest on a cluster, make sure to include namespace destination, -# as the manifest does not specify namespace, otherwise it'll go in your current kubectl context. -# -# make sure to set the secrets values, substitue placeholders. -# -# $ kubectl apply -f ledgerexporter.yml -n horizon-dev -apiVersion: v1 -kind: ConfigMap -metadata: - annotations: - fluxcd.io/ignore: "true" - labels: - app: ledgerexporter - name: ledgerexporter-pubnet-env -data: - # when using core 'on disk', the earliest ledger to get streamed out after catchup to 2, is 3 - # whereas on in-memory it streas out 2, adjusted here, otherwise horizon ingest will abort - # and stop process with error that ledger 3 is not <= expected ledger of 2. - START: "0" - END: "0" - - # can only have CONTINUE or START set, not both. - CONTINUE: "true" - WRITE_LATEST_PATH: "true" - CAPTIVE_CORE_USE_DB: "true" - - # configure the network to export - HISTORY_ARCHIVE_URLS: "https://history.stellar.org/prd/core-live/core_live_001,https://history.stellar.org/prd/core-live/core_live_002,https://history.stellar.org/prd/core-live/core_live_003" - NETWORK_PASSPHRASE: "Public Global Stellar Network ; September 2015" - # can refer to canned cfg's for pubnet and testnet which are included on the image - # `/captive-core-pubnet.cfg` or `/captive-core-testnet.cfg`. - # If exporting a standalone network, then mount a volume to the pod container with your standalone core's .cfg, - # and set full path to that volume here - CAPTIVE_CORE_CONFIG: "/captive-core-pubnet.cfg" - - # example of testnet network config. - # HISTORY_ARCHIVE_URLS: "https://history.stellar.org/prd/core-testnet/core_testnet_001,https://history.stellar.org/prd/core-testnet/core_testnet_002" - # NETWORK_PASSPHRASE: "Test SDF Network ; September 2015" - # CAPTIVE_CORE_CONFIG: "/captive-core-testnet.cfg" - - # provide the url for the external s3 bucket to be populated - # update the ledgerexporter-pubnet-secret to have correct aws key/secret for access to the bucket - ARCHIVE_TARGET: "s3://horizon-ledgermeta-prodnet-test" ---- -apiVersion: v1 -kind: Secret -metadata: - labels: - app: ledgerexporter - name: ledgerexporter-pubnet-secret -type: Opaque -data: - AWS_REGION: - AWS_ACCESS_KEY_ID: - AWS_SECRET_ACCESS_KEY: ---- -# running captive core with on-disk mode limits RAM to around 2G usage, but -# requires some dedicated disk storage space that has at least 3k IOPS for read/write. -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: ledgerexporter-pubnet-core-storage -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 500Gi - storageClassName: default - volumeMode: Filesystem ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - fluxcd.io/ignore: "true" - deployment.kubernetes.io/revision: "3" - labels: - app: ledgerexporter-pubnet - name: ledgerexporter-pubnet-deployment -spec: - selector: - matchLabels: - app: ledgerexporter-pubnet - replicas: 1 - template: - metadata: - annotations: - fluxcd.io/ignore: "true" - # if we expect to add metrics at some point to ledgerexporter - # this just needs to be set to true - prometheus.io/port: "6060" - prometheus.io/scrape: "false" - labels: - app: ledgerexporter-pubnet - spec: - containers: - - envFrom: - - secretRef: - name: ledgerexporter-pubnet-secret - - configMapRef: - name: ledgerexporter-pubnet-env - image: stellar/lighthorizon-ledgerexporter:latest - imagePullPolicy: Always - name: ledgerexporter-pubnet - resources: - limits: - cpu: 3 - memory: 8Gi - requests: - cpu: 500m - memory: 2Gi - volumeMounts: - - mountPath: /cc - name: core-storage - dnsPolicy: ClusterFirst - volumes: - - name: core-storage - persistentVolumeClaim: - claimName: ledgerexporter-pubnet-core-storage - - - diff --git a/exp/lighthorizon/build/k8s/lighthorizon_batch_map_job.yml b/exp/lighthorizon/build/k8s/lighthorizon_batch_map_job.yml deleted file mode 100644 index a2671b66c1..0000000000 --- a/exp/lighthorizon/build/k8s/lighthorizon_batch_map_job.yml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: 'batch-map-job' -spec: - completions: 52 - parallelism: 10 - completionMode: Indexed - template: - spec: - restartPolicy: Never - containers: - - name: 'worker' - image: 'stellar/lighthorizon-index-batch' - imagePullPolicy: Always - envFrom: - - secretRef: - name: - env: - - name: RUN_MODE - value: "map" - - name: BATCH_SIZE - value: "10048" - - name: FIRST_CHECKPOINT - value: "41426080" - - name: WORKER_COUNT - value: "8" - - name: TXMETA_SOURCE - value: "" - - name: JOB_INDEX_ENV - value: "JOB_COMPLETION_INDEX" - - name: NETWORK_PASSPHRASE - value: "pubnet" - - name: INDEX_TARGET - value: "url of target index" - resources: - limits: - cpu: 4 - memory: 5Gi - requests: - cpu: 500m - memory: 500Mi - \ No newline at end of file diff --git a/exp/lighthorizon/build/k8s/lighthorizon_batch_reduce_job.yml b/exp/lighthorizon/build/k8s/lighthorizon_batch_reduce_job.yml deleted file mode 100644 index 1bc9cb7f6c..0000000000 --- a/exp/lighthorizon/build/k8s/lighthorizon_batch_reduce_job.yml +++ /dev/null @@ -1,42 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: 'batch-reduce-job' -spec: - completions: 52 - parallelism: 10 - completionMode: Indexed - template: - spec: - restartPolicy: Never - containers: - - name: 'worker' - image: 'stellar/lighthorizon-index-batch' - imagePullPolicy: Always - envFrom: - - secretRef: - name: - env: - - name: RUN_MODE - value: "reduce" - - name: MAP_JOB_COUNT - value: "52" - - name: REDUCE_JOB_COUNT - value: "52" - - name: WORKER_COUNT - value: "8" - - name: INDEX_SOURCE_ROOT - value: "" - - name: JOB_INDEX_ENV - value: JOB_COMPLETION_INDEX - - name: INDEX_TARGET - value: "" - resources: - limits: - cpu: 4 - memory: 5Gi - requests: - cpu: 500m - memory: 500Mi - - \ No newline at end of file diff --git a/exp/lighthorizon/build/k8s/lighthorizon_index.yml b/exp/lighthorizon/build/k8s/lighthorizon_index.yml deleted file mode 100644 index 1e7931fb2a..0000000000 --- a/exp/lighthorizon/build/k8s/lighthorizon_index.yml +++ /dev/null @@ -1,74 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - annotations: - fluxcd.io/ignore: "true" - labels: - app: lighthorizon-pubnet-index - name: lighthorizon-pubnet-index-env -data: - TXMETA_SOURCE: "s3://horizon-ledgermeta-prodnet-test" - INDEXES_SOURCE: "s3://horizon-index-prodnet-test" - NETWORK_PASSPHRASE: "Public Global Stellar Network ; September 2015" - START: "41809728" - END: "0" - WATCH: "true" - MODULES: "accounts" - WORKERS: "3" ---- -apiVersion: v1 -kind: Secret -metadata: - labels: - app: lighthorizon-pubnet-index - name: lighthorizon-pubnet-index-secret -type: Opaque -data: - AWS_REGION: - AWS_ACCESS_KEY_ID: - AWS_SECRET_ACCESS_KEY: ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - fluxcd.io/ignore: "true" - labels: - app: lighthorizon-pubnet-index - name: lighthorizon-pubnet-index -spec: - replicas: 1 - selector: - matchLabels: - app: lighthorizon-pubnet-index - template: - metadata: - annotations: - fluxcd.io/ignore: "true" - prometheus.io/port: "6060" - prometheus.io/scrape: "false" - labels: - app: lighthorizon-pubnet-index - spec: - containers: - - envFrom: - - secretRef: - name: lighthorizon-pubnet-index-secret - - configMapRef: - name: lighthorizon-pubnet-index-env - image: stellar/lighthorizon-index-single:latest - imagePullPolicy: Always - name: index - ports: - - containerPort: 6060 - name: metrics - protocol: TCP - resources: - limits: - cpu: 3 - memory: 6Gi - requests: - cpu: 500m - memory: 1Gi - - \ No newline at end of file diff --git a/exp/lighthorizon/build/k8s/lighthorizon_web.yml b/exp/lighthorizon/build/k8s/lighthorizon_web.yml deleted file mode 100644 index b680e7fb2c..0000000000 --- a/exp/lighthorizon/build/k8s/lighthorizon_web.yml +++ /dev/null @@ -1,133 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - annotations: - fluxcd.io/ignore: "true" - labels: - app: lighthorizon-pubnet-web - name: lighthorizon-pubnet-web-env -data: - TXMETA_SOURCE: "s3://horizon-indices-pubnet" - INDEXES_SOURCE: "s3://horizon-ledgermeta-pubnet" - NETWORK_PASSPHRASE: "Public Global Stellar Network ; September 2015" - MAX_PARALLEL_DOWNLOADS: 16 - CACHE_PATH: "/ledgercache" - CACHE_PRELOAD_START_LEDGER: 0 - CACHE_PRELOAD_COUNT: 14400 ---- -apiVersion: v1 -kind: Secret -metadata: - labels: - app: lighthorizon-pubnet-web - name: lighthorizon-pubnet-web-secret -type: Opaque -data: - AWS_REGION: - AWS_ACCESS_KEY_ID: - AWS_SECRET_ACCESS_KEY: ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - annotations: - fluxcd.io/ignore: "true" - labels: - app: lighthorizon-pubnet-web - name: lighthorizon-pubnet-web -spec: - replicas: 1 - selector: - matchLabels: - app: lighthorizon-pubnet-web - template: - metadata: - annotations: - fluxcd.io/ignore: "true" - prometheus.io/port: "6060" - prometheus.io/scrape: "false" - creationTimestamp: null - labels: - app: lighthorizon-pubnet-web - spec: - containers: - - envFrom: - - secretRef: - name: lighthorizon-pubnet-web-secret - - configMapRef: - name: lighthorizon-pubnet-web-env - image: stellar/lighthorizon-web:latest - imagePullPolicy: Always - name: web - ports: - - containerPort: 8080 - name: web - protocol: TCP - - containerPort: 6060 - name: metrics - protocol: TCP - readinessProbe: - failureThreshold: 3 - httpGet: - path: / - port: 8080 - scheme: HTTP - initialDelaySeconds: 30 - periodSeconds: 30 - successThreshold: 1 - timeoutSeconds: 5 - resources: - limits: - cpu: 2 - memory: 4Gi - requests: - cpu: 500m - memory: 1Gi - volumeMounts: - - mountPath: /ledgercache - name: cache-storage - volumes: - - name: cache-storage - emptyDir: {} ---- -apiVersion: v1 -kind: Service -metadata: - labels: - app: lighthorizon-pubnet-web - name: lighthorizon-pubnet-web -spec: - ports: - - name: http - port: 8000 - protocol: TCP - targetPort: 8080 - selector: - app: lighthorizon-pubnet-web - sessionAffinity: None - type: ClusterIP ---- -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - annotations: - cert-manager.io/cluster-issuer: default - ingress.kubernetes.io/ssl-redirect: "true" - kubernetes.io/ingress.class: public - name: lighthorizon-pubnet-web -spec: - rules: - - host: lighthorizon-pubnet.prototypes.kube001.services.stellar-ops.com - http: - paths: - - backend: - service: - name: lighthorizon-pubnet-web - port: - number: 8000 - path: / - pathType: ImplementationSpecific - tls: - - hosts: - - lighthorizon-pubnet.prototypes.kube001.services.stellar-ops.com - secretName: lighthorizon-pubnet-web-cert diff --git a/exp/lighthorizon/build/ledgerexporter/Dockerfile b/exp/lighthorizon/build/ledgerexporter/Dockerfile deleted file mode 100644 index f7129d7be2..0000000000 --- a/exp/lighthorizon/build/ledgerexporter/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM golang:1.20 AS builder - -WORKDIR /go/src/github.com/stellar/go -COPY . ./ -RUN go mod download -RUN go install github.com/stellar/go/exp/services/ledgerexporter - -FROM ubuntu:22.04 -ARG STELLAR_CORE_VERSION -ENV STELLAR_CORE_VERSION=${STELLAR_CORE_VERSION:-*} -ENV STELLAR_CORE_BINARY_PATH /usr/bin/stellar-core - -ENV DEBIAN_FRONTEND=noninteractive -# ca-certificates are required to make tls connections -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl wget gnupg apt-utils -RUN wget -qO - https://apt.stellar.org/SDF.asc | APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=true apt-key add - -RUN echo "deb https://apt.stellar.org jammy stable" >/etc/apt/sources.list.d/SDF.list -RUN echo "deb https://apt.stellar.org jammy unstable" >/etc/apt/sources.list.d/SDF-unstable.list -RUN apt-get update && apt-get install -y stellar-core=${STELLAR_CORE_VERSION} -RUN apt-get clean - -COPY --from=builder /go/src/github.com/stellar/go/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg / -COPY --from=builder /go/src/github.com/stellar/go/exp/lighthorizon/build/ledgerexporter/captive-core-testnet.cfg / -COPY --from=builder /go/src/github.com/stellar/go/exp/lighthorizon/build/ledgerexporter/start / - -RUN ["chmod", "+x", "/start"] - -# for the captive core sqlite database -RUN mkdir -p /cc - -COPY --from=builder /go/bin/ledgerexporter ./ - -ENTRYPOINT ["/start"] diff --git a/exp/lighthorizon/build/ledgerexporter/README.md b/exp/lighthorizon/build/ledgerexporter/README.md deleted file mode 100644 index 5534b2809a..0000000000 --- a/exp/lighthorizon/build/ledgerexporter/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# `stellar/horizon-ledgerexporter` - -This docker image allows running multiple instances of `ledgerexporter` on a single machine or running it in [AWS Batch](https://aws.amazon.com/batch/). - -## Env variables - -### Running locally - -| Name | Description | -|---------|------------------------| -| `START` | First ledger to export | -| `END` | Last ledger to export | - -### Running in AWS Batch - -| Name | Description | -|----------------------|----------------------------------------------------------------------| -| `BATCH_START_LEDGER` | First ledger of the AWS Batch Job, must be a checkpoint ledger or 1. | -| `BATCH_SIZE` | Size of the batch, must be multiple of 64. | - -#### Example - -When you start 10 jobs with `BATCH_START_LEDGER=63` and `BATCH_SIZE=64` -it will run the following ranges: - -| `AWS_BATCH_JOB_ARRAY_INDEX` | `FROM` | `TO` | -|-----------------------------|--------|------| -| 0 | 63 | 127 | -| 1 | 127 | 191 | -| 2 | 191 | 255 | -| 3 | 255 | 319 | - -## Tips when using AWS Batch - -* In "Job definition" set vCPUs to 2 and Memory to 4096. This represents the `c5.large` instances Horizon should be using. -* In "Compute environments": - * Set instance type to "c5.large". - * Set "Maximum vCPUs" to 2x the number of instances you want to start (because "c5.large" has 2 vCPUs). Ex. 10 vCPUs = 5 x "c5.large" instances. -* Use spot instances! It's much cheaper and speed of testing will be the same in 99% of cases. -* You need to publish the image if there are any changes in `Dockerfile` or one of the scripts. -* When batch processing is over check if instances have been terminated. Sometimes AWS doesn't terminate them. -* Make sure the job timeout is set to a larger value if you export larger ranges. Default is just 100 seconds. diff --git a/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg b/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg deleted file mode 100644 index 6379725b8d..0000000000 --- a/exp/lighthorizon/build/ledgerexporter/captive-core-pubnet.cfg +++ /dev/null @@ -1,200 +0,0 @@ -PEER_PORT=11725 -DATABASE = "sqlite3:///cc/stellar.db" - -FAILURE_SAFETY=1 - -EXPERIMENTAL_BUCKETLIST_DB=true - -# WARNING! Do not use this config in production. Quorum sets should -# be carefully selected manually. -NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" -HTTP_PORT=11626 - -[[HOME_DOMAINS]] -HOME_DOMAIN="publicnode.org" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="lobstr.co" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="www.franklintempleton.com" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="satoshipay.io" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="whalestack.com" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="www.stellar.org" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="stellar.blockdaemon.com" -QUALITY="HIGH" - -[[VALIDATORS]] -NAME="Boötes" -PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" -ADDRESS="bootes.publicnode.org:11625" -HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" -HOME_DOMAIN="publicnode.org" - -[[VALIDATORS]] -NAME="Lyra by BP Ventures" -PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" -ADDRESS="lyra.publicnode.org:11625" -HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" -HOME_DOMAIN="publicnode.org" - -[[VALIDATORS]] -NAME="Hercules by OG Technologies" -PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" -ADDRESS="hercules.publicnode.org:11625" -HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" -HOME_DOMAIN="publicnode.org" - -[[VALIDATORS]] -NAME="LOBSTR 3 (North America)" -PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" -ADDRESS="v3.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v3.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="LOBSTR 1 (Europe)" -PUBLIC_KEY="GCFONE23AB7Y6C5YZOMKUKGETPIAJA4QOYLS5VNS4JHBGKRZCPYHDLW7" -ADDRESS="v1.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v1.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="LOBSTR 2 (Europe)" -PUBLIC_KEY="GCB2VSADESRV2DDTIVTFLBDI562K6KE3KMKILBHUHUWFXCUBHGQDI7VL" -ADDRESS="v2.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v2.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="LOBSTR 4 (Asia)" -PUBLIC_KEY="GA7TEPCBDQKI7JQLQ34ZURRMK44DVYCIGVXQQWNSWAEQR6KB4FMCBT7J" -ADDRESS="v4.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v4.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="LOBSTR 5 (India)" -PUBLIC_KEY="GA5STBMV6QDXFDGD62MEHLLHZTPDI77U3PFOD2SELU5RJDHQWBR5NNK7" -ADDRESS="v5.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v5.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="FT SCV 2" -PUBLIC_KEY="GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" -ADDRESS="stellar2.franklintempleton.com:11625" -HISTORY="curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" -HOME_DOMAIN="www.franklintempleton.com" - -[[VALIDATORS]] -NAME="FT SCV 3" -PUBLIC_KEY="GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" -ADDRESS="stellar3.franklintempleton.com:11625" -HISTORY="curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" -HOME_DOMAIN="www.franklintempleton.com" - -[[VALIDATORS]] -NAME="FT SCV 1" -PUBLIC_KEY="GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" -ADDRESS="stellar1.franklintempleton.com:11625" -HISTORY="curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" -HOME_DOMAIN="www.franklintempleton.com" - -[[VALIDATORS]] -NAME="SatoshiPay Frankfurt" -PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" -ADDRESS="stellar-de-fra.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" -HOME_DOMAIN="satoshipay.io" - -[[VALIDATORS]] -NAME="SatoshiPay Singapore" -PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" -ADDRESS="stellar-sg-sin.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" -HOME_DOMAIN="satoshipay.io" - -[[VALIDATORS]] -NAME="SatoshiPay Iowa" -PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" -ADDRESS="stellar-us-iowa.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" -HOME_DOMAIN="satoshipay.io" - -[[VALIDATORS]] -NAME="Whalestack (Germany)" -PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" -ADDRESS="germany.stellar.whalestack.com:11625" -HISTORY="curl -sf https://germany.stellar.whalestack.com/history/{0} -o {1}" -HOME_DOMAIN="whalestack.com" - -[[VALIDATORS]] -NAME="Whalestack (Hong Kong)" -PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" -ADDRESS="hongkong.stellar.whalestack.com:11625" -HISTORY="curl -sf https://hongkong.stellar.whalestack.com/history/{0} -o {1}" -HOME_DOMAIN="whalestack.com" - -[[VALIDATORS]] -NAME="Whalestack (Finland)" -PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" -ADDRESS="finland.stellar.whalestack.com:11625" -HISTORY="curl -sf https://finland.stellar.whalestack.com/history/{0} -o {1}" -HOME_DOMAIN="whalestack.com" - -[[VALIDATORS]] -NAME="SDF 2" -PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" -ADDRESS="core-live-b.stellar.org:11625" -HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" -HOME_DOMAIN="www.stellar.org" - -[[VALIDATORS]] -NAME="SDF 1" -PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" -ADDRESS="core-live-a.stellar.org:11625" -HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" -HOME_DOMAIN="www.stellar.org" - -[[VALIDATORS]] -NAME="SDF 3" -PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" -ADDRESS="core-live-c.stellar.org:11625" -HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" -HOME_DOMAIN="www.stellar.org" - -[[VALIDATORS]] -NAME="Blockdaemon Validator 3" -PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" -ADDRESS="stellar-full-validator3.bdnodes.net:11625" -HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" -HOME_DOMAIN="stellar.blockdaemon.com" - -[[VALIDATORS]] -NAME="Blockdaemon Validator 2" -PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" -ADDRESS="stellar-full-validator2.bdnodes.net:11625" -HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" -HOME_DOMAIN="stellar.blockdaemon.com" - -[[VALIDATORS]] -NAME="Blockdaemon Validator 1" -PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" -ADDRESS="stellar-full-validator1.bdnodes.net:11625" -HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" -HOME_DOMAIN="stellar.blockdaemon.com" \ No newline at end of file diff --git a/exp/lighthorizon/build/ledgerexporter/captive-core-testnet.cfg b/exp/lighthorizon/build/ledgerexporter/captive-core-testnet.cfg deleted file mode 100644 index 9c7dadc527..0000000000 --- a/exp/lighthorizon/build/ledgerexporter/captive-core-testnet.cfg +++ /dev/null @@ -1,32 +0,0 @@ -PEER_PORT=11725 -DATABASE = "sqlite3:///cc/stellar.db" - -UNSAFE_QUORUM=true -FAILURE_SAFETY=1 - -EXPERIMENTAL_BUCKETLIST_DB=true - -[[HOME_DOMAINS]] -HOME_DOMAIN="testnet.stellar.org" -QUALITY="HIGH" - -[[VALIDATORS]] -NAME="sdf_testnet_1" -HOME_DOMAIN="testnet.stellar.org" -PUBLIC_KEY="GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" -ADDRESS="core-testnet1.stellar.org" -HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_001/{0} -o {1}" - -[[VALIDATORS]] -NAME="sdf_testnet_2" -HOME_DOMAIN="testnet.stellar.org" -PUBLIC_KEY="GCUCJTIYXSOXKBSNFGNFWW5MUQ54HKRPGJUTQFJ5RQXZXNOLNXYDHRAP" -ADDRESS="core-testnet2.stellar.org" -HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_002/{0} -o {1}" - -[[VALIDATORS]] -NAME="sdf_testnet_3" -HOME_DOMAIN="testnet.stellar.org" -PUBLIC_KEY="GC2V2EFSXN6SQTWVYA5EPJPBWWIMSD2XQNKUOHGEKB535AQE2I6IXV2Z" -ADDRESS="core-testnet3.stellar.org" -HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_003/{0} -o {1}" \ No newline at end of file diff --git a/exp/lighthorizon/build/ledgerexporter/start b/exp/lighthorizon/build/ledgerexporter/start deleted file mode 100644 index 11d863effa..0000000000 --- a/exp/lighthorizon/build/ledgerexporter/start +++ /dev/null @@ -1,55 +0,0 @@ -#! /usr/bin/env bash -set -e - -START="${START:=2}" -END="${END:=0}" -CONTINUE="${CONTINUE:=false}" -# Writing to /latest is disabled by default to avoid race conditions between parallel container runs -WRITE_LATEST_PATH="${WRITE_LATEST_PATH:=false}" - -# config defaults to pubnet core, any other network requires setting all 3 of these in container env -NETWORK_PASSPHRASE="${NETWORK_PASSPHRASE:=Public Global Stellar Network ; September 2015}" -HISTORY_ARCHIVE_URLS="${HISTORY_ARCHIVE_URLS:=https://s3-eu-west-1.amazonaws.com/history.stellar.org/prd/core-live/core_live_001}" -CAPTIVE_CORE_CONFIG="${CAPTIVE_CORE_CONFIG:=/captive-core-pubnet.cfg}" - -CAPTIVE_CORE_USE_DB="${CAPTIVE_CORE_USE_DB:=true}" - -if [ -z "$ARCHIVE_TARGET" ]; then - echo "error: undefined ARCHIVE_TARGET env variable" - exit 1 -fi - -# Calculate params for AWS Batch -if [ ! -z "$AWS_BATCH_JOB_ARRAY_INDEX" ]; then - # The batch should have three env variables: - # * BATCH_START_LEDGER - start ledger of the job, must be equal 1 or a - # checkpoint ledger (i + 1) % 64 == 0. - # * BATCH_SIZE - size of the batch in ledgers, must be multiple of 64! - # * BRANCH - git branch to build - # - # Ex: BATCH_START_LEDGER=63, BATCH_SIZE=64 will create the following ranges: - # AWS_BATCH_JOB_ARRAY_INDEX=0: [63, 127] - # AWS_BATCH_JOB_ARRAY_INDEX=1: [127, 191] - # AWS_BATCH_JOB_ARRAY_INDEX=2: [191, 255] - # AWS_BATCH_JOB_ARRAY_INDEX=3: [255, 319] - # ... - START=`expr "$BATCH_SIZE" \* "$AWS_BATCH_JOB_ARRAY_INDEX" + "$BATCH_START_LEDGER"` - END=`expr "$BATCH_SIZE" \* "$AWS_BATCH_JOB_ARRAY_INDEX" + "$BATCH_START_LEDGER" + "$BATCH_SIZE"` - - if [ "$START" -lt 2 ]; then - # The minimum ledger expected by the ledger exporter is 2 - START=2 - fi - -fi - -echo "START: $START END: $END" - -export TRACY_NO_INVARIANT_CHECK=1 -/ledgerexporter --target "$ARCHIVE_TARGET" \ - --captive-core-toml-path "$CAPTIVE_CORE_CONFIG" \ - --history-archive-urls "$HISTORY_ARCHIVE_URLS" --network-passphrase "$NETWORK_PASSPHRASE" \ - --continue="$CONTINUE" --write-latest-path="$WRITE_LATEST_PATH" \ - --start-ledger "$START" --end-ledger "$END" --captive-core-use-db="$CAPTIVE_CORE_USE_DB" - -echo "OK" diff --git a/exp/lighthorizon/build/web/Dockerfile b/exp/lighthorizon/build/web/Dockerfile deleted file mode 100644 index 83d0002ebc..0000000000 --- a/exp/lighthorizon/build/web/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM golang:1.20 AS builder - -WORKDIR /go/src/github.com/stellar/go -COPY . ./ -RUN go mod download -RUN go install github.com/stellar/go/exp/lighthorizon - -FROM ubuntu:22.04 - -ENV DEBIAN_FRONTEND=noninteractive -# ca-certificates are required to make tls connections -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl wget gnupg apt-utils -RUN apt-get clean - -COPY --from=builder /go/bin/lighthorizon ./ - -ENTRYPOINT ./lighthorizon serve \ - --network-passphrase "$NETWORK_PASSPHRASE" \ - --parallel-downloads "$MAX_PARALLEL_DOWNLOADS" \ - --ledger-cache "$CACHE_PATH" \ - --ledger-cache-preload "$CACHE_PRELOAD_COUNT" \ - --ledger-cache-preload-start "$CACHE_PRELOAD_START_LEDGER" \ - --log-level debug \ - "$TXMETA_SOURCE" "$INDEXES_SOURCE" diff --git a/exp/lighthorizon/common/operation.go b/exp/lighthorizon/common/operation.go deleted file mode 100644 index ca5f7bfe61..0000000000 --- a/exp/lighthorizon/common/operation.go +++ /dev/null @@ -1,52 +0,0 @@ -package common - -import ( - "encoding/hex" - - "github.com/stellar/go/network" - "github.com/stellar/go/toid" - "github.com/stellar/go/xdr" -) - -type Operation struct { - TransactionEnvelope *xdr.TransactionEnvelope - TransactionResult *xdr.TransactionResult - LedgerHeader *xdr.LedgerHeader - OpIndex int32 - TxIndex int32 -} - -func (o *Operation) Get() *xdr.Operation { - return &o.TransactionEnvelope.Operations()[o.OpIndex] -} - -func (o *Operation) OperationResult() *xdr.OperationResultTr { - results, _ := o.TransactionResult.OperationResults() - tr := results[o.OpIndex].MustTr() - return &tr -} - -func (o *Operation) TransactionHash() (string, error) { - hash, err := network.HashTransactionInEnvelope(*o.TransactionEnvelope, network.PublicNetworkPassphrase) - if err != nil { - return "", err - } - - return hex.EncodeToString(hash[:]), nil -} - -func (o *Operation) SourceAccount() xdr.AccountId { - sourceAccount := o.TransactionEnvelope.SourceAccount().ToAccountId() - if o.Get().SourceAccount != nil { - sourceAccount = o.Get().SourceAccount.ToAccountId() - } - return sourceAccount -} - -func (o *Operation) TOID() int64 { - return toid.New( - int32(o.LedgerHeader.LedgerSeq), - o.TxIndex+1, - o.OpIndex+1, - ).ToInt64() -} diff --git a/exp/lighthorizon/common/transaction.go b/exp/lighthorizon/common/transaction.go deleted file mode 100644 index 104fd3bc6b..0000000000 --- a/exp/lighthorizon/common/transaction.go +++ /dev/null @@ -1,70 +0,0 @@ -package common - -import ( - "encoding/hex" - "errors" - - "github.com/stellar/go/exp/lighthorizon/ingester" - "github.com/stellar/go/network" - "github.com/stellar/go/toid" - "github.com/stellar/go/xdr" -) - -type Transaction struct { - *ingester.LedgerTransaction - LedgerHeader *xdr.LedgerHeader - TxIndex int32 - - NetworkPassphrase string -} - -// type Transaction struct { -// TransactionEnvelope *xdr.TransactionEnvelope -// TransactionResult *xdr.TransactionResult -// } - -func (tx *Transaction) TransactionHash() (string, error) { - if tx.NetworkPassphrase == "" { - return "", errors.New("network passphrase unspecified") - } - - hash, err := network.HashTransactionInEnvelope(tx.Envelope, tx.NetworkPassphrase) - if err != nil { - return "", err - } - - return hex.EncodeToString(hash[:]), nil -} - -func (o *Transaction) SourceAccount() xdr.MuxedAccount { - return o.Envelope.SourceAccount() -} - -func (tx *Transaction) TOID() int64 { - return toid.New( - int32(tx.LedgerHeader.LedgerSeq), - // TOID indexing is 1-based, so the 1st tx comes at position 1, - tx.TxIndex+1, - // but the TOID of a transaction comes BEFORE any operation - 0, - ).ToInt64() -} - -func (tx *Transaction) HasPreconditions() bool { - switch pc := tx.Envelope.Preconditions(); pc.Type { - case xdr.PreconditionTypePrecondNone: - return false - case xdr.PreconditionTypePrecondTime: - return pc.TimeBounds != nil - case xdr.PreconditionTypePrecondV2: - // TODO: 2x check these - return (pc.V2.TimeBounds != nil || - pc.V2.LedgerBounds != nil || - pc.V2.MinSeqNum != nil || - pc.V2.MinSeqAge > 0 || - pc.V2.MinSeqLedgerGap > 0 || - len(pc.V2.ExtraSigners) > 0) - } - - return false -} diff --git a/exp/lighthorizon/http.go b/exp/lighthorizon/http.go deleted file mode 100644 index e61ad4c716..0000000000 --- a/exp/lighthorizon/http.go +++ /dev/null @@ -1,78 +0,0 @@ -package main - -import ( - "net/http" - "strconv" - "time" - - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - - "github.com/stellar/go/exp/lighthorizon/actions" - "github.com/stellar/go/exp/lighthorizon/services" - supportHttp "github.com/stellar/go/support/http" - "github.com/stellar/go/support/render/problem" -) - -func newWrapResponseWriter(w http.ResponseWriter, r *http.Request) middleware.WrapResponseWriter { - mw, ok := w.(middleware.WrapResponseWriter) - if !ok { - mw = middleware.NewWrapResponseWriter(w, r.ProtoMajor) - } - - return mw -} - -func prometheusMiddleware(requestDurationMetric *prometheus.SummaryVec) func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - route := supportHttp.GetChiRoutePattern(r) - mw := newWrapResponseWriter(w, r) - - then := time.Now() - next.ServeHTTP(mw, r) - duration := time.Since(then) - - requestDurationMetric.With(prometheus.Labels{ - "status": strconv.FormatInt(int64(mw.Status()), 10), - "method": r.Method, - "route": route, - }).Observe(float64(duration.Seconds())) - }) - } -} - -func lightHorizonHTTPHandler(registry *prometheus.Registry, lightHorizon services.LightHorizon) http.Handler { - requestDurationMetric := prometheus.NewSummaryVec( - prometheus.SummaryOpts{ - Namespace: "horizon_lite", Subsystem: "http", Name: "requests_duration_seconds", - Help: "HTTP requests durations, sliding window = 10m", - }, - []string{"status", "method", "route"}, - ) - registry.MustRegister(requestDurationMetric) - - router := chi.NewMux() - router.Use(prometheusMiddleware(requestDurationMetric)) - - router.Route("/accounts/{account_id}", func(r chi.Router) { - r.MethodFunc(http.MethodGet, "/transactions", actions.NewTXByAccountHandler(lightHorizon)) - r.MethodFunc(http.MethodGet, "/operations", actions.NewOpsByAccountHandler(lightHorizon)) - }) - - router.MethodFunc(http.MethodGet, "/", actions.Root(actions.RootResponse{ - Version: HorizonLiteVersion, - // by default, no other fields are known yet - })) - router.MethodFunc(http.MethodGet, "/api", actions.ApiDocs()) - router.Method(http.MethodGet, "/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) - - problem.RegisterHost("") - router.NotFound(func(w http.ResponseWriter, request *http.Request) { - problem.Render(request.Context(), w, problem.NotFound) - }) - - return router -} diff --git a/exp/lighthorizon/http_test.go b/exp/lighthorizon/http_test.go deleted file mode 100644 index f59e2719d5..0000000000 --- a/exp/lighthorizon/http_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/prometheus/client_golang/prometheus" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/stellar/go/exp/lighthorizon/actions" - "github.com/stellar/go/exp/lighthorizon/services" - "github.com/stellar/go/support/render/problem" -) - -func TestUnknownUrl(t *testing.T) { - recorder := httptest.NewRecorder() - request, err := http.NewRequest("GET", "/unknown", nil) - require.NoError(t, err) - - prepareTestHttpHandler().ServeHTTP(recorder, request) - - resp := recorder.Result() - assert.Equal(t, http.StatusNotFound, resp.StatusCode) - - raw, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - - var problem problem.P - err = json.Unmarshal(raw, &problem) - assert.NoError(t, err) - assert.Equal(t, "Resource Missing", problem.Title) - assert.Equal(t, "not_found", problem.Type) -} - -func TestRootResponse(t *testing.T) { - recorder := httptest.NewRecorder() - request, err := http.NewRequest("GET", "/", nil) - require.NoError(t, err) - - prepareTestHttpHandler().ServeHTTP(recorder, request) - - var root actions.RootResponse - raw, err := io.ReadAll(recorder.Result().Body) - require.NoError(t, err) - require.NoError(t, json.Unmarshal(raw, &root)) - require.Equal(t, HorizonLiteVersion, root.Version) -} - -func prepareTestHttpHandler() http.Handler { - mockOperationService := &services.MockOperationService{} - mockTransactionService := &services.MockTransactionService{} - registry := prometheus.NewRegistry() - - lh := services.LightHorizon{ - Operations: mockOperationService, - Transactions: mockTransactionService, - } - - return lightHorizonHTTPHandler(registry, lh) -} diff --git a/exp/lighthorizon/index/Makefile b/exp/lighthorizon/index/Makefile deleted file mode 100644 index 38361d7d37..0000000000 --- a/exp/lighthorizon/index/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -XDRS = xdr/LightHorizon-types.x - -XDRGEN_COMMIT=3f6808cd161d72474ffbe9eedbd7013de7f92748 - -.PHONY: xdr clean update - -xdr/xdr_generated.go: $(XDRS) - docker run -it --rm -v $$PWD:/wd -w /wd ruby /bin/bash -c '\ - gem install specific_install -v 0.3.7 && \ - gem specific_install https://github.com/stellar/xdrgen.git -b $(XDRGEN_COMMIT) && \ - xdrgen \ - --language go \ - --namespace xdr \ - --output xdr/ \ - $(XDRS)' - ls -lAh - go fmt $@ - -xdr: xdr/xdr_generated.go - -clean: - rm ./xdr/xdr_generated.go || true - -update: clean xdr diff --git a/exp/lighthorizon/index/backend/backend.go b/exp/lighthorizon/index/backend/backend.go deleted file mode 100644 index 580e5f4d6e..0000000000 --- a/exp/lighthorizon/index/backend/backend.go +++ /dev/null @@ -1,14 +0,0 @@ -package index - -import types "github.com/stellar/go/exp/lighthorizon/index/types" - -// TODO: Use a more standardized filesystem-style backend, so we can re-use -// code -type Backend interface { - Flush(map[string]types.NamedIndices) error - FlushAccounts([]string) error - Read(account string) (types.NamedIndices, error) - ReadAccounts() ([]string, error) - FlushTransactions(map[string]*types.TrieIndex) error - ReadTransactions(prefix string) (*types.TrieIndex, error) -} diff --git a/exp/lighthorizon/index/backend/file.go b/exp/lighthorizon/index/backend/file.go deleted file mode 100644 index 062b1efcdb..0000000000 --- a/exp/lighthorizon/index/backend/file.go +++ /dev/null @@ -1,214 +0,0 @@ -package index - -import ( - "bufio" - "compress/gzip" - "io" - "io/fs" - "os" - "path/filepath" - - types "github.com/stellar/go/exp/lighthorizon/index/types" - - "github.com/stellar/go/support/collections/set" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/log" -) - -type FileBackend struct { - dir string - parallel uint32 -} - -// NewFileBackend connects to indices stored at `dir`, creating the directory if one doesn't -// exist, and uses `parallel` to control how many workers to use when flushing to disk. -func NewFileBackend(dir string, parallel uint32) (*FileBackend, error) { - if parallel <= 0 { - parallel = 1 - } - - err := os.MkdirAll(dir, fs.ModeDir|0755) - if err != nil { - log.Errorf("Unable to mkdir %s, %v", dir, err) - return nil, err - } - - return &FileBackend{ - dir: dir, - parallel: parallel, - }, nil -} - -func (s *FileBackend) Flush(indexes map[string]types.NamedIndices) error { - return parallelFlush(s.parallel, indexes, s.writeBatch) -} - -func (s *FileBackend) FlushAccounts(accounts []string) error { - path := filepath.Join(s.dir, "accounts") - - f, err := os.OpenFile(path, os.O_CREATE| - os.O_APPEND| // crucial! since we might flush from various sources - os.O_WRONLY, - 0664) // rw-rw-r-- - - if err != nil { - return errors.Wrapf(err, "failed to open account file at %s", path) - } - - defer f.Close() - - // We write one account at a time because writes that occur within a single - // `write()` syscall are thread-safe. A larger write might be split into - // many calls and thus get interleaved, so we play it safe. - for _, account := range accounts { - f.Write([]byte(account + "\n")) - } - - return nil -} - -func (s *FileBackend) writeBatch(b *batch) error { - if len(b.indexes) == 0 { - return nil - } - - path := filepath.Join(s.dir, b.account[:3], b.account) - - err := os.MkdirAll(filepath.Dir(path), fs.ModeDir|0755) - if err != nil { - log.Errorf("Unable to mkdir %s, %v", filepath.Dir(path), err) - return nil - } - - f, err := os.Create(path) - if err != nil { - log.Errorf("Unable to create %s: %v", path, err) - return nil - } - defer f.Close() - - if _, err := writeGzippedTo(f, b.indexes); err != nil { - log.Errorf("Unable to serialize %s: %v", b.account, err) - return nil - } - - return nil -} - -func (s *FileBackend) FlushTransactions(indexes map[string]*types.TrieIndex) error { - // TODO: Parallelize this - for key, index := range indexes { - path := filepath.Join(s.dir, "tx", key) - - err := os.MkdirAll(filepath.Dir(path), fs.ModeDir|0755) - if err != nil { - log.Errorf("Unable to mkdir %s, %v", filepath.Dir(path), err) - continue - } - - f, err := os.Create(path) - if err != nil { - log.Errorf("Unable to create %s: %v", path, err) - continue - } - - zw := gzip.NewWriter(f) - if _, err := index.WriteTo(zw); err != nil { - log.Errorf("Unable to serialize %s: %v", path, err) - f.Close() - continue - } - - if err := zw.Close(); err != nil { - log.Errorf("Unable to serialize %s: %v", path, err) - f.Close() - continue - } - - if err := f.Close(); err != nil { - log.Errorf("Unable to save %s: %v", path, err) - } - } - return nil -} - -func (s *FileBackend) Read(account string) (types.NamedIndices, error) { - log.Debugf("Opening index: %s", account) - b, err := os.Open(filepath.Join(s.dir, account[:3], account)) - if err != nil { - return nil, err - } - defer b.Close() - - indexes, _, err := readGzippedFrom(bufio.NewReader(b)) - if err != nil { - log.Errorf("Unable to parse %s: %v", account, err) - return nil, os.ErrNotExist - } - return indexes, nil -} - -func (s *FileBackend) ReadAccounts() ([]string, error) { - path := filepath.Join(s.dir, "accounts") - log.Debugf("Opening accounts list at %s", path) - - f, err := os.Open(path) - if err != nil { - return nil, errors.Wrapf(err, "failed to open %s", path) - } - - const gAddressSize = 56 - - // We ballpark the capacity assuming all of the values being G-addresses. - preallocationSize := 100 * gAddressSize // default to 100 lines - info, err := os.Stat(path) - if err == nil { // we can still safely continue w/ errors - // Note that this will never be too large, but may be too small. - preallocationSize = int(info.Size()) / (gAddressSize + 1) // +1 for \n - } - accountMap := set.NewSet[string](preallocationSize) - accounts := make([]string, 0, preallocationSize) - - reader := bufio.NewReaderSize(f, 100*gAddressSize) // reasonable buffer size - for { - line, err := reader.ReadString(byte('\n')) - if err == io.EOF { - break - } else if err != nil { - return accounts, errors.Wrapf(err, "failed to read %s", path) - } - - account := line[:len(line)-1] // trim newline - - // The account list is very unlikely to be unique (especially if it was made - // w/ parallel flushes), so let's ensure that that's the case. - if !accountMap.Contains(account) { - accountMap.Add(account) - accounts = append(accounts, account) - } - } - - return accounts, nil -} - -func (s *FileBackend) ReadTransactions(prefix string) (*types.TrieIndex, error) { - log.Debugf("Opening index: %s", prefix) - b, err := os.Open(filepath.Join(s.dir, "tx", prefix)) - if err != nil { - return nil, err - } - defer b.Close() - zr, err := gzip.NewReader(b) - if err != nil { - log.Errorf("Unable to parse %s: %v", prefix, err) - return nil, os.ErrNotExist - } - defer zr.Close() - var index types.TrieIndex - _, err = index.ReadFrom(zr) - if err != nil { - log.Errorf("Unable to parse %s: %v", prefix, err) - return nil, os.ErrNotExist - } - return &index, nil -} diff --git a/exp/lighthorizon/index/backend/file_test.go b/exp/lighthorizon/index/backend/file_test.go deleted file mode 100644 index 6197f7b5c3..0000000000 --- a/exp/lighthorizon/index/backend/file_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package index - -import ( - "math/rand" - "testing" - - "github.com/stellar/go/keypair" - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/require" -) - -func TestSimpleFileStore(t *testing.T) { - tmpDir := t.TempDir() - - // Create a large (beyond a single chunk) list of arbitrary accounts, some - // regular and some muxed. - accountList := make([]string, 123) - for i := range accountList { - var err error - var muxed xdr.MuxedAccount - address := keypair.MustRandom().Address() - - if rand.Intn(2) == 1 { - muxed, err = xdr.MuxedAccountFromAccountId(address, 12345678) - require.NoErrorf(t, err, "shouldn't happen") - } else { - muxed = xdr.MustMuxedAddress(address) - } - - accountList[i] = muxed.Address() - } - - require.Len(t, accountList, 123) - - file, err := NewFileBackend(tmpDir, 1) - require.NoError(t, err) - - require.NoError(t, file.FlushAccounts(accountList)) - - accounts, err := file.ReadAccounts() - require.NoError(t, err) - require.Equal(t, accountList, accounts) -} diff --git a/exp/lighthorizon/index/backend/gzip.go b/exp/lighthorizon/index/backend/gzip.go deleted file mode 100644 index 63c8e332c2..0000000000 --- a/exp/lighthorizon/index/backend/gzip.go +++ /dev/null @@ -1,74 +0,0 @@ -package index - -import ( - "bytes" - "compress/gzip" - "errors" - "io" - - types "github.com/stellar/go/exp/lighthorizon/index/types" -) - -func writeGzippedTo(w io.Writer, indexes types.NamedIndices) (int64, error) { - zw := gzip.NewWriter(w) - - var n int64 - for id, index := range indexes { - zw.Name = id - nWrote, err := io.Copy(zw, index.Buffer()) - n += nWrote - if err != nil { - return n, err - } - - if err := zw.Close(); err != nil { - return n, err - } - - zw.Reset(w) - } - - return n, nil -} - -func readGzippedFrom(r io.Reader) (types.NamedIndices, int64, error) { - if _, ok := r.(io.ByteReader); !ok { - return nil, 0, errors.New("reader *must* implement ByteReader") - } - - zr, err := gzip.NewReader(r) - if err != nil { - return nil, 0, err - } - - indexes := types.NamedIndices{} - var buf bytes.Buffer - var n int64 - for { - zr.Multistream(false) - - nRead, err := io.Copy(&buf, zr) - n += nRead - if err != nil { - return nil, n, err - } - - ind, err := types.NewBitmapIndex(buf.Bytes()) - if err != nil { - return nil, n, err - } - - indexes[zr.Name] = ind - - buf.Reset() - - err = zr.Reset(r) - if err == io.EOF { - break - } else if err != nil { - return nil, n, err - } - } - - return indexes, n, zr.Close() -} diff --git a/exp/lighthorizon/index/backend/gzip_test.go b/exp/lighthorizon/index/backend/gzip_test.go deleted file mode 100644 index 730e13185d..0000000000 --- a/exp/lighthorizon/index/backend/gzip_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package index - -import ( - "bufio" - "bytes" - "math/rand" - "os" - "path/filepath" - "testing" - - types "github.com/stellar/go/exp/lighthorizon/index/types" - "github.com/stretchr/testify/require" -) - -func TestGzipRoundtrip(t *testing.T) { - index := &types.BitmapIndex{} - anotherIndex := &types.BitmapIndex{} - for i := 0; i < 100+rand.Intn(1000); i++ { - index.SetActive(uint32(rand.Intn(10_000))) - anotherIndex.SetActive(uint32(rand.Intn(10_000))) - } - - indices := types.NamedIndices{ - "a": index, - "short/name": anotherIndex, - "slightlyLonger/name": index, - } - - var buf bytes.Buffer - wroteBytes, err := writeGzippedTo(&buf, indices) - require.NoError(t, err) - require.Greater(t, wroteBytes, int64(0)) - - gz := filepath.Join(t.TempDir(), "test.gzip") - require.NoError(t, os.WriteFile(gz, buf.Bytes(), 0644)) - f, err := os.Open(gz) - require.NoError(t, err) - defer f.Close() - - // Ensure that reading directly from a file errors out. - _, _, err = readGzippedFrom(f) - require.Error(t, err) - - read, readBytes, err := readGzippedFrom(bufio.NewReader(f)) - require.NoError(t, err) - require.Greater(t, readBytes, int64(0)) - - require.Equal(t, indices, read) - require.Equal(t, wroteBytes, readBytes) - require.Len(t, read, len(indices)) - - for name, index := range indices { - raw1, err := index.ToXDR().MarshalBinary() - require.NoError(t, err) - - raw2, err := read[name].ToXDR().MarshalBinary() - require.NoError(t, err) - - require.Equal(t, raw1, raw2) - } -} diff --git a/exp/lighthorizon/index/backend/parallel_flush.go b/exp/lighthorizon/index/backend/parallel_flush.go deleted file mode 100644 index 6f65bedc42..0000000000 --- a/exp/lighthorizon/index/backend/parallel_flush.go +++ /dev/null @@ -1,73 +0,0 @@ -package index - -import ( - "sync" - "sync/atomic" - "time" - - types "github.com/stellar/go/exp/lighthorizon/index/types" - "github.com/stellar/go/support/log" -) - -type batch struct { - account string - indexes types.NamedIndices -} - -type flushBatch func(b *batch) error - -func parallelFlush(parallel uint32, allIndexes map[string]types.NamedIndices, f flushBatch) error { - var wg sync.WaitGroup - - batches := make(chan *batch, parallel) - - wg.Add(1) - go func() { - // forces this async func to be waited on also, otherwise the outer - // method returns before this finishes. - defer wg.Done() - - for account, indexes := range allIndexes { - batches <- &batch{ - account: account, - indexes: indexes, - } - } - - if len(allIndexes) == 0 { - close(batches) - } - }() - - written := uint64(0) - for i := uint32(0); i < parallel; i++ { - wg.Add(1) - go func(workerNum uint32) { - defer wg.Done() - for batch := range batches { - if err := f(batch); err != nil { - log.Errorf("Error occurred writing batch: %v, retrying...", err) - time.Sleep(50 * time.Millisecond) - batches <- batch - continue - } - - nwritten := atomic.AddUint64(&written, 1) - if nwritten%1234 == 0 { - log.WithField("worker", workerNum). - Infof("Writing indices... %d/%d (%.2f%%)", - nwritten, len(allIndexes), - (float64(nwritten)/float64(len(allIndexes)))*100) - } - - if nwritten == uint64(len(allIndexes)) { - close(batches) - } - } - }(i) - } - - wg.Wait() - - return nil -} diff --git a/exp/lighthorizon/index/backend/s3.go b/exp/lighthorizon/index/backend/s3.go deleted file mode 100644 index a4f5a7e751..0000000000 --- a/exp/lighthorizon/index/backend/s3.go +++ /dev/null @@ -1,220 +0,0 @@ -package index - -import ( - "bytes" - "compress/gzip" - "os" - "path/filepath" - "strings" - "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3" - "github.com/aws/aws-sdk-go/service/s3/s3manager" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/log" - - types "github.com/stellar/go/exp/lighthorizon/index/types" -) - -type S3Backend struct { - s3Session *session.Session - downloader *s3manager.Downloader - uploader *s3manager.Uploader - parallel uint32 - pathPrefix string - bucket string -} - -func NewS3Backend(awsConfig *aws.Config, bucket string, pathPrefix string, parallel uint32) (*S3Backend, error) { - s3Session, err := session.NewSession(awsConfig) - if err != nil { - return nil, err - } - - return &S3Backend{ - s3Session: s3Session, - downloader: s3manager.NewDownloader(s3Session), - uploader: s3manager.NewUploader(s3Session), - parallel: parallel, - pathPrefix: pathPrefix, - bucket: bucket, - }, nil -} - -func (s *S3Backend) FlushAccounts(accounts []string) error { - var buf bytes.Buffer - accountsString := strings.Join(accounts, "\n") - _, err := buf.WriteString(accountsString) - if err != nil { - return err - } - - path := filepath.Join(s.pathPrefix, "accounts") - - _, err = s.uploader.Upload(&s3manager.UploadInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(path), - Body: &buf, - }) - if err != nil { - return err - } - - return nil -} - -func (s *S3Backend) Flush(indexes map[string]types.NamedIndices) error { - return parallelFlush(s.parallel, indexes, s.writeBatch) -} - -func (s *S3Backend) writeBatch(b *batch) error { - // TODO: re-use buffers in a pool - var buf bytes.Buffer - if _, err := writeGzippedTo(&buf, b.indexes); err != nil { - // TODO: Should we retry or what here?? - return errors.Wrapf(err, "unable to serialize %s", b.account) - } - - path := s.path(b.account) - - _, err := s.uploader.Upload(&s3manager.UploadInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(path), - Body: &buf, - }) - if err != nil { - return errors.Wrapf(err, "unable to upload %s", b.account) - } - - return nil -} - -func (s *S3Backend) FlushTransactions(indexes map[string]*types.TrieIndex) error { - // TODO: Parallelize this - var buf bytes.Buffer - for key, index := range indexes { - buf.Reset() - path := filepath.Join(s.pathPrefix, "tx", key) - - zw := gzip.NewWriter(&buf) - if _, err := index.WriteTo(zw); err != nil { - log.Errorf("Unable to serialize %s: %v", path, err) - continue - } - - if err := zw.Close(); err != nil { - log.Errorf("Unable to serialize %s: %v", path, err) - continue - } - - _, err := s.uploader.Upload(&s3manager.UploadInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(path), - Body: &buf, - }) - if err != nil { - log.Errorf("Unable to upload %s: %v", path, err) - // TODO: retries - continue - } - } - return nil -} - -func (s *S3Backend) ReadAccounts() ([]string, error) { - log.Debugf("Downloading accounts list") - b := &aws.WriteAtBuffer{} - path := filepath.Join(s.pathPrefix, "accounts") - n, err := s.downloader.Download(b, &s3.GetObjectInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(path), - }) - if err != nil { - if aerr, ok := err.(awserr.Error); ok && aerr.Code() == s3.ErrCodeNoSuchKey { - return nil, os.ErrNotExist - } - return nil, errors.Wrapf(err, "Unable to download accounts list") - } - if n == 0 { - return nil, os.ErrNotExist - } - body := b.Bytes() - accounts := strings.Split(string(body), "\n") - return accounts, nil -} - -func (s *S3Backend) path(account string) string { - return filepath.Join(s.pathPrefix, account[:10], account) -} - -func (s *S3Backend) Read(account string) (types.NamedIndices, error) { - // Check if index exists in S3 - log.Debugf("Downloading index: %s", account) - var err error - for i := 0; i < 10; i++ { - b := &aws.WriteAtBuffer{} - path := s.path(account) - var n int64 - n, err = s.downloader.Download(b, &s3.GetObjectInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(path), - }) - if err != nil { - if aerr, ok := err.(awserr.Error); ok && aerr.Code() == s3.ErrCodeNoSuchKey { - return nil, os.ErrNotExist - } - err = errors.Wrapf(err, "Unable to download %s", account) - time.Sleep(100 * time.Millisecond) - continue - } - if n == 0 { - return nil, os.ErrNotExist - } - var indexes map[string]*types.BitmapIndex - indexes, _, err = readGzippedFrom(bytes.NewReader(b.Bytes())) - if err != nil { - log.Errorf("Unable to parse %s: %v", account, err) - return nil, os.ErrNotExist - } - return indexes, nil - } - - return nil, err -} - -func (s *S3Backend) ReadTransactions(prefix string) (*types.TrieIndex, error) { - // Check if index exists in S3 - log.Debugf("Downloading index: %s", prefix) - b := &aws.WriteAtBuffer{} - path := filepath.Join(s.pathPrefix, "tx", prefix) - n, err := s.downloader.Download(b, &s3.GetObjectInput{ - Bucket: aws.String(s.bucket), - Key: aws.String(path), - }) - if err != nil { - if aerr, ok := err.(awserr.Error); ok && aerr.Code() == s3.ErrCodeNoSuchKey { - return nil, os.ErrNotExist - } - return nil, errors.Wrapf(err, "Unable to download %s", prefix) - } - if n == 0 { - return nil, os.ErrNotExist - } - zr, err := gzip.NewReader(bytes.NewReader(b.Bytes())) - if err != nil { - log.Errorf("Unable to parse %s: %v", prefix, err) - return nil, os.ErrNotExist - } - defer zr.Close() - - var index types.TrieIndex - _, err = index.ReadFrom(zr) - if err != nil { - log.Errorf("Unable to parse %s: %v", prefix, err) - return nil, os.ErrNotExist - } - return &index, nil -} diff --git a/exp/lighthorizon/index/builder.go b/exp/lighthorizon/index/builder.go deleted file mode 100644 index 324783b4f0..0000000000 --- a/exp/lighthorizon/index/builder.go +++ /dev/null @@ -1,366 +0,0 @@ -package index - -import ( - "context" - "fmt" - "io" - "math" - "os" - "sync" - "sync/atomic" - "time" - - "golang.org/x/sync/errgroup" - - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/ingest" - "github.com/stellar/go/ingest/ledgerbackend" - "github.com/stellar/go/metaarchive" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/log" - "github.com/stellar/go/support/storage" - "github.com/stellar/go/xdr" -) - -func BuildIndices( - ctx context.Context, - sourceUrl string, // where is raw txmeta coming from? - targetUrl string, // where should the resulting indices go? - networkPassphrase string, - ledgerRange historyarchive.Range, // inclusive - modules []string, - workerCount int, -) (*IndexBuilder, error) { - L := log.Ctx(ctx).WithField("service", "builder") - - indexStore, err := ConnectWithConfig(StoreConfig{ - URL: targetUrl, - Workers: uint32(workerCount), - Log: L.WithField("subservice", "index"), - }) - if err != nil { - return nil, err - } - - // We use historyarchive as a backend here just to abstract away dealing - // with the filesystem directly. - source, err := historyarchive.ConnectBackend( - sourceUrl, - storage.ConnectOptions{ - Context: ctx, - S3Region: "us-east-1", - }, - ) - if err != nil { - return nil, err - } - - metaArchive := metaarchive.NewMetaArchive(source) - - ledgerBackend := ledgerbackend.NewHistoryArchiveBackend(metaArchive) - - if ledgerRange.High == 0 { - var backendErr error - ledgerRange.High, backendErr = ledgerBackend.GetLatestLedgerSequence(ctx) - if backendErr != nil { - return nil, backendErr - } - } - - if ledgerRange.High < ledgerRange.Low { - return nil, fmt.Errorf("invalid ledger range: %s", ledgerRange.String()) - } - - ledgerCount := 1 + (ledgerRange.High - ledgerRange.Low) // +1 bc inclusive - parallel := int(max(1, uint32(workerCount))) - - startTime := time.Now() - L.Infof("Creating indices for ledger range: [%d, %d] (%d ledgers)", - ledgerRange.Low, ledgerRange.High, ledgerCount) - L.Infof("Using %d workers", parallel) - - // Create a bunch of workers that process ledgers a checkpoint range at a - // time (better than a ledger at a time to minimize flushes). - wg, ctx := errgroup.WithContext(ctx) - ch := make(chan historyarchive.Range, parallel) - - indexBuilder := NewIndexBuilder(indexStore, metaArchive, networkPassphrase) - for _, part := range modules { - switch part { - case "transactions": - indexBuilder.RegisterModule(ProcessTransaction) - case "accounts": - indexBuilder.RegisterModule(ProcessAccountsByCheckpoint) - case "accounts_by_ledger": - indexBuilder.RegisterModule(ProcessAccountsByLedger) - case "accounts_unbacked": - indexBuilder.RegisterModule(ProcessAccountsByCheckpointWithoutBackend) - indexStore.ClearMemory(false) - case "accounts_by_ledger_unbacked": - indexBuilder.RegisterModule(ProcessAccountsByLedgerWithoutBackend) - indexStore.ClearMemory(false) - default: - return indexBuilder, fmt.Errorf("unknown module '%s'", part) - } - } - - // Submit the work to the channels, breaking up the range into individual - // checkpoint ranges. - checkpoints := historyarchive.NewCheckpointManager(0) - go func() { - for ledger := range ledgerRange.GenerateCheckpoints(checkpoints) { - chunk := checkpoints.GetCheckpointRange(ledger) - chunk.High = min(chunk.High, ledgerRange.High) // don't exceed upper bound - chunk.Low = max(chunk.Low, ledgerRange.Low) // nor the lower bound - - ch <- chunk - } - - close(ch) - }() - - processed := uint64(0) - for i := 0; i < parallel; i++ { - wg.Go(func() error { - for ledgerRange := range ch { - count := (ledgerRange.High - ledgerRange.Low) + 1 - L.Debugf("Working on checkpoint range [%d, %d] (%d ledgers)", - ledgerRange.Low, ledgerRange.High, count) - - if err := indexBuilder.Build(ctx, ledgerRange); err != nil { - return errors.Wrapf(err, - "building indices for ledger range [%d, %d] failed", - ledgerRange.Low, ledgerRange.High) - } - - nprocessed := atomic.AddUint64(&processed, uint64(count)) - if nprocessed%1234 == 0 { - PrintProgress("Reading ledgers", nprocessed, uint64(ledgerCount), startTime) - } - - // Upload indices once every 10 checkpoints to save memory - if nprocessed%(10*uint64(checkpoints.GetCheckpointFrequency())) == 0 { - if err := indexStore.Flush(); err != nil { - return errors.Wrap(err, "flushing indices failed") - } - } - } - return nil - }) - } - - if err := wg.Wait(); err != nil { - return indexBuilder, errors.Wrap(err, "one or more workers failed") - } - - PrintProgress("Reading ledgers", processed, uint64(ledgerCount), startTime) - - L.Infof("Processed %d ledgers via %d workers", processed, parallel) - L.Infof("Uploading indices to %s", targetUrl) - if err := indexStore.Flush(); err != nil { - return indexBuilder, errors.Wrap(err, "flushing indices failed") - } - - // Assertion for testing - if processed != uint64(ledgerCount) { - L.Warnf("processed %d but expected %d", processed, ledgerCount) - } - - return indexBuilder, nil -} - -// Module is a way to process ingested data and shove it into an index store. -type Module func( - indexStore Store, - ledger xdr.LedgerCloseMeta, - transaction ingest.LedgerTransaction, -) error - -// IndexBuilder contains everything needed to build indices from ledger ranges. -type IndexBuilder struct { - store Store - metaArchive metaarchive.MetaArchive - networkPassphrase string - - lastBuiltLedgerWriteLock sync.Mutex - lastBuiltLedger uint32 - - modules []Module -} - -func NewIndexBuilder( - indexStore Store, - metaArchive metaarchive.MetaArchive, - networkPassphrase string, -) *IndexBuilder { - return &IndexBuilder{ - store: indexStore, - metaArchive: metaArchive, - networkPassphrase: networkPassphrase, - } -} - -// RegisterModule adds a module to process every given ledger. It is not -// threadsafe and all calls should be made *before* any calls to `Build`. -func (builder *IndexBuilder) RegisterModule(module Module) { - builder.modules = append(builder.modules, module) -} - -// RunModules executes all of the registered modules on the given ledger. -func (builder *IndexBuilder) RunModules( - ledger xdr.LedgerCloseMeta, - tx ingest.LedgerTransaction, -) error { - for _, module := range builder.modules { - if err := module(builder.store, ledger, tx); err != nil { - return err - } - } - - return nil -} - -// Build sequentially creates indices for each ledger in the given range based -// on the registered modules. -// -// TODO: We can probably optimize this by doing GetLedger in parallel with the -// ingestion & index building, since the network will be idle during the latter -// portion. -func (builder *IndexBuilder) Build(ctx context.Context, ledgerRange historyarchive.Range) error { - for ledgerSeq := ledgerRange.Low; ledgerSeq <= ledgerRange.High; ledgerSeq++ { - ledger, err := builder.metaArchive.GetLedger(ctx, ledgerSeq) - if err != nil { - if !os.IsNotExist(err) { - log.Errorf("error getting ledger %d: %v", ledgerSeq, err) - } - return err - } - - reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta( - builder.networkPassphrase, *ledger.V0) - if err != nil { - return err - } - - for { - tx, err := reader.Read() - if err == io.EOF { - break - } else if err != nil { - return err - } - - if err := builder.RunModules(*ledger.V0, tx); err != nil { - return err - } - } - } - - builder.lastBuiltLedgerWriteLock.Lock() - defer builder.lastBuiltLedgerWriteLock.Unlock() - builder.lastBuiltLedger = max(builder.lastBuiltLedger, ledgerRange.High) - - return nil -} - -func (builder *IndexBuilder) Watch(ctx context.Context) error { - latestLedger, err := builder.metaArchive.GetLatestLedgerSequence(ctx) - if err != nil { - log.Errorf("Failed to retrieve latest ledger: %v", err) - return err - } - nextLedger := builder.lastBuiltLedger + 1 - - log.Infof("Catching up to latest ledger: (%d, %d]", nextLedger, latestLedger) - if err = builder.Build(ctx, historyarchive.Range{ - Low: nextLedger, - High: latestLedger, - }); err != nil { - log.Errorf("Initial catchup failed: %v", err) - } - - for { - nextLedger = builder.lastBuiltLedger + 1 - log.Infof("Awaiting next ledger (%d)", nextLedger) - - // To keep the MVP simple, let's just naively poll the backend until the - // ledger we want becomes available. - // - // Refer to this thread [1] for a deeper brain dump on why we're - // preferring this over doing proper filesystem monitoring (e.g. - // fsnotify for on-disk). Essentially, supporting this for every - // possible index backend is a non-trivial amount of work with an - // uncertain payoff. - // - // [1]: https://stellarfoundation.slack.com/archives/C02B04RMK/p1654903342555669 - - // We sleep with linear backoff starting with 6s. Ledgers get posted - // every 5-7s on average, but to be extra careful, let's give it a full - // minute before we give up entirely. - timedCtx, cancel := context.WithTimeout(ctx, 60*time.Second) - defer cancel() - - sleepTime := (6 * time.Second) - outer: - for { - time.Sleep(sleepTime) - select { - case <-timedCtx.Done(): - return errors.Wrap(timedCtx.Err(), "awaiting next ledger failed") - - default: - buildErr := builder.Build(timedCtx, historyarchive.Range{ - Low: nextLedger, - High: nextLedger, - }) - if buildErr == nil { - break outer - } - - if os.IsNotExist(buildErr) { - sleepTime += (time.Second * 2) - continue - } - - return errors.Wrap(buildErr, "awaiting next ledger failed") - } - } - } -} - -func PrintProgress(prefix string, done, total uint64, startTime time.Time) { - progress := float64(done) / float64(total) - elapsed := time.Since(startTime) - - // Approximate based on how many stuff is left to do and how long this much - // progress took, e.g. if 4/10 took 2s then 6/10 will "take" 3s (though this - // assumes consistent load). - remaining := (float64(elapsed) / float64(done)) * float64(total-done) - - var remainingStr string - if math.IsInf(remaining, 0) || math.IsNaN(remaining) { - remainingStr = "unknown" - } else { - remainingStr = time.Duration(remaining).Round(time.Millisecond).String() - } - - log.Infof("%s - %.1f%% (%d/%d) - elapsed: %s, remaining: ~%s", prefix, - 100*progress, done, total, - elapsed.Round(time.Millisecond), - remainingStr, - ) -} - -func min(a, b uint32) uint32 { - if a < b { - return a - } - return b -} - -func max(a, b uint32) uint32 { - if a > b { - return a - } - return b -} diff --git a/exp/lighthorizon/index/cmd/batch/doc.go b/exp/lighthorizon/index/cmd/batch/doc.go deleted file mode 100644 index 70e55009d5..0000000000 --- a/exp/lighthorizon/index/cmd/batch/doc.go +++ /dev/null @@ -1,52 +0,0 @@ -// Package batch provides two commands: map and reduce that can be run in AWS -// Batch to generate indexes for occurences of accounts in each checkpoint. -// -// map step is using AWS_BATCH_JOB_ARRAY_INDEX env variable provided by AWS -// Batch to cut all checkpoint history into smaller chunks, each processed by a -// single map batch job (and by multiple parallel workers in a single job). A -// single job simply creates indexes for a given range of checkpoints and save -// indexes and all accounts seen in a given range (FlushAccounts method) to a -// job folder (job_X, X = 0, 1, 2, 3, ...) in S3. -// -// network history split into chunks: -// [ | | | | | | | | | | | | | | | | | | | | | ] -// ---- -// / \ -// / \ -// / \ -// [..........] <- each chunk consists of checkpoints -// | -// . - each checkpoint is processed by a free -// worker (go routine) -// -// reduce step is responsible for merging all indexes created in map step into a -// final indexes for each account and for entire network history. Each reduce -// job goes through all map job results (0..MAP_JOBS) and reads all accounts -// processed in a given map job. Then for each account it merges indexes from -// all map jobs. Each reduce job maintains `doneAccounts` map because if a given -// account index was processed earlier it should be skipped instead of being -// processed again. Each reduce job also runs multiple parallel workers. Finally -// the method that is used to determine if the following (job, worker) should -// process a given account is using a 64-bit hash of account ID. The hash is -// split into two 32-bit parts: left and right. If the left part modulo -// REDUCE_JOBS is equal the job index and the right part modulo a number of -// parallel workers is equal the worker index then the account is processed. -// Otherwise it's skipped (and will be picked by another (job, worker) pair). -// -// map step results saved in S3: -// x x x x x x x x x x x x x x x x x x x x x x x x x x x x -// | -// ㄴ job0/accounts <- each job results contains a list of accounts -// | processed by a given job... -// | -// ㄴ job0/... <- ...and partial indexes -// -// hash(account_id) => XXXX YYYY <- 64 bit hash of account id is calculated -// -// if XXXX % REDUCE_JOBS == JOB_ID and YYYY % WORKERS_COUNT = WORKER_ID -// then process a given account by merging all indexes of a given account -// in all map step results, then mark account as done so if the account -// is seen again it will be skiped, -// -// else: skip the account. -package batch diff --git a/exp/lighthorizon/index/cmd/batch/map/main.go b/exp/lighthorizon/index/cmd/batch/map/main.go deleted file mode 100644 index 384e99ee80..0000000000 --- a/exp/lighthorizon/index/cmd/batch/map/main.go +++ /dev/null @@ -1,144 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "runtime" - "strconv" - "strings" - - "github.com/stellar/go/exp/lighthorizon/index" - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/network" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/log" -) - -type BatchConfig struct { - historyarchive.Range - TxMetaSourceUrl string - IndexTargetUrl string - NetworkPassphrase string -} - -const ( - batchSizeEnv = "BATCH_SIZE" - jobIndexEnvName = "JOB_INDEX_ENV" - firstCheckpointEnv = "FIRST_CHECKPOINT" - txmetaSourceUrlEnv = "TXMETA_SOURCE" - indexTargetUrlEnv = "INDEX_TARGET" - workerCountEnv = "WORKER_COUNT" - networkPassphraseEnv = "NETWORK_PASSPHRASE" - modulesEnv = "MODULES" -) - -func NewBatchConfig() (*BatchConfig, error) { - indexTargetRootUrl := os.Getenv(indexTargetUrlEnv) - if indexTargetRootUrl == "" { - return nil, errors.New("required parameter: " + indexTargetUrlEnv) - } - - jobIndexEnv := os.Getenv(jobIndexEnvName) - if jobIndexEnv == "" { - return nil, errors.New("env variable can't be empty " + jobIndexEnvName) - } - jobIndex, err := strconv.ParseUint(os.Getenv(jobIndexEnv), 10, 32) - if err != nil { - return nil, errors.Wrap(err, "invalid parameter "+jobIndexEnv) - } - - firstCheckpoint, err := strconv.ParseUint(os.Getenv(firstCheckpointEnv), 10, 32) - if err != nil { - return nil, errors.Wrap(err, "invalid parameter "+firstCheckpointEnv) - } - - checkpoints := historyarchive.NewCheckpointManager(0) - if !checkpoints.IsCheckpoint(uint32(firstCheckpoint - 1)) { - return nil, fmt.Errorf( - "%s (%d) must be the first ledger in a checkpoint range", - firstCheckpointEnv, firstCheckpoint) - } - - batchSize, err := strconv.ParseUint(os.Getenv(batchSizeEnv), 10, 32) - if err != nil { - return nil, errors.Wrap(err, "invalid parameter "+batchSizeEnv) - } else if batchSize%uint64(checkpoints.GetCheckpointFrequency()) != 0 { - return nil, fmt.Errorf( - "%s (%d) must be a multiple of checkpoint frequency (%d)", - batchSizeEnv, batchSize, checkpoints.GetCheckpointFrequency()) - } - - txmetaSourceUrl := os.Getenv(txmetaSourceUrlEnv) - if txmetaSourceUrl == "" { - return nil, errors.New("required parameter " + txmetaSourceUrlEnv) - } - - firstLedger := uint32(firstCheckpoint + batchSize*jobIndex) - lastLedger := firstLedger + uint32(batchSize) - 1 - return &BatchConfig{ - Range: historyarchive.Range{Low: firstLedger, High: lastLedger}, - TxMetaSourceUrl: txmetaSourceUrl, - IndexTargetUrl: fmt.Sprintf("%s%cjob_%d", indexTargetRootUrl, os.PathSeparator, jobIndex), - }, nil -} - -func main() { - log.SetLevel(log.InfoLevel) - // log.SetLevel(log.DebugLevel) - - batch, err := NewBatchConfig() - if err != nil { - panic(err) - } - - var workerCount int - workerCountStr := os.Getenv(workerCountEnv) - if workerCountStr == "" { - workerCount = runtime.NumCPU() - } else { - workerCountParsed, innerErr := strconv.ParseUint(workerCountStr, 10, 8) - if innerErr != nil { - panic(errors.Wrapf(innerErr, - "invalid worker count parameter (%s)", workerCountStr)) - } - workerCount = int(workerCountParsed) - } - - networkPassphrase := os.Getenv(networkPassphraseEnv) - switch networkPassphrase { - case "": - log.Warnf("%s not specified, defaulting to 'testnet'", networkPassphraseEnv) - fallthrough - case "testnet": - networkPassphrase = network.TestNetworkPassphrase - case "pubnet": - networkPassphrase = network.PublicNetworkPassphrase - default: - log.Warnf("%s is not a recognized shortcut ('pubnet' or 'testnet')", - networkPassphraseEnv) - } - log.Infof("Using network passphrase '%s'", networkPassphrase) - - parsedModules := []string{} - if modules := os.Getenv(modulesEnv); modules == "" { - parsedModules = append(parsedModules, "accounts_unbacked") - } else { - parsedModules = append(parsedModules, strings.Split(modules, ",")...) - } - - log.Infof("Uploading ledger range [%d, %d] to %s", - batch.Range.Low, batch.Range.High, batch.IndexTargetUrl) - - if _, err := index.BuildIndices( - context.Background(), - batch.TxMetaSourceUrl, - batch.IndexTargetUrl, - networkPassphrase, - batch.Range, - parsedModules, - workerCount, - ); err != nil { - panic(err) - } -} diff --git a/exp/lighthorizon/index/cmd/batch/reduce/main.go b/exp/lighthorizon/index/cmd/batch/reduce/main.go deleted file mode 100644 index bff9f8216a..0000000000 --- a/exp/lighthorizon/index/cmd/batch/reduce/main.go +++ /dev/null @@ -1,389 +0,0 @@ -package main - -import ( - "encoding/hex" - "hash/fnv" - "os" - "strconv" - "strings" - "sync" - - "github.com/stellar/go/exp/lighthorizon/index" - types "github.com/stellar/go/exp/lighthorizon/index/types" - "github.com/stellar/go/support/collections/set" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/log" -) - -const ( - ACCOUNT_FLUSH_FREQUENCY = 200 - // arbitrary default, should we use runtime.NumCPU()? - DEFAULT_WORKER_COUNT = 2 -) - -type ReduceConfig struct { - JobIndex uint32 - MapJobCount uint32 - ReduceJobCount uint32 - IndexTarget string - IndexRootSource string - - Workers uint32 -} - -func ReduceConfigFromEnvironment() (*ReduceConfig, error) { - const ( - mapJobsEnv = "MAP_JOB_COUNT" - reduceJobsEnv = "REDUCE_JOB_COUNT" - workerCountEnv = "WORKER_COUNT" - jobIndexEnvName = "JOB_INDEX_ENV" - indexRootSourceEnv = "INDEX_SOURCE_ROOT" - indexTargetEnv = "INDEX_TARGET" - ) - - jobIndexEnv := strings.TrimSpace(os.Getenv(jobIndexEnvName)) - if jobIndexEnv == "" { - return nil, errors.New("env variable can't be empty " + jobIndexEnvName) - } - - jobIndex, err := strconv.ParseUint(strings.TrimSpace(os.Getenv(jobIndexEnv)), 10, 32) - if err != nil { - return nil, errors.Wrap(err, "invalid parameter "+jobIndexEnv) - } - mapJobCount, err := strconv.ParseUint(strings.TrimSpace(os.Getenv(mapJobsEnv)), 10, 32) - if err != nil { - return nil, errors.Wrap(err, "invalid parameter "+mapJobsEnv) - } - reduceJobCount, err := strconv.ParseUint(strings.TrimSpace(os.Getenv(reduceJobsEnv)), 10, 32) - if err != nil { - return nil, errors.Wrap(err, "invalid parameter "+reduceJobsEnv) - } - - workersStr := strings.TrimSpace(os.Getenv(workerCountEnv)) - if workersStr == "" { - workersStr = strconv.FormatUint(DEFAULT_WORKER_COUNT, 10) - } - workers, err := strconv.ParseUint(workersStr, 10, 32) - if err != nil { - return nil, errors.Wrap(err, "invalid parameter "+workerCountEnv) - } - - indexTarget := strings.TrimSpace(os.Getenv(indexTargetEnv)) - if indexTarget == "" { - return nil, errors.New("required parameter missing " + indexTargetEnv) - } - - indexRootSource := strings.TrimSpace(os.Getenv(indexRootSourceEnv)) - if indexRootSource == "" { - return nil, errors.New("required parameter missing " + indexRootSourceEnv) - } - - return &ReduceConfig{ - JobIndex: uint32(jobIndex), - MapJobCount: uint32(mapJobCount), - ReduceJobCount: uint32(reduceJobCount), - Workers: uint32(workers), - IndexTarget: indexTarget, - IndexRootSource: indexRootSource, - }, nil -} - -func main() { - log.SetLevel(log.InfoLevel) - - config, err := ReduceConfigFromEnvironment() - if err != nil { - panic(err) - } - - log.Infof("Connecting to %s", config.IndexTarget) - finalIndexStore, err := index.Connect(config.IndexTarget) - if err != nil { - panic(errors.Wrapf(err, "failed to connect to indices at %s", - config.IndexTarget)) - } - - if err := mergeAllIndices(finalIndexStore, config); err != nil { - panic(errors.Wrap(err, "failed to merge indices")) - } -} - -func mergeAllIndices(finalIndexStore index.Store, config *ReduceConfig) error { - doneAccounts := set.NewSafeSet[string](512) - for i := uint32(0); i < config.MapJobCount; i++ { - jobLogger := log.WithField("job", i) - - jobSubPath := "job_" + strconv.FormatUint(uint64(i), 10) - jobLogger.Infof("Connecting to url %s, sub-path %s", config.IndexRootSource, jobSubPath) - outerJobStore, err := index.ConnectWithConfig(index.StoreConfig{ - URL: config.IndexRootSource, - URLSubPath: jobSubPath, - }) - - if err != nil { - return errors.Wrapf(err, "failed to connect to indices at %s, sub-path %s", config.IndexRootSource, jobSubPath) - } - - accounts, err := outerJobStore.ReadAccounts() - // TODO: in final version this should be critical error, now just skip it - if os.IsNotExist(err) { - jobLogger.Errorf("accounts file not found (TODO!)") - continue - } else if err != nil { - return errors.Wrapf(err, "failed to read accounts for job %d", i) - } - - jobLogger.Infof("Processing %d accounts with %d workers", - len(accounts), config.Workers) - - workQueues := make([]chan string, config.Workers) - for i := range workQueues { - workQueues[i] = make(chan string, 1) - } - - for idx, queue := range workQueues { - go (func(index uint32, queue chan string) { - for _, account := range accounts { - // Account index already merged in the previous outer job? - if doneAccounts.Contains(account) { - continue - } - - // Account doesn't belong in this work queue? - if !config.shouldProcessAccount(account, index) { - continue - } - - queue <- account - } - - close(queue) - })(uint32(idx), queue) - } - - // TODO: errgroup.WithContext(ctx) - var wg sync.WaitGroup - wg.Add(int(config.Workers)) - for j := uint32(0); j < config.Workers; j++ { - go func(routineIndex uint32) { - defer wg.Done() - accountLog := jobLogger. - WithField("worker", routineIndex). - WithField("subservice", "accounts") - accountLog.Info("Started worker") - - var accountsProcessed, accountsSkipped uint64 - for account := range workQueues[routineIndex] { - accountLog. - WithField("total", len(accounts)). - WithField("indexed", accountsProcessed). - WithField("skipped", accountsSkipped) - - accountLog.Debugf("Account: %s", account) - if (accountsProcessed+accountsSkipped)%97 == 0 { - accountLog.Infof("Processed %d/%d accounts", - accountsProcessed+accountsSkipped, len(accounts)) - } - - accountLog.Debugf("Reading index for account: %s", account) - - // First, open the "final merged indices" at the root level - // for this account. - mergedIndices, readErr := outerJobStore.Read(account) - - // TODO: in final version this should be critical error, now just skip it - if os.IsNotExist(readErr) { - accountLog.Errorf("Account %s is unavailable - TODO fix", account) - continue - } else if err != nil { - panic(readErr) - } - - // Then, iterate through all of the job folders and merge - // indices from all jobs that touched this account. - for k := uint32(0); k < config.MapJobCount; k++ { - var jobErr error - - // FIXME: This could probably come from a pool. Every - // worker needs to have a connection to every index - // store, so there's no reason to re-open these for each - // inner loop. - innerJobSubPath := "job_" + strconv.FormatUint(uint64(k), 10) - innerJobStore, jobErr := index.ConnectWithConfig(index.StoreConfig{ - URL: config.IndexRootSource, - URLSubPath: innerJobSubPath, - }) - - if jobErr != nil { - accountLog.WithError(jobErr). - Errorf("Failed to open index at %s, sub-path %s", config.IndexRootSource, innerJobSubPath) - panic(jobErr) - } - - jobIndices, jobErr := innerJobStore.Read(account) - - // This job never touched this account; skip. - if os.IsNotExist(jobErr) { - continue - } else if jobErr != nil { - accountLog.WithError(jobErr). - Errorf("Failed to read index for %s", account) - panic(jobErr) - } - - if jobErr = mergeIndices(mergedIndices, jobIndices); jobErr != nil { - accountLog.WithError(jobErr). - Errorf("Merge failure for index at %s, sub-path %s", config.IndexRootSource, innerJobSubPath) - panic(jobErr) - } - } - - // Finally, save the merged index. - finalIndexStore.AddParticipantToIndexesNoBackend(account, mergedIndices) - - // Mark this account for other workers to ignore. - doneAccounts.Add(account) - accountsProcessed++ - accountLog = accountLog.WithField("processed", accountsProcessed) - - // Periodically flush to disk to save memory. - if accountsProcessed%ACCOUNT_FLUSH_FREQUENCY == 0 { - accountLog.Infof("Flushing indexed accounts.") - if flushErr := finalIndexStore.Flush(); flushErr != nil { - accountLog.WithError(flushErr).Errorf("Flush error.") - panic(flushErr) - } - } - } - - accountLog.Infof("Final account flush.") - if err = finalIndexStore.Flush(); err != nil { - accountLog.WithError(err).Errorf("Flush error.") - panic(err) - } - - // Merge the transaction indexes - // There's 256 files, (one for each first byte of the txn hash) - txLog := jobLogger. - WithField("worker", routineIndex). - WithField("subservice", "transactions") - - var prefixesProcessed, prefixesSkipped uint64 - for i := int(0x00); i <= 0xff; i++ { - b := byte(i) // can't loop over range bc overflow - if b%97 == 0 { - txLog.Infof("Processed %d/%d prefixes (%d skipped)", - prefixesProcessed, 0xff, prefixesSkipped) - } - - if !config.shouldProcessTx(b, routineIndex) { - prefixesSkipped++ - continue - } - - txLog = txLog. - WithField("indexed", prefixesProcessed). - WithField("skipped", prefixesSkipped) - - prefix := hex.EncodeToString([]byte{b}) - for k := uint32(0); k < config.MapJobCount; k++ { - var innerErr error - innerJobSubPath := "job_" + strconv.FormatUint(uint64(k), 10) - innerJobStore, innerErr := index.ConnectWithConfig(index.StoreConfig{ - URL: config.IndexRootSource, - URLSubPath: innerJobSubPath, - }) - - if innerErr != nil { - txLog.WithError(innerErr).Errorf("Failed to open index at %s, sub-path %s", config.IndexRootSource, innerJobSubPath) - panic(innerErr) - } - - innerTxnIndexes, innerErr := innerJobStore.ReadTransactions(prefix) - if os.IsNotExist(innerErr) { - continue - } else if innerErr != nil { - txLog.WithError(innerErr).Errorf("Error reading tx prefix %s", prefix) - panic(innerErr) - } - - if innerErr = finalIndexStore.MergeTransactions(prefix, innerTxnIndexes); innerErr != nil { - txLog.WithError(innerErr).Errorf("Error merging txs at prefix %s", prefix) - panic(innerErr) - } - } - - prefixesProcessed++ - } - - txLog = txLog. - WithField("indexed", prefixesProcessed). - WithField("skipped", prefixesSkipped) - - txLog.Infof("Final transaction flush...") - if err = finalIndexStore.Flush(); err != nil { - txLog.Errorf("Error flushing transactions: %v", err) - panic(err) - } - }(j) - } - - wg.Wait() - } - - return nil -} - -func (cfg *ReduceConfig) shouldProcessAccount(account string, routineIndex uint32) bool { - hash := fnv.New64a() - - // Docs state (https://pkg.go.dev/hash#Hash) that Write will never error. - hash.Write([]byte(account)) - digest := uint32(hash.Sum64()) // discard top 32 bits - - leftHalf := digest >> 16 - rightHalf := digest & 0x0000FFFF - - log.WithField("worker", routineIndex). - WithField("account", account). - Debugf("Hash: %d (left=%d, right=%d)", digest, leftHalf, rightHalf) - - // Because the digest is basically a random number (given a good hash - // function), its remainders w.r.t. the indices will distribute the work - // fairly (and deterministically). - return leftHalf%cfg.ReduceJobCount == cfg.JobIndex && - rightHalf%cfg.Workers == routineIndex -} - -func (cfg *ReduceConfig) shouldProcessTx(txPrefix byte, routineIndex uint32) bool { - hashLeft := uint32(txPrefix >> 4) - hashRight := uint32(txPrefix & 0x0F) - - // Because the transaction hash (and thus the first byte or "prefix") is a - // random value, its remainders w.r.t. the indices will distribute the work - // fairly (and deterministically). - return hashRight%cfg.ReduceJobCount == cfg.JobIndex && - hashLeft%cfg.Workers == routineIndex -} - -// For every index that exists in `dest`, finds the corresponding index in -// `source` and merges it into `dest`'s version. -func mergeIndices(dest, source map[string]*types.BitmapIndex) error { - for name, index := range dest { - // The source doesn't contain this particular index. - // - // This probably shouldn't happen, since during the Map step, there's no - // way to choose which indices you want, but, strictly-speaking, it's - // not an error, so we can just move on. - innerIndices, ok := source[name] - if !ok || innerIndices == nil { - continue - } - - if err := index.Merge(innerIndices); err != nil { - return errors.Wrapf(err, "failed to merge index for %s", name) - } - } - - return nil -} diff --git a/exp/lighthorizon/index/cmd/map.sh b/exp/lighthorizon/index/cmd/map.sh deleted file mode 100755 index 390370f2cb..0000000000 --- a/exp/lighthorizon/index/cmd/map.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/bin/bash -# -# Breaks up the given ledger dumps into checkpoints and runs a map -# job on each one. However, it's the Golang side does validation that -# the map job resulted in the correct indices. -# - -# check parameters and their validity (types, existence, etc.) - -if [[ "$#" -ne "2" ]]; then - echo "Usage: $0 " - exit 1 -fi - -if [[ ! -d "$1" ]]; then - echo "Error: txmeta src ('$1') does not exist" - echo "Usage: $0 " - exit 1 -fi - -if [[ -z $BATCH_SIZE ]]; then - echo "BATCH_SIZE environmental variable required" - exit 1 -elif ! [[ $BATCH_SIZE =~ ^[0-9]+$ ]]; then - echo "BATCH_SIZE ('$BATCH_SIZE') must be an integer" - exit 1 -fi - -if [[ -z $FIRST_LEDGER || -z $LAST_LEDGER ]]; then - echo "FIRST_LEDGER and LAST_LEDGER environmental variables required" - exit 1 -elif ! [[ $FIRST_LEDGER =~ ^[0-9]+$ && $LAST_LEDGER =~ ^[0-9]+$ ]]; then - echo "FIRST_LEDGER ('$FIRST_LEDGER') and LAST_LEDGER ('$LAST_LEDGER') must be integers" - exit 1 -fi - -if [[ ! -d "$2" ]]; then - echo "Warning: index dest ('$2') does not exist, creating..." - mkdir -p $2 -fi - -# do work - -FIRST=$FIRST_LEDGER -LAST=$LAST_LEDGER -COUNT=$(($LAST-$FIRST+1)) -# batches = ceil(count / batch_size) -# formula is from https://stackoverflow.com/a/12536521 -BATCH_COUNT=$(( ($COUNT + $BATCH_SIZE - 1) / $BATCH_SIZE )) - -if [[ "$(((LAST + 1) % 64))" -ne "0" ]]; then - echo "LAST_LEDGER ($LAST_LEDGER) should be a checkpoint ledger" - exit 1 -fi - -echo " - start: $FIRST" -echo " - end: $LAST" -echo " - count: $COUNT ($BATCH_COUNT batches @ $BATCH_SIZE ledgers each)" - -go build -o ./map ./batch/map/... -if [[ "$?" -ne "0" ]]; then - echo "Build failed" - exit 1 -fi - -pids=( ) -for (( i=0; i < $BATCH_COUNT; i++ )) -do - echo -n "Creating map job $i... " - - NETWORK_PASSPHRASE='testnet' JOB_INDEX_ENV='AWS_BATCH_JOB_ARRAY_INDEX' MODULES='accounts_unbacked,transactions' \ - AWS_BATCH_JOB_ARRAY_INDEX=$i BATCH_SIZE=$BATCH_SIZE FIRST_CHECKPOINT=$FIRST \ - TXMETA_SOURCE=file://$1 INDEX_TARGET=file://$2 WORKER_COUNT=1 \ - ./map & - - echo "pid=$!" - pids+=($!) -done - -sleep $BATCH_COUNT - -# Check the status codes for all of the map processes. -for i in "${!pids[@]}"; do - pid=${pids[$i]} - echo -n "Checking job $i (pid=$pid)... " - if ! wait "$pid"; then - echo "failed" - exit 1 - else - echo "succeeded!" - fi -done - -rm ./map -echo "All jobs succeeded!" -exit 0 diff --git a/exp/lighthorizon/index/cmd/mapreduce_test.go b/exp/lighthorizon/index/cmd/mapreduce_test.go deleted file mode 100644 index db529fd8bc..0000000000 --- a/exp/lighthorizon/index/cmd/mapreduce_test.go +++ /dev/null @@ -1,232 +0,0 @@ -package main_test - -import ( - "encoding/hex" - "fmt" - "io" - "net/url" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - "testing" - - "github.com/stellar/go/exp/lighthorizon/index" - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/network" - "github.com/stellar/go/support/collections/maps" - "github.com/stellar/go/support/collections/set" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - batchSize = 128 -) - -func TestMap(t *testing.T) { - RunMapTest(t) -} - -func TestReduce(t *testing.T) { - // First, map the index files like we normally would. - startLedger, endLedger, jobRoot := RunMapTest(t) - batchCount := (endLedger - startLedger + batchSize) / batchSize // ceil(ledgerCount / batchSize) - - // Now that indices have been "map"ped, reduce them to a single store. - - indexTarget := filepath.Join(t.TempDir(), "final-indices") - reduceTestCmd := exec.Command("./reduce.sh", jobRoot, indexTarget) - t.Logf("Running %d reduce jobs: %s", batchCount, reduceTestCmd.String()) - stdout, err := reduceTestCmd.CombinedOutput() - t.Logf(string(stdout)) - require.NoError(t, err) - - // Then, build the *same* indices using the single-process tester. - - t.Logf("Building baseline for ledger range [%d, %d]", startLedger, endLedger) - hashes, participants := IndexLedgerRange(t, txmetaSource, startLedger, endLedger) - - // Finally, compare the two to make sure the reduce job did what it's - // supposed to do. - - indexStore, err := index.Connect("file://" + indexTarget) - require.NoError(t, err) - stores := []index.Store{indexStore} // to reuse code: same as array of 1 store - - assertParticipantsEqual(t, maps.Keys(participants), stores) - for account, checkpoints := range participants { - assertParticipantCheckpointsEqual(t, account, checkpoints, stores) - } - - assertTOIDsEqual(t, hashes, stores) -} - -func RunMapTest(t *testing.T) (uint32, uint32, string) { - // Only file:// style URLs for the txmeta source are allowed while testing. - parsed, err := url.Parse(txmetaSource) - require.NoErrorf(t, err, "%s is not a valid URL", txmetaSource) - if parsed.Scheme != "file" { - t.Logf("%s is not local txmeta source", txmetaSource) - t.Skip() - } - txmetaPath := strings.Replace(txmetaSource, "file://", "", 1) - - // What ledger range are we working with? - checkpointMgr := historyarchive.NewCheckpointManager(0) - startLedger, endLedger := GetFixtureLedgerRange(t) - - // The map job *requires* that each one operate on a multiple of a - // checkpoint range, so we may need to adjust the ranges (depending on how - // many ledgers are in the fixutre) and break them up accordingly. - if !checkpointMgr.IsCheckpoint(startLedger - 1) { - startLedger = checkpointMgr.NextCheckpoint(startLedger-1) + 1 - } - if (endLedger-startLedger)%batchSize != 0 { - endLedger = checkpointMgr.PrevCheckpoint((endLedger / batchSize) * batchSize) - } - - require.Greaterf(t, endLedger, startLedger, - "not enough fixtures for batchSize=%d", batchSize) - - batchCount := (endLedger - startLedger + batchSize) / batchSize // ceil(ledgerCount / batchSize) - - t.Logf("Using %d batches to process ledger range [%d, %d]", - batchCount, startLedger, endLedger) - - require.Truef(t, - batchCount == 1 || checkpointMgr.IsCheckpoint(startLedger+batchSize-1), - "expected batch size (%d) to result in checkpoint blocks, "+ - "but start+batchSize+1 (%d+%d+1=%d) is not a checkpoint", - batchSize, batchSize, startLedger, batchSize+startLedger+1) - - // First, execute the map jobs in parallel and dump the resulting indices to - // a temporary directory. - - tempDir := filepath.Join(t.TempDir(), "indices-map") - mapTestCmd := exec.Command("./map.sh", txmetaPath, tempDir) - mapTestCmd.Env = append(os.Environ(), - fmt.Sprintf("BATCH_SIZE=%d", batchSize), - fmt.Sprintf("FIRST_LEDGER=%d", startLedger), - fmt.Sprintf("LAST_LEDGER=%d", endLedger), - fmt.Sprintf("NETWORK_PASSPHRASE='%s'", network.TestNetworkPassphrase)) - t.Logf("Running %d map jobs: %s", batchCount, mapTestCmd.String()) - stdout, err := mapTestCmd.CombinedOutput() - - t.Logf("Tried writing indices to %s:", tempDir) - t.Log(string(stdout)) - require.NoError(t, err) - - // Then, build the *same* indices using the single-process tester. - t.Logf("Building baseline for ledger range [%d, %d]", startLedger, endLedger) - hashes, participants := IndexLedgerRange(t, txmetaSource, startLedger, endLedger) - - // Now, walk through the mapped indices and ensure that at least one of the - // jobs reported the same indices for tx TOIDs and participation. - - stores := make([]index.Store, batchCount) - for i := range stores { - indexUrl := filepath.Join( - "file://", - tempDir, - "job_"+strconv.FormatUint(uint64(i), 10), - ) - index, err := index.Connect(indexUrl) - require.NoError(t, err) - require.NotNil(t, index) - stores[i] = index - - t.Logf("Connected to index #%d at %s", i+1, indexUrl) - } - - assertParticipantsEqual(t, maps.Keys(participants), stores) - for account, checkpoints := range participants { - assertParticipantCheckpointsEqual(t, account, checkpoints, stores) - } - - assertTOIDsEqual(t, hashes, stores) - - return startLedger, endLedger, tempDir -} - -func assertParticipantsEqual(t *testing.T, - expectedAccountSet []string, - indexGroup []index.Store, -) { - indexGroupAccountSet := set.NewSet[string](len(expectedAccountSet)) - for _, store := range indexGroup { - accounts, err := store.ReadAccounts() - require.NoError(t, err) - indexGroupAccountSet.AddSlice(accounts) - } - - assert.Lenf(t, indexGroupAccountSet, len(expectedAccountSet), - "quantity of accounts across indices doesn't match") - - mappedAccountSet := maps.Keys(indexGroupAccountSet) - require.ElementsMatch(t, expectedAccountSet, mappedAccountSet) -} - -func assertParticipantCheckpointsEqual(t *testing.T, - account string, - expected []uint32, - indexGroup []index.Store, -) { - // Ensure that all of the active checkpoints reported by the index match - // the ones we tracked while ingesting the range ourselves. - - foundCheckpoints := set.NewSet[uint32](len(expected)) - for _, store := range indexGroup { - var err error - var lastActiveCheckpoint uint32 = 0 - for { - lastActiveCheckpoint, err = store.NextActive(account, "all/all", lastActiveCheckpoint) - if err == io.EOF { - break - } - require.NoError(t, err) // still an error since it shouldn't happen - - foundCheckpoints.Add(lastActiveCheckpoint) - lastActiveCheckpoint += 1 // hit next active one - } - } - - // Error out if there were any extraneous checkpoints found. - for chk := range foundCheckpoints { - require.Containsf(t, expected, chk, - "found unexpected checkpoint %d", int(chk)) - } - - // Make sure everything got marked as expected in at least one index. - for _, item := range expected { - require.Containsf(t, foundCheckpoints, item, - "failed to find %d for %s (found %v)", - int(item), account, foundCheckpoints) - } -} - -func assertTOIDsEqual(t *testing.T, toids map[string]int64, stores []index.Store) { - for hash, toid := range toids { - rawHash := [32]byte{} - decodedHash, err := hex.DecodeString(hash) - require.NoError(t, err) - require.Lenf(t, decodedHash, 32, "invalid tx hash length") - copy(rawHash[:], decodedHash) - - found := false - for i, store := range stores { - storeToid, err := store.TransactionTOID(rawHash) - if err != nil { - require.ErrorIsf(t, err, io.EOF, - "only EOF errors are allowed (store %d, hash %s)", i, hash) - } else { - require.Equalf(t, toid, storeToid, - "TOIDs for tx 0x%s don't match (store %d)", hash, i) - found = true - } - } - - require.Truef(t, found, "TOID for tx 0x%s not found in stores", hash) - } -} diff --git a/exp/lighthorizon/index/cmd/reduce.sh b/exp/lighthorizon/index/cmd/reduce.sh deleted file mode 100755 index 1cfbca0ccc..0000000000 --- a/exp/lighthorizon/index/cmd/reduce.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash -# -# Combines indices that were built separately in different folders into a single -# set of indices. -# -# This focuses on starting parallel processes, but the Golang side does -# validation that the reduce jobs resulted in the correct indices. -# - -# check parameters and their validity (types, existence, etc.) - -if [[ "$#" -ne "2" ]]; then - echo "Usage: $0 " - exit 1 -fi - -if [[ ! -d "$1" ]]; then - echo "Error: index src root ('$1') does not exist" - echo "Usage: $0 " - exit 1 -fi - -if [[ ! -d "$2" ]]; then - echo "Warning: index dest ('$2') does not exist, creating..." - mkdir -p "$2" -fi - -MAP_JOB_COUNT=$(ls $1 | grep -E 'job_[0-9]+' | wc -l) -if [[ "$MAP_JOB_COUNT" -le "0" ]]; then - echo "No jobs in index src root ('$1') found." - exit 1 -fi -REDUCE_JOB_COUNT=$MAP_JOB_COUNT - -# build reduce program and start it up - -go build -o reduce ./batch/reduce/... -if [[ "$?" -ne "0" ]]; then - echo "Build failed" - exit 1 -fi - -echo "Coalescing $MAP_JOB_COUNT discovered job outputs from $1 into $2..." - -pids=( ) -for (( i=0; i < $REDUCE_JOB_COUNT; i++ )) -do - echo -n "Creating reduce job $i... " - - AWS_BATCH_JOB_ARRAY_INDEX=$i JOB_INDEX_ENV="AWS_BATCH_JOB_ARRAY_INDEX" MAP_JOB_COUNT=$MAP_JOB_COUNT \ - REDUCE_JOB_COUNT=$REDUCE_JOB_COUNT WORKER_COUNT=4 \ - INDEX_SOURCE_ROOT=file://$1 INDEX_TARGET=file://$2 \ - timeout -k 30s 10s ./reduce & - - echo "pid=$!" - pids+=($!) -done - -sleep $REDUCE_JOB_COUNT - -# Check the status codes for all of the map processes. -for i in "${!pids[@]}"; do - pid=${pids[$i]} - echo -n "Checking job $i (pid=$pid)... " - if ! wait "$pid"; then - echo "failed" - exit 1 - else - echo "succeeded!" - fi -done - -rm ./reduce # cleanup -echo "All jobs succeeded!" -exit 0 diff --git a/exp/lighthorizon/index/cmd/single/main.go b/exp/lighthorizon/index/cmd/single/main.go deleted file mode 100644 index 7661b160dc..0000000000 --- a/exp/lighthorizon/index/cmd/single/main.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "context" - "flag" - "runtime" - "strings" - - "github.com/stellar/go/exp/lighthorizon/index" - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/network" - "github.com/stellar/go/support/log" -) - -func main() { - sourceUrl := flag.String("source", "gcs://horizon-archive-poc", "history archive url to read txmeta files") - targetUrl := flag.String("target", "file://indexes", "where to write indexes") - networkPassphrase := flag.String("network-passphrase", network.TestNetworkPassphrase, "network passphrase") - start := flag.Int("start", 2, "ledger to start at (inclusive, default: 2, the earliest)") - end := flag.Int("end", 0, "ledger to end at (inclusive, default: 0, the latest as of start time)") - modules := flag.String("modules", "accounts,transactions", "comma-separated list of modules to index (default: all)") - watch := flag.Bool("watch", false, "whether to watch the `source` for new "+ - "txmeta files and index them (default: false). "+ - "note: `-watch` implies a continuous `-end 0` to get to the latest ledger in txmeta files") - workerCount := flag.Int("workers", runtime.NumCPU()-1, "number of workers (default: # of CPUs - 1)") - - flag.Parse() - log.SetLevel(log.InfoLevel) - // log.SetLevel(log.DebugLevel) - - builder, err := index.BuildIndices( - context.Background(), - *sourceUrl, - *targetUrl, - *networkPassphrase, - historyarchive.Range{ - Low: uint32(max(*start, 2)), - High: uint32(*end), - }, - strings.Split(*modules, ","), - *workerCount, - ) - if err != nil { - panic(err) - } - - if *watch { - if err := builder.Watch(context.Background()); err != nil { - panic(err) - } - } -} - -func max(a, b int) int { - if a > b { - return a - } - return b -} diff --git a/exp/lighthorizon/index/cmd/single_test.go b/exp/lighthorizon/index/cmd/single_test.go deleted file mode 100644 index 58620d2ef9..0000000000 --- a/exp/lighthorizon/index/cmd/single_test.go +++ /dev/null @@ -1,279 +0,0 @@ -package main_test - -import ( - "context" - "encoding/hex" - "io" - "io/ioutil" - "path/filepath" - "strconv" - "strings" - "testing" - - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/ingest" - "github.com/stellar/go/ingest/ledgerbackend" - "github.com/stellar/go/metaarchive" - "github.com/stellar/go/network" - "github.com/stellar/go/support/storage" - "github.com/stellar/go/toid" - "github.com/stretchr/testify/require" - - "github.com/stellar/go/exp/lighthorizon/index" -) - -const ( - txmetaSource = "file://./testdata/" -) - -/** - * There are three parts to testing this correctly: - * - test that single-process indexing works - * - test that single-process w/ multi-worker works - * - test map-reduce against the single-process results - * - * Therefore, if any of these fail, the subsequent ones are unreliable. - */ - -func TestSingleProcess(tt *testing.T) { - eldestLedger, latestLedger := GetFixtureLedgerRange(tt) - checkpoints := historyarchive.NewCheckpointManager(0) - - // We want two test variations: - // - starting at the first ledger in a checkpoint range - // - starting at an arbitrary ledger - // - // To do this, we adjust the known set of fixture ledgers we have. - var eldestCheckpointLedger uint32 - if checkpoints.IsCheckpoint(eldestLedger - 1) { - eldestCheckpointLedger = eldestLedger // first in range - eldestLedger += 5 // somewhere in the "middle" - } else { - eldestCheckpointLedger = checkpoints.NextCheckpoint(eldestLedger-1) + 1 - eldestLedger++ - } - - tt.Run("start-at-checkpoint", func(t *testing.T) { - testSingleProcess(tt, historyarchive.Range{ - Low: eldestCheckpointLedger, - High: latestLedger, - }) - }) - - tt.Run("start-at-ledger", func(t *testing.T) { - testSingleProcess(tt, historyarchive.Range{ - Low: eldestLedger, - High: latestLedger, - }) - }) -} - -func testSingleProcess(t *testing.T, ledgerRange historyarchive.Range) { - var ( - firstLedger = ledgerRange.Low - lastLedger = ledgerRange.High - ledgerCount = ledgerRange.High - ledgerRange.Low + 1 - ) - - t.Logf("Validating single-process builder on ledger range [%d, %d] (%d ledgers)", - firstLedger, lastLedger, ledgerCount) - - workerCount := 4 - tmpDir := filepath.Join("file://", t.TempDir()) - t.Logf("Storing indices in %s", tmpDir) - - ctx := context.Background() - _, err := index.BuildIndices( - ctx, - txmetaSource, - tmpDir, - network.TestNetworkPassphrase, - historyarchive.Range{Low: firstLedger, High: lastLedger}, - []string{ - "accounts", - "transactions", - }, - workerCount, - ) - require.NoError(t, err) - - hashes, participants := IndexLedgerRange(t, txmetaSource, firstLedger, lastLedger) - - store, err := index.Connect(tmpDir) - require.NoError(t, err) - require.NotNil(t, store) - - // Ensure the participants reported by the index and the ones we - // tracked while ingesting the ledger range match. - AssertParticipantsEqual(t, participants, store) - - // Ensure the transactions reported by the index match the ones - // tracked when ingesting the ledger range ourselves. - AssertTxsEqual(t, hashes, store) -} - -func AssertTxsEqual(t *testing.T, expected map[string]int64, actual index.Store) { - for hash, knownTOID := range expected { - rawHash, err := hex.DecodeString(hash) - require.NoError(t, err, "bug") - require.Len(t, rawHash, 32) - - tempBuf := [32]byte{} - copy(tempBuf[:], rawHash[:]) - - rawTOID, err := actual.TransactionTOID(tempBuf) - require.NoErrorf(t, err, "expected TOID for tx hash %s", hash) - - require.Equalf(t, knownTOID, rawTOID, - "expected TOID %v, got %v", - toid.Parse(knownTOID), toid.Parse(rawTOID)) - } -} - -func AssertParticipantsEqual(t *testing.T, expected map[string][]uint32, actual index.Store) { - accounts, err := actual.ReadAccounts() - - require.NoError(t, err) - require.Len(t, accounts, len(expected)) - for account := range expected { - require.Contains(t, accounts, account) - } - - for account, knownCheckpoints := range expected { - // Ensure that the "everything" index exists for the account. - index, err := actual.Read(account) - require.NoError(t, err) - require.Contains(t, index, "all/all") - - // Ensure that all of the active checkpoints reported by the index match the ones we - // tracked while ingesting the range ourselves. - activeCheckpoints := []uint32{} - lastActiveCheckpoint := uint32(0) - for { - lastActiveCheckpoint, err = actual.NextActive(account, "all/all", lastActiveCheckpoint) - if err == io.EOF { - break - } - require.NoError(t, err) - - activeCheckpoints = append(activeCheckpoints, lastActiveCheckpoint) - lastActiveCheckpoint += 1 // hit next active one - } - - require.Equalf(t, knownCheckpoints, activeCheckpoints, - "incorrect checkpoints for %s", account) - } -} - -// IndexLedgerRange will connect to a dump of ledger txmeta for the given ledger -// range and build two maps from scratch (i.e. without using the indexer) by -// ingesting them manually: -// -// - a map of tx hashes to TOIDs -// - a map of accounts to a list of checkpoints they were active in -// -// These should be used as a baseline comparison of the indexer, ensuring that -// all of the data is identical. -func IndexLedgerRange( - t *testing.T, - txmetaSource string, - startLedger, endLedger uint32, // inclusive -) ( - map[string]int64, // map of "tx hash": TOID - map[string][]uint32, // map of "account": {checkpoint, checkpoint, ...} -) { - ctx := context.Background() - backend, err := historyarchive.ConnectBackend( - txmetaSource, - storage.ConnectOptions{ - Context: ctx, - S3Region: "us-east-1", - }, - ) - require.NoError(t, err) - - metaArchive := metaarchive.NewMetaArchive(backend) - - ledgerBackend := ledgerbackend.NewHistoryArchiveBackend(metaArchive) - defer ledgerBackend.Close() - - participation := make(map[string][]uint32) - hashes := make(map[string]int64) - - for ledgerSeq := startLedger; ledgerSeq <= endLedger; ledgerSeq++ { - ledger, err := ledgerBackend.GetLedger(ctx, uint32(ledgerSeq)) - require.NoError(t, err) - require.EqualValues(t, ledgerSeq, ledger.LedgerSequence()) - - reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta( - network.TestNetworkPassphrase, ledger) - require.NoError(t, err) - - for { - tx, err := reader.Read() - if err == io.EOF { - break - } - require.NoError(t, err) - - participants, err := index.GetTransactionParticipants(tx) - require.NoError(t, err) - - for _, participant := range participants { - checkpoint := index.GetCheckpointNumber(ledgerSeq) - - // Track the checkpoint in which activity occurred, keeping the - // list duplicate-free. - if list, ok := participation[participant]; ok { - if list[len(list)-1] != checkpoint { - participation[participant] = append(list, checkpoint) - } - } else { - participation[participant] = []uint32{checkpoint} - } - } - - // Track the ledger sequence in which every tx occurred. - hash := hex.EncodeToString(tx.Result.TransactionHash[:]) - hashes[hash] = toid.New( - int32(ledger.LedgerSequence()), - int32(tx.Index), - 0, - ).ToInt64() - } - } - - return hashes, participation -} - -// GetFixtureLedgerRange determines the oldest and latest ledgers w/in the -// fixture data. It's *essentially* equivalent to (but better than, since it -// handles the existence of non-integer files): -// -// LOW=$(ls $txmetaSource/ledgers | sort -n | head -n1) -// HIGH=$(ls $txmetaSource/ledgers | sort -n | tail -n1) -func GetFixtureLedgerRange(t *testing.T) (low uint32, high uint32) { - txmetaSourceDir := strings.Replace( - txmetaSource, - "file://", "", - 1) - files, err := ioutil.ReadDir(filepath.Join(txmetaSourceDir, "ledgers")) - require.NoError(t, err) - - for _, file := range files { - ledgerNum, innerErr := strconv.ParseUint(file.Name(), 10, 32) - if innerErr != nil { // non-integer filename - continue - } - - ledger := uint32(ledgerNum) - if ledger < low || low == 0 { - low = ledger - } - if ledger > high || high == 0 { - high = ledger - } - } - - return low, high -} diff --git a/exp/lighthorizon/index/cmd/testdata/latest b/exp/lighthorizon/index/cmd/testdata/latest deleted file mode 100644 index 9f53cd22d0..0000000000 --- a/exp/lighthorizon/index/cmd/testdata/latest +++ /dev/null @@ -1 +0,0 @@ -1410367 \ No newline at end of file diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410048 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410048 deleted file mode 100644 index 6eb8fee0ce56e3f4040293df7a501bffa7854e40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4160 zcmZQzfPmUN*Seg9WlQR9*WK*0K07}&+-_@44Kpw} z_C4@&G{~l;MaKgmHZm}R=&MDUH8swiW-o4VWe9GFVZ}(4L(3R3Q(Msm{q-NIUjS<%}4}4Www{m^j>zuFA zHD0|93p|oHWy?+wR%40R~=pM9l8=0*0dy7OG2YA4$7d0+Rxo4S0{Y!RJLerp-{og5fMo0kBU zMuF4=0VwVyvVy#QO!JNMLqoN*OmlN0j3O;UEFEoaq4F?w3|s*ZRcrK4H&?hU`tmdQ z1FziXq_5%o)He1W5UK0>ee6F-Aryeq1dz=Lwig)po5J1lPaooDk9n+fp}f0^>%+TO zr;8heXB_^|r+>bG(IREhnKe)H7bre&@V_^Azi+nF=6TP#c57{3bAU}XU#yHBXcpLy zyf;>7@0__-MnHa9!4&V`|K70fVqI{#QaU#(Rdp|~#4@lSxfSbx>L-Kj2V!ghBo3An zsJ)o2V`1#kq}M3-ebsr9i!-|N8A6IeF5AiFwuL>I1Jk^t{Iazo&*Ke2&)eM8uP8Rm zzp_WmI#W_li!t)|jN>~yf$GFP%3i>fGlAU#^!K5wpRabRA8Qc)VGuIu!k5aK>W4bD zUSEu2kh>rda#f`_apDdC;%A;uIAzo7W*zK!y>Exk9+Ms^`+&bm|2O`&<%GJyVY_=| z>HTdSiW7>baBRIBCgP*I;GE#|dHQGDESF5aDQf@l2ghwD&I|WXiMUnBR&BL@CR2KW z?@GzpfSjinByMnV0v!epKf~9K%riD9o|(p?n*B=UTM8$)vr?yzQl4kt$&;oNgbjh} zQWylpPBSoQXam_O;g_`NL^)K9v!J-Z%GlW0!~iG&6@$|${((=K%H#NsReLJ!xOdev zWq!cn`+gQ7R{JETeBRgW3slGy5E>NV;|kIO0qLi%C0ESiEA8F-D_?JN6o=cX5{uX6 z83Go@>$GMrzV;cUib;&`07NwdBh;-9jImi8`HW`Au2>_Pfx{E^xQSsr zAz=qTh3iX~-H;LdGykxgW9y#hJKt{Ch1v-&HGphbn80X|G$?GDfq4s_2MFdxpaxmD zko(a57zr{286X*s#DuE=$0eKxDFYZ__5;giZ>St2sLX=_BHdI?V>f}^4h^qC=Qc{h z3s$DWf*YPtfC41MAtX%D>Q@$^8Nl>Kc6iawZKS)Y3YxxXCGqTFX-U;jk{TJ~!J&0&R_1*Tw@069oZxC*k$BjVijc?*r* zgta^xByK}VYb3e}RDV+;4l&9jaHKC&Yx`EfDQG0FV$8*=!gHTT+5N9H`=t9$22&Tv z#YG!IQ$`Au#+7D(c{O(jR1E{cwjEHRwcN%p43PHSB$z%JjbsTD6DA93|G;_hIul}l z(xM>( z`s*Is>DwfU0@ZH-7IPC|`d~B`2ch_#2=mpR9jzk8d|;anQRfrg2SL&Ub32H};(oX^ E0IH<5$N&HU diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410049 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410049 deleted file mode 100644 index e253f8658a74075490d75c33bf3dce311e5fa836..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5340 zcmZQzfB>Zp#fB4WC#ud-U%qJ5w)s~kuVL+Tb*NtTvhlQK&jO_speo_oJJ-6Lgk?+W zZP(rGvOYUMG~8}$P2}1@en!oyhQDHpZ~V%fr78Tj39c2%2|m$b4%HLHD^UfuUTS!c$L0UriG|&kN)hfr*l6T z1C=;D3R?eAdgqeuT4(D&G%|$Wc=cH^oZZ3XrQDpkGdcXN#fyXkYu7$V3sbSvNQs+R zHRaCX5Y62@2Cp0Uum3IT+Q|ClgqxT~hUg)UW98n5dXL3T_DErl{o`S&mfpW$%Hpuv z3z?d}r#iU|5AAfCwR7h=9>re!2zSn|9K}kPR}JxRKQM^4F!Dawws|=y6d0I~pIH!B z0%AeHsU#qs!r zc>dK5vHLchyZJDbb9(mtANP2+U5jb^|9i%VgI;&c!+SJLbb5b&Vo3S-H#I1xflK!E zoWvGohuPPPZ)Wh9yF9#}`fQa_*TSi9no5jB0#8m>J!O1VE4)j|<1u^m<82T(F)$E{ zE1)_SAZ7xoms;JpdjH4vsV6>vsco8lK+$Z5`dyKYZbk93w$lRd2RbnDJ2^0LE3O18 zoead_cmvYt03;5U6R5qItz%*A(WKWX_kGoQk&83B@)<&kLN43M<+g=Am;=+8_v~l> zSFX#|lLGd~t8^74tQ5SlNxfp(>6ahlZhJo~^8~6B_b7V-Q_ci-3ot#%G+p+eRu|W{ zVpILY(j`V;^ffPE3~&*9d)Oq~dd5w2VS!qOvVeyNE*o!k^LF^X-qdf0J=2N@iY{Lb zZDpgfH}gT=;85b|RqlBpaNEj<8Hp;VW+!iCsMN81_cFA1NmKTUn@<)sEm;|(WFe-0 z=H6SkEmsm}AqZ0TGFODppmQ<Jv+z9z0-~uJ9}|eith&{8lB)H$R$Kq*MK&>Y&b*YjaCXHD=kAHx<=qG|zvW zDYb6@G|Qi_C%Ts(JNumdeCLW89^v_$o%1{S_sBL^8vT0A4+}rT*N)6HHYlE%#-f`2 zO66M$C%3awr;k#eXWq$^rW1q>f$CBi1jJ4=Flgui*(m8RY0-%qs2FEKaeUUv1`tCP+t2bY}bLkuiBGsQtm$R?wT2KnCb0)vkiv3 z+YkIXbBGsc9yncDaMw*tZF;Gp!H?3x)O2Xf9AK$X~x9vYMDcF%Nd?7Zr!>nL1DeyoK`=L@5%8mxuOl_e%dJH zKN1RHtqH2WI>Yb6jVU3Q-!=YMOEf(T3OknXRvH}9$|2d#ld@HqbbO>s*NM+`EBm^p zG^yX8yM${ePz?xxOLQQOlBU2ikT_sw5X}Yo7Yqo-2T+yORxK7_SqX}hNf2EK63G%I zCR`pI=Wrgdq5-p^_9rb0vVzJng6bofI^x`nFf9LK#SIgGr(Ioe1;!=lM+{ z#e4?j_$IpCKuRMphk|GW*`H}ok&c$3LN-=wFRS+1*95X^$gOos`*hu~ zvAs&QPpD_xX^*9G>ow0oM z0!h&Rg2~bK{|$a!_IbDCxoYw*&xDLRciX?#)3sj-Tla1Ol_4O2$QuwoG6}6~a`(do z3HA+v3av#gIjEeE!S)05aXi!=C~-ran=GhZwxGKSYd#(%Zlff;23bBPIiI2Uorrw= UkT>BF+4-30F$@y&F)Z%^0PbzrfdBvi diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410050 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410050 deleted file mode 100644 index e4e5598abee9404b292accdb2a25effd7bc2e25d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5136 zcmZQzfPjK62AE0zUHReUE=6lGcL$+M?nH(KEFH}_N?@7fA5|h)3!G* zz4!Xc(_P^po01kCkAm39zzCwR7G>T{3i$4s%Rc+j3sWJp_y1p9o}Dd2ltLblA6%rVlOYR=u@Kf~3#Hs2m z`_2FC3Higi{}}75tM$b@+EP^+yiOVFU((q0RzcIoj&bSRF6}!L!`XGhtl9*2Gd0dW z_IASkn>&_xD9^Zk#V)5hpTAT7&tHzef)}fgUOH*|L{lQ4L9~U5_rbQ!%TquuWj{{NhX?)-wA zN#@}_8YViuKR+>~{QH|46w|;ZdwNb{i?YM)YsEJ+_{&`$UQd0tN~vq%)Hh8fMk0YH zC##+^zN!`8rR4FLJ^JxBh?^J~2*njp9SaaMfz+S+#v0Zw+ND@5eX~&f<$p$RuRAHb z-IiUrcSys3Ppj8d1_tgO49r?>49umzK;__g1L;Ep%uqfrlx7Oyh6EE~^T9fp%!%lIZE@{;rLax-5?B$H?2L4Z02?>{-L>`C@{CB2J>9|Dwg>x(lI?dC<3U4?chuX;qi4O(`ORs+&udKX_S5#;_{`quT zYmUrO*YM}2hYw6HbFvi@oGW?Z$Amwg?@#15Z3q%B<+#4Sjd=pQ$feC`7Y~2&kUt9w zFL2zyP)aL1n6@TW4oIg=Jr_Yc5HC1owzQ12Lr#812FxA;{@sk5QUNk z8Y}`+RD+C?lZDyWo2=}d_xg;xYgCV7NAZ(3_hs4h|A92fo=pW&AixMV7nrYeZ$8|A zUM=Vwuj>F-s^pVmVk5*Po zZpblz%^nabn%j#Yaq?YMWgzd|H)9H^k>Ex7bpJ)Pd zJUk2t+6T4crS0YNHvzVq;hc5Jj63W#ZI4ZUX0>$9@fiY7&fi~_aRX>3lVgadUl7bO zKsx=@wd9Ife5Jixf92~OV_U1wqrh%(6NR~wJA-R zPk#v~_U>;}2+3gw8VF7g#p&*1pY=DKTHc=QxV2Qt_Q1&vCp?zw**|K1+o`bm>>O}< z;8v^ys-FyU3d|vBG(4;XYAn4UJ&= z)JX|7v5E1Ttk>Ow=Eqp>nE&XPYVm8Cl?NaE+rHu4wgUci4rht2YxLZwOjHzVx#R5_ z7Qm~kn*BG63F-!ivOghTb}!rK86`D;+l9XEXL%Wu8~vSL^;$ItamRj?Rhxcc+T?=% z6Q3I%tu$XXwdh~K50Q<%XPDmP`IP)rUFgCIbQsHC#+lOcTp6OjA6-dS{h$Fi&6$J5U@jGl=HyfO(W)d;k?%{pX*-0Ev@HFnur@$r2I~btU6Vdm)nsTM znXnF8kuksc4+Ka?Fao*1pk_nUWiv<~42Y;R#57i99s{}vR8J`YHNo06VEst$L}G%a zKm?970I?sK2E3svQR0R;H>KXAv710{hlUp-UI&TWCDy_|dEGYzL7`IrUlM}q+O4rsjrsi%Qu1dtEzBLO9Gv7zN-^IxzLK!TV)_2RVC ztoBVc4W7(Ut(@pnB z&6@Sj_8-Wmq(#T$AvQ8Fg6OM7nKzRHzI*1f&wlj6RLJc8{}-3%=&|izz0pZt{7F*&%Y!4;$tDJ#*Y=&8y>{)h%;I?-Vaiu;&*OWv=<1Yz@{LBBrQ6D;mX*#+zm_Z& z|5;0I$^luQ(??ju6qcBu-^07JCbr^Ro60BFlv5>>n2nlr{gqtm1SKvmf04h)Y+3YA z-K}yhrQ3pb@cHdse0<6wd&icON0w%Gtoz}(Wt#H&wbvO$TbOwtY}>p%1>|Dp<7bvl z0JA_m28L5fKq7^~$J+r!Pq=?`#}W_a8Mm+4T-Dxq4E-{&AByZ0-}UJOkA0;O6Szfnu&@q zJz(>JaV&q}w%MM8&Rl;F1iJ=KcAD$^?C7pD&nH+-HL#R@Yj-L0&Y|iK>xn^fUT$&j zIy3HsUh8x|vEA6{|9tJ1@6~0MJV5ineo65Ue9BZF$9JsSQ)$P&tDY(I0}kK!vk0-; zCo$#ozGhzr#yjCF*lXGzoBYgb>6+s+1fHC~ zzbxYh&`c)B5Kq4#uuC9B`l)Nl6|?wCd$<0|*P9&0;dZLT;&pk3fQ9iot(l9jeFmvw zG}MLZUQ2V-|l;J zNq{};oa>tBE|(dfGZCH_yk+f-qmt(Z7(d0z&U-VnqgQaP2s~{cVs%z^dG^rq;;QX6 zVg1oZ?o8KOXR{=Y`&5i|JlopkQpEUyIdgII@t||ETecUi|IT%MS>f8Z>S8}7sx3Nx ze;1o63C?q}XFd-Y0{vh)(>?O5aM#fvJ0~so%d|3Hcrdgi-@fv@%Zg=u8`mBA z=(m!FixK0s&|^qScci zIZzyu>vs(Mkpz&KU>AT0P*@`a+-@V$OFx+QdP@1*;u6d9G8JH*k0|Al|j6m)$ zsM)Zz4EGZe^@y0piqv~R7lG<7MW8vnP_w`kk~@)@a20TSpdzsN1d9XHS`<_qCH)iS zre*i-XzV7C8$lQ){SOkiQ4(HCKtE9<4#9DWR9=9ilC`>rrLtwuu33|Eew=*d)t8+6 zuX|5iNYXv++zAfvZnuLKkyvjKY!3hp1=oX+HUZcQWCBaVLEG4Q698 zA8Xp7fjyM?1H;)!0!U1_cR_wa2C%%0o(@2IL1HNRhD5((*pDQD#DqJCc();yx5T(f z>cZOx(Dnj`y+{H`Op@J%Qg0C5ZbkA3+(sY+n?vRV)_-WXjrzUOP+^~b&N=DneRjoD zL+&xLq}aZV3)!p+){mJd!Tl+qoVW+5%>)6Ua0bOI+!}&?I7FKct_Nxsn8G56lBS6= zAJV47VieZ!0Qmvt4@epY3ednFO8kN07bF2BCOjlT{y_%#@+PQ{f|55$^gD+ANCHSq zxO0eg8(bX`?g6^L`3KAfLVbRoWxM+itCaHt)o%b|3^!s3fYc!6FCxre&OP%+J_+W- HoX!9MR%3pG diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410052 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410052 deleted file mode 100644 index 2aa528231a7f7af967bb0788b6cd99d683b1273c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5052 zcmd5<3sg*L9KTZ;6&gwrF`Ay^QI^u^Wr`$^B$I5yW~LO`q!?6ccL=FXCy!DnGDu2k zYD9xWPYLZ7V$C3(FtK*Cm96ae-MM$%9($S==lIT=e*r26clO^BFLa$>#5>SOe&i4(_*ep|?+yd840?5?1_NQUilYt%5dVQniOV&l>j;GLK4}=+@Iundff=y%|rEOPSUc} zh~!ZwJjpR(zM=-~J7A=z;Wd()@T&7@;&GRCl@%Aw=0w+CD7$m^EqjpTmEs$Z+nCtcYfnhReD23; z#+2h5AFi?Tj5W2G9M}FQ!s+Vtb@z@^A9pL&XQu%(r7q;5KZQHz>3!eEOj=C(LFvz- z(?%|-NHnUPUOUkS*1d9c-YU5#XUxoZ37-@9O-Fr^kjSn_F;N zoNkg^#FmGj#`DoYyP za7WDde*pX`Ij~$@-GP9&wUu@3tw%IA#|b_JnI(Fu&aDh(yyp53XH3tYn0&JCFQ`ZQ zZVIkWLh}P0Q>SZFGIi!lS(}&-EUWw*g*H_t60hPR6Fm4?3{FW~aDK~`g&WGZdb3qF zs0Es*oDbW6fA0rLus@^JSz41y1`g4=+-RDxYvt09{50Lcv&k)+jco4fd&lr3nk&a9 zRW-YoYyoL_ZEf!GXXhn9ChlA0HnE`XrA9vQbr|CSjEBxE};sdc%Lx*s_qR zY0ogQnDROw6S=Dx ze%j9@U2ne644hG9);a2g2Kc<=IPpY970)(+$g0D`Kpbcqe1i zrGs*_H!dFve5tVJ*B9_u@nvc+%Pq&+8*Q8GH9Fwb%{K4HQFLk!-`^FB%J~QyjE(h4 zp!)%n;T$MqHAROM*l%=9`C@zE4(@R@U06~<$CeAq=gj_=K_+U-1M*Gkf zoFkar|DKB=0MnEDuZ#&|$LH%?!}dAz1<$E3QA2c22zM5=e1=o|AmZM;?F(=ag*t&6afSBO9@dauq+f#aS z)N?_xZ9F|#M>t63F25j!`6A6GAn2V$tmA%>b%|cnfYqBLue(fu=K$XDeL;>XW122)1q<9Uvx{SGPte96^+; z^wh`pnO+=)w#0X+F9onEen-I}eY&d<@xuFo{Jc{jF61@Np`JMsiB61xBj~A5%p3Ch z(;ECdOx7;k%s(e}0RCYPcph@ze1$ll zlSi*$+GX;qKl diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410053 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410053 deleted file mode 100644 index 25b592c2eafa3346a294d62924ece5ff1aa8480f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3896 zcmZQzfPjf_Kcv_(9FQ?<)R@MS(R8qS*&O*L)8l`f`*8V8x7jIPpekXuiB?m&`u(>b zPj=e1eokVml0vQ7(M8#z@$)ndd?WO~U$_&R8s4I7j#+O?9&uc6`o_pWw>)o2+x_R1U3kJy;bJuMS#fi7f~yZdrcqijLofanSLZ|+#)p*-XE6}z13eEv@PKYuy?3SO)}dg-L;6HSTy={&Rf zA8zfIS6^uIbDjRFglAjI+E@PBQvUy(h3@=}$n0Gx*D09$rs@Joe0#Yn{=e(8MwBKvV!+AV2^-WT~xE^8qa^H0NqfI(FCEF*Oqyp7| z^nvvfv=6H8rS0YNHvzVq;hc5Jj63W#ZI4ZUX0>$9@fiY7&fi~_aRX=`lVgadUl5Q1 z1L>!(C0ESiEA8F-D_?JN6o=cX5{uX683Go@>$GMrzV;cUj?qvTrkoM%KVTYr`TN^+ z-uv^@w}qVWesnU?V;cLaJ-hX!^Jjeg_qcoO#G8{X3f~^KdVBg^{>iQE8V#%FD(u#N zdcJES=kwn-3QyLu01X6(<(wre3gkALa6UUB|JP-i<_0!~MILL}HL|?c{7YZs`1TV6 zzmo$nJ%Pd=7LFiV!Yk6)Bdy9fyUf7MI54>?KiS*F)Z5Y47EK*PgGFG9YLHQKvM~F4 zla-zGUY~Jyjp|YCD1OrBzAStGKal!hS7#u_z(CL~KsDC0PJz_|2}ZE{fMJoea{Zci zeyPn3J#Jw;?iAh74PIFF(j41dia;$pBH{(f8Ps3QRuH1Ej zzaYFjRsNQv06Z+1GZ!}>4>~8iWqZ;3?_9^16|Q}&F7{KR+M?t4cd?n0+mONn9ClEL zz|#pq{lH`nayQJs`{Ih1%+CGO_fF_A+uOimU;c|J?;Mw$oqOwTm{qF&?OukBi}vUp zg?X2O5$pz_f7>z&*Pakh)9@7e{?jFjPqtEam7?lBcmL;Q(H7EkmfStB-Xnkc*U3)- z8|0;Tzj9`h*>R5ZGTX{PhsH@&8J+zg|1z)JTJkAeW^ZBof_E1lg+Dp$FK(B#K42xg zS%`#%t6;Gf*j8{pg0P^j0J$FqKw-cPOdBBcz<>zLh zpw+do_yx&<%_1T$iO*ZG1cJz8$nFKDYjWd>c40!9n~?oSt$1Q6t7=G_dj3WgyGz3- zVW-b+wb@y6VaM&+Ob%TB#5wm6B$R>W$bTS!#UqFYa(_Xi0G2P{

e=t(eBLd$)kb zfa-W z`imNI2o4jZas(XdBBEU0mzA$xoBDa0M}g7PM@FlzCd_VSDKehrd?-+0OC(qkW?hGz zc3@!+Nh?4N#I#kIetl?w)+=BukO^eNq2egvN0j+LGary(KGw8D1A8d(2S%_U2_P}y tAqnymGJxe}^mG8y3yVWgJ_nmc?Y1e{A=v8^SeVenUT~WjyS?zR0RXwZ)C2$k diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410054 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410054 deleted file mode 100644 index 6515d892d672c3595c26bc913d3d5461422f65a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2820 zcmZQzfB+v`d9EwwtuY0f$=X&EHt&4(Ra&Vy`L)#K8Ag--ewg+Zs7iR^+Yc#r3ZV7wf76 z+j=KO`NxB7N?LS04`L$&BZyw1a#mu`+)_4Q%~{dWYnE6aUZro8X(4Lcqd$A=>D&*- zKqU@U>>fs+F3f*yFUkF1P!OcCCFIF5=7OJ^ zk^6rnq^a$f!T??naX(}-i2|PJj^_1~dt?(`-kH_rMkGDbG#K1r( zu7K*8pqSx>Qd-%;v^DV)0xw8N&RpRg+uhVKx5rwyV}on$#C73282FtWfMEcNFOVJ> z0LLGUQ<_yh<4NV?YhOxccC2*2RbBnEfwA1;0LzQFZV5fPDtkfdWY4C;lrw_O1;(wl zcyPXI&HBWQNv03_Z~7leUo*w%NemN%Z?9req-#{yQP%L&8+QI%a&Uv8!m%fu3rc<) zN%uvWt-AX*%l5)zZ!VyLU^nfHD_$}?_fOwDp~Gx%1B-q6FQ&Y6Tyl2qt+!!Tsrt8j zp?+Wis)xB7L`!7ldb%Y#W+q#>`j;Aerst%j_@pJ5+uEY3V_?o)+oiW(e8AvfO5OfPrjrFWkV6{Mk5$rx-SWNN=st6Qu z6aL7eyE^~&w<%Hw{yBd9l4h=Jd*$?euLn|B#4dz{N_|}H;eFw|$}HD>t{Im9_p4{j zJblrkd~N&!KA>4lfr-i0JGU8Y=?2d|6FKQMpA}z3#WBH(2D_J0ZcUe*Z-6ZY#U}(H zhXqs!9G=X;_y&a)7!czhre7Z#koCayf@qi}NMekjJO$$uW&Z1Rpfm_~6HG6N#$rA! zZG-$lOM58s2Zpne1dy0;pCacykRUTy2I@bwauuW&+QiV#Z3EP zVJHC$H+WbREE^dZ#I;t|O8~&vnJTHKsr_ zS=(yD=AEy;N-Gs7zm}Rj!)Vgq57WMWud_)#v-o+>ewh;-uM?+TXc9LyXnM5&l2Fa# z9sk_~I6yWfEjnHdv5|ohL~o51*tEM=!|&<=EBB=jw@x_8AKo15E^{QHdS4UEC7W)b z5{H(`KZV;T{Ce`f+ezh~2T%FJnT{%#uJJd&$Xa)h=PmDasYmyBShmU7Nae5B*u7=h zDc#F!gB~8J{xmB>;__NWN0!gMsqRygI>VXo{JEcbx1?|)->O}26YI*j`*x}Yg>Q(y zm}vMRXxj2G`W8n5?*H-i&_xD9^Zk#V)5hpTAT7&tHzef)}fgUOH*|L{lPvI?rtW zhg-Yl)fbxlT&I63;n|k5_LYCOl>a|xp*z3eW|Db$kA{g(@6S&RDgXYa2E{aR$)28* z*rMz(`&#kM4E}PLhu2e|ty1b*IQ30aiIGU)$;qmxjIU~icPV*1W{-XhiYG826jwk+ zOi;{FnpHjHN#)~fUrJ_ntaQIsUH!6wvE1SS%Zs;e2|c+gdl~qh9DrfK22>A@GawBD zAU2p!P(K3$SZ}baGg#xCB`XT#HkxogJ0btqWtrv%HiktWYuPojyw?0nU*q`p6Ht-3 zN7)OoT96%JHvrRtmTBLby#9Up7nz=YY*?PLSX+7r-sroW=E4=t&RCjds zoxFXitKutuKiBJa`Y+k_;n~9(`<0o$S=X@u%>wzE;Sj5{s>`#7mKRrTuLYwv_ICfAIr( z#;mgmwwCLkyf!j!`SI!_Ywg40Bwqi-c{+XE&xG4#+iI#nd5Gyp?53b8T%R7E{5_3_%zrZ$v;tT>%+zk~2`HdMEr_4}3;V=QliPrKz2|zs{ zy%=U92_P|HrbEIV&I8#E0#N&bWkNhuju9xv4rLSPrWv~wXzV6fIsk>&U~?NK;RW&+ z3P6fDBqm%5x_Vd~g2M!@TnEWf5?<(bAtyM>+(u#?@|+HZr7v2!36u}u>5Axb1xXJ) zhJXxgfx~-__f1Ke_EXmlyYqw9Gh^SYty;eL!Q8XgZrl(~*3Mmhf R;U*NjkvK?9lHCN?003tm(hdLs diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410056 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410056 deleted file mode 100644 index 728fcd2b229e23b205059769a2aa299f7d5153bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6084 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 Ljz{k!!D1c&OB8C1 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410057 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410057 deleted file mode 100644 index 2ffa35e1d04a7a346665c2f659a30abf40515597..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4876 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410059 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410059 deleted file mode 100644 index 7911dde3ff96236dc754c033c8cea4ba3505a6d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4876 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410061 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410061 deleted file mode 100644 index bbd1823295d4c4f2a435d19a9c2207e29812833a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5944 zcmZQzfPgirSD!xOaLM0wic^fGj_2i0k#{pApBJ6|m-PCV$GL;PKvlw$Dlbza3OBCc z=g~eMwBz=_IUMO?-md=kV)21XFQHCABL?ph7Ms|T#ymp_m>G($e0+b4Z-eDtjy?#$|X8&)fyKag-_?ah|f)93o8UwlyFbo|Dd z4UL=UC+XLR`B$8I^YEt8*`2Rg+qoWW>JB&99dFpPfw!`@_4{JgcVE+Xu&|z+-x~Z^ zd}8{^rOPhwKcN^G8(cL>@nM^phUSHmKRoMrk|zG#cJi?!gJ=sc?}Kfdm#2VS%zXUJ z3yVJ>76hD12GS`EKHd%>dcysiJC=AT&$xZXE~h%5zf=CtUyi?m7psq6I%)buQzCyl z&uspOTf61e7n=NBr++Hp*_N{Qm4CLB|37D;JHOy&l6iQKhKWw^&rb{~|Nf>1#WZlq zo}QD~qU z9;62dK=B8TLxTDl7R=Q=V7!;fB%qC#GMo zF3Z_=YghcPeO&C`65M;%AH1sR=JmsacdkkOm1!-rxpTd&lC@ZY27<$YEpLXk_|G*9 zn~JukM8$1t@QeL*?(@nBA=TQ{U3!N8vEVS^R;&lApA5ucf5RMuM8m^Gp!Q<6j)k#D zlU}3T_f_XbF3#x6X9y_@xojtw+ZOg<4or{dk>HC}I=4dO8=qHNq#RvU?08tL=AU)q z0lqJhLjMxa0o93nl)Zo{X9Bwg=x>R;t81=nF5M>n%_vRxHam;gHNhuRVK&;zKl!pQ z-MsU+O6`M(oqC%4t{{eD;j?CyOJ8{H7r(}AZRFG1X0COf1?mQeT#-j*dh9$WuC95n zusfvfXyl1)%<+aC$98Hh>zO~(UncGB(@h06E_ygkK;R5?Wwfm-c`?(`2mOT`&ooo z?UR`Dd0#WAjA9B14GQpa1!;kR^i$W8D`xSP_HO-^uQxf0!|haw#q0760Sn`GS~C}4 z`wUXWv~H;bL^T5=)U6Jic#MAiDB1YljOCP-8egb)jKuf+)%~x6H!Hu{%G8(layge% zoOjWWH6Lv=m-tWVV=dfj^LSU#6sI$`{Ih1%+CGO z_fF_A+uOimU;c|J?;Mw$oqOwTm{qF&?Ou?fNO_GB>^@*xlgjgDxv8TPdFEtG-Z|BV z7KbbU`zLzG+Y3BMZ%bYI$T%;k^uP>O|C^OR-pH4}6zUYy;NSeZ^Oxt7?QY*X71cO^ z#vp|JbH&-<=K=LiD-hpXHwgi`baJ4`NBz~awCoKxHf=V)i>IaxYBHbiM zV>f})9tZ=&YtXrklJEkRy(j=F;*gkdCEz>==fUF;5+-PE5l9{sZGr0{Scb2Abn?i{ zTcSYoHvp6D1SEUFOe_vU@jDUbZ>lN!M2h(g$ni~d-GY=xko|zg{S3!)6CZ7up)9BL zN&ERJ^P?j3ohP1IA^PCOa?8bn;mJlbq54z)0|Af?GlCJw{RfqUrAt_wfPsj*gMoeh z#~5h+0jf(t^)?*9ECC82G2zni_=Ji;^KlEl%1^gvt|c7lRdo+y5XIyq+bdZ3|KlYu|#6K@hO`2DPogfQa@rgY4Oq zInXvbC=a0I10)U-6KXiLodV|}%1E&Nz_4wH%Av$Jk#4e}v74~wk3r%#O2P}|FO;}K z;vg~Mu?A9)3}9s(dj0_E1?4-Gb^(duMLV}4mA_=T2_+1W0ttx;4k8dAMJ+fyKvGcq zk;)TLIDzs3*bE}vL~0oTvK7|9Lv|-D?a<3!usuj^e~`Nn01__XFe4%ifcmIjCNgN1 zNglBN_IIgib=fTOKX)IPvGA*|SGmzLVSh3^^8-FDsAJLl56EE(az7|O;q?x|z8O%1 z)P=V-Ks})R1k#Ue9x?|-9H+`_k8mx&(H%ZXgO(3_!!VBK+7$k0^ zB)mZJh!R&w93&<5C0WgUuo$yx`>p(S7rci}vUp1vw4HAFwc? niM?Rku-gmrFQ|VHwwKyt5ny|XC?Bb9FSH!QRxU%G#J~UmR@7}m diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410062 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410062 deleted file mode 100644 index 9b942201c7bf38440ff06066d82f176f6d49d0a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6732 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410063 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410063 deleted file mode 100644 index 2ff80dfa664fde3f1705f5218c0e55cd8c30a679..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6004 zcmZQzfB1z!}MbNBmn zXZh8LJr-u7dK&jl&rIdK_mb&*_U9Y@GxClKIvOtdv3$Sa%VQJkm=bKwQ%a2rV=BOz>||zPZ?j;3hz?#c+4LCcpJn`3=D+g z3aE}1h?zj@Q~U#;GL^^i9jo?K+HvoyXUhD5!}t9xLag>lO!>U8*_VN_Z2$9@fiY7&fi~_aRX=`lVgadOAwF& z1L>!(C0ESiEA8F-D_?JN6o=cX5{uX683Go@>$GMrzV;cYPNJyh7EC!K*nhw@CUxrN z7N>|K@!?4d?L zn)xi}+H}j$te-E&4>S-QmYzp~FIMT?3XN}kUTKkXbXBqAVX>Nj)`_buhnX9Jfet!yx68t|P286|LP zT+Ui%<-qi2K1hS?*;Eh(0*qjDfqoFSnz%6NU74ig>A#1s?I{a2jGOqUPkYko!^WbO z`+H4g#q@h8R(Tk2lZk#2Imz_vflvna)odcO>b(0y&R5T40mTJ4K5}?|e|@C+{GXIz zt)b9|a{Dm8Du(FLI?rovafv^3q|Jnpi(VE${cG5|v7n=-kWHu~V zc=to@YEYRW?osvvW)~CKEkJ*FRGkV`?hTL<=6tkp-Ol%_XZTomePQejQn#PFf$ezO z%twy%_h0P%o}t8YP<1-*zB~OP>z4URWv+c;&*{7S;}>428yw^|-(Rk6U?2VbPWs1n z3-+@33(Rd36|SpdKbY(!F^etrvcUxBM_2PM&1;f7`XEz;(?WTF)WaOF3;N5iye%<$ z1&UK}_!+)-WS+4>@ys+9)$CU)-%>caos~L$l=3|DPM$QKAZ!R!m%<<*cA9}f!vx4i z3BRO8CyqeHI17pktc;CKOpSm7P%$`7M0vmz5E>NV;|kUbCWy%A4B)&5(ZUFItHX9h z{hh7P6vL&}-vnA#^e?T5&_Ac(bY=h9j2X@zU+-S{b@|weswX{{+^VgPIn94(QR8#v z>}0FJy|3S1O7}5$;07AUYIDQsy^u=Xy&kc`IvKOs%OZrDcZK#|-}L3r!Ts+4!Z@L> zO!*H4KsGE)7=hehP&rW8GDGtYScr%)5Yt#1ehp|Gs2o-WY66uZZ~(IeD1gL-OM~MQ z&I8#E0+755D#NEk0DX>o{212C3(uRv;*tWe5>qnng-?#F-CtAMxg6O+z%WhZ28a1PhV?5)&S3AU`1k zSe`~t4F92tSs;L(Cn2H`5)@v*aLC;O6C~Ka0V=eX&UIsev~ecE^ucJPa6n?hWFc(; zI1gTLK3)#7jgs&JwNp_5Qp6!K;Y#3n3@U=J zoB+w8v`YxKch^0dZTyEz6sQlF4;UxFZG|$hI0(h>M3~?Fx>bo3^Fi$zcsWOOI~++5 zvLCRxpTTAC1HS{2C%&n)*31m=oV~D9YJ=Wa?U1``FWvWilm5T~s-K8_4DLIDl@si1 zA?j36+Z+R6aVJVR6K6iSP9@fStoe%u_E6#vj9@_$Kw`p!lbpN)PirLl9m9Sk0VF0F zZbM4T#JEXoWfQ0^h2D<`Ta4roBqqsjLdlavj|m|818yUbfz2T^&!3UuT4`i9d2dB~ zXW9#|@IQ^)O;*pD>7BYXroA@kDpWtVKG{F094PKl%043cz6|W^KTLu4e~p3qL1Sug yfaFdjCR`ds3 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410064 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410064 deleted file mode 100644 index b33caa50d45ff3337366c34615cd62184331b5c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5940 zcmZQzfB;2N-Mi}@R|w41GSe)n71;gzum6kxO51<;-I{o$?u@G$P?fN>e2~nxEw>hy zbaFUs)ZeICH|2sibHbH7PWv9T@5{VlI`dBM*O=PZ6A z+9|se_@hBKB`rF>5Mm<(BZ%G_E3j#It%l##16J-!A8ws+l0UpT)LrICK=r;RmP zKqU^_uheAR$``o%bRPr zd|P4fgLf>;%^wPx8d&{o)MVFx>{oty=27L4RX62h4#%W2$Ul5qvsJg#<1;fLzRc{LBmc z1t1m#oC1laF!*>ofanSLZ|+#)p*-XE6}z13eEv@PKYuy?3SO)}dg-L;6HSTy={&Rf zA8zfIS6^uIbDjRFglAjI+E@PBQvUy(h3@=}$n0Gx*D09$rs;!3$J(~)mK!6c! zE--G3w%Y#VeRg%j?1UU~lg{|81pDVZWNwu{>C8L#GS+O4e0957)#iig9e=B5P18_* zQZFp;aD;iis+7bOwfwvXGr56gf&HL(qq=_1{=I51iY2vnnC~`f~CE?CL{$*y##4M z_5;{lpdS`|3VwWdXR^HJ8lF$4sw!6YL9%AK7azm)nP8S_1eGjyFX z|6bl5KR1uqb78mdiJzFp^+T|L1!OGP4~DNDnP+TJJTr|&HT#vyw-iopXQfUbr998P zlP66l2pa;`r7#GHon~OrFa@$v+@7@P#0jVvXF+j+m9epj0Z0*43{I!`2R>ygkK;R5 z?Wwfm-c`?(`2mOT`&ooo?UR`Dd0(?HP$5%5Xi$KUD@Y3jq@TK$TrrEUw0G;Te7(t0 z9B!vdEMAvq2v``e)0(;X+Gmg|)~^qtsu>ueZgrS!@#d_jeo(yX>F-&^lJ{oW$7s%2 ze*VkuH4)EF*L``Z`dF{=zTGNq_Sf7w;o2YbxmEa7Gq*pCIVSDL>?8w&Vjqx=7B0tkK^+DY z=LM+=^@fOYsJ__IASs)$Rai!#k+00hLCDTR+1BX3Z_kb1y?4xYfvWboyb^T>sV2f5 zi~+7-r3`00h0<(9C+7ay8pW`>$Vu~}%&NrX$^%(lAurl(oH~9$YzMm)1RcQN)oqs(h=+t2r6s7a!MNs@}Hh z(VX+&S1~eNfAo2yqT&r5UQj{>#|ObQ#Mrg~=%uX;OyAW(egk4S0G9ut_+Yu?=PYJ& z!0+CZix(yJs-$ydidAe|K4!XK+EwG_rZB?+s0O4Dte2pDP<1bDFPFawu+Z1OX!rE8AQ5O{L_{<4f4K=YU!Lp)uAfD9NQB5g|))!c$9XN2Yv2a$EUmM@K; zW$Q?=RJ==T+~lTzkfHNmVAVm*6%4bq&R#OyXmI6EK0kli2v zsT)9b`y8koBdE@VvKfeU6R>QishdDNoNe14;}>w;+p^K3!oEeL?xKp=oC&7h=Fg6$0k262zF7w~ii zF&aT)u>>Xjh%^8B99X(Vm<(ZIF&}H%p@BV=_yfb)NCHSqxNmXgW%P6a(hJJxpg4r* z8xsAFVLy@p5|a$Kk?1C^l})gC$FLVk0EtPmn?UIn9!^BJi;?ueZ3Hr~IpojT;0p02 zF>0H0)mgr7TdJ;BuzLN`^EN5#zE`ev75Yn7o+Q|Q2Ifg<*a6FFOlKjv6Nw4;39hsT zYUAOoH;8nT1u(p6=_agse~`EhrMw`~P3Uog9I2$lAyMVUpm7^gJ|o6WJa=Sa>5Eq3 zg;HJ+-3LYT2RxSn8Q3DuF-GCWzW7s@1h#K(ObCrHX1uy|gKR{V(<}4pLmS*8wnFt| zYiIt0%7OfjQil`KS7l&d|G@y-Uo`>h2eo_9SQFLGB+gCm&(PRSAh*N9 z3tkTl61Sn07bLm~G@L+&UacZ}z=#`TYXeDi#!`7?FR&-a^ay=Tlj6Si&Q zW&Mv%F3^;Lo<@0reIfKT3hJkV0TKOmagWUUDbTTlUZ5sOI)my*N|;DYs3f#64Clf0 tinf652ezXTZDORnN~D_%XzV7eX>^dd4JD0|=q6CxhZ=E+HI2gJ8UWPuSVsT= diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410065 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410065 deleted file mode 100644 index 13b942c6a252824830638d03b2607ebfb1828eda..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4992 zcmZQzfPnm-{KxDk-?MK$WxGSGVWx$d;u>g)a=&*fFw`g!s7hE-RQK+B#}xuI zwahe2Y6W)x{_FqZztZ;KeYYkasXOCp_QcwG-oHntFE>waOn=PYtJulSSu5f7eP&AW zFCo=yJ#HYIk`^6b2CWxnF;(yX* zfJz(^69j#)bS<;?dVVm}>(9T6P`#Jl>3h|e%wb&?7iII{W1z631@pAG&j0eJe3qOw zr7X#&bJso2PW|fjeao_*Z{@poI8LGMU9?~CblD>Rqsom6J)3rJo-ElTJe!*{Oes}u zwz!{$N^H1_sPb8@=MK2XSelv)+2=YGIwt0C9$i>XZ&%AIn z0I?w86i76M!N=PHL{GSXbH@@7-0}0Jlj&%zVgqO^8e>7bmte`OfnDe(J;~J{rQO@<=@}bpqK_O+0%0p zTa+DUUn{R5o738X&mZK2SCul=w8+5}C}n%T8>(n+Qln+$JcHY`|p_e1XL6b61L2VfZR!_q?dW3^;{ZWS_~wCi(^fT{C@eG;#=pXW0tGQ;C3#Xs;V zQ+XWUv1(7H9rvz!rpymGeBaL^#A=_!l+XK`eHj?r766^Sm4WHI2GA`qcYvJ*%y$QX z43;~7&SEA9{O&!ucu`WXN;*fTSjD#GW2XD1T{T{A3NsvlYC!tHdI{PGRrk{Na`~G8 zTg`CJx@5*3_L{cGCO@-Uy5{%{fhXthFUz^$K#M1?w2!I?ANI!KgxndSyY46rw z`FfM1INVN^SiCOJ5U?;_r!{l&wa-8$5=Aw)z-oa6BiMhyuxz>adQn^l8_%Cwv5(fw zCp+8P_WO7qFgNd)Nr=BS|DE5nt1feRisc`~m1m3I>rb6{sF;6})!H-C?NWh-)q;0; zfo6fzi{WcW<{29l&rD-c&3>iwErpZYS*g=UDbF+SogU z-C$Yt+EB;LRr!{&BwIn~x2xW3j}=Z`HmUD-^0^Cvv7sS0<~zMKwiUbVTNl4$@sDqp zCxm5Xd5O%B_&WKJC&)M!`}cuK-!_S#xOAO;Ud4o&%k_iKd3|IpXttYnT3OmW;4IY6 zl>a~gWW)UmxqIymR#Mp*gM4I0o0$KsGG-z-W*(EX+am85j_(JAkUd zbr+-#gz7?2$c95jQNoWn^Hb(P>sW-raKkW!u%;av*h7gwFoFe10Er3r8^}+{0G5~0 z(*Z~?s7?fxGw^&vqTey>M-o6{!kt6B+emlQY-qe=*o!29#3b2GDCIFR?HFiUhnL}S z8-WZg4navHM4AuML#+7>d)ky#L;mhRv?46>x4XyWIF_0uuSDML!-}yB1>59)%R=3U ztzP;Cm4oGZxSd3_ZNxN|hVub^3~EoQ1I+=o9pC^d9FUlBX?3+?14$1&mjM~rA`T`G0IDKwzZR*J}7(Jz(X&^x@VCC;7vhL)~SL1XS;9V!33~ z4OHU5m|&f)p5^V}?5e?Y{`paFbq=o{-RmCA+wU7Cw|@$KVe@{~2KSCjXZw^N>)k$n zW}(yLnFr_T&p&)^)A?PiAE&d(H1P%hI%Uw;Iaw*)H#$Q|c>?2sc|~p8=WVsz{HUXM z1z&LHyr`O_$^Dl4PjZ=3+WL2y#q!=Eke8xwryUX0&+3)@iQ-+ zVn8eiI0X_-Ves*G0MQff-`ugpLwUySD|R{6`TU*ofBtg(6}(t|^wLSwCz=xZ(|Km| zKit|aufEXa=Q{mU3D35awXgiMrTqUn3*GqzH_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ4fsxFc69> zpgLwCW&)|dxAxkMSDANrCR7%_siiuOqZ7+Wh0;-1rxcOjvfpPE2d`;fZ_a^`EqUJeX%bs5qRJmLC==t1dOsrF; zbiWCIWmh@p_&UF-6)IX6-djxhCBi;Ud%AQ;*a8#bUujOSvp9eTgZ-%P&>$ODDN!e~ zI#kuLqF81&Texj<;F535cXb?e1bJP+e&kkM2~v#KCd`wHLE>EQ~#x z^cv;9uR1SsaYk1@Lr77`Wjndtwy+0tU>eeM-s}pxaDS_w!?_gE`*9av$xo_PuxwP3 zw7R?V$NY9PpgM7nvKKJrOklSF{q6Q%Z0X*GHd7{lI=iHj?rBC>9n?=z>PC4Wh_uPIPln%DR4#RdOuq| z&uNM4AMdYX&)J@4T)&-E<+MOJ?C+YS`(a!7SIu6wIeWz`layX$A%j zb08Zf{E`-(xC|BJEGRCpGB!3gF#!rd#o%;`f8bN5@;JU@)t*W_?p^gvnICZYzMn;i z)jo+SpZ7KU0u?d^ga!rpxPr7mK>Dd`$rZEsN_)5d%GaA5#o>0U#Nu^%hJc0fI<1+D zuYCroVs^Ja08!1r2z9H2beR5mBVDy~`N?1Z?fT@nw%g~l-pZEU^-G#&bGzP}bas=< zTYusAH|2C9{%J4${AlWgrNR$QulGN^t6tfVp~21zGWLQY-wB1jn2o5a#t8XRNJ*+L`hn2!L!@m@opl|DbZ9uw@43EkP)sfrxNsU|;_}0~*$# z^ur3%3!-6`pfceqz;OxZLCOFInEk-Ac^OoW5me^E)Dh_>78<(=s3XZ;TmEg@wb!8)ty#v(Z%YbKnYJfAP= z?ToEoByMNRT{-P{K5$oA__ZmTm-OBpa-P%m5UL+NA0r0^tn37}aW<%p0!(Jo-BqqsjLTOhK(+7d3b$DA0ZX=L^#UUtZgh=y2dWbb2 GZan}7&ZU3= diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410067 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410067 deleted file mode 100644 index f093ddb04064f16254e209f130ea08a765cf2231..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6092 zcmd5<3pkY78~}S(sJk|4bo@eH~=bZQQJMa5`=RF64cy0MH z(m-zW`JwD5&&o?2o-SNgml%6fi?p}c`Wo`5l7h4?(^kY zJc|$k?$cJ>mn}T!JIhk#?6z&!F6iA&SJU`eM_T`+T0(ZU`!E%-Fe)z&&^!inJr`e1 zU#jlmICM)aOUzaEUawzJ?WIn2tK%_@(i6HX?~LW^?%pE3gDgSTY8`Pha&Be$#9gQk zEhN@hW*0x}BigeH`fo7?r4DeyGmFbz*TqHDYJ^CgP8?BNsrw`=B~@%!HR&?tO@1FW zw^#EYQi3<-I+N>KeB?YN9U?ieBj@I27K~V^yr$1Y7N&Tu3Z7*_7C$oJKC-n7?XN*zSaMgrfJ77OzG99|%91 zKVcP^UaJ55j`GIBQ1jCeB$fvWk;5P%Un{_l#0{4FPZL7N^U$7XBi4P;IUwqyoESr~+`L~3 zx0LMI#H|TaDwhqcuO%r^g5&JG9vPRTHVM3VK#bvNw>~wy0`}S;61)nf+9sN4Dd%Pt z$8rMq9UUbfk@u_k=j^2&y||U(^=r0rW10P zRC-v$=kA2T((3#7)@B8$97x!7St~Zz7OWKvZ0Rjiktcg zxh@Mg1hlnem&zBNfaT=w!!AV{0nrQgu&`-m0b!BU@IWY`Br}owzn-qH!DMxriA?|C zF+=Sg-e--Ud3TT5Za=GZHT?Io{MtO3qy|#yNxkLk1I`H>8NuH!vIq6&hC=Z-6l>y0 zEB6kp7-fly87qZbD0whFDl0W3CDj2R3z{!e4MEB}fX43wob45l(K%61Z%;avN~Ns> z6X+bj7a#|SR<_nw7AA-+@+U}ai8A6Ps00DU%AjNnTV7-YYd6Kjt}bddQgo}77L1B49=+@p;i4o9!$x;Ii=4$ zCCoW*np!B|HO(32dfhu?BmU@Mz`Q8^xJSQ+ENHW2+4UQ5%Fqrderuv@xQ4ZTG<7xr zCTwdGSZ?TUKTCvq*fjFhY!TOJp}R@Us^5)YQm%%#HP`dN6DS`*12n&mfFT#%*t}2B zaBPIYHzkhp2~f)*i0xG5i`KJ^Kn{*Ezu^LhBl*inF7QVfR@6Q?56_@$1o+N?@d;A@ z*iMDauZ#)Cj`{0b!}dA;g6||>B8G@haQ9Ord}0cOH_kMNo@J}MYQL*Dq8)wM`GDeU#Hlno!cUHl&@EROL{@Fgmy?7Zt;2_$ zu+3(w@sA52%kYBd!K*ZswRfWOQ`HUZ&I9{*(l;r<-5i>IeTanK&$WR({LbSy98Mg` zU;b1)kuw40+BKAOQvM5uX)xt0V}h|`{^IBVH-hbR`~|KBbvNof~PD(cHhYy`vY6w*Ym+JrCuDrhO*?Mm>?l5Qz6yipJ!y9+#Ln z5?lS$-G*m>_jzkN$~npB0%B75%9y6i(QgFX=lCmzA058)`7<+!A+K(IqNefhCDXo} vGrHW^;&f?%;08FEBl$=^#)d6T+PeVyZMUAWo{9cRwMO9g{0wRhwvqn>J?*N~ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410068 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410068 deleted file mode 100644 index 7329c0c54e02dfd794a2f967e62554069b812f79..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4864 zcmZQzfB=t)ldku^%s$9+(%|07pO0()?OT3QRea^;yCGI{n(v#>0;&>LZfAM5{raN! zTkkwSejwtpko@^MQ#PBiWbX_5sq?33(N*=EDUQcoHci_6UES?(gZjjUchX$sc+{R0 z>wd|N+NEp?vMFiN@$C>B85lwI)uPOsNdezIbJ=GFrGUdXa4M(qAz#3s_$?ZgJ_Eg?}Kfdm#2VS%zXUJ z3%3;@76hCEiKa04csqdT3HNXASmL2PtUh|_r0Ek)iTvq2 zv-uxx?Uq+xX!3KN{;7m#Tguv3{@GIg|D1*H{DPZF=HWdWCOW-8KQW~I`p=Z0FzmXzE|@Q}GPt z&QB|BeqK{PsQOpzbo3PYdru=cfd+#8uszojReSUF(7AfY z7U6gMk7+^uAPiKG>~3T>L+;zRQ|?Vuzu}hn?9hDK6aTMXzj5hc!PA1%UV@F{o5W@{rXSb=ehcc_h;w$_w4#)@BHuDWoMzMiz}zN?N8Q@4~WQE zqU{!=ckx=h|Ma?59QkQlj_ys|H!ay%lJdp%2-}zY zrqds7(#a{=KG6ge0U&)~y#(!ps(WdBx%^Flt!6l9T{7bidrjM8lb=~FU2}Yfz?1X$ zmu1`ln#bfA;^`LzWWYfBscXph4DJAnTxM|2B~8- z)P*T$1p5ydmS_GrD{xC45Y;=gZ1q)--#kB?7aVo;S9<)aJ(;UjRK<1ke`#~^-K~6! zu3h>4{vrQMCV`~WbCy`>xrQ;Pu8_-T1{w%X4~DNDnP+TJJTr|&HT#vyw-iopXQfUb zr998PlP66l2pa;`r7#GHon~Orumm{;<`58_wCKcrs2FEKael8Pgh|IvAmDbqM~S5^(X%q<2&9ZCJW9`tGgUCr?^&-neUU`Fp^` zpI_#yRxDxa(pdKW-OiV67cK~fENh6}zwq~_UHMKfbv?)5TmXd&%OQ!0d?HU>KC{Lj zo>XpP=3vg&aPH}eS=*mSJ@_&=ZBqu+PH-s)WTS)$R1Orj%)qh$l#jrGVA;XIAg;Bt zX%4jfz%Ua@0Er1R9TJyt9>{JGfZ7jC^P8Y@j6g9FD4TG3262-GjokzaD_D39Hn&j{ zUf}!%3u_RK6mdvQxC(Ujus8&V30ivqBnL`gG<)%Nmwqun8`(=$xfETH;R{sRG$5sX0YFQ^YVlv6v@EH4)^h*okNt-0_mnSFxCHB? zVoRg{ps5BH=I}I1M0=iref_%y&^A3NAFx8r0#ir{6Nw2|0kRJnz{*3sX_QDe+0fWc aSkveraT`h+CDBcwE*CZ85NjHRMK%E0O>=Mn diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410069 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410069 deleted file mode 100644 index 5b4dad0b871b3bbda8feb9716bdddc0d68a4c560..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4084 zcmZQzfPgrQ`lG_fd&6rlEjZNj{RYDszSa+CI*&wg%l!TH?dqh>KvlvX6DM8oeVKib z<)p#AlRqEV{M)zuq^kJJ%XdSp<}}|opS8H?Wpk%pUtnBJuts!gzQooNi=w6Boq_i= zt{XEor8|IZN?LS$4~PZ=Mi8+=<*dY>xutBrnzN##*DSFf>0c&I;1^Qun9ZhkqGrke zSK1pm@+0nl(AyLFzvGX)z(q!_Jn`sQ<$bHxx;d3zWpmQl;vK2*Qefiu5O20aIULgO z550P;=iFIRJU!4zeih4i^TMBd7vFpB?)p{6q~qP6;MokKEuy>+wryUX0&+3)@iT93 zb%0n9a0(=v!rt3Lg5o+0twLFTQGplfuC73lFinqe2d<@evk|K zdSQ`WwB5F{9V+%pchX#M{NB=jDv$^0FmU*7&$Y{*aiB&*@O+K~kA3@}qz8Z1-aI{Y zuHLal_}%_vTHx>#Z4rj4hk6Jl9HqC|-!yooU?3B4?}K-wecm<652cqD{L!9$F0y*+ z)|urXJ+fz0p;{Rj!R7+PqSy9h3cq7@#Qm4A=69reCfoOYQ=6|p|He+v`!X=LEdWOERtBc;+CV)hZU^S+t3WZ9y>s5mZQAd(2t}v|2-X zN{>XhTs*;F&mMd&@m(AD=#J!xiig9ouqjj#zOlq5SvPvYxdCAsK z8+slbn~WEu>LGRGpT47;GSrAlT*vsm)&OmHPJ-!!(MSmsi3yX% zRi{DhPg)dI3{{B|=0v&a>HG&Yb`#b#I!N3`Nq9|$#|}L3;UXl&A>KL-)cyejf_;#6 uk3yrS2Z7T224I;p0jdp7VQ~pvLX;{R7~xMqvPhg54jpeo@wi~6I&$9uzT zFD*FK^8E(G8NSvJXF88Wam)Pu^zG`T%^U}e{z})r+Ii=g&W410pMP+!F=X(o;+b)T zFF1LcmW>a{rldv34?%2XUBFrPPV$F0hq}ug38>!J#B#}| z8>qzL*<&4{l!B|;ox%}G&kDoN_Qtd&@5xP`R~fv1QtRcz&Cbid&R*JI6R~uOQ*BE` z|M8FYMyvZ}Vq79JWqsal;$n&Fo+%n=ZdzUWh)dPpyhiVmlhxec;nRIKeS6Oy^WW~# zLZ(%AqLs`GZl3Wt->EhKWXcwq^N03$r5&B0vA3b_-!ul%7BSuj+cqyx0lAp@_?eG+ z86Xw}oC1laF!*>ofanSLZ|+#)p*-XE6}z13eEv@PKYuy?3SO)}dg-L;6HSTy={&Rf zA8zfIS6^uIbDjRFglAjI+E@PBQvUy(h3@=}$n0Gx*D09$rsrPj<-c1T)N)P5{mygkK;R5?Wwfm-c`?(`2mOT`&ooo?UR`D zd0(?H17q6)ptV~Wn7-?P><3~v0H(=1Kn}~^IdA1Q?e|*pa30T0eUp?gu1DCu+&7*6 zXp>G($@YmRAjgCBf%Ou!5325^?d9?}0k)dqoOQ{JJM1-Wk4=7NwRFw#83Iqv-(Qw- z185$TV~D3;5Rd@_>8GwGSIpuo?cMq-UvF|0huf(Vi`V5D0v5*Wv}P{8_8Fv((NGtr zoDu9lU|4R*+wgxmlj$X)m=^8_RVCNCH0R~3Z*(?)zVP~?6a!a{o#}c%>t1(m3vlY> zp1F=8aY=Be)uo5Op1$gf?KxuD4e}Q_EPdx1ud$WfV3+UnzWUBK;oX7~wG-+i&J}3| z9NxT6>+l+Idf-;92dbY0ath2LXf!;m1Zpp4>sT0jH0d?UeP4B6C3e1?#skjr** zxou$&=D_q#d3WuV`%11!#Y&nXeaYogp&cL0Z><0Kmtp5xA4$n|bAjr_J<49dlrw?d z0`zxy;`GwIlE3e4oLC-8*)Tt5tC&2&Qhc4m?DhIKr!}6=Ff=k#h*6B%&a}0|?GwkB zd3x=!ANOngjM*VNN6VYjn;q%~2O%f1lP;V|#}l^Ax#y##Wo$Un=*@L|kF>k9Zr$9l z@Q<>|+UQ6ImR%pumOP4e*tSCEisZ$kKFz5@uiC@=3#Ogp1v-p*OXWo+DeIoMTb{79 zPy4(`yC>)CozzKhi@waV?y@^!B?WdoI4we0FwY>-pg3TL<~Oh~!Tbpe7f2pOG77@P z5UAo&AsBang3 zA#PKX%3abgOYE0eS=4CwKP#@hQ$v38^^0ldvrmgWsjUR-2j_z_ypSzOly0fDpAruQEpn~v53ZQ z0=XT8QPTe)aT_J!1!^}?BM!l30#bPaj>^=pZ(7%=ak9jpU`zCPle}`i$I3f@u5COJ zrNCu>bMED3U`0gL8@W5cYJmj7dK{?6+QQ%u1EemW2-63nkrE~n6DEtR%z@aSv?$05 zsuCs4iE@+Ksu&u(32Pc1ByOW5yg=8FyZr`o>5 zO0Q$*GkeYYzW#&JE&hM?hHJJsd`Pvn+n#o~ebXe@i-kKw(mbSIAHJG3X_8aJoOyLK zb(ntS${qvRl(gvhafpo!j3D}IQRdC0fbX8U?6V)eFcmU;|Nq71IeKjSS8sHZ7ypwk z161Phr6qpj%*rbgde!?+ovHlZAz~0y#LBwd54dyJ`75Kzn(uosV+lU z{`}>KKZ@2r+Z^=uipFK3^;2haBp;c!JM_DgWm$$6vvV)kiO#G<~8ekw2Yh zHvhw|-SX-SO@6M^Kb7!oOIiEMKU>QGpR>@NUvM+YJiJH4M5p)XCx(=Ne^Y~E8n|Rn z&q-`ic9?yw_+|!wxy!@rsn1p^buFCwrm4h8B=F>9)l{K1g#c68Pu4x!E%{n<~r?p!F9S~E}CA!$uf`>|}1 zt1nVZp?(mBsR!u+0u(_rklV!~ZPteZgT7M@0kE`N~pz-G8c2#CvS((?;J$IXJ@PY-K!1_UDBLrYG0^}EzJV~&e0_I7r<$n@@eglc~H9STcdv~m+lc|mktkK_+{3;`L~0tY4! E0Q!ruDF6Tf diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410072 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410072 deleted file mode 100644 index 73d1d63c7e147277f16d51941eae375f406b7b6a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5316 zcmZQzfPh11n)kG+DXjVuH$lx>Y27Aay?bqsO$~S*@0%AXs9U%KRSCC$*0JTC!;!3T zKuMn6+j{r@sJ*Ksb2zx>?8w?TFWTlqL#3~KnAUZ>%T0{CGqc`A$-G*OXUt{Fa=>L$|7!V~v@;`z4z)J<=3j*E9)uN?JrOymA^ zI7>y=CA8!W`J>mY%9ZNivXWYJGms6e3-zop+FUMcOi`7Rjoiu%-DUm;&XEy)C zt=;nK3r&8m(?6B)Y)e`D%0FAm|DUtaonLS>$vnJA!$ha|=O>1ge}7YhVj8$)PtQqg zQFfSpt@vgJf4R%U>#5IHDRnKJ`lhMGNF?y&WYtr~SGB^slsq1@M?c;MaT5arp|}F7 zV*z3&koqa_uDx|E<3DYOp#d z033f1CIf?<#Oj^WGv@wjX_U?ry!Ie_d(qDSD>G&E)^z{ln^m#q2uPjm*;JTvMzFcS zxGj{((2x+>RrmF%M3?*ArC}3$3)1vtFKM@IWz-cc%(i&nx5}%uh5h~Ax+{F)MRpgB z7`WMYF`C!s&HH2V=kt1gpn+gNbWM95Vm@*5UFQ|T2_X}1dkS+YElFN9M=;0f>VqY1 zK1UfC)CCxX{W2IB!!m$+P}~kolh2`I90kP%R)!`9rbueQYG2x3E`JkXs~OH&m&~}s zUeos2?r;q8jSK=ZU?Ba}wd9Ife5Jixf92~Ingl+=}%;^%H>@B}|}h0#P7wu$(~c#cUl5 zV~-}iM!D~+&Wl`}(Us2-QWSF8PA<1C?7bdnFJQ`NR$N^YrTO2y<9vSS^anBj&Hgd& zOpm+T{P!pi&|%>4t3CML$nJQN#p)js74mbe&o7yfcdzW#;)gFp9&321Cw+y6-%6nR z$v{lC@cYiR?TMl1ock-+hbr-~7p9f9L~IlNt0%%@DgNR>?k**uT`1uPb_+24?l_0V z7M;}(NLp>4*dx2SkA1}q&Nch%Rp$6~i)l!RpI>~fa8+x*lbhh9#h2C}yR)^yfn6$m zX8+R**<#ZZ9WJxO!mqoYz2$z%`vvE=y*Qt<$w6+IG4w$~c^%__Y!SZe0Bejz&n?!+%HvqFczVroi1B%~?FrUBC zXcj5v1JfrWzKJe(k@UbE3Zk(EsJB+5)dR80)1IZqGX*E|xx{TwN?BeawAgBe>Y7VV z!UrJwiOR>|HULOH!EzO-#+p+Ll#fAW>m-mKWPs#OBqm%1jx>-9wjY>}!R=6>YLvJk z&P~jqd`wF>Va>;b#BG#>7pQ(j0Z0*t#Dpus5r=s5F}zHsK|VwAJ7M!5{;=4+iR^q# zbp204K7MW;pLRD~V*R8?R+FxrzP_I|_T5ouId(7m?x(qPTmLSBggi0%xJ4WmO9cB3 zz;=q(EQbToG8QAAkpz&KFw=3>#}NC0W%+TaN|d-E&P|dVXzV7e`FN1Hjgs&}&zHz` z1}Sj}&Z{`;V|YF$F}!H!Hl%z;jGK7QY=NaOTDb|O-Y2^6jpPq_E(0>KMVygLRiftX z<3di3_XGcC&gNsx{4AQ&wIa8lx8&#F<8x~v`qc&g0|Al|j6m)$sM#RDgW6ZzP(B0U zK01TAjNhIQ(0+P4P#>r-1_wy)L}J3F(Zd?n_5<4wOl$2>l_=?-I5$mwKw~$7+ztyb Zc={hCZlff;Kw~Y`h(q-90v_og1^|-1y14)V diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410073 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410073 deleted file mode 100644 index 12c2708a1742789404390329814afb6510fa7d77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5080 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 Nz-g^@C% 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 PpQsUs=;Z}G(m@OWU!ZU$ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410076 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410076 deleted file mode 100644 index cf94b43e9a064bb822a713af6e836a228a28053f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3876 zcmZQzfB^9wJ-f5^PpoAK;{NsYshFVHVF|w}QhODJf1dVJck8zWsuJGsw!JS`X_bfL z9k1g&BL3D_CZ0UjwA{3@Fh4EV-(+d+tab^T^5PHS|4&zX#9lYd3!Hd9bL;#G;d zySmxxZ8XTHq(#RcLTqGU1ko#0&Pwc=Tgv9EIV(DP%@XUwtMrXBEktd5^k;89o%_KU zsKnue`wE-u_p39#mC`a~vg?0-X_#UN_e)VtbOI5E#?2uS?JC$xS3=g-lJin)BE!iL(0FusX;LfT(YO< zB(^9!%)VB9GlRd}<>B?zXRDOD7EXQBRAM9&cyhApDdVeJ;ay4|kJ+OiZ-cmrfq_t5 z0o5@BF%w9AWW=vMHx66r=qYEW{G(eF)W*ERd<%Gr`Nd>Hth92mG2R|1tz z24Zl$0cmsq5(mo()LzWiu`u>%(rc9azUsWl#Ti}s3?W4!m+jc z4YST%=UJPlJeh3tZMXA(8%w*O-!+Sz1AgC52C5VHD0=}@&IEP~Fg>U{du(Aiel9r6 zqg{2v%i@F2lMd?1{%khmT3i3bw`kcSu1lQLm|4TtdHnD>ZL6u6U?x=7d@w1!?B*|# zed3xS{7^SITt9i?yRM?9*}VDh9#ret{cGUzw-4aM{&5FDzSK7o*`giyiRN8;%lG5DrZb<0MkG>K;7yPZ_4sQ z>%qy}dveSJ8~i>s}IwsTi(G?=1wxt8zjl&oaEJM!1ccNYDcnej+Usc?eJ zvgXgN&0hb4B9pm*<}uq%oIT@q7K@PCG?vwUFK7L~e$?}3VN>Gdtt)Jw6dvNXhuWF) z9|(YKxL<+Xe^5D4*fIml4^Z5L0TJQMz`p)%2{f#&fto;Z1P3rnfC5NNxHLE};XFtl zXMoucEPL-jwLC=TKIhD5(( z*pDQD#3aLQB)Um!WfLskG3-SWKw^^YCQx|@4=1AQT_inl8-WaL4%yBjYOb}rB8vYt z(`5O+!;i z5Nxxrdw6pB*+-&4+cp4G{RFtJPzDwUq4=E$^EbtZ{UXJDlzdEdUxI{u4D%xZB*Kox diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410077 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410077 deleted file mode 100644 index 3a04690232c4a82afa74aafdd64a9f988fa11bb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3700 zcmZQzfPmvCPD?6OygO+%0@4)r7!$_4N#*Ewa21wryUX0&+3)@v{PJ zZh%-2a0(=v!rhw{{^UOPW(sY8bAy7#QgMip+1_ljV zAR8QSAblW^wCE%skOGNu78Dm)85^6Jn}Q?|>cHw#`~#mdmB;ZNtM*jdaqp^U%KU)C z_x&tFtoBJv`Mj^$7pR^oAT%hz#}%v>Or)Q>mRvE5ue5jTuYA49Q5L1NduGa`HII84^8 zIPIf1g*{^y`|dY?0_HwBTDY)NvF4X^%#Jm&>tval6dopXfQ$v1*?DGzxBeSuow?4l zHcxpn+34GD=l?dAc0s>u7C8s}zMag#@8keX8!|wp$YFxaW^l7PX!zph-CZ-8ws+{V zo-4W3Z71Hb_|CVr5(#zgbCShC_Q;-11yLZt2sRg(<|6}KuYAvrJCthN)THX>wNvPa zzG0Z(v($&5#fx6pr=Kgl#=@z@eC_%<{_UrFXP%BTV+PqdC!3({$isC`C^AEcy0 z#)nDSitNunI)4*~cf! z;~L?&BtkVL$o|9cEw$eheq{AMEaNyZ>C&c4xrZ(X=q~iPe?8AbW;ZBZF)MAJCjMud z_pjM|Zq%mz(XHC{SbazMddro9e={bvSAWh2*$o6K|A7EV!@`~s$OV;IAOH#nW@tQt z1&B!R4D9RQ3P97m4Nwy+SR<5xSpwuDG2tq};ST3P@)-lneqj0c5N-^xT!E=0&P{LD z(AZ5Nx5L6~u(^#9R1T9FUZ8S|8gU4=AE^!lJE!^GhvMr=a>DPeGu8%5{b~QYT&UFi z$&*C4`H=hp Mw-Ly|<`9@X0MuV)mjD0& diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410078 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410078 deleted file mode 100644 index 72dd66b70905f66127d834cd482c9405bfc9fbf5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5132 zcmZQzfB;`TVU=qPx0PAe*fsu4?pkp%EkgM3ouEBe?M_^53tsyXs7mm{4K;r21XFQLglQ)p1GxLzM8Y5qt`64KDl#E|muZ)#9X1DEXS zIf*UG4zsTn-^}1IcX@a{_1P+=u7y+IG?f^M1fHC%ddm2!R(O|^$7A;B$J-!oVqhQ? zS3q?vK+FVEf9e}+Shr}GVzKniLh+aX8NI#kr0jNEcH!P34gWo@UQ-zuxOXrxYqc>j zmj(fqgX0aP4-GIw`Mgk?DTMn8m`T`turekyyLu1Ee`Sh^Q@Ftw3gpD@^S~M0RpPn=^EkzHcahL)xR?JQZud9e>NFSY3j0y zUjOs@>BU)FT>D-rY!ki&RzxK`Bb_3^#!XCfct7Rej=DXO2Vw*NooQ1#E>VBs9E*ZZ z^R%$S8_vg}b}~ZZgTcXc%WdUFM)@7vr^s1dnR-0vkFxOLFHR~RJ z{B&(U*(7<>eqD>X8sF)K5zhNIxvMTbnD;RM7$}Uvaqnhx(D230ySrvGZST-!Jy&w6 z+fKY=@ttpLB@*h~=Ol|U@H;sG)4wcGJ#xGwvl&kOvT00SB)7%$Ws+2TP)H@~+ub`l z=3T$Y_bNmG$QAF?Ah$F9`p^KPfq)TgE-+vFPFIVW_aVq>Q~Gh6kP%Odo@#H+TFH@~*-JryUST5{s#*VUl(2KIyDYe(i8 z8x+q>V^Ph1rSdI>liOLT(?==KGwUzw-4aM{&5FDzSK7o*`giyiRN8;%lElikOn3U>X>q zZgt>RO1C;&`Nir;|E1Td#}=7+NiKibU#WF(qKc#C6r*qYIc7;P3?2K&l z8*B@X^X48}UdFI-(H^~{P+Nfdz-|DhgS7U4E{!H;yIKE~Z!v6GwmE%%mP}!gRq zKi4A3Kb2xpZyndZj6D{*Y}>K$JWkc)s;~C1y?FG-T?1YoG0SPJKoeQ&cP{?tc{}r# zW5PHK+bAK2S{*bcQ7T!Mq#4CRB|4+EeuV}_=6uow~kXOOy7 zA_LR|3PYHgF!PWFFyw%Wz;Ogu46+*pAbAZ`Pd+*G`S#%_Y8F;I97 zHn&j{Ug&W`ZXANc1g)(Di(il&vH6Smyah`jh&+az<|v6L+Jy=EX$RycWd9*^P{g6Z zg6;?8xIk``GW;sIwqQF?Yktj}tE<-k2opPN_;Rsx?&i4@PTtyl>&QnzsCg;>fdI)3 zj6m)`B)d?`5hCg*2KM!D)j{&wA z$stHglHCMKx8N`Y=_I&q+kJ}ZUmb!vjGq+pdYo-n z&H436Ufvm^pHT-|Z$RoFU>_2x-oV9%m5-7zHxSdGUYxdJBDBvQ2-L?3(+i@J+>gYB wtH6~8!S(~&G#8;NQSt?GZd#v8V>e+f69$RfC>cB016qf?*IS* diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410079 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410079 deleted file mode 100644 index ff584b34d8666c142fdbd6d1f84121697aa0b2fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4852 zcmZQzfPfH@p9>!tZH~ROxhj^?J3#(?LivZ1A9tSnT~(@|_c}ohs7lyZPgvy|!);}j zHFk|Zle<=2Op6fydnahmRl5@x+k)4AOp<=6%sNl}(HRGh=Hp%a_uSH0Fyl#d#+JQP zZ@&4Ze(xU0rldv3KS69{UBFrPPV$F0hq}ug38>!J#B#}| z8>qx#`{Clt;`>zpG!_OphkWFp|KDzP2G_0GQ=e5D9aKuRI+(J^_T8eaK<8Kf*V9GL z)!v!Az+B~Q>)GS#=RZDNQ2T{rZZ!M6+?uFOCG{PX41Np7JdoWatL3&gKi#5gx5W8X z6)JHmg?%dWOFy2HpImbNy_>iUSHb^_{ZqEFOyVxSpux-_+9J>UVB6;9DIga!A3rOw z;Q)vQ0jEHsDGWZ|4j_8M{hK?Mcqq@feZ?-PI-kE&{?A{Izk(O5k6t=y`b1MAe>%@> z{)bz;<<%FO{9LDhD&g6dvi6mKwv_)rXQ4a4;AWC}c#npOPVdi83@QKqrUu0{aLJyY zlh~r{F#B5Z%?$o>mxtF=pRH2rS~&GhQ;CsC;K|9Vr;M*^g?A}=JZ6u6yba;P3?2K&l8*B@X^X48}UdF)hVG$BUijnGi+S6NAD=q7N9<` z8-V^5x8hlDw{fa&yuaaf_Sued(|E=SSvO1=~4Rq@-UhXEC0C zv|!4A(FJRs3H4t8@1>|U`vS;7u=`W|1D`UL$MGGj_Eg$&@2Y3Y{D8yv{VYPP_DM|n zysz1pfw64?(AupGOy6~ZdSLMn(htnbC%AzW%N;*wF_Qy+_nut5D5+N^og-7MV%zdD z)BVz}8ZS4684f@-@bDpMAJmSQwwKG_1lVeZbJis@?y%RiJvRB7)zUS`X9zqwe}7rV z4WOA!jv<~dK|lr!q@TK$TrrEUw0G;Te7(t09B!vdEMAvq2v``e)0(;X+Gn6TiK3cY zFy)M3{{h2Nr&*SF;!U1MO)pk$-#h)x)Rt5!FB9Vj8VC+c!`F_?Gd3umnZ}};{YvFq3MaR-Qm2nn zo@d_4lcp1d4T0)X7zD&lGcah_1KDU{c~TN8##vBYU}bDOb`(+3{2~mIzV+WLfz_Ml(}ky`vb-GUB&lUL>;6T8V0+5Jz&t282dBZ|7yD3 z+M2uhOK$DDzhb7Ga(rvx%ef!^t~A;FZth=^9Zx5$$U4shG>^sATk%B7(HWjg`|cP@ zIB|yXT5NbI-*coV?xI=SmHM}Lpmu^wRv;S|CNLT#4GLRO84N0az<^+R1ysfK>q7&w z9;j9@g={!foDo#U!_*LIz6=TGgZu#V2QBTP#2*;Jf+T>%goP3$&BJ-1I06A^IHJ|_ zFu%(IRe;T+cAWsW8&OswyB8KFG_e<47h$)TA%D;DxPW+w26PWpIqV*N-dVz;~u6cvhqx6>1ikLQ0rOOt=bMX%uWf(0^~CDpA6mI5)k1LSr{! zO{0UvZIpzUJuu9v5r?W*fbdb0WC5@8kCXl~S08$_!G2u$k)x+vS^fU_6i!28=iwL(7-v@*_341#OrM@G= nUSPV0ry+DdlG{GO3=^dGG#PF}i4Pqs_5* zHdnH%tu0~cFtuRYfqmKbMuy7TcYo;I^kMb*MzS14YqfUrm@>?+VTa7D^ucob* zL}s?Yc0Q3yiI+X@KD%Vd!*am+CfmF2y*3TkH?!ZqdEr!FjBMg*^H-fk^$wCH=`1=^ z7WUq9X%9S^di7EBLp8boM=Vq>UTvHwwR-p4%agqHN*P336nGzO+q^sl~gB}`8(zR{N?y7c(MBErIV&lG$rz<^UUUd zxV2keeWA(Eb^50go^2^>U-@TC`Tuhky7LQeCYgu#Xqf2q{`|y{^6zhIP)q}t?CCj) zEy@nFuNB|S;4gQ1cs=#mDy6Q4Q{Oa|7>NX)oUD4v_^MWTmy*Y0_UOmkAZ}t{AQV?X zbu2*41X6GK+L3w22E{YeSX8rLseDV}UeNu=*7Lz^6>*aeT+BJ(YIcyXu)TKj83v zKZ_8neG*eX?`!r2YGDcp4GQpag=hwm>8GwGSIpuo?cMq-UvF|0huf(Vi`V5D0v5*W zv}P{8_8FvxDJcp>fdC`atqya!_4Wxpp8f6qhT}YqZmKNnyEgBf@a|rX+1}9grzUpz zXU!4%wEl`YQ_WGYz0N)S@d4LbnYmz%;22cQ~|K4QZN ztnQ`l@{tVO@3yzbj|S@0#DB0UzTwLXdaVeh^I>skO2ckgr!7L z%`KR6MrfQk2(V1mYTn&lv~kbYBhR0l=KSIrc|^)4%(Q1lVM&NXN4@!rVp;FoNmrU} zH>^GQUoE%A<(XU8ryE>PjVE@N&++5{8VC+cDXCC-!;(dJc3KzYJ?EbBVORB1c1E`O z4Ymcxd2>2B~Kt=oX+F>shD3 zYJmhJ*nPlq=+8QX9g7}EWZYA@m9r@JrKHf=m+n_37$<~^bj^Nyu(-8!*Lk!1MVZTz z_dcqPau&T^9h?#V^VZ2QmpYHcvfENzK(oMMapIRvWAY-oEuJrvq}qc*Dp}v|-qA7d z`bEB18Tv=Ac%O!bg&fFks3X8(QEKiKWMLUq5o}Ou>XZ{2k`ZQR7>Fe-+-wdSzPNdJ z*G#7E9lEUNN-lNViFYi%^KGp}LY@1ZWHFGru&^NL7NBC}umHOc7#7zL*BkwuzG_S5 z?kAHOr&g>N0|rx!WiSFZ4J;j#N_O5F93i@+D2`SnT-&7A9anP}|;e(Utj?;@3N|Db6nARmbdlO-=-66vM}8oLQ=z8oZOgQY=mK$GYu z2cRZu#GwMna-@6-;xb4uY*7l4b}*cAilyL(#>5ucO%t5EPW@B+%Gh>S>Bl8cup(gH zfhCRpg34i}QG$IP1_m*WMILs*Bnay7=mO2*g_;GXkP;>m6RrZioFc03AkIzhz)lNI g-Gnub4idMaq)`&x1nRR=BMucnv5cNZL1G{P0Q?W1ga7~l diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410081 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410081 deleted file mode 100644 index fbd441566ec2d9a2a39ae33b54a0c3ea46c341ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6180 zcmd5=2|ShQ8vo8Y5wgu#FOscfiLsm*4jqoQB(ii%x2#h-*&<77Ib(vTM2H zpwJN6m#ZwPu{AVVr_w}(dB4y1nZtFjQ;pxR=l473-Ja)t{^$QZ?{*+)PNXLc-&E#} zK8d)%lYrl9UfJ2MpKDf9RwJFHIFhN5KmuBPov(FCQx{`TR*J-5l}$EVzhNV#!s=Ta zk^@22rUw^YVk~trH~7Qk+MnB0q+e3}!+Z|0WOLt5PMOu0qPogbbYM@8DOJ;R9w8iD zX2>)M7hgTfx?S#yon20I>#&RJW-Yh!Sj0m3T+rxLk6XHIDV=CQ)9ssq^|uwNf)9!s~vB4s*hl4Nm+l4IQ@{ zi|1=GXdlVsBjr^x2~3nT3!-_%+ww^_l}u-hp1v1&TV$1j|S9Q z)4O`874;|G&9*=SA(C7@`9GyQ!-xr04ZL{7 z4)owTz3#&_+w@n>fe#PJAw-&-(B|E7kjKxUx83DMmeiT#i^HdURQWAy zV%FEHwaV{0BW3dIYP(EZ~EO^<9m>Rxf*)J6m12_MW7C#&|;0eUkO+ z=i-s-Z#Rs7Cnpc_=#!x}k|%-c1@J-Q2Fk?(_akGxCz`2>nrbGel!C;^vzqvu)#>x#RD+4dx2hJqp*Nn+2q#!J&HOBCKSGH1f{%(GuU-*AqkYvz+A?yWbQ5qi~AsE<&}_ zoY~hF6G&x&08R#7TnPe9d9|A(9pE{z;XsgieZs^pfp`_nJ&sB!1z2J<1j zNE>)H1SiuKa_ZW?o7`dz;KZ$sIL8|bPpx*lmr%X0PM!!S^KITfD%bw%_@j%#cM94`S#G~=z#T2;FzMqD z40=r?1$eW9xsczyY@BdmKLWRO2tTe2`v;$SaZITe1dUa{m{SDROU=9bnw$TjbWHTH zn752h$ZJ>QNB53zHIj%Y10C27v0c_5syCqX*l*ZEM{XDU_fELNEpj@QF)y@k=a1A&4p&qvB{uVR&OFEuj~(M0(#7M2B+B?U|H0PR!omS2M7XS;Bn3I8 zi07FOIEsYtv<_eyDJqP(*51rLcu={bWm_GxFA0Q*WBQA6G z8f}dkeG$-1f3s}AA#18fDYl=&=MXH|Zv?qMFEF13NFX>6>tSN82$^_u($MwNgV|vv<^;4E zs@UjU{N`J*Z&*X*UDl0^!!>WOioU14B8wM7kVO(UX@?%S{H^! zzpJr zT8_^7cTCy21D$69=S*S}TVYH~=I9rK?Q{5o--`?6(_*;fnFH@Z;7$_)?gMkO?7Q=3 z@4xrkP6`0++hEU$!tAh;0oz|)4Ce`R#I#dH?y+bE+3-WVRdp}_%Q6JF=Pky;QfJK!Iybg=N}z3siT5tF`qmH) zee;rm9sFOzyfENosLl_={uqU+vxZ4bVk?Yk$#wjNU|X<`KN6E32*}?NLrFNWi_eI;jCe6lR>ZF#F&2WB_KN_4zy1VY?x~0X diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410082 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410082 deleted file mode 100644 index 71ae40a074df6e2b389b7c4e4b7bf478c7134196..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4884 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 N`8jepZS8WC{U;MeVH*Gd diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410084 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410084 deleted file mode 100644 index 55e0a613568dd2f70608a15abfdb800c9903b283..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6220 zcmZQzfB6#6)DQVFOUWknhj39c2%2|m$b4%HLHD^UfuUTS!c$L0UriG|&kN)hfr*l6T z1C=ofanSLZ|+#)p*-XE6}z13eEv@PKYuy?3SO)}dg-L;6HSTy={&Rf zA8zfIS6^uIbDjRFglAjI+E@PBQvUy(h3@=}$n0Gx*D09$rs@JoG2U0A1=e(8MwBKvV!+AV2^-WT~xE^8qa^H0NqfI(FCEF*Oqyp7| z^nvvfv=6H8rS0YNHvzVq;hc5Jj63W#ZI4ZUX0>$9@fiY7&fi~_aRX=`lVgadUl5Q1 z1L>!(C0ESiEA8F-D_?JN6o=cX5{uX683Go@>$GMrzV;cUj?qvTrkoM%KVTY*Xp4(& zSi5;DllU!8*8<^}`&Ym4xL5wa#>VT^+y(J`*F^7##|idJ&a>ovR{rG9jw=^maU>)x z*rBig_I&@h87J6)27<$K-@T>X4^N&jXg1(J_Dl20)P48mwT(Vy1x>cs;9-y2W5B@g zc~(d-RS%Z2{^7y8-Cm!Ue1HRwWzPi&oD6U+nLgm9AA%h?}jNe7wxc z(mnes<_UTjd;_Wz_b7V-Q_ci-3(()zzBk)0{7d8ExnnL>AU%1Pca7ZY+f|0z>zIt2 zMMWAi{`QCFiuSj1HRK7D%#CgQUR1s3>7^LO{XMytX1?4Y#sYPNgV5xd&gNw=ex~|z zguQ)ix#^zkhr5n>-Chil+!cl~)mvWigx2lc`~E813uTq$qKVC&i5CS!Rb`bDoxYlF zY2@YxIt(0shOZr&XKYYBGmS+x`<2SK6i#kurA{BEJkPw7Cru{^8v@m(FbIg9W?;~8 z0kToTFKN+96Q~$xL2-eVv9YlUNB}AZr->*tm;yqB0(@M-dcg!SWdXCh^#Q03MyOjI zn2%%|`yGw!Q<3bGpU8HZ<9fz}`0wB1zo{OXZK&ZpWx}!&HtuxI`+I!N7DuL?=(Aqy zf9Gnq`3K$unROXYy#jfF=CSI$JwDBAuU%MNt?OJDeT6?Zhg&2QjGqVCwJGPTL~Lz` z+L`hn2!L!@m@opl|DbZ9uw@386`;HW21JB21N-_{A<(b}6%e2@9u8oZ00oekaA|N{ z!g(OOK>%t$u$%|w7Z6|s)rSx!0|RkxdijCIZUVU-7G8tRZIpx;D8Eo64#8o9)b;{L z-~Xs@rr*qOrS>uHS@-vFhVS8P?KL6(J{xs*9!xAgZTcP>$0<-6SDFE)(cEKDH4FsH zQlLVsN%orL5h1j38D5x1KhZ5#Qy2+2mZo-;I2Z`G# z2`^Bchysuz4v7g@f~%ZCPop5cpfU(l1}edH6Rca;J#0RgsU-?jzX4cOO@Qfx(O4XW z;&&p<&uRVSM2h*KG8F7LkQ<1u^O5wx+zz6#xS!$be8KE*C0!fO8XJ3vG)`e#*U%(Y ze^lqplh$B8H;zMBp!(7CF{m901F*7F874qPJCs4{Qi%*SZZMpMB!I+(nT{(TL+UzQ zbq#TDD&9e3H(|}kgT!qpX_-Vfq328Fv_?uC;>yRcd;m6!2)7a6$AKj)L|+Hly`b^~ zp8trj7nrW$X^3`VLYbS8{YR~MV)(X3#YDvSQI2cS(u(%QZ@Lcm&PSfry1qdHV=fa z%)2+UV6x0#sD5ny$X`%7j66w1Us_CKk=r_;i$G(3p#D2-%oc1Uk~@)@P}`tm0&pI@ pjgG%=C(=#8acG*l32WXTByK|~FGzF~Xxx?>afn{GBSkX<0{}zyZ?pga diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410085 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410085 deleted file mode 100644 index c5b81a78f23c3dd0e4be063d4d63ae7c68db8ee6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5972 zcmZQzfPjqSYs(n^ZIk!ywP|^gnDJgI>}|%?&qWXKAH8ps8RB;qs7jbyH-l#z$J&Jl zrgSdo)-i2)SsuIasL-Na2RZI8sxMFXdi3(;kyo{kORnx+k*E1Uq^NdZ)u)!$SxMg( zao3&R)w2O)Q_`XnLJ%7n7(w*bSb3?9`f%%nll|(#CFtlkLXZ&kF3R z0I?w86i76M!N=PHL{GSXbH@@7-0}0Jlj&%zVgqO^8e>7bmte`OfnDe(J;~J{rQO@<=@}bpqK_O+0%0p zTa+DUUn{R5r838enZrqG=si-fs9T;0H-6r9T>ziQT^;|)F1J^L!=33?cOW0*0m0T>1BQy0R5i*tbZUM%w40)+YhF8ffFf99&`x!<1xqJ%$8T$!*+=hV!aiHa~i zVDo`-T(an<8*5Tb=;U*bAsRe+SqhO4%lai>)h)3wdCPEo|*b4DPLTVuzk62I{ncm zot%>G6HQW~`oMY#+6T4crS0YNHvzVq;hc5Jj63W#ZI4ZUX0>$9@fiY7&fi~_aRX>3 zlVgadUl5Q11L>!(C0ESiEA8F-D_?JN6o=cX5{uX683Go@>$GMrzV;cUj?oYn{0xj> z{{h4D?QNl^bBj){y%hWCtDmi<)~vi;C%>6X1-rdVyOMJ^Lbtze@vFVbye?^bVrv;D z71*qm2&?LBxy~t9IyrUXq1C)VW5HoL?O9h-#bcf=$tuo;mu+JU#5%Mic@1n?G#h8t z35OKwG4MM%0Mmss$QeKk0w7u<(%8SuKQqJ0FFQE0BFMl!O+To@(9zZwDi2e~uQ8|oARSQpoj-(7C0;feoU7(GGo*@yP#H)Ni*+m`A&m1io3(Rj_f^I z`Q}HBHBwlB)k7Tu3J-AD5Y*4Wzy?wa^RIZ$jQH<=wO9O>jEntjRv*C?{qb5%^joWG z$MT(cypkdqHZIzucNA(1P#@S0K>rH7eORmRXTi1Tz{j(S2NP~wJ(=6J)mTUJv;TRm zKPP=RoqXnEc`&ZaHumd^MRn(#HrWO`l?50ysCF@i*!?`#4>Az!e#6&}%riD9o|(p? zn*B=UTM8$)vr?yzQl4kt$&;oNgbjh}QWylpPBSoQxB}U*_y_3+mI)^OU~$b^c)X(I9tQ$T1?fDgz9C?F%%sjXyKmnM2a5*3YIW2<3nZc5fvK&-)gYr5EFaph2g)kWyh;vi% z4jQ`&nCIbT^%4-Jp^)Ej_%WM~*CRV6cFoon%Bqm%1di=oB8Q6YYY=flKyHVH7d(9q61Sn0BP6=X73eQŝLkjfEoq_@o6y>4b<(M|30a|ItvX3nt9 zNGMpH+W-5XNloBqo@2P`I^?ti3v*>yDk7#0$Mow1sILLn1Jetlkqw86ql6!k=ELez zWTU}sEaqcPJ2bF|5`SPg8%Y3(3HL6@Psjk4m(kM!NG~i7LHQhP7PZ^#V25C@Phep} z6MMmZ1?=`REY9D^Gj~G9R^D5e;++%rJqai~V$vr6L;J>qYkRJ|DEkf#yc8%63uAD< z7bqs~QTBWeR1P`(z(NH3+F<=J7=U>IT(&~Zfm1Nc-~ym{L8{}3Gau+a;?2jJ2WVgq zCH}w&79;^ACOp(g$pb7vT_|lE68(;0Kav0vlVrES{Xm3!K<<}<*+6J)hG*ICt8?0y zF9)gzjoD$i5kmki4H9AgnUM#=BXb!Bu1U3>W9FUk$+sN#D5$7h? gXEb&b);vE*+=fz4kmx4Rm>)Ib5WTL1M>>cB01P2%U;qFB diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410086 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410086 deleted file mode 100644 index 53663361c20d6d1e8990f9089f3cb0830aa0b0bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4528 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410088 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410088 deleted file mode 100644 index 388d4569e7bdcf1d7690ffaa5cc12dd719c1a05f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4048 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 NI7F{o;gJqv0070G+ob>i diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410090 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410090 deleted file mode 100644 index 86779c9e3ae619f3e76c48d1e787c73560e13054..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3696 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410093 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410093 deleted file mode 100644 index 0fd9c17e7b7113142fbd46186fdce5c7d00c9314..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4696 zcmZQzfPiCnHK)B5a5%VC%`xlr1ePmPdlWP-tL?h8vhTs<4^4*zfvSWvT5UtS-!~Y2 ze*euTmvb3scF5$ul$EV__Zf->E$Te8j{B(HjrgxAA5TYL>p$7<$0R>z&7&v#gfG;k z3jJbk$lnICDQVFOD~OE@j39c2%2|m$b4%HLHD^UfuUTS!c$L0UriG|&kN)hfr*l6T z1C=;1i&#cYU|hqxq|-le!7Jf;X>Wu#h6H>rnsBkXU3~jR_AAZV6+u&fNE~L$S({NY z=|Q6Xt=NQq;ZJ?*)Kd4A(ob`B{1|)7*Qv?%Mu|YA8_L+jfybv_+Tq!M4rIQ$Q|eK7Q8U z{tt)+0jEHsDGWZ|4j_8M{hK?Mcqq@feZ?-PI-kE&{?A{Izk(O5k6t=y`b1MAe>%@> z{)bz;<<%FO{9LDhD&g6dvi6mKwv_)rXQ4a4;AWC}c#npOPVdi83@QKqrUu0{aLJyY zlh~r{F#B5Z%?$o>mxtF=pRH2rS~&GhQ;CsC;K|9Vr;M*^g?A}=JZ6u63=U(Ey9mV< zPzf^-GlA5)EMt#aUL$m{fpzJe4DXuv&sFy;JY|zP_Q>V)w=6c_iwwHQ4gkYI3#J}O zgW^u2G&{M{!pOKHBGlhY+odAc$<@ctB+%9tClEA$8n_)C z-XOPw!xfme`;yYu6zr5$k|;WJ&Up3>#@0U>!Z%zuKHR}=b3NBWV?s(J*N3`3j<9sD zk1tFu*cCqS{Q0!XXyWvj=8uY=cisk>2R1XUCo1fcR(Z&WZx8l8m8!{^ekG2fWv=li zmK)4_^_3YC82FtW(Bcs0PjGk=)X%`c268tv4j3AFj|ks;%jwtn_lk9}hQv(g38o3` zep?N4GPj;-3Q~E_uyN5Iy`vy2z`i*_n+sNaCKnyDgJ>^nabn%j#Yaq?YMW3?kD{T}!T*#aG(9^;f>$5db# zo}G2d5a^EB+L#gCE>(L;C;l@izcCBXU6lK0dbps(kJO*nUwc)>GCuWYTO8BJ?Nevg z8}0xrv%sYo$Zn7WfB+P)FaQc$W?(weh6xanE*PW^8^O{R%uJYexFtXV3^||}I4{@G%zj{9VE~n51d71}jW{2R4g{xFo*J0UHh^5M>VwObAGW(lxp9M7uDd%uOiqfy6;#!b1Sv z56E$W+%99VSRugYR*;~yEv2yQWuv)EC~IE%(;TJMOT4=}f*kzTLri4Uftn042U?Fq z#gOd>*Y8kqSbl+*FGSR_3{0=rVJly_L1rRZgv5l}MrQd!oSW8)U@2dy=_XKE!NLok zZU>3mP|6n)-GrPDDG4w1@&%L*Vc`Wzo8a_7?e-x!WnnL0U|~WJds)HmfDkZ0!^>0} z`x)78n)(^s7Dt3Nvb{9-GakF?>1X7&8N;p>Dye5vgqC*PpQN_#^D~~ssjWw^Y+Tp% z>)i1NTyq}ZhNg*>|3Cm_!^%TOAom|s4i;`|P(A|@?Pvz}^)EG`ZD}{4KIC|STY_XC z%xDk|vJV-+>Kw5Bz_x)nR1y}ZU@mcPdU1rtZUVU-7GCi3caXS^lJElcho}*U=;bdo HDj^gA@L7$r diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410094 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410094 deleted file mode 100644 index 7e05d0f9bb48272b1414932159b8cabcdcbc6645..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4628 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410096 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410096 deleted file mode 100644 index 1ce5bdbd0597e9d1068a26b0fbf033aea0d65cee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7196 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410097 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410097 deleted file mode 100644 index 36c7b0d4e761c76a35574fde0f04d8c00b2e4390..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6016 zcmZQzfPhEc2;LDP?hjR?dK(j989b3 zeY&2i;`y1cx8Y#zmg@T7XSY=0W0^V54TP@$sgVv>MnC6pn6{u%O#s` zpc02iLi$1Owvll=U+456yAaU-bSJ~B8!K+<1?~CAKQ(oShGFuW)R3p?5s~eolH!$; zm)~%OhD0|%7s}P}+?N*gfHQXThOV0p%RjBiS2=KwEx$AWV27C0?kOLZZ4+CxOyw-U z|NSX@ygQwi%I}zr>0<`b7DL_#+cqyx0lAp@_*s81 z6%Y#oPJu*I7<{}PK=g$BH+L-YP@Zx7id{~1K7XhDpT8V`1us?~y>!y_iKayUbe`G# z54U#9t1mS9xlaF7!m}-9?JNImDgS@YLU(?_%_Q^i9t{(n-k+ZsQvUr-4T@>tl07{q zu|?To_O;@h8T{og53i>_Tcy;saO#_;5+jkolap0X8DG^3?^5!3%pU!C8^lcv420qe zsE!4QnLz4wp4n9WShRFi@28#qms93RTQJ4X3pfAV;CcMo-`FXCLK*m-92mG2>w!uq z12H(>fHXP)iG$??YA%3*&=4r;=2C8cfk4?{+|E>MTJ=Pk->ARih0o93nl)Zo{X9Bwgm>&8cb^q4lxccz( ze)(kwUo2Xhry|yxH0xO0w{5Z)rk)f3mMe4idOXvY{tw&ctz{19eDwdAXqt4+#3Mhe zDpK?`kMKa<;GoW3aDXLJO7V-aP4fQv*0!M|7yQ@fer(QUy6U=Q>OAbzGKy%N;~dd^-P%`aQMET zMTpfti7B7=HTyC!wk-e#%~l4c@1{UKDB%ap4_QDlmOFmVVkQUt?mfA9QBto;I!C5h z#kS>Rru(H`HC}ECGaR7$z8GwGSIpuo?cMq-UvF|0huf(Vi`V5D0v5*Wv}P{8_8F*7 zqNwH;OgSUif513-yi}-V^Ph1rSdI>liOLT z(?==KGwtGFuna&eT6WIN>8subdJ<}AV@|?jI7=)rN+CcTpFh>AsP#y<|5kdXH@*Si$*wq=V zoe1>|X+2S4m$b@5K74zy@2ONx&h#sB3@vkwH?iDc-m9<7kN`4B{CW$BBGwJ;AjLbT z%FW1I>9=9+8Y#JDUz=*U59?mp&MC0ctRUs)uQJnoka`5U!AVozJ~ZhoLyEDH|2n`1voBBG*E?^lraqO@5H#Vgm@%WB(Yrq^plplKB5 zcaU~&5P@V75)&i?2FU6`0+2KgwI5i9I)GJyi~-iwP&ScnTHQipH-X#^3$MZEHcG+^ zxtyUS4#8o9)GmazOJL~>oF0fMYhYypQSoj)>l6dS#zlMdj)GGQ8!XKLX;_%h#9rmY zZn*4)r4a+L8-N70%QL7SVQnfU&j&{@3&(OL97S?q2 zGakF?>1X6T#qdACRVuL}s=(KxYtpVyxu&XS*9>@;tn2%>f=)*4zZ@uglCuA7arxF=zrw|>(z}~ z)9UL_`?1_e>#NYY_SbM9M>8}rq(Et0X$IJ~%H0oDLwNiLsL)#CgBWxSXc9~xj7G8q zi3yVh$1$7(WI)p>#Qvm3LGe&YlrSeaCI!@FE%AO6jopMbjSdpGQ4(IDaXb`&6mdvQ zxDs6D40;*`=>^3vsO=6f0||}?u6yu9dj2j^puP>jGG_wZRwx6DgHZfVg!vCHYg{75 Xe9%}5yi6i`oDxY7vLCRxA1)0585Z8r diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410098 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410098 deleted file mode 100644 index a05bdbacfc141b2dd8172dfcb8c94dbbe8343527..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7080 zcmd5=2{cvP8$Z`9nUXP-;nr1#jCrPR;TgI_-AYL>x=CebZMA-Fmp@1wKz$NxX;XV1#`r%c2mlCz|6h+?c&8gN~ zak98i=cWE(|7z9K{Ql2}cT0w-H9Oj?MxAx$&{?Khw)&KTyVv8wPiOQRmOxD|8TZzW zYD7q||F3ej)UDHgJT@xHj*b^I4a;&gw07!?8%JxT+`4&Y7?ZZbnOwld{5)P(9?Oz20b~8R^*!W>`L}_sb1Y*QPiB5?~c^e z@^Qqf?NDHp5Q`y%vmdSG`{NamMLMulTby&akHj7C#3Kk|Y*QbfI{EB$0* zt5XbJTJp+O+s?;cuWumY;aY;N1=q3|5HV*(^=eu$q()m69AzN1RaDr=wKNxIS}G+y zeQe?$b`>eFle1HV+wzajUzD!%DOaOvYsYo}i0seTnO`32 ztEGB3yDZxfP0>+}`P7dvMuxYq*&I4c?*1v~a3yL=gKx8&Fw?B9aeGIOVpMwKy90Z* z=Gs>#kgK%o7jKACw0z0y__AOW6M{??E5OGC9uCOQSZ)7`gSkcMZjhB?;q!VcMyg#| zi>*H0;I`E9f!had5Ypi^1R>=k9DnKob~J8K4mJ=^FVJy4vo)2_DYbpxUQ;8jzHgqp ztgo_hc;8No+M;0V9~UH10S}ZzWvA4K@x3vwW4^O9R@uNGvW--@T*dfqLWjY*ixo*C z?Z3C2cWVOkajc+QnXLf?-gZUi~hnn~1)4?&4q0HVgnh zDWBjcI6Dco59HX&_&HZ4B#&JAEzxVR!Rt;Zb)N&xXU9i==Z9Kn<3FSf9yn~h5RsJk z{B=vC#Mu+72jd3R`!9ET+k{KBZRRCgK)X<1E;Zu5C$+q~$yjBpqZ>Z_-=N zERlA*!*eOL21z;*3i4?w;3xcng8{yD$7s|~faZhm)?D)o?8JnJ&-eOxJ*d}xIMi>q zNv!u+_SPKLs$YVSDc~~b{J@soVuliO!vQyx9im)s3BEe_@RRM80e_kIQb}p46tBFh zQw7SF=cS8nLd&n->fN-_Kk>r8$DX3uhtuoLa~GRM3V``gTcmwcy1(=v@p#G`ZBbxp zS9Q9pZ?>)7BmN2j`$pB(Ir)h2N-83tkOTM=z=LB3CJ1mV5VH_dNFq7sWcs?u4u-OVXnP)txdN-t%ZK7|V-XTh}2~;9lD2I%gO!0*Ed-Eny*{C zv?rx_zsPn@u{~a)Lzb218rD}tOrPB>FDRS4N6JF)@k1%;R~`Fl)w2LF)PLTTV~3p9 zemS2e&1=f@!H;bCNMUo-!PYrW^O7EP+m`zSKCpMJuS5`qV}KsxKadNan1T4XAmmxM zYIKy3{JJnyT5fB;n5;;ckUxz&$3$PrSgXY*x+$jQ*)lc2b=&N%xCP{90|&{@9A!lw zSj}-Y5WKoEDo6aH8?h>{`K@l&A3E1Q&VtTjh@oegRX(BQ!-%E6>VG-8eB#z{fyDB<>>! zg$8W{57AsQD&d|F$N0T{y$!Urb@YB1{fhIUJsW!)huyk0b~JNhYJ`nF3*pE;N5v#a z7*?uMWb&>6dJvNobK56bcPG7Fp1;gS=4I2f*1(cRjT+gDY1+3+Mv}#g5YdC5+8L^^ zKARn#MKKdsTj&`=33r5eQNIYjjHOk?m|j0~?S%HxrQ}PRcTc*fevD{(&K<0Ive@(t zy3WPJp?`2&Arw{`DNPHzLkY4-3kq@Z@JWf=tp27<`02gj*v}h|^z_eDPKE+<)!ShQ z8ws5ktXXf1XPfGJycrpt*YvlXGjq7XbGlYp`RWRp^j9^8qK#{QI{veFZ}2LAA;FNV z)h`-``EPbwnJ!or`i5d)uJ>y{Tx&d$q~3m=#*N48x_V6yMP_BKxEt0SYMv(*5&vh0 zU*Yi*R3nJ(7to+??41N&2QeD@ffMY@&_=Ye5l0D1hPgbL4xWp;KnSi+qTx!w01HWc zM*U8_LEW%n`oY?_!@iMV?Sau#pVJ5eFqsw2FeZo{_t*D^?OXf>Yny)~hNw?K4DmV_ z4WDer(`>W*!VfN3pV?NcbxTKCKFKthv$#Rs(VmjB+$8{uFW1s=HH5bI}}wolN{GQdI-aM;C0@Uejh;wR%8rm9hApD%vbKJp(^z@G=6 z2^c2~jLX5P=fB0J=I7rsjrkFdS8DMgZjrCGES$o46%-14DQrE;Z>KXqti6dn ziS7RnEHbPHOoAVY&)&E{-^#(cTEMJ($Gf=^Vo#R%QWN5-vHEd8UmWHy#^l*p$A8~Z zro6ZPUH1|9nkjIO@iTk#qjev<^G~|IWksyAG`*b{P_A0{x=w3{}K7m#y=*6_j0EW_MZv<&Ev-OpdE|D uSV)57Yq^H%|Jr{8_Aqrknqf?nuKn)>+nD`gnwa3f!ha)%lh%KDax=+z20ah!{e#Ob7chlbEX>OAq`@YGRTT$63<8@ql zS9+cpV<5<;q(vv9AT}~Eg6OTW0-JW%YWQ6}VCBB_;noQ!`NNw--DQphRPSqIxn$D~ zRN^r2us8eWH*Y#lao*`P-?Uf0+xo53vDDTh42jDOr=>Y6Rj3}hbyHV)fkyYsj~Uzl zUWolv@Z!;=>o2#ih^yc7Q-mWv(S5>Y_0#_sznKuC8t7KUb)-o?E?G?M)5LwIVPRKS zI=Wg%+zS8A{wccXv63FYeDbtu{}}cw>&0{>%{_W^0y~3fi!twmZJU>;fLzRc{H(tp z4~PW;r$C}93_jitAbP_6n>&_xD9^Zk#V)5hpTAT7&tHzef)}fgUOH*|L{lPvI?rtW zhg-Yl)fbxlT&I63;n|k5_LYCOl>a|xp*z3eW|Db$kA{g(@6S&RDgXYa2E{aR$)28* z*rMz(`&#kM4E}PLhu2e|ty1b*IQ30aiIGU)$;qmxjIU~icPV*1W{-Zn4dNyS210QK zRL26uOd$3D4*xc@D^8Ru&ps@uJ=vS(*o4VT%9rfwKJdv}s$mmc>)XXepGSm1Zj{xn+l>pfDvpi zFm99G5|4ffW6p~5ot?F!u!Zr?`K&pb#c~f`Y!Zl86q@p50$1;k4A;n~su{k*daIJ$ zmK^u|zVLBI%C}2u$+Naz;RTun_JiSTN9GwD6wgd!QO$m(@-2mv+gYj8M=8%U@8n6- z3Brazbtw!2Vy77xG<-q!12G&VEjrl*IoyvRiiL(XDE)hqdpIH?MnO@yaNJ4`eLJY=UlP zY+C^I`c?*}?;tP3{0q_w%Bv+nisg=!w9VIrS0YNHvzVq;hc5Jj63W#ZI4ZUX0>$9@fiY7&fi~_aRX=`lVgad zOAwF&14M+SL{ZHxm~uvFoH)3MA6-}VMz$lID@39;-e!T)%SN;Lg+l8NNth=n{cXMN z_G#ju%-z4Xj#~!&PFDzr;dQtPsG6j}pB9VbtZQk?vi)Tair~C&3BqJDs++R>RSXu_v z&tO2X%x7Q_(^%xP3+N(Hxo-y41gb~j0Lh(5Ot>_z_ypSzOlzS~l_=?-NH>Mi*i9g} z!@>)m{s)QMC#_}i|LDYm&}Rzn4Ga+)x}{~cc2H?%YeoDS01!C z#ENWO`vMx@*wW}fs2nWJ;c1kJ_8J5G`ezrQZ8jgEIjm5#z!Xx#L}J2Kkd;P>bW;wE z-Gnub4idMaq)`&x1Zp2sBMz~qQHF5UznvRyM{o!J&XkrB2=KzcFS tHiO1(NNoaQ+@!U#2^Q}dVS*%p#3VVqP}(*`_urBH0k;vzz~&H`JOHI2M)d#y diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410100 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410100 deleted file mode 100644 index f4be81c033177eec4bdaad32f43a1882c05a96a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4844 zcmZQzfPgesonKyR+YG(3_&UB=iJUsRc-mTLm+NoY4#t^od(N2$R3-e~)O^xov-z)c zn)WuxS6y~mrPmwHWOzLFe$|#`pZ|GUPukY{=bG51k1Kuz3m0+=Iah2-dT6!1=6YQ3 zy6|`N{wxC7l(gtXJj6x@Mi9L4_bmwQaU2WOW`cUQK%tAf?`#d_+l-t}oc*L)_^gX$M zzPx59$K~HI+h^%=Zd)_&>PIe7nNEA|Ys+J}XF7>@h%MA#vZTLX@@?6LWvStdS4nm2 zZS!<3o&NUAf=PR}-Z8P$+0t=XtcZwC-f$gJr+v-uxx?Uq+xX!3KN{;7m#Tguv3{@GIg|D1*H{DPZF z=HWdWCOW-8KQW~I`iwErpZYS*g=U zDbF+SXRNKe zEWXm-t-td1CP#6&ohq?-U7jIeVZ2Uj=HhFg!HQ-~Yk=xtgu2yXcHo?+4t1Ty67w@> zcz;_txq?aL5^L4&^%d58R%=e5u%g5#_2U&ux0?KJJD#Ka7L~MAp1r8Pk2}1`Tv;sD z{UAJCY~qyzA1pj(tTWG@N&TJn3FfD*Pfg3&&g2O!y!%nbt&@S@$pM%)jDhwehY2#9 zAz>2#t1Ifo-P`3hIM@{Qdi%~V;*XA-$+>dL+*twIIh#RlXZrP_0Yn1-=*L0W1ROAg}7Z&{Byn? z+x10szWs;hKR&a&Ie}(@aLmK{jTD_(+ zFmUf+VAg75U@i><*$;9A7QhS@2>EOv2`al`)yw)q@xy^BF^fU7SH24%vAQ zTe4aw+0BnS{cS{*|ehnrW^6 zv&m>mQXa6S&TlMxah3=X|foX^%r9(jDkVB+~nXYOlXoS||=i7$lLecQCd zAJf9^RVJ=KpsjK{VP(w6TkBRmGMY5?wON#x@ifopZVf31MEQW`f#beayoFKyy+r({ z8G3wQ@3M65_shJJnwgYA=SHA%w2X18>gdmxV0MB+m*B5Dx^$$m>0my+H)f2(|bwg=GLfX z4V!mw;FdFe_M&?vsSq#<5^it1#yT2 z7# z!Drst=KB*59Xn?9;F(j?LH8wHPYO2t`lPud*Iy4B$Dp6n&Hf`3PN+|m^mc=lktxvf9%dIKQW*~mZ+M;|qV3DTzW!MbwEYWe zry}P;c*X>Z!|a2549LJ0-eCKIX(0-#7$toY=_U>uy9wlWSa`wH=OA$#CE*3?!%!m* zv6kz5@@u)5Z@C;Y`wkm}$enFAb&uNVj}}1h`w5sc{3^CyJs%@>_;z5h0Nane{p$^9^3xa8=d6E|D?+R zl{ge6F8FruXZzD6rZeX1iL8H{V)Nx1E?f;pX>~iMgXZ-^w zK>YxuPJu*I7<{}PK=d|G*V5^4zbu%vXX_mkJDn{J2mk5JRAW8)`OD>~R>r7H(|Km| zKit|aufEXa=Q{mU3D35awXgiMrTqUn3*GqzH_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ9q4G0in18 zDq;p=CXiyo*N)6HHYlE%#-f`2O66M$C%3awr;k#eXWq$^rW1q>fl5*s1jJ4=FlhJ# z+2D8s=>vhJMJKy~6iAG-pt!)w*x1Ct6eNLA2UefrANZ81JdW>JwWrdKdsjVE<_8?U z?`IKWwNGNo=Y7q-K=n)kp+NyYu3)`jBK_30L%_m# zoz~37*FJ+3v3`9B)4&LIt3!TK_wn*8$DZpNJKg&m(!$ocUoE*`mCr1y#D0O9M#-@U z0S`{?cI~M}D&E4V{$3*f(+oYnuXkCx_WNaLOfoc? ztdy8iJB!_ZI>W|Ad-RS%Z2{^7y8-CmQjya!C*lKUMy^($T=<&n(tP&mtm(bWv zAh*N9Yp}VElJEkRfz*gYaF`&~hv4Wto^2mId+WhJvue!5D%rQ~pL8kB!}F9?W*fbdb1>lJElg3k4uW91;_*1YJF>oIy{cAic|no;{(}VaC;d^0EtPmo8TG%RgQh6 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410102 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410102 deleted file mode 100644 index c56cfdd6ad3eebfc6643b7ee7ef46e83214fda69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5768 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410103 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410103 deleted file mode 100644 index fe975c297124754068505a57ebe9276723b532b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5580 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 QKmwkUIK-Ncq4CK8019JomH+?% diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410105 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410105 deleted file mode 100644 index 8362ded3c74037907fbdd4e6fb742064ecab5181..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4984 zcmZQzfPk$JbC0XqWG(s_uRN#gsmI*=W-UGXVRN!X4)Pg2j^lg`R3+S=YtpH1T6<^N zw^d&*vgoch^YUkVrvBnxyq8GIBa0VIUp}iCw7g8zdFrBg)~cqrrg!Pw?5U^4mvP$Z zt+;DcunJ^T(xMYp5E~g7LG;z4%$rF8-#v5LXFqyjDrENl|BK6W^w{>V-smJR{wG}q zsKkNUVb%@pIa}K&)=aA^o8~gVJ2R+H#j@huw=CC=DHezQD$j*HX|du+wZ7m#catM) z*W_=lU6W5utKZ$4)1H3iF;_?GV;@;v4HMHT-c3x254TtIoO1r~CQfTx>xGL;d>Yp+ zR=E6%Gr`i~9@94Y#Rum7xXK{fV#)hp+vepdAQv+qKkFX@ zau*bw0*R(D_;@>j=xv^^rPJSjSuknO);lJ4I$IhJ{?nPM#(MJem&;MDj8T`S^UUUd zxV2keeWA(Eb^50go^2^>U-@TC`Tuhky7LQeCYgu#Xqf2q{`|y{^6zhIP)q}t?CCj) zEy@nFuNB|S;4gQ1cs=#mDy6Q4Q{Oa|7>NX)oUD4v_^MWTmy*Y0_UOmkppIf76jwlX zEI`ZzQlBu1|J4=s;_mHo8ysv3dcA$;7x72O&E#CUWbUj0?VQaFwww&WFfaisWd^B- z0+4=i91_$IOgkX8!LH6=twgA2u!&a=e6aACvCceqCiQpPCzzkMJ~b_8JCi4{@a{(y zw@#2j;@4Y16tQk#2Pv+%+SGct*oP~1LGVh^4eR6U_T7DS>-9wMf6GpKvUNOK0WuHj zPO!N!^GVQ0I6Q%d(%9|bcm=r~9Dl$(l(cAX=IyYFtXdm(xGU6eTG5|-_*?d__=>42 zFY0ZiU-+)`^jEEUx7N5ipp3b$Dyc6>=HuevBi#k&S51zTYVTwNnguq~=FX8P-2ZrY z7@c}FZ@xm>qIZ>w`C&7TM=!8AU328FVGaYolLG^{A}Fs<2H6k9*Z@czEGJNVFsD$Rc*^cgxU}o9Qu@@duY8@~ zmv=ZM%h$mPTZsH1xz^;*eyVR*X33o)DTqz{Rx<0`4 z>iGyywzsFE{9^nrxp%B7NDph(WZI;`-uo7*dG>1CA^O?BQbB@RY=U%@pQxi#OWJ&nGBBtMFbMl) zFffJ{0rjASAF%9%g(*itaeumlP~#o%;`f8bN5 z@;JU@)t*W_?p^gvnICZYzMn;i)jo+SpZ7KU0u?d^ga!rpxPr7m05RdhFk@N+R5>Hm ztqyZ$UO&3%&H{$5c5!iW%qQlCPA%VQV^Wux_$PLoQc`}}4VOtNi=R9$vS8>ased-X zxcTXx1<_N4Zy&oVxqY?O6fU57Ea7sAMPVH3vwN9W-tCk3src8y?w{>`yluwYwbyu~ z9QH%)O!*H4KsGE)7=he>P&rW8G6TzP3n-s~h;U|LU;m^68rGmB!V1$1qG6VxGT|z~ zaS7+a>I$g+z_eQem16|er!aLyx=Dn_ZUVU-7G8tRZIpx;D8Eo64#8o9)OG_$U$~6* z#g$0~e@rKB@eDU@-zoU5;~OV7RDeq}K6bR0Liwe+3SnDHbd@=w4 D6+gw9 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410106 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410106 deleted file mode 100644 index 4006a603f1e97908f69b71b22a9bafd090578a83..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5080 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410108 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410108 deleted file mode 100644 index 16f2f5f6da07622db56ca83939c1d46fa8d10770..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2968 zcmZQzfPmwnT-Mi)+N~?io&59%i|F4oWp381oFDt%4x0HV%j~EvP?fN)Se1VK&C^rl z?2qIa&ra7d&Nx!TXZTd+#yRhePs-%~oL;BfBlJA5e!?el7tZioQaZ)AudZ1barE6h zmfv35XN^HNB`rG939*rZ5kzl|71*@9R>SY=0W0^V54TP@$sgVv>MnC6pn6{u%O#s` zpc04B2(F1IWtG4620uIM<>>RIF6qaXx3g2XU75$n{!A>mk4LYkaFa4yxa*9vr>5N& zb8`A#8oHeOvrl-1S>?$&%q)gNt0wzs2L&c?cZ?DzdF*H3rcZ8{P-oHZM;BxtRI*+1dNf zfLIW43M87s;N$H8qPKavmQH{BWx=F9Tkn|I>1=5@_)ll18tci=UoJ^@S!s*Xf^1c($diedV7m<^RuF=*};=nPeW`qhX@c`|}e+%D=y0QcS|5^U9zMD1YE9nubBmW3UN3XWm;9NOWE6Y1JcLxOzzFMBkVc0!QX5xwP{U0|hm?bBf znD;PqUkcA^+jqBuRbPJI333C-9}NA9HNrRIf^s(0*h~Jak~*K+`6(g9Aw-DZn}3h| z)`#byez1b62k8LMVeJi(_mh9tbMbF{eCW)*jo~qS>XZ8G z<2Ub{dZI^5@QXe3uQQKkbU!-$?81>Uwcn_R<6E{aW%r0+_+Jg5nGUVDSWUKMa7vj2RlIU@;>6&meW! z2$T*$VF)u5W*)Kth8$24IF8_oL3V=xBu#+ z1QcF_&25x~7kZqK8;9U9K`Yl`@e7h8Hh&SHw_phbk;jnJ93}BYyD%X??SR~b>_21< zia0b_(EWfM7s&N5!||ek>s+@Y&f1IF6-q3ax=z_^J@2k=d++DFCLf=)!1NK=Jj`+g z7WyC>WEV;~La;6YYLGpf3eWo>Ly!R$522)SqRii?bO2ViAe#+hV=*7(2be$L`I`pz zP~r~^XCnz9G2y-i`3D)`N&_IhAUTvgL!#d?>_-wnV#1w6yxWlS3^8tk)=%i|6tKle d4nbm)>?TmUg{Kjs+dN2m;5Gso*c<|r2LP<3&qV+L diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410109 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410109 deleted file mode 100644 index 20e756d66d12a96eb7c3a23ec5503d5c58375003..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5084 zcmd5=3p7<(7(N$Kl55;libN@Sl*CnWb%jJ}J;f{sdUSBPjQ4qrSU-^!tC*_3A?oanZCPnbB$Y{ zWZ0vFep4B=yAQJ(e)Z3`p4dPOROoWuGd0ep_prc^qU5faN7vMtl2pq#wq+MH)2O`X zaXmd&i|XpK?mjfV8Tw*Oa+$(-DH+Y&g|)stlGAwMn|3x*oF(mZ`gN0594|3yY!T~7 z_T00LlUlcU?MB`hL?MbX;d#O3Q(h3U_?`P{C9aSXjxxygLRPJ0!nwf8f_0>|BlA!Z zzfRvoJD+`{Pdjz?n9_G051pea&Xq@G(x?LdL6tdK`XBS=mwCL(4~{w0lONJwu}CK) z;IXHnQ=EprcKpY7#H+8@+s>6Ot$Z(iL)1)WTJ?^{o2en@f*S8vxoWXlhu=o*)SI}b zl&fC0uu9V@R&7P2luKiQ5EFt7mn$GA0e)gozPZJ?HSvVp`mL+|I}=mnI$E55nDw|< z!kSlU`U=S_ut!po*&qf+KosT-%^TbY2gv6sAw}@V!nL)8N`Wnn+IzAsXnT2;K}C1F ztuw8SbH*cAyDT-7qe|?7`azB-ols}SW<(~$%Zu-Q8D!^ni2w9*x6TaNr4>uGw0)+B zDNEKx?G!ILKaW2DnGE8SHkS3`_~wZZWINMJI2k-kX`lo34F&9;rHH%oJ?Lj4OX zvQGup1e?iW<-AU{fa?##Yp-825kHSyJYF>KHf9CvLoon<_dW2}vC%7P*dM5Mv{PA3 zE$-q{!`-|E=lhQp>@dIR8bqz9PhY5+ahda!`#wtW(URWUez<7C7T*BfCYN;e_fkM3 zihn`Nc=e>*apRDnAf#5iEM(S&Ch>&KX-RI*I}-cY`Q9Xd@w~x#$~rS!-CY(IA@r?I zyX-CK2D&~@7JeZN26#r6LlRU&X|?Wl+b)DIRqWe6_0KH_+C3bY4)WBM)FKtWpaRO{ zu^1K$5H{kKfxqaWp!h&6bedZHl#a#POpJ+H>M>zo{W@kFyMdwRaxSQvZLso&!Nz8< z_}Fagyl3GYv3Ns&9`bkdblsG~P5NS8l?@zeh!>4TfCeYp&A$I+rlORYWCu&#=z*$h zY~*u!w`nQmt*dVb13B=nt}R6n)kA>B{Y1flI?hFjPy7i`#&M;PJ665<3R_XxgHKbE z3zrRE#gsQ$G?SrMZxdG=f340?2S^p0zgDn>(jwrX*jk{XNV%1~kFf&BuZQo9oDEtw z_b;W~4I6(d@r`w8jxk=g|AWvviWT}e5m5DboI$BkLG_Hp87PZ*@ex% z)$X?3+W(Ph=}Vf0spBzgt{^%+yX#c{wn56>Ey!!PxHkbM$^L=tdB#YV`9kaLJ%54SMf`6hF+NJ57dTo2?g zfZrgDgBVbT6dw`vPh-7x-!lUJNH=TI<3u?X-i+eS(gKd*g(c7Q4Z1QTvN~PPy>iZG z&Bc5h_)fy}4j*_88{-TS&P!zKO+Ea~9ppBNK_YSO!oE(dU=6{c5QW+YHFf}d_F3#i z!?d6d=)vzam>S_k0zx7Q5u#==&i`S8a(KLmy8lkFeNDUu`cE$Ik;Ds)$$&nA#}Yn6 zb%_u!;{Gx0T?|j*r#l%L?`Qo<1iPTFi5eQcn2>tH7hod$_pS1T#Qe^u6~Eb3Am$p_ z&23wCGNY*Vv_phf+>dgnZFBZ1ti!_W)ei=qM=(zS%RcNG{$3FMRD?Uz>+kPBv$xdte=A#bR^V};w>7cu*^}$1Y?EAi|8ZY3AWGr$OtjP zJNUQA;lMtErt*0}Kt^G8OyEANbhB&;0i$FLmn)fa9_iDI%{SFDJ&FpU-vcOx*9<;G zCLVKmMxaBK?;EkMHa2#y;M<6qtQ=#07C#a7D~&+^khL=kcD}}sL17^=NF)(P=o5L6 l&y0cpU&3<;?+d7|Z~Z-s0vY-|!DBKicG2(Xq3sf}`49dG=CuF- diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410110 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410110 deleted file mode 100644 index e9ded78c9b1d6706db1aaa168a5b015bedccb3c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2740 zcmZQzfB>nJG83wvY8m@PJ$(N(^M;*3qwB?I8?&vmKU{ps5a9I{%yW^oDE$f?fuaAZezkB$h#Z9~U zlcB#!aSzC*q(vvDKx|}S1ko#0&Pwc=Tgv9EIV(DP%@XUwtMrXBEktd5^k;89o%_KU zsKnvYFP~XFs@l5*+O4NPQB&>6|3C3+piw)^Sy`vGOQy`=5SC6pccAiT$|>FX5{>My z*LU6aeU-~OZI*<4`|QNqTKsPe_J--l_T9dDc7ulrbLYuZPFIw@wEnX%*WIw7-}>s2 z`yV~>vl5@Tp69dm*xMTY)Wzee!Q>+cmDu|=tl07{q zu|?To_O;@h8T{og53i>_Tcy;saO#_;5+jkolap0X8DG^3?^5!3%pU!C8^lcv420qe zsE!GW8Tu7#gm1(Jn>9)2|d6+r|!SADX~$-6aF=9Nd>77c6A1+Cq^xU`1KaBS|Gs)b{{Z}Ea%|7q;YoA?tWcq!{b&B z4{HCNa~Ck)Xa8gYcW_%d@BgEp`u5lM_^f$(i%+sZSNL?)w9<=Z68d{Ystk=5G^m68 z0}2lYoyo^upXuH)`PI_;kPZeu+3jo2%=)TSu>W%7js2VKnAbwX!UkkFNIejM!y-93 z*uyixBrGgc+aNME$2GDv-NP>sGb}dUuh9CCJoE6$Raa~Bwx3(PyzqLNOTOgKq$H!* zyX}EyAoE~hLC`Hg#n!Vg-7kk>Fs_R@<8;E^cXCl)D0p4l zfIKn@G25I8)Uf$0VmR$xFdodH!z zZT`RjO>3a|fSC){kHkS@f~7zNIR4-~SXzbJ4=i^&p>m9%{0vh^l$(~UJwRhO!SW&~ zyat=wCJCttfx-)3-VhO&@VrGtnFPsO8yD@-I|_Cm zvU_1+0v4mTy~>B(7#OhI%b+uB-<5f115DR`uKYgv&AvHV-n&0-unL@d`Si(vqNEgG zuuDnIm&BAaOus%f0K*!dm*Fu6WFQ9&R2&vQU@lSSzg`EbV~8~$Ykr}DJ(TzZBUq3G lkeKk0#8rL~mG-G!uYg^JJq^Ob1ndWD+Y7GGu-gj{C;OJe}t;=RN1V=Y4_#xn^k0vwqo>ty7ySMRWkAc?rvh{NDAp>eUHm-et4eahPmOM;M zsJD|f8|tbyxA%QLpy9qbT0JE{@j-z9SzqeApYzvU=-NgtvsO~bO0%{R3W7bkXV#^% zJwiCRXShk`F+)}CS}P1KvicbD^%5?0XA8M%hXZ91yv;wx0u__0i2?5%-}NEAzjtS4 z+-?l0in>{Fnc?X$R!CaM-1?j5T4jNu@0127pL&}fQ}dzw)liO%mL0@}yr9Wj5Sugj z#g6jC;LM)4slA@bCJD8owAR&8^YFqAdi` zx@9rt#Jig(ck=LgS3^>fYsGpCZe8+#5%bjEE8c7aOYGJF&w4;M)+TUYU`Y&0el>DF zv4qj7YfQ*LR5?jVRpzT79%*rn!nrmli=LI}W5oVz#Rc8*>lzK)2J`ntUzy0KO*QGS z%R1XHz(9t4?u+bZ@Y zQC6oRNTdkjQ9At6M3uGq*&5KVWg@l+>o(P%Tw<2h@TFykC z_Nj;%!%a8F+Z!+L$tT7=S19E+XVlJ7{;XwOgoCNddWm3ynGk06{)PrrPk<>U}e zKr59+ceEAHvn`+o#JyKCW4vjOTj3;Xrhb5rAJ{{vBqN`^lCiJ-4Ticmi+KX�!iOm_z7SH7^M2>`bb}NN zjbG^Z#C#dqS6mY^D$^}*Ff3nM3!xZH%5rpysWMWp&wSV~UrF8K{42#-Y_&AquWKbK zpz?jcMDb6BvVw~7)R5iyAB%!6I4-$No)lSwf0Y>)kU+@shw}}IpNBnp08iT@UUR@w zv8cP%(j&v6s>e>BVpt|~YPgJU3*nBO2VrAC_%VI3XIlKgyKoE4@s_5)lFuu*`fuMk z(Nqmxj}c-u|47fO&gByd;c)myS0X>NsFv3 z9@s=H9!_v(+OeIA6itB!WX@d!hLb2Du}{b_Pdwl*H$?TIx!gw=cP3py?>zW`!WcP? zi$pGwvy9{gXM|xz{R7UCOAf5#;5Qc2n;WL+*IyYEj2(;Dw}$OY;sw83^T8n!6Rvs= zp-)p>Vo0(lrnAazvEDsrT3o4?Y^Jxrr>lZN3LmMw45gR{yUehh!FqTdi#c3JAZ^zA z1Zc@tX?3DC3cR1&9`|6{Jli_^?m}g_W1lCfNW?$*ekakjX)&K0rj;XK8Pkk8`i)@w zl6b)!Oyi1cz$K#0pzjZ7^gj+i&QW+TmS4u0k2%Eav+C0|X$=sRn^~fM2SLP*7mEop zH{X7hs?0y-7aBdesL}M4a;=jmfA#PLtxayRbg^ni+mrm>{<4y)hzbNL#Pbw=Q!vap zFlMpWIC0ZK$Iu+!-$g2!W*Y#N@SCp(_F=UtVghpl23#U${gdYs@ej_WE2@db6dBG9 zlZnq)#x&!({6?^SNxa~?Z9X{s_*^2~*_a74jTSuU&V(%LD9sOrbp_lH)Ty@@CaDJ2 zqzS)<>om~*@9khuvp>`g6n7ae<4Vl^*7%2 zUV5&rwiHml3GSSDbQ`Ce0noJXJ_q~WUTs#NV;|zKwdefXldC@GfCs`1_&Lk}0E56T AcmMzZ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410112 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410112 deleted file mode 100644 index 18b155faf91f7619d8f337bc538079e64bcae1ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4884 zcmZQzfPjfR9>*6Qmx(P?d}HL*_Ulhh``Wu#i)S`T8-%EapSbuAs7m# zxVnJ%>a+DAo01ltm=Cd$fe}PsEy}!^6!6_Mmwoo57p6jH@BhEJJV%dh|LToS^5TEe zWq?W?KL6>Nf7ESBO=n8M&m_ZD7rKMYmv{W=`6xeQd;%dU4AZFRk4Dvt4!<>`Q(a-Bfze zn(y}9^Hbhws7L7=_FijhxX*sB`;W$v$2+P{NG!R_xHRrLgJ_Eb?}Kfdm#2VS%zXUp zuGl*u76hCEiKa04csqdTZJw^B)8BqsFlo=$J0^BITN)1j)0wHpdh+v^%TcY2QJ1Fk z%;tZ%wOd|&p~=s6`lk|}Z7FMC`DaV{|8o|)^9yb!nTPjinCSHW{KSy*?{8{QOaqtf z={bol$_}%y72nL@FL!x(J@wftrLKij-!zpNi3FaUta{4$s#bWHlE-8A=*Qb2Zem~{ z6jwlXEI`ZzQoqvEsCed!Dvi{zDKE6EUBnISfS#{Bb6^@f*RB$g zT_$kNsCCU%O$94S2i~81=ByLs+k9-&J&ucq?SSgUJ<49dlrw?d0!$CDVpxwEiAn!I z{i&(TmSxZ99SmPAGOhFUm$z+-OZjA@o%LC1rHssixygSyrY+gl?mzw2N|nCX5)S(O)mS&)634c)JZHpf2aE}$ajM*02c2RaNKer&o#Ues(D3SRj%2U5X)?n9bt^2xP# z{OaU*)_i9*;5q9XnDYPg#j2TJmrn z&rE%jlrOGF*uLC1o&IQ(PEN`8i6$V&gY<#*60{Gh?xpSJ@;3ptn&F&v$&5SfHEoYg zerC0F&G8umPtM<8mT?1U9+P8;r(Y0|0R!o$t|eE@;w$al`YT^=aukQ#sS=CVj;07p9yM>_1>w@}JZBy5m*$Ojl>~m2E-!*S!-my~BT(h)us65p~}1 z@tV-;-!lUmq%JZyzM3Q3bEa_KRLg_2qN4P@Q;iaYU93TA0h}HTUpq3-*r0f38jEW7 zE0u34oZQYzojyu=o_QxvnobZl1gcA65D+`fz@QNVath2LAUbK$$)iv)&Vu3sD`R65 z3sZ;!m^xy^k0~HDD8R=Rq8UVz8ZJ<`I@~G1{{ zy9t&SLE$yn+(t=wf$|GA;t(7rNNo{t^er{EQ+pkJ$4sDUrlzY`Z0BLK_q?n4z1L=| zM=y13JzD`*1kAfw(&#U!94w83%6c#$qMQ@cSmclgOoE`Y)dHxA7pfIZAtg*CCR_!s zGzzvK=)ajzl_+6Oq?>^GmZomPnnnkS+b9VyQ29iSICKET9eNsFWA$|Y$w;Rz?Y&Qr zy~>R{<#$m#{9MG{Kh_&qEN8`ag4)X<0B)%QXcP1lxZ=HCAuTW^ddjgs&Jwc}9$Qp6!K z;Yx6oGY%jXNNE(L7gSGz>K%9)NU%S!?mVEJX4i}uRIb-U7pllJX`oo%f`5(^i4w1Q*5)B z{+SKcPeeXOgfqc@2&_Ma6gLFii4xAlng4taa@-RzA8Y=ifjyM?10(v71dy0;pOTYT z9ALUh^gD+ANCHSqnCZC6Vn|v?$;U`(nHV=|t!#qDJBGbT0!U1f-Gq`SiS92W`2%hv Kkb%u1FnIvu(vTzo diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410113 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410113 deleted file mode 100644 index 13421a93a1f7021c1c097141559b6bdb12200ad1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4708 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 DLZM~f diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410116 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410116 deleted file mode 100644 index 36387fe2ee8e536942fb848128c15a6ece89731f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4696 zcmZQzfPlH|m)vAqFTXi1_W7`!=E2@yHj+Naryky~$?|Jsr)b$jpeo@nY);cmSFp=g zY?s;@K2dJQ4B1(2Cyat}9VV|WJ#Zj+^<1uG?{ph+SFWjdj7wq~8aHt72yS|E(M9l> zef^n@Z89L6k`|rV0S5kzl|71*@9R>SY=0W0^V54TP@$sgVv>MnC6pn6{u%O#s` zpc02QCu_qm@7v+t>V42IS8Yv}yvw$UhfF5O2Y>rLZThY~p4VlqDpJJSEVg+~EIRf1 z&f6CX^DMQm%AcLZGs$tUpfYoac|l)oRmh$Dam$UR=N4$UJmkpkKexqrOO}J%k~>bT z4m-vkT_&2o$l+m0`10$IBw3ucEK=BD!OX)q_x~v~MIHvx78l+J+cqyx0lAp@_}N`4 z4ImZ-oC1laF!*>ofaqhw{{^UOPW(sY8bAy7#QgMip+1_q4? zAR8QSAblW^wCLm=AO#ZREGRCpGB!3b2WbETm^v_>;ve{wsXUJFShc6pj(b-wNJtX0{ zo%Pw$g_~r;E-)KCGtk!A|J>`{)ZdGK%AM+Snbh86A`aXW97M|Y0p}9U`l~IxI zp^o7Oj<&WyIZ&8`!-JsN3=AN5Fv!mF(FkaGb+_@>L92;dehB5M-?et}ogmy-toQE! z+WvbW^MYNSK}x~q!e|oo5%wq0Q0rNz;C9f!?cneRxg8v?z&yXUb-rUYMBMYv~C(_eSZ$CDvdoA0> z%?CMvW`WK0)qas;D!+lncz)B@Ut3?OC8W*JoVnrU*#|=NS!b~AU&p}j zlc0VE1~!npp>e=4!OSAcP2c1(Yi7+mbF0g9Hu3IqIp&_V%u%+oQ{wCs7KV+B_UIi2 zSpf!&U^f8$>tR>@c3!#c_S-dc{5P(d*yzd{;pm=Hmz84UUwrQRe)q$?2gPDr`yaRD zyESvQ6|GPDEYcKWy02G0UMcuhdQoR5`+ap*3-3q8 z#>d)pw8{=Zt<-_g;Is&0g3}pP43>t_#X(`o3=AhHs5+w34AZX z;4ndI2f*qfQ2GKDWZ?8b?eYuka75l?fq4XMA0!RY!(LV-dy&%!z3qmj1De zO;=`^)oIcs#fNzGGolqwB7K{KAhDJ}=L7aecQ~)r|+LFa=73 z;{?b?HUpMcoM46$Q7oR}#K6Uy9#VmA^8i3tl7oOumBo)|cdkb4l&Xv IG%mpu0B?z+5dZ)H diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410117 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410117 deleted file mode 100644 index db5ec06fd7eaf71ea28faafc725a753250b323bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5836 zcmZQzfPmGLzfC^pb3|hHu5VkkJ(k_w#SwC;Lb1Xn=$TS0!}Z2RKvlwX*)O@twqAa7 zTMv!bKdEU`YkO5Z5cLe#cLfA-eXxgU&y zY8?!}oQrw-exchowI}g)vzujvTdReSvFd-n{wtv6eq$`l-6>)%pH@GN=aV|8@G?mL z+g+>KJj|Wio8juc-({Jq&gFm3t2oh=Oq|LM$BV?Fu#%jKw6#;8lvd1muJ z+}bU#zR=|7I{i}#&$g7cul%#6{Qo%%-T4JKlgz_=G)#1Qe|};}`S&+9D5il+_Vk>@ z7G;Oo*NSgu@Rz$hyq@}Ol~UKjsc)J}j6?!YPF6i-d{ryFOUdIgd-UUN5H~R}5Q;0H zIu;;i0;y*Up5FPZ_ULz$kl3otInUpD?qkvqYsh;vH>fPc@Y=K&>I+wJl|Rz8>u*+41gVofn+j9T2sRfO zw-yC2W$PwhvhOxCS$I;yI44m&ecr;;*KMMTI`2d?%=_TQmhz^v;p5I_&p#T@OV7N# zghx`-QhjyHiTwhve#Nfj2O0?WgW+pO<{29l&rD-c&3>iwErpZYS*g=UDbF+SW(gF4ioxj=|G=kA<#Bw+sy&r< z+`H(>k}GEMmG*A^m9IBBio@+x ziN)*k3;_${by_nQU;7MF#k6jz14K0gBh;-9d}?Q2=kEBL@U9{L^X9DP&ym^RBg`fS z7|+p^sm?9r&_CPd$kB9mji%q$Kw;HrEeCBzx>bW?R6(*x7)G{_YP_J?`id#%D}+AgMnGAje)r|3aA_vHlkDb4o&L6<=IZgiw>ZT2 z&$Ciq(OQ1r$jcq11_-EPr)!7@*f6y}RsYJ=OU<-a|Jh_TrK!s*di~Gqrx#~!aqWAh zuub?9SP_-%jC6_s8#gh{;r*0jC9*7P6ccxA0xJ3Phb1Vuv&C|jPZ#W-^+Q|rs z4+e)=MMkDWF2~oo_U(1I*6e!N)h#S$IH4o0(a+|CyNhv%&BhB=>l9xfzQQ@%QZ&y# znY;V6Q|Q7s$x^8jON>}-LE#0C`w3J~+`ex;}wm>;hIsm5;f@U)? zumjaH_-emMF_quIVm!a;>#waZ)DqHWXwKa5^6Uel`K&Y8_OAn(7wig74zRQbrb*Dp zK!jTutY@79Ifqtm2ZuMvd~mn|%e9zH*SqdBR5sMi@akIW(EaYW_3D7KrA3u9ME5xg zhPyA2Si1D7|Kr%Cc0K0m#H0&hEs}?uD??mE{#Bj2y7AjnP}s37TO05q#>GB!(|(qQ z|I1QEc@2~f>qYGsG&}V4RJ>%RCdflTD|H|=IBq~paJWOoU}1_b4stIuFz&$t50WG% z44Hm?kb$NbWOKPe`jG^Xm>?N2fQ1i?4@yT2F#CaRg7r`dMxYqH^d!zr?>^AjO(3_! z!fUX(jgs&}juT4a5F92*?K)80!otf1$OfkeYUgKgdfB*WkKR!*3)Gf@0a%#O!(LXH zr9c|yXHa<1+isfr8C-TE!kVsr#$z`<{mh0G&kS6mdvQxDs^rus8&j zH|YIpkQ^v|f#pE*B*rDWUC0TJGPjZFCZ0R8u=GVMH-XwA@Ngo!AB&_16uvNkEpQ_F zv!+_Uf2^=*K``5|!p~b)y{vk&W#z2C33Gg9g(D7ag6d8A4+KCq%m_vx_a9UaWCnh+i**O;xPLl217{nu!f~Iu>HXDVhdC;O8O_xO%M0b z*i9g}!@>(5uY<&Gl!O;(+=3c$2o4jZ_8K@UCx&qCn!P4g+Evc-fpx8_so^c*kj%R; zF4&%Aeb~~oU^XPa8Fip(6p|i*{bZyxii-`43sAiU2E>dREKY0x2pyLI`I{4}6-*%| xSR^J~1+KIUwjb#4nNXD|aY3A$+K$rLO<2?JAaNTd;RPC7qDCBIO}ns|2LPbaZOQ-u diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410118 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410118 deleted file mode 100644 index 5d5234d0a0fc97954f0a4669138c027d73fd3803..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4876 zcmZQzfPnqiZ_Df0UT`%LD_XU#)UD;a-G>YJ&Z{q)z;VUL+vMF}peo_jlfO+q=W|42 z_O5SRv^|#H-Ng}dsY0>BCFq$_E5r51MeYR?6&ud-ec7}-SD3Hg{7W(W*3y~ty&V`* zjeVAh#i@X7N?LScKg31`Mi9L29#fzWqTvS5J!O1VE4)j|<1u^m<82T(F)$E{ zE1)_SAZ7xo?_9e|M0T0LHKW!wS2Y!^BprBv?wPYrkZ<#`N%uG|9=2oPcX9xRfdfz} zSUr#i0dV|5m<$ZAT`GESFJ33CPpDd`$rZEsN_)5d%GaA5 z#o>0U#Nu^%hJc0fI<1+DuYCroVoCxz2@DvaZgt4&&EEc&`FuCKrEt|5UZbP)u5E73 zjr{K1w~^_7T@`0fQp(2vsdq|d-(!5Z?7@O3nl_*RmKZj;I?iGH9hYu&gc)cWI9$4> zy$&&-IQg#g3gLv13Aa6kxs;Y9FPbBm<8<}Gk~W{C3=HZ548ndH42)qVKs{*Ta^enD zi~|%dh9(B4NNT`pU)o+Se-mJ<8O~Xk%(%l|)ArcpXI4ws9G@ZZfS53mD3EG^s%M0_lfmJxu(*St@BUxmT~--xU0c60Yt5(>=$yY#{>jIDQ6bwI zfe#+($2Gr6`!V_Z-v#Hd%Dv(g<|}+~sN_L!aUJ`ozo75{he;ae;<_`GA}8N) z@Vlc}%9(Oezaa5U0N=yDj=NV@80`tXm@b-2asz_u^kCnT)ZFIuhnb1+#(;A_0k1%Gw6{eek3ukuWv-(0Nqb?c3HdlonR z6VR672b#t5d_%j!y*sy(HW`?1yi)ry{JGfZ7kNKbAt}7=dDLP&VN* z4B{pW8oLRW#z5gU*xW`*c!Bc`EUZB^Qp6!K;VRJ8!{QJeCTMK{kQ^o9g>D!1+(u#? z^4yVyr7v2!36u}u>5Aw&7fBC1hEU@WF3f#EGpEIFvEM>#FJb1zZ2czNf={!MUB(*&66sE3htlRcvB9IZ}d8i4JrA7O$KumtZfGBFTnsxeMN-5z_bI8TMU0A2_P|HR*?`UNaZ{kZbGpeiG#!>*-Z>G z<;$fmUx_(a|5str0^xVI$6ofTKG=4=qRk@n{tCSo0jP=C>XUy^IgGqYL|>1Aef`5d z(EeT|&>U8{%Yh6e47eCiLWQYuj3YC%}W4d)S-XcRciAdi=*OcKYL@JS?fkHefw?YF?Q3`4F zNskbjhg7DL%tdqY_CC(>uAcX~o_xR7?|1LsXYaMv{_nNcUVA{05sDC=(QRt)yGbvM zrrvw_$yejhkkSkOZn|eYQfJ=Qx!?-81as>e=gE=E$*RITQ`5cI1b_pJ~NRM68++_8B7XrB)g=C4@O2r2(YZ# z>D*vP`C#D29^cxbCZOIo+)+hSV9ULfp)*h9tD`tz(Q4v9MS1A~4i33Bhvq!Jkz-SF)9D~Lm+@MH zf;(YR9nUrs`O0vA&mr@*`OIH@DM$eE5Wk*!jDq{8j^W1jYRxO2zklQ9Tl>zBOy-bT zuu8tvS}-%N1Q7~e%hQ~FJ(&s-GZ&WLa?OOb5h{kQRK&Q{cEPraG8euS&s(0 zvWYz)N}7hN9?& z#VR$j7DuG?+t|0XWshJ&kcoT+_}IXY3Gyp-X*0)Nm3$P-ve2b-!Fwtvx31J-BPn+S z_o5=TNI@mQLPhw6OAuu77Enk12IU~xB^7mp0=98_czO_3R8-ZJVaH#5C_i=mhL=n} z|KTmzV<46DyvBffmXhCOqG3k7CmJ(wFUT11Gf_5~QjEx`Ec)kIT)WNp6tDNutk1tH zL|bvJDfZIn@^RwVP)V0Pd9HQs@lFIgv$5q-~1ZL`lKTOFH296N3;4zfwxfOg`zl=I zG*t8tCi2QJMqHh!Z`jHkDD$!)k7TG zX2zDLpn?-4F(v9D7J%^+MsOlpW`kFCqm(+i)3@GxKPPa`nJZUWZ`9{VS1?m*m=bjl zYd3TSCYdm$Jt!_)!QO1SG`B8FK_+8KR_Tq_pk60oPgbB0wTVmr#$wCpdM%TulHQ!u z3PlgwM-<+cV~3tqrzqdqP~nV_fcb=iwE;gfh5;5V;D^q6LV1v9qu-n(&+PJU-B?r@KYV4^ck^aKv4DARBtLw9$*b(0 zyJU1$&$@NFZGs`p-Rp@pY&S_688gWDb3FHM~UyR^F3gG^7nE(Ojed^pn9#C)23_cVtN zM{4x?hdTieBzRVVgs(Sv2EYl%95cv0FrH9iAbt>uQhS)MMrCkWSmJ}wwGrT13jbzI zKb1BK{csNWJf?G;sm$YVW}$1KtKwD~$pxcd_`ZPI@w@Z%$euCMhw5U$#*bv!ObyeRvFSeOs*}cq6vl|Ls-4+k;p^|{^-0d5zrdHf zKvh8Gjgs4*Di)bwn-PtK$YDUBj@wuee1647W5-HZ&|{%*$Jc=#&dbPIfa!B@Kk%bA zkb&|zal;DTKy+1{Weh)T#A6X1AH*qsXBxA17{ky>L%IhYEB`Bje94p3K5qjdASOZndI@PA5o48y@5gzW-LX9P>AI z)bCh@uE1i{Egx^2@Ld2fu9Uv4j=c@w>i`$056nr#=*akXNv58dqA@mS4AZ5`X~r~e zjQ%9preZJnzB7jC97k6jW%;K(^L>m$S(t<1*+~nRp75P0y?N98U+jbc|5cFYB5^sq z90$;txig@jRM+7*8U64MfX)qKVEFxtP6o#g%W?1{EN5%aKH8tV`->Up$vlT$i6M(R zEwiusSY{0dx-RlV&m_C|%D++=qUiws8gn)`SZP534P7+0!YeEWm*IyyesYuqs$V{FbKreo8L tY1|n7Nw7`DUjL81W;9n%!(K3E?hNQ(6tUy@zus#Ye!H8*UW2c_KLK-Q<;ws7 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410120 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410120 deleted file mode 100644 index 892ef45d70c653a22339ebbfd1a5b34e3c6003ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6060 zcmd5=3pAA57yrhayd^4K%XC#LR6`i1Chx&`MGr}2JiA0i4=KHbFd~miDbGt86r~51 zi6U-!{!58UqT*KcuQYVe`R1Eho_4+OQ^&bWC!XQr07`*AMe?I zy8=v31{KI%G0yo-J*>uMnOM~R&0BP4lrsZGB8%?IJk)ul(IMfJvCG&r0nW5O^L8D3 zMg#}j)=hp7*4uxe1c)Ag+N&$S_|GrB&FNYq*Upp}t17X4Q~-p$c8i=Tm43eTk>j!Y zWr1hb;u&w$%$e8c2KRUlup*wQObQ6vJ?(%nOR9EtLfpFZw>yv%0|csouE@>AQ!-bk zi*(Zbl**3PtYFogtUJ(A7~2*{+-J5?OvY9xb6=K$gZIq-;?f0#h`PrsqXw9&7nADU z&iC!3RJG*~H-x>pmm7vC?zfccx^O;^1{n+9YPh_y1!`=mgS|Aw#+nS<3ly`E)c&DU ziC4=W=`5LF8gXNE{?S>&wSz;=E(dTfO{tQ{WuBBBQk{KT=MzJ_&b_BJ^k6=#bnBNt z^ffbr9<0|}89PU3e%z;b2yJx4%h5GLV#b|xkNq>r$J;hP@RiwW`s6=eJq7rf)5)** zZdo+hp*Bgqj?g-HWenc(ndq8l7ucK-{w4R+R8bR=p%8V$ zdupyR`?}=UPEV`vx2+BZ5b%vFY7s;&3Fv6tpdAzt&mOkM?Uw@2IA?2iub6sodBoHi z?qv!pvU{XMZOHOV^kLO*JbN$*aE8>t36z@AGH*+cx%R=}mb&??dSG4PKv2OKsY{till)V=^*HZ8yOGPdq>$M_`=L zSjmPrZkVr6u-vP)=a&x!cPBTd#cV{JHaRsf`6VE7MOQkV?>&N zjI&4yV0|{SRjRkM^gDWXX}|s^btg~5rtKD1A9H$=6VyY=$-P$?XWvS?4>4on6X?|T z1zJU$TA=Sx|1EsAOfaojtv*FaeZ$LN|4S2}WP*>j!3RB2|6A#)PL?jS&7U+TwtdHB$>>Qp8Y;}{a38Uc4AGcCy8TQOhTYV>DrSg%% z%6(J-6tH)8w6`&XCT#JkYxCT8Ogbd4YZ%v)TqBO8ET~ z42|G0uH;2q76q#fZ8M!l_kK5eKJ{SuRfABK>S@+?YbnP_Ph|;R9)qHDB7Rr!z7Wu;QP`@NB)^{=2)_ErEw{t*`*qnY&>auB#g@5j{8QY_k z8@h!Mk~jkPI%9wr!sz(E9 zou+ZLxUZp0K0`Q7_ti#^mtA# z+PmWpZlvpqA@<$g16?aGd{sYa05vWSAMFM?YE&j*eVp*=hky0Jbn zxP}(8mk7n0-|fuxy$|dKmN`*ZJHi1m z;b;9Q2ThB>;5ar)T-&pPCmYk;Q3jHBmC0df|TX@pdpxUCuCD3qOUOy@{wj#~8GnNye&N z04Fy%9qcuY%@1Nk$6nxIeZv9qaRx(9Mia~la_9K#XXExG{(|Q<@80@PV~F~Mrb^%M-_X<~T+Ax!~A7U{EwHzuu>4WufeI9U=)AP+* z1OSOmGH0=R1m@@%;hBU-vxK1G1}h)({*C$3<$>W=ES%-TvA8@FlVKiXk5v z+-1!E?`I8eoU?}OXFh6TM6{@D2&;?NfH~m(kSjbqc~tJ}ujK$bKga>jeljLDd~!;d zU{2$%RV7!pc!WSOKPy7AF^7uI~G+HaGEP{veO|KRx zu`>ggfYn1nSf(g|XBMWTbvmZ=<`i!~j1Al`xVI>NYHoqyfj7X$7})aW*4&qO>b ze1i%v9(HhQCUky~$DFk{CNBJO>UW=DPLMnNL4R@gr=Nt|kNAtXUiy(3j+@8$4G$HW zc4o~2BQM^abs#ac+D7QPf5u;EOEl@3l=uby=K*EBy>^)WitBUkUii!;_diDi`s!9R0t0Eq|Qk?oTkMadY%1;Wj2mzmpTZ1OFQ_95+Wfk^c*p C`l8PO diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410121 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410121 deleted file mode 100644 index 48e472d68540518ec8ba5aad06587c753c1c0fae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5948 zcmZQzfPj;~AF-J?E=_OR6rZQG`~Izb7q4TNIrkfcYJJn%%`n3Qs7lyf&AZ!EqWi4q zVwU7h7tR*@G?sq6HRHg`pLaLyocoxuS9{%~+I1f<9Gw6Dkh+6~oGMqrx)<*&oeszB zDE4a2Iy@U>Q_`Xn=OH#SFoNi-MVU8~0=|3Zvd@0>!c@rY{r?x2=jgHRU%k;uUi?qG z3{Z&!%SMYu2}^eRADC~ji*=hBtLm+Ta|GFbFKQChE&O-J|MxdJzr5XZI2~B79h!0d zeg8h0-@0?{GnPIsyI{TMSlBC`8!zs#s2#eaD__}hZZANYDqF2Y;( zUgN&(wAa3`)cjCU!bo8V>%`nW@Hl^7EI=QLT(om!|W~ z=6|@gTV8#k$l#E|muZ)#9X1DEXS zIf*UG4zsTn-^}1IcX@a{_1P+=u7y+IG?f^M1fHC%ddm2!R(O|^$7A;B$J-!oVqhQ? zS3q^FK+FVEZ}{4gdBz6CGt*d9vtOxvOX1{pR_gRo%Ja-SdD3)(upv-M3WI>yX$A(3 zSRfl5ZygH9&PRLfz`{r~KOaY0r)M&V+}WcgKb8J08C7YS5JHlGmqjY)oxG zb8tcFtH(?p`4)T&!)NeRU+!I~HQ|IONBQSMu{Wt^>#~`F=7GcIkL9zYL4kcsB9~kI z=)AjRDm$N@wuaww-!Ew{@?Py14KQAMA&& zX|F@fCr-ZWyh1o3WWsGvVJ@X5$&2O)<~Ut_u%ylBC3zATbV5+!~q~m_qp=C1ABLZ7-L<39!`+=d4R+++nY2du;MEtEFp>&k%TW{{FHI zkpGw*LwqBHKsum+n0S>akZOP`XN0(u!69hV0`}$KPk)d+x*$d~d`fmwq^`@QmENL3 z??S8MW<~wXbg-I}sdqp5^pbWlMoCd+ozt%!=i2hlExHjfZS!P(PM~?W4 zmYuQ9R?Wwoo@zbt-!360_`38_@X6z||F17&0HrC>7I&a}kRQ>*IjXLh{(bmSFN zp{sw6T%E_a&BvcE@Sj|Dbn(2;1wOkYtHe@$#Uv7vMLj&GeeStgdoQ76%ORmH|3x3vraq37jP}dAQ&l0ZG$a*EZ=Hd}awTIv|%B{ExsD3gKqxc2L zhXJ@h1!^y5>sT0jH0d?UeP4B6C3e1?#skjr**xou$&=D;*4b8b#rs=K4~RB!k` zt2+&Kk@*7sZ?4VVyzb5K3I2_;OMvRcJ<49dlrw?d0`#}Zb>`@Wk8eg~1<9swckItK zaD6YmPPS`55foK0Q(<+2DWk<9DJgd@I@y}FokbhaLSXCNk7BB>`M+s@=&u^V_RcI;qN@G3t#s_8B=kz!{ zfjU0rKM(-fu&`qUa{oc)KyknfEd#(pM3kir?CT#)fR?8*Kuw@>7!F{T00oekaA|Oy z!+9XPK>$()f$G*%P&r1Rm?xA?O!*FSlMapD1adnpyat=wCnnPjA4DH*-co%r_3NH%q06Ch3~Gab0IoEHl12&EYp}cv zO;-@35hNB%P{NNm^PkUwrCWr_5Ed5mv8Ejw*h7gwFr1AffW(CR7FS+IPX{2qpnMLB zLwLR+(eD`cBMBfe$#5HqZqi!W1dDeJdyxc?m?XOiR35^^iRkthk{-B?Kn6C4_?xsT z3GBEhT9IolbD#BQe}>_#fJaI%C;jJGYV0rJ3~S7x=Sgro0ca$!UIeCn2!NG`ptc4W z5Nw+u@+MplOfQJW;$D#8m^;ZvbKpH)05Y)F9O}2UWbFDIL+%|-Fh~G&dIT>#f(_C21P|NPiFsm^U|TS`}Q0wTfYaI4$$ij zs4$pc_WfD~~^Ot=zII3NR9IfI@?L3&~BOn4bca6Dk$1Aez( wt3-i%fpr1n1Z0aqY%C5!@jGGj?|=W-e25hDQQCh*k5eGkPso12;(oX^07wCJ%K!iX diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410122 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410122 deleted file mode 100644 index 17c1509e02cc6da1833fb82660549b78794a1b7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4908 zcmZQzfPk0+*^gDSEAxs?%xeNBaLDX<7_7j}m1NAyYWM!*3f~%_D&dpAAF-J?E=_OR z6rZQG`~Izb7q4TNIrkfcYJJn%%`n5m>&F4kG^S&%%Ibfbj~1>@;z;3a7rC|Q`uDDb zKeJzaUjwo!Y0-%*5E~g7LG;#Gfla$>HTD5v0= zIa6=H=gpJdC!f3LHi!DuGEQH2lgBGJExPf+#(mGCoxiMnFSzdO8J<+d8<7m6Jz;NU zzwKZUxv26!V(ygu^DB4CZr&uNv4+8>vRl5H@y2|vTn5n=FWv{+HZM;BxtRI**j=xv^^rPJSjSuknO);lJ4I$IhJ{?nPM#(MJem&;MDj8T`S^UUUd zxV2keeWA(Eb^50go^2^>U-@TC`Tuhky7LQeCYgu#Xqf2q{`|y{^6zhIP)q}t?CCj) zEy@nFuNB|S;4gQ1cs=#mDy6Q4Q{Oa|7>NX)oUD4v_^MWTmy*Y0_UOmkAZ}t{AQV?X zbu2*41X8cexjAX6?vBz^z2W<;?ljaz<_q+{xi)w6x;MWk_&3TfVc>Ui0EU4lP$@Xh zKze`xna!{|_20fV;o&E;Wbemi_deUetg}#ik?^f6^QB`iFlswTgEYvVO$AXPzz8-M z7`Jy4N;MOz@6I>-8?)@+r^R<)XC1Zv5ZP_r(sL(SvaWhZba|wcxP1TjjR#M*wD|wA zZJOY5%;me?w}-nw&P{8u1i2sVhYefr-!YMzZ#n5>Y?6~S#yhC`94^>l3ny3$l*-CJ~V)6AYcTW z3-rU(Sf7iPCQGB6l!N~jJ~++KAhs>as%p;G!w;h-PgM42bKU)5aevu|Z1?_;ZgRTZ z25++bL>Z3W`SCevSN5(;$9RBdf&F0k+L3w22E{YeSX8rLseDV}sE0CMFPBm^v_>;ve{wsXUJF zShc6pj(b-l9-$6CTn$7&Aj%_!jOBc7Bb90v%ukU z>KkiVw`iAQvGmPC@t6M@y}j~>pr;ocz)|2?fE%*&IU%QBx@U6b+5j5=cAHbtN(qqtMmzov+7Zs z^EuJs{$qX4Pkt%Pf?P@Se>89U0E#P?%ZqaKgx|MeV_;wZKnI%7Vu6}ip=N<8m?c0C z5)-ZhoL=EPkli2vDLX-R-+8DUBdAV+vKfeT)BO`Pb`!|$u<#meZlff;KxHL0;t(7r zNbMJJ^!4(~Rt1O^wf`)S)nwt7IkLd|wLwo8Q~ru?9FNv5a^DY)V{pp>$i|gsP|_&D zx(sL#xGsgJE2tfC3X3Hu;YXbLDRW@y7H%+^(P~r~^XCnz9G2y<&m6y@e z0Z1<>pM&BMo^MF>JBIy80!U0U+(x3Cq%M`nK;s?5UL*k|CdqCBm51M2G5iTQnTC4sd%3m>qOk1+5zxb)fYIq>KjEmq_&n zE;g)ugtv}}X*(=VYdZ>UpF{zj%!$MO*t~)(4T9|lmPNauW})N@;@s3alg4hsS|$t< zw^0&apf)Bo;t;(Y0Y|0Ow=2qDPb^q2Ozx| z^$Cf7$FLts0EtP4+mOm(V%(&)vI!RN81^CwATddH6H0wT^f&~PKj1b38Q2^GlLr7R CB%zl8 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410123 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410123 deleted file mode 100644 index ccf93278b9e89a08a1ba72e5a45fedb23dedcf2e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3924 zcmZQzfPf{keII?)oqqBNp3AFAdmh24*JT~lB%pCS)xiIJC&OBxD&d#{*^gDSEAxs? z%xeNBaLDX<7_7j}m1NAyYWM!*3g4Q#hh{Ujy}f;Lb5yM3)j6u_n%)e74nFFfT3Q}) z_qgpdKsF^UI&l+XBLgFd-Wn^gX?LxL-_-+F?n@tTop6#rygAfe=14&Gz9yDSHr+ra z4$k&v-$XwAP2VP;uxL_V(846&i4SK)yq#&W&$v>aX=BVU+Yg4_C#Hq+nXsw(rQKDy zaq{6?gEM`%K4s@`4>=jcacA$biQLo6Cu+L7n#B5UbBdHRVe49EX6HV8eefld`*NBh zB2u@7mxqQQ>$z)jDF4fo(>jG55nrzyJ+^#`Qk?2_2GJI8-Ur(@FHZrvnECkGU8O!C z76hCEiKa04csqdTZJw^B)8BqsFlo=$J0^BITN)1j)0wHpdh+v^%TcY2QJ1Fk%;tZ% zwOd|&p~=s6`lk|}Z7FMC`DaV{|8o|)^9yb!nTPjinCSHW{KSy*?{8{QOaqtf={bol z$_}%y72nL@FL!x(J@wftrLKij-!zpNi3FaUta{4$s#bWHlE-8A=*Qb2Zem~{6jwlX z%s|WpQolO&-@Y~B;U}_W@5g2LKHI>ovrv1H@U1KJrDHEJYCA_W@H;sG!@vuu6dY$D zJwSlWX6O@oy5?j33JLG2Mthe`&8jxrac!Z(&fH6D*ea^pXJ^a?X<+*Gp#ek#0VCL4 zVBGGvnX3Ks|NUH{h3V|4O<1g=c1=iieEwyBy1SN~$E&jkb{|~A>@hWcGxw)UXXW~4 z#&>3aH@T{F@4SKSmVJByQ#gQTf&H*y>-{?>Qu8h6zu8g6cg5z|>a(Xl`(2*izbw(# z=6k{I8&E%Z!qkHt0R$jFfc-&GKLZ0BNNupIGe{qU;R&DB_st^Yy*xQL2i8VUcAt8v z_hi=GqF25Tmab$My~nU|(H^~{V5J~}5$pz_f9=Co)J*MSevnkJ>}znFJ@4`38G`=u zAx@Ld7fs0R{CG(3X{0|7-)yc){{jyEOw7Nazr^82l5ND^VmE8My$w5AfM$XHYxvrc zdBz6CGt*d9vtOxvOX1{pR_gRo%Ja-SdD3)(upv-g3WI>yX$A(3c#!=-3NV;|e#LA^p^~L%_m#oz~37*FJ+(GKujWfa+j` zy47J#f4r=9r1__BC+@y;y;&UmIFx+; zzE?b{5rLhb8FeQFoV>BAbWeo)52*f>|3H9b1S6394=M*s%b@xP42UTE7}(d}KLIWO z;((e!M&}= zA$oZMj>=9G)tZ}zPOV28{A~W!?d>o$eikCAURd#RUEVDIXr80c_(rccKED!OfQR3WGyaQO~(9%s<)94^^8%i1_(M=$Kp#Y?a zLt?^}psNSfTk!H2OPvOj1Di#J+lX(g!kmO?vm(0}r7k7HUSPV0ry+DdA}4fYJ76wA eSC2bPkm?{Z+=OB`5(kM1a}v(HhP5mK=>h=DAf7=0 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410124 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410124 deleted file mode 100644 index 0e792ff9b3253de4b667c3fc0a21d4cc12b8a136..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3844 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 UWcR|tgeLZa>pAT9f*k@P0OOy)tpET3 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410126 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410126 deleted file mode 100644 index 67f1d1a7a52e73eb1d403bdbde0724bd3873e67d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2752 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 A=Kufz diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410128 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410128 deleted file mode 100644 index 17a0790a93f7fd43198ebf0ec08f392a608d7178..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4960 zcmZQzfB=5;+3G3mZ{ND?jACmEyb`VG$oIyr0b6Jn78$$6K!7eQv_uDzj+9qOYd|rng@2QE`38 zviezv7qgPBS^>9BFjGV0frxv_bN`ZcY)ja1e0jEHsDGWZ|4j_7)r)%l-w_g@a+OzeJiJi`thJ*ifW~#BC{QTu|R4ZfDrRhAg z`5$iWmRDbB@^hX3sf1@+%Gy`{*;4-hoQ3ZEf}2U^;XN8AI=w$XF{J$an;I0;z$JTn zPGXC)!|ZFtH#7LlT^?RfeYQ%eYvI&4O(jMmfhQ-co-)3w72c)f@t8gO@ivH?7#Ik} z6;K@u5Ho?)I~f}hU`oXs+y@KQ}=Swp1J2?Quz#pg- z9A_XsK!D6igmJxnDJK4*Wgv2loKQu(z#;ohgi8&88Y$ev9FQ6Rtw zHWwJT{?3^ON4Vc>FORfNy0EWN;6+Jr>znf5JK4q8uC!NKRA*{*|FQW??HTg2hkQ(L z7q!mU>TnfxX??l#=LG4Oy)Gd4gZ+@=ANZ81JdW>JwWrdKdsjVE<_8?U?`IKWwNGNo z=Y7q-42*3HfYxqhVES$cvLA@yAZgJ_ULc2M@0_=CoA!Gxc{q<}roKta7uO?fU+$Yu zf3!&_r)2v?6OiLU`oMY#+6Ptl()M!sn*dwQaL&49#vS&Ww#Ozvvs$|5_zZz3=kG7e zxB)be$uY#!F9^thf%H??k}GEMmG*A^m9IBBio@+xiN)*k3;_${by_nQUjyeKMnheg zaz?QKfMI#$g~G=lDh#~B63h0Te`vKUWpA17sltfE4<0|e|GRhkffOdgA2#++&SajR zaO&=s_^AhGtuwpf`~S!7Yu59POMmeL4Frefsc)=d-J)HJ#nLwm#b5qs^!B=wvfFLh zg?ooI{P(naO=V!<-oe1E)yBYF8UxgW5|%JW0cmC^8x*!6z!bv$1k508K198lT|I~a z4)f4p7iSQMLw26SmaNuEcJrf7e_K#<_4wXf9OC=uSt+k*Ex&K%qW;1;76qN=X<>yooR358WQ4>Ag9De!!F5*eYFQSBCx1F712u|ocd{kf?D2%~z zf5_+6#n&$mp3~iW!BTd|jn4%FYD>O6eQ+C6iGUi+)rYuUfsQ)bkag51vZ>q7&G1_DN~xuEo`Gj-DLNdY-47B5MAe{E&O zq9*3MHs_w`xlT6WO5gg=p||LP&9gY|;##p=`Rk78-#nl3cKyxzYXFv!edp*=l`D=gEU@WMF;)p|Pf2P&hC{%R8_# zf?)zw1uoklWhB&S1cfDoFoNn7mJBIy80!U1_YlwFniEfg*R4@Y??-=$X2_P{^b`t|~ zI1!z9k^BL-5y-&ikk#*w_J0Xu*kbuvNIJEJY3Fj*olM)C*TMj!*5L(Fz|oafeJnmzS^tiYweS{GMzN!HEv zv(9jicl5qA&F3{lKlhIRKmcUJ+j2neKd9NfIM}dy#|x^CftdE*;P#-5u zFNj8RKN1tJ0#_OY+YhW4u0U0yC;)y@RSm33KAy ev?YwjZo-;I2Z`G#2`|t%05#$eYZ`^dCj$T=AnmIF diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410129 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410129 deleted file mode 100644 index c942106cf9d08958a9610d20ffbeead5ebd91479..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3976 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410131 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410131 deleted file mode 100644 index 2bd09182e856e66328e3217653d245212092863c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5880 zcmd5=3pkY78~!DQLRW(A{8aHR{O8aQhUxf-#0s5_Gx52kN0`LIk)$`=XZYZd*1oZhaf`P zvfj3JQJk_%t+VIoZ?fT8L?xd5U~0oQ_qHPp{@(2sKugvB9FP7mCzCfa(nverU$w)K z%60$Ar}6>EZehQr^Q&9JM@c}1HlH*4k)2-VoCC&Q z!6g~vn`I1mDPw`E0`9G?@tZ!se0zWX(}Tz8R^9DKY#WP1zHFk!5z2ZtPXA#}a z86j+Vu4O%$GXLCPA-C(Zp1JD(KX$WPJA2SY(?nx&@ zmS?H7d!GMRK@ee>^K#+NMnpZqP4P|*rz8*}mTJ5rduuVQ355@P0}-dsX|SxJ+ec@Q z{CW5Qx4N7BnaQbLbuUbE4P=_1{Mqi6NbqXQR>)WGt4{sMu#h}{!j$hja4IJ0$B9$1 zFRxIR6-0LVTX^pMm~66dd<+SEF&X6g)vmc(ErD;z^GO{>o+&zx_vF0q)d(>m$h&+6^rXQuw_8te+P7!yLRAaYceA$xg#=iD4mFRD>Vh8v ziVwudR!vjmJIkWfTSByq?*}C9m0e#{b)tg1kTAv`c4s!}sakHUYaLmf_`C1ov#Bm= zk7|eYt+$!1rlhadZ_^x{Q~;ZLNaa*&U&@l!-}FfDY2ciBb~<~UbRy^x&CQ?g zS6u=00uhx30Q1LwaDDKI#J`Ad3>vU+P9!VRl0+g?J{6AQdT1H=1*4xB>!WmWD_!U3 zo(_6ofy;&7wG?;DT8+#nweiki3en|DR~ILH;biz5%)cJ6B~K}OU$x+y=Fw_sI1pP=~N$87!a>Fsw8wkGrbBc$Y7*>_5lc%6+zb`<^@118&8k7qgug z-8>_=w5hhEESIrC14BXW#Zs?>B+47UFx0G zgZlYTBY&kHaE&oJ_S>PPN=`yY?kX%s*5djQ1Q2}RXd{?u(mHk|KJ;#%*@WtGvMw^T z$idB#nw4_**k=`gM54M{mxEAUI2W}Ge0$!QmZ#t7Xq`D3;K2MYq|HD%?QXAAj!tHn zyLVbcUAKGI?JClkhQ6}0!Ml*<5H=pKUF0-WMAAD%uy zRkL*HP0?R-F(bi}O;o61{r({FXlEHBrRa{R?t^hI2iFIWNI>4AU=7q3sLO(fU}L}5 z4fY#^dS*PbisGJevuyR{FSr)|J+Hg?7$=!|!($-1pZ#LzOQAqrAjbt*nYIeG1!0Qwik*ZcA{bJ(VcN#SM|9$zzs37!KHPZH->u{k0Kg38+* z_W?=kYG`7qtS!%y<(b*IvAn^dX~`8{-=H)?VaW}JV)L7@65l7xjkut>4r{=1DMank z8hr4bIT_>*haPP~c&%6kHw{Z%7cYgm16$v}fRK$Tzj78N!?dJ) zhA~0R@OkAd%o1$V;S2t^;{HJCe=i?mF)Wrl{^3jLU(~Dwd`bKt4o=8L8KrVW?7tkY zAqM}A_``Xc4OrmWnNBRtl;3cU&{&YbUmzcd&ffcCLf1n1fVDSc3Fm1x_zmZ0Ca_%n zam|vc#52gW`jGu`LacUi0#Fj{ZR8|)V_=`4HA%52xU;f&afpG=so z&>k)l!~j&C!nsA}9i_5y^N>eLxrB4z9wgl_;;}0r)@X;1nt8FY&*3+jbn9^sHE=fislD(}3X11+LpbQg$Y(Hv|lT8JCR`tg`Tn-bbz zJO%sz+fguuvD~5LZy5w*KpHASi~aBJT2E z(V943oRb+@7yb6~_H~iN#JId`gNi3!?${k$bBRcvA5-u4T<=r?aXjW1!-jrK6e);( zcoW8XfhB0Zz{YUioUk?`!S@(M?qn~5eUtG8^Wc47Bx1yfRYk-9=2LWiaJTHiG;vPR lrev6U4$Uwoh#lukbO)Fv*rvmmAK2G_;ltO@CFV+D@i!GJ<0b$A diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410132 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410132 deleted file mode 100644 index 09032447acf7064e3eaf2acc5bc230ae3413ddbd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6120 zcmd5=3pkYN9{SAr7TuQT1PPMY^`+hUuoX^lPj^layKhHex|NGDX^7~)j_j_j$ghPkU zeq0mR{5m2*Y*Y1c@3UNnVRK4~!W`p_%J*M3e89Y%WbGg6zFb%K9i-D zYU^KXHh);W!OhVBQFlN+ebo2f)9~KFVMa9Nho3;j@t%~R9w5S^02a+PlbZ2?6=a9_7?KyB&{`uPaTpTCEsW_(Fa-#>z9h_ptb-w z2oxJs6{#o72bd|Q28~pBAVuMp%kHb5_uabh=8go{zIZds?>}&&qnKch)4?x~ZYra|kIz2v;SHC%Jinmn$yUWQMpdJtJ&3T=6dHvEg{N? zjHLzGa~m;zutFNw7p9`_E>fLt-loo0OLe=@tRK8Om+gDG;_guV5esjbOgPJkT7y4a zAQ4c%V6Ww7hU#@Q(%%Ne4;-H#6+XaDRjc$;_}AG=x6%ue2V8!&+V9EO8(|ZcN7;Yy zjIxNf+>gH&ZoN^b8z_FJ8vhGQ?IjgXQA&q>i`nlaHIRuf5!NYxJXW0q@O z-KFA$eDwZcH;9KdwY|B`4T`=YU~KL)#E%iInK0-ntOrU0;?rNKr-oC#&$Yltiva{=Lcz1Q=|uQZV6^=QTKcbk7}H0Okgs*%R>N z1UMG+{>gfy@%{4aIQIkS7guk=FvVP+WlWQv37-kJnbeDa-tbMGP7l#@a*}go+j*2Fb7Ez<;RBfLZ`A6JJdjC1M z9-8Ruqovnm_m=eOaP6sMfN(CPz~dXt}3V|w&aIj&d!86`|IqrCg4 z0Kb(Hv5QaW+~3Q-AO^e(^9VPXIF|SIr}9JNgYyup)wr4qh>0}Im?oXk&ji~{>g5Nz z^Z)4Kq%&GRR397ewIn1U|6!W>FY(6{3B#Ty$A9k<=S~c`^Y+rY zJgh%FPvL(~C{6hv753W?XM@?ob&>%cS4YA81AKz}pLB+1Ai!dx`-5*Av_HFHOTg1|9hut%!VD diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410133 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410133 deleted file mode 100644 index 6b6bafbb87c82b8556271791b296f5d547e96c2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5292 zcmZQzfB*rVtTsuTppP9suO~H%h3;fIy?(K#)4E;ljO$J$e%^Njs7kng#c$h|OYcj& z*dq@9dj5FkKBG$$Ch%PA%iXe?_uw1uxYfmaT|xZQ&nfEf`0@ENYc0Qa&(j%o9xVOd zW7*kDI{?pNhcA#&U7ZfI?@zO`}c zKVJsdUhtA0U|M_#$8L!PpMJ;V!&-fZq8^)BvAleej`(WGVe6(c z+58W;cFU_TH2Jws|5U=WEoJR1|7Q9Pl%<`dM-~(#K%qB3!OKyCc4AGt+#JLKEi?&l6`R898PBJ-vM5 zR)$}9fJ($Y%3grg0trU28-VFx|3r?PbCy2wx-l_6q(R5Hx`eC|u z+aHmu2L*E{Z*BY&v9;cE^<)F#NoK!!CJIDFZadvK@kTH=&@8Zj5Ba>h`1-}cbGlnE zSjz6W@wq@iZONCX56-S=ojv7G#+-{#|M~;fGeaE#@~1>-NvNl1MZQ^Pwpp&Zi?(07 zOG-&(psg)X4i+w8Iz3J2!l%W}nU5YC{SjVmqRjAG%01Y_dRo_`Wp-;n7J$^i!(#1k z?YCDWikJkW-@e>1(aNo2|0Z2SD!Q`@Ub^X>ow%6^M8&;bhI=FnEq$T6^>v~SMSuVbEvbkJy0&+!iw|7q~ z32$Nu)2rSBiVtvD7`}F7p0Ppk%rq9&>{lw^QaHJtl{$Tt@;viSo-~~xYzS1B!XO}a znt?$h8Du{Y!$H!bQ>H);NQ|?fxWLNT*u=sJA`4Rorc?X_pE8xl@g1x7RN8Uxs%Ogl zfW!CwEJCdINlf{?uh|!%{31PDdZEQVQQE z6=Rv?9plFwym;+hc7d7=bDq_E|52Eiv9bBTzVG{4?XN!Z0nG!4%c*ayVcnu#ipA15 z3&mglXY}^Eld{`w*@b(DH2n9pdQD|u;NHQ&tkuTATpA111M@G`IdBS=eqd!DQwaAH zuq0vg!OED-?CL=bkokEe`Sh^Q@Ftw3gpD z@^T03WMH6*ovtAsVB^&ORQ)ScFE!Iz{b!TWl%_7L==DFZpI)4`#kKF1!ZzVc)U-3w zDFSTV#59NZQ~vF!+Y@;pHt^q>Hl^bd^%u^uDCjg#3oE?gd>m>gBP2c;92mZ@yV@Eh zVV|^90?lLj9;(P;;Pd(T>D31dKBi`*%=lHJ9rkI;;-uU==DtA--5_={>ih=+ zAR8XPK<+=Ndw6lMLFtMaSdIoj)iDs01{SAPK7^*T7@$5*m|hT#l<1I{a24Qm4Clez z3bj9J(FsT%0vQ9W-(c#9b5q4u8oLSPc35}~Hn&j{UZApq8gU2?6Qs5jIQl+Zs%p9t zlrXhtfz3guJMWhAUkOlkICjsfC)$ds`k??UeWpOuE-cM3BBfVYn1kwiFd(9SU|?T= z&jMOUBmp&n@(~=sECC82G2zl6`;Y-7KSA0Wptd3hR1zi3iF4E4D>QZ!$nCK3f~V0z z;xpA52Rfks2q70e(Q4YLHs2hu3vN1XX7b4V~BYucfKJ(TzZ zBUq3GkeKjL1NjLV!16MBIsoZKmIIqbgx}$9W+M6zFn7TE5y@N$HhzPI(PQw$6n7wyqI3U(5*dtqS$7NfSkrb}MpvKQoE e_*e|J#~PpkkFDH9@h`RQg^qDxvlr?l1_l7IuRIh0 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410134 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410134 deleted file mode 100644 index 7e60093de2e0a15438629d7633887443ae3fa2e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5124 zcmZQzfPfD&(g$4y?guV1n*8D0!_8VpTp!+j-OJzq%a*vx z!n>~aRCDvsKwQ%a2rV=BOz>||zPZ?j;3hz?#c+4LCcpJn`3=D+g z3aE|+h?zj@yQaMkF`qd3uJa1vgpdihJ%zcHmLxBlBbeiK^}&)hpQ8*6>H-YHei;mm zVHH5-;CKV+0|8)s>HsN_7)L>Ift8_&fhm*^q#$Zv+FmYy6JVL%_m# zoz~37*FJ-kNfbymK$SB>+{xf@o_|Z)qON@*8b^MfO?n`D!v4g1y z)^~MwOtMV3wFSz-!VOHH6xGVHIH5H=;^9>%d5?l=oiPtJD_`lI{?gv$5XV2&0Hi+H z)fsHGjf-%(^6ZZIvdv8MJqk^nKRi#InPlXY_4oAhiCYlKY3{zZYozQ&_Q+&CEjq@XgLW#{L#Z`j;& zb*2k6Ec}4#VPOGs3(UVDHiPEfy!q44crMO(u;A0%?&wyFPV+g7R$G~GfrDUbht=5$+UX6P<* zS!1Kpg^ch&K|%X#otFHIXg$7t<)!t-i}x5T;0Kxo_JiSTN9GwD6wgd!QO$m(@-2mv z+gYj8M=8%U@8n6-3Brazbtw!2Vy77xG(h)|^&mzQXpTv~U`q7%n zIV04q4%0F=JYc#b@38Y`bxr58Q_3|Q+w9BE$j|lY4|bKE+WMu}K9TWLPz2uG-B|@`5DDe=Mgmd6xQox!P|$Z_aZ61NxeRl64&uo~$)a6z0kQ zaPwgi)XtRuKmcUJ!h{jX{Rfo;g)K8MAA~~r3`B%81N-{BSD;~?4AjR8(+i?umY_1> zD!_3G=fTn&)P7)D1WL0Yzz8a{Axs8h-BdthH-X#^3$MZEHcG+^lwYV3hu| zm7B1Z7lXuYl!OaSYQI>o@SanT;VqhQA&yB8KFG_luo$xB@J!ul~_d#T+&2m2D(uJkB-0b=6x RFSYH3jtgM37wRMi1^{gD(y#yk diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410135 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410135 deleted file mode 100644 index 3106790205f245d344c4e9f5de54ee5e74b70c53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4876 zcmd5<3sj6*9RFq_)iyO_Ob_oS(=>@TGij8V%5+38ZKPVX8DwoLdU$QyE)|89=xs%z zlF*ASiWC~;ks=9U+o;gPE7acm&G*guhW0d_bdGyY-T%Gc|M9#3*S&)vl0g@6-n+z( zb#P73wF-5j(t~tYT}$m=c$vT0KH=A6b1VQC{nhAE=Y`aV^A1jldG)3t*Pu-Jq;3nP zY;$>8(Mk*+P|==~=;4u-W_}^lXv#`H!(zKpQvMa6S6sj%V871Z(zs&5i?OLw8!n~4 zWLIRAoi@`?`diGc;}wduhZS`c>G;GbyB+gfX);pP%Bh*^aW$+?DU$!tcjegVZE#BwSGwBJ#zJh8wN9G&2(pGM%!fSMa>NkppJVyJt9DLpNUS> zij$;MrQ;sd265P2gt5hy@~9y9hzKH9xKye5cq^nNLkxFCh}~QPoC|D)fpM*!DZ9_) z-{;LR%nLo=ZJ3}pw4$T4$~}zaek)EjNxdOIY8?9j?|rtB*t0P&C?exyUT{w(f6{)x zS}#-A?fN{!9q-!_QTL~1&Oe5#jJcBh(-vJpQq9_0Z}s3=4R@9`W-=oW>}~lYV9JPj z6)`L^x7xrpk~#ayFpnn%5=;ovSF8Zv5b#Ha{G9sL`(v-#rg}e0X>X142;*-vO+I+k z%rsw#=9c?tYa60&B>+BH09etOK^ZV0jjsEYM-WBG`-9LL$|1)-Ol+-QxQ0ygXnl4f z+RSEMNzNo=-B{y0^hEzX@gaeLMRRsg6Uqzzkx&dE5BB+xdBQTAi|jS^L2v8*g?xQ= zZEH$-5GUbend93Cqa3n!P>h~#?S>vw@tt@|c4tIFjfVF-x0Tg5XQZnv8U}Qs=Z%bQ zb}@~Pskct2d%EnZv!$yr)0Q4gru|}7{U|Nmt{g$wRD|x}jgS_shxTzGa0dBnjO{7; zt@SfA=6!F1t>I^s_nAdiaEp@#$5lCGDe3e~j>W~8Mzcd%m*S~UuRlEOeHZYNEnV#0 zoS_6ZR)}ksu1i!4+>zhqJ2}Q($wnN&Zw~gR^10at38^(5kWI^PWGKc?lHf!HQDjEO z(97&2%Wr06trR#r=x8OH{F-9vV9ECH>R8L8(zE? z>@_kTy49$qQYbs9iCZ)P&KX~ zi2gD_f94bMOl(gX`h?5nnL+_9FgKAsRey0l$9r!M0#aeqh ziZ|N>MzYgt9-Sw+HeH?3OEQb%lN0Vps<<6@>UC2)MNA3DzKzk zBg{y#hkJ-~y+~)u)DfGZ+?Z?7t>_@3dO^N7@bsH@+K=C%ERGEczrD9@Dv4A@sWh;1 znKqR=QkPRAM&b1bS{|@cU##p zw4`n>!d;Ue&~TiW^(jv^x7o+MA%2I4Q+ z2fs!PQJ)ar0BHCOBYS9bcH7wAJp937S@g@Uo#AIQ&ADkRB^1U);|y)I_W?%q_60MZ zGw@Eq2kvutN1;QGJ4sS=SYF|B_>3FD*I<_MpM3g7$;7%L=#n0Su-9wu41%43oZ}0( z5H<*;^i=Rn#3AXCSltrx0Ll`-p}OQadw<>(I1c=6LBtFDo3vi#unTfW8e5_s)COUL zK;q^l_lk5Sdv4kH@xU<2*d{Cpq!fj`aYE{*Ql?`!7)8Fbal(8a!--dX@u| z#!Iz4Jii7D6GH5Ja=%jv=a5DS5WNL1arC`gNGYW8b~%2lAj6K=`(eLVU@sbq=|U$0P68?A_`JX1xrW;RzxSt{ fm{bQDQ?LEv8^I>iFZziI{y6#?G3@pIiAVlF?%q{} diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410136 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410136 deleted file mode 100644 index f54a0d0366ae01af53c9b5d3c70d9eaa96c0cee9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5036 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410138 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410138 deleted file mode 100644 index d49827bbcf3615c94f6014bf600e412972c7f0c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3876 zcmZQzfPk+$6JJM``7Db(8(sZV@>=P+4iUCF^GpISFJACua_Eh-KvlvI!Cqz1%B21XFQLglQ)p1GxLzM8Y5qt`64KDClq|1X~yG1a*xE@7Q4?+wnUCbPv>{p8#) ze!{)Kt6z)#nrqOX{NO)HZ_YA4oF~_}+h5wX!$?OdduDvK>IJKbed~L;V(z&mv_6dZ z=>6c!ngWSWVXyvAoPB>;qKuD=Y4(bm5KV^p4SFYyI1Cs>TcUU$Y}>p%1>|Dp-Hm;1ozSg~7+$0Yq=}bS<6!_RE4vd$!&&vD4YoaPXhbOf}Y%pTAs=YGsVNG@WNQ z|HG}_^6Cpsey-C$mGEp!S^LUATgv~Rv(TMia5Kp~yhp=Cr}yV4hLnGQQ-fj}xMWYy zNo-Men0>AIW(I$`%fsub&sHgQEu8wMsl-Sm@Z@CGQ^r@d!n>3_99^#+7|;B8LVGbsKp+FZlLG^{Vm(ml zBp?RI8<0i^AaSsqK<&kB9SdWRCcQ?v@2k#>T%6IB&k#}+a@kHUw=L|!9GFHMSqIUl zjQmkbN%_U@D(X&>dlJ7U9KNwq?#RybRlgYYfa=6O%3i>fGlAU#ObPHh$XadJpPS)U8a~!#yyD)v2R;kl-J1gH|{Xx0-6U7 zm+gHkw9{2(C%hD&%;abHpZCjovjD#(KFd#KN*hkU8q5w(SE4PEK=sIBg3M+(>}7b} z-a_E^J?07jxN776&Q++5nEtA^c|E`2EbXfQQ$TK)J(~)mK!6c!E-;PQHgfVV++xu^ zWoOaHLvkE9&({_FJioDRX^x6g?e?Ixacf&oo3U9XGkd!%H+Z>k{i`#=AFqeM^nEE? z`K)yF`AM8WvzT|DoA~LwO;3o2;zAAs9<4?9-5l41*47=HWZj;elmF)g*hWy?LI8@p zp<1z5Q^W4Fu!?a~gWW$VL1akjD zb_0dkO-a22@H0N8$D8c>3&M2Q>X+;sZ^ zjok!tJ1o55Wy~OP8ztcds=KKXhu|G#1 zX2+Hr-}Zz}w=;moHxYFWqAVa-|H9f=NaY9A9x#O^VWNZ|appgtgIqolFdu8$p@BV= z_yZ$YkOYvJaG!$wgbYAsHoVM3N(Uglu<{(9Z%FhzhW$taNKCK`Km_q_Ln?2Hag)}{ iCRn^<*o!29#3b2G@Gu1FB)Xl5D&*- zKqU^@yBwUnnbcVhih9kQ_VW6gl}o?x5!UZ}e7xa{vG0=67!v z^)Hf_*jKypm)^;a%@T@t?(}kf=dFp&({5}K?T_Tho#1r)(abBs^|8CBF~8J&s?4Kv z{;;~sDkJ6}*}LXz9jJOSKcoF=$>-uz^X=;;zB`~dOdA{lw zgB~${$P=EvrYd>YBwa1@B@IELjM6M}sb7w&FK)Y|Ecxup0}qhHWzVL9C=g%-n+x=V zZ+sm0N8|jwo#nrB95QBQOmO4h`0t{6V@0Y!@3NoQHaXV{oV(R{tj$;7&Uni8*hZ&s zO9Q$#hPj`RIQf!S+K?G&7C0VGePa#l7VT0jmcCgi{_;Pgx7VGN-EPY++&iSIFS7yM_>UU^FV+XLNPFfa6bVv37ZdA#$;w!4`P7KXABK?aRzZX zWal|-$!eWsH$Uq1w*@s1Jp!TQgUzvKT znbzt*n~bJ3by-EP|9SoN;;b#MeXkU@315O(NgX>Qog%=-O-yrmKjq(!x;>ExVgvu3 zX;V5bQGekai-Jz`w6MY(&c~s4GD6~m!6C#n?rnDd^Pa%JukO1`1@M>87u{*drF}JD zTq`7AcGB9=;5f@;*3bTzUr3$Vv_St?Pr~GuknSU|#Mbmpt7f)j2bu?td&Ad`%riD9 zo|(p?n*B=UTM8$)vr?yzQl4kt$&;oNgbjh}QWylpPBSoQWCGbJai6s4R47!8v!J-Z z%GlV%z!WF|6@$|${((=K%H#NsReLJ!xOdevWq!cn`+gQ7R{JETeBRgW3slGy5E>NV z;|kIO0qLi%C0ESiEA8F-D_?JN6o=cX5{uX683Go@>$GMrzV;cUiuLP5sA>jAs9PP% zu6fTm8o288gN0U!v(BBopT(w@7xF+T$lBhHDv{h5VMV?6=|g0&eF3wUU%43AUYq27=p+Ncot6J5kay zaptGM;s)7hFdIuaV9k>>u!j_96)&F-dk4N}eRTkB8(BxQ##tHivM&W?lZ{vQ>FUYV6V&X2lq3p|*V&Hu5E{ zZ_C~MZE|%wRDTMzjR!M=5vg4VihGoLl8Cl61N-{h51{So44{5is6Ai`W(kmk#DuHB tmDa%ZG_W0@0ab~T{)u!`4UOG|HSZ4+w^0&apgs{b;t;(KhetYy0RZDfY+?Wa diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410140 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410140 deleted file mode 100644 index d08b2214abec7e3739690691077187461c63141e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3924 zcmZQzfB=#Ftv{ud=KQK*erR^g^YFK+d)FF@`+FK*h|b?>KIudiP?d1{QP-nZOH=f& zXO&#D(bXuNy?kXOQ{JXUl2!G)yXIasTXN4N@*;fLzRc{2be> z2oMVbPJu*I7<{}PK=d|G*V5^4zbu%vXX_mkJDn{J2mk5JRAW8)`OD>~R>r7H(|Km| zKit|aufEXa=Q{mU3D35awXgiMrTqUn3*GqzH_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ4fsxFc69> zpgLwCW&)`{>}7b}-a_E^J?07jxN776&Q++5nEtA^c|E`2EbXfQQyBQ29DrdE1yc{w z0|cP>1IHmj{R|9jAhp4+&LDjZ@5R)A{{QL_7kfMUcWh3RdVALK4k1}dN8RSXqU@?7 zR~R-f+M{?d`Ux4 zD5ErsTWka%C`&%O^1uV?-)NBCAO}DJGMnLV+3vTCol{Sw?)vAoV4;j1*Y0Xr zLkFeL1^>>x-oY;M52T0b*M|m}Ua+}9KeY7pls*mRnx)M@W!IW=v4`J9rKPOvBfX%riD9 zo|(p?n*B=UTM8$)vr?yzQl4kt$&;oNgbjg8QWylpPBSoQWC7VIZckct3g$P?g5m-z zV`CEoXr_dz1Jfz~flryrweBdf+;MPFoMbxm>S~DPnko4`5-^Q z{6R~5DDekIupkK_F=3$uN%L?Xr0fEPIoN+_bqc6l1H~b^e#fvMNdSoncMUi{!g=`I zMxvXfE|thY;~m3ZBmpEQ+!e&S2_A+ZokW*uNdAD^2xMS$h^$7%%{@&?tQVhlibvdb z`A{8u{^W`&p_#`l<`!{Pbb!hb5J>qC1V9>Q1S60OE<=H0u(S-%lSGt#4D9P~*Fej^ zOrRN{auW_UG84bN69M@n)!AH2mf`*XGb#g1z+0!8Bn}c2<|LeX4Qp8f(ggr2{*EgE diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410141 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410141 deleted file mode 100644 index 71202b875420d43e9d4f28581fbc3af340557301..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3844 zcmZQzfPkr2?K*f)@WeV8cfMbAu6p?k{!>AbHJ9D)-T2UGvRl&-s7hEQf9p>vr8&Q9 zm>-%Q^E~`*>fW`6;{Kk77ozibnol}W^<(~tUb~rFuALK&m(N+JsTbVN)4xt3SoPWG z7q`Texe`D&B`rE>0iuC`5ky=q%DkBr@ZB?)efFalrb1@#|G&6AM~`j)>WxnF;(yX* zfJz+xs<>aV*^>S04lDmVp+_MTawX$D9^EQ5X1zJrtZ;g^UZtdSSU6pgb z?%!**4T~DQwWe6s6x}>y)}6*<|Nr@&8|(gOmCtNrtvbLU+7iqAVB6;9DIga!A3w+T zx&_37fKwpR6b2t}2N1o@)3tQ^+b;_y?b&+A#7<{R!@++#Gu2p6e*SVfs+BS7(sZ8L z{13Nw%d0Om`MFO2RKl|@W$i2fY$^YL&O!ObM|@E#2lo!+0H7*hWIO$~}^;F3K( zC$UA@VfMA+n;HD&E)TD#K3k>KwQ%a2rV=BOz>||zPZ?j;3hz?#c+4LCcpJn`3=D+g z3aE}5h?zj@^Mt3bsY>28Nmt8!NkdR5qcn?L>X)PHi`(ufOFp~uz=MI`$pIJ!(J=KO zJwO18KX4op)X%`c22vaB>I~Ay@V9LD+r`eQCsKF)^IEV_#*S-uwXC6o(&vJIXI}4M zm-xr9anT;VqhO^Vf)VTnU^?ItUr=$7C3f?UYucg1^j{Y5+ z)1=;>b-Y7JR?<l^2|APGoasv=c6z2O?WJH;{6&M&5m`CP?hx%KFhNauu zLgj%JBs>m#8D6)y5V(DhdBQ)g+PJ@S6>1}Bk@Ocw&Mfx^nFS6D!`F_?Gd3umnZ}};{YvFq3MaR-Qm2nn zo@d_4lcp1dLHRy~K|t&@1A|63$bOJU7)V-lDgnp_iE$Pb7g!k^8=IIwBw^~nbc%oA zQ>OAbzGKy%N;~dd^-P%`aQMETMTpfti7B7=HTwd!Fa?AL1^Bo^G=s?WQ`eF!X7QEw zZvB<7H#v&K?No`y>+%c%3*&WKGZ$a`3{u1FZhZhm0|6t{tqyPJHGQ~Y5wYb}Ygdo& zryf_UiQUd;Uv8CTm{!Q48XqdL#i@14-IIM=i#5GZDxOJtwd9M`)P+?&C-WJO3jN&i zgB55N^V-W+=eVz3yxe%>YgJU>%{R7_XL4|AFJkvub~A0$bE~&t8-d~S9|%BpL-~wA z?ms9S6t>L3JQD-uGY}EZ4D9P~OF+Xq3#bp|7C3-e0u(@E!ll7+3FkrbHUrFlV3}(S zm16{zWiWNbx#`vx8oLSPc35}~Hn&j{UZDI!jW`5{2~xcYj=tNccqUKgwB|6{Bt3oV zyy7!+TUlge*g6uvf0#NY=!mNlG>*YF7LW}~GcXz?4GVKnxef-zltoOxJ~Tk<0+=>9 zjchnn5GDMGG+%}U^RcEK8rVaLKQMv?NdSon4@QumkO8Qk2IXb+bO6!|i$hR312&7= zbq~mDr1}`yy|6H$iM`-D577=lwwEF2OzgEJE1y;t%_Bz{=j`NIp%3Soc4{C$J0CKp2#fWIbFi2f0fu&6p_kavQ5mPjC$YlX9Ma diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410142 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410142 deleted file mode 100644 index 2add5746e716a3faf975ab28af9704f06d4b27b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6092 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410143 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410143 deleted file mode 100644 index 1291873dc681e1f5764efea22b5c8ccc9025917e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5960 zcmd5=3pABk6#kJs$|Jenj|fT9gr*x^E^Z;x&5)9s(v`eLU6qu#LCw&WM|v462CY1A zuPH-CbB)9;lA4f;avL?8Q7Yz~-~ZqB6U()l)@rYH@7d>^z0dy6-us+?|389^5QR5q z*r%|(bn~)xHKf*V*4mzwZ2B|0^VK%h`lXbUZ9r^$RDzxMeS6`1sy^=veZFyF~OepwJ5jNaF zUU(|_zUNZ6L;9wMhu{5;For&QIeSD(s{e6(YwTQVc1!5}9Wr6G&U;?Z3bm7Rj`E^+ z8OyKbuypH8nho5NwC%dYHg=sK!Gs`_#R|xYf}b#yUl-KY__3?#UC*kTLoYvE_s!jH z(#1+tUgX`;Ow~9fc@vSbrXn-6B7rCi{Lr|8b}YehbWS#+$>i#CPleL^FK#nztc`HK6A*-?msgxih-(lG36fo8aI>f9;qFfT8|ltXcb1IBc_Y zZMePm#YfHdj8pXM?M`GDvr473{>qruh*11#;2Jg}JYWrUM)?n%hs|JY+CwKB1=A|0 zM^BoXl#TYO`m2TNP-0*E*m5uLU$t^hCKb~|btkol*?3O56UL)c=DSH8*d}z*az3S& z^~~(d*_)ZNPj0v8?PvvS3R}6@)0_c=lXqRqwmrwBcOPaC1{ty3#aGttBKL;vkR_X( zGswzq=?8L3A^YKVfRYfpKM)i1$mP5soy#913i950ac%qx*5BHx?{m(tW?eR?+Rw#) z$<+uo-o|pZ^*fw`W68!>HGNp;Igd41=4Sg=wsB5LgVj)9F7CAw$t=;TPoJji_d@4i zhQtgT?LI)rXGzNJ#XydM$SVASAo>A-ALGk9crM|XL~v-ZnF*j}paTe(6V!jg zbXRA(YX9N3u+tpbSo$dq$IG3T z>c8B5#%uU!7nXX1Vr{qa-(+!V_mG5p6&B`-77I)$*Cj|Zz}^S6%2#Zeqq;R=vV1!_B#x z1)fLRX3mfwVy^PgVYosC^;=%Bfh2^k3u60#vf&bsGLYsuEb4#lgMK;@~yPy z;16Zi+z!QmUW9wL@1&&~NoZD)@)EXGGBd;PBrXojmAO@3sdSrvPJ;DQ<2HRaoOfDT z%x3zV-MZ$DS{_L;9desgGwWYE)i8lgaF?#*Ac)pcXdi;W2|FjSi;qh|-qm45LR`1! zVx+SA7WQm4*(hnIBUR3VJeOkJzWPw>;j0gqE(B7Qv=<~>C@laEA^Hkb6sfnD+rFHX z8t{SbjueGh&u>yI+`4UVm2Z+uPr`ERAKs2wM=`-TO$E=CZD2m4zCSqRKaci4er~CY zuK4hkx2i`?-8IWXF8-a9t6o=09z1Z{!7r0pwTE)$p@bW+`Qf&M*KD?~Nq*7H^zQVL z+6(c*f<-l6v`k?!mst`>=g6h5^BSVSORc9mIIJ zUlH(ZggJ+j0QZuD{)xOYupGP#t$-55!YAAa7$C6xY4jZMd(bu{%s$8u3v5mZKCdwz z5fGEp7sdp!+= z=L^{RoPB%(n=n9N`MZUCA`*U<$L0alg>x{5{BtwKy?=9^v6Coq`-4y2%Cfch4FO3`hq-<743ZXgL)OM4bo73Tp6?koCKCHFi|{F`{2$I z$0SGPzW_A?#I)-RV}jUmfAQn`m0wr4 zvU7DIbKSGm*lfuV(+ulnc{PUqOBptcG49nYdkzvBQwbJJHT*teV`xE|gT z5|hBe|A2M&5iQ8fS3%wFs5;H^ee&kIsSs*<3(H H@IC$qz?p%+ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410144 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410144 deleted file mode 100644 index 75866b93dbf7b8d36c0046c8da4896bcd5f43ddf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6080 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%0AVftD24Jr3I(A^bf~knNOD2 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 C9_Y&e diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410148 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410148 deleted file mode 100644 index 4e22f6aa659456e7cdb4413c7231fb7b6eaa3749..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2920 zcmZQzfPhsOjz|UHQhj%jRs2eby;~piQ}xBK*mnMJT6M~v-}mzapeo@MX5-YiTUN$e ze^{Kvr6#Rt?{-T##wO%MURj9aF;lro$-DENm3w1tmTE75_54tK>ecX9*`LFY`P^Ty zL3_=SnM*)6B`rD`3!;I55k#y|IV-VeZYi6u=B()GHA}1yuhKWlv=FuJ(VxBbbnXXZ zpc02v##tMdeC0l5|74cF=$aV53r!!QH@y6D760rIx7qCxzv)Gnp<=yB@Zrc{ z4>MhWbFm7p057p5OYKZMUSGaMoc4(Uuh62irC;PXW1@`S>|s z>lP3T0#1QMQy6@_9YFLpPuJ4vZ@(;J!O1VE4)j|<1u^m<82T(0Ubgh zu7K*8pqL@&P|Ho{+jh+POe>hUCv(j0D`ZhScP3GTw|?WV-``Sv8Tg$XfMJjfR1c0b zAPoY@Y=+9KGkEw8u9wC7_Oq-y zec}9ib;l~f%91jfqLB2*oItZc{$Qw8ZNGkMiSgMy@vws_fh@mQ8GEkak>XpBJyE|` zQvJkxuphV;R{@ny24WPqgY-iINE|FDPOLI7iVGRvKm|O430IC!BD0=}@&IEP~ z(BBElw;pdPmp#>d)r{Zqh{3;}-iu!nBkq0^UGtz*$;kV6Y~8ZkcKj(hx_y~xi9&**%!M+6g4*+YsCGCIMu=ibQn{%siET62L=8MgY1fK zg!#)K4@{Stsxz}_#{bIT$OwUxV8?^f00dwQJ5U@jL(>RY6~QzG3l~Vbf*OsWuvo$f z%7-u&M46wy^Z>G(V0u9`7V}~G0pt%_+CzyyFr1AffW(CR6r4BUJWw2g0MvhIWid#O z62D{Ek0gM^1iJu45brh;<3nqf11#P#>_rklVv_78co>3o5?%Ho`2%hvkb%u1+h&PQ z__aBsE4JZYjO-SN`w~;Em{jy;9SX6CyP*B44U#^I%9FWgV0IBKBY_I7F4j(FfRvMy zVESM*k~@)@Fj-vj39&zEQP3i&N|f|Zl$#D&2h-S1So8iMaT_J!1*+>%08+#uG2u#x z&y$d{1f|R;SZA)guT~T~OB86^24Je60Jjy&z~UejzY}3TzvjJ7q?nJAkBP3!Nyx`A GKLP+K7trYd diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410149 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410149 deleted file mode 100644 index 7e28da78388291b99ebb85d7d4284abef1d5f67e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2744 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410154 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410154 deleted file mode 100644 index b873382d7429f70568184602c6002a95d50fcc08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5896 zcmZQzfB-jDdycgNos}C3es8&Z@K5FUt^I!_c+Pf7KI)6(zUXres7m%6{qr8{6&%K$i@mf)@q;2kk-A|th%2{gO z^W{0naVX7l%ba%U{ExDEddGO1L}gC||E=*=PFZ^-bRmOiOBU~gZJU>;fLzRc{9N|Q z6Cf4@oC1laF!*>ofau%>7g`#=Nu01PtjK#mH)x%qgJS(+t4ijFKkc?}QMe#9oo6=x z!>!%&>I+SNuG2r2@N7$2`^rCC%Kx9U(4Aj!Gs!%>N5e#?_va^ulz)FygJK%EWKYjY zY*BWYeXaOr27kHB!|SQfRw;EYocgAz#7HFY3EK&9X~ z1L*+*WH!Uz8p$2C_Nv^eyT4DdtYJGiCFPi7gNFV2#{pi;9(?If1!<5y3#Pz&!R7+v zcJGX)WeX1VR4zJvw)E7jTSd|${}^Lg9`C!xUv{~;Z1tn&{S5B97B;Rs!=2KX-|2sB zw!OnQa5aNn%sl4$yZSFUfQEtnaL}Z($LaVx-p!$gR}an#uJ7J&TzF%`$`f0Z6!Wic zwphn7V_E}}A5h#5Op`M~jsgOhILKTEFBbW20mA%$mwhP8KXcXe-0x2TQNkZ4u1r{^ zb82SIL`9GqAYcTW5A@4wsZHUnegAfNh^PFxE213TB%ps~`ek{pMXE(Ra-R#Wad~;^ zZoPlmr{G<_U;;bqyOmi}4rEWT$=BSGUd;zI3+$Hyjft(X!T*;nkYsme`%Wdu> zlClX~g=GX9`O178gzPMoZH?~x_T1>*d&gWCsA`|fD^YilY9idh7~l$4%5cV0D9uK6 zV(y=`B-;TEBZmv!u>~4VCHB8o z>t%kLl@rE(H^Tq$iV1q=C7ZjXw==DoaOdXR-H#UiQvB$=QNBQqYx7FyDbv*t=LOZO z7sz~n%?@-YI6e$tJ2KDMpm=5)i)!{Om2WAW+|Ej!K1z9>c_&YrP7pQ(s!L%I5IfDl zpiuQ$Fu&_64eE3J47f@Nos}1rzC~t|eE@;w$al`YT^=aukQ#sS=CVQ;v{YZtsvw14uUKDKG^L6f!#vyJn6+%COZIGOvAm9L{` z*tThzT}f|Si;B_?$zNp)IQZXCcjL#EscSFoxwmE8?x&#i1P+&rn%(mX4=Kzr6Fq2E ztHqJDLQL{Vw3oxZtGSlWdgZ@Y5mRpLjIw?c6nH3s$J_AKsn=r5mwy!M*5i1)yR*V= z7ysfD9biBF`p^LN1S8m7U|zeZ($QDq?|N-hcjuGx{ny`W|Jil#7xxv31Nnz9aZ57) z{4;-MTGuY6U)jRVs}vXu53|h?mDAaOF^OsUo0)eCUULG?W8K)fPwIbP!LNO5*<@Lt z?aSiKJaAw|QL(p_>lKOr9(R+WcBcFX0w5a}U%Wu>Kd2li9GHRSN<5TLOr8<<$gC-W z=HIDc{V>fy8fFP96RrRp?r?V-gVc|8{+(t=w zfy!V=Jir4V7V^l^2^Yf^hu|M89L$k0gM^B-w3nKM>&_ zkoyy0HW2Cu@GRSt=pQHuZ2vKA0AdU`VhDiLAcYSR=2uKV{xh8f^BLAX+Hdg6RH(Qs z{?Pw#tR0X1B^c`t3SQb^Ts~n#RdO$^500KEVO|E&pzs2P13X_5>?cx~7l|{U^1Mg` zdr;z&MDsC{2a*616CO42bPN@NwE@w~W{_Tx979a6L*Qi=(c>9Ndf+w!8Q2^$_4B6a8Lv%V=NHR7@mpPerGa&M!mNaF8Sl8VuSPe2 znnU%IlqU)H!GMN>$6b)}F#&g?q-EmFPl3e^ve95RmT&;2Us%}!4{sXSLy12yoQ))a z#Dsg7qC82W-!be*5jw;bkpz&KB)bVEPZB*=faDLjjX(xA Hhrr|kXAow~ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410155 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410155 deleted file mode 100644 index 52c73b5891ea1ec43514c52576ba76bef67d52c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5132 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 D5wlI^ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410160 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410160 deleted file mode 100644 index 048e5e4c93189dfda55507c10b22771c6ecd9767..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4644 zcmZQzfPe;`C*PVCx{SqEeytVKI&m<3-tyHmyi%@wSevi;p34@bNZ3nZl_iVl*3t^) zbwYoC>XtXlSZywLUHCcwk>;#^@ki4OHPWuMO-i#V(VZ`PdeZ+t*I&Jgywp8q@AsJB zZ~JbBd4LQ_T6A&_hz0^i5V18@VAJke4Zo`gtlXDA+&bYTe|U4KyUdY*>U~Wtmu$L$ zN*uTY-n}^b(D*^rmq`|GGBS*PdS~ONKI47lxO&~qNt4bri#cBSI6-Ym^NPpyGxlk) z-95eKefZMklIh_#VWG|}F}zg^*cC3->^`T#^y%{{4%@#~4GwZAA4z{+r8;3}@r`H| z@1==5AGKc0_hXuHv%2NdX_=&0-edDP+W&vH*ww5UyMRHorGWRrw#~~^KrUuJelGj6 z28aa#r$C}93_jitAUb!!g_eeI5+`g6EArmY4O(aDpjf}ys*?HPPrL0~6fOu&=b6p_ zaBH```a+YR>-0}0Jlj&%zVgqO^8e>7bmte`OfnDe(J;~J{rQO@<=@}bpqK_O+0%0p zTa+DUUn{R5o738X&7KkzA2c^u!dYEPvd_pW-T%nvwx-_Ih%YM;cE&-h%<&K}Tn8^XZdrvN2l+>$|&XFlrv2FR7>3(TfjhCCk3sE0CMG}us2H3kB8@Nwga!rpxPtY92_oX2f!W>q08|Gf)U6H?KGH9lG7lWxIq&~M z$Idegy&P;M#T9a+wh1)N+`{=PRf2Tj=#4n7|OiT{js+i{l`OBU= z$;%Hk4;(Htdz;tH5mh?OkZrub?=KD$eLT$z_82j#-C}mmZtK5_T8>i2gg0ruMZ6{KY+~z#%=yI*&SzEUq)QK zDOaU(X$A+cnaad1zv?u9R*SRmvQe_W!1CH6e)ZyV+bLf)!mOjU{Iwoi{Sjrf>MeM= zvLeY86t^s0y!S3$ZqE^2Sdz2SXnH9JYC=xma7%#VF#EvfLJ4rVBMC5r*^vANDx0Q5 z8ZnJoQ{sS05L5@c12yqN zwSp<6go(t2s~{_l66dDLKtIvcO<2?DAaNT?8YR(9pmq#3;t*>ZwLTh~KUexg^GvsZ z?;$2P4kbn(`B(V+`jPXpZn^Kw7eqi)2B_T#0KHSN&A9!mUy5iCdoNKCk|aOGw6bO6!|i$i$XPNLs2 z>_-wnVv^xD^5Y%ZUU0h;NdSpSvYQyry7N4K!K=E}S|&*$MDBET_sIk1egF8gq?xAt zy&JOeFw{i!JOj4~$bf}6yj&;P9s_EUJ)6ovLYhWNYs8tKGKU27vE~^X*h7gwFya|W z0Er2Y8dCBMsQm_NKf?12iGIhhA4vd-Nru} PAnAeI2xMS$2uvOTGRH)Z diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410161 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410161 deleted file mode 100644 index 0904b1bce9751d64d0d7f18eefc12cc5b3bf9e37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7296 zcmd5>3piBU7e8aX38_3PZyBjv@(7ojJToC9rCaKcT!m5-lDS?rbU&0H^poDX zN{?G)l$4A|AyI$aic+Ko|9#G!GxrdG)0OYDzi*s%_E~HHetWI8_g?3KAWq{?DRgx` zS3+v zcs}jQ-Zr;W)C`h7BD*(mu-jeBcKO@5-_IHNP--31sTa>rEpqQUf2^hhM)F82OXDCy z1o&FL!@JJIvDbF1(5aVg2GWL|gKbrb+JZN;E}JXKvNz2JDrD<5XAbv+117^G^)T~~Km`WG?N#!aq*F4R5mbF5Wcn1y_0BbgOd z5PI58K|6b}_->*xG$WEWbP zwJd%PQ?FXfB}R2wi_p530uCn#8Y@;nPZ0e0V13=cmlBU(Qh9Kce~xd5dXJ}wn1yPz zovNR$Us;*K!D+fc#S@Z}DTg4|`bBXJ8+26!k}@|GsuL(SfX`4nQC0WOGr+<(B-UjliUU%GNYA5qP8 zZnU+A^@97(+S%FfLIVwhi)wWQHmWKJRX;p+Fu?(}Lu`4#6T{&FQ;2}_Ae#v;w2Cli zgt|bQaV?sMMCw0HRsPLJm_PFUnZ!e>xo2i}gv%r*|1rO|ap%F^H=m{lmk8NVw|Cw? z=J)<~Ww5#k7z@dT1!u!);W{?D3!0|i@;SXT0=n){%RU-vY@oz0eilX&fF#YyU_ESy zvB3{(u#DV~X%9gHm=S6N=JbCX0l@|P0EK<;KB<{@LS9tUGMrafnE5ms7RAf^w*US#GrWEM(a z2vDNXS!et|A9-S~IDLvqzNp=5H{Uksa$1r0Jro7_5#a#lYj6K%g+W%)9e{ehKdsMJHr~-(Q4~cv0zqPh5K(I{L=5x*Hu$)5NdbX~ z-oWF+jn7%E6%ocBpQCTMI2;xbKdxG7i8x1CA&H#l6LolT^4jvAl#uGu8}-7nHG}Ovs=Ppdx6JxUOVqZ;px=)@}T!(VOv?o6j z^B40q$Z&_e$ejrTg<(a%+*(f^c(7NsR4%--f?s()XKX+dcTvrpV3)im22uxv)=9uq1ewTd3R@;1Q<{-ZC_rKcpxD; zI;M7^{_bkSQo4M~zAcC6P-3N%b5u5EprD?EpaB{4BLUnFpwHlV5YfF8>Ehwsg@i6v zr=Vx=E?|V;JGmgBAHze&MgJi2$8~VlFK6$D>##)h83C?uFg+eQnNBb#m^uC%9S~37?_4hSZc^#@cvii?(#-nyxZaqawebX(2x!(6D&m&76nE7^z}` zjO7ge{Kb5(g6cv%)FiUY{KY|?j?_WTm>qV-KyY~2BltR6jw1LTX{G(B@(`c%#tEEh z6U%AD9GxWG#^bM-_yEE3zl`DV9EFF$@{7Bsztdk6#|^g!P7+R#PIW1<=d%1AJHlt< z&1ZJFhTkxd6LI?>R%fJkpu_mo0yIv;*E4>9!Tb%^>qz6FK8N_ebVcCLDa;nW;&K7& z9+GQ3jz9Z5#)s#C&4Zga6M!?Gd3@s*TnA3XTsG>y$J3hm&bfbskL$pR|AHHC&5VN+ za{l0m;Kc9d1O=oSeO(*dpm@(GKWT86Xn3D++=h*vq-SmFin4`e<;XV!FzQHk+7}cJ z%Qfzr;i0CL(L5Os?2$See#yYzSCAob6T*oo+URE>fC{6B8DU8Zhjf7eXF*sIXhh|GUM)%xd!qz3BDcz)1|LO zK3eu+xjq{2;WY{TO%-ccj&8#`c=(|NG+_{0}<4brJvo diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410162 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410162 deleted file mode 100644 index a8a09edf7b5c752ccc66d008751f2cb9ab568e92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7176 zcmd5=2|QKVA3x6$vL>pjhI-XRcHXnaM3FRhSwb66Q9}%Rme63L#ozK;kSK~?Go!`G5NAx#Mv038lx>x#3Xrk)1MaY=8b*rt-gNM zlo_2Onx@vT(`hH@b(y~%w+T5|kiX?+KOW6`Frtsiwc5sQdKF=TjD_uU@zIMUsB3a`)PsLXHizW0PAp1-Y6!Dk?qp zy|r@8?~e+zIwX6iHiz2+`M94XADlS0kAV;iUaMMjPYCrmtsIRqfYlEs=<2uUM%;$Z z38lt?q5dy-I~>+BnHrsI7$(@%{r$<~Q*Oxal-MQR#NMJfb1$v?utxdv*5m8q5A+^q zeyGw{%MQ5bspqm)iKd#^-3}P<`@9`IHjMuEdgh9_X(qesg6{c9GtHXrc()c%H}Bp4 zEGAf6&c2K_qg)v2OX*)LHD=0Wt6X5PE`Pg23%fqCJm&>M= zOm=$E0#fyK0)rWmtEbw>SIf1gfxfMI`(0vHN)0YXCuOthqa$U~glQtH*1sO_Ee7>r zei;%oW#b;WO*RndS_sz+@JrbRI3{!^^SMINxrM(rWl5xK&=?)&mq&Fp(swo&ss0#z zGKJ=iujAE6nA{WNTg+#6#>y^YQC^hdIm*UYS*?b72d|~cJgjca_qhvo3Yadkv~hqP zqVeKOik2E7PQhbhqVYgQu2S&tHY=gX3YUFZidMUs$}6Qx6zZ9p%MwJd)TJM)x2Adh z;E|N@YAs#p=km_<+1`Jp)Hr7>)R^D(I)m%W^u*YMnex<264v?kwW&6hlbDTpOXJ%^ z%UJuEqhxRN0O`3V@Htxx0bstzfuQ|jdmeH<0Hctsqxn2+sak$?V6>)%mRh6S`PIAE zZ2%=UcYAk48qgJc&o)HO6b46gPOD$Rt-y5{2AFG3>~R%;Co*wDcB-3|cJ5P|?}Av{ zbT+?sU)A@}HFWWndZy1R-A>2b@@MA#B5xF`E3Zxkdy_qdv*WK-Ef>kZA^|mG_)q_= zamQ`b7B^t@2Sb{A-}>~at}x{#F{hYfR+THf98Cb2DFe+`1M!IQa1FLc0u9WOm+&$C znbOtN|Pz{tF?`U>2+*DdDs4!^Jy&k+UlE9AEK_Oa5=mgUmVVxYOb|xXPxwB`H`?>btGwd&DFJ~7xg#0nb(xsgl=YA<5;aZ|NdcK5@ z0`;10)NK7%!HdPk3iZ4F^qggmWtbBaFZSQkOeT_<%uN1Vv@RCiGRgW9x-Y2 z8PlK~9U<8Iar6r@p_*Y`D>02{PP<{sf2zS`Fd6!?_=e*5QBz1`OQwrEN;w)@ZTpI`PLxk`M>J}7XH z4O8@>8PT8YbQfiBtd2zJ=sZZ~1E!NSK8h;IZ zcaVenB{3ZME=6mJ_@X&X7Z3g-&UawQ?*uqmNZ$)kUL;z-x||2Qkk?!e@y{ouv%S8&a3p{wJTcfpG$#?eSo z#Wizcil5i@CpqYOwqwj&za?k-E4w{5uBs?j!#`n_qat^y1YZ z`DHjU4Vqs@2)2It0Pk>vbB_~gH^OFoYi ld*vqZ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410163 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410163 deleted file mode 100644 index 67783f0c097567624047aac8693f24b3ce1624bb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4700 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 TB)SRIXQoCRVojsaG{^t|*4tNU diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410170 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410170 deleted file mode 100644 index bc33e9d5e459ac5486407b67174c370e3cfc1e6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3876 zcmZQzfPe^dj?~#d_qXQ%QC$_9xy3xrJ{m8Vk@T#ZzfeSpY>nyGs_se&zb)0irKiBEblaf0e$HM=Z z2gltxcZuIAK~Ab-0}0Jlj&%zVgqO^8e>7bmte`OfnDe(J;~J{rQO@<=@}bpqK_O+0%0p zTa+DUUn{X?C;38cPO_3Y~JcQ=rqyNgn{45fq`3b6;SD9 zAO^=9kVXd}aj={~?Zs>z3uBKay+*n3tImsDoY9rf5K4K&EuSqvp#hBazsuTApdjV6<1a=EBJxr_FSAOyqH&@|+K_2~-mQI0eQlrET$$)@BG`Rax6gE^*k6sW3sRr&|9NCLkB*J1 z+Ih7!cBmU1qOxB8c;;bhz3MUd?m*e43LS~+{*hnv_vijSvMtxd{=%e{OB*d7t-q*K z)L>_+x8rGAl$unVO2eDkmlAv>Q^8399DX8)QYMSLHYHyuBtOH6Vxkv)z)N+V!jEyqDYQbH~5_PvEkh9qt82 zYB?Z1vS(AFS{WF@<^sdweXvR6hIavZ6F0@)~TPg->9Csd5Hpt!)w*x1Cv2q*v* zgVQPgflryr8GwG zSIpuo?cMq-UvF|0huf(Vi`V5D0v5*Wv}P{8_8F{d#!!v2M_m<`x?Q5Jb|5@=nVJXy3aH#-fql5`m z4ivV`45GOgpnSr44XDuSj>Z*eewzf-2cwZJL1MyWA#n-kLGm~Q%>JZBLEE8njG!_N zrjB6#2Py=*ses0A0)-Vcyat`yCQR1mH?DC}jb`b_uL~g;ah(%?DFhEI|oB;>>?O2f2JAU_REgLj!v# z@drk*APFEb;XcKcm(kM!NH45Bhvyp-{f=Qjk^mBu47VYbx5T(fYh@EG-ZAV&5?V|YgXs1%l0V=!0vXsG0+R;-*%Yp* diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410171 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410171 deleted file mode 100644 index 996be09a5357574c171f48275a4c98135b2216e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4908 zcmZQzfPjS6UU9P??CsgHdL>)&J*O3u%H&pPYP|iWIYaKdr4y$QP?d0mIY;X3pZi<$ z|ER7C&D>%hXL6{&KX<|}qsZrfbkem0m$Z0CHun5xdC)Vf@a@{o>6T(ECu~rj9;5lv zVPI9O#g9|&te+D2EbLRBxxI7Dx&`@3zaI&`eRRgmMrZCNSB1%IR=le8cH1tY$@R>6kq1iBW8M3^X53k>#@S?WNjkY*UUcj3rVs|vmTKMy+cqyx0lAp@__+hF zFF-5^I0X_-Ves*G0MWS%F0?d!lQ>~pSdsUBZqPbI2gUluR+Y>Tf7)%|qHsZII?rtW zhg-Yl)fbxlT&I63;n|k5_LYCOl>a|xp*z3eW|Db$kA{g(@6S&RDgXYa2E{aR$)28* z*rMz(`&#kM4E}PLhu2e|ty1b*IQ30aiIGU)$;qmxjIU~icPV*1W{-Zn4dNyS210QK zRL26uOd$0xcbqRzF&3|suIJT1{N&l&Q#tOQ;?o67_g|B4u!=Fc#lY|601SgFpi*$0 zf%E_YGMk}p*7wkbA-_I2O`q*?s(R1Aj~z;{3LS%Wf2i4CELkuo8>B(@Y$}KX0Y*HiuV<|e?%8r_O^hVV)+^=tew-Ub?_6rhn5E8obKm1f7MuC*sr}|GhlO!>U8*%zpVDIhc`z{eG$8APU^x|Uoqi?6hI>#uyh z$x$3`r%EhdmuCoA7_ZZsx%k>=kQ%069~wY35HLdB>hNwJ4@0x$`j8gUl5po5jVBZA z9k)$+^RDmBKC!929=t~Fb8ImIK!zBzhI68GVf&9-r%cn8P*?KKgp0Xf{C?UwA+u3t6hz1&WpJO1^5 z0+;RVa4#@Y%ONITPfVV|(77^G&wRuGjsux-mb^5W}GDZCGzNo89Qz~Tf-Z% zQ_S_j|4Sd{9m-f36f~b;d*yTI{nr?Up!!M5lLXt&3=HBPWx(zN znR${p^Pf|kCuv|0N}eRqe2nNr56SiEC|36cO3ljQJ1 z$&*C)@sRuhw-Ly|=8%^hZQnPZIdSXxtP9i2+gXnGM(3Z-o3N1gf$YcGiAk-Xej5m+ zK-+jQBN&m|b)dLMsV9l(3o@{;zq|(8AFKuHX9ZaT129W~d?Y4J7Sgtc^AK%0a6Jud n2V8{8p`?Ez-Q+=IH({-h2Z`G#2`^Beo*Hq8UWX$^GXnzvm1dWs diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410172 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410172 deleted file mode 100644 index 98d8c4e6a57dd4c5a6621440c30bcb2fc2025c0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3924 zcmZQzfPlT)vCgW(bz83UD|;Vyn#OElVZ!em8pIU%L(Q=DhUzS!D&d6HUU9P??CsgH zdL>)&J*O3u%H&pPYP|iWIYaKdr4y%5(ai$UT+XcwD|tQ~I)3_Ny~Ar0rS$Jhx@E6> zZhXo3Ga6)5(xQ`hAT}~Eg6OTW0-JW%YWQ6}VCBB_;noQ!`NNw--DQphRPSqIxn$D~ zRO0ZJzj(*HeA~XSA`yyrIbGv-TKPCRX<2A{l*h5|KJ8=2b0#MF%7<9~?ZWfE%)Kn% zrTl)+Uq3Z1JuQb@SIYHQvvbxQyq3l9E*&G7<@a}GPh{xAqQceD@-4>YllwGH`~w8n zS=a0~nfvun=;0J+*QjMiW*M`DrtB?PFBSjxX@GGTgJ??)?}Kfdm#2VS%zXUZ0e2A) z3j$7oL{k`iyd6Mv?t%*~4c{bA*cMjgy`LMj&d@=zez8?0^TVHZ+qWoO5Sq?2oB!d~ zZh7^ECO_BdpGtVPrL2ABpDpG8&spfsFSwax9^Rv2qSO2H6GO_szo|hn4P3IP=Onf$ zJIuaTd^3Z;+~wi*)Mu-dx)x4-(^O(45_od5>M7%^TH#$v9*^0hA8&)WiGhJoTmjWF z12Gdwz2R#|<{29l&rD-c&3>iwErpZYS*g=UDbF+SVa%< zyn*zAK+>YqtUwAR##vBYU}bDkTmDN}hI-?3^>r5*RKdZx?|IDFsF zBE)K+#FWqbntg%lnF2zC0(@M-dcj2cscXph4DJA znTxM|1}S1nih^ligu2zCJ>&82l@2$a+=_L_*)fE@16c1w0@*RPuM zUT&w)9sl}2fy;JwxEC0yJ`NwD=^_>@rHGqsO*cXBE_uUPb=$sf}dyd~1+sPmOibNucu{Os0wR>vHLM4g9Q zEGPX~%W}_^7ibpPzjd>|hb|2H^}%WSY>!jbd;WdwPUb!D`rm^OVOOzf%(JpEJV{>1|{XDtzpyk1n(eq~+vzW){djJ1kb8MdY1ijO$ z3tyxd#kO{yT)5T!CFlD9P+DfbvFF;^8K1WNeK`5g{M8IE;+7udoiP1B+smWP%68Tb zOIV2aVkXayu-%2AkU`2`}_GL5?_5;t(7rX!RW^or2;Q zBnLK&h`1!ad;l8`BoO5UvU@@4n%sDzU6@ejCS?CnE1nouFmS!zntDH)`DeruO?KV0 z?=E&cnzzvN?t?#*Y%Xx|O@Rh;%6}k$#UqFYa{oc)VEF=Gju27DGO(||>;bK3>wxBf z>Qy*E@+cA$E{z^Puyh8tA6Gd-oSQB^p|P7lZij^zJbexlx1p3HB)SRIR-#56qL(A! zNaqykOzgg4c3ELX_41#a&TKs4<&x=f{lee*>|Mgno@xBhl!0E?L50B-EX?6)lwkc2 z)Fyj26<$9f8wO%yNth_%N1XX7bAaU(+)c=KU@;$S+M$6xl=uU~*+>FNOt?=$enJMY zyo{a>KzhOL9^`VJM89L$k0gM^ggb|Lw;`45#JCCCh62SqhP_AvNKBI51gm$!8i{VF OBKZSuBQ{4ONdf?bpsJYw diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410173 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410173 deleted file mode 100644 index bed9cbede3892e4c6a865e90700b56a1546629fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3844 zcmZQzfPjgQvoF8QXMKCU&Pz0XJ;(AXD|r4t%H-kL*vs?dsm7$WKvlwfwPT%Ch3mFl z=U4VV>@kZXeDn9Auf{VZRU(P<)a_qXHh zIRAb>(Gz4-(xQ_OAvQ8Fg6I`0XC?N`EoJl7oE06tW{LITRr*Gm7NWL2`m?v5&i!Bv zRO0X~>*Fmmxs7V;F3-!fAIcyr3*EMQt|1Q=N%YCTWWb9Y}>p%1>|Dptl07{q zu|?To_O;@h8T{og53i>_Tcy;saO#_;5+jkolap0X8DG^3?^5!3%pU!C8^lcv420qe zsE!$knLz3%CQo7LT$!n7zTtnzfy_9|U0d&`HrWP?PJ47_YVU#kYzBTOP#9DLm4f38 zN=pQ!>iZOFSCqQAI=h%vI68)wl_!^`+uEY3W4OI0A~hg~`?KAWo!a%Q=De5N>2t@w z{!ietogMB4Mrt`A^}(*rK#B;r0M%H}It5k>BpAW&1E!JVN6vqXJIkY7Qj#na*^+d; zYHIx3SxX<;nFn?S9CTs!*wxLER9pJNdJkjFpSq_)x&QtZyRUPOIG(eKt4v4x94H*X zVPW{%k$J`j#WT}bRI^{Hd`scvc2?^2QOfhoJ9*M{g0LY_T?&JM*l7j^jRuhYKnw>- zi%xR^IUq64g5m-zV`CFDONcB?9hgq>4}8j09>;gA+EZ!Ay{n!n^8*gw_p=DG+9xsP z^S)+ZpcbZp(4YVxSBPd1nSSb8a>XpZ(%!AV^7ST1ak!l-v3OmcAz)#=PHX1kYo9@C znARE-@o4CX4lUd9SX0zi3!xg|2A z-{IZfO}Y_l)^eI#es%3t*OXh#GEd)ijZ0aUQX|w>aA^Q?Gn5abL0nK6FhlbUSejrS z18Ms8p#fPBSR<4`HXJSlESF$vh%{e@1oJ_Dfcb-#_Amn7M6UT5!Ga`!#Ds+sBrU^v zkTQ?~WQb1&~w(Bi!3uWf>UG*3&g6~BH-^`4#)SP`h6 zh5%Ta0o5-s01I<)8UYCsQ9m(AT`GZifJN}QXDcRWh1kn* fP`iWNc%ogHQ069-_(0+yF=2s%Gq0h?6I=rTdWh8b diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410174 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410174 deleted file mode 100644 index 395f0a84e345462bf2db5c4d58d5d2bdf0638089..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4704 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410176 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410176 deleted file mode 100644 index b0e4cf4dee13c8e2a5b02be14762967771b72ca0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6248 zcmd5=3p`X?7e6z4kn!k2ggl~DQZl+8VLU^)V$he$qY;u6{{ zr6jl8%cNVlBy~v*z1)x_Bkp(han9T`eSD^`{C=(9Z)UHv_kXYTKWneG_c;fGMrFCi zs#PZ8^sXBBn1Veorj{)+TBatv;?pppcIlY|a{iZK)a(q`zs|adD5akyyk1$HCDUP9 zF)Kr_RbIwB;G?eRvGmrDT6%)?^{EBxZf;mCA-bQj7QonjX458#nJa$#vUebTL zOgD_mYBn+(%6&DTNuT-nSx7jPSF>EC@}|Xwr-^^uQ5RLXo>)};`s zoNF`fVV7L%MX3^}g_$Rn6$_d#T9tamO=&c3*qguO$-x$um>YAEe6;4jnsV1056^3M z8GJG0Nl=6#yJG=yk&fsJRsbXs&k%W9R&tgG7z^C~z1oHfv{7M!UK(V*mI7M8(+}bq zAEfT+2kr9jK4D*|K~dbDqZ=mBK4|busltO{Q?*j~|>5sFKx^2H!!nkzr5!jf@#`caOWrFQ8;L1wZl@qnoxr^z1yRv@h#O zS8T}2IqPatRalyhOPu#9Sv;Ta^1N)669i4PRxoZV{38OqGxP+xisCYxg~pR`hv9X5lGCK0tqWJU}y^bL)Hy$KNrxCjEP6p`KoDaTEwcy%pW*+ ze@ysPf9-)yWgCifo4wTxI~*2ktdxGpB=59% z1>@#ggucY!M1%{R4~==wR~}RxSykjhE6Flbyhuzu;yF?nEjSpnDyH0~cbdO-*d3)> z2bZFxhaoD}7ZTsP$DBVSTPUtsBwg7NqDn$JP)@|$Fi43jbj#^A`t`0}UfQPA zTMfOIz4tS%&EB@?MFnLUbWE2!_0iaxDoGu@ekK1!wdMjwu(fOHl5n`2$Tz*hFcswUWQ3e@%_nq1hTRjs}TK-i095I9TV2pGWpxDYQ>0FgQ?G zQ_>Ta>0j*2%LHMeyAb;x;yh0Ci{m;_H;Fj8p$ z7v)upf|RXj!D?N>zD@gPCHoe1?^JII+IrIcXHuQUfUD*e*d|Y2Aza|R%8eSC*8RcA z^{Mf?-=rzcK0^%_y6##kn`4KG?ZtaN&3Br<6Vorw?qHkS9aHwYY#T&VH+z(voL(g7Q*t1C|a8@(&TF`Xz;LK?f#!6j=4|5jYsSH;yUgfVXf|?3^QMaMRQ*EpXFQ) zuec_c%+X}i$fDY1=Sn^Z#dSrHV17tmIcWUQk*pdJvz{7ob*O1t<+H0J%H&(F4yE&M zpS$arAc(|IB0dtsqp_0G@n5Rrzs)R!?zq<4%84AZ#>&{n-%DQ~9fzCISVw0JIA&2f zJ;p~fEm#qiBfW`AuY-&nmVI1kSQm2oxG%stn3(`PG$9ulqnE)#u<#!N;RnZ*PIXJz z;vvu6JVbgYDtP$(C8tw{Z26uTu>)+gnIc*5`%l#CtQPAkJ@5D`*ud1a_L#SzwhD)A zWBUG@IXihHHNf5M*Auv;cX9IyrcF(RP#$Yu${&6Fp$CX(c8+g5g6owRzm%y!ve zW&QYGpEdQDBdyF8(kUEi7B zZF%0!usxy?)zZ`+RxshCE~R!vwqS4qTHloZXE$ndMhXS8$^XtuuF zsvZ-DlLJ{Fuvdv-b3uza;Z?Z^q!zeWz&8-oKm0uL1|1`Sa~jq8<+OE@IRWliyuLSX zUlT7-W8Y#8k(l6~31PDkM6Y@K(ovhT(AHhCK{%{5yu;T?f>0roQYG7RI&Qvf1_AZoApgSDU+i+MDP;cNdQa(ZOp90L0(-WFm4*MU

!bTM_v)VfLYV${ zU83DcgT~Ddeij~;37-*Rn4Z=l2i*GyHWg`cfx)R<$T({{>fa;V{sfA|Vv1by$0@mc zk~snHSe`LZp3rx~?Q7x%p7nVIw{SR>3mIn(;~4<)SzUsf1@GXc(a0`c1xsY+zFrEPrN~+IPfuW5T>e)|Nw7F60 z2d*h@^(zH&gJrCDxslN!ZeDWV<@orP1x+uiD@VU)#lm*LcSNq>79OW^hsVnS#6O$^ uW4_Dr$%#72oB((51Lq3^ZV~xTxbfuj7jlY&Z{%C7Ap-;iSI6LzEB^ua!|s>> diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410177 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410177 deleted file mode 100644 index 794bec0f8a84eb857c9895cbb0ef3dcedf87a379..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7168 zcmd5>2{e@58-K?LWf@sgCzGX;L?x0eePfv-L$)@e4`VGtmP*P_s(<-P_!3b{`9ebT zm9!#jq@>MOQCgHlBEEayd0*%CZyeL9&gnU4=05Me&vT#Ov)nuPy@McZN?XxnBht7w zR{pLhZ|-BmVpezcJsZ-TZeP9Vc^Q2EjX+9#fvSdv!H&Q;u*ag9$aYI)l{@P#=R=PDPrACWBVh4(Q%^hdT-Qt2S7`X>E)l;77-F` z@B6wxbTscX_T)d>_Hwb9M%VDm`sDe1SF_IR%Sa8nN&^vE!Uw+HI&aS%_?R_Rb})Kg z^@OPkw!*xgyK<5m=**_4rg2o$LoB9tFXiC9Gnp1qs?Und8U5}VriIcLPqS|pl4bP= zyYHBB9Gfa&FB4&y&|vF%yfz}Fcj|3jpKV@U z$1Jl|X>z+yXl*CA_Ak4bFIy{?Aas<{TCi7UPMUgu_JWE(+w%jW&kW`V4%cX^q{=f$}GXrkRm~-pP1X?XxSi$vxTVru@eXtGTL+ zradT7_cwZlQ1oda1|dKc%^7GR5qV^KVcewi3iX>By(Jf@USB@>vq9VAe?Ro6SQqXn z?{Z!t1r?~(&ai<*Lgxj!JyqNmq{Gv)v_^i_L5YOR_a0WoyI!GO4yIgSNDGM?Hu3je zuTI%0ZgxP+wWIY!;Q3IAy^l_n_uSb#f5T=8uV6Bm3-zJ6H1Tm%waZZW4&O@FhWKZx z_Plzv+D=P8G08^T0^VIheGrs83*-+$`{2WgU_Ux08eF$YSyRm@Y<}pBzO#2E>JnqO zAlAOt^~+{>My+W{#+BEz_LZee`c2;#?cAQ1wL~fYM*1v~iIb_NmXh-I|HgR%IVq!n zm$*0&iUs(+CI4@yOdo~xM*88UEv{E%G8cy}FDttnoY_XVm3Se^h*zwcoaiULmfuRD z=7^Pk-T^oJwxJ0xZ5n49ck9UY3S$^(sfq&^DHXIGdERrMP4RQJSdw78NSDt>aAG`T)Uvo*OViOTq58{#m_OlLJV^d?-zzLt4-Lw0<&MT zLZ(swED)C#4HXGs(8QK$&eUDhY`U*;|CJ|8RDe{mVW;E@D9r^9k~tj}MXHR&Ty!Mk zJO>MGkSt&QxpmTK9h|mb^N6*68Lguq^OvI{3K4~{YmV+B(b*G&;~>{q#yol12PB++st zcmYl{AF8kQ$cedfRY!PdZ|a=Y%bX}^Aiswp?`7;&UcUICxGIoiBBE0&5kxTp`UXBD zGlsIY4Yv9pm3YXnByp!fUG;O&3L_hjwPH(#>Fqmhi;oFXT6y9U z7*U!PrR_uUQj`PAb@eRs`lnTUU`)`sRP;_*vsW@!ZtL`iK2ooSNhT|wFfT7ZRoiq! zaaB>|%6J43WFcgwD1__}Hyhkv49-Xc_YFv}P9gDF3qD7g@#kmDYb@Ih88|;_wUwa( zd=Q|f(DFSZ|5}!6Q`vib=l!xPj|8NyrfAK~j0|jEw31!4KglND+6bCN6b5$;1w_fb?9_)F0`)0L#$mz>#x~a3{^$R zmrdPWTf-}>9DZbtPoqH@W|PFu2SUsE@F~&He=xY~Y1}455$XY+t!gLVXKr`Uv2WCn zrP|ni-5_qDa4OSOhG`}b$CD~EnbZG9!tyfaiagdGzgr{#6 z&>59`ut&j(c&{YCosoBS<;m6*p`-=s%pUqJzMkgA32k}GoBb}vspB=n?fO9FUg_2c zzA@LGyLRF(Clc$Axw@r?+O;zNDN7W6ey91A8$3Jl=vf<^Spo(pZq^Y=6>g0vk+Ahh z^LgN{yggV&#o~;MjIu`hw4K*3r8=5ByLVEN5{3>klwV1%S#^aZ=&*T8`|O0a&Fl0- zLsMm=B$ZCe`sT)nP6V^@m8Luw;`=(=-A=K>!Z7(mnqlyb!MdA*ekP{p8hF&5N->+v zVL*V#<46LJU$8Nl4>EQpK)c4x4T4NjOcHd#@?!>+@U@5P<3_*(fklVl7n=W(CukcI z`%D7Ql<%=I5qeF!}cTbf@|z&?z+Q8DKd6F5wx@3y}R6UQ3; zO|bn)yx?8H&&VOi8l{JCwC}8(zkXJUnBb>?H%j_GDzVpi`X^VvNZ6QW@`nYMZ}@u* zX{?$7=Ty%+wg|Xr81J442x?F2i9pzQMfhHgs}qfkWp6|lFxYQB+rDGEC3bEB(-6+7 zTw;>_#h5tO=x>7UN8$zV`$iEVaRhcmWUPHA6iN`zsZbZ@chq~(xc7$HPy23}+?51+ zxu9eZf_4;f05r;XF6M6uy4U$V^TYQj)Heu(>-$WC4$%P)_>oD>qNzFjJ(E)XVsq6x z20zbQ@S$h%^IfNJGYu8f!ZUI(eJ<8S0z2^|C$_j1hNCe$)F ln3T?h;d%eYE}?_K;*hwG*Qhng^?xe_KSnG9h7%Bc{6Abgo6Z0L diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410178 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410178 deleted file mode 100644 index 16fc67540660f379295b74e16b08ac0cafbb002f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5828 zcmd5=3piA1A3x*T)GTr-8$OrSx)jn(vtnfL$x&;RoK|K9&O^PYjA z*&*iATIhzv3i62pXY8o$J(hOTkCGBcIAl9-kOy&78p`5lI z4~s@?8V(TDM!k$f*WD2DZW(I!oT8(rGj;zv3)A~8elY>rgPIlJT;B*X zIVV^CKARCDfH_^YM)rQZx5$?o-`m?~UDq$Ysk8Bjo{T}bPFz9Nv0*Z7VwB-Ash2KG zY+%>rdj8Vpml^7;>Npdsa5ZX~x}rd1hX*m=tGw@@H*${1KI!Y!?xJ`!bA}lTO7$y> z?l2vpbHZ|8Ia{fEbklm7EgM?mR<3J)=R{4vBK>U#Rkg&;fBNZ3-(oHHM6uoLu0Wa{Y@nq>7GD^F`PdJ2 zA%PRJ6$YNZ?nCi5w{(q^15&7df?bn_ck`9%B%?`j;#~#j71fd`lecu(PJQ_&LNngw z`@FB!(@OMs;k;#ExH@Hh#N;SMpP6RZI0P&7YG(A^R0&JE zG7!w%C~aLCwU$k8)Py(1v`5swr*IY%1TC~zu$?gc5dijbg^|m1Kd*Od@sQEceY|t! z+wI+L#Uqmmv_k*NA;zxNkoYzun1dkL6xlQ20}s3mMUtgP{Y7;(?H)W|UCn@ON&|a1 z*{3R6?HeM3l^M|pE`h1xK*SUFMdHGH8+ftDqv4=mhNM(mV?XP{YSJ^qpM?l!H*?On zclD8bQwNh~$o7HtH1i_dpa@wpKW+A!lYGQdLZQEOoZbhHS8}6FHe?X%7uqA z`tmZjXkPg-Syha@$ad=VZ1FtP^^0ZSWs?|24nfl|YKsq0q-o zo`{ThY>MvATYr>!HqvENf#V4n)690;zydb@!)`k4`@NWVM8VW??WhCGcCwjzZ+u)O^vJg~V^f;OfE__s z3o|Dm^}$@qZ|=-$bC=d~gnA)*e>foPxn86D1eWze@==xqqkzqo65!Goj za57GE%Gh>uq3_J#cMbi6vjUu1g?&l_Jh*uc)I%xJ?qz z1fEJx&C-kbboiNYt_wjw!dAOgE#^eDm-VG^>#-xyet=2Xe{$uSng%^Lr@N;04w-7= z#__AHGE!pmc5G9>t#EK^6PgowV#6|42Z9$~uQol=e+{((ya=K-BdP^CLi}Gtcpo$t zGGWPZIs$APjIH^L596EN2h5E-&l4ZifYq^-c|N8N9@sbqPUMye>cI~|WP$Sd!8t+* zz6SR2&(_Y9AE&^HTsZt<>es+TYoou12M#j49O4o*srL54tIQF3SVMO z06WH4Fjmpu1lz~(1%8kJiakU)!96DsiWupV<}90%jlTDu7zSUGgItr|rAc3|D3KxF z3AjyJE06LxUyn|q*D%hxJ<7*9YL$BI3fK=hyII0e`1j1}=W!8WHy7l{d+zyFFoL^%1dM~!Smt5tu_b{*9v+9?iQ?tl6AryYZ? z+a3;B}X^ z^D%Ys(1KIoM2tnG9_|9E4Xn`KSm>TyWbHipaSEKsh4ZKGNq#VCW&~r;VxGJ>1x{Qv SCN77-lkfl57I9Bvi2e`CUuHu9 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410179 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410179 deleted file mode 100644 index 91f91e0338c6685da2475667ff4ab8c8cb9852d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4932 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 XEZ9}p^C~P%Xkst8?Tp=Cct8OF7+J`O diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410181 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410181 deleted file mode 100644 index e9e3eb84c82432be9791b07ce75fe406b4ded425..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2840 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410187 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410187 deleted file mode 100644 index f2df1e2779f1af5b9c617cb3a645837e8a9062ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5164 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

B2P zU%_|5guTnd|5tf!R$ual0M`FG(;ti6(B?FH&~*C)Y$vMg*NRsmx{!5ma2Wy zBqm?K+RbiqM|KzU!&3*Nn>-$#cTibe(fo;N%Dt!FUsvXG|kG6o|AR9Guds@=lG1jAM*XL?*9Gvsgz*7%}Jh_ zGTjkR&zsjY`D(j1Y_Mhnn#FSa_lnBhpTxY+{q2~1*!kBB8NR)>i>p66N6TFD{Crw` z2iQhX+(H0Kyh6o5;lKKxeq+L|e4 z!hFvuVJ>Gs3D!9_sfXk)-UQXosPi8PfE;*z3grHSO7h}h!_s&=R2>5`b?)M{NI_`5 zo(9y%3DXOrk=&2OgsZ?6reOPlX;2iZ5+z>{=ce$dGJw?Bv@tp!qyRbCF zh?HJoVGe2wg8>n34+i%2=M$jqj}D+FR;X4m1+xUmL1MyH;7X%l`+@$`f~rIbbK=}| z?hlRKgf)#061PzjUZ6H4HR2F!8jW1?UrKV~rS<9ATjY;%m1Q-W?lal;^3k>jH%r{Y z7dDweBhLI3Slx#(7|Oz8KGw8D1A8d(2Zpne z1dy0;@8ZhK=;;8Y7o*KXqTey>M-o6{lHoR_wgNG3lDbqP18wVK*o!29#3b2GC~Y31 R`zuKPfZGUUU~>pe9stQedNu$6 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410189 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410189 deleted file mode 100644 index d5fcee359b5f559a9376510bf95e124f2b049447..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7128 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 DG+}yq diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410192 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410192 deleted file mode 100644 index e6d5af2aa516f3fbc0eb79c1764e0deebd533c86..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4708 zcmZQzfB@x=A9EY0-MOf6=GD>8w^{*FfoIn|dt2A7VLMslSh<2MP?d11)SBy-@otmX zg_ft^4nEE#<;tt;`ovr>Bl&xzNC)@I&nX>S_@7VRz_(dF)#|K`z3$|zH%qxP@60TH zQhIZL+3K%7U#Xw;pqzuTl8^WwX_XtqTi_tP*Ey#GalS%C+8m|0-#R`oq;m zY)PrN@6O>q!f|fR)-#JHgap2QvRg{VPnhFPu+lO0s7ve&qAe46A8gyaJO$)p=Huu7 zJrDu0Am9{8G=;&(+W|!9F1XOr@J-@`ZDB>;`?*2u3>_5f7h6>_Km2L8eT%{cq3Jxc z`5$iWmRDbB@^hX3sf1@+%Gy`{*;4-hoQ3ZEf}2U^;XN8AI=w$XF{J$an;I0;z$JTn zPGXC)!|ZFtH#7LlT^?RfeYQ%eYvI&4O(jMmfhQ-co-)3w72c)f@t8gO@ivH?7#Ik} z6;K@u5Ho?)pK5yS>#8y7A1CkguS;gNU(Y@3WzfYU`_fn{YVwzwBhC!`P7c5@=m#nV z#~DZu5FoP|x{k2FX5P17pW*a-YgXOD;_9!QMtNm=YiA^`|Gj5Y1uIAc)2|N=AQ}i5 z!R7+vwnjZZ=xdbC*Ap$<;}iWXH|z*sxnPNPwKT7*+m(y)*0cSa7nyxlxaXhe*tMi& z-k+O`F1%X3RY;*pOZND$?O$i{0nGyYVSW0N19Kmm*4o;>k7CRH_{e9Q(XN{F3!m)p z)D3Yc^6w(X5B+x)`YiO?<@a7yYhA37Z2y3xuY0|s@->Cu!6DrSx?umxo=pXN0OSX- zxj;WyE4;Y+IaB@7#fwLu&MW&inR%gaq_@GV%A|{n_s`!@T))3sou^n@?!IT!n!4Ze z+?o!JQf|{0s5PoBPfjXK+r|v@LyCXkQ>OAbzGKy%N;~dd^-P%`aQMETMTpfti7B7= zHTyC!wk-f!yOn|Idnm|$Acg~AzKR8MSnl{ai${u~4mjMdN?|-jycCiu*hrE^=G2B#XY!HRUSbC-mpZrm0;w+kd?&jjwzz zn<96lSWdBRjiL)%kc8adW43=l=?t763|~7k&)A@NW*UoX_A8ZdDV*HSN}WDRd7gPE zPnu2;HUz3mVGs~I&A_113uL2(<>_pw7-vCoft9hbiJ2u(04fHji3mTYfY6`-A6Kwm zFhNAPFfgrK>HyWj2z9H&C-I%`^^F3Zz1A)I2^$jT$1p4V9Q?qO#F3({y@^9Ib6V5E zIrHX}v{&07S|n9`&OT{lMsHryY2m$l-3r#!>azjOV-Y)C82In?eHkmmH=@s;WYsL< zyU4ZwWr4QSkFQ!A*3MCZ+L`hn2!L!@m@opl|DbZ9uw{mp1z;f}!kK}6{n-o9uPd8gU2?6QuS4IQm|D{!G4fU%gnVV#m>$erfw={?~j{{FpIv#ukm;Yv;av42@%K zY4jIV4i@I1`U(t)$fsf&GdY2E3Ak(x0czrfY6VkB2@{D4S3y=9CC*J8t7z;dtZ8(R zxD6$ZlISK-{YZ^C#F|ETZ#D_~ba7h7)jc<-%O)7A{8|}%WX8rn*)x_#Pwl>>@D7?X zz^z3f8&{e^DMtymUl_-wnVv^xD65XV=vI!RN81^CwATddH6DZxn z!-?p2Hj*B=jX(xAhfFft@0UnJ wM-o6{lHoR_v`mbfq%M`nK+9qbdyxc?m?XOiB~KFFe@F5M+(sY+n?qpo0Fx44m;e9( diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410193 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410193 deleted file mode 100644 index 02cbfec4d72a3ca47c808158688620d1d24dc42f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4884 zcmZQzfPhB~3OP5<#k_rEW44Nied)Vtk*_|i?^;^D=UZ0&+NTQJfU1O*JATYCln2B{21XFQLglQ)p1GxLzM8Y5qt`64KD_T$?f=SS^A^Yf_-berR^^(2CzBQa+MWNFka(#`f|AK-}Bq9%r`5KD%i$c zw*J`$i63P$`mtwiawQbAn9o!$+ z|F|u*b$?E(QGpR>@NUvM+YJiJH4M5p)XCx(=Ne^Y~E8n|Rn z&q-`ic9?yw_+|!wxy!@rsn1p^buFCwrm4h8B=F>9)lG($@YmRsX#R# zePF!=?Sra&X?wZ+O@OUtIA>il;|_aG+hdcTSuI_2e1^c2^Y@o!+yI)#Dd`$rZEsN_)5d%GaA5#o>0U#Nu^%hJc0fI<1+DuYCroV>Hx-DQ5)x517Vw{EL>3 z64bl!mt*RO#?QT5R-N>?aVYyzrDk1RPuR2f&q8X~C?uykvHaL`E2^dWd9L@Ay2C+d zRCd{}xl(`aU_Cd`KyX+ZzIJ4uu|e_7G#1tDS1R99IJupbI(?M#Jo8STG@T%92vnEC zARu;{fkC4W2o9J2y9#|4dhPOiud1~!)=0L0z|q&eUQzj) z!tdaaZUbFL27V_8U_P4wR1fnj$Sufh27@0xn*NKzGz&Na##r#LVr`!E!u!)d*_4Biy)9l!e0pql(S%`H z9hc6Lo2x`Wh3s*nzj=Gqqe!x5W%k#a=49*#I zkM^#e`5LHB+@tITOgR(SEkJ+s%PD^MZOq!rcqPi@WM;$`Pt9W6qg*LFL(V%Uonf{p z&HlJWU+QI}_^sUfcH{E~E(e45rkoGEQ}DVw@TVff3=XIp9F7Pd-YB?o%AQNE%Dsm_ zvi>yC4SDP{DPybjLo-h8`iKv{2d+;kdnmM)XM;p=NBWd@lifY9IeymuRAXJ+qZZW3 z26Pw;w~nlGyZ`qa)sI=z<{ZEC_3o|+$?8PAf5*Se@IUyZ-w1Vl%6}jLvSDGz2;}~P z%7Nm58Jb4GLPV4!Vj43!RsoFzm0h7gO}tREz!c09AP0#FR{@T5I1gku2td*(sN8dg z$}xiK2Pm6xS<1iwa+4d4-2`$wEW8Gr+b9VyQ2j%VI0T0YQdJ2bF|5`SO>3z7g56CRMb@-lil0O#1P}tG~vzv%KnR^*#7r}ZQsL<+}@goLET|Nn>4@M)o6Nw3vg|rpm zJb1kUu|H{1&@reSO8O_#O?5PO6V|*xNZdwAc!AnuC;%zqkeF~K>(b?ffh zHcXi$3RJ%VnCd}oUKD`EK`4GF!hAIg*;S;NkCKmxZf}y1kN-W0oh`pL#cHnCw`0-z zPD{e}6|jlr&Az{L-M>hNUE0^60f4Rj_YW!uiW8K&m59C;1N-{3Ine%AFHk=#)GRQC w6wXLYxC&fp032Vq+J8j4iG#*&!kUii^_i&=hv@Y_Jkmi703C{k(f|Me diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410194 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410194 deleted file mode 100644 index 3c3da42cdbad01c35ded372655189f13a703056b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6092 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 p57=U~Wtmu$L$ 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 ZrqMy-Hk33|KIlOz3eBqgF{!c&*V7Bv5jpnpiH` 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 J)HN{A0RWfCZ?XUY diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410200 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410200 deleted file mode 100644 index 336b92a9f09d896655f160ee3817e49c934a69a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6368 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 C`QUQ^ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410201 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410201 deleted file mode 100644 index f1aba9d150fa5471099a95a73b784417402a7f64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4928 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 ocEBkt4npxe5#~4B+zlede0ci@Wdwg3PC diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410204 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410204 deleted file mode 100644 index ab8afac47c7c054944d0e0e87476ea1533ab23f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3992 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410206 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410206 deleted file mode 100644 index cd1d9c6e8b2e8843de4af72d922d8ab60559cbf6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4884 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 E0EyglGynhq diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410208 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410208 deleted file mode 100644 index cf0ee7f8acfd158854afbad539346e177386db76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4992 zcmZQzfPjz3cHBt*bWQR0Gy%O&kvw+}99XkV{rHY~zZpgBj$OLC9H>e-uIu+ojXj(1 zWrc2B5uy6>t^eA#$G%7YY(KWM)}q_rc3J82me;8o&1|_{r~b^kGxv-2;?;_ES*^B8 zb@S$2dUkRX$fl%4r=~${WMBl*TVn+_?XK1EyL!ONed)uk6HfAnH;1~*90{o2*Tizk zrW>flVW%vY`{GG#ddCw*cCHYJh>n``C6R5q>c6Ova$f!T??naX(}-i2|PJj^_1~dt?(`-kH_rMkGDbG#K1r( zu7K)TfS3uS-t6vEOP{-ymltd*IIVHum`usN-XA&ohs+#YCNqRxyz!2K-^l?O2J?VQ z!EpwqCDKgt3%bmTv1ImiBD*Xd(ZPC;*v~4-QVC6);KYgq@YAUOGTK@K5 zs=b!2%H?=rFTeXYj=wA?%twH`{Lx+pDN#)vGZie&A2PvK!d@4Oz{tV%2Xc5 zcdXh|X~(^*o+G}pnXs~UfNzRe-mJ<8O~Xk z%(%l|)ArcpXI4ws9G@ZZUzw-4aM{&5F zDzSK7o*`giyiRN8;%lFQ>LiM4Zo!l@g8c^!OGD`mLD9V8tQk_XpWhPs;dZ+ExqfHj z1sBPXS06(Ebl%jE~!UG0uYG z0xM%<6LV9b08|W46A^w)0ii(wKCWQBV1kHnVPN|8p#iFc5$aY4SH-z?xAs+btv%wq zOCW?Jxp?;ayrq9u_^N2AUYz!U=bA=x=KTuaySpnoCLG=MOt*Tso=?C_kyhzP99zHV zFi&Czngsy>e%-$UE3d=YeReSVZQ-&q)*H7FTNnRgfC6V$GSUt=gP#R3# zRy1h3c5g-drtRkThOc!`RchSdH%p3T_TKrzMxSGDM}ic|o=t^mVFa5Cj9aIRbG&&9 zb*HWnzp=2f?*r@V1&>oy&5yDz_-lE?Bw^mQHM}$L{%&2kquN@2{r!I3$#!d>X78{$ z#xDK->FrfZia})oi~Y&wo^_hff>pdidDir7d~dUGOYEfJ*Z3osiBW4e^H03`K z0NJp3Wdw46LFGXHVg{BK^PzkOBH~d@Vf}^4hyfr<~B;g3sinmBM!k~g4A9B8q2^io#ViJ zhRqp*tI}mE<)h4XnYGHVZZ>>3ZSM`mrDq&ZtN|+ml|K*wOFl3fBnk_2P<;jl1nWPb zDsY~I)PYc42nyM7s3=PK5odnN9B3VjFc@wah7i`YLj!v#@drk*APFEb;eG@82^qlh z@&b?`QaS+X1(jEza%KUL1_KiPj$uEN01^{sI;4Du^AP3C0+1Mr+emlQY-qe=*o!29 z#Du$o*ziIrkBMoELDM?C42RnYWMFX!N*W>3e2^Yu&1bN-KN7GY{#g7Xo7(;B<03x9 zH#3^Fbn9^)iw;jWudloSbsx5R=^s=MmgnJi648cXU|)Z-1=^084m5`qY8IG63I`-6 zTm`PU0o#wOULw*>3N&^T$nCK3S^)FWAaNT?S|ia-ptdMA;t(7rNO>L{M2l`Po4xp( zu)0!3D9%0ql{6E_?iOap>g5X=nEqyMD${|6BYM3A6$VqVFo&m6g6&BL263&GO|W_c zBOM|MATi-ql9fh@bdv>@%QW> diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410209 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410209 deleted file mode 100644 index 57eaacdd4c78bf1c68ef5120bbebc6d10b1d7621..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5012 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 TbQ5U&ff{iL4ioe=3JWIy?z*6E diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410211 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410211 deleted file mode 100644 index 622e4eacc073d5a0f97e702bd5ce4d347752bf26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11720 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 ArT_o{ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410213 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410213 deleted file mode 100644 index 0339effab32dad40113e28dab617ebad5c7946cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6184 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 AF8}}l diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410215 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410215 deleted file mode 100644 index 3db4b13f69ab8ee575f74e961bb887aba6e4a61f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6116 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 Cx}jzO diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410216 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410216 deleted file mode 100644 index e699e0ff178dc0172181ab326db2972a97af16a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6016 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 K8?iYONfH21GI31+ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410217 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410217 deleted file mode 100644 index a0e424ac4da1a452667dd2405b8e009489105094..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3984 zcmZQzfPlbxj!XXB_o`h}5YlmT%bcrk=5lY(DdK;zYRTirYXkDCfU1P68M~Q`S2?`A zl2GapULX2VId$3xr@85ekFX`=UKiaxX9Dy8E&jRp0i{w;q|(SmNv7~ z(k?Gt|9}%@Q_`YS$00T{FoNi-MVU8~0=|3Zvd@0>!c@rY{r?x2=jgHRU%k;uUi?qG z3{Z(f)(wwCfo^>QA0BG`RGj87yLDz%mX_YnL)k7DPTBrX=I^PJ+cHbL%jtu+OXdAJ z<@P(w7G>MxzIwTa3VU75{3h4us!t=g8zkRddThd@HUHg$W4|$owyflRux<156p)LVkDrg) zKL^BufKwpR6b2t}2N3=K>}G4nb{WU6)tAF&Zeve6bEhgHe($kG2c`uy&8l2tFr8;M z|HG}_^6Cpsey-C$mGEp!S^LUATgv~Rv(TMia5Kp~yhp=Cr}yV4hLnGQQ-fj}xMWYy zNo-Men0>AIW(I$`%fsub&sHgQEu8wMsl-Sm@Z@CGQ^r@d!n>3_9B9Ai zo2AcItooxE}L3nV{+)k7Twa}StX&M4a;q_!o7Gv$|ZZCT00 ztIIrMPk;0Kzh%X$cuS3n7?2{_v#C(642)oNfqoEfE;3%HUZvsC{D0a;lc%go?^R;^ zLU&0tHu5u6$UJd)>0I|qVfF3HKe8(J@Hf7@BFL~|JKqJH4@WbOc7J)U266+~O@^-> znP+TJJTr|&HT#vyw-iopXQfUbr998PlP66l2pa;`r7#GHon~OrSO{dJxIJmn>32{u z&Vu3sD`OzA1PVaK;B<M7IyJ?Tk+t$^twrfi0$b+{gwUJ#ANAt-5tNb^B@h&7*q z+q+=d#K;M+I1c!{@-RPEA0*J*xc71KO|zn{#XRfyRiW-f&-2L6hUFEwodoL#1_p7h zl})fbiQ#P|0VF2eN?hp$Y(Fq9AB3tz2|FU)WI^Thg6<}e+hO4a&tHSYZIpx;dYmAq zWm4i0942Vx5vXm2QXY{QUbJ%?Qh7v-n|SWX!qOM5+ypPvL3x1aHWrdUK>A<+Tf|wv z?fd!i)ZRUvuNPEZ`y#-_m3%Ayg(2gYwhvCnwWL#TL-nTo2Ld1)W&|UU`wuDyOaJil zf{3;l1N-`84AA!20-!mdwkRCHECC82G2znaVGT-8pm0LZn<(j@I5!=gLt{6A+ztyb ZczH2M+(t=wEd=_D8gYnTUVvg24ghU=mQerz diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410218 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410218 deleted file mode 100644 index 9ace28860431465090334bf3ee74f58bdb37ebc9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4336 zcmZQzfB@l=&L#i3Ik&Bs zkjwVmm^>9^Q_`YSXCO8*FoNi-MVU8~0=|3Zvd@0>!c@rY{r?x2=jgHRU%k;uUi?qG z3{Z)K-de+XZsI4@I`^8Z%`iW|C2yaL_NhOSUz_;uPktKq?owCXnng*T>=u*m|LXhq z^7Y)-37J{O`-+{9O5J%A8@Ggs*Ej57*KxDdUtHPWHT9kgU+E3&x^Kj15;s#=O~OyM z;?to6M{AgV{Ac?(b=K*)HX)|(Z!av++UyzZvG}m%xyTO;qAjaJ!O1VE4)j|<1u^m<82T(F)$E{ zE1)`NAZ7xoZ>p`Y>5)`hGDlLG^{;%cDM z$v_N_Hz17;K;mFIf!d4NIu^zrO?r)T-&dU%xj3UMpCP0u4o3ZNZW3yX&alobX+miQW@`_ZfGwZP8>2002M*P1ExUPXt>N;< z-M3RiuQ5X1;9wck+I~>w@*mCwTc)&glT5<4ez|M-+S4xmriU1lQ-BCpY-0QoZ8^(7 zixPXQBbS}relUFQtO}P^@=Lk4tYqTVWgfAozxn;&vSL-drA7r(T11!+wig&yMjwrA6|(}{Cwh?L2a*Z_c+r zy{p+_etdFOz~WYz^W<{^QDtVIjf5hn{qd?cc&zK`{YS5C!Q5VG8fgTor)nDEN&S*^ zQt0>V4GX8=k;$L+(AqCVhtV)=$61Zou#43N>w)&6q!F-Nfc{Q?E4jHPBBgDU@+wZ1 z?UCoJZ|an)$xhWZpT!-fzs}yxcmB&KZam#T-x(Y||CJ*l#q~PVo|SJunVe|f-*MyM zWIk9Ld3^1OU|~U_9^b=~K_oRYTx?DWjjbD5Jm zgS>PX@~yn1xxnX}`8jnNZ-x?9p!=AANo@Z0^Zpt8SKD8#t}xoxZ?iIpe}_@+v+ON9 z-aYQft^%rI0EZu#0gpon4?=>{88d@u?nRg&!T10wwA$$81`>_-wnVv^xD(%rNl8t)kP zA_*WdNp=$`oL0hg5z~%=rggX;m|hT##UUtZgh=y2dWbcj!DRaup4WGO74f-xp(hq>?W*b%OG(ZCE-Pnx*J{|(V%Wc@jDUbH_IQ4BD?M;x?hM~-oY{# Kkj7F@z-0lx(Grya diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410219 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410219 deleted file mode 100644 index 19b9595b10257c2e37cfcb635b88026e7239a66a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2920 zcmZQzfPi@$tU3Hof9caa;5M=H_U<`$>h>uHLe;e~4-Q4Vn{QhRR3$82(z)b6H|Mrh zwJN8blqc=h+q!TEt2EPJH~SvjxrPsfuauQYtd-ttdUsVy^Ze@6-Hb1%uQ;`{a=YC& zqqVPlW?u%`l(gv7MTm_Ij39bztiYz-wHkg`4_LV`eYkbPN&fKWPX$gwYO|5ZL+Sa`dNS^kf8ZpNALf{>YaBxWa9Yw63D zUQ=K1m$KC5vf$mAOYJN>Y*`cjWS7csI#_9Na_g7>G2t_&+(lry$@x$|A(}z7Wi{`EZJU>;fLzRc{QPpR zJ0KPWoC1laF!*>ofav#UH(NWl%Q$wez8p4l8++23J5>qsdyg$TFfE{IR^<|d={&Rf zA8zfIS6^uIbDjRFglAjI+E@PBQvUy(h3@=}$n0Gx*D09$rs)|;oPI|pf7U~5zYrZp!>k==HDbdqRu`;i;CFHWhQTVJdT^Wp zX%IkWGu$$J+#PDXcEZGCitDq_rs>}NGdn3@x9Lr>)^kn%zbx878f4F=f+!GR1e*(t z+uR+Nr}xZUw(8!Ew5y9hTtB?QQKhqZ$!=FBF1!5nlJXnZH=WxQaLW32-fF8O&-GP8 z=JhD~?%grVba`JI&(V9I*??w&{K4@3-pps`-_)kaI4zR-q;ccYn)N!__F+9m0Y>W4 z9Y*VBf&IX(*a%cQ8HiEb4$=<=AaSsqK<&kB9SdWRCcQ?v@2k#>T%6IB&k#}+a@kHU zw=L|!9GHfjS{C6Y(?k1mtO}#0yUmWtI86Gq)04||>X(uY3Nu^yf$GFP%3i>fGlAU# z^tbf44z^|1Q(RvB{&dkumcQVh?9tZ0ou+XM$}dh|B#||v<6+Z9*>5&w+N$r(mqg5! zD9xGT%uh(xRY)P&r0W-i5M>bJG>&RWx=JEZ+gcYtXrklJElM zD-?hfaY#(K5^%nS^Wbp^2@|w>0u;ZX@B)=VU_dZ`uDjhVe=tfEsCWY~OHY7mgHu=> zgyMH1%+J2=RYZ#U!1RfTZ=%Z$Bt3A)0~uJ{&mfr0SAFwid}VXWwUyCTm7!%`u}+E) zjb}ZmF7f(eJ68>?pQwC{2xo$I1FU>QiW>s%L06Fdnn2$Ao(ZC)`{DBdD zNCHSqxKGK+EAVoUM89L$k0gM^B*Se;X_*)|Y0Ywg#XE+*NCHSqlHG)oCy8#uAo&As LBang3AuxFWuc_>B diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410220 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410220 deleted file mode 100644 index 1d2bdc322dcc7cbc66ba61ea06b83efa983a1056..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2900 zcmZQzfPkW!XIGRXymkcHKWRFuWqLyR=K2Z$U+NzH;OcBt^6gR%P?hk!4b~j~r@!=R z9&npjd3*O9J9YaM1EK2Lm-e^pi)bK1{O$DIDGdBZO=b#lNFw<@+* z4x8K;r%nUel(gv7HHeK2j39bztiYz-wHkg`4_LV`eYkbPN&fKWPd^#Es=H z1$wHLt19J=H7sY+s{XX`K`KLa$O7}FG3ok?C6}z-WcEPUdQB4By|*d`dWGK~h~1lY z?Rik=Rp-YOUL>Sz>2#cF6U{%=6foPk`h7%Uz2HZ8ej5hSmNmQ&wryUX0&+3)@$*Of zV?ZnjI0X_-Ves*G0MYNyZnk!8mvQV`eK~CAHuj`5cd8QN_a0kxU|K-atjZ+@(|Km| zKit|aufEXa=Q{mU3D35awXgiMrTqUn3*GqzH_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ4fsxFc69> zpgJZfX2_{!5neJqv@gf1Fj~6X?3j$hq)$6NxlE^iDcPVfvxT35-^l?O2CISU!Epwp zK>(S}P;y+%YjJ7C3iITnYL`XX`b2wH?B{ta?Y;JX$}8txk%k}*vS(946bLYa%>~Bo zPmVMCx0XFOdEO__n3Y%q+HqDRHtb?` z!Fs44R>9PR^Z)^hAL4H9Pg&eGm8a(QosQKWEw`5i&snh1S-HnpnU�T3i4~1F|2$ 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410224 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410224 deleted file mode 100644 index 72fd61b53c6fd43ec19896a51de4066567bd089e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6136 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410226 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410226 deleted file mode 100644 index b9d6071107a8c88b6d2ee8f322e292fbf45128dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5920 zcmd5=3pA8zAAiS?r5KfnNThFLrDQ~C7nL=JNfMn+#h8dyTd^+nkxEV2-PV%pl-!nf zq>NjfsN9X@ERxPwZXrxkES44D`@H6T_jNl)t#kG{XXg38&-=gp{*6Fv@M{ajX>s4qdl=7;eVdU=PJg>5;6TY$E$6Woo6I7EeF-*4Y;UgA5GF@=J3ic< z?BIN%wwZE3;u+T!S{2BVXvxV;VF1L$@|qrMDxf9@7pyWM3#t*A`>HbyjF}3?!R$KQ z=p0e{;}>Tfjuez7JdCiq5aXM)C30CTsWm%9>GOErlm z({=5B*htp+aikw&jDI+4Z66@6SjBP-TyGTH;PLzz$$MApv!k8AsRYMgdvn2atMuN= zunk=8r|NdWDrT=l55CHo!~{X}fkUyElCw>_#u46&sIWXu6k3o*Wn%GH?llt1)WWBB6#ov4gKV#3{ z4k7Qul4WNpD)*Y6QW$Yo#mzK58~wu7;RA&iEn$8ktNqqi7P}Ey2jRt52W-EqoRK4ZML`4mmuP68G zw&d2mZyh*$TG6?2LVWqi^(!PE?M$Nvrc3iN5kDSH5Wemmb>45RcBt8j`8yHreWkXeEUZ&MIjuZOs=cQ9*JF*aP9b9(3e6fO&>=`H z30j*_b^<>qMyKv>x9MPZ?DZe28&T^G8DM#wN=z(uNUb1*$NjWQDbR21(&K>M|N428 zMTT~j#5HjgA7$VVR@L30ry!7Jmcet(#Tw@t;@c+%H04`}x-- z|1+vMBta!q5LV}I|S`}MpgHI*I!T(KII zH!=u_Uf74UqXFiBji;K&%XPkVrLT=%)pGaFr88?9n`9|b{dWf1nI_e1g{ohK#Af!^ zo^_-*TfQNNRRoqC>1#0~_luv+t6s>hk29*W#EI_8zh#>lD?v)^~g6fAZQgnXqq2>)& zwls?`*NKd_V~2+gc6Jeib{4+ePlt^8_!txLz(FLykE8r|Xs>|185#4Q?+_%jG13Rd zh@A0YGn3$pA`>nSI1=NSD>sOJxR%jYFxO`nyLEdSuPa=7NRQ)@_vC7`uTx8kEqb|{elB4JgNr#< zqx0T3U?Jq3oxF44bs_XDg*hdDg6U^~{RzW_H!_RV2i;TCWtmhn&O$p!j8!#-{SM}7o0TrCTNN&VIZbT6J^7q@^T z7mR62PYV1y59bGF5hi8>(p&n=2c;~|l}lxV63-5a9p0Nsf66F!eWA;v@xm%n;QIqI zX1mLc^WTBogFP0je*yl(f<*6>)bszxN`&RX?_q445k@huC<{ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410227 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410227 deleted file mode 100644 index 8a5b0da8c9b76b221c9811060e3d2546327dcda8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6140 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410229 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410229 deleted file mode 100644 index a46124cc12bf45b481a7e657105b98c3c00a5b74..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4728 zcmZQzfPl<^RcBmo{c+2%bB&mC*M3!VS3g(Q%7158^j@F5;_jIupekX}sPtPWf40>7 zy^ybCb5myjc6F9>#XCi&5B0fx>ca|xp*z3eW|Db$kA{g(@6S&RDgXYa2E{aR$)28* z*rMz(`&#kM4E}PLhu2e|ty1b*IQ30aiIGU)$;qmxjIU~icPV*1W{-Zn4dNyS27+5mn_WhB<|q&aOt8l|JU#g~=Hi`GK~!XzCbhWBhW%)~#;YTK39fBWv*_^`+-; z%uoFE)_>2H*Y`A)UV+qycpCyKBHRLWa|rhnuv#F&2zDPZjp%xOO>$`5YwNMM(&?^q z?y?OFR2Y-rKYelf&v&<#O!?o~)^9oc<+6(U?xq*7t-`f^k1qPxIs00OU*Dal4(?NL z^8w8QhlSy5N9GwD6wgd!QO$m(@-2mv+gYj8M=8%U@8n6-3Bn*prZ5PIon~OrSPrru zh~Xe<(HR{e2PDQ>P+VYTY;0m?36X`V1Jfz~flryrB1kSiVEMUp_SCZf@gmsgn5rht419d#uqRD#3SZa*W%>su@*R*fSC|nFp3{0VX zkP@)km$sM7-vrodhI7^>Gw!h0v^_TYnbp!Y$7cvUIe&jy#too4CdUxp$RLmoC?FJD8&vm4;&`1BR*!GU(j94zk6vO>%>JW1%{vg zTCc0|S9&9KTjQ}u9yH%C(v7{8hSwryjcR{0NFymVXutw|tYj^vL0{gL|Za zz!{rnoA4U(r7VWq8Lghk^O`^{1($k2HY`qIG)Nj04$RQ90mLU=tOu+S zN+25!7Xp^QFf|0rDxgB3`7$J!5Ap-dAGEZG5$Gmz&Bq89BmpEQER-PW6V3z05ePu- zN2`Nje%}IA0XB=um#7pnVWG(ZXIxJ%Qa`2JutBv;EYXy}ho5 z++Vy#*k9Ct_ry61_sH8;E!E}n_&pEmzLft!faGRIAom|s4msSwLPXRZ4D9O?V-gVc`YO1B1kEl!O~@KmrOV0#2h z8vO;8gM~S`?F|wmT#qw|%lM|nKI*jg|ToNCZ72cnGck4w+)sz>4Ulsf1-CO(| z-+t7C#y5H!0-j)i3|Jc72oogOu3}&i*IL;$2UvH2+Ey6p5J>=u2{RpM-3YcnY0;_o zP?acQPNbVGXzV7eX>^ddjgs&}FE@}Qm6SL{Pop5cpn4M2Mg_}(;+DklqMh4FbQ8}V dSy=j_m7CygL{K;p-8Vt<2S^_bV2e1I90271gLnV{ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410230 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410230 deleted file mode 100644 index 4a9b313035e3a49eac9079881c1fedeac6128241..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4808 zcmZQzfPl#F*VU5}q~3G=>1DZfm(lXVSzYm4zv8*vY%}_k0^XhjsuIo&Sartb)*rVF zJJ*OQckNd-clC2+t^9XpMep^=EAE~tdi7oT$n+zj-g*SY=0W0^V54TP@$sgVv>MnC6pn6{u%O#s` zpc02fhVCL}Inyl`$0~xkoJD;EByQbV``0QK4t>8A_?G+57E!%h>Y}>p%1>|DpBbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}*}54TrNX%BV_yHXNnvv>Bc_Ts~ahZy*s9Drf45vCrb z2MECN2VpWWSk}zYOTRRE;j&z%NqJAS^f@KVnWyi4l(+lXw2lZ7_EL~K*|VuI<&0o+ zfpNRK_p0K@*#X9$-*weDi>`fc8k~G7%%iY=X%`bw+LsSsNSdUa$?g-i&J7u z?!7BgN>iCaLtek#;PKzIga>FK*bnalm#n_RxJ755xnAX(9>vD_^`_UJZU1MnG2MjY zx!#3ls2{ch)g!wbnawbAx!XUB_DwDQ_xEQao1NA_iQ8s3 zN%EpOf;moCA1rC}Im*DGF2Eq{m%+doHW6e$$Pq9A%vZ`lHb{)4pt!)w(8RzL$_FU{ zt9@yEx%^Flt!6l9T{7bidrjM8lb=~FU2}Yfz?1X$mu1`ls$+5t@r?`u>41XtQ`eF! zX7QEwZvB<7H#v&K?No`y>+%c%3*&WKGZ$a`3{oahAk_d>&IoZQgM&eT`bEFX6ZP2q zqi61&bT_T#kd5b?EsTGbPTbd&dtu7^44LD)t^HbW%g*R&KehdH@ZKsOW4*Nd;mbaE zdFfeAhR3VnYe(i88x+q>V^Ph1rSdI>liOLT(?==KGw`vTQ71%w6#__%`gf(c^6g()ctrhyUaR)_N1T@N(gC_8?5-;>wUt`PTrMQwYc z$0Ju8%g?a~3|4k=&%HBWMU_R*O^ty)HuSgaHN);di&rXc-%}-cQ83^c8_+JxCx&NSYp!CKJ%+sL! z00u;aGXwkj!!FRUUJlg63e^gxV3q(mNKCj2aQcSxKz4%w)P7*vg_*?&Dr;fth;!4S z2Q+pQ$nCK38f|{bU?qZ&_cFZvp=T1- z7`V;m=aZ}(`Cp%LiL+^D$;!9oXeQi)nu9Hk{({QE!W^DP373xy;xfLeKcHpg1fV&f zas&>LJcY!BOOusGiFDHx8oLQ=8XY8VLrJ3~x(QUzQ6mnqrqSLMmHgSawSUiho}@l! zPiV{C1*Z)&!(y&J{80Jr+VgD@(3Amg^#R$q(hR5^-2_WT1lv*!4B}cVo8|!ZY{X_J zk|jt?nCUq4AlUw-MW?<(RicDBk#4e}v74}_(Lv%iO2P}h+(1q!q{Jb58U^VE)svv~ z1+RBV3@_TbjYK!`+>wQ)FIu??)SiHc6Vd$?V{vNp$}j$sceVfedU8fyn~^u%%Vx diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410231 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410231 deleted file mode 100644 index ea5db50f215e33d5ecb6bdad13cb505218a1b1ca..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4876 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

n) z1u+QfXJB9hsSois0x4&h*xP6C+B%UnqI27&)$>`Nox8ODncI!ztbDn!GPakkjC3pZzP->&^4=QQ(!iH8!BXNm|H z1}V-wDSz&FzgoY+qfJj7RUd_ah?|@DyI<}G7tkzlIu?5gcR@yK%Y^2OXa9Mk&$i|zW|+Wk7`2J>0*-z?fESPvS>bDVp6 z2&e`K7kA2s-kSZ=%$;-{C{=WX#`U&j3Gb8?flU{H`;vJ}+0s=4^mS#ZZ5DdV= z9G*srD90G2E*0;9mW3ewFmu6XB5{zIFzX;?51dD28YR+AAvAUq)-*au+=h}yNpusa zOhbt)Bn}c29&4a*Kn9R95n84orBRSxWI3=|M7RxJFAz~j!rTF?E0Ns`DtqB&2@&>! z(lsm%(Jo9Va}!F~BY75y2@4XOc@3j1aTl$baCRE|x1GV)IS%#S(b%Tau=vKLXFsZL zT))+J{tc+E1_AVP1ldlIyFlR$YA=8R5p_BP)2|N=$a-LUK{T@AP;rzrPNexVB$$si zf78GoO8kKlEJy-KOn6A5q!*wla(+bbo5SL82Q2uh+=hcVYU83kdPl)bWcR|t1T02n zdm(K{?DjHnZaTQ0U%4ir@%x>n%s+Y3e;IF=*dljE?eH}}&SLJQWQd~~QlRZWn41}q z+JDI523Af)AB%x~{UHfxKWi0G6D!mnFa@&&$U$PlRp5$KP`rWSR1B&T7N%e>ac(-e hg~o0Ixg8c>@UnT3xQ&wV0`--t5r-IMGc+#26aZ4)>!|<$ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410234 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410234 deleted file mode 100644 index f72536d0d6d2a3a5ad34db123fc395c917ee2a4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5436 zcmZQzfPj>3*I(?or5KyW5v%mEd3)IHKV`T1Klti)-G9`pcVY2cpeo^-1+_b5ySHc< zR10vV9ox@(h#}zO7m54Z%Vei+RBGL;w*1KR-0yq(tj`_$y2$Lyv=_V?b<&yF-5-ed zUdX<5kevf$Q_`Z-3LqK?7(v9=Sb3?9`f%%nll`ZknvGIVThBGT z5_3Z?a{bE9|2Chk{Gs*tuI5EvW`^wQlQO|;+BfW1;pW=Od3tBTqO(D(+^RJK&xPGl zP;q;&v_wFeXwow@)VGZnU9}; z6}bb%f`C&X(G&(BZwCrW^Q9oI&-HgA%5?%MF*w@G|j49VlbU& zHvhw|-SX-SO@6M^Kb7!oOIiEMKU>QGpR>@NUvM+YJiJH4M5p)XCx(=Ne^Y~E8n|Rn z&q-`ic9?yw_+|!wxy!@rsn1p^buFCwrm4h8B=F>9)lr5*RKdZx?| zIDFsFBE)K+#FWqbntg%lnF2zC0(@M-dcj2cscXp zh4DJAnTxM|1}S28w>|(?473mGR)+-T;OP_hMu&aNRTlUy|GlDP*Pa}v8E4tz9&b_> z{bwEGB{xI6ig}oJ)xO>$vtS|*1HY341Gi!$Q2k^eMhO=n9|l0;U^#)>i`hCB z#vV<2jdI^tofo+{qbr{wq$uRFom_5P*n>GR4d3Sfkw0x_yWO4nf8nXzj%AD-dTQPqi`8li+AT$jl9QG z&TtuT(>f~mX_Mu~lXiEL^FBuBhNm6dHs^tgp6-h$3|g}{I_>$y4RwPCz`jn^=BLE%R_6uzi6!0u3#@J z^05fd*5Cs=3><#7F@CvW>sGgHEqi6Lk+pb|`qJ|^<|lr7>%Zs9>wB6?ufX9a+Oi3# zo*CvDAPo;Mg8CU4*g)z-yp6!xt=&J1&JDHMtX9;m)cVN4c{|IW=QFH(*gt$cX1;X7 z8cU!ej3{K6AawH9d-r^XpBo zJ=^}zVq>}q$8)_4%~1bt1**sDUxNDK{sm=81_mvKIgj7`RS_{;6Jgz@Tk$pIh2p`+ zBYTS~C$~@YHhomS@ioV#A3PmIGp?C^s?+l!}?*Ibx>X5mkj&Bm4~ zO!{)+HdDBROXhB!68Tg*bA6j!-7_QAgQ^dn&YmE6|B@&d&|H?F%MXj&xkT`JxI z)B_4bkbaoCU?Y$?NKBaNkT`<#U~vGoA6PF5K;;-gjV1g&iW$zzbX1e--fTms7yqVpEaQLsFQ>|Rj1CO4jF7bcXs z36%ce0LimROt>_PTA(O$Tp+gr8Tc5LRdQ9b|DALTzHpoIj@TNB?>ApAyt}K7RrI56 zNS+4NMD%h5ZV`|H%@?_sVS)tfZJ+VS~eJ~oyLr6@REUq*Nu|H{1&@re= zlzc&?oBC+%CQw*G!wZo<2Z`G#2`^Cl3k4uW91;_*gxK^6(hEwvp!~cMrkh|pZr!c# zIp1H20@ZH-7H<<^`d~B`2ch_#2=gDPG%}H5J_B-jM0EQSsl0%>9YkYsKf|$08;gt1 zH8gG4_IZ>2{u6U|qk$KT$+O?BA+I;yliqy`sz2pF5Fi=B2;}~Q%E8j*PAH$4ehCBn z`h#1beUsHdeXKCOAR5V?NKCj2TxkGoKd>&5hN?t~8zTBjAU6fj*i9g}!@>(*&kPc` zQ4(ID{v|cy5WPJBj>?mg{8sC}rff{M-V?dU`ScS;Mb{vov?J|XH#~T@sxmSS8sF%3 z4YoGx7N}YVBKjz>HY=!~1Bx@0w1~t(V!}+vRoAqD?FXh&m_n2=C(=!zHY+XNgf)#0 z61PzjUbJhUp|+F2>4AvwBBlQeOD_c5tVGxgO4qP7M7uCSs)NXI6DSSB0a6A-V#1|S M)B;6uwpn310JgjT%>V!Z diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410235 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410235 deleted file mode 100644 index 33a57241a9793962ca7efccf50622e371abd054f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5156 zcmd5=2~ura%)S5byZ`0){kQwwK@i?3ociLe+S#Cw z9aD0Ks4Jqw+NECkC=ArgGq()&?`qq-3vi8Rs{_3=4f3Z*6ShuLqB6Kn4X{w-P+C0sU zTG(MG`Y?M-+GVyVM~oTAvFo|2uDbck%!4moY}eUwPmr{#Wge9c7~Z{hDVn?H;>J9P23u3xS!Wm-Ay>|h{bsgb8v zcPhy?|F>%9l4*$2ov1Sg{hf^XEit; z&6D)1xfkr=UR04%5=-CB4mzP1Hzq-$E%(5b>BkMn{n zG~dcfql4q;0Tu=_R=FBMbg}`B#tq8B;gwQj4Q_Bv+Hb3$v4Mesp&oqji4Wyx*skmn z30y5-@du4u@Zc7WkwpKe#fD;UTs-mci{enILxk>NN4Hvr%A%KbRgK=;*zy69x$igY z#;lTBUgb}D9OyHaVsK{W(bUElkV!;Yz650k_Aso};^eSk)8&O_!9I>})`l9J6b?!@ z1j~l_TXd&otWo;QWzoWj)05Q$YIAHd3|33OJvX20oaa*=|ejQ*74hu<%6`%&8zZIDl0Y@?%55Kg_AJVOGNFum?Hax;su$$Ee&wX}`|h058;b9TbX4z` zx;%O2T$&nEUYhhOebW1vLw@@VZGyXNH8!3IySTQKQF1&9@oaOhaTfnjnpvt{BncFv zd+^hV{?qOEq4Z=`$t4mm1GG)*HC)3(TNT}YiLdFmDQ5vbkiU+V2%>Qe&;r;&tuDrZ z;p@;hBnM00s+4w5!7f#GkKFNUV?*RwR#e5s6wO8YE!GitB8%=Vm;<HZUJi zD}BA$lPgxWew0m-ajm=Qlj*+TXo;-$k_#LOUCmf-x!FEfYjTtQ!Dk0_iaVcD_1i0M z!{Gv2c65?z$T>$I~~F5BYY^oGYywC>mGP?ps(9yU%%oWB`^^ zu#Si46dHrC$|Hyb^ohMCEL#NWAuSW%AARUU&K;H!cgM-sI~x-8O{6uIvK%ZM$^y9k zfJMcE)r0ba3kk&l@-K9pcwcDp$&AEVtRt6Vx5>_rF`a?vk@Oa|MunO+@272KI<{=f z?Mle5j0j+FF*<*1MUV#F>Yb*S9Vdp>@T7rdJ`7XG`f=zd8-%#Bo`8$BhV* zd=4~5{23kt%s!}xsn|0Td?(<1LiA67sibd|G2uA|{q?h9`yPK~13&+W7@|HQoIlX; z4WwQ%bs&w=VwO+beL;7&Wk~Lz>R$iVQe91Q(Z4D-NnbMz;{_C5ZB zy;>kvgb9Ju%2oV=)6Y;kn9(xsfZ!s7a}i<4|H z#^GmUxi6fJVeO+sh;J+vi}gX^oPd7sB?2J<{}Gx5fy9)5-d}jKyxs%;2dL`BI0f=w fNKE#ljOmO0;wQoOJ^q5<>^~xg!}|q4=#cmiV{D=7 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410236 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410236 deleted file mode 100644 index fd88249d7b337d3596e968320c699174dd905710..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5044 zcmZQzfB@m++vooba=M?w{qKskwc*USzm<+$W^vGa;FtM)=K24cKvlxK6+ZuvSzr0@ zvHZsW4(=1%A8~xm;Qw@iuc+|<+lGg24Z+Jlnj5vTeSc8ezWv@T$*bpji^R_d-{8G- zDE9jC>90dTHYF`Otqrk}fe}PsEy}!^6!6_Mmwoo57p6jH@BhEJJV%dh|LToS^5TEe zWq?W?797i&)GGVIlEeLU(v}H6j}|2==5iYT2rZa=xI<=I_-gft`gDQX;9WMCQvaLe zIXyKnd3ZHwgM8D9>$~%|{�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 UNO_eEH=)>##6e<`>?XJd04GlWh5!Hn diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410238 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410238 deleted file mode 100644 index 93c5b7b42fe4fd34580b7b8a0883d2cc61ab6d95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5196 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 CdX4n} diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410242 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410242 deleted file mode 100644 index 340c16c7baa03be9be2cb5f14df96e86946a5d58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6556 zcmd5=3piC-8{X#}IiZm;xlDt^_`{&wa+eg7T%r#3Hzqj|WiYNMCJj2>h{7m3$|X&I z7ue0; zwXW0Eo$*8W`db4(t6eQusahc44ruyV%@JU+ZDE~ zsfT{nN2-;p(j*zF9{Wc0-}fV{b~01UbDhsEGjKZLwAz^Zn$3J;nW1g!ZY|MMmV5(R zVrE^p;aS8eu)hFCy%T>5OhyCF?WxFVNpxj<&rOAd%wICwfRsren* z8LXy5w2djnj>m7EFwM|bF>3!qDEyUghG+Keyaacan56izoQ?k9YoC+|JoMd#Yss8E zbvM1hv%(%9&yG`2eDwuKc3zd(Rch-#2RW5Q$%5Sn9$vWVf7^EVfCjzeh`Z|H8tYEg zP#FgGKacC0LzZlaulJP7(K<7B7L#IM{-~{_@U#a+OslDvHBf{jUaR#6y$5DzYXSRb zzZIG+-zT>`Cbz*QwS>;R@gRJ&+okHP$|H`^$HKCg9G%0N+g=>IU{R*taQ@=j&0R&I z`_7COvBqy2YNmgCmuay6psJom-0%y`@R6 zlwvZId!vJO<~dzUQmNIwqrN^yY1Je8SC2|~m@sTIUjaE9xTsLxV~x{eDr=K?^>;Q( z=UeXBc%(c2(&}KyFfNz1k|M`2c62i%HnGJ zL@Qw8Jq#P$R;cqtt9ln5EQq+Yh>~ujbq(guUK$=FV?Pu& zUvW7tw0A(#O2b%XTs$fcn`>!-i7m_p@=4(0;}YBxZGtO_h+94yhPtbwwW1UTbL!^Z zN{R8u7=eria|PeME^TRqf|B^zmqW2!x09T?sSgwb!uGIBWeirD$GRv9Uk@nre*)xW zR)uyG;#7nMytj*rv~N+|>kZ0LW0~18zZCqO+h6K181Q@x)zqb=s*Vv+$q9Ao6y`pS z*UJ5DomIK`o`>eW){5&~)ISMsSwA`p$6#SHtoiu9^t0DS{ceqVE%vISYLACq+5I)l zdRx_?uezOo;sncmJ*aYUuU~o3@SER7DWz^{BU|;@Eu|kX3~O952LMC-Da%-hG)0@s z*{v(G^OGp4zcwePZ|vUvJryfbY2AmzWe|T->@pw^;}5r>bSFM;lBc7mG*QjSy3u9`Xt5VApLoN zwZW;wc}7SG=%9B4{`hj#D|yImwL)6a9g(FfmYNj{(;rsmsi@zQ*uK!#&y3j@Z|T53 za9&qe?NCc=))OOMcrb6zL#?OsD-JGE?B!BIBbb@m#ht0g4_(N82kZb1gpO6r$JG>G4<^RDs z_q|I@Pnvsypvkx9@p%4CB=m@WdNIng&&*oeg|EZMxj>lwO8+;*m>^d;Uw_8v2ZHTg z_<}W*uT9T~JWfb43YUR72=3{Ngn>zGasIuw&!ebPK>i%aIfn^7q9 zO%}Yi3dge|dAbvIAK8m=b?|k`&t(^fJ~iad3QAmbs;)WR{YBrJvj6t9s3|?g?QI49 zw9Ww2!8k+b5%?;^-Y@69mq2G9Ouk&98N?4fYe9X&XJ`phERcr8>in``mZ;~*KGV;tds2h8r)nSKt^j(-9|F{w zsni!LL#z|SxLIXdzao^-1}Ow4{)3{2=ov5mX9SB~XX8I&Awb~$GvgoPQ>bm$&%gA1 z?uNOAjEUe0tt|*9kSC{luANHE5#PaJE`{Iq$PHqgDo*%*mw+DPNPrqXm3&HIBkxsW Sj_~!2*u}pU{+(UIKK6f0_IW7) diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410243 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410243 deleted file mode 100644 index d224ba2805e671e678a98469cbd21df0e5216615..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4884 zcmZQzfPe!V_zq3bofr0bf%*0fjnDe%$(a zzexI&m&^f;tt=Hw{0|;I$M&#omB5ywLz1<>3M{&+Ux)r&k<2t{KDVD*QO3o2*BNKr zTED!3?9`f%%nll_%z6-|H5=&X%e&Hxzs7H>YHmfw`Q@e^0~x8P8uDM%sHX z`<0&(D{r3=8L})p^u^yGPH{e=Zt2%+KQc%)oi-1UUpk#ZwB<1GgKe9ar+{3{eEfoh z#Rd=y0#0QB=@bSZZwCrW^Q9oI&-HgA%5?%MF*w@G|j49VlbU& zHvhw|-SX-SO@6M^Kb7!oOIiEMKU>QGpR>@NUvM+YJiJH4M5p)XCx(=Ne^Y~E8n|Rn z&q-`ic9?yw_+|!wxy!@rsn1p^buFCwrm4h8B=F>9)l;B*s}#TwrBvY+_*wk%g%P(<%OePnpW& z_>NV3D($#;)iY&&z~TFT79m#qB&K}c*X#?_!W0l16yW0u(F`KfPhCr{n8jDxyY*MT z-sC6_w^Jn+ugfz8ER5G_&0Ku#Ge`}ip)QC50Y<1>9cGIg{+ux-<=nx;w+h|Egr5Ja z7E+&8y-Tj=jBSMWjdWADeBbKFmnUs6dTPD5uOR)w&G&3?K09{bcG>9rv*NTaC(tZ# zxDa$RW7`6twObjOzUP4K2Vyt?=GAl{hh^`aw{n~Ido6i5k7uU7Ny-=3BWz#pn@)eU zNhhad`$Q8^q=NJj8%AJtFKsWEzX`C_4Cky%X53+~X?tw)GpnU*j?WNya{m6Zj2l4n zm>feq{eplD7$7w)p>g6cS$&>je><<#>(#%1>AsHP{989eDN$$I?!Te24o`$0$tUic zkof-5y8J5cdYdozg>3p0L<$e}-#uV$@;O03=K(X&esEa&?#T#mHg7O0bJ{v{UCWBr zweeNrAsXE3`TOrp^)3hejjR0EuONl;(&IXnZAsvlpvO8!ZmP0=E<4l>4pW-t-n~9nHaFl;sa~qefubBS z`@kg&SPuNS^fKd|Y1m4g@~biOt{cx5%vR)#HsN`mKj+T3)hW|2>qG@uK3(R>19TXR z=RK2%nka^OiDo4%jx3vUihhZAi%9-uJ?VT`^2((d_E5)zOIRQqTiAi(fEij2ft3?1 zw_xD{DZ8LnASf)BFoMc@mjEZ#BfMG`<_lI$jU7=m;X zUB4pv18yUbfz2VGcmHkNueL)%`?`OQ!`2#)t1Z&Z z&7IG3=YaU+${3T{)I_FRN_xuvuTS^I-P&X5@@eU*9lxRRO+?!!_cBy11HrZ?P@&bn ztRoDN_U0s*J{XOZFp-!rSzPrZ#Qvm3LC2sfQNo-^Hzm;6O<2?DAaNTd;RWh5pa7(Z zLt?^}fWiS8z{(l)Gz!uSl0#`b6YP7eyLI9Ymy{?_A8@RLaRRbMAT|~Uq4=E$^EXbO UY($FrDD5Gl`$QzPhhTmL0Qx$T-2eap diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410244 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410244 deleted file mode 100644 index 9699ceb49e667d0fa7e3b20a74341d4ff5cb04f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7236 zcmd5=3piET7eANshzNO1Gp3ZTR2U&D3Ps7A zFdC2IuWqO$c~**l425C#InKS`!S%UDzOVg#>~;28kKbNv?X}lE5Hv9?pGki*OO^C{A}jw6|q2%&W;MDJ|Hi_)`5K;1Vdg!&7=eG0v(#e%%8XdCU0Y#N|A^p3?Z& z4|a3cwhk%>UzG6WD|uPnoZGiTH_gsxRV9DeSiWwuzv<1>j~@_nU`xcr>Rl5SAsjqY zkL+(EnhqJcvd6sb*5KFd9q)b`tHf4(>#p7^;ZbKXz@n2;y1UoG$r&R09L!BCkKw}0 zN<}qhzUo>0@mTY)XxNXopAOw0kj1+;?X~f;HpyMuKu&b-&C6QytdnZYyO?K7|5{#JjQJu(!I+&UmRW4vAK_)-xz;o~8tPQ#WV!FY% zo?2bWW+&FX^4<4KVMSs|lqosdCrRZJcMNaq{g7313HYz+%2oS1?;Qyfnk`m!2G=|nfpSlW86&z^ZZV_r4F=tEU5ePzG3$n87k&U}tOM=EJLoaeeg#gASdCqMTM~+f6Od4PHK(gzM_mL z4H%TO4YX)ndYifSkmrLiyd&U&kqW&4FauE6>bUseq+wh9ppmhcd5au@OV_(QB-M_?#4AX8>m^m!>dvsiGoBjoJ*5jGahO($Uj@ww$L z{VnhV$I3XJdj}{NH(mX?dNBSs>Z8IJ9KywMT2ePD-YsgzhAk1%~Bn|t(jBkIDObb2Gka!wTNDcNf_ zR^u=ENaSSQlSSfd&qP_dH*R@^+Qd!71Jc3$t!eq^m`5!^?1+O>9K{B&r=(g@;(*6p zI(wrm=ys)IJ@vejzFMc9W=s3t15krvVAKcut`jS}T1N-SzTsRz9}@dgiq$IWQ(}7SBvld9wikQl?GXVsTakc@PRUNhf~Z7 z$$3&|OQg1EdMDiUO=s2Edcr*S3}1)S5`rK1*zzx>+&~+$2h|U{te5Xd)I=?kIrv_B zki^NgMKZ`l@>io@D=RfF2&e)+60}UH8iM5S0D4mX5)*6u&^1ms4>v7!bv!{GETC)j znThkyVrpq_YGQ!MBLA2<*U;stQ>X+E#i~>Adhyw$fWSzLYH+3ruqAe-@j5z%Mkx`O9g1Y9!Wj$OBGlA`_9R~vt^qK`8$BViC#*F;myBImvXOwbLEQanfYr8RKvaN#{UfNPnVXMeTz-Oapy~gZmX4A9I+b za~BvBjGgAM?+x30`~}~C-y(*HPcXeP625GF$tY#${9FE%TOL@*7^=X!rP8^Ls%J_EIczXGRc3;>FQ+UxC;&N~F=AB?H(%@2v@IG$S)Wo+;N5`=I9RKrv6sf95b1mn<+Q zhG+jf!8RX%!F}hqh~dYcPTCVD+^Z-BZ!(LZ8RazX=U0YlQXPxPnsCt7cV+9$@1dZ6zn z`J72igayXLFh{==Z1eHg)VhJKhs?(it*w~e29}+6r_Iq{3x^GJ7chg#z;7@I^P35i z!jtZg`S@$noyPQUDa<~wpcnx;UpH``U?`B~0Gax2`62WSk79EKw5hmgu+dO+Sb4uE zDnEO@!1S53ep3WWk#7JPnOSg7%4O#ClW6@0Ry9I1@B_nhya0CQvyW-8F$)-yzTK(k z1U`54e46b{&T;S4>|+)%pl;USW+&Ww@ylHO5nds-E(ChD$l6T97f;4v7hwYkBM(q zLh@m@;JXL%o%|4xr+r67RAO*qNYonojfwt5yTj4*JyV~_B&O&E#>DXKe<#=`)r&b| Oa{WdzWY}lYV*M`yQN#}b diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410245 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410245 deleted file mode 100644 index c05484d31ab4add8d50459bd2e8149954cffbf0f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7088 zcmd5>2|QHWAHOrGXrb&8@hC~z3n4<0Z7_IxmN1q!Z?Zq0CB3LAC1ii8(JGV>m90pY z5VA)2S40b?r_l1h_s+fZ?ud`^%IDMhe8xHVoZs^OE$4U6y#RpLbg4d-V@_|Wp2Ux1 zi~4IoK2m*w} z@tZM2MMe|cEZtpy$}3!=s&cs7j80VVlDQ+X$6E8W|3HvM?HBuy+jqUYDeWSM`W{vZ znw8#rHm(Tp{&jD0&yAdmBnUCf{f7dYQqYLjs@=h!1Q;1=LH$r|&L-^$VQtElCac67 z^PL`a2JCbws-Tv}5yF!GI42jsD4DZ2@2KeNvr1!Gvei5La(yE%f6DdyTBouumE7*M z(fXK_l1${-JAm}%vx~WHD94JL6vw}Q(MoRdYIo=K)9r0_=}Q-E?kyJVZ~4dr91QGNw!C68BDG)MEo~4)Jr&)Rd)r zw>0;h3pct40IRtGZWDI^XB7*Q!*PT7pa#ZsKiENY3&>t%HF+gDW#k#`XtVlv z9SgQrd#k+kbLUb~%#uz_YZ-ya1jx%n5po<2C!m!gyfw^7^?m-1PwWcLD~09CI|Yo( ziidkuisZV3lk%y-xxs7W#5*>=4w5R0E#cTjaZ~NMnNcYD_ldg34m_RZCOIU{b_e)f{9WXp5HgcQsNnjkX#_;4sfj)Sp)dt_;Q<4P-| zP(CSJ9iF!?uJ6tzi|tt^c_=C*yRC$DakoE@+l9TGUktRBW$6m_cNaQ(Oq@3?iv4W( zEYE9nSX+JxBo>k>S0AR~K9rf`a&=zWvevjtUiSW^6tNosWu55P3THY0`#W!#2bWM{p*!j`y$T>J%P~;_Q`W8gAAo3x-o~I#!@;gBEBsOV^>ds)OA2~y8(aT#&d`+xl9?PUT|zH)q1XxfKz(=@zIrz(rsfb^}_$9|8L8guXLrBo!AR)`+a^|vYIt+bj@R!^zD zZd+qs6h?NlXFin2u>Xee*LNPZPTo)8y%iWjPJiD^G5K{{{kuc1_a2@Jx6Kqn za-a;QM+~C-Yes%r?ZFWZS^;kD|97 z3h@E&7HuKi3YOt?D&;6{2qX#M;;R7wscdk}kS|Wu2cgIuJ5>J^6_r#JzzJlI-jhttp5y)OmsA8A zh!;F*HXx-E%DPQdh-$?Gu_MI?jX^>@!P1;yqzAFk8Z$KzNQetX$Z-f(EkPV^t82uX zi%-}BjxB6TxwQU30JX{7kr%-7MsHz|2g)r}CMpj*@y!}Y1m`ZDm{ykoR6lr5Ss-gTaE_i$ z%wh5>m}5*3cGO=#8@BK9*W{S|*BHV+nRIuA>J=1UXbsK>CSu8a&4Go3fBvmK*qnp% z`oG3$?tJonn6UQ!s5k**qKi$fmY1=XBt=yk-dI)8X8Tzk(Cy(!Fj|@G=6(soB>+CN_i>?l6X8hIER_@j>$p)<0bxVG@(= z9Akp8qyD1j=TCx-p^nTF6LjwWh#0~?!PF5r=nBlB3q315l$n^Uj#typ1sX&5rJ2q1e+1TBC*Tun7tVX$g*i9krxrwwx zN^MtK{|@8mwFo~Gdvad~s%ysgLXdEifZ8BHdpC?m`OqHYFw78Lo$mfg)9*wze~C~s z-%aU3hX(%#OwNaQ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410246 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410246 deleted file mode 100644 index 2e785b821951e458e92323ab3f4ca960c8de246b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7160 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410248 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410248 deleted file mode 100644 index f074d88542ddb802ddf9d1a82b6523417b262da1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5928 zcmd5=3piEj8eSW&HNKJCTj3Ugq z49O+u86?%tEh;fQ$St>Fv`0dHSDc@Bdrh|6Shi`~GjOe=P)I zkD1mQJyXqrn@^jM89R?m&0Y=U@Q=}m#$ zewmbs%Idt?f3iINRc%gAwnPydZ0lcDGRiiDWd3eRHxyN2)tl|JPFB_&th1e34?R&+ zYn|905dxf>Je*po1OeTI;o6%?>suZCiaeJAaR?)Vt%-KtzA_DZPh7R_f*+ zLMa8&FIb~Og!APYk_Rnq&(*nKUsuGEc^z(140b{AceRY=i%o4dWnuD{UxyUcfm z^x1lyt=HXJsXGpbD{87-emjQHW~Lo1YyyR3YLkwIe4(G%?AhulN;m5M)uBI&6qXV_ z66B@6!Mr+Bu12Ft`EVG?xR>~CZ!sGaf-F@lAh!zq_@I0xrHwIF`ELi$s1%=yEl0Jt zOKTZ4ymIStoA_j>p2aSM?=IUYY{}T5YTh5Q&a0AFrJ{Br<<;ZhNkvT zz`%Iinf8r&A0PebT85tqspLhI3GF)he&+!lif;MFsPXb(Ga$%EK4?KU-3wLN<9JQ8 zy-&j0)0c}TUAILZS@Vw?FP&k!qnM6Hu5$d%=5Z*+mz%E7mPQB|R(eJ_8$+ut1RqSP+tGBImVpyy0HSIN--2d(ynQi(9Yop5K1G-3uj+f-B5Y-dq^NT!( zHSfas(7bdcT+=CcIdRG`GjNkm?tQOjt3Rp&Jk3L^ZCYL039cbnZ65)7d`|E*(jlDJ z{-mi%!T9=wN__W7np)l1h?aYg@CBPB%U^t4tsr*3NoF9N!62Y^L2Z+Iw^wBfSw!w> zDBtE}qD2T|#5Q<`ju$HPnOv?H_WvxW#FI!dGU_}sAmdXjNb^t>$&$KC889ki3{h*s zc0pTc3?!LA%G!x5uR~O~x*Q8`_RTt>;(hPG623;4OkearW0W9qF@a!zGN5nl2X)Z_ z?l(ag$%$zVyxu1uTTB)goz7r-xYjjk)lZF)kBBoPGVQWdYU2GPKIiD5_CYL#KOqm~ zf;E_?IjQp6w`A%4kr9Y<{9}qNBh4|cod5KUR>Ht0we`tTpM?gfs0XWkx!PtTE<{SRPfc3qyr-!v?Skn;1W^z}#1A+k zghR1Fhl3e~gT(@QYv4Xr(^S>M$)K`ByBg^u(1d)ArnXi&Ids-&do91xe|yp)MWC2^mkWtOoX036i@s-hP!W z>08{#-0vZ~vy@OQHhWvq+)QD#$IYU<3-d{v1q0BWd=S9pB(?_g$`9%f_b@t^&M_h- zZDxN1%X17+62Vl_iQ@!Z;7G0nT1V~`^bH5*AJhnaN&pT4@T$-?@W&%2y%oj;v2*hE zv0?j=e8FD%D{6@51b0S3%NO?i;1|chk;}#e?}vZp ziP!T3PKWCe5Q_o1IAJL%rqN9OE#ZtoU(wAlo4?nprHBssYlL2teQixt!u03&; z$<5?)dK12n&FRf$#=gKmj)5bWjg1=v4|pK{QjUkE&Zi<7Wt*@!#sv&0 zgX=FI?5nrAPMlnZePk^&y!1a_VH%7J5}j4glBDuCzwGbq$@zZjhtp2?hjf`(Z1Z~( zW1|zN7awq#OTL?eUR2Hug1rLYPZxxZW8g@Rb@rK@d-dm84)qVt0hZX>oZRt7fd4UGMxRjQkdpE z!+HL`gNuV(AwVovxGU+S5ej|DAx}g}rQLp814{kG>e4ID>RK6O#wt;$2QdA`-u-5< zH5mJxb2Sg&fe>O|c@*|7r~v50?_k`4!-*rg%ky*1)4Ly!m{eC76U5HR7x!%Tkzkv1 VzgQ+F_&x8hsNwwk1-Iys_#eRif7Sp1 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410249 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410249 deleted file mode 100644 index a96bed5f947cedbdfe496f9a79a7f26eace8f5b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5132 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 U8ztcdYGYF)4l&XvJl25>01i*~Z~y=R diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410251 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410251 deleted file mode 100644 index 3b0f0f86fcfd1fd235a4126d0e0828aad80f0274..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3844 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 Yo*N`?qa?gQ{WEIBAx8Ry#wC~n0IpW#!2kdN diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410253 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410253 deleted file mode 100644 index dfb813015cb042c177467e0afd9cfbd2bdf05442..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5088 zcmZQzfPifV>i=}!`|?>Fd2%9;F(pA}p+d^i-n7>%-v7MgJ zf7#!e&S-sq{p$V}rs)C){nqF;%AFQ?*8W7m)7H`Gsp>w##%DU!0Mh~ z*-z%Mx}}3`N?LTf8Db*?BZ%G_E3j#It%l##16J-!A8ws+l0UpT)LrICK=r;RmP zKqU_HrW|{OJ=V-f;G8~L=;h?%#?os~HRkLRaJ-tH{C%5sm^te+@&b^bHW zh11_{S7G#7XEJ|jLEMjjw{>~a%{~<>ls6}tte^C5&Uckqoz0s+Xzsi8JkgOg?XzCi zl%Qhw5+}!Fg4x0|+D;kB-qB{sm_Chl`|EV2f4k}3rKX#_ey!JzzF9&`%lkUa5BB}_(_@d`_h>=>>ghbQ z`5$iWmRDbB@^hX3sf1@+%Gy`{*;4-hoQ3ZEf}2U^;XN8AI=w$XF{J$an;I0;z$JTn zPGXC)!|ZFtH#7LlT^?RfeYQ%eYvI&4O(jMmfhQ-co-)3w72c)f@t8gO@ivH?7#Ik} z6;K@u5Ho?)EAg%W@}qlW{NmaCGw&!AR<^cm7dLmY>kd<5J6*UZ|2YG{lLIge4gr-i zgVaL-IR0Q92QO=nOTUEpPEURtvr8&j_RWW;i?$yQ3ZM6KPZ4cBe*~mX_G~IlIV0Fy zVBD6fZIP~9x#D1>&?@unBFmpXh6@+&shhki{+p17`sL`gFWEx3FMc|4nt>$0d-&Q0 z_qFS_1>wt~dFq%-6i5&Edcf0)lV2qdPJMsJDr)cJbw@afA6@p=*5`%@+ximi>Q z+WXqp^5`Dp%iJllHfAyv>-qSB27<%V@U@!pZHd)aj#?=b3l% zr0E1 zOb`(+49xD<2cS9_p>B2fdswvOU}9&{UuK2eGw~-4Ogx_bib%;=e<9Yi|MJ8;e5|Mc z2Nk4O+Ai6us$8WvBlqgd>)v+^dYca_*ghAtc?I$xI9zTXwD{5#|L^Yv4doBzNpA}J zHgeT}IAEaid%@B)melUI(0p-oz5_5w;GWD*hTQ0Y7kw73&iMU7E3$R)s!3cH(Fdcm7IMAGXRxmfdX!(j1 zg_7AzUpzNSj*>{@VZV28i@{p)q_dgt^|!Y@PybWe?wvE~ll<2g0{>bA{m&_%{>-Y+ z$O$xyCFS>bUYi@!e!MUcZRI(3@pMA=;*T-GUNP0L<=*ywDUpX-n(`kAKz2js=J%)oNv43y76M7m;NU%zJoG;Qqw>SKlJ1<^1|P?>NQ;5dTwU~vGoADGAMpmL0$ z@*Jj)NH^Kg*i9g}!@_H@xs8(W0+kQch(mCgAhjL9(YJBUa*h3V71hqOql7Aasw9MMh*iBf|=pb<$CE*1st5M<#iG#$1#~LUc zkO8Euhn6WwX%wUvSq^L#5#dEj`44jktR6siFR1K=mnB5l3rg3pG(@{Fq0CJvVUOfl zBql6KaOO3PvgC^lLu@KX?jkndSAWuO%u_kZd}ikFRCzY@Ck<|;fgZt76Vb~NxJ5t) z$X%fDJ`NKk*ro>7-8^?>Er5CsLG{D5f@vfVAu-`fP{IKy3QK2T`+;c?;Z~&ZCeBSV zS7_`eklSJ51y7%Y#BC_$2#Icj!~r~tPzn`N;t)N3g7kvSM#;~E#%-jfFR}N0+hOU8 zR^i2fT<#IwFGcbPQWTFVrkBg_OXMm~a)i$_udl!14lC m#-XHtBHaWW8=|S3u$C8t#BG#>7ib)U8gYnTUce(A!~g(J;GgCI diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410254 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410254 deleted file mode 100644 index 5338e11b07a31e897a02b63ba0122a862daca410..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4932 zcmZQzfB>E4$2wiT{&%19uCbXr=OxqD9ShC2U9^t;tWbCD*yK4GKvlxq4AlSWy!Yj^ zIP&B~9%D*^%tD2frM+pdSG@n5m$y>UtnISwm52?D>X8rmCkoAd>t!~jZ*9kAzF(YP zuC1J5c6UKGB`rGL0kM&R5ky}t%DkBr@ZB?)efFalrb1@#|G&6AM~`j)>WxnF;(yX* zfJz(`vhGJ*;|W{5=HCa?6+cdTJStGToOeUx+hzIVuUs^y|6F+4k)uO@kzJ(W-5dM% zv%LOxCSWF8>e(Y4pX*MqzM8|OapSkq8C8L0*J?J%KdJt$(H-IPE+#U^rf~1+ZJF)` z+k$HIowYo6ES>y!-HaLE?lkiMSh?wlW9ardjB2Z-+72r+h_;;LeXwow@)VGZnU7zP z@C7lT;1noqQW$)^9YC~##%B*LHQnU(YrS^#%@SH#-q&G%u-Pv@D< z|8Q%!y!t|upX>BbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}R5o738db^%i818FCo6uli$Ydl1i3+^P%aY?T3TH=e^uhL|e}vVc>Ui0EWR?pi*$0 zf%E_Yhz;fw)X%`c22va1Z3xoGAh7MbuV+cpU5)uo=Uw-eTBhGZeD(>*QFZbWv z?OXsNV3D($#; z)iY&&z~TFT79m#qB&K}c*X+x{*tP&@?N$b+@A)A6ffx>e>30#3!*a*ZS$9@fiY7&fi~_aRX>3lVgadOAwF&1L>!(C0ESiEA8F-D_?JN6o=cX5{uX683Go@ z>$GMrzV;cYPNJyh7EC!K*nhyV?3nBrYjE((!Tm8$n7Dujg8g9l+L3w2 z2E{YeSX8rLseDV}G_(WWp6h@EC&(AWiJqqse3(U}cUG0uYG z0xM%njqt-a#?49V6!v~NVs|dPi@&llv7-CNgzL{< zh@J!akHtmO!X{32R=59w`+loM;+l>s&n~%te8E}!wxF~LpR3)WcBcFX0w5a}CX7Jt zFQ^~z zR(v}A%n7xgvR&Woa`!)o*m7zMG>);Q(SJ}mSeS$AHZUNfEMQs(;_iRX^21yB#D-bGJy z$d=%;4`vmJhSb|o`+<2d6Do-k=ES*4<_eA7gf)#061PzjUdU}HaPT8V91;^6GnjD> zi$nA@3epRb1Enu`y+dMn(avq8xk>Cj-*#B~qLrH%ki&`Sb}N!U;4uVbV2e1useJ2A zPfOj~QrU66)jewGTD#C01>4>ixQBTQ#$0nPhU!NzFF^e(7y#t~ly(;p{WD1W8roKZ z83d!TxDzEU6K8(P9AMbN>sgrDAR3GLSo1Lr?4iUT7|uo#Kw`puie3&A%9F6P1~!Wb zzmw9xhB*q>&qj7HN?AaJy}+~sk6UDOz;+-BATeRqW7JK!!vrZ0k>Ms3yOB6ZOp@IM G*8l*&y`n4t diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410255 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410255 deleted file mode 100644 index b089316285603351a4aa78543a163cac98fd3f3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6208 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410256 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410256 deleted file mode 100644 index 0f076b8752f11f1c11b1056a874c1de967f62eb4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2864 zcmZQzfB>m&RVjXa8>R)g#0d00RM!suK2mviD@sxAV@g zZm_#cIQ$4JJ)ojt_?K~|(sQlodzHbVXX1U=uQYeKB)Iz2hyF?y2j+P0wbBB;mH!&A zrDQ8wrGacpT6B6c#6|{25WPa>ti+zVrEI>Mv!bKdEU`YkO5Z5cLe#cLfA-eXxgU&y zN*pwge%Lkj0PmbIo6emK=6=f`AKDb=m$TJlYxb<<(5(tQ59_@46x1xfT)F6c$exlT zChN_sLf3BCymybz*RU@6P_~5^8NId~E{T1?F7Vj-Gy4VSofog=e3--5zQQfyiR`wP z&pG>Foaqv|cX^j*pxyG@j~EP2`W^SGdv&j#eV2XuqkIO@mJ7TOwryUX0&+3)@e9&1 ze?Tk-Pv@D< z|8Q%!y!t|upX>BbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+})|^&mzQXpTv~U`3x|g<>%ijdpYKC*xB{S}@*R(x0`I*(yHOFTNJUM@VS;h^Zc}$KWo-RQ^1`MR1 zx|Uoqi?6hI>#uyh$x$3`r%EhdmuCoA7_ZZsx%k>=pgM`7np-gCj9~u((^$f^m(wLS zI9-1$e{-|8N37D7kn2kNtNuGZ-|(gC(runQ)>D*zYxXcSbqd_{kt%p-{p3ui({stB z&)VEx?+wq%a{~YH0)a7QYpx%uWZ-vl z0H&vNF!dlkKmZRLg8CU4*g)z-yp6yb_3r*#^~uubMv-mA+9jL#&xLwNEPY`WbKg$X z`{^;L4>nNRw!#f00pK4zB6__yq+>q?h@YEl-je+;ylr6EfY0&h3>s65;BRsVo`v)&~yt(ava`c9~mz42w%HL#`Nv;$#*Nr?NQOmLVn1HD08)`o=E%!p@7(sa+rj95#oxb~r#%_Y;S5SBjHn&j{ zULb#=#1#?;i3y7hoN)*a6SVpT7QY}luvtXJB`J9e<|tSmLv}AE@kF~YAwTWF(jUyv z$dQKZZeHLyBPu=dKeAvUtqG>Wo)Kot%7iM)CT6?ltLE&x2pd$a{ee z0hS~GfdI&c*~JLt{(@QnvJ0ggA)<~E)0h~Y2Q&^;uN46G^Fqx6Q!q<_93&=O1unmV t%Q0N#2vKh8zXGfqXzC_dJfft}LE<))a)d-Tf!Ze2h(q*p1Rm)i1_1ji>u>-7 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410257 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410257 deleted file mode 100644 index 6ae2724bdc3b942b7c5f468073e91103d6bd2392..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3892 zcmZQzfPl#Br6Ip3H*1~9eqmuWC%Td4U|Y&n-RIvf8i;UTu-@nnR3$96tt!QjZ^N_z zml%QGhYH)LI0~8-sT`Dy^xAl4|HIey8#;UEhW39KEPi$Ay72ca@}Cy)9di7zU8Ul^ z*83e#-<}8Al(gvdOo)vPj39bztiYz-wHkg`4_LV`eYkbPN&fKWP_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ4fsxFc69> zpgLwCW&)`%(3sd78~lIS0!emfwlAfs))y3Gx|$yd#Y)dO^VaWZ8Bht(=V2!q7!>CN z+2D8s=>q{^e8TL6iSq)58AH9n(hMA`FLpFY$|h_TmJw*=EAw#>va?XOHM;NHbE9|f z9dlivs(mi6L_z6`7{gH;NpTO!c0j|(!SaL8d8s(2%Irkbc13wdCPE zo|*b4DPLTVuzk62I{ncmot%>G6HQWqYC!tHdI{PGRrk{Na`~G8Tg`CJx@5*3_L{cG zCO@-Uy5{%{fhXthFUz^$K#M3Vb$bf+%c% z3*&WKGZ$a`3{uBv2ucQEzzFsqFwbw^bv$N;hU2m82YRQ=tmr<UAhRLk zL2LG`X@2~in?)l}>H0Jq{tMia2Cqc}82FtW7`PQ1f$Ap%F-lkh`7i(vD}maJ**X@+ z9!+|Ua^F{-7r8j2E1w~xDCDx8Ty9&~gE=q_*8evzj-B|g&f-d&$H6e+IXj~xYn7*d zH@s*wYxS@8eL_HW;vQu$V9J@mZUOrHjmKZZ%kQ1JRVFd*zZIR4MEAD!Ve?m8K zFFSgCnYI5>QSCsj-oM?-O^&`CH&mAF_@DeqN%6<)k34Noxr`P|r%X3xxL$01mL*~J zUzLjO4V*xSF*nBi*!w{KpZDGmUf%*wN9#nM^p;Ibw!0`Rm(syK7b0Xe&ppg@J{XN;2@(?~3yE_$4^qZ4!0b<26m$V9#|SFZ zVd{uM3|qwORtI)^MUCT5#L0Y8%TO!ZU@m=+|OWs z_pr~)_czn`S6c+^y|T*WcyCa&nAvGf_zGyt|Am4i5@Mh_P5*%`Y~t@Pi%Urnjv!8hbvgx@J& z;)13O^fbx~Or!sxaD?UKmw)24IfXWj%KuVZMOt>_zGzzvK=)X9q hN|Z1s(oH=yb`#b#I!N3`NqB+!C)9{TtZ5V$^8hJ|*@*xE diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410258 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410258 deleted file mode 100644 index 75f90f17a7039dd3d7d765d69d3bfbe6af34cba7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1528 zcmZQzfPj}-JHJoyI@U3Ttv5b)xA4W?8}=UiAJmY<#}Sy^cj)RCpeo_W>!l&TCpT-I z$bMmAG$*=|{V9|pL_huT&yrU+^%!d{9`k_&W8kY zUlb}7ZvPLmDQVH^`4Af!7(w*aqRg8~0pC4y*=Ij`VJc+y{{M^1bM)Buuioe+Fa9T8 z2B^eAd_&C_UQwkOx&2Qad&B&1U&{;+%(XY)U%oke+tjd~=}+GDi`&oDUJ!pF+;fLzRc{DRDr z6Cf4@oXP;wDGWZ|4j@`V*!R~@k3D+dqXqe^r}NC_ zf4H?H#YbvCdgO^eSl({6_eYAS%>;8>tO=3#<>nT6;_j7%P?hk@texMdcpdAQ z!qyufyIc6;?hSj7{SRtL;^PQR?mKjK3)A2K;gK^O6Q{k}(cWJz%J*XN(~WI4{}ZP+ z$27c5K41v4DQVH^B@i1K7(w*bSb3?9`f%%nll zC;6@}l*hA)MeVlUyT5sF{k-36FON8Oh_+njeXwow@)VGZnU7zP z|HJ`eLBOdDAf3YCeb((=9z^MieV{q)$Q_dQyWzj`{) zZ2pH^yXDmvn*3a+e=6bGma_Jhf3}qWKWCvkzu;z)d3cY8iB9j&PYfyl{-y@SG;qnD zo|D+3>@fRU@y!hWa+inKQ=hF;>RLGUO;d@HNZ`rIs;7*vYK3bH6_YL6g;HzN_3yy)5YM_~EDeukt(GK=VNUVMy^0e9BZF$9JsSQ)$P&tDY(I0}kK!vk0-; zCo$#ozGhzr#$9@fiY7 z&fi~_aRX=`lVgadUl5Q11L>!(C0ESiEA8F-D_?JN6o=cX5{uX683Go@>$GMrzV;cU zj?qvTrkoM%KVVq;6!2DFU1hVU@5yR0YrdDiRNBt#ci22&R(-$y@BT}xlm6@qUpCRQ zW!v`~0>9sVcyc1${0--`ppWmrN8S9se{(h$&_JeDXZhb>3P1RG&V|($JJ%oeix0W2 zDsXB4k*M_4ZYJkBK4441VF_VD9RbT1;vS`m2~aUmyf8!44_Js`dShS^_b7Y8fUE~< z7MMbbG>8NvD38L_5M@5deULN_(*~!pm=E#;%pbJ0hZ28aI2%a-i3#^6$Un${8O(u( zBU(Al0+a*AA-R6XupdbPi3xWOv2KH_BO=a0?mq#ufl&Fxvuv-??ffs&dYN_lug`oG5@BjwKV5b5FU=VXmZxrdFoWgyFR41P z-++1YKM)|fnGwkS1(rjk;Y(0H;qsD!K}@61_Y}~3pfa-7eph40}>Oi0vVMj!nDyGqMT#LqT0=XT8QPTY&aT_J!1*-R`5r^Qi0I8gSMkRy- E08psyga7~l diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410260 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410260 deleted file mode 100644 index d04eb76a42d14dba7466dfcb084f9e61c6d99cae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2736 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 Khgj1nEam~_hjCB< diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410263 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410263 deleted file mode 100644 index 8effaa7aec85513ba74c93f6630cdc5e40ca403f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2428 zcmZQzfPmf)qVI0%GVbk6-6*yB^o|ZW*SB`Y8U~Gf((da1D9cj;suB*p5xS`U$EC}9 zduE;&I%vyOZ4;q-)Jx#N^2)SC$*U`RjJ^miUBCPPo`WWD?=`Gy6MImi*!nXg(Ufyp z^i;z*#VH`0k`|rb39*rZ5kzl|71*@9R>SY=0W0^V54TP@$sgVv>MnC6pn6{u%O#s` zpc02A`!|>TiCOBm{-dhKg3i6`kK}PWb>UQzA8rDx}K^}G!0nEREy^KNPH`TTC$ws#M*vI9al zYVZ10kfXPhf7Z-Lv9q0I&TN-tb4W{9UH0VD3&!lJx%LdAE!TM;Y}>p%1>|Dp;};ZI zKny52l>ua=F!*>ofM^Ac&mLN8y2ufzOc-(NpH_UL_&7UZv<&NG|; z;nr?>^@S!s*Xf^1c($diedV7m<^RuF=*};=nPeW`qhX@c`|}e+%D=yXpZ(%!AV^7ST1ak!l-v3OmcAz)#=PHX1kYo9?% zB?_b(Kr|3ALfpyVaGK-5okt%6?lOrdY-=jaoXo$T_5CehW0}d%qO%GkiwErpZY zS*g=UDbF+S|nkCfABh^s*%5A|1HoZ)T%rh*pyX`AYeE%x9$S+D3ukywG2_KT^ED3cE8-&y)c2|< zHgiJlO!*H4KsGE)7=hehP&rW8G6VC|H7K8mw8tPWL3td?Ce}@`JV7fr!SW|4yat=wCU8%SzwA5X_QDedC=HRSkveraT`h+CDBcwGMyT6h&7GE(gpzfvTPav diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410264 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410264 deleted file mode 100644 index b1e18e3af2857bec570265a0680f14b85518f5c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3588 zcmZQzfB=6hp0tx$s%I{_thM!WS=-)zxzgwRSJhCzr&d$;DB4v6RSEZg5Pf%3mvL`r z>PD%}r+0M7xxTeC)-Y(?lXh44M_HcA)Bg^S_g;@t3py|49DKCxeBfMzz9@ynzb`^v zxt9swtOD7TwCMCch>Z-4AbN$$S&2P!OWAxiXGKS^Sz>*7mA+A?g{Wd{>x1jpH7?khzop4x1PH7cioka zS=KvmFaBWVcFttQtdJ{{_H&+KtXs#i|H7dS+I1q^^(yLD{C|=;W1=h%Q{t(=YBA~4 zb^hl5nBElpCZT?Vtl{(KFukkUiw*4dz09`TYC6?oXAgsD%MIQK+cqyx0lAp@_yq-4 z9S{owPGtb;6b2t}2N12G@!3O5O*eV{TCW{_vxJtG_jQ;b?ECAd#~!`!(SrQd(|Km| zKit|aufEXa=Q{mU3D35awXgiMrTqUn3*GqzH_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ4fsxFc69> zpgLwCW&)|dTWY#*YL>^Ojs7og+Mjy1`E&Zl@;FsB{Ue*3q>X}JUS?oyTLlb*BS58~ zI0MHUNFN9QowRmE6Alca|>O+>v%Y z>{-Xtl7IF8Apm9u*gjyKt8Q%(nH=V-8glMJ%<*1^rAeNv*8C4HcHerj$l>dq2Tq=a zch}u}H&K#NrepU8uDOzG^UaM9D_XzrywZ}(JM#`d&^)l;x~9DjF`qd3uJa1vgpdih zJ%zcHmLxBlBbeiK^}&)hpQ8*6>H-YHei;mmVY7jH(EWA;D#lSzTwrBrVql7-2CVj_ z?d9?}0k)dqoOQ{JJM1-Wk4=7NwRFw#83Iqv-(Qvi($C}=;u{$RWWYfBscXph4DJAnTxM|2C0)MkZORcXN0&Dm=6Tl4soq6sy_aqWlO^r z?b}m|d8%%(bFntqByoLvlDg}H^mf*TrxrV0{jsy(_hi0*>DLz-DVH0nk`A9Oocs7Q z$i3h&F?{XFJY$36nQ1Jl*{@W-rEqdPD|PxP<$30vJZU;X*bt~Lg+V~JwWrdKdsjVE<_8?U?`IKWwNGNo=Y7q- zK!r>Jp+NyYt{^QCKuowW{rb=VRn7=?tAhd4?+M$ocWkb<$^2ZWa{HD-FW=l%>(W&; zHC|}g>iV(8yS%fI@O8@EB`bYoPQ{{}*=u4=1^#dswep@=&urZf3KM2#C#R(oa_40~*$#a*-9L7evD>L1n^Kfa4O*gXCuhnEk+fxf3eK2owXQLoguDO*@~^*i9g} z!@_H@xs8(W0_7KK#349Lkm^cs+;M6cauv2p%`Ka2|1|2x)5m7%+0Xxe6@Gnz@9#m; z>=nXbMZmm^C5`@q%E7`Mo<<3mj|}26zDck$au(1W9;jJhiWX^IqgQ0&)}^mA-Z5pPc#ivV!)8mX$EklFr&!%RDN$DF(e)jg zcEBwuARCr^Ky3yXfTdATdjkvzwoibn!1V&8jRMt$pkS6DM1b}pm5D@}F9XyAcN3CX za8DqKVNE+Uu!jh9M~)({7!V65o|Azs0S5v zKmcSSyO$Z}CXhNJ>;V!Z diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410265 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410265 deleted file mode 100644 index 50350cf53eeb6a0b0c5a37d13c85dd2780b4c114..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1476 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 MHUb&g90HRE0CkqsPyhe` diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410269 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410269 deleted file mode 100644 index 194edc9193121525934d5d4ee7840d8dbaba3177..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7072 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 Da7K%L diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410270 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410270 deleted file mode 100644 index 5ab3583ffebc20586f1357eceaa8ca6a88753361..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6052 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

GHP2rQMY*0>pSe3pBxS@O3HySWUrfK7Hz z9D6n!dynfO3Y`Jo^IuO*vyI&FCQ3`icd-WD+nszj`HZn6J?Gq({ZQY@3Z^K6jEw~$ zJ5BFIb-|4!!iSie1>ab5c-@~X+|64Aw+>zqnhf5IbZg0)zIVRRvtRrCk=+$?JJf0x z?q2*?yG7{{zqKW49kMgJiUnSz$ID#?Ho);vxM%nNxZLHRA;PKsDSNgyHB8MsVV#mJ z&$hpA(MDxvU{eCjBaz@-V2@}4`J4jQ-6p(o>~$KMqjULqGPYKKVw2E7sN&W|@e@Bm z-%v3B;JXd;3HkzqxN3# z9n3HE9eh8|SdR83p3uP5#ITJMlU$Q3o?nxN3C;sCPZIBS2z?;GxO|Qr&0|VMa@kk9 z6Hnga+ATY#xMySk&68Xw*RAn?c<7b9#=CGo0s(gLXA*cE#O%Vc!+5sfekAzDD$yf< zRRZSVzUTnxHv{hrOWeuGs#s$**Lbd>{z0y_W11uKUouSP*CrVg#E$dTijU|k!8Q@T z;NJF2#1Q3#uwI}PU0vlel)m&*g~9<96P`694C}x2G2`}XZeM$X_pSA_cQ5VLDgyT>@*rK+Gg5auBiE+Lli8GcjIa}ucI-w5$H zRf4lIBAr`A$Ii!vCo_3HnAQzgSxcxRF^eRjS2O0eNMh;lnl zOzFB(7EnSdrm8epmR@a3p!T&8H z!E**LVJn`a@Jx-01m|9aJlGcu)&p@Lye}+?#&->XCmYHgcd>r zp(oad;mFZ`baxb21rzMgLbm?-vCEZ~p}- C140J? diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410273 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410273 deleted file mode 100644 index 92710228022088d31967d5a7a3629f3f836f72eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4864 zcmZQzfPiLAO$nd)b>`;wwpVVgpCH?t9J~2Ju92G8bPcc4(*ACsD&gBs!s|MF?v`rJ z^nDr5Z=P`M9mAU7Jr-$tldU2iT)LK_wn0fMUqWfllJ2sT%}uj@rkaa0mArkF7`paT zcGZF~dyq{@i%!3R*vP;LqOTTZ-b@Pk?wQLz`_T(iA+z`YUtFG}$F_gI-;jtsx&)WWqV`BwiEA8esl_vSez00#?hR6UGKX$&vF{yxVrZoQMenq!o9$x z%TdG1zASh0OP@8Lwm+zf71RkElnf(a#yB_4ufdRL*575HZM;BxtRI*1qI;+ zAQl9i$^go7mq_t#I4J$m1x1^KI|^UUUd zxV2keeWA(Eb^50go^2^>U-@TC`Tuhky7LQeCYgu#Xqf2q{`|y{^6zhIP)q}t?CCj) zEy@nFuNB|S;4gQ1cs=#mDy6Q4Q{Oa|7>NX)oUD4v_^MWTmy*Y0_UOmkAZ}t{AQV?X zbu2*41X4ePm1~;lst-vM{A;9@{$*x8=au~C(|6^^jRVH93tLMo85qJwWrdKdsjVE<_8?U?`IKWwNGNo z=Y7q-42*3HfYxqhVESGKvLA@yAZgJVb|8mk@0_=CoA!Gxc{q<}roKta7uO?fU+$Yu zf3!&_r)2v?lT@G@kUp?pg7!hxy|let{wBayGn}(7nQ@1`rtPuG&#acNIX*++$@%-s zGHw9PV{#1f^a}zqU?Ba}wd9Ife5Jixf92~gofi()Hgpn>49G<@yIJY$36nQ1Jl*{@W-rEqdPD|PxP z<$30vJZU;X*bt~Lg+V~vj8L~agj(LWs?2!!(x$;^QqDSo=9>KA7GPCHz_9hy(>z|_Os0j0sz2EIqvQ}vInw|-$f><@|!`5WY2<<8W=D_%w=#$_+NLuFaJrTy+Em>*4|6=cnklo;Oyby z8a@dOmNEI)rg-P zp0{ge+^ddjgs&}FE@}Qm6SMq2#PqQaulQ&quoVfc+t*nNbMD3 f+{AN77M8wfGZeenI_O}CbfvSX?H8mxC;@6p* z+uL5bwSIzZZ*uJB2f0RSUeh(aN=y5@f7|;kTC~C7)%1DBWp0mD6h1UZZ*t3eqw_v4 zb^gDm4^<$Wk`|r*2(giY5kzl|71*@9R>SY=0W0^V54TP@$sgVv>MnC6pn6{u%O#s` zpc04PC`Ofq^F$I#rp{H}WxIG{XQ9Q2X1sLR$g7Z z&g8P1lz-{*--~SZ|9rW;fLzRc{DMNq z1rQ4YPGtb;6b2t}2N12G@!3O5O*eV{TCW{_vxJtG_jQ;b?ECAd#~!`!(SrQd(|Km| zKit|aufEXa=Q{mU3D35awXgiMrTqUn3*GqzH_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ4fsxFc69> zpgLwCW&)|-!1w5Ss{XO{)-Q}_+`IW-3Yx{Q=DVh z600p^Wh2p5bm*Yh4*f?$f9`|K0{bDwKkzA2c^u!dYEPvd_pW-T%nvwx-_Ih%YM;cE z&-5n$)3x|g<>%ijdpYKC*xB{S}@*R(x0`I*(yHOFTNJUM@V zS;h^Zc}$KWo_;|<1`MR1x|Uoqi?6hI>#uyh$x$3`r%EhdmuCoA7_ZZsx%k>=kUB;~ zU6^u4u>XMhBwE=u`sv2Jr@WdX9*bYUIlph-Y754VQ+^nFJy>&ub)z@KzGstT|9(2H zR#BZ|a#i(T^zzKJlmGO7D%4!{XQx;nC(uA}Sk7SOnkKsHL(&BQ8fm3}nOVsvRkp&|0Xg})P3 zZYy7uuvnJwFFNDe?&nE-$`%Y87wyqI3bh5O59|h@f7zx#Zf?{U-}B_{v(xcA_fGVj zrRJHclWFjLeUms}SFhs>J<*k+ZAD#Yc)V6t-qha5@0RS?C-sTf+dy<(^V+4XKoglG zVsc(dyinjg{7d(*c=mMno`ALAA0KefoXZh$?T_VdUx=;1^bY~BbOUlf41mIn8Ja)9 zVnq0#LF!V83{cMlpaz(kV7(v$NdSonmj=fXoCnFr3^4nF_2L`iJ{bPs;KljrT#+`M#W@9TzX zD_30%fCMu}IRXoP5Df}1lyZb{z0ANMdlpzNLF#5?v%qW=k3wWo(l~ME+kPOye2^bt z{s6}t$XXiMLy12yf(1zci3tfXsDI!bAOkr+qPJ&3awvI*M89L$k0gM^ggXZm*2n<2 z+mP}MF>aE&p_CUSx(U=@ Rphg^m!vv{rhXoOs1^^LD?>+zk diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410275 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410275 deleted file mode 100644 index 5799bbe692ee6c991d517d5b51b372541390c640..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3896 zcmZQzfPmNWWy~wCHm3?9`f%%nllxNh5}izS7%fwN9m&oW@1V4<`7p@g%=n>}~h;to7G zo|oC>claXLotx$?R$mydeO#+wWouP1d&}Xy|81HNNqZ_Wh_*cDeXwow@)VGZnU7yk zmDvDdLBOdDAf3YCeb((=9z^MieV{q)$Q_dQyWzj`{) zZ2pH^yXDmvn*3a+e=6bGma_Jhf3}qWKWCvkzu;z)d3cY8iB9j&PYfyl{-y@SG;qnD zo|D+3>@fRU@y!hWa+inKQ=hF;>RLGUO;d@HNZ`rIs;7*vYK3Xu z%iF1@{cYM=?K@{HNjut9UFJ8-|CpI_b63#^2ldrG0(vKp$)`{M;oYe;BUC&3qeZF& z$Y0>FNbwJR%2Xc5cdXh|X~(^*o+i~x{g**oX0+@}3rOCHYSnW=A*^2PNC+n4*M(;scp$tl@B(Ige92BZ(Hm!N%6 zbuVo%m%j|*uW5U1@-wTYYmUzlcyj*!vWy!*^Ozh%JpF=z3>Zj1buGDK z7GG)a)?fL0lcPA?PL)`^F3%9KFkYuMbMdv$Aa#s}x-jL8VE+Nba>wLO>*){M*S~S? zy860)cg+1?uisDlKYiA`le?^W&TW`@d&cker8O7)4ffu8@p0=yTh3FHoTR7g{P$=y z(P+!N%nvjW9F`mS9$in>Kepcbh4GAgH$SXN^4T>*^6Zz@AB;RD0>8>{MoL#;^-zbv z!-k-K1_m~e7hq{;;|JuNdQSAN-&w4-0AeLpz)=+{r0tOwssUs+wyXcQKav?lGCk8b&aqnpiS zWc%+4OQv1!xbpX>mD%L0UH82hP2+xZ0Zn9Hw7BEi_4j=`6UE&Q?3J!qW-F|#_2yWQ z--a1>zXTk%e*oJG%100YOE)0*!vH7@n1T815lnyx|1(Hk@-0Bin=tLji4$fYTn@-U z4s(z=s61eR*$*tIKZ5z7^am{eplk-B+;rjC1sb~v^e99z|lpRiMWYES-Vv$5oCH<)&`oc{Fwt$n7AEl0FBC+fd3865Rx9 zn@}ST!Fd*`905oAnM8$tP22aYw=GSWTHN`TRjRUlcdSj(#o4RH9`NZe7X~ZBtm}}| z4lK+cg4F^Ef^ATs8m3<#8ld$GOdFg=HXJI55`ILP|1J2bF|5`SO>3z7g5 v6CR8pKOqBHUPey`Aic0Sd<0~J&7yKUAL0(|^$9FYXkjm;e}Uazc-Q~{^J^so diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410276 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410276 deleted file mode 100644 index d1211ac0b1cc9bbb18676519a04f03eccbc13457..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3896 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410279 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410279 deleted file mode 100644 index da3e54d5b6027ad34ed38ec76a728b665fedfe19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4876 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410281 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410281 deleted file mode 100644 index 02192f689091107bf59937492e26ed11c418df0e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4100 zcmZQzfPnYL?045Om{uhUJa7^d2$$1y(kSkH5;|kv$|YYmda*tLsuIq33zmJH`sk#R zTkieH&(im^H~q}=c{i(_)kn8?@qN|CWE-0s%`;jjV-smJR{wG}q zsKnvJs+!n%PTfZnW|$~{xGz$rdvav~^NDv%JyQkOKTVaq6#iE`S4O6$_Lf(g_T9z# zdiV9qw>vC|WB4MK=2*U5oOk`M*2`=1nf|F&f15n%aA3KEjaTvc9tG3ApAYqStGB33 z)ZNxS!RS!eJhr9b>6dDlcd8t@%<0B=kx$>QofM^Ac&mLN8y2ufzOc-(NpH_UL_&7UZv<&NG|; z;nr?>^@S!s*Xf^1c($diedV7m<^RuF=*};=nPeW`qhX@c`|}e+%D=yDr4NZo89>>kHLxatBx#Xo)gR6Ex-qIt(0skz4P0^BhgS@#WNZN2b;4DQl0dcNLrx zb$9)i&~tm_XO%E8wk-grq31C5P!GYwi=ci61~#A`#t?5Ku-1(?JWmHH={Z)tVae_P zx%={s%;1&KdOBEe@e5LFOzfMA$IW0FB4b&$6+6T+VQ3ph3TkRO5K~3*&4f(6=*KlzlN_J znP+TJJTr|&HT#vyw-iopXQfUbr998PlP66l2pa;`r7#GHon~Or0AkeilC;gA+EZ!Ay{n!n^8*gw_p=DG+9xsP^S)+ZkXr&m zg93b9fh;IUKXom+VisR%@77=WdXu9#+)kBPye`iWurOYyHFNQ`&tPRUrZqs7GeX_! zps%GDcx7f!Lda?hwVF?}F8VykIA-am`R}c*yp6&cCO$k@?V4EmfKS)9^8UIs(9>u#w&IC)4EsD1-5$xeXjgV9(VgyMH1%;%dCbc+=8f$0+w-$d6R zNP1vy2hmvE&oJ$wMnzumG%ZuEtZCcVJ*iMEW$X2};#{A*Iy2mF@u62x{VD%}0LchO zAom|s4wf$A6&DfZB?J5Vtuvry<|&{#pt2SYklcyHgiGT}17Q1sX+Q|75+!bkbdw2< z-2`$wEWF@l%ph?aCE*3C_oxwv;4neT$Ka@(82znJ$dz@E%)K2hT~oL9T>g9D+LNyW z@|P?FHvNn>R)oekdR+r*L%;wm%;9O2h&Bj=?Aeq#&~^+c9ipT~Bn}c2W*x4&rUh>r zCDKh6G!(lsm%(Jo9Va}&s2aDe1lBqm%MMJ-SiIWCa<-*628;630J diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410282 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410282 deleted file mode 100644 index 8fe66297d17eca696c3b51b690eccace1a07ab45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2684 zcmZQzfPf8WeRq3QceEV4{cSGq3a9tdG8ghz7Ck*PJxBXU-h@k0Kvlx;joI(6V=%2s z6nNkyCJ-*C=cG~G`6P74yp>D7Z1iG%z~H|1cigFPwyz1b-6a%j(6_0yKL zILm2%VVYb$oBQ6{g2(YS9!EIjc!k&N32)msh3jgM_X5XczbbvTvK{+3xEoIVQK%6R zzQI6PW+G>k(%pkivv$8eZ1u}t^{__W2PF;(yY+JycMCFzw!Gqfux<156p)LVk6+Mo zE&#D0;8X^XPGRuzb^y@|8lOG1)O3^Aul3r|H%n+~d0&V5!M?wKdhF5r9xcdUJ)LJZ z|HG}_^6Cpsey-C$mGEp!S^LUATgv~Rv(TMia5Kp~yhp=Cr}yV4hLnGQQ-fj}xMWYy zNo-Men0>AIW(I$`%fsub&sHgQEu8wMsl-Sm@Z@CGQ^r@d!n>3_9s;z9L3>ws>I@Td4_<6 z@j9)Ui?4kKDPsEdp#iKIXdl$A4$HP`T{-dVkW;eh%>V4yv_&1fHD%7JgiolSyQ%re z$y?bEoKpn23$Cd4DeleT`0({a2j2wt4@G-a)l}2|Zf?}z02vE1^VB!iux`;V#bW83 zh2k&&GkSa7N!jhT?83c68vc7)y{0lSaPMGX)@oy5F0BMAM+p~@ekfpu(!5ZbDTMn8 zm`T`turekyyLu1Ee`Sh^Q@Ftw3gpD z@^S~M0RpPn=^6sgx(sT6s{WO!mzrs<{i6r^!lIIPcP2e;@bC0VVm$Jh?Uf_ zGtwynY}~{&hxb$d?Wo%mc_23M-)IgGMPE`E&rdPi9{@v5}Aq5?|316r$jNSJDYG=xS zAONyq@y-b3{)5Va(hoDR+;|D)6O+dn*w=40f#x${USnW|=>^d+OHi3`72xy==Yi}7 z0jT}JGFKcb#|SFRpll-DwB-Vg-2`$wEW8Gr+b9VyP+3WhI0T0YQoRX|zN)w43$Oq4 z5#MxnefnNug$o`3!&`M1n@6ZDx4j#wsB{k!$Ba7AGzu?+kkTkG4mK<xutBrnzN##*DSFIY~mRl8U zd-x$meexsmW4E_Y^?Lf^rY_GTpEasm59I%ed$b^b^>m)u z{13Nw%d0Om`MFO2RKl|@W$i2fY$^YL&O!ObM|@E#2lo!+0H7*hWIO$~}^;F3K( zC$UA@VfMA+n;HD&E)TD#K3k>KwQ%a2rV=BOz>||zPZ?j;3hz?#c+4LCcpJn`3=D+g z3aE|=iWwSmzy9HNw<_;X_C2(ER_qU>chRigw;pjV*E2|bwf=h&!;EPSz%Y0TR1b;34fTlGGUd@shKqs6=8b7 z<^$vS_q`vk2iMj&{bCHi{!U)Lwr0D~ua67XWhfO?Y$~37bJ^FaoIh5#7g{lw^QaHJtl{$Tt@;viS zo-~~xYzS16!XO}ant?&%ERYRzGe|2iZJzalig6Yc7g!k^o0x+%K*iv6ihtl!rt&zx zW7VEYJMLZeOqm~W_`aV-h}AxcDWCT>`vMg*1%w6#__%_!KtTGbYsnR}_)2@X{>s;z z9L3>ws>I@Td4_<6@j9)Ui?4kKsbWfsf~saGTYXs?_VmVkrk18@i5*%&dcIoX%ImK`S@tM1`|*As%ZCQJi@QK^!SvvR zMPIb;A;&szaf#DDyDt04-e4>XElofDb-^<0*{Y6EOH=*>0gw#~6GkBSA5;z$w#>jZ z@CwQ&CY%}A*KfH14eK*NeXKCOAR1-~Dif{(9G7q&$ZimT+7C=~vQRljP~L>HiFH#B zjok#xSD^43Y;L0@yg>Pd8gU2?6Qp_o9DUk1j|pD`-nFmYZ{_~J(TzZBUq3GkeKjL!f2Z8}i>1|Y_8BZdG-4O00+g!vU)E~|pdQDV)9Ih_Fj DrSPoa diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410284 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410284 deleted file mode 100644 index 96ae96bfd67907fda52328f4baf8ff52d32ef2a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1476 zcmZQzfPjLa{I!SgsP2()U}|w=m=&x2C+Ykh$E~Gj)o*$E7qfZ;RSC0tKEK2J?q2Mi zVzs$HzUNg z`xNu9Z=SyVEq=~KyX3+FYd+omEwi`tHa0y_ttiu5uk`+TVcqv&gGIhfPOa9fCi9)) ztj%g#c-(EW#xB49pzJ+=?(xU-{GMX>qG@~S)XZFKD^U z0kI(9R0fbvVes*G0MQB>pFOnHbd%Sw_1e)lOK53%Ux)d@zQ2BY?9uxkEy!Ozoo6=x z!>!%&>I+SNuG2r2@N7$2`^rCC%Kx9U(4Aj!Gs!%>N5e#?_va^ulz)FygJK%EWKYjY zY*BWYeXaOr27kHB!|SQfRw;EYocgAz#7HFYV^Ph1rSdI>liOLT(?==KGwvhJMQ8nh6iAG-pt!)w*x1C(5+s382UefrANZ81JdW>JwWrdKdsjVE<_8?U z?`IKWwNGNo=Y7q-K=n)kp+NyYu3)`jBK_30L%_m# zoz~37*FJ+3F|Awb09FjN59(Hjd@+I2PhWhlZu$PX`rH3VrT(kZJHmWy4!wT&HC=!9 ztI2H(r`Ddm<`!#X{PolSVs-alDNG9-xvhD>TUW=%7mM=(&12j@fBJ{wU+SlsnA%LI z?{o2Z`Fs7TW%>_-CVi_HwyNg;54AJpKM;V_!`uMm{)4hXVap6mJFlU9V#1k$ef^dk zXjq>G>SKlJ1<^1|P?>NQ;JAeIKz4%w)P7*TQ-aDdg3>dTO{|+ZXzV7C+hO4~*xW`* Uc!BZ@HR2E)CP-x`Ec(DS0IENq0RR91 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410285 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410285 deleted file mode 100644 index d63e8309f7a8e293a059a1edf1d467063e00a30e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2484 zcmZQzfPhs^`%|)63pDc0oF$)VO|r?oo4U$iHjo zm*k(c6=YM=qBA-W8yOfu^wwB`O}lF~{H`9ba$ovz>x7g1;mx7$GDiZc_cgIxvgrmY zaX85xD4-_Bv#&WScbCd@+ZKCS%@8gf^Ce3sg%t3Ys3TfmQZMe-+PJ5MJvh_<}teXwow@)VGZnU7!4as{(M zJO+kS89*Y1!N=PHL@Q`~_Rvz(O$NAG*IAb<6Ap4t2l zw|2{`FEshNPXAQGvn^%qEB|aM|9{RxcYeXmB=hhd4HKQ-pPv{~{{2l2ifQ1IJv}F} zMcHBYwc?u@{N*kWuctm+rPQ@>>YJt#Bay(9lT}X{U)2ilQu27r9{qS5&<$WfD6W8t zn4p-!@U@!pZHd)aj#?=b3l%r0E1u>cDi0f8bN5@;JU@)t*W_?p^gvnICZYzMn;i z)jo+SpZ7KU0<|y&ga!rpxI#38$n;a!k}GEMmG*A^m9IBBio@+xiN)*k3;_${by_nQ zU;7MF!;};SqCkKV>Q;vy_ms6kzGfS&O3Q-puFt-;$*ldp!=on$B%jL^?U^@K`NaI( zeG_$MTeAc8c0Rmz-a>=l``z+SoD-dscV66}RmBN33lugC1l`Qowg71DRtBc;bwK4X z|AMrF(mBW{EO-2z#Y_(P-FtHJqNHAxbdF51ifzlsO!rH>YP{SOW;g)Vfb zZ7-L<39!`+=d4R+++nY2du;MEtEFp>&k%TW{{FIz8$k1z978-^f`AMdAR;U!ifV4b zlruu(#NpGT=#AS~%OBNQdds8R_Za`@ aKTIo)hIdflKdc-xEy))7aZ{G7uJ9^FX zg7CtyhspNFe|J1vTl@3)rE9CcKLm&6S)siW%H0nqSQq=by|UFU+{vN1t1C&Qe(mcJ zfnR@OA44ro`40p@HY^+&f!u#kIZ%8s1M}eH_@v72D|8WdiG&25x~7pUx^MjV2}1gRbZ zM_;DBU-9#ak2bR!-q`S(>s#@S)05{Oy?$}3^|9Aq`ONL@p>d2YjsAkl!P4jxD4%fo z!N4G<(b)q`e+Yvsbj&>;Kc$Dya34ufyDUN%~`UKEukXKX}s*RDQn5S9t7~Td|a9 z_rH@Xg0F*YN?LTr2x21xBZ$6QlzB5L;Jaro`|L+AOohze|9^3Ljvm|o)f=7U#s8$s z0F^j=KEO0x^LL7QU0tmU->rrxp+TQ_8_HK--h46AERNNvm{;)6-0l$3tb-h~ovY5g zTxz7(E-|P4h0gT%-O3ARxH0B?hNTkLA|3CA5NiyMW1K31??%EqG|)5jp%@{aexw#~~^KrUuJenHDk z2gHJaQyD-yg~7+$0YockeD=^%(@kE#)@w)KETN_4eI4cp`~Ldru}ANFv><==be`G# z54U#9t1mS9xlaF7!m}-9?JNImDgS@YLU(?_%_Q^i9t{(n-k+ZsQvUr-4T@>tl07{q zu|?To_O;@h8T{og53i>_Tcy;saO#_;5+jkolap0X8DG^3?^5!3%pU!C8^lcv420qe zsE!GW8B+WMpE8xl@g1x7RN8Uxs%OglfW!CwEJCdINlf{?ui2M@v26hZ1LIZ(rtkGY zJ>Yl)(qI6LPj4WD<&K}Tn8^XZdrvN2l+>$|&XFlrv2FR7>3(TfjhCCk3Y#WTS=U*+{4uXF+j+m9epjDKxXd)DatgOaY-m0Y0uk)1ZKeaA9DWF|7fr zoDu3)hbuM(u6F&^()VlYZ}h)by0=n$$E)`KO_n!XW=+5Ru(|5T3F}2WvRIZHy*a#2 zVezN5oJd6_z318bt4~`jSX;XUWE|7m|2y|BsAGB&70vYJKmQD^|A8X^7kF=PjaV@E zz6Em_Ff70ziL_H7L9Wo7*S}FHqS-jW`5{ z2~s@-j=rWROFCQrv?{QrE%+F^{M$csiD~vaum3Pte|0|;s=x!TyMPL@rO|)T_{T`2 zM3e;#?CUo#ftCm7f#$G6%>q-jNTWo$$$`dh!kR`0iQ7=pD2Z+YmFd)oL#$~OmNo!Y Cl#wg| diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410287 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410287 deleted file mode 100644 index b4b099a2e37601e07375229bebfa8a90d1465c61..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2484 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 CbB{a# diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410290 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410290 deleted file mode 100644 index 57f569dcdec136e2ad18c23dfef6c7f0f6b6c2b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2484 zcmZQzfPn5*L5KXWFy7pvyI#q{?%aHrCegEhu3ngITHR+ApSyiEP?fMs-(D&*- zKqU@aZJ&1+#9cnG?Kbsz$iwBHk1I8fM+U6(3cMupaQ;b-ll@1k4yvuY!KpxVQyD-yg~7+$0YockeD=^%(@kE#)@w)KETN_4eI4cp`~Ldru}ANFv><==be`G# z54U#9t1mS9xlaF7!m}-9?JNImDgS@YLU(?_%_Q^i9t{(n-k+ZsQvUr-4T@>tl07{q zu|?To_O;@h8T{og53i>_Tcy;saO#_;5+jkolap0X8DG^3?^5!3%pU!C8^lcv420qe zsE!GW8B+WMpE8xl@g1x7RN8Uxs%OglfW!CwEJCdINlf{?ui2M@v26hZ1LIZ(rti%_ zJ>Yl)(qI6L&jcWYW$&D~a+~&hEqOSPXQsYM$`{uoY+vr1PJgsXC#PilM3YpY8jwD) zUV`>P)xEU6T>d7&Rx_NlE}3zMy{7H4$%Q9{N&0}&5@$?GcW&Wg8c_fV>`BTZ@(vHu7H^Gq2LX1F>epukNVXn%NO9P!qQ#EaCqUSDzOL8 zK9?M7{OrG<2WTKDJQ)mMJ2KDMpm=5)i)!{Om2WAW+|Ej!K1z9>c_&YrP7pQ(DoJ4w z5IfDlpm7DrMhnZcc~CLVg5m-zV`F0z1E2s@3{DdfeoO(OK>cN)>ihSRM@oLpGzTLp69=u@l^cOzXlExiY zJ12`SPu?}_`0aPw`1q9fnWS%K2b#z9D`sWuEY>%+yK3j%d$ax`XXMrQapv6%R?OL0 zcsBGxdMwn=l>a~gWW&OQ5y<@ql>>z>GcX^1gz||AX9o85n?#^teHo~a6{Z(N!z@8% z!c~Cd63zqJ4FXX6foa|eD#r*a8=!0=-L!E7jok!tJ1o2go7*S}FHnA=MjV2}1gRbZ zN8fY}qtf09_k!{T8;{PIbn5W>>7VbnxXPKqCA=WesOB(=Z Ce2|I& diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410291 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410291 deleted file mode 100644 index f0233c2ee231eab584649e65b140ea27232060f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3772 zcmZQzfPlRpHwrp)a-MD8vSQk76^UOfQd(b}6FHW7sn7z zTB7;cRW*=JNsG?-Kx|}S1ko#0&Pwc=Tgv9EIV(DP%@XUwtMrXBEktd5^k;89o%_KU zsKmj1g`4e^XsvX)^*JUy=0{VVrk|GkcwY2@^I{&I9Sl=VSHF6Yx{|S1=jF_dd!7Hp zj(q97mUif}@zk%L;{Ok{c(8rCayWSR$3GE;D-4gAe|=`UkvFcY@3Lrm?7H9FyUPB~ z-Dsn3@h@ajbPa!7&gWbERK4tqLb(|uPcl7yvgJcq;ZFw9me0HowryUX0&+3)@e5ks zOF%3LIF$jUQy6@_9YC~##%B*LHQnU(YrS^#%@SH#-q&G%u-Pv@D< z|8Q%!y!t|upX>BbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}qtBOgDY4_e3I=Td(_&L^{**rAXVh!1 z{{CqnJJbyhUtE_}%y=mKMZzWH;x!k=tMd-M?SD1z|Bswf#`1*48~Xa0<{xCSyQlS_ zSejXCQn{LcMCaNKy&F0nZd+%z-{2<5ui)@Y@eh2;R3687tlCp)$GxkbDf0sk-}kc! zvDzmw<@3H~Uk1ju1;C)$%E0u!1*iul{DAo(4JgL4cg|b6P5Zr;JerIa0a646E@wz-iz`}T)*38A%K7-UT z8tTH7GlKmGj1%!Y-Z#Y-CfV`Mk3O@bAnoeo&%OccAG;P-*_>uGx{%$@t`W(2D)nC@!!v1_Dcn0+>2t!;dK- zG$_Ew6`~nL5)&>AGp02_budER>cI7K&m=jn1k7oa6pSRN~D{dXzV7eX>^dd4JD0| z=q6D8NR2qenntfq^E|-BXp#Nzn*X~cC#OW6ZfCe)&$4%B;!DA5*Q>g>p$P}v$^){I zk`FvdLDOjNHJAv&wi8gH)xP#c(Du_Lm_8VdWC;=zCJU((;XHUb3b8+FQ4q`>C}B>d zo9t-pCah_6khqPK@S2PiA7Ca@#33=^szBj@3_$H}P+f?gMnQT(a-e$pB}fSf5Nu1Y zyTvyn=$0r@=>}kuodDK^L|}0cir)#FfAhQl?N6kb5BD3$8AP}7k=g-B&c~VwVe$aL C$GHIj diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410292 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410292 deleted file mode 100644 index b7e3e9ee0c68dcf807a403fa817da3f70b823878..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2708 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410294 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410294 deleted file mode 100644 index 7bf15567d2abab732618ef533fe74388433f8d64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3920 zcmZQzfPlNM>hrd{a3wk3k-l{*GH?FubrIgHRy^WT;}5EO=@BCXR3*H6@*Lie(sEN8 zR|s61VZm}Q&EaHb{z07!&88|>l}`A4)%#o5yQ_J=yqc?@z;e|BL&a`a=aoWVPBcp` zHOY9K^BrVU(xNlb5E~g7LG%ihvl4sema_S3&Wes+v&8!FDt)6&3sKu1{n=Yj=YB8- zDsf-Pv@D< z|8Q%!y!t|upX>BbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}N%EpOf;moCA1rC}Im*DGF2Eq{m%+do zwh*Ws9B&|fAOMWd5+DT<<0vRDurf3;Fop7g6h!Sy+sox|0&F$IIqQ-cci3y%9-I8k zYU!HeGX$QTzrQTw22dT7V~B5L5J(3Uq@TK$TrrEUw0G;Te7(t09B!vdEMAvq2v``e z)0(;X+Gmh5i2|txsB%V#I~g2ar!Rgbxv$J2^XQR%o}G!6%NOUZ)4I=Re}=tJnIePnooam+vqDhNorc1`WU|?)p08AI4bO2Tla|e(HhYvyh3=C`_wISYyV66$i zrpZ3mvN}^AdbQjn9EIRfw(dV~8fZ>5`&beg~7dLj><%L)tTzzemLXFm}RRJoJ`yad6BhH|9%Ck zM~;7FHbaY~C+DVrN2fYjHfUV$xn0EAH+N|`>!(jG(eofb53w85n`wUr;tE9GHP|3i2Zu5Y872;xfKQEYQ5M0H}!vq7y>GECKS6 zm~a)y=>{YS%8v{%`+@l?49o}V1(rWhHW7Ih^dd4JD0|=qB_y zAvX@u(N4as2P%8vWeE}X0@F1-4bd)4D036C|ELvD z3~4WBP4D+|N!HiLkz(OBHgTf1?t|M6g zGcbsIl)Zr0PsoOW*eD)_NTQ^1;>M-o6{lHoR_JVT6|v{pvL;vK_YBmpEQ$!-FrTX@+*bUPJE N58Or|1Diu&@&I;3rf&cM diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410295 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410295 deleted file mode 100644 index c2a15d1d09c22c35f5b5f79320b98a73e13c3a06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4736 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 P{-Q=4qL(A^NCzvn*jgQ1Wpeo_T#;aGHbCzxN zm+8C6sJKe~7~6`D7s>)F-saWbv^+j@OYYw4Z4K+%x3F0*H{AEi<+6ouZ0r3==?qt2 z{m|-2l@9{hl(gtf7Q{vdMi9M1<*dY>xutBrnzN##*DSFU-c~e+V*Bo z1@B-cth){I^hSaSIJxYY9(_zs_}B+EdmTW_xYdsUr-cEkAi5Y}>p%1>|Dp;}`UP zF9ES2;8X^XPGRuzb^y@|8lOG1)O3^Aul3r|H%n+~d0&V5!M?wKdhF5r9xcdUJ)LJZ z|HG}_^6Cpsey-C$mGEp!S^LUATgv~Rv(TMia5Kp~yhp=Cr}yV4hLnGQQ-fj}xMWYy zNo-Men0>AIW(I$`%fsub&sHgQEu8wMsl-Sm@Z@CGQ^r@d!n>3_9N)Wst3mz z)DC1eL#B;$59`d8FN<`}Em4Yan|Y>n+R4{y2Nf#QyidL`Nv;IxVfyu<0Yn1nZHY@dud5okYS!tRCbP|tu2~5h89Ur&Q1T0PIah8KnD!jl;q-(UfP=>w=r_G~HxvL3KTD1pTiMo^xDsUgbz)AtfcFdvq-LH?kn zJ(TzZBUq3GkeIMgf~0de4-`ir0QDbQxeAh_#P1mPBMBfe!7cz1;5-QD;d2{_@ga5L z?E`4MW7vx%fW(Bmf><}f!w{sC=yD9nA8;Fi3~UbBx5@VI-+14ZJdL@hE^Ou0pHUkV zq8nS$rFCz1?DvI&AHn)D^CYrSusjJWzrcWCc?VPlE^kF!;Ci51!4$ILP;r#BOqBUQ zGeL1ftoc~;Bn|AL#2*;Jf+T>%goh-tX$+(n7KiY9l*)Aq#8Dd;?a@05W+J;67A9aZ OD%%UG%dp!E4=4a>9qqLM diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410298 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410298 deleted file mode 100644 index 717df905de6b899430dac5a3b5ff350bdf98c65f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4080 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410300 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410300 deleted file mode 100644 index e8c2d1b8420721e16b6ae891e05f4c66d44a7891..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4148 zcmZQzfPm^UX20(Cr?sIOzpQULGk&c#`SIt@!ri?W|2nUAoo$~0R3-dC_+fmHj84Pv zmz!pZ=Q+$2v~OI`;iI ziNnSRu`4E}H@v!YqPd~#)=W*kE!PX4?M(2G47zvGC9__mQc5jtWXZFX%Ib zfLIW4Dg#KTF!*>ofM`NyP3M`-|8Q%!y!t|upX>BbB|O_w*1q!3mh%7SEOh4=+)OeL z@6j;P>HYbMA?4rS)S#FKF4@y_5?hoVW?w75nZaM~^6+}@!pZHd)aj#? z=b3l%r0E125 zo#G$(l&L(9?^v~`(vEvqJyYff9KP>o5n{DZV#?=z&AveOOaY-m0Y0u^ydO~>OOi8T z*tlqq-chJ6Kz(300Q1M^1??^Q``#4ZYZUNded)5_AisN#Y2k6JEh}>ltCT)6uN0l{ z+j!jZ=kub?ms84JY_F~^ITco^R&^|iC9K3Dj|*rb*uNg^J5pa}=4K=x+UT*zz_B*w z=fbLw^`Nu^10cVG{Yy|kntwBGoO@Vju6$Xfb8d-JgxkzB zt;|BJd)Z3=91-z&nkf{pPA>hT#M?;@*XB5G zt6l!wB;eR#dleq7B&TlO$O(L(OXh7^xIutNJwVMi>2kQEb=vc7_wMil4P-v&6c{vH zqv&11ax-4U9nU zKd2lmU%<-|BI;uX_VpWDpmp+fpgF8iv%nP05+DbO30Hw0Kd^KLwjWnHLZq7%XzV7C z+hO4aPoIOtZ7AgkiEaY5tEdr&=;a7F($gh)nA!jDn5uYkzkWf)hEHXo`N2 zV`Sc{5(}~^Y0;TFh>Z-4Ao^-i=FOyl@1D8rvmd=M6*7DO|Hb7wdTjewZ*-Cu|C25Q zRN^3f(Q(3?HDa-e%%YvbANx4w3*PpRoACYWrSmH~H}BM&`fa=3-f2yHml%3mvv0Po zxNW#Lp7)2@>E1IoF9J1domiUgrAD%O*cHs)>F1s++ho2x#!=s*%|CYL*{x1ZNA)bu z$S+ZmnC!>a%x75_yz`TxM&CqteM|lJlM2%ecyF(}$spSDm-oT8&C63jE@nP{L7%Y! z#DaiR89+LP!N=PHL_b@5Pt9Vdi&yBsIi)+#tTx)5_9VD?+4iqBmTvc-nI8^@S!s*Xf^1c($diedV7m<^RuF=*};=nPeW`qhX@c`|}e+%D=yAxIxX!mnwvkF~7M)Q4UzH&0C$y)CeUFSvzWv9&q$vA7MZ zIZ%nXN7)OoS|Gs)b^|aS_<4s^TZZZgbc{lw^QaHJtl{$Tt z@;viSo-~~x42rN61_80t3=A4KLH5HO0-}=^om~nQ<18pHurf9_F*k)MfT;u1DgJ>^ znabn%j#Yaq?YMW3?kD{T}!T*#aG(9 z^;f>$JC;tvdGBMBfe z;XVcV2N{6MKn7SiqSZ|>zyATM0GmbSd=0jmVdJ7bdPl)bWcR|tgckNf$`kDNGSn<} z72R&Ax7=TvF{gy-h^|;nSk_nL?vI-ftaqce2~`OTQ!tk}H?7}8V>f}^4ht`M z9vdWXqa?gQbp7NMufBDt(%2i90fvSYL-Q1Snn=;#c zVaP+KgL20o0{^IQLMb8_wy>J_bsc%gT(hd!%dzQb?3rImO5Qho zB_sm(#s`CJN?LTL8Db*?BZyw1a#mu`+)_4Q%~{dWYnE6aUZro8X(4Lcqd$A=>D&*- zKqU^pg5_Rb{nyeteSb1n(Hssz?H!`8e&s7vrO&-k!9AOYC1iH=92>K#Tryq{4=w+s zxAmthqp7uyj?@jCTcQS;|CxU^%n>MvcyzBzYQg^L=bbqM9lahbepD+|VH&=^O?1Ac z(45UBe_oeJIxsr?oYgG+yGq&Z$8N!2e($x^pNak6Eyp0*@{jkyw#~~^KrUuJenFpE z0K|fTQyD-yg~7+$0YpDrdr!?`r;AtUzd5Bluoc1KRc-i)^HI{DopP3&FoX#_w z|KZkddG&=RKiBD>N_e)VtbOI5E#?2uS?JC$xS3=g-lJin)BE!iL(0FusX;LfT(YO< zB(^9!%)VB9GlRd}<>B?zXRDOD7EXQBRAM9&cyhApDdVeJ;ay4|kJ+OiZ-cmrfq_t5 z0oAbpF%w9=;cG|c85p)g_GM^snbU(&ol4jNz)0!hCn4L3<6@O85lHf z0omYq1L*^Sq(x^}0V$9eXF+j+m9epjIYQ$Fu&_64eE3J47f@Nos}1rzC~t|eE@;w$al`YT^=aukQ#sS=CVQ;xzT2WrEK~r>@R;DcYtJEIM!nMX-$d88rk6odS zhd0a%2)tu$zES#|nD-LD?9Cmi`+Qs5_AULt?`EHdAT!8VkfAM-o}8Qh9i8fA*`RT~ z=XMce-`u6)te-xaaYb!7w*TdE2FA7pz_jrVs1&3J20;D=`;+gYNFUe@!2Gd0_1uF; z;+YN-Q7>Z~ZvB2Yaf$Wp)Ebr5sc+^5gnyePpT_FElk?#m<8zBOf9Ut=uj%=*{FF+c zzzyT6jkE5o-~^cm_AkLW0{NGLaVrDU_g0YoAdN5p%-0KmY?eEI&SEA9{O&!ucu`WX zN;*fTSjD#GW2XD1T{T{A3NsvlYT)5R&_1XgFKsWEzX`C_4Cky%X53+~X?tw)GpnU* zj?WNya{m6Zj2l2RnH)nrU4noN7$73;NEFq8k~0`ELerqbamNVtyN6X9b@dkoe zaq}GgStXAO7yLW2v0`a$^DFa)umjrXzpI=&fA&L94UhQ3m9iK4S@cbQ%KCR1|4HHk zng$Nbj&EJJU37F!&Ei;0+$L;kU*jfcl6mCFA+b{@-CM$#_@QYBl!npM4#Y)3(&5{= zU9%chL-VgKb2fi0!kHCSC(QFlZ)e)x{Zm{0#REYKWzVL<)H8z31!^-r=J|Ww7ooe+ z>vr)SzjiB1%ITes!I{Nv;?It^#zfp|zkXF=L%wu{F!Sre4H^f}^-oJ?`KoogB;k8r zQ}SxoxBNf@S$rF&AMk9vcem4%vMHUf!*#Dtj+33oUT7OqhHf$0+#P>i7R z9Hx#qH>v)iv711Cg@xB(a~mb$1xni}afQS|V!~nrXB>jV1g-4=$v2R=1e--fT#{1O z!W;!FbCKN(O4sDZ6Yau;GB<(J9~>Zg7KsU$Mo|kCMUD&PHXg&7CD)8k<*k_h2R3bviSf=Wal6?v2O8hl(kLv9 zVPXCks+O2GH3R$l^?RW0>YG4)tT4SG8Yy8SG2tq3rBSf`z%*J1Rf!VjM6~lkZi=9> zo3N(QLE<(_!VA=xq(&TKO{47cXF2x0@kdv!)Zj2Zy2rj4 z8sF&cF?fOjGGJ*G-X0^^Mg{gacy52|0oHY(vJXAYk()-r_Ty@g5$Prk8oLQ=8XY8V zLrJ3~x(QNGBc%$Y!Uc&5SAZVpxaxV3UXUD0yJ*n34XIs3gqy_P^G$@MFIt5c19JP3 S=zboOKj1M0WMGRpm^=V7RGQ8J diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410303 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410303 deleted file mode 100644 index 4e4b63d9f24353c9cf47315a6ef0178b47812cb9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6076 zcmd5=3pkY98ve&+qawNF5*5XvV!9e;(;ksqXk3OKHb)sH6-utTgwjFwsYFhxEoDeZ zM^n;6ITejdxoo$zCt{SQX=`l`up1$W{eQW*STHpJ9-}|q%{y`9) zWLJpo;_TY|i{Ny<(|F^H-q75OOQN5@vmc=kB$sa=8haDj7!*;&icGU1U$;=^98U7}(uAJ2 zW!A*=JR&66vfZTreJeuCw&@PlJgbv^j5F6cmSL`PkA9#mMx?vIT-6yKY)Zk`Jqo=0zt!H{x5&fh`i;g{fYf!75TIfRMG`AWHuQ)>C=5qF4 znMa|DpS)V3P||oe+OEmv?CsTocHtYfa(=z_Dru!>`l7af&iy{aabKO}Pme<0jqe_c z8{D#gU{K{@or$)m&q^)RT?*$C5&1*ok{#EJvY8OEaAh4?R0L`~sqzLb6S3YzgKbYf zS5yC{sTJcxdf?5P%d3mFcRK{IZoCaQFniiVDYrW{=gizrcH*MHonJd#K(6xWxfXQf z(&)9|@rU|4Kl`_Bqc|VcTDu}?_!YwZFy>|d

~)#+OgEuo!h4rue98*JLy;?;KW6`GIXh0WM%9|8WzGre7F}YlUDtV-5M-)a0XtFfCj{;NwT>Tla~Nl3D2bVT z_a;!|`#QBB6Jk4L+!mzO{9$`P2$+D#oj3@hb^+*U-QYYZpq|-04Ih^TeG+36Z&X~P zulS&nss~$EMLJq4$eJdzUVn+v>ZdkGTa!wkP;`J*ndu98b7(C94ibGMYKqiY%4|23 zKfY&_?Sfq1w?VsJC3mZ5=pCN~$G#(m8~!oG^NwPIF-`~1w0-b%M9Yb=Oy85&(9@!L zkXB^o9PUm(rJ2AwOAozuZI5vI$}$VG&Le|7QR3^m@76**>dE6x<5!{x}&BlDb|Z;GDSqJm1Quz*4Tg)8H{r6)MZE z{P>{Rqm-PAvxRaS$l87LJnqffL&oD}ZnpbPDA--9;((=kF}KN*`7{0QQ(JvUYMJ86 z*JZIbXuJrCjR+0KLH`&K*aH4#f=1+tt;zhgcWk-c_4P`+VGDMv1S~Z=G_cdWp(NbO zY;hV5*x>n@(jVW)bEC%Ke!7vivqYpPsrUzNql)AnYF1ulnsoQ0r!4PQu%?i)qot`m zP%zn1)$AE^S}HJ!J>sXE>>_SfwO{}DU~g&tH3iH5b)oq+uv6R@{unbSq49xSF4FH~ zD#x)@b*x?s9eHqGEB^Iny;8lsx!&bwa~Rsr!`ue~#`4z5g6Em7!Kuc84 zvuoK`b~otNNPz`W?!>ZBT$n4Lpx86!cfC=!^gLatqq)i9YVpe9fmdA`a}xR783Fdf z7zohfxtVhQN8Gu{PRY2;a#-BZ>t$WxyQ8J$gf_*)$EwQTr;cVY77uYv;0}rb#H%bS zW9$@@*BN|Ka*LqIX$ z5acgmLe2%mnUW;3%9+e1@(iK)&!hl>p}=h66hviFo1Oi|-`@_=UX2I5cxP`7%rs za!&}BpkgWc?7Q~KUh-b-*(Hm()tpZ30K9iRr>us^O#@6@6k=Ep;O=%BVwr-mLDdUw*ZT zCd;U{j$d}ok~5TYJIb%OAonqhvxoq{Lm;ZPUI;kr@HsdGfiej(C*)7C^_l6@SbtLP z%z&RSiQ|*B5C#Y;o=F%JQLyvoV;;b{#AoPQ0>qBrc?9?!3Bxhvoksw_fIE0?KZ%<# eKu|IFpWWX+%?V+D`#&&E@J;9t)M;bFC;S(-vdDx0 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410304 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410304 deleted file mode 100644 index 1c8773292b757f2661a6bf78c7f0511d767c5e40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6376 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410306 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410306 deleted file mode 100644 index 94cfc57359a85e1fc2c91ac11e94f1dd98a150b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6692 zcmd5=3piC-8{P*;m4!mPE=-g~w*zwIyo(|(?F*4k@*-}>I~{l0bC3qgdB zMCmk{zLa$Z(<|d`^CZplwPzR)Fp-MikC`-I?%U@i0i@&`m(=#JE_q|y@yj7W&B|q) zF9Zt84vUw%^eSh&=f5gu6?8sI@5q0*xZ_*C@*O^FR@3ejdG)Q$XE|X8=hIA;T%e^* z$))i^iwF&tKdY4~-1sp-{40%w_3JODnBG3GtMAE}W*)73GW)?cfdLRvfYZi9(ze8@#bDncU@vqq1qPfz5H^Cne>vjIJrg(?{4~*WL}Yr zfr?t70^K>(r2YO}`84`M*T|x)8n)hx%wDf-^@^;JTl--3dD~D~!{aj*w+gh9id&i! z3@YOElsGS*GrpT^$ZUGewM?)=rbURzyvWH+V?o5C_a5m=OT(J*;dz0Qg;=j)!m=s7 zZayP-#cHR)#DLt=3;LPv4Qu^UuXP14S;=oQF1C-8J~^pA`v;W;Nrt_bba}2#S%Le` zy~_$3Drab)_O01u>>RCbsCBUS4Z<26aC2}8ov8F6abx&g=E-Wm8jndqE9#%PHJw*I zkQCb<=C421o_lN_&!9@v`GD$|FDI;jnIpu6Afv?!$kD-11j+|D#kh$xxOKPlXhY5QYtBVDOcMe~pBo*YNrxc#DFJk*!${G}4B z!ecXXqqj+TW(jf?H(pMORJ8I-x+3Q{`}cdD2rcpqGA;a1ATNetfCb^#sQL&3V^4Ej zqmQb4nk|o}bI!eDYn7*{e!Y@Y8gYW#HotiGF84Lu7Q@ZEP$BSVn!tlqXlEj@;a6P3rX5FCuEk09behu}cskeK8I`ormGtFBkGGi7-5wazh^s|q`_7o;V3Z&Ee{ zvNOI6Y{ASRG!z5y_o-=1{LzRl|4FrQF;3Arxp7aeRqeypsFcUgncI2x3L1)hmYEfTe}MD<#H1M?!)N;EP%98!1ISwJ39{487hMv$nTGk@V@)PMUAz}Rx#1x{)#E7` zQ^dm2%Ekdy@MjCJdh^Z%*?@!DecN@8t&>>E^JlaLc}!*)T+-y6t?qjH@b3cdz%TU6ZimOwW@DX9kK@9Y_J#sgBUc+`6 zUj(-ga{d_l4t2rahSe!zO20qOm>?dUug?wJ$M6N`%cqDT$_eQlg@#Yr{^S=E95ZI9 zx0LWU@|U^BC?DM5sp-AAymiXdixGp@uo!QVc_{*SJ$`@uLe z*jQ*m4D<!udku0UiEKuno)6F=B%I_`*Oxof}SMqZBkEoW5C7wQ{vo_j~P&lIjLG)ZsoKdKr)37}R zo-J_uLZd{y!w2vNDmZ36>QKPWo5UWA>yr$Ss(7#PY(u;eYq-!p;5G#78y0VBnA)?( z8Pf;n$7h1=WB7vK48(kg{F4Gls$!9OPZqS|`|$ho1M0%P0iF?v@yE#5_+k4vOhTVA zN=PK3RqVS6>mAN5$_MPh?330Lz*BfX=t0Mzz|&aP0TD;31_Zg9d_(6BG)@jNs0*?D zzwDCe0txc>`4$C+VL|hS0tZ0nldp+LrQ`C*Ds}A{+}GlLr@BJ78!PBU`{!*twP2Dt zGW)){s}SgjH66Vp0Da)QusIQvfquaJ$6hJiOW@y?BppH-RECi!?7-uPzKweCf#ah- zKJ)?i1C0wbG!Aw?#*Y!SkQzu;lp^>?_<^1o|LBrOtG5S8jKB<9J#gI_PM)*Jh z4_sS%a0{c|V@anL#0*$9!te4c;5P!}hz2M_T2CpApJsWpEM^SjBUggiJ&&~yzq*~0 zv8U8breCpg=bi&gzaKo1uW4~WMJ^rljfIssM0lrzKX?w~eF%j+Aw;Q+m59w>s7L6+ zK}j#9DsJu{a}1RQ>;4D7S1Dp*j58+470wrtXP*hSVYxO&OwgBMu_C>Ys-wmb<%G0% zLtXM3f{sN28?lE|xNpQk;rEVYyu;Qf3U*;$<2f{boRHRp{{a(`chGT?v!p6c$v@7u J_v1t;`~{ckG!Os) diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410307 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410307 deleted file mode 100644 index 28c6d05d2a6b118fd8059eea35a3a4294ee82398..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5688 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 s4b%7 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410309 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410309 deleted file mode 100644 index 884a3c530b065a5c2881d1302381aaede943f0c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2428 zcmZQzfB@xnl8Mh);<-|GzurAbV$w9}`yBk$g-_?y=-gXp{N&?Ppeo_Np3jRX?Qh&&lv_`g>?ZtK@vEEW6g zALDT?hLbiRo01ltSpl(;fe}PsEy}!^6!6_Mmwoo57p6jH@BhEJJV%dh|LToS^5TEe zWq?W?7M<ITvarwKEm9Oc&Z+ZLextackCug>#tyor#^Uzq5~d zcR=>ONgIq5p7^*=kvWC2c-O85qd7&Rx_NlE}3zMy{7H4$%Q9{N)iXJU_(lf73-Se%;GEU-TEtEZ*mle+o=+Z*X0=k7RKweW-h+= z8KhLAK&k;m0|6t%oeT~ececFilQ+!&E4JwEoA?(y|HZSOiHZJh;U1T@;i>4xyanCI zSGqdqPkXuRr^u#Iu4fV5%XsES?VGelrm-g5#Ge^x7ARa83|~7k&)A@NW*UoX_A8Zd zDV*HSN}WDRd7gPEPnu2;HUuh3VGs~I&A_1X5XeRgld~6~Vw|8bF*Y_fF#rlc#o%;` zf8bN5@;JU@)t*W_?p^gvnICZYzMn;i)jo+SpZ7KU0u?d^ga!rpxPr7m05RdhB*u3D zs+?we7+u{s8kl7@a%&Btd6xi6v|CSGz$NeW&D2c8&w|rYr2IJw~3$I zFWZZAGE|!pg~v{`ew{`{FUXYP_4!80GoOw=v5 z+B=IGSC9tQS%eAeub?LF@YsD>?`1+^bv!t=U2Ghpv24r<9M)=w%RXW#fIYZS(RJkc*j*U(gq9 z0kI(9R1T0%Ves*G0MXCZ-cz&K>EadoZ%*mXGpmg@r#%TSUbg*fjiuZDXXXb3r}NC_ zf4H?hw{{^UOPW(sY8bAy7#QgMip+1_q5s zKsGquK>9!+Y0=rMKnf(rSx{VHWefzCAPIyzu=*7Lz^6>*aeT+BJ(YIcyXu)TKj83v zKZ_8neG*eX?`!r2s%Hua4GQpa1?vS9>8GwGSIpuo?cMq-UvF|0huf(Vi`V5D0v5*W zv}P{8_8F{b#Lw5c3TF`Dg-{GkA>2>E zOv2`al`)ywfs6;6&lno);tb+&$j)=vlGQrNZhqA1ZwqR!9^ZS5Lwx@{E9Dif<@b%e z+(Bx9fGT#nhIoJtQ~Oi(uS~tvOl$R@O-56ix~!tt|Ga*Ban=^szE=v{gfD>=QOV9o zrwFid6Vn{tPx-f_ZcpTa*uZ~h+LVq<)L%HqqM*|}Ev)c{^Kq!1jF9+Xa5%|i9Cq%d zm%gUrg!F0tpCpP*fSCLsQ6SX-RnG`RoU@#^NpC^%X?-&aFQ^WS!1L3#qd&_`I_C{sX|80X@z~^vaF%*oIvxK zd(X?Sl6TZgepyn&%C52Cv`!||RH6Y4}tpNdO>EuEJ0<0WWWF%k1!69hU7!2{lGX{0hMC}mG3Zh zM3jXfH(AiwO(3_!!fUX(jgs(s1oRg*;t(7rNNovl^ey<#si0-eQ*_cUXNQ)^jJ~2N zxvSKIdXk;QdwH|UT0cSJm{A9sM&ac$QX1vO!G^^JOb-JwW%uIL@0rm0p$=#_C?CNA znQ0enKhWQEp$4JE1#xcrW=mr?VNJV(#BG#>7pM}ZMjT>IyAP~#7%kKDzA&8nsrE!# zIcz4A_5B5ThiB9Uu}-@CsM;PH-`L96Ur;$%8inUC!gUyfxQx%90%$$91ZWNq)GRPX mW*PMgQ6g*ZBR3*&q+Amn!Qt{(c zFl%;FN}7y*5T{Vd{G%EBpR4^%($tQ1ey^Qj_HcsF;@z)ewlCmpFaM$zt(nxayC{0r zonJz+dqFlOEjqIaVj}}1h~642uxWR#hTqi#R_;q5Zk=$FKfF2AUFJwY^}Z&SOE%p= zB@Xt<4?`Yk_|CuK{=~$Ke?_|Z*O;JxkB;n6Tr-c)Hb-Xf`h&51mx&esbhS|o*sy}h zk?V!x3*!s3S)Zqb))nt$V>}(98TEeA_B$(D>b(WGabCV0cS8ATQ~7C==!(r-Qg2Pz zzwrM~zxPjjB(&>psORZuM4#pfdsEbKBbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}41XtQ`eF!X7QEwZvB<7H#v&K?No`y>+%c%3*&WK zGZ$a`3{oahAk_d>&IoZQgG1P>6#^R=_>4LWf6 zwX-cqUca$5TKMNBX(waz^ZhsGS_*A@5_zSfWV^^$W}tbD-6t3yiypbLqsBJ%oew3H`9SPc7x)hZAoVae0J*=QY*4r|1Jex~lutw$Glr2;HwZxO2gcD_s2n3GEkoHvx@pQE8oLSPc35}~Hn&j{ UUZA`}jW`5{2~xQUi#{+70EVfcbN~PV diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410312 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410312 deleted file mode 100644 index c56b241904a673d288dcf37130360093ea2c895e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1472 zcmZQzfPjDDZ#K)W`L%SXZbQnoo6J3(|MoYm{;)k(``KCfGdBY(fvSW*9FUtQWuMWs zK!{T@>*mWHHM@dEx1?7sT@=1OtWoe-{oN$(NAsCXwiVd^xT@t4s5I-Otze|JT-Ow? zkGIwH-YHei;mm zVauWVK-$0n7@rq`43JqI1;qtch9(B4P(DZ=9LFzhFPFawu+Z1OX! zrE8AQ5O{L_{<4f4Ky^%xA-<78ARSPUe(G9s#Vo$k-mSm#^(IGgxScAocwL?$U}3yY zYv$r>pFzqb3ZxpK${8Wj?U|J_uQR&XDM&a{byg!&o?h`c+U+qkMYFZGwcr*`kU0x(v6c} zFn5c5@b=FWvg4Lr(ofA|uTD&a*r_h?9|%C|VQv6&e?i%xaAgLj8+It4h%jalm+_hM z2O7@Hfckh~dOzH!1vnnzJdoWW0JR?&N1LE>jG(j(WfSS9+&46K6Ugnb@EUAx Wqa?gQd4(Er2o4jZauXJPU>X2WT&zU^ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410313 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410313 deleted file mode 100644 index 2f97426227a57e031390eba64f1aeb85fbd68687..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 520 zcmZQzfPnvQ>-cBv-Z>-KfBhe(q?IOD%MbMiZJD?@S1~3zXO*@JP?hk%@Hd-f*Zf+# zQ@0`I+D+!3&VTzGR)5$YtNrY({F$49m7lLnS6&lvcw4Ks;psK%6OMCN8Zd=SX#ebS zp+d>mtL+rXrldt@_Cai9UBFrPPV$F0hq}ug38>!J#B#}| z8>qxVd&bS_M;4ZD(q4D@?NhDEGyZ6rfA71a#`H*JSKR6PIgL$U_MaAKU38XP@Amf2 zKP_TUAN@%Z=F2YMe)dL1PYyJ5N+k;eXwow@)VGZnU7!4 zk6!{}LBOdTAf3YC zna%%jYqz}mLX)5C^iL%`+fvrP^3Rs?|K}`p=NH^eG7s<3FwyD#`H3Or-`~`rm-cBv-Z>-K zfBhe(q?IOD%MbMiZJD?@S1~3zXO*@}#oGfila4QFU1YFzZ-4AbM-8z^2``8h%#~Sh+8KxOKuw{_y5dcbOvr)%%)QF4=Sg zl{l2Qt}0jEeBfu{mG8;S8d!T)YpqZC;)NaxwGq3;GEh zAQl9i$^p_T3_jitAo|(bdukRtUA#j7%_-e^X0_4gv?syE%eH^5v2?rt%=}>Bbe`G# z54U#9t1mS9xlaF7!m}-9?JNImDgS@YLU(?_%_Q^i9t{(n-k+ZsQvUr-4T@>tl07{q zu|?To_O;@h8T{og53i>_Tcy;saO#_;5+jkolap0X8DG^3?^5!3%pU!C8^lcv420qe zs16kVOd$0I8WUS%ga0pEAj$5`_N7$S`hsFiSMvj*Sm_yO-ufLa11bUfJnSR`gW?h( z8ys&SeINjg&qojzT$~rkXAJd*h;pdD*wG*?!X1nOu3)7MXFP?{Y(yvK{@EJEu)4@e^PDC9?B*u#oLHN#&D#$3SIU1N0I7%ad4b%2P&Oz|nHfY|K=NQfM7WB3WS0Gb#_?jH zCT6HsFa@&&$U$PlRe;kFoCnhjwI7&1I-zompnL>VN2Hr((AZ5Nw?o5g(7BD0@B-yi QD#RfqOpwZKSoDEu09c~O)c^nh diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410315 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410315 deleted file mode 100644 index 166968ec0de3c4931acb5e84ed9f60017018f0ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1728 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 Tp!`COI0T0YQrQWMJ}?ac?kJsL diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410318 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410318 deleted file mode 100644 index d51a3f646882f4ebd9cfbf7d9d26bc1b4acf47a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4892 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 BU?2bh diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410320 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410320 deleted file mode 100644 index dd8d016d948bb5b4c0a5826c237b30541ddcc245..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4884 zcmZQzfPg!x`fhU*HI>%2X@B`6+OjU=t|-getozcplnnV>b2_#IRS7RLIntVvzauBQ z;^9OKfxMIo!$)!!)iN?N;lU7`DT!A6grQ=i*}Wc*6;QatYTW1Uf4 z_;z5h0Nane{p$^9^3xa8=d6E|D?+R zl{l@%%%O!-fKe^|ET$nsrJ zZd7tQJkqh+Um~oac{41xo8hC}?&MS3xfmZp%1>|Dp;}`S` zEkG;?IF$pWQy6@_9YFN6wfEF4cDi_l{+m;}^UP|a&1p}9i{lw^QaHJtl{$Tt@;viSo-~~xYzS16!XO}ant?&% zIgkyGH;_IMNLqA`6G(x?I17pktc;CK%t0D}0HzL1r}zgxWh#&3J67$fwBz1Y&y@KA zhwuAYgjns9nDTjFvoBCRQ$T1?fR8I!FPKO_buGDK7GG)a)?fL0lcPA?PL)`^F3%9K zFkYuMbMdv$AVo|`Q7{dRP`5fLeL7kkvpOnIz@$%S7V9=cg_ovWT~X~muRe)rWOnT; z^X^D<;>Cig@k=M@^FOAmE(m|I%+~Prrd`iwdTm_4sp;cu zW3CSm%0+!Ma~}R!{b*;f5YH`*C}#fVZ?etmVOuOt=Dpset+vTl!DRDuR-k!cKeP)a z-R0s|S!A@YXo=&5aP4pMG93J0KdU`De4{RTedTAcAGj6Q0M$a5Qfa=6O%3i>fGlAU#^!Hs)k!68vx_Uh~EIle(w$}OZa`w3O zcTciiu+_PBzhs%;bW1r_vw1hJe_J8p^0D5S^Ol#JaN5*~-wZRIeX%^p=Hl_|8-PY0Yt`6fg2=iRK$#n2@wsvE$?dF+qu+PLfJ`8%5n(8_F+uG%XHT z@N9X!f9Eu{?Q6Jz4g-fD!F0>mwg4D3TN#+X_X72xgdec1`vVkXx#Q<7W^%yq-jj!vU%fte2pDP&;1QUM_zVV5=Fb$d6c zoMDMwclUQa??R>8_mjnE@9;2q{%@jt5S#CcB%3FqhkxF5gIWqMVS#LvaD>W%;)9t% zH1`ga&p)S#5d7pFp?ga+d(uI_cJ`}{ryXN zpN#bR1M_1p_8soh+&Q~Hfw}yz#ZzUL#SZ)rp!!q(0|Al|j6m)$s2nU^g6d5$Afn6@ z)97qh0=fuP&h`K`@j|tNDI|9yG2tq3r2(-0z%+0dsuCq`h;vihJ{r3T__Z$FeVf(+d2z!Li|CN5+?1?ce4lwO>m<(X+Z9 z8sFH`=s&0&EX?6)l!!Jm1N-__E1>P4mj9;AK0Be#fvMNdSpShTD+Z1jM*W>QadeG~O}nMG`<_lI$jwwhht! QY9xQaZ3Hr~IRqvT0Le9fVE_OC diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410321 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410321 deleted file mode 100644 index 9be013ba346500d092b18a5b0735035fe4a4cb2d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6140 zcmd5=3pkY78~ab zEA_ZnxvX*{({1lh|Jk7tMBB7$D6t_SB`7TY1WkeHCkwP>+kJE?r@b{*3!<058j*`E z@O>yJnor{?x2ft$NBPHGX2qMv-t{Y8`FVhsQs}JySsJ0Y3WK;!feY3}#$RyqZ-$<> zWYs+$^N0{&pR=@HvF!BjPaHHEo}TCax3VfvSKrS>&isgON^$MZ7Y0CuENfXblFa3> zyLsK$8Hs~$^zL5&Wsylsie@om^?Z_^+KS|2I~zL(mMrJ{4JX|Q^wZ~t16(bK z2iT{EiC%>x_NDX94nLqBU@%{ov#t!6=*IuzXZ40+6VLKa_U6t{{gSo5H@YQW{Ha>g z@Nni}e^Ka3lZ01YrTI*%hbcHH>CWQ#&($*xhYECSyn2d459f{) z(cj)S(M}J%zs1=7h=!ro(V>?Jb@cx}>oy;lv#2)JJ8}s*r6u^juQc7Vv(=|3Pc<$x z>FL1`{ZC!k32HS4%_R3YRhzC^o?Vy5Fd@ifxdM7(;ExFP_4-zeocd{TU9za!j^3q% zRB;Il)mVGgZ`XdyW`A*9Ru8C95gGYf1kv~n$Y|c+JlM8m-IfL&d?y|l9B5`>U}$6t z2maPW^{GxSkBIbdq;7tP^>&82}B=+{P zojWZYsI5+?J^{ASHB1_JLUaajTZ|C@JO=%PKSA4)RS%y<+Z`?B4J#eF-HnY(I{TFU z7X~jikNm^Wx~X)(Ek)%N8R$Sgbnc}7_ro(U`+@D06XNVz!%3jB0jiy6)_Y?sP)h!*wVRq2~*2Q&6JSO45LTcIM&Y)MWOou_YW z4Rt`97N1tGHmI>>Bl|`})pr|_&W>EvPdpfB=6QaowU$XMvO3nQa!IxcRhlNB9&0wD z=1y!QKTjMj2`lrc{Mu!-E?Tu(F}&fnsEXF1UG`ls)qdRYGs7iTkH7IxR5e#60_pP(thMZR+-KX zR2BXZPz<1M#BDFO?=E*z@fgaFMA9PWy7V~@oNW)6IqP_Ke|Uyyr)lAHM`tn5sQAk_ z=6|z!KrXL5*J!yqySA7Tnctrs1u=+8M(Ef+__4Yo;J~eRy+dio`r4Gq_HT!aF54k35V&4#|&eF*s*whY}h^|UJ+nj(~(0oCb;t?nm&yr zjwM^6lyjn`gW%=UqMn0159eiCQNrfZ?;Ng{I?m5AmCs#_GX(JbhChbI9PaCA6Jk%3 z*NBZS2W*7<=ut2UtF!2Q%n5kFiRcjg`6rH|{=xcw3I~FeiM?Vs5 z9}+Kk7MhM6PRLQ2TiQ3DOQ>5(aI6@=-%RsL5f8ZH-S^tqUe@E`acSr8`1wZv+W<1g z8CaXKhQ(aKQ6cshWIgd06R5(n*B{Pg^LXLhU z*xupj6fup@2|jUC$>Dg8!ns&};TUR52yDWiT`(s7eTKo~h35l7EfK;m;I)85GZ&K& z!-QYobma-LeiYJF*h6afzr0c7?dY`?#VnGwL)U35iF(m9Ijl{$`C*=Dufxu1FkUd; zf;u9^T>vuce4Zu$J^%w3nG6f{p(L*rS|_ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410322 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410322 deleted file mode 100644 index a27642a4367d01c08bea87be95b9955a317b9079..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4628 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 V2`^B4oEmWm4iluh5+3Ow1_1bCZ?XUY diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410325 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410325 deleted file mode 100644 index 26e0997a8d17472f44492f56854ff512ef8e94dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5032 zcmZQzfPnq$?nUjLnc{wG+4cIQ(`Bqrn`XJMTi-ra^g!&Mh2E7{fvSWz{_R^g!RKj?^*ixYdKH0 z*%f)d>dzXGO-YN+{DIiWzzCwZ#tLlOU8~`D^?;T8(uZ3ooa7I04t19~5>UObiRF?_ zH&BT~*fdU^w_oHFHTRcJ-EJTh{Jgey?a%X$my(vO-*x1{!~O$SAq@WZ3V9niH;EVq zNMC$0;k&qTT#OFujI`gqn|E>Wc#5$Wg`~@CDAq4D{k7II@_63Q6PL_v4=jJV&+G7- zOTG!a{>Qw&-!kLG+@5k1*QC`^@}E;oj(AE(Y@3yF@`e?IXsamigKe9ar+{3{eEfob z@fRU@y!hWa+inKQ=hF;>RLGUO;d@HNZ`rIs;7*vYK3n%p^>%CJD*kCIzMXt;v$tO7h1o4n-O?6JYm~~mb$s{!$i+7p_?;YpVITlg z57Gk!;P`_u85n~8Cwt0I=95TM&2r;DmUFr1N~M^tVbAOdMV4$GlfET`)XAPrg(+tQ zn+uFv_5B8p3pZDu7O;^qm^EoabJo@FoXX0Y8qxi+OLp&2kKd#A*4)y<@^qZd3#o&4 zQ#20lJHp`N_sjL`zQ{>ZuM1g$27>)C*F}%rMJD7|{Q8xL{%tLb{dYX4Zd!-wj!ja` z3@RLJra}E63{wwv2*?j$e-PBqz`zDl8{%yQ)~d(vd)HR@z@q@IyR7?^;*>wkwQ{Y# z-nj2*2BrSd-rMkw(9I!%Fyz+Ucg=CFvvi#e+^$dGSAqc zcxD=lYW6FYZz-JI&Pts=N_n1nCr_GA5HsCA2F5@Es2H41@eh2;R3687tlCp)$GxkbDf0sk-}kc!vDzmw<@3H~U!X#!fY6`- zA6JkT2uMG5ExBSAUuo~wU-^2Iqd447l~}wk&k(RMUZ*v4@wLxjRWqhFK$SB>-RjW1 z^X3|+?k4_pcFq4{Q(v3j5Wc_Rx^T4g#l=@cep}6(ba1}#N5SM^qv_js|I-qZ+92NE zVz?u7xyoXt)X#@x7s1m(*Rd7&Rx_NlE}3zMy{7H4$%Q9{N)iXJU_(lc+88ARhm`D^zH9*xfLfpyVu4AvwBEH=QOIC>X8*-XMDW{0A z7nrW$X^3`Vf|S3>a1*lskTV~OI5b$${eT=7$bCD89h_E~i?@5birjbLpY^Tv(^1ug zuz*JG%#VBLEH7)cx(qchf}^4#FtubC9?Vr5qvAO`t|BHR2Gx-2smDIKNl%>l+vT ziR`!_y1Vn+_OIvdm%ZI%*6_UJ=831eY4jIV4i@GdP(B0U{s)7&j8E{8gYm< IjlyCc0J4>EGXMYp diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410326 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410326 deleted file mode 100644 index a09965dc8d64f9d19681d88d6e338bc30c3f7a44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5648 zcmZQzfPlWeZ(Y6^R2_KveX+w-Rh?U3;~RsoGTk=P(zX+^TWI_js7iSMx_eQ3XQsHH zT6VoY>2w+E)23PO>(;kV6+IBUXQ6lH)tSfYV(+g{UOe$kuAmg(6W+-UQy5~OC+KWm z5S#aK9;Fc;=b&0y{R$jySc~<2M{pAmq_U&@8?>ebFm*K+E)@iS*>Yjw82gG=<|Fvzk zdFIiS*`B% zUiLS0=EG@Cw^g&kokO_ZSUd8E?>K95@bBHJx|Xja^ENSvwul#E|muZ)#9X1DEXS zIf*UG4zsTn-^}1IcX@a{_1P+=u7y+IG?f^M1fHC%ddm2!R(O|^$7A;B$J-!oVqhQ? zS3q^FKn#ux!`F_?Gd3umnZ}};{YvFq3MaR-Qm2nno@d_4lcp1d4MF~55D+`fz@YIK z$Ogw7kOqUKMd!4D43HRSL2-eVv9Ym<2}BB}4os)`2R>ygkK;R5?Wwfm-c`?(`2mOT z`&ooo?UR`Dd0(?HPzzH)Xi$KUD?~GhOh0ukxndSyY46rw`FfM1INVN^SiCOJ5U?;_ zr!{l&wa*|m%2)CXfKC3nA)^d2b6lhFrjSc?4Y=I=ZGuxL^ zRqG3iF}ZgbP1q_dBhbiK=Hnn_XQ6Csbl*RMSE zZ);iXzvDS|(>hFdY?5MTP~liJ4WvHA+X$qdfuLJ}ZVuso0#*wo7{TrXmSv(X3{E9i zRTLYpO}p*$l)K*VpnQhTY>#gz+4Kq`GxBHgJ(TBApPQM#Z>lt7i{iv1o9>>PE%&)h z?e*uKA>6Pu;`lhI>db9WF)FovxJ*$;CFhz6!j8K@WsD9;+27??u&AZf7Lm$sM7-vrodhI7^>Gw!h0v^_TY znbp!Y$7cvUIe&jy#too4CdUxp$RLmoC?F=kN)$*nK$SB>+{xf@N#^b6qg@iFn@a5p zQ_Nz{U0YVATewqBXIsX-q>?|XY8u^s)!wd*`%X=NZj>#WxKQHA%gaGej-y?5u2hQY8urYdP-My0G3i?}G<}Ey)gy-oGMnL$ z+pk+jU)fIUynWi5mRvsni@fdKzq|I&c~$;;Q{m$zDUjQletl>F(LlfmHWwJLx38Zt zX12?fYW#g){m1GY(XuBqy#x$@O5cc8C}j|5W&fpWFuiEslhh5DxK4cSf6rx@?x|nz zyW)$>&5hNod9}HLX0dwc-<)LWebV4!n6Hk}4;}4=HS?p+$rygMa(TLS?%qis!8U@+ z954&!ZWs*`1cd`LG!K9k5s^+I{(I>hBd9)vsUgyQ8DuxX^nz$C=ELF> z_wqx&OO`t%B0t6d+Y5(kE*7g-?Ze!rWaG>GxnA#syVNOuZ@Yky$naT8A$I>Vw1!)BvP_L1My8hm<*R9!#%jE7<;| zMQ31bL|B-Dl@d{xgWLpcLV?>Av~m;HJT^$&MoD;q+F{g)LvXo+lrObG?#-h%o_Ab_4m8G&i^7eor4MnQQS3<%fb4B|3A zxgF5Dd<9Sw4^%6dLQ0rOOt=bMU>f}cRf!VjM7l|Z#%{uzMhA)8C(d+Y^glwN$$ENFbEz~ddtK%!vfC_If4 zY*zs_%braI)n{-$NamqbFi>%n@FULrlsP1rk2USkz#dBcfstR41dy2Uki?aj(bEA) zFQ^PeX+M$ZcMSWH1dy0yxDBaIM~s`KE|q}Vj-YgfVK0&Z5|d;%!P{XVokX|)k^BL- z5y-&ikX_EI0ss0F!&23lu4nE!&zbf$cK5X^hJTh1MD=<6mlQztW2-mN+cxn0MZ}m5 z1N-_F0nl-qH$XdBakvwklX0aru>H8|4dUFi{0)uWgf;IE61Sn07bLprEtTRBy}Uq9 G77PGOi7g!f diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410327 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410327 deleted file mode 100644 index 9f1ccd3ec30a627c95dc0fba75b99ab2eabfcbaa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7596 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 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410329 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410329 deleted file mode 100644 index 5a9e0f15d510beb5bd3009ff2f4271ad334f077d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4244 zcmZQzfB?aS)PL_zi3zjM33^%I0~(ZX7cg&8$AF zw9d=ytOUrWq(x_iAvQ8Fg6OM7nKzRHzI*1f&wlj6RLJc8{}-3%=&|izz0pZt{7I9dk^n#JrC;8^={@vs@r)TyJF5*iS8P9c^2q9wmM(FfMeeGrI9?lc7|G4vq|CJO zsO#dbr>^PP>EadoZ%*mXGpmg@r#%TSUbg*fjiuZDXXXb3r}NC_ zf4H?Pgq5e`b}Tr|Y5R3^p3tKu8zLBHOlts!fjCep zD9*s~2GR!t!1&aLu;AhxAifui{I&pL{=dsU6y=|}YI^SXr+_Ho4-;1=tkO9(vu2_q zOb^(6U>wVZytv;rdF|pYy8x^`^qdNm4*X;vQu$;P!ys0QB#6!;J^)8}xShzMgV1q`O%`WVgYQ zSG&aR7p%3AweDa-wMRfg}lbskyPA=71E+o=t_RX9SxI^n=F6KeAg5Q<+5~d462^ zIQi}TmG>fJ?f1?}+~>DpU4-}DyG{A+Rw1Xf_kLdWoOe@7e$CF$SKHF_9xc7>Zo2zb z2^Y{nX7xf1dC&hGZS$+n2u9?^ZSDOVwV~E5;Dyn|@YGGg<*i^#L2(8Fu(StGV?ZHs zkMyMnpmLz_V1}ksun@s?4T~E{+J+i~pkS6EM1W}=SQfzeM41nAAMxgc`~dR@E$yMi z9~i-cB!I+(g%Tv4!g;WC2n|QH`T&%!L2*c~-!be*5v711Cg@qS9-47DCQ4(ID`UWMgkT^(8 zf^i5A6SQ&yQkOyU0oW`e!i$tT8|Elj-Hq&Cl=6uPdl{qb7u;U-Y}g99Yb zA~E68C~ASC$Z>((zGqN4ct3LK`8j&#C3QR4bXu20Go(e zj=(JfGGO@vUXBoK?*Qv`o;$J@&@vd^t;h+I6u;puM~HIMnOi?->?V+3Vc`WYM+S-8 zP|6V!-2_QrNT~uT;*gkd1?X{(D}93Wg5*&0^Pq7XQhp}FO=9o&2TRsVG#&s{_1={gT*p4Qi%r#5Sea*xb`LvO9KYTRmBw>13-j>&B( zYwKLQLjYt`(xS5x5E~g7LG;z4%$rF8-#v5LXFqyjDrENl|BK6W^w{>V-smJR{wG}q zsKlW$mUq%aPS>^5Hmo|vvS~qr!O1N*Z(WzoTD_N1`pMRdCubBaTdHKY?)l9}QrlHJ z)?B*8^|WYxpW>+(J5RXG{awW9%y4ww>zbU73rC)Q-&RoV6!|^(5A%)MrQhcy3NL&2 zclYg+CMUkG3O=*{`~Q$vUyuI(d0c&EeMsQC9P$4<(jr{a7(`p8cpq%rygUWuV&>x) zs!wkLu^`}74vU-@TC`Tuhky7LQeCYgu#Xqf2q{`|y{^6zhIP)q}t z?CCj)Ey@nFuNB|S;4gQ1cs=#mDy6Q4Q{Oa|7>NX)oUD4v_^MWTmy*Y0_UOmkAZ}t{ zAQV?Xbxcsq5cEIUQ+_g^M4D=r8~3rC%RN^r#dHmOW=|-xWb2spEt!Ge$pIJ!qCoY` zKnw$*_yflwLH!I2Y#_BE-iBbk!NRKl`p){suU1Q6FMmF))$L%vf#MeeVEzTg7Yu;Hj2RgJpzr_#BK*%F zdo~pq&Q4l!_qp)AGEZG5`SO>3z7g56BbI4v<&CL z@+Q=OXyq&<-$LZTW)Ts_@cc?d`h__ImX49#OG%g`n*+83NdSonvz~-7AwL~}>_xE~ ziG#!>*-Z>zTRq%N5v74}# z34_FKl!O;3ucO2j5(kM%Fb=`xB%yMIi131!BSh5uFh>zAM~JYOK?;$EXcs0(

#W b0;NGXK#CkBCR`dtEl?D>Oh;~4!!-Z^NQu`g diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410331 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410331 deleted file mode 100644 index 2dd6f71e8861f310ded735d26e1b1340a3a5cd80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3124 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 Sx~Unci5hW;UN?c=0wVzQ5z(Lk diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410333 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410333 deleted file mode 100644 index 96e0cb0c76c969778c57e6d5a7cabc1fe316e678..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3608 zcmZQzfPlhRYa`S;6D7At?lUm)kBxn>Zh64vdgC(grOq-MQu73Ws)V1;f8WR@vNw{6 zP4#!7UhI0Qvy8g}a-*Yt%)UOhX$fcCKmWrmx9{IJ>ZY-!%<%H72t1@2*YoSec9*W= z4a@76e+JozeTCjfPq1@RhIX`w#~~^KrUuJeqqN8 z8xRWuPUQjV6b2t}2N3;i?L9S%oi1LX|K^nLJhR$pbJ~;O;$_>v)>yjTe`bC#a5~Ry z{)bz;<<%FO{9LDhD&g6dvi6mKwv_)rXQ4a4;AWC}c#npOPVdi83@QKqrUu0{aLJyY zlh~r{F#B5Z%?$o>mxtF=pRH2rS~&GhQ;CsC;K|9Vr;M*^g?A}=JZ6u6ybaREso z20-x#jzfa_85np#YD2t@z#m)sh^?%Q}=PMc(%A-(CCXyefaasqk@<6i}_WN7)OI9U#C6b_3AA%k~CObg`{jXW^Kj zeiYk4guge1~NhJ zhXGKSF$2>CD1U+h5&mb8J)0;2)B_4bc(_5eLlP2%1G5h%1){-mge1TWW<%`C9aS-NK9C4;EY3Xn4r}Qu=oYZfz2W! zE=kE-Fh{}i7_xgo>6+YlqFtC!<|dH4-~h?9NKCjiidvv3a$F$S-wgWqH+-IR@Y$Dy z*^-ZhGOzfAC8ZUMFdG(6Q#F>{DEP(8Swg~u_mbqHHuc>_>0 zsQv)eBgg>cPf%P-zyyhLht#Fw5TG7VT?h&nl(0eyOe7}EbX2ua{h+o5*nVL8=Y^_7 z$&*C6>9C;)jok$DCoH_+d1H{cjgs&J*N5;J0y2;y4v7g@ffC<9QCxWgq?Z*ekCbmo z3@_TbjWRbu{0t2eq(A~Qp=~y(EY38KUgm?_*g#F-@FgakXiy%2$^tQ<8^Hb`!VRR9 nMNA>wPZ$_BF507a6dW{2!3MMs5+C%k7ujwGX!!_>FE|YV+EPsx diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410334 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410334 deleted file mode 100644 index cb69dbd7c2dfd50b10eb0cdda083c6f414debeaa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3660 zcmZQzfPi49gXWg!|L%4b=Jr$ZT@!MaC27msc!n~itG-OL)Mm8+RS6fqS{tF(nJBqE za-V^Te{Af7b;|=T*Bh5{FLjpDkeVmJai{(N49^J-=2QMG_A=TX_VHK6Q{UssFIrFU zn8sAJ&=zD<(xS7v5E~g7LG;z4%$rF8-#v5LXFqyjDrENl|BK6W^w{>V-smJR{wG}q zsKkL?SYywY&QFd4Q?@7W)U=WnD6Z-O?l9$2%wO#uvtyLE4*Vv9%=svh(xmnNe-fCgq#}iL)TD0)M zm$yAr68bp(*iUZDJ!~$R%)O&U?#tQ(Ti!OuzxGvos@TdP+A7EUVB6;9DIga!AHT5s zYYm760jKhTbP9uyw*!cNw)UQy#ZDKm(0_AEcb-{ov^nibaPhM3Uu!Jg?msg>7&x70 zHvhw|-SX-SO@6M^Kb7!oOIiEMKU>QGpR>@NUvM+YJiJH4M5p)XCx(=Ne^Y~E8n|Rn z&q-`ic9?yw_+|!wxy!@rsn1p^buFCwrm4h8B=F>9)l_e9Uyew5!I;O<{%uPz^{Q zST8~Qpz2=QUM_zVV5=FpMmNmifV4blrw_;2TWrND(-d~ zzI?juoWSPf__s$--4e1kce4`wBGI>K?&Wt^jr_ADI@IO2A4%HP7 zt0f}dyXm*w&tnD}2ntVz-KyKf^)#Z59TPw8xqmgrQfO-O;VGLY^iDH+bZ5DkSTqB_ zlLIh42?CX}fYd_)JZuQ+XJFs~>R}A=HUw)87FPY&ch)z4wOaam`SW2Nw=@eBml}Ui zG0?8h&X_uX14OM6SnVOVU$>0DvYpm>`?NJJxqSW?dE2{xckQ3^s{Hk)!pBKcAVq9_ z*(EaLp*@LOH{m40B0f$8o)5CGY*G{*?!{(?FR6yD6huoQ;!38!NQ1~HA!)=5C) zKxugbP#-T$FNlU&g35%e0LL$!2eKOkp!O#%I`auC#|SDfplssY)cy~sm!@ul<#kYa z4K}w?5?-M4h#GMS4iluh2^@XDH0GD@owUkzu6#(AdNJoQY3WZ677ghwHLtiPR(Ugg z04oBQjSv=?gybD46G9P@r-2H=^$5-KHM=?=$T6V22g=*X0G5v=VS>cCSL(1~1u!jv z$}xD^2h&exJ_6ehj00GHMoAY$x#{4l8#Hzk)_gQb+(t=wf$AcZ@{P+2#yP4!f61@4NzSI%Clgv6X8~1*+zUj1RAX3 z9%U~;Or+oivLRtaFMGlEK-)!7XTd2@7>K~!M_d>X-ELzF;eNuvuyN5Iy`y0JC=3IN Q>_xVl0a};90|CYW04PMe8~^|S diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410335 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410335 deleted file mode 100644 index 5df72efbf5438932bbd3077042e02bf9047c18bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4236 zcmZQzfPl$A&oWi6>d9@~-)P9%_H4z^6`4=T3O`?G9wmPis7g54>7cpg`M8dZ&EVWrJlKE5i?A-g{s`2`+*RyRsZ7TJ`?`>OrbjS6e zbsVBxw+?`8N?LT*2t)$`BZ$} zKqU^b#Vgh;pPr)H@Ha7`jw|%qy~@&5*FSqyoPA!aS!UMU`lII^WMs?R`(^bj?QP=R z4VtGO9A8mZ@Nsq?W9(CH10K80llXRO1^HWg%snA}UN&W+#FTD7$NA?Lbrj#9s_f^R z!n0~y(hKjUljZN3C*)3A`gNKwQ%a2rV=BOz>||zPZ?j;3hz?#c+4LCcpJn`3=D+g z3aE}5h?zj@4PQGl&)A@NW*UoX_A8ZdDV*HSN}WDRd7gPEPnu2;HUuh3VGs~I&A_1X z0muf&8%Q4rBrQ4@38X+`oCU=NR>sCA7N#HxggUVL6#u}dOyzNW$ErP*cHFz_nKD1% z@O?jv5UYI>Q$Fu&_64eE3J47f@Nos}1rzC~t|eE@;w$al`YT^=aukQ#sS=CVQ;wK@oy#_Q@BzjZJ0GP<6Babw$90)Tf^H0l~!%~as1w= zk6*uTIC}Hc+sC(bHs~F>H|Muz%+1IKzI4wMvq}z&^rvwE4Frb^K{qqDEdaV;D+AN_ zNg$`d90H<&>D&iMvE1=<7Be~Eckju?i;{X((m684Dz+^jGu6iwy0-Wkoks;CoKb<}44ZeOEMtuL0cJ(zJhDv;^%y}0#{ z4zX%ihtzWz7;Nod?H6WIwA^d9+Wg9goInG?VHxy4*;9TppG2B!mK*o6oXb5|D#dgS zduC54vSjO+^evfz-^l@(&qRUhSzwL;((te$sGos>2c$m48&bR+a{G15=quZ4owrY0 z(~`^Qf04Jn`*+v=Ij_oJZz_D8Bn7jB_3J|rlN*S+U}8|35$rBt`IFj`qvq>zQNnoM z+1~x8NBbNW96PI1#wb2*-G{w-Li}s|-7Y+SEk3tL{_8Obo839=QKu(8x+YVc@?9?T zveV(;ApbMJsAn!<6mq!WwEU7n#)3*`t&>~W4qUUUJ$w48?}D1cPEb2j{sRG!4UT6J z4dniV%7M)RhLHr6PfWgLU|+vn1)6u?1NE`O^nz%ZC8$ie3UIu^d64pg0cJliee*)) z7(wMDOdSzr3D8Z;4$#<5Ah*N9Yp}VElJEkRzto6BaF`&~#o*{WV754}v%l&3J?83! z39nLeq%4;t#X7#0aQXRXHKUPo7+4XoF2IsTe?jG7VJ-*dGY~9885qPg+W&n5rbbXX zIuWQ3maf1?keNoo_5=L~s~k|moJcqI(b!E`)94^^8ztcdstc(Rhgj3-6uS@BMkhn0 z6!WM4J-Tb!8Mlca($p)t1RSMOubg|jYBe<7fm>=oHZ1vo>IM`*M12BO4{qC$T&J-0 zl{Wx2gX$Ady@Cv2`2$}65aSN1!-f^mx)I)HgBc3*Adrv5gvmncUN{d?hJ)<~<_}n1 zf)d|Ex=Dn_Zo-;B28r7!2`^Cl2IMz5K#DjdCR`dtEl?Dc20-;Ndj0_E1=S7AFgcJi z62psjZbND#kl`kXpFti10;E6!Gofubh$LFr0tIn}7c30H;Y*C0Xiy%2;tG@p!Tun^ q4WyJsOd;G)7#KD#+M{v>g$inuR3=z`ocDQVGJbBK)$j3D}IQRdC0fbX8U?6V)eFcmU;|Nq71IeKjSS8sHZ7ypwk z161O$kTL%9`uk$)o`3UZK7Ppf2W{tok%{SJE zc%N%Ysa;j^n$O_Qt8%8tJBhtNyXx1^RqMr`h^?w++Za1>&APrM>C5#sAF8Z&C_hxX z?E2z;X=*v6=)p^?Ry=rn<&Ul5{-E@#r3xQP8YAxe{)KAo>^_QIqgYs@v`k-Yb@RFKQliVIGtxU z|HG}_^6Cpsey-C$mGEp!S^LUATgv~Rv(TMia5Kp~yhp=Cr}yV4hLnGQQ-fj}xMWYy zNo-Men0>AIW(I$`%fsub&sHgQEu8wMsl-Sm@Z@CGQ^r@d!n>3_9RNKeEWXm-t-td1CP#6&ohq?-U7jIeVZ2Uj z=HhFg!767=YXH+gH$dI$uqblNH^o}%=KAgH1h+V6ns1ihETDb;$izas^pdkV1=|hN zH%*Dsoc*V4Z(sSduHv#AmpPf@?0cED7ME6AMKiGh%>#uE13@=4wk-f!yOn|I`(&VU zn14Z9LFqgYNU_}Ua~3l>;CJuI#fy@9Rnj>!#VWQfA2Z!A?W*x|Q<&iZR0GmSY#4#n zy|let{wBayGn}(7nQ@1`rtPuG&#acNIX*++$@%-sGHw9PV{#1fbO{17V1S6QlqjmX z1yjxljT46ke>Jve<*P-xum9fh^R8=#e2)0+HA`+esS4*ziIZ&knNHM-<<$rsxF7yv{v$7e%hROi99(}PddhUM z^h;79pD#K=Elv3k1VA<{92tS!Ur;$vd@uv^p*)mNIBpmi#5CIf^#P3o#m^+5K3aLyjb&iiS|7YR!X;PA*05EuYueoAM=xD|h<-7WDU>bl>q z@~h5WjJ~ka*G1qxbBn}{nvHK|pZsK>DYdPvxutBrnzN##*DSF=_OfKvrPI)%Z<+W|yBTYFEM7%^TH#$v9*^0hA8&)WiGhJo zTmjXw05KCt{i$!PVcnu#ipA153&mglXY}^Eld{`w*@b(DH2n9pdQD|u;NHQ&tkuTA zT-pLu4vsgFJ~Y4#vHlkDb4 zo&L6<=IZgiw>ZT2&$Ciq(OQ1r$jcq11_-EPr)!7@*f6y}RsYJ=OU<-a|Jh_TrK!s* zdi~Gqrx#~!aqWAhuub?9SP_-%jC6_s8#gh{;r*0jC9*7P6ccxA0xJ3Phb1Vuv z&C|jPZ#W-^+Q|rs4+e)i(ddwux*PwLKHTqhzmjp2&nv-87Pr~GA~?5}FREr_3_Q8} zb^fGHFL!69A97q4;=*b8FXq7BpUVq=hRzHZjb;U!2abEg*N)6HHYlE%#-f`2O66M$ zC%3awr;k#eXWq$^rW1q>f$CBi1jJ4=Flc-NvQgqbY0)`QjsS^q78Dm)85^5e7y&sz z08lO!>U8*%zpuDIhc`z{eG=7fhs| zx|Uoqi?6hI>#uyh$x$3`r%EhdmuCoA7_ZZsx%k>=u%a2$8lXBDp>B29*SgN@1uNI_ z`)m?(L+9$wt*Yx&+E;ph`J<-n*n2no40`!yS{B}smRA=|`0i>w_vXpTzAJqDJhLVx zhu@b8{RJ`(94-YK6I)}0|1Vo0$?nYdrBv1Wf?`Zp^8=w+=^1C<`W-C;sss8x>?8w& zBB)%0`4^-gRK`Vt83=Ju`eY3ChKO>gzSz+qDVwlWSVo|cugu3m$j(C9*66-(&yC)_ zcg%Hxs`k0O5(TGiBHY0k;0nnGXFP?{Y(yvK{@EJEu)4@e^Pz4>H8Fr-+&kn zfbkIq;CJuI#fy@9Rnj>!#VWQfA2Z!A?W*x|Q<&iZR0GmSY?=kDdue;Q z{7rzZW;kbEGUE<=P1|FWpII$kb9{!tlk@kNW!wOo$K)8|=@JBFzyJ|xTcW7u7EC!K zG>+ijH~6=cdtmbACbSzmU4V^j;)Z-{#(2BKCAwhl2YED!0uh}m1X6@ z!pko%iD{l{dA23+%$8hEqfQ>6fh^g=M>+xz{eR&TZ`{HzeZqCF;LhEBGqPICQzhOQ zR4Kx;ug-rU0IG+V=|Jv3sDpTMutD*}46G|?W*fbdb1>lJEkxDX9^MSktJeN$c(lTJKZKPi}Mgnw464 z)_zmszwIJNCttnhSaQzsH8d@tr%_Nj3T_|+ld>yxrcgNEzZny zkykqMBL-ws(xS7@5E~g7LG;#Gfla$>HT1bcy?h$mDrugPCBIV@rr|ux)uksUj2?tyUrlms>J(X+vepdAQv+qzp(q) z1P}`XP89&@6b2t}2N3;i?L9S%oi1LX|K^nLJhR$pbJ~;O;$_>v)>yjTe`bC#a5~Ry z{)bz;<<%FO{9LDhD&g6dvi6mKwv_)rXQ4a4;AWC}c#npOPVdi83@QKqrUu0{aLJyY zlh~r{F#B5Z%?$o>mxtF=pRH2rS~&GhQ;CsC;K|9Vr;M*^g?A}=JZ6u6yba&A zjyE6;21$#~WdRu=G0uYG0xM%<6LV9D6igkMPVoRNKeEWXm-t-td1CP#6&ohq?-U7jIeVZ2Uj z=HhE0w=gjM`p^KPfq)U}RtK3wN8;Ak6f<1aJfU>++FgFtH+k3Zy*qfwBK7y>NoyE0 zl0#DqIqqzkW-(#=x%?@&>-yUBIB$J8Q~xx-;6T$NO;(^;ps-;e=w`;Y1wd=JGBACg z3RDjBFGwpWoyP(xmc4V{%5B>3wdCPEo|*b4DPLTVuzk62I{ncmot%>G6HQWqYC!sk z4I{9+m$sM7-vrodhI7^>Gw!h0v^_TYnbp!Y$7cvUIe&jy#too(OpYO*enCJ63=k8R zjE1@}<&4lcamc8P)m`vk)Naz6D=!stY`M72uAbwU=?naE(df-fKaIM-j}E+FeR7Mt z@8_uS7nh|=V|u;YStP`Fs`F0#v$*3bAJ9OioHb`Se|+R!yy~6X*Lc@swNX1Z6mHMB zzFTI2_f5mCR?DE4ru+v2AR88rj6m)$s2nIhn1T6F5y~eVHw+A78ts3N0gVI2&lI3O zUYK4G4YLH530DD*e>e|hHwZxO2ga=}RE`l;HbB|Lx+#gqZUVU-ghAmo*xW`*c!Ba5 zHR2E)CP?)VIQs53%D682XnNn>fva`z=MO@zO;--G) zLFHg!4o{;*l&cKv>zDq3maU(F=77o`IG{xuCDKh(XzV6XSi!;z93LQ04-&Vbq)`&x T1gev$5r^O~K~JNgFaiMp@E?(Z diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410339 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410339 deleted file mode 100644 index 334ae0a8358552e08aee2bcce97900ae596321d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3772 zcmZQzfB?1BYnT6aT;6fm)5Sor|=I^~0%ug!K<+U!Ec>W3jsuK2aUOUy*?s&N6 zHFXExMH?S5{$dj2ea!Za$Ap&iX5Qu>8Ub`+t6GvZC{vesBFV|MZLUpq>r-M`VTE7@x?dNJlNM z5cl?Pb-T!NviJSOEzb+;d06iXcHHTkxp~t@_9Y1ytFGT#tY>sJIBrNJ z2-fOvsC2Ely@tU?AwjsM+~l$FxyU`A7}IY}^|Nl5EoKmHRpx!LZS(RJkc*j*U)cS7 z4~PW;rwV{{3WJZg1BiaM_MV!>P8YAxe{)KAo>^_QIqgYs@v`k-Yb@RFKQliVIGtxU z|HG}_^6Cpsey-C$mGEp!S^LUATgv~Rv(TMia5Kp~yhp=Cr}yV4hLnGQQ-fj}xMWYy zNo-Men0>AIW(I$`%fsub&sHgQEu8wMsl-Sm@Z@CGQ^r@d!n>3_9b{0|c2EGRCpGB!3b2WbETm^v_>;ve{wsXUJFShc6pj(b-y}7ENv$6aCW7sw_9=Nm_@otvder$+b4G zgN;|ZC$n*We2`kVVaoOgoprR#g}S57m>H4c!mAVUednXzpF(3@Ksn7&T~ zDu?+Oq!pCT6M+=V-Z^jOHtqLX@^Bu{OnsA-FRn+}zT7vR{%DgR$&g+&{qsG!Pt?oJ-iRWd51j`BS`M_wjkYk5nGNOKtsE zSv7-m{>8H8H_kHfJ2^0LD>eevPX=PJKY=tl0EvU;1Zpp4>sT0jH0d?UeP4B6C3 ze1?#skjr**xou$&=D;+X_aEI7BR^Y`Ym?X2B9V}$l7k%K!9AzS{LeNDDE)b~6R1wy zqwEDtITP3|z%r+?K3|>X$V67h^ttipA6ZE z@!!3dk?rqEzeQhtZDpTEa>JE? zckgyhzwk~mOT$X<&?>g<<181y9<8+2xwCHJPUFYsAtxAZdiGoF&D`C@3UnCr*L5=A zLpDB6zOnUW-@#vxb>HZ#6n0MZa^Gv(`tDf1rytbuDgS{0$cBX-Bar(GDhG-KW?(s{ z1mzP>+YAh18ts3RfX0E+_f()hUYK4G4YLH530DD*b2txD)-b^A2j&fDs2n4xY=x;K znBTx|`ty&*ZUVU-ghAmo*xW`*c!A0&YQ!NpOpxkvaP+CP_T{zKS?i_8U$x);Nvh<) z)h?;MJEKia{}wZrZc%&%Rs<|kv7}L8e1ZWi%$1;gV(KLZ_Vr7rK`=a+3j#-Gnub4idMaq)`&x1ZrPUBMz~qQJoVhyJufq`BtgVuyq?-=;Vj;Z!7|T z9qZ*ytynX4iGMFN;ecCtKsHkHfhQ?w8qEdOv0y;3UIVJK`f+UnUqM!#*Nt7@r(oIG*b`#b#I!N3`NqB+UCnx|Z;*gkdB{<>`Qnw(L zqaeMY_yyHF;xOF=+d1oQ+VZGu5(TQ?08FwIVESM*76+mDoe1+)53fB%ius^=9-RL` TZXmk-g`@}Ob`Xul{cvdjVS=py diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410340 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410340 deleted file mode 100644 index fe15d2001e013d07a703fc6214c48781f6dbfaf9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1476 zcmZQzfPmnT+e;auA5WWp_ulb;iB~@s*K9kk>b3g=vw-TxEZ6WTpekXt)N7ajc3j?Z z*we}@TIy!j)aLKK7R*m7&E>T&nRxyR30}S3`hxLk-EW7T6b<)W_?5S{v-8&PeEtPD zVy-OcyHW+RDQVGJKZuPCj39c2%2|m$b4%HLHD^UfuUTS!c$L0UriG|&kN)hfr*l6T z1C==Rw%F@(&$$%sQdzWN^8|i-qXS%Dt={D5I!OLoE}ADkr+mA3O!9)RU|z8vxtn@% z&yy!?UdF#Lo{MeDd=;n3olKfV@7)5<2K}EfF|G2M>bK*^U+VU(TqSFFZ~M2yw!upu zx$a<|pHbDe-$=N663d(73^{uz+a4p`%gOVm$D15ad&nT#s>1tV+vepdAQv+qzp(p{ z0*D0xrwV{{3WJZg1BiaM_MV!>P8YAxe{)KAo>^_QIqgYs@v`k-Yb@RFKQliVIGtxU z|HG}_^6Cpsey-C$mGEp!S^LUATgv~Rv(TMia5Kp~yhp=Cr}yV4hLnGQQ-fj}xMWYy zNo-Men0>AIW(I$`%fsub&sHgQEu8wMsl-Sm@Z@CGQ^r@d!n>3_9p)g_GM^snbU(&ol4jNz)0!hCn4L3<6@O85lIa z0@>hr1L*^Sq($dSffPuLv!J-Z%GlV%%n~GlPzP3@;ve{wsXUJFShc6pj(b-Ht;@v=8c5hZ+}_;zMs$E=*fqb*!B8bkaqu6`PLzVEh%;HizX; zgbDv~nQBi7qf;IhCLcOqFzL2L*Ps7zGb7gUbVq>W)|J_OK=T+we@xgYnY8iMuJ&cQ zmqqj+7`Q(yep{&YX3DoG3WnFj6`^*f{09P%dYBu4+<#CuC~TR5X-65#CnlU3*w-&L zfQI!KpgvZZUJwnl1eFO_0gg*J4`eq8K*5@n4^_dh;w*UH-gNd~7{A{G0lLs)U0>ZZBnsemrgZ z-FwIXC0_klT(j-Cs@Luh%mS(#vs}ZY?ydf&AG)LYQku-Hh7a?3*pL6db;9aPLeQ@q zvzc2Gnu|d;B`rD|46%`c5kzl|71*@9R>SY=0W0^V54TP@$sgVv>MnC6pn6{u%O#s` zpc01zQp(Bmcf}SnKK#3zZ(-JbkD87Z?iRtx`L*)5ZX8W(Re!soJRs)BwyLFdLcddt z{6a4DTn^{X>Q;+$h&mE-okwuLX-|q-free-?zgiJY8EEhb9e7vxnpP59do;xNt6HI zwVe3Rn`7l>#kLM}!~JDVcb6B5cgPs2Y?|}@KkFU~cLvc`Ro(~NHZM;BxtRI*h24LB zKr9G2RRE+@7<{}PK=iY<_tY$Qx_E{Dn^U^;%xa^}X-|TSmu>%AW9fGPnfbxM={&Rf zA8zfIS6^uIbDjRFglAjI+E@PBQvUy(h3@=}$n0Gx*D09$rsZ1OX! zrE8AQ5O{L_{<4f4Ky^%xA-<78ARSPUe(G9s#Vo$k-mSm#^(IGgxScAocwL?$U}3yY zYv$r>pFzqb3ZxpK${8W&zu^+<HCJmYB;HvhO)O3nut)=oGSA_+bE#)=h04v8Pgho=|UN(9^`lQFv*6w3rcfiV))vT zdBz6CGt*d9vtOxvOX1{pR_gRo%Ja-SdD3)(upv-g3WI>yX$A(3Z$LJhU(QuQ#W+Fn zYiw*{Y7P{Dioxj=|G=kA<#Bw+sy&r<+`H!K}=h&tSy;r`TH^j-dQKMC`YKatw6*_c;>!i2eg#iq8_9#x+3J>4NE&Y$T> z_|&#fp(Iu^S+-29>TaVZ)Y6pyKmcUJ!h{jX{Rfo;n*&S(Do{QH5#h|hzJAFWXjp#* z>SKlJ1<^1|P?>NQ;JAeIAo-U8W|`S;OJ{f70S;my3?7$n~@Ze@kO;f@8yqc*ACv9oO1Tew1sux+6br+M#GX1 zxU2z+i+hwkp93}s5%!?+3JeIAZ(#i|KF*`K}(meL83OVhZDO?$Rk+fU1N`X0%!-^&hI= zIVInb)5L#$&g#vxRCW3DPVuqzd1eensBTH(wBnGYV%AZB^raux<156p)LVk6+mR zPXxq*fKvrPI)%Z<+W|yBTYFEM7%^TH#$v9*^0hA8&)WiGhJo zTmjWFK{12jYe(i88x+q>V^Ph1rSdI>liOLT(?==KGwOAbzGKy%N;~dd^-P%`aQMET zMTpfti7B7=HTwd!Fa?AL1^Bo^G=s?WQ`eF!X7QEwZvB<7H#v&K?No`y>+%c%3*&WK zGZ$a`3|2W~S_7B{x&i7|2gV}`602Q;BID+7-xV!;&{#B#NAKV@Hz7MWomA}`9!qqZ z)XI9V{x_Yn@$d#ij=G;~Y}S`A^n30x>w;MSZpRLgdqH8t&^7IKi220Hcb!)VCxlG6 z?J3Nqv?O`a9Kjr?s}Gj6`5a|nP#0hj_RC;k3|k9SjutLwi=bj0pl~rXF)&3^16KRe z_Hy}~09(y)&bnmA9rl{G$0k3sTDs==41p)-?=Q=^0aVZA7~&fl1Z2PfF<~N6Ak_d> z&j@iRgTv!AAtzmx^aYBM3j+e4g?5-W^fE7yJRi_1c45)g*{_8rUhrG8_Y;fF*L`bp z*41{`2K%1*_59J?O0BKjZ2K=5JOZA4|_(UidKe;NCSGva*?i&!19~ z)tk7-LtGhZXUcyd0J34>!3gC3gUW%zl^K|yRH1xg(jEi*`Xw3A^!E*@j}@jDM8hmW zWx`c};}OmS*$o0v`+;RqFjS5al*gfLV%@|-V>iL_Cn&rIo7*S}FHjjmjW`5{2~vFm zjy};);Q(O*zGj5JEP9AFTa y@yY%HEeqBF&EbKX1*T|`Mu~LO6dJn;YZ@ISZbM0L}(meL83OVhZDO?$Rk+)(cH=jgs{`dGN`_(wVRBSzkUNH%+bh?(B88 zwT>nx8y|pdN?LR_4nzY1BZ$} zKqU^pPRGl5vhKdC{ETT&z-bvz_D|<-mw7VfJZTZMln86Me6&O=PwMoVBiV^|%w}a9 z3^zFQFaB_Uo94s~*EIgf?c#Afv2))}vHY&>kCnaV|I`j!v+$_a)@;iJ$18OVR~7}% zOXg{}voJ|m-a5_5GG>K9UTgt?IlFwryUX0&+3)@e6wx zI6y22I8^|oQy6@_9YFN6wfEF4cDi_l{+m;}^UP|a&1p}9iVqY1K1UfC)CCxX{W2IB z!`1il;|_aG+hdcT zSuI_2e1^c2^Y@o!+yJU$at!f}3j@*y8icqTcJu zh!%lIZE@{;rLax-5?B$H z?2L4Z02?>{-L>`C@{CB2J>9|Dwg>x(lI?dC<3U4?chuX;qi4O(`wd6Cl zdxJb=4$ivZtP*cCv3B~u{0&on?h06TE%E2=nu@n_N7aiy&E`5ieP_sj;Zt6f#!kZ-te^}^NbCOXQr{JX1`MTmcq&HtkmhFl;@du@}%hmVMCz0 z6b1pY(+mt6KY(nMxKCPi4wfD`LHWVh7?L57)PdEf_y;~^Dv#qkR_&>@r{c# zF!Lu{;kPvb3p_S7&Y8=~#2+-(#iDb2#i8W~9TnrIH0LjZTAK172!L!@m@opl|DbZ9 zuw@38rD{+<0}C_2XKgIXqCaz!WXgD3NY5 zps|~Ojj;c>4jV9OcEqhNWFaSo$NTjj=fOwkNb5(*o27YUjfNnQ0enKQM3F kLJdNR3*y{#OP0oN!kTsmiQ6a%FHqZq8gYm5 zUjHZ%6ad+jwCHRy#6|{25Ph{M^JY@Och6k**^geB3YoqC|Kjo-J+}R;H#*6S|4Ekt zDsfQy#d*QdMltJ|n{9#Otg^>NW};_A;%`-~73Y%U`T3eJ-Y@)!>8c~mOQJ76diyHx zZ0!S`sjEHLE8O^bd!E+^O_uqeOY*kZ@XZFYIBs z0AfMFsRAIK!rBbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}PBSoQ`~^nabn%j#Yaq?YMW3?kD{T}!T*#aG(9^;f>$VqY1K1UfC)CCxX{W2IB!`1_p!~6@<3QFfSP%#crxEPukm_qp= zX|URtwwKG_1lVeZbJis@?y%RiJvRB7)zUS`X9zqwe}7rV4WK$E#}MDhAdn6yASO&C z3ZxpK${8W8{fYPUXj(ki*N5iyW4dW z{w??`Ui)nGf*VqWKR>MEWLWpvdNK3MC^iwx-r zV0u9`%o0>4Tm?8D;XIJtAON)=7)NnXIYv+(hq8%uQ`Q+8y9t&*LE$yn+(t=wf$|D9 z;t(7rNc9Og`oeyh?wpf*W=m3C{=IoubiAVeai+XjGVju>ZEqeVIQdya;}}~S{Rfr9 zNTWoQj|}YV7cYR8kw1XuutLoOQ?y8h8fdBvi diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410345 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410345 deleted file mode 100644 index e2533afc30ecfc1ecba5ab83055037d492c51124..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2764 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 aCv5(W??(H7kzzhdT|;zVf`qyT=0^b1i?_J| diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410347 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410347 deleted file mode 100644 index 8d9b81b6f86fd67869dc097de9917f76643a8f0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3700 zcmZQzfB;4hj@U1OD;ivW?6JPEq*kWb@2*tl-qKqMCdXLU*6z~-suG^E?|pOr$_bhB zH;w|q+uCdJg zbo926wJ^x0q(x_oAvQ8Fg6OM7nKzRHzI*1f&wlj6RLJc8{}-3%=&|izz0pZt{7I7LDJPS!uhQ@G3w;eVk8=F`CS6P`f2Cn2U--*PrE~o%J*#YFSEXLnEH;tY`R|Hb zuuv_}xBGjNvKsa0-*~=w4HKtxU3T)dOMYodzA`!Wsvn=editmNVCbJzO^pScXZ)Gm z?zn8tXZFYICd z0AfMFsRAIK!rBbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}@!pZHd)aj#?=b3l%r0E1L;8WiB;3f2oI(obDWu9(GF+Pn2vzTV_04!2V!7O%@Q1T2i# zY0X@G?K4=>jA;!}9gI-7I#j=3VRZfe+1F~Z46ToM&A1z@uq~#Xug|USg6#3B=Jn?; zNB`kt_{a3zGlyS9KK+u6sn2S;m1$P`@z(XKp&g$E#RQ~60_DPkSj;lW6dKBj}gPSwa`T$RN@H1lkaQl^*m3HNyYaLUM92fd#&#Ir? z?(gJOGfk+`D30MIiv!5vOus%ffM_6KgqX|VaHfl+Vv}xmmsxJ)_vb;=YG3Pb-xICH z@}~FgRhvHM3r<&GZ25HaijSoC)>bu#L=La-9viYVWMf@UGo#9P zZ@;*B^(JYS|L&DdN4jHP|C;TX=_PwvSKBcbY$GUrLI6tGL&ZShzzmHiun^&RWf1o$ zd%=LL2Wl3W!eR*{sEmNAAJ}=v~@n68YY<=DT zJ3Z|Me4z17QoTXAE``?RkTwC-3Iv5EVWNZ|aptGM$`6FWP!<;Rv8Ejw*h7gwFr1Af zfW(A*7gt_JPX{2q81)8;e#fvMNdSpShTD+JTVmWKb*V%K+Fro07fAq#NwS+z>J6gX Qd`SL)+X!S}a|lcx0Q4D6c>n+a diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410348 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410348 deleted file mode 100644 index 98c5b3fe96b553f8d5f91a1545e09b1aa21a8b36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5252 zcmZQzfB=1wj`;I9#BlGVmE)VkA*%bAU;l+v8vi>!Zt;e@tXyCi(A1ar0d)s7ombUlzr!4*vt^QSJ z)f`uuC%jCTtkwmv73gf~Uc)2ZY`?DP9N)7J6D#f+Z7vfh{1KO2<0-PvR{8b)FB`(w z*j)N^GCH1pk5}2t`P=iCttwY)dDoS?a#PMprGJYml&&&}w(9Uc*tU6j3dqIG$1m(* z^8m3R;8X#SPGRuzb^y`O*4|UI*y-XG`fpC@&NHixHm5xaE?&0%YmKGb{b%L}1E=%M z=6|@gTV8#k$l#E|muZ)#9X1DEXS zIf*UG4zsTn-^}1IcX@a{_1P+=u7y+IG?f^M1fHC%ddm2!R(O|^$7A;B$J-!oVqhQ? zS3q?vK+FVEU!XCuH8%MFvIUat&TL;wRjn^5#&k755Q>$aaptYx(K4VCpwGijGB7AE z2eQHO2GR!t!1$a7VZp_DfqceLZ-^*|>Wdu>lClX~g=GX9`O178gzPMoZH?~x_T1>* zd&gWCsA`|fD^YilY9idh7~l$4%5cV0D9uK6V(y=`B-;TEBZq*PkAYJiuD{5UJb%GGLhk1AH2#ENORCOFTQgj$p8G>~ z$JYy7VjD^Y*Xg`5=Q4wvGtv40Pj~P$1_pHjV4Bo|sYmiFIBp2*XJ7+`Nr<;0SnKUqVpiIf zf39^*J#t*=n?0+3a=X8iQ_VD?Mx!`}lPnGl8yD@-I|{W0s1M=>1_!5Kn}wnaURJy%*bNO6f&sBDIA z6<<*m+mI zA$?Y3W~0L?evq*sGf#bE4eJ)|QY@CfSt$PUKclzTos`{f%P!nIq~X7()oUsP1NROF zX00{`=F&Exa%6viw8H=>4S)bB4}kzv2=@~(gRuD!^=5YUAO^^M#?W9FXAp-&cAmqQ ztky|(^P^6GTTpZL_}*I_;``@WDX(ZPzi;FP$xu|X(=`NAG^+in`d6l2YNoaN&nBZO zOwjK9y*O)&Yu_t{ZNis8?g9d;_%qTe0&LjCG>7+7{_UvS6L}yu@ZXs>rQ;Iy z7tXOL=rm6YE4<-+9IOZ!E|BCzO|2>7Ea4{ z%vrFbUTu?U;j)FwEs{bj@|f4>FilGrT>s?srRkC%rb*6Y1)9h5yFNeeC)P1#2h)EjgK^T z6DX`;;WgOYMoD;q$_i@4AvjEs+Fs!3doG{OA-?Z*l-bQos%pWhXN&Id2g?&+BVd*QIY>;H={VCU z*nXh@Y@sSq!kmb91;|Y~GZy?*yL!(pMX+b4NhzWOIA zzm;bz*Ca)|dsR z{$cS8k^`p)BEpOKwl^##A===`X%5sr0J{mKj0k&y=^CDfXcs1wxe3{S)QTsDJr~pu z>oFXAENeJPAZl$ai)a1h5~i>-f0bt0FaNjAsSX;<=;a73^g%Qzyg=a%FGmRXTNoH* z&!#dU>p?aU#iI}jlr&D9`6+WqFdu9Frhz?__yZ$YkOYvJ@Q_4L$H@5+eY^=IhmvPV z^gD+ANCHSqGTergXNYkVw0=VG6N4>AatIQWWH*7*Exc?Yx_^zN2W}&ffz2T>c>oKb B%Xk0) diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410349 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410349 deleted file mode 100644 index 26069739ebbff90e84d7a7706147876961d00ebc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2888 zcmZQzfB-GFgo8)+-V`zYbNgZNS%Yax?ll?jf33d0Df3y9?9R&(Kvlx}A|3M!R=?T! z&3F4ZJ{E~227#pRJvn8iGV7oI49l6qxX$^Ze#)~g`XWnHypwHQx4NB*nBc#x&e|u0 z{U&4EeRq&eNsG?bLu_PV1kqb#1vc%j)$qG|z{-8;!>toe@`pEvy2~61sNUDaa>=F} zsKnva${oFHPei50#--joXW{(7X2PYs#3g5DpF1hMVpr@L78Uo)lNKzfT&{3A@2~gq zQyE{M&*p7>cIxl8LvsQ*luqT77pSSs3z&L-%Q=T@oHx8aPkZq1j!T8f-(ALBx0dj$ zR$Vp!GpQlNC-;!hG_GHVw|}YJ&SxW^ob+$uet$`;#a^BaqOH2T54LSyo&s_)^YIIN zLNY)s2sl*$q*EAtyd6OFv$glsEOxqhh5nmUy7SCxqs?hgf{T}J|5{_|cK@0A!NBP} zv-uxx?Uq+xX!3KN{;7m#Tguv3{@GIg|D1*H{DPZF=HWdWCOW-8KQW~I`tYT;5X$A&$0bm$t!_-6V z0L33T4hidLU<0WQ@iqi&?KbY}Y4>8zHT=sTvo?0d|HH}eSGg;PKTF%ZNhH#clapcN zqCI*?p|$|^LEOOLpy1thdqs1=0?USi($1H^qHQ+^G#7F&ds@!>F30`xSFKGqS3dt? zZ+%Io!F;)Fm)AqXug;mbmoN7Gd^KxQfcrX-TS0zixcy4ZO1tvUwT`JrjthOWXVp(` z_jhutnI_a|6vuFq#R18`VE+NNfM^LHORr={3s=`t*GhNyur#kSw;X>bM_XH{JdlEf z$Mk8wC-`_in9BeB%|5Ae({a^DT#w>hW^i*RS|8x)4t@qw2MY_rZed^w;eG-#8w403 z?qhIR94d4C(GgyUrwUwqSC#Qe&;9mK^o8vn9 z!M+`}%DY~Q&v;h+;7O**hA41YT+P{Z;uzc236oxUf5>51Qe`N9ueUrbmCu|;?DBeKzY9&-9EKI>%;@srRLt{5#&0~Ya zZIpx;$X_S`DdLcra3$#K@#RZoIj~togcrPCAy~)4oP<4ppp=C~*b7Y8@HB+(N8~b{ fvM@m^pUH3&irq*YBql6SaOO2|{eV<{!ZiQ@RJq?c diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410350 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410350 deleted file mode 100644 index 0a8b820ef0c159a91e162aa550f28f165725e6e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5600 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 LyS1C5T6N diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410352 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410352 deleted file mode 100644 index 3a972c16b1235ad33e1209c86f0f0d89a7239ea8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3936 zcmZQzfPl*Fvy+$JofqF>9CcUd`_W68^Srs{&pu)G{?#@vU%y!fKvlx8E8qS#etfgQ z?9bzM1(Qv7Z`vvDvv|7CY&ktsRm0s`N~ww0=M*j39c2%2|m$b4%HLHD^UfuUTS!c$L0UriG|&kN)hfr*l6T z1C==34C*>AJMCT~@0ERRVZAMU65q58zehY)SALWhXeMA`qxk%LSJkEaH5+u!z1FZ6 z=_*R-J^0^j!o+zq_cY5txxvY5Q<^w;=lbG5U-N`srQCk$u*xta_p%1>|Dp;};HI zS^;7~z^Ni2oxe1lI7}h-4Kka$KT!Xj3RJ!|WODzns()zO&UnI1)N#)^wiP z{13Nw%d0Om`MFO2RKl|@W$i2fY$^YL&O!ObM|@E#2lo!+0H7*hWIO$~}^;F3K( zC$UA@VfMA+n;HD&E)TD#K3k>KwQ%a2rV=BOz>||zPZ?j;3hz?#c+4LCcpJn`3=D+g z3aE}5h?zj@)qA$iU0mnH{5$4s|I*KLeldmxk0w>~X_~)GzFd&F@-M@TX$`K>9!c7@sR4EVwuai0{QBzb!zR|L?L7Mfqp0nx6apDIiMt!^D*dt8`AyteL0? z(*rgi7{@QBJPbSkN!oO6`SLwUiQ=5v$usHeUo3_<-ht{gRYFSK`sWy=E6mKGb%ZZTuj=T(z+3V7Gkh zbsz2fiOYU7@H;s$a4R+f)lUXu6u$uZFaQz<%L&w8%+|3m_Gr>;l>5HwyvW5FUHJ?l zMIo2%9?XGh@G}-UYd#v%YJH_tZZIrQ_yg zkK;R5?Wwfm-c`?(`2mOT`&ooo?UR`Dd0(?H17q6)V9;!3VER52s0StdplN~Sj-Rub z$pOE6PcB}R)T@%tkttTOZTXn#erZ>Ymz%;22dF-P?RaT>x%^Flt!6l9T{7bi zdrjM8lb=~FU2}Yfz?1X$mu1`ln#trC;^`6uWWYfBscXph4DJAnTxM|2C9=Ns<{PI&ItA&C{8Yjf7I^xirxAq`rab>C2wwDU|oAT%iZB? z>pQ_u*Mi;he};=4J)c!KY4@8?$EO-UIWpakbMDF6q1ka-BA+jO1*Ju1-Zm@wN1R_j zf9LHx%l_cV^WYhOA7&PRkvTYLVtntMhB&aL;IM?SppF3N9UxoWqckxADh7%dW@!Ec z3lYq(u(SrrvruCY6iTE)gc(6)983jK=7Zcvy!jwM!2Cf=dnoY-MzA0WATeQ~1j#RO z9;7^FfQ2JkJ;wr62Z}>-{f=Qjk^mAD?ix^7BLh%bkIijxbwtED$o;A?LkX4dJj?dJ zl^1hr2dduy#29YG5CEw`3LnDe?_){tUi_Q{^BI;s@u(E(Q(omIcF?C*Mn3pk@4PcS z=5wM#=5NrJ4u+P^z&!aM2$0;&2;}|(@<9NWhV>y_V1-YpykuYy(`a9J59mEmnK=Wf zj~AvFL?eX*5)-Zh9zReKSUiI52d1$us5na45#^>f4LcgU3FLNIc)`>CAaNTd;RUMq zs1b+YFhMFOz)^W|mhSC&&wn4z=(LeEZ;YNgAwK18&~(?vvo%WJ(j~bS!HO{J4X`kj zfQET4ykaBRCIV`-`g-pUv|Tg_*)R|rDPbZp;i_7pQHE0+1pOi3wMNBMu?;6;c`n=>?TRDD4q~ZRB+~?!WRB5(Vl5)&-0c o;I={;SR91ncOuL`vG9@>DdxlLLXZ=PZo4DZPso12;(oX^0O`BwGXMYp diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410353 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410353 deleted file mode 100644 index 5fc02221c38e07e6bba4d8bae3a1d17d7ea56139..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6092 zcmd5<3piBi8$W}|wGc&FLbi~|tt2!?a!rUNmC{6uN^Vmdt;DdTTsQYzD=JC`tD>7r zl$~WU+HR_?fATLjOG?xK`_7!1|FMn7^r+|QeV%i^_nh-ye&74P-*?^*LD*z+@ZAx* zouz(s9bL90+%HNw^1Z2r^vhOhE$ik7`K8N&l+6C>tnJ13b3J4AH{PE)($M}xuASJ4 ztY+iaeN|#>95R<^A0HglCg1Cg6>;)&v#nQ3?3`IhDwj6&xtJZwxVYHU^9OHaHd>2}!F>bskc2kIh)+D+nt2+3!TxuQ$7Z^wm%z+I;b zesu_Jcb;#wM>WW;&g%(FZtII5Wi{^}d$sy)x=nmdsiKcfW6nDFbKZJqUY$=HP7^vK zT65oLba$$Y@~ZGQ=YgQ&Gig2U+er6T*1nIJQJ*++xpMY)AMxjR|M9%i5|~&N`^L82 zGicLD-G>>~CqQ1pKFq4Kcdo`@VzyY}(ms<*;bi5FHw>O!F;x&2Hr>fwT6>x}n98BC)iv4ghf$7G3b zRhM^72e#0-v z&_4vhg9Nd6&@Lna7xAYkGr4`X{8F=@AOr0aT-U9#fDa;yxuiocQp5t)+=1s2!W0WjAR0xak&TkbUAn@bS3-0N)`3CMZLKa z89D}(ASy42I(9-iFE1f~ScU2Ghjp;3%JcO7Btk<{0c(?u-Ze`xrhmq=j8)9fbVv6*Lg zXiF~3^Q{e!3NlTn54HLvoW-yPuYk zzICMbA*GSh-(-JJkK2Q|1iEyXDea=If0WJ9Ng+r}$XHhmFlsf!{~miS|JHxE=^C-V zC8e=DqZy+ObAg4u>V__>HiNroDkrDo+H7Z{pZj;$Gm`&&I@!A=Ko%x5CYX z>k%-#fRH?@^Q3gfa_ROFS0Wu@(~cu1OYs$^Lw=OOzBdV>K8(hQA zH$e-lQTE;gdN*8+PQ-I^p~bcVPH~g=$p})ogE@%x$~2%S%F%@*u3&L2S7M^tMg4<$ z@5dy^<$^y<-3?zE6VKZHMzBpKUhus0C348Kc0+vgm0OEOG$i`J3lVL4&ye`&%Xj_B7DbRr&q+Yg_(3fTJ1Zq~^f)7~FmWvB>qIrm z_U~2TJb+1#i#eZ|GQTn=o;CW7V4F(3;CXi(;Sz^qxgrzoGykqpsLMGsaQDDT&&_4b t=KOZeSwQzZ*mDxOUNMFTpmB5O!~O+#5Ax^OAK%OKd!GM{UY;|@{{g&}sdNAU diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410354 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410354 deleted file mode 100644 index 942e9e3aa1a1ebbaebf67bdd4a15022c9056e5b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4708 zcmZQzfPmT7b30c^@}E4(+u<~8_wDpMeMKi)Bp>aMb!I?@N?LSw7Q{vdMi70qDD!4gz<1AF_SuhKm41bN)=h%DHi_%~$7k6rv0-pfz>AB}nC6>p-EXaDZmq>TM;pC@fRU@y!hWa+inKQ=hF;>RLGUO;d@HNZ`rIs;7*vYK3n%p^^+L|d4xT-eYmhotM=2M_YyyH@@##%UKyx~0ur8}Z*fhp&0OeE3V-bdrb7#W@w9B0f*o3zL<#axOX3 zx@JpkwC|J$E3R6+^*(r%VYVqJ&@6}_78UIljBlTPc-xd~%L6z+OKNgCy~+P+F!Ruv zu0AQ*iNyHf$jfg9tN%sHuG+$TsrQ*ld*m($-ak`I#2wDPw_5Df&3 zU~_?fxXjg6SCVDall%4bzpRD1a}%R}pM9aSs4B}yxqPy}Ov0MU>v*rNHLH+Fuh|iF zJ)HaYq>LiRHZi@Zii}HBPCnoVng#ZQ;cG|c85p)g_GM^snbU(&ol4j zNz)0!pzu#&5D+`fz@YIDWIqtYLDHgghkzWA7-vCoft9hbiG?vl7N!nNr}zgxWh#&3 zJ67$fwBz1Y&y@KAhwuAYgjns9nDTjFvoBB!Q$T1?fR8IgGl)z-buGDK7GG)a)?fL0 zlcPA?PL)`^F3%9KFkYuMbMdv$V3jkbHGpZL8=!7=h|7K3_scom`_}g6O1?y)+k27U$Aq4<;@l#gJtiWw{n~Ido6i5k7uU7Ny-=3BWz#pn@)eU zNhhad`$Q8^!UX9fHjKdPUfNzRe-mJ<8O~Xk%(%l|)ArcpXI4ws9G@ZZ%7Nm88Cn*Ag(efqcR*F(d=Du< zpvE95ES4~W%3GKU;>=H(gX|`lUJ#ANe2^bt{-C8jl=uU~*+>FNOt??M`2@}b#SsWV z!x62{1<8TpkX*lG*pDQD#00wlL=f*b65S+qsYC`E?-=$X2_P{^b`v}dK{|=9ACUY3 zw-Ly|<`CCcnNeCBR`oEHE>J5H7rfhk>Y7HHm80owxkf_|e@R%_k(MV3)#nTh;vQu$ zKz;+&DHs5YJ5kdzQRY9VJWo>J9!mUy5iCdoNKANe!r~4lKw6$8&F>iYBMBfeVNM}4 zPZHrKt(8r%c*n38NdSpSvYSxyB+>0o67r<(frdTx#{cH(t`6vbQ`W=p@~2(aEJys^ zU+;=v>BgZype|1N4+KCq%rA^U?k}huDDF|}NrG({1_m*WHuYOT<3Q~gP#+1@{(%FS zB|rfrCR`d!d;AHcG+^)E1>i9D>6HsSXE6 zrAI=`%)%#L$E71WHmNy_l{oq_`ON;;)1sGr-)ir?-Y#f-V@so;z5@ur!W>=)5Yhi& zU|+v*4YUvP7ib16*d!={lrWK)a1~^wQ6k;sL1Q;zO{0UvZ76A!L^pxb0yW|gYZ`^c FJOGf3S9$;d diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410355 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410355 deleted file mode 100644 index 09230a1709e844319d636ea44049afdc603a2998..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6060 zcmZQzfB?Y_U3;d=RhBwUk(zOFZp1{r+w-n4?78RCChM_d!sNtCpeo_n)^j^oNb;XN z$=l&HYxnK+JAFkbS|lIsk9B50Rp9s0%%62X*Bz%Dp;O;V9p}3d{%_9BV;6!BOup(; zv|6?MQPXmeO-YN+&WG5@zzCwZ#tLlOU8~`D^?;T8(uZ3ooa7I04t19~5>UObiRF?_ zH&BTKYw^|othTr>S@ubdvFGFdPH0|viRqwy{hfn%?8|R^%*mb%qOHcf54LSyo&s_)^YIG@ zZ@d7pAmCIHkWOLn@pb^wO9E?nZbY&Q7CA1_JG3d3mtppd_Fqow3E$aj9~=ptGHW`| zZ2pH^yXDmvn*3a+e=6bGma_Jhf3}qWKWCvkzu;z)d3cY8iB9j&PYfyl{-y@SG;qnD zo|D+3>@fRU@y!hWa+inKQ=hF;>RLGUO;d@HNZ`rIs;7*vYK3JS(7#NPc{8q5~U!?4+Exea{pP95r?sDM$GqptAA&z@F zv&P{M1sL+pk|q ze!KkZT+R(5CO#7^!-S^Swmi%4T-xZXeQpO+>z*&yey#%f73|-6avqL-{49kl-~O=a zzx9COeWQ4&ndf?~O{MqFUW{o70jf)35N!du0PaU18y$fC4iofGcR8!AJb&tGt?6fG zbI+Ul&}}ovlU+SLhu)nmw%%LM0#au^>l92mBh(&;%&Xzs>{u(UbWeRWEx$f(!i)KZ zGw-fi__k_t^_+*Tr>?)1yTr93`BCti`>cO5Qp&Tf6NF#Me!n1ex_0Kqnc+U5a0UA* z#Xs;VQ+XWUv1(7H9rvz!rpymGeBaL^#A=_!l+XK`eZg_az_^ux>H8d@9u&6&^W;vT z7|Y%{Z{;@a_geCB9?wjDlaw#6N7%mHH=X`ylTJ>__K7B`P<>#%1nq;`@zVBk`I`V+ z&2Y}TWX2u#nzqL#KeJl8=J*VOC+F`k%eVn@zhj7}Ul5Q11L>!(C0ESiEA8F-D_?JN z6o=cX5{uX683Go@>$GMrzV;cUj?qvTrkoM%KVbT}+$D0y$c}4b*LfMMTMt5an66&G zepP~tz?q6AimZXR9~a%ZdpP=M!Zf8nb=U7bY<~8x*s0}a(J9NHZ?ld#v1IT84Frd! znzGqPn@JB&R%&0%pV*SSm@&AozTRJ8uXyL4YfT05&!PFj2;>ZyBhc6kDND7#20XIU zI<$ON%tT?I(BA3Ys&8xLBemOe<5iOO-U8`k`t_j!L<0dM*j%6=6dL^5FTP-^YkXS(@}BwmyyWjGHHGF8fyYuOJug_^ ze!q$vXcpKHhOZr&XKYYBGmS+x`<2SK6i#kurA{BEJkPw7Cru{^gEC?YgMip+1_q7) zAp3zB4w4p~I|k%{#5fCz3#^QdO)Nm31p=5lFik`nU&X$?>v zj8L~aYztoeVWvg*->WbG82?|fb%S`5mTWrL`qm#Ii&Cx~R+i!R^SyQctX!zIxcf@) zrxnR{+A9_sGt6Rr$lacHs4ou`F04xLU#QQn`ukohe6#u~f#99SJ|^v-m*%|bob-mx znrT8j)J||I4`jpq3d&?V-gVc|8{+(t=wp~new#E}w*;4ndJ z55eLWBnLK&h`N>ddKH#H5cMmvdqL@%+<2m0m{8^>WdBhso*2Sc|KBi~VVgxs|I9z% z?3+K&7QP_x$?%Q+5oh}Y7iMPNg9bBtIRXoP5Dm>2;5G(WoQO7wxJMbV*$HWvAe#kd zqj(e|i;~8PGynM<63hqr0qPG#x}kwRl=uT9SdavenDF34Pshml5xwsMl0(TeB>El0 zek1`TCK+x+$}_~ciRX?iEZ#BfMG`<_lI$i>odqvjh;Az*>4Dn_WMFg15zh>_V^6DI zU#T{bo$;oHi^W)|yISvR2u2|HA5;#Omf>|f5p5_2_Vo)r zpzSD-Ijm5#z!Z`@k(h85xY8Qfeq41sac)}hgvM?Hxg8c>@OT|0ZbK{E6t#^ zKMD3#fM&^_1$Nz#+N@CX!4wutP{NNm^HYH32ck_(z!hMP> zFQcaekY0>>gG9e$*pDQD#3aLQNaZatZi4n}K;;#Ny+{H`Op@J%Qg0C5r$+Jz+(vAU z1d0i7VU(F=eA3|BmBdZ;L3jS1ySMk^)G1fOp5_WgtoWd-4AqaV-uMNTgO!8udV`35 zv6x1idKoa-K;tp9f#&c+%>q+M0fxkctH71k@YWkdx=Dz}Zo-=P2Z`HI$_oC!`M!j@X7Mj!SSl#94w)@<@J8hF^`YjC?RXYEm zukx*^&imxtF=pRH2rS~&GhQ;CsC;K|9Vr;M*^g?A}=JZ6u6ybaSthJ1E~%1HU#NoNLi}=HQEO5E9fvZRL#9oVW^?k+y3DYge_ZxG%Y25E@)CK&sNGk2 za%Si+KDz1dGut%(KBnZ<(o4=+cKKP^0&C4VRBrk30?h*Z*YLF?^NbCOXQr{JX1`MT zmcq&HtkmhFl;@du@}%hmVUQzJ7zD&lGcag^?FSJskhJLB86X?vZ_a|^0xM%<6LV9D zBseUfattZ{flryrB2X&3YEZ zv@^HQ_*4>Kl)RM*l8*zt{Tw zDwoN3*3bNN>MJ|YEO5AVO?w?;K5_D0=M};UAro$U3UetfNnSKZFvscYgC%V~z!*^% zU=a4pU|jOYGNQ?s%E`}xspkRTi0n;ySFPFawu+ zZ1OX!rE8AQ5O{L_{<4f4K=n+HA-<78Kn4sD6DAS`QVme`j1YG+ICQ-`C~zRW<86|g zh>70DEx&nIWm(q!-fYdhsFmMl&&?-G8zW2HRw{|>Gfrm^zVk+mu_f4li+N6_HvVEdSDpVg>FG2gDb`Teqo_?f;C8MD(*zF)ULDQ)NyT;s<@70g@ zb+#{*Z21>FOF!iL(huhhbEkamwSRT@b4JUxyR6mq=FXVy4eh*ZwN!VyGE z_?Bq<7iAl2M}~$+6nncTXSrL1MV8yzg4_tiAONN(FDlwC7~eko@U|(}mIrWtmek~O zdXxXtVCJDSU42rr6G7@=We`EP0NotI{RC_{NFUgJ3=9rsCl_*x1buCr@X6 ztS^;v_1b4Qgav)lH&nN(v@)+aV`Baw?c}z+_{Gl)17Q)6@*fC5c0(P(2;}~PvO(d= z3@nq4p?qS>8!?SGbs?Z}ptL#%s1H;g!U4<@pa2pRE)9-9I1gku2te%zrt|qwIYv-j z31t)MCbe}mb`!|$u<#meZlff;K;;cJ;t(7rNbNFk^aaiN{`%Izb$wFo+3B7S>kZYa zf3_XG;eN{fllI<)vK$YgaSUz|0okxL1EWFGurLR;8^C~wJO`=YAZ-npHaLxJI8+cN z{D?GP2HKW^n+s!LF&}H%p@BV=_yfb)NCHSqm@gq^F`S3U%joFK`=B;13?PRaSd54^GK18m5?I2KMy}oEqxA<|6^Gy)U8xOGSjo5g5c}e6r3!#}d{9_3^;;f@q|K viNu7fz?DY9_5WT;A%FelPY88mhi)-*au+(t=wfyRTV5rZ-4AbM-8z^2``8h%#~Sh+8KxOKuw{_y5dcbOvr)%%)QF4=Sg zl{k1ii=>tZ?=wFoz4v}{jMw4HTjItYzIwUY#W5Qq<6NHPtrER|Z;J2ByBkX%%}$6} zA#*LH*;szv#P__v_XZFC4s+ z17bnIsUjeq!r>2I9oYWJ(v(-L05;|qpbe`G# z54U#9t1mS9xlaF7!m}-9?JNImDgS@YLU(?_%_Q^i9t{(n-k+ZsQvUr-4T@>tl07{q zu|?To_O;@h8T{og53i>_Tcy;saO#_;5+jkolap0X8DG^3?^5!3%pU!C8^lcv420qe zsE!4QnLz3dUpq3-*r0f38jEW7E0u34oZQYzojyu=o_QxvnobZl1S&~k5D+`fz@Q0= zGjP0t^npOqqH`C46iAG-pt!)w*x1AzqyY$E>cDi0f8bN5@;JU@)t*W_?p^gvnICZY zzMn;i)jo+SpZ7KU0@X7Gga!rpxPtY9iS$#~k}GEMmG*A^m9IBBio@+xiN)*k3;_${ zby_nQU;7MF#FP{T)4&LItHZWuv%;O~*DU+S`I+ZDliuE5^?c*4<%S#Me@;F3{)+O{ zhc9&^AIop$a9`vR;jHszy;h^-N+!mm)vYZ#E9XybVFeirGBjnW_Sb+%c3Ov)&x)BS z>=W8My<7EdjeMkbdv3f+(%xGPjBN{mX~P((6xpv5=5EPZk(s_xsjluW0VNR`kp^ZN z>5jIxXzCc$l+8ZcOnPv#Qu|u|#FpH}jKO{N_5K2T#XI+0YbuC;4pJZD4GB$xZUMSE zg!>6l3m7nh-3QD&XR`exx|#Iuo!UFON!it)et%Q!*?-&BKF%qSbQgWxbL^$qh27bw z7nyBQ(EG(;?&@~!LH(x#ys2^puktVJaw@U|O#_ET*R24j=XnxhLQmEUW(;`%5WGS@Z7wAAT_@Ql0s z=L{d7fTj-vp#9)5gt-GsgQ-8emrlPX_s($c$Gi)!zv{ncUSmAJQX=z#h0{lGVaI7I zAVsogQ(;;d!R7+vb+@8{$(&x9lgBuh&CWA9SJ8TB@4L?L3jZP|2rrMGwkK+J|5~0) za^_FAoU-Zs@8dIj^%fDa{J!4Hll3S&wyzeU)yHC zalEv1?fS3k-n-6;e}h_@@*fC*Y*?H!0=fU7av*;(1M`3hlut}q!oa?MK?Afr0ciu} zH#mS<0u(@E!ljYJ5hTbAmVw$2OlM1=Dj7j#Hk3`Qn`CJ0CXm}<;WgOYMoD;q;)xn@ z2o4jZ_5nEhR0LV)g?BstZr^_3|Hr$DvvY$3?YQr98@PzhD3c3M+z*Z86etZ#GcXz? z3=4BmISmFxl%)(zzdkfT%XXMHIE`#LR1hWnh%{e@1oN?`9U9m}i9axc1xWyj2@gh) zpO678FQcaekX~3Eg31}NSyZk&A@10?XpbJSCWF}l5d_)?DNAT!FQm@KZZAWO&+TXf zkCXd?B#T-No)%s|^HqDXfd3CSF8f`pM6X<&0(Bp@^7$834msSwLWJva25}jmtPE&f zz5%F-2Wl3WLW)QvCR_!sI0e@axXNcD-NZs;H-X#^3om&2JV@LIOM~EmCeclx_82wd z5ItXl-SYL|!(~Tm6<@~8@7YrpcK72YhDnUc9c2%~#A-RF&5yVTRs`xlKme{Z18PG+ zyA2Es1lv^%4B}cVo8|!ZfZA3VW+Dk7F`+Ji#4(%$WWdTDu>HU^Iu$C366Qp@$%4ji z!kR`0iQ6a%FZ6N)IZ{c9L-aHX(hG`TQ2K)BX%fSWc5WllO+0sGVd;xjZelTg=fC0RZVSIROC-gDdYHOR<`2XOAofm3U_A?jW zZPPnz)E@`3DQVH!br2gF7(w(3m9r9i=9aSgYR-y|UbDpd@G5e8UluuS^=_a3;v?J3-b!)IT-ove>hzEQ7hPwHmwUftv1jA?3MSD5j9V7V zznxS$yWJq;-A#|+vrM0>y3E+ttmCkoX;8Z=an|Ga>Q8*lM1l)glRl>9KYA{&?3^0- zRA!c1-&9Gya`TA-9PG;XPC_BI)+G@u8VB6;9DIga!AHQ($ z?jH~f0!|eH=@bSZZwC;)B(R3(MkK3Xk>e7*Lz_Z*8D`ID|K+5f@SUyp!I97@v!?UR z=6|@gTV8#k$l#E|muZ)#9X1DEXS zIf*UG4zsTn-^}1IcX@a{_1P+=u7y+IG?f^M1fHC%ddm2!R(O|^$7A;B$J-!oVqhQ? zS3q?vK+FVEKbcXGN7!TAhYP#3YCrvXFYzNM&(@dgmBFfxjMfW3k4iBxwk-gLfgwyi zNDmNz;}61QV3@|Y@n3#@abubN>2}|BwoDt22D!eMotHdw{ZrY?@s^4pb+TtuVagf7 z<^to^Alb#yiaq|ZLZgRAM6jti>skH-zdbKW*+_C+ymj}1G;>gQ^U^PgnXgVJg#P#W z7%LUkTCn8T8`mqFYpn&!y!n9!g8k4n?RALx#L0J^R|qGBOt|eS%%!v>dC?rf9H*-f zmbCdCWnfSjU=a4pU|8GwGSIpuo?cMq-UvF|0huf(V zi`V5D0v5*Wv}P{8_8Fv3qClzvs-6+zP6mg#X)AXgvD%u<|9r2KiLKqF{DSXW|4*z4 z&=Lwcon2ySvvSSm?Yul!OSjrgjoY_8#fM18Nsr*BrQtZ&%)Hx?8g;4t~Kd+GFR za_zk-~C5;l(tqs8jiw%+!6=f^4T z`ej0iQ0|;v#o4}pwkJpji#Xf^X<+*Gp#ek#0VCL4V7%UD_Bk0P>Z23yv^c_&YrP7nsA$rJ_wvC|9;njk-;xIJmnxvNky zPEfoW8=II}0tKLAa5}|5@F`Py9N)2OPo*9Au6m}-4>)|^&mzQXpTv~U`+eE3VgYKkoCiHl2U@%(>QyE&rQ1 zJl}s^8elPH@!C%^t%{1SZ5FcX$MbR$wi!;o_5I+ToC$THaADaNX6LrY_fzpkHqXs} zT)oBh6K6g7R3d6W&5nn=Vd-H|`UZiN|3CnwVPV1u@Fi(Tx9t?;GX9o85 z3uK^S4KkAzsufJZECF(mm~a)~^a$qx3sEo|YCkalt%S-kg34N$I^x_ke+P}-1j|pL z@EUAxqa?gQ{-Q=4g2M!vgkNa-4CKA6H{2}<}8XMV~Y?Q`} z@`dPn7s(%R8-WaL4tc+;U|rj+0H)Wqci8{(x(Mt}6Ak&d`km`hl};mTW#J8%Dl(~S$Zo-=P z2Z`HI$_oy1n3xvq^XICSONXSuCDm)Mug_98vL z9-17kasY9I2$lA$l4G>BXqiNDMF9xeciq5VX)6*&|Z9iA7##bgR%$qWmHPwi4ndFHDadg|)b@W=xZ{AKaG7a%_A3GW03=gR`Vm z;tBg&nfH|yoBdC}ZEPCnpZ}#rV1$$DVlih0Pmz$Fxuksj$vsbOg$4QjlR+fAw5FR2 z5g{DtvxJ-YE0evM4aBcnTj!-K|B)jj>#W41azQ4swBgtrIaq}_aj4X5%j1!pcy)bU zZryQzvA?=%Cd#!&o{QbbU6>5x;oBcM-)7@~%dep?FS6^_zSfZNEa;hSS=g^-{}B$Z zVln+Cc;+?5K9JLk`Mzj=T%LMorgYPp-oAL{aM8HfCwliIavZvKDYCmwRIimVS_HJZ zWZZq_?O|unUiD7m4#67Y302(KS9bGP5Y?%j5Z78ibymyDd5fJpQIbuc-6dc8nH(!WBs6B9c;yPY_EHTydqhH`|?U5gt( z>m|!=To)r!FV`+%aZX6***fcIWeb=fXr)@gdQ9*K5A^G&nat1goD=l?uG@oefz#N9qRbmNW)*yb~_ z2nfJG&g`J31{kmikclNOq0xI&tQw|M1KVqVf5gIfn@rfA73$k7yMG_hf!nIY)H2mU zoQcl)&-;jZD@f$d-H?qvdyZgTIMdm1-K9k9;^_BfTn`bYRf=h;eh6_Fb%gjjcAj#o z((=xa#`BF~Lbd!sEe{yKmkf?K_vq57MPuR?>H!hcZH`tksa%&WsdjG{kxaJZHVTlU zWIn8(sr5_J!Fzr1&fA_;E#=4 zEHuaZ4*L^V*}Wr6XOBzASg~l=dnt|iI&mt=6-ZpU-Z2TZ@OD;twn1#Xv(<**PPAo+^3}eR*y?9UKqlKrNZaJE=(T< zf^ZkeE80U4GpvR))ssa`2Ry8KgYrXr^j{rd<lGo;AN#&ld26MmJes_?Y_!KHm ztCX%mRKXVx!2q9)_1RgMnkw)@uQ> z{guM!084kVPn7|BmQ(z%W=!OUVCueP=9zWOU85d@9no8EHA@U0K1 z?{2vrEFr?uDomVIeniTjie)f#wq~0gW(015&vWr(rixK9Pc*W)Y)voq=yI(WO1Do- z+H_RCy)QjXAI_c_C&YEY3Bs9!J7^Jh_+Dy6=U4!jcgp`0)E3i0`Vo~@-*F=qaSPT{ z_Be{FxXPa~LxhaH+62zQ=H5)FE?6J0W@=z*0t;B9hsXTxTTY|;xCNXLkx9(Y+3zx& z=0G>30NI@7vw0S3+&Gcj;lp8}(L4FOYkyjAM2>j^E^UYI<0)@-N2hSR@tKIOx9^bSDtT>ErZ?)`(Q4mTCI4KbkfZY5uB3dz<5bReB{U{-tHCn1R&*Na zKwcSOo`bq0ZAG5IS^`ZD5UmYrige2a!GZe~)1l8lDd{u(gEe65Sj#wHvyP9_LNh@l zVp&??J9PwV1R9#hCC^ykH&DYbUHeDzX(nhy1OfO$_9Aukci27?)0T%`ThY3I6qja# zMx>&k7ZWvyz>@CsjAk9wh_J96kO?1+=wfzbcdOBAg&nLr{JV1PY~Z$edkp48R`b?X9MaB>98R1fJUThEUgcAXZ`ItLMNs@Ym5nC z$MQuzN4^nki_VeH!~~v$SEGhY&k>~PQIW@N2HLh8w+tQjvuPWa_F?BT=@F2ri>oVA zb0MpEq0Cs0qx0x*Sk9>&rQ`o9w5~9Pg8pX(_DrZRq@y7wjY!o1`m~KMj?#(Abd50q z>{!029Q{VHE#l~BVgfZ-jT$b?(Qb|CF`VJy^Ej7#Dg~B&Evip^-gK*tWm8I$gPM$*D2ALZw|ocR}%nlWjP)2gq{TSHY8 z_iBra2!U)$T6A^?#6|{25WO{4VAJke4Zo`gtlXDA+&bYTe|U4KyUdY*>U~Wtmu$L$ zN*q4WE7FN1nemo0%yZp(CT`SeCs!r@XpBP(RSpHIIGT)DR z#`~Ru?sHmf|18gaS0qKY_wm2YYX7^x7x#U_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ4fsxFc69> zpgI;HW&)`%(3sd78~lIS0!emfwlAfs))y3Gx|$yd#Y)dO^VaWZ8Bht(=V2!q7!+3k z+2D8s=>q{^d|raE;NrYMK4YjiM3h7I#f}C^*@UgaG6Ic!Wj+oxM$m$At(Qf0^ z@dIKz*i9fmlHwkc?SO`nL+)Dx!R4ODmA@mEAD(&C)63(KP;R-|=IGRf;|cn)-o4t} znN6a0M<_m9v#K`e{ES^1yY+W@Zn+`Cf8k7vPSsUTpgY0wVffmSdBz6CGt*d9vtOxv zOX1{pR_gRo%Ja-SdD3)(upv-g3WI>yX$A&OP#mJg$GHbkG0uYG0xM%<6H_Cg08|W4 zr}zgxWh#&3J67$fwBz1Y&y@KAhwuAYgjns9nDTjFvoBB~Q$T1?fR8Ik3k0N}x|Uoq zi?6hI>#uyh$x$3`r%EhdmuCoA7_ZZsx%k>=u&No;8lcJ

3Z_L-#;I}L%xx#(XaNIMRoJT2J!NI*=B-7upN;;0V+1J@aW^2QZSK zXMxOep<1O)zX59@3ew9SqAl6MT#hh?PiAoV=r$I?k6*MX02UcOIWGZ8^3#$DdPtve&P{0Ks!#3%)^6 zOUy9(117Nzds})T#pd+S&CX!wqVi$n7P%tL)NI-(+iJ@54w0F*qP5PC7>N3Rvj0Rc=2HzfU=B)p4vmZJdCzr04K#j5fE=;9>iRE zYF5WjYU~1DUB_;Th3BM8HCl|IJt`H^mg4L36rE zc(O-uN&>6vEjBxp^A9|Mb#>4N>@6Qx?ic(B7!P9CcrUL4X4u`N%j$NLtnpY#lDm* z*%@gYL`8)xSu$p9*=ezqWC>YAQravbSwfcI^EFK0&*!9b-}mGDdz^n9WZv)B@?5X$ zdb;CplCe5Ds!2&z7dW|7)2C&<{FESFu};fv;s4%&!UNqm!&chb14w|#rES>GEA|TJ zG?(>*eB`k%D69W%#?!^uortbziMwgIZGI5%mlXauBLd&6BhY&%a{`tL&DO=!r;Z2c)fQ>>|FCs71@kf43k z`l{G1itPj{2t$S8*adGS%{vNdg4y+*Fw63BX^DEx`BGNCpgo%Snkh&e3tzN$$JoJ= zxAEULu!!I;&83WwqTwO#vi>fdnGz>|Hr#%ab7U+@-|}uL$_(r(;g>r?$w#$&B``k0 zEtixw)Zs$YiZCsjzEh1kb&5ZO)_tP}36vIXy0%9YwZ&^2BIbbObc1J;!+3LjZ>Dgyk&P@5w zcTP3HdnkNvfL!Ja=a3C>?VpbTT0l!i{I~pL38@lV^^yBMrTP-6TCdy)lEd}&$A7}J+`1@vNspDDDM4FIrtgmS0ETKpFl0FoU4_^rzDMs^Inokh zj=|_2=c$jIl_ChzREG!mDd(C}Wg!tG#IAzbzd3O9lFvWxKmLK3!f$&VjFWEr>6;qM zHh@w$k;rN7X5{`Ue7Ukc=lI?aLDIbL;uviWD9IoE3wkdZm+(4~^=2A9Xl_aJ-0aYt z@Y-*I))<~9>ILjDDp~!|oql^n^0`OFzPCF+s=;N`lmBwpiPwEU^&l!)wzUOuW2nwo zhT}21_&oj$4}-D4pVmp0X`x)O)1NcQ(0i=8!uW&Rh38*vq5rqx`|Go96_`4wov?)><6`u%F(BOs3K zdi?xW>Ze2DiSNU0MGL;Co$YHtb*C&Za$gvvh^>dve?V}fS!Q<4!x=Jy>oYyE3 zec*K;&d94|4H&+p7b9T(jcw}_sE!CsH=sTiC=uS{Y;|lV=p~^7?8*oP^HpZ{fFh3T z6eKSVp|;NCv*unQ2H?mp2km_2*Zk?WX$%x1qHx>dcbRe;iJ9@(O7!vDItG$mIo|?S z@Fg`IXiYLQL59_l`xg-`Fxv(3thjRL-QT0Jh3}AL5t+lJq)(u0;&3|@9WTAfWIK82 zMYl|b`^y5kA=9}jJ$aaP)o7{ovkey$Y_m)I)fe;ao^4n7AI6U`umB$pZLx3*mB5S% zdXK*PP;2Ba;xE9tHjwuo1scCH@K;@Beq`)-gSKZf$`3QU)J(qL*p}0bE{zQIrbo-S z`|P~kOK6#9$Y^#&|l1Im!QYY)G$PHh2F0Lra%o~>2e_WiX!6Kzi* zJ<+|l9j1%a>}yB8l#TcD;Ap1~j%0jE-#umLWlF=}u)PdEiFuP9k3Gn_X!9-tI%f+* zl=~bN%lCiYR)oDm+X_jf8Y)Jci6!(It-W_+>8d?u&(0ni1)RCb_}F#-HJoLWP`!wW zS8X^l0BtBuvL7x;+ZqA}AF`_cc8^zqHcwKY(pKUsP-j8Nx&DAP42%*!f0s=j)xo!7 z>=hw!;`Rx8*&hmK1ia1MN^HQe)cMP}=VS_*NLm5E&>$(Ik0_K$##W<&uOIsz zdC$048!hwQUONg)rVjYcO3b%IgjD3^+ahqysRb$m4h1L3*kgD%RNDUjy7tbgAiTQ` zMDePq1E_-^B1X*iK169lo23HA;_>vS?V1^$o&8x=2HHI7-xYilao2b=$&YckG?-6y z`q|_1HsNzGHeN6lF!7ce{P4}@w95u8mpot?n3_Wu**0-)C?u{O)>xz#yI@qeJj8BZ zo{BjTLNVI~^O_C{eFACUaL_1@(fZL{MFzl0iwiIj6( z8*i}b5th?1OQA0#rxC08I6b#%DH||r6^FEhpKqrA}DV76j|NLm+$Ki z?V!nrj)Fauz{w=2dO~6loen|Ho0dBjSftrV#x@_3tW`L;pEJ(@+Vt7=248n#oXR6q za;Y02Gg78*AUwZH)ql|1!^M|pop736FC2Eba6NhSBzt<;x^e$BGT z=KYrr+V4>be5YLvWM}x-ty`CBx3+jNfcOy>Fy*RW8S4D5*nq=N?;WX;=1YP3hiC6j z(oDvv8=azkXD5{p3g=3F1mHrYf;uNp<~3+Y8{V$3cY3;%=&o0Tm^c7~EPdCW{$#2z z2NF;Y16%xha*0L~&MjTOY3;pKDBsy1iA4pKw1RFj$3Wrw=vinaB(sg;^f4K{JYn&o zdM;;s!IG(=$g@B4WNKWUisJX{+@>MCC(;3XuzjUta7%nVfkkLjfqrLvRtrLiX#eC; z`hZM3O;w3Huk7Ha71lL!^ zrJ4~6k7#e*T!stIVM~HQ!SpD=S}H;@N1j%Ycj&QrBRf#K9ct<(Tm5x{b0|kfQAHAW z_U{l^$onkcQ^jr@?+ST;Sm=~n2?lc!cSSsd_sdyOise|v*IX3OZs5`Z>)D7xnh7J$ zrmJUFlR6lGSus|Z!xeD~}@-qEc(f%V{fsOe2jJyYF+qF?i&Gq!rYDDx3i!q4{8 zOp|OGfdIqnS;gh$vE3tF>CnX*qIjQ-TkYMW-229NN)gsn#9jHpW zWr)Fr=e<8wKD#LspQ1Geza{zI;7Z*P41kpR{N)jE!gXdMU3L_!_Zl~r*2haKeg8#g zatcbJw+XF*yys;xH+j->)kiOwac#P4{#gZg6`?8hsVw<9uX`eU1i>Kt+LY!k{~hx~T*~uUv{S z&mdw)(3e#ago~l%g&eLK01{h3{$kCM`U)Cf);&zfN^PEs1RD318BVSGxc=MI36GPNYwe6EX)9W0)kO+8q-t9si_1T0D|t3jRn zR2wZkj>>ZG`NKDWt~oQ`rF3ScAC`@IULO3taHBqeGBOGhik4Ik=WF$u*TC1dr z(y?Ah54X%9H(=O?-z(tq$vQn#Zx=H4c=*T$lg_@R?cUa|%Vl4iUEn@JVcXAiy8dsW zZ;o)O9O;EBtkFj`ZG!<63yT(SXs!{B(@SxcX|af}A_n~E_ECUNRaZ2w$V*BQ#ev(u>>vT)Ir+?P>CBo+k%L zXJtnzc)G%P7-CA@gIDD!62Gu93m*`=$>yh6 zE4phDp5cQ=2hkLWg|rVji%CZuEvXV?l%5vpxLWB$s*t^u2+5HJZ{KUkh#SBXUB6{~ zuJY2*jcaK=#2-bdkHT5(?iS$CX%5%Km1BI0NcGErJbo*O`9D?Fn zAoz+r23eU$vdPCu=vHj^9;2B?f7@fu4-cuHY@W8r)p!nx41d=Pu0?*th!YZkt>NNY zf=Mq2P4!q_Q!mHZ>ucHfLsQwHIT01XFNDA7YdefT9tl<#VZr))i9ME^oO2+7HxGY^ z8Gvf6rJ+qq68q!9tJ|b7bwXa>N>35D3fNC0f?bHuXo59BQ=(t2Rc+JzBkAQZ($Cz> zc&0@jmme(j+Dw~??>+kKNSkBaM5CQ*_x3{R?HHuQKPdekC`yKwbYKo-{7J2k2VGEc zZ%d8vcf%Lp=fe>kvh=+on5tx0Wr>XUEB75^sYZZjD1CHdSS2WzB5ESv=IMWM5Sn(ioTbeJ?0KUD?*GR2krgm#$Z zdIvJ4o0a-ii7(fG@qvnFBC7YISwyOkI;k_r<;KdH=ggOJDuCaD-xSlI2@8B1an)_mV|k#*A>h@K6c$rBv>K zb1KYk*&m8pex)5@$ce6ASpERkdepc2*?90LZMX()>j=%mbiHi3uCdex&n&suQlmO54?S z3_*)a=1vVMmVA_|emtynlm(Z#MJUXsM<~G0Z0)McqQdeYMFx`OmI`2t#JS(&>N#*> zAW*@=LgEX3%5;#jQ}l>R@_ zV)+_U7TS|li>!Mz($qKjVlB7GHC%|7kOmu2T}Cg#p2)8dgj~HBneAX7$f%QtKUB0@ z%RY7WhwHKVZoI-Gt1tmqC;Z{OipU~A2Bb?pJRLLf!V`DnBXKwu7|RZHe}D+4eg-W7 zL`(4qOs(H#2zfu1N3(n}ROnwo9klu8n*KYBiLnLFkFoj7QlDQFit2tj_DWnMGEDr~ z-PdD3n3pgQV9vsoyN^)DW|JGdN45DCz1$u;R2B&!D$A0^Il^)9zDply{F%twvzs(c zhaE%$7&da3xhlVm4L;dE*GvfnS-c;~wwg0T%B4~S6@I6OCXn?t0$jGuoITG74F||Q zIkkupva*L_af{*x+9Dk=2Kp6b!ot~!lHL~#1xf5IsL1XNO^uHp4iNG5UJ`Jiyi+}1 zghow7X?DH+-I&RvRp!!cj`_2B=Ad)7ZR*o3_?ca}?(BCz7rEoaL5FEn_L_OOT#3@* zwQLaaQ)>69JY=l@e{6gxGXLBE;~R`)%=q2E4C9QJ5@GaLb%aLb?yVYk_c+#Hk^&Xo zLS`6#qz(h^ogFwe6(>&{!m>p@+>)LD2$rmR!*$k0K=c$kYrtjm~$n zNc{?C1eMCRtUIc)V{>QJ9 zc%d3@bNxc*pJkG;``DdNm0;#H#vCnfifgFC=uEqc57`#zZ>>uNnzzV%~Ef28ReW)Y3I;ctJ;w>hJE#1fKMq^@PT;JZvUF7;w z04hqg*~Rz!9{&2y>G`8*1KW+2gPnBwkkTRj+v;OCgG+Gv-rFwl+m9G$F0Ph-#9W?~ z{{?51bPFjyR-SsW7?tz_L|gp3qgVdR4WYX)b~pm{!SH80-TKD=L-Kv$BkL>SN6}tI zajsr~jN#*xDw z2Ro-T1yOn5Nn0)Y{vAi|NkKqj5%;uX9sI16Qut@1h!ufAO3dt25_&QRIIn%rb@MHM z`*_kZIIevk8*}A;`_Zo_i>B{XpI6}e7_?J}JbvT1kqh)QxePS3WUS=*7I7R5i;IFE zcOGP41X#0}Xh&~~Ctc;*oF`LS;P{~dv;`W`5e0Z5^6$vc8x2)`dv<1Lz$^XV|ujO$oRWO%p77v{yuC^YQmWzf<$$HxcBLK;>ywf^pcmHdxb z2TMa*YVB9`SO3?i{>RH;FE|RZoQaAhh`T?RG9gAL*6Ah7up1@_Tpvbs|s-pgweUCzol?4v{~< zln;Fanui8=?Sfx2)DscNjFfk=;g_~h@0ZH)4^J^8Fx`;)_1Mb1FP3R`w<3=$XCgxi z+qk`K35%h=h(A%lF7trH#o@U!JxMP!kbJ_bAeZ@ZG2uVP^2hCj4`(I@7dF;FmxR=U zQqr`MOn%1MB(-pE;YnN@ZxcQ8;Kn%j9f)i#-hzW@&vtd4LW8~w2Yj=EML_J5>|i! z_|)zWBMyymnA@k@Ym!ulDi6Q$EF%uMuSVk-(!i)S7g?S%|L3#)+I-S2K6J(et6)y{ zF3U!HCfdDAmoD*7tnZsH-%c?MY5l%kf8|$0Qt7h1WuC{edR*!PRr_DcH$foqY0e)kp1B&J)uNz}G18=?A?F}HF7Z|G3j=KZi) zUiDU`*;udAn|L(V=5vkGg|9V99X%vB+`&SzMJ_+%8X^xtDDrPM z^WWEeDP)max4@I1!(|DDGD9uDli_{)pPvOF> zrs8f+196;Icd%XxA%291a)|W$ zKjUPYj+teaQtc?Jaa32!6Mr**V#y+;W6~VNtUj3_Aq}KZ7jD%1yISe5!>RRmeHuI( zGs$7?n!o$+Hx9-;W`8)_pURqA^yNuDBa*@HC6%(!0;q2^Sqzm$s78dfd24fQf=9C0 z!Qh}9nOj%U_vc-ggiUX5s5mvRY;J}nO)FI=I8D!mZPmTew{&SkFXM4~*hp-aqvPYm z9KL&0@*QWj$1@B$eP#h(6ah{)&TnxV`qtlj;GzdaA%6i|5~1vmLNz7qp!?e%m94&GW;8LTy#> z{4=+sw|`t$*T;xaE^tzEY{Jb5pNgQ3>M9R%0gZr$3`Zj!QIqS|;|U6OC z{WI)3{TFxZ#WkR}Y>tH0}BuG0nD%b>=paae||n=&qTCLNvKaOT?*T&GkwbY(TdGJ84J-= z8h{>~rz}rjWLz0Tw$pQOTf6lA2^VfltS(~ zpG{q(3UO%>O7v(UU7s0i{QK4`qhfv0Jx=-=#Ad_dMgEUo2KEEX)Jo!My?f-B^|c?M ze|Qv}-&u!Hq{xt#RfQ2-F0{|p&I9^%(sc-|j5HTEw0}$&*ShByO1P+1#3@pr3#a0^ z=cw%O5_*hvprG)^UNCqbEf6UdI+ML0!+Yo*2BZCa=oc%5G|or)+xXLvud>n?=!oj| zLgBZD!}+C5YvNd|Y4OLBNhXQ#cb6T`1oW|Vdp>#l_HE4%pYoS8`q4?ygLRI9&-fCZ zBJ}5Al^=-!YcWRSMWT%EZj&El`ERXRx5b#}1esziW~?;&p$O61Cx$;bEk$@7YiK-g z-TMv1C+=E+$fCOgAOQv=Bo6w@oNPGVM!OxTEX= zJxIL|^QSVdw&+8dfM8Fak!^{Wc!Gp^P|MD!6E%WlySzqj(2fJfv=Q0~5$+BjUtI46 zLa)`U;|9d!m23tsQ!6@PNL(`nCkRVjT7S4lKb=wO-ZQZh&yW!6c=k~8mQB0 zqsfY6DUrI5f#OJ~M8{6nST^*$N@#>Op5Dk_L-&$%rrQH{7)>^%1}`CbfO~9KYbp5s z05diS24QFR-MElPXH75Y=Nk$vWPn9lX5T+nrsU`6f31xYikyI6d87$oHb+6UA{pcx zlxBVw&v`~djrylT3J8=)&Hz_XGvRQsDuUIi#n`Y=MVExx0~}qWE!SdhR>=F5CL|ix zl?pOW+QpME&osaq>t_ZOXaWW~cl6BM_?ZZiCLqJce1?wrM#&M>AwjwZ4ncP|)jC4M zmk!>!yxaAm@qSRi;@(KpNozCR-d({sn560lQ6I zpY&^Jpazds^Vw#AqeMFZm4vRj1oUVSst zZdlJ|n-_{7M)|VHOUzi9&}DbN0B23^ZkEEIlbyaTq29VLARg159qQkcbZ_3ek@(?E zH?L{bVw?!^6*-0leNfFlS`2y-4XCY%xF@5uwLAf$rwP>KjMJ-tXtasY-OM;z^(k2a z30gqc^r`V>q+5fSK!Z*IdGylyHUV6=o3)BRzuxN5%;rP2BWD0S;~xhBFIAK)2tmd% z?0=WfI8T1My?T!(OS)(Y&w&z3OzyX`m8;z}a)l805efHj6W+#qv{(Y|RXsc(8cMq) z3qRiIg~O=1QZh#?_bKsB@tf8)gcgi=({)gYM>Pc3GHMX6NJD{?~;RCfaq2- z=Thu<9Zj$=DVSk{-DoY)S3H0dcpb2{2t`i z8iey*p*7YTAKwZJ2}Wavw8A*!7u4&f%2b1lC$Og!G{Ybb7yK^@w7cNmNbNh@0r}q& z(lxjgeFd9xH`3Px8&}P;jB3I|sOaxRJy68@-3mloxtr+_Sk2Z zE)&ZDq(ODVc{n<6oYD-3d&6{(ZDqTPDBisQxjY!FfOCmQ2;?7bEEb}B5@yui0mD+Y zLeNRQ2smlyRv%`iciDCKVyN@!6k-XR+E7UV2ZD7Y3&mtyMY{&bn|(nLcnys2jkq0eyo?z-!2m8SpYjTEF z=Fz36fbbh^;!s`7ccP>q))c%_4L=`Te5Bc9?oUW+-!L~(F={$DX4)HRn|@St|L~zU zZyaSi^&}#Cdrx@*(jfx#1m;G52sH*AU%oTa*(m2TeH8DAbPCkBJc#7|X$-_pT47?} zlTT0XA1c5+i9xjgp40Ho$NpV|4hsvx^y2>f#c<(%;d9Xa(Fv)y>U?LUzV`N}dILFXD&>0}({jPk*}Az&C7$J)-|a=YonR5a2$RWr zddFP|9>AGLT8a(OK8#%P;gDDYSV}ovPFv8T(S#$WCVsb(;)?7KSjGKvop3wCE^!F7 zSWdCAd|G}wUZ?TEa-k-QdEWH?a-Qmwr9d1xMjxMU{1ZVZ3gx zd}ag1?=5`(SA<$6*dRm*0@Bkyjw?kUZ>Zge`hXl_Nm&D(c73LA%Bl@s-wsT@{qaPO zLM9*`G`dEV%#fKBISwD-nI(zY8E&Hu^P+aQxq?hz5|U=yq3illCdpq#d$=EtEzPYM z*Z{E&a7gZo_Tb-$`pJR3aLx6ZhH2>O*F4iTa{`j{dA3c&H-A66;8MbYVL|Q8six&ir#5*5g zQ5F@^0pQkY6fy-U)!t^t5%`abWL(u%08-5$~Wy?Cfc?mc-fyI z*cOhgAaeqRf;5D9_VaGc2SV(=AD|2)bPI9reC(9~KsO4|G(`ju?j=j4_cQ_sX2t;t zo3IOq_|d_}ir(NXhHiT)sEnl-sC+kiG??J~*r+gLC*+K8`^QW!AVzULVjLX~j9LUF z#t3&&g;dBjE{_g&J&r_RT~r*N&ii-la<<(G9)wJLh;VwLKR>Xk;5onTY3ee`oce== zUeF7OvJO!gKZ1-3p*6f;FgzQ@X6m0}k*0(ADdH}$C4x7Quz-3il7Z6gq~HUwwc!44 zd19z`xLnBrUsgJ(9$;-x{5DZ#aLxBibGpeVb{EPKtOb)Em!!3%JKa#m*|<}b5QoZ4^X)6FG1RU1`vtI z5HD1^xd32X!|M9>s4@(!A1{U5q3b;%hB&6(ysZOKqMQMzVOgezf@pFT9@R|SL4RPm zaWz7n)wqAm2YgXMTsIzn%n0OkR!;2)@S`;#42925Pp>Ms6sSWI2EDNEFP$aErs)9( z(38i1MqXWF;<)5NOvdoELdr=(vt%4}Duobcg6Uu7KP@0$l+o>aLqAVu2rZ;6x0`w4 z*2Z0DehJ}%XHXwOyPRqqxI+!Pf8s&9N9+5L3>{En9SB|s{3K1f!#3AM#j(b-?ZaJ6 zLEnmakZ_{`p-btiai~nul>$r#rbsKqkSXe;(p1MomNWvkJcZ@Rg*>+>8UYJ=$5UaGb-bt zV3j!Gy?d@?&|*G+8GwFTv!bPW()`6;LRw=*9HXZC1YAc{7{$`?>BjYY&z+Wk=eKm% ziBv@9GG9FivP;|99-iJO1^z06iq9yX^^hvyt}Fk*c_oE_LX8Hb-GTy@Eam5SIqw)+ zM3`5NU&XdT0C&gT2S6rjz#BYK`2=A)pNn;%LR!-GGef+4yJLlr^eMdtQ7_(mu?P+y?wnDi=~&VSH0G-5nw`vqBt!hJ>bIv5 zet%bZdQ~o)X1T%V<)20Me*m9R?)*`fSX8!BXQh3cK~gkjTMlYCJ|FG^Zc0Zo6`!(q z(_a(x9B~A*ssT5ar%F8wLQ2JI+T{`*CyxVkS33>Xe!}1zMLU5%_pcGO{Akb^#|haL zi+UC0aN?Yh3;tm@fV#pgMM(d6Uu~PzLUL5Q#>!%h>QxFTY#DF3ubLnrxBchE;2pKI zKZjzGhD{bBf$&PIJ?M#;iC@{R#d_bSLUNV9kmEDX2pJ#1m!`Q={Ac0QJ-dFIh=<m`Z|tx>%$;WF5RMg964BuRSJRvw}TJH zun#`-EAOv7f3030??Kk8gLgDKH%r0reCaX`&m!~2w5Ci5hhPl@3R^(##D}IE9<_Ne zLb`U(CXlBjiaoSLMLM`R>8=sxKQjR4OXm?G&y=po^6LB3BanP-XKQb^`!3IsNG3#^ zaj5xWt5JLK&2{~M8MX8nwFvqjrOwZT;fAQDa5Eza8rWEmX{M=K&j52r9gK_h6r%8m zSbW`7rW&5ZwvI>7jjYwGMHR<|CqrbIRyPV<1C#sRS}ZZ~S@?jEw@?uBM_5ziNbpiC zg#+1j5%OyA0n(8Za;n)Wman(}2cjRNTFVV@1d_iGofT)m)Z!0@_e*<>_8{d)%QEem zT>bL_e&io+NTlb&YQ;S$`Yo)^-biCh;<m_yFI>?r49Oe-jNRBcVhbBAG8xlSX?g3!Io zbGuM9Hvj}v@AP1dHj+{;{lLi|NIg0^0EoOJC^jiQ1u2*g6?{dKlc=En`2h$a?LrRET*3fo zIbCW9b}9aiAcYTCa+?T&dUJ{S;R&ghT#%efMC31Tped*#`M~9WJuqJm-V)aQxH7vd zN5MOJ=NGeo6}a_KYIwSoZBSAOy63cGdvKn=Y9YUUu^VN_S10bzXU8&7d7(1JZ)BWh zjx}2d2gL8&H~URP)#e6BD?+6=yy` z81GDdGxnf1L}|7lKGZnr@)*>Wex!WmCOi^XzY{9Z4JuwowZ*mxe5=~v3As=eUdIR& zb0s3bgU=n}Iusb%27z*T_gEefQZ1-AIg(BqZcE&Amzz)--%Z$;JF(;1YZXV}wIu82 zz;KUc(7EP6@&dpZ|5cG%2;u2RdIkcm!UOc5V?Dt(ss0<{3f;`PO%$=miUH~WqBXej{`0-%rCFs!^`h@p5{=-QTP@SsxN%495k3Pd6lcg)1TeJP zvFyg2L^8W7z%?b-eSj!Jm&~rxHV)Lu;?rxyVnS36v^ymXgMI)TYRsSxDmOQgTjAUP z1j@bUWmZY;h@0y(@pQwudmm^tX&~=p&;h#Sa?y^@*B5?4MF>_Adp#WFve!8Doqjr~EwkLcNC`r}XocQr%KpB&i9V@CJn^;Fi>I zlL}pJlc@T+HYM4ec4@Z(1OTVaCfF$xdV_yhOl$`%W$wT$gZWJS1*8Lo^Q{|L@3igp zrX^H7Pa?7771Z2fSFp`M(kh{Eh&Mr9v!_&-fm{L% zEC7wlq*H|tL893|jfsNn;U8*+?jDpCPWJ zO^Vv=i1QD|hgCxdY;;(E5d=FUv>#)Inl_Y1BjheU6X5D@R9k2=#f?D(WjGg< z%4peXVP3Dn1gDztgh#%GasW3e842fIkPk!DZ4_h_+E!l-$`XP~=S?Ib!dJsn8U{uw zt8N-$cacRzl^3K|5<2OfS6)XIRhND(3;E(`W9;m16mr+C(Y|L3xImvp#~A^WP*({Ip>Y)kCC$WhJ%Hw2MsQ?^}sUT z1|ny)zsPH#+efC94KH{`_EBm6zaeB z<_ypwJO4m&la6t1N$(EpG)-V80>Mg!oOvvV4Hxa)a1`BiTIAlcgPd7ac{&>tx(}fH z=FhGH0-sJuTYIWT$QHFAq(dqSz_PhXtsw|%1untkF^DnP2m$EZA;ptjU>vW3cc;~X@TOpOLQelX6qN7DLG+!*)!(474g9B35M_4D6)_?nBLb!7mmE&5?1BXlT53Y3+Gqm)M%U;(3@~E-9MhVrV z{W&b{D_GiKUO8ya{o(eE4O804Ux_tzisMuFcS|^ z^fdbL31r7o_Ru0NRD_C@rMMrFijzr#Ceqd)O+o9W*{Ql09rZ0cz#T?_4qEK!dQ3|( z6cm2A?{Ap}Q^33bNsFOv5~4>UeOJ~Hl^5H(6#eZ$Wu)BC(qL#6NX(rpRaiT6aTpn# zp{5PK(~e|6JpLla;k_GTB%>UXRgg&Kc{4^k|nw3jpEJ5%Z zx(pgla#5ThG*4A|+;6Gm)8)b~6-Ui!WiqJC=OTO~6M`>FOb3zHK#Y?$O-=H(?Bd3+nAuN5=a8elw1Px>vK&idVVjD8#niYqL8OJ zKFsv74H6gSR44P6hOzm51T~@EV5=j)Za%nOd&Sm{cybePON`HbMF|zs?=Sq)J%>cD z-pr|7v6&fb9n!sM6L%4*h#)!jD(66UB3uf4#9l2m=ahd45sWTAhs^6uzMu#yTz0V- z&807o_kT&iv_p%Bu;VLAG;*L>q_>~&Rl)ef*O3m?7ITHZT8%P7#T+P*6t+FYlL3V= z2_%fidasl$cdPY5v8wfj^%;@2s?d>RCzfYq+dOZ&jY0xc1!Mo$`6IHZ>iFSlwlm%A zgzBu#JSV`2q==|~#l4awMg)V*iA99V@?>}@^gd+uGn zkJ!HoAk!WZry4;1-T@KB?I%b@R~SmhAv*Z8RcAQU$&T60LCcJvEfFHu^+3F-DUq6a zFqS3-vgzOjHh&VH1L{YIF3%9~400bsb*_F532{#+ zM)r4f>sEYt>B!Hpnt%6gHsk8YugAjK-drZ=?oA{#OpkFIBVtBN@l zN}NUAvy&Smzv8q9HZx03N***+5p+CM{$afJ0gMe{v{}ar6PrxFKQ(haz*08e8>Jgg|>$gUoJJE?Fz`JVZp_$MYR&c>~BBX>;8yUA4Bk>ZEs@ zY}k+{BoZ;)q%6CAXwU-GBcdj|*f^fg+!@LSoXdT-QW+{#8a#Ue;G&LVZSn{;CsgIE4QcDowAjlBurhwtPz$CT5e?$$lG}(=h+-EH@K@5!eozE0PGk!@-C^wX zGCV~dO8+a9kl>g#+(u25IwBL<+yDh^qat{N<&UJ8cYTTV^@*+0-~MFcBtO8HV*3;P zwE`kA294nFZh&f)QX(==*{$J3!>z%Bdv{5Hp83%gD~b=dhkW*Q<=FFeP%k|Njgrgf zyzBOUxvR*O)pH&`&l1DUvpf++$2na-Kg}^(NVKB2^&^yM)w`xz@^!u{b$o^%je2Gu zYLX($FgOe|kLXijmh!hgRQ4H5du_`yg@hUoXbOa?0RnM)Re5v&2ij9MPtSf18FEk`7GYmmvs^wXy?pjNN$1MAnBC&Yf@ z2#J(y(01D*(oY&URYG=wL43^nb->I8peNALceLoU7(`k@g!&F`clmIejLg410Ti*ODXoW7+0VU+L@GVXv~K%by5+ahwC(GfaOy8ADTob6g7b+TB|zt3^X|ezwh2@5-=xfe5xL8(aayK%0d$YAmNrlb#(=bMO`Ffk@!Sil?WI0?g5%1$7((4`kbAVmg${Fr4vMsYajLp_m<& zvT79dS0Lcbi~FuSEVI)W&u{aCcVqfQ2JeCUuKAg>Rjjvw0q9?g(I+@0!`!X^3+!{= znio%AZC2bniIlsB#RupmdKH%i#DJAIJYkE{CW!^sYLQ-?aS7GPcYJO5EtG|iq8y=ig|wPy=j@ezQxG{160wy-=!s8(OgwLMUjX_Yh~>LF%^cn$ZJ9vik@N z^5)Oe$C#LvQ3+vfY#`msV;fO~<-_ueAHGFGOUg{_#g3e)&rnNIMc^b4h-s<9TNLqD zC};sRqDymYksmf0(qWgXqwT<}nuI`L5D{1yM&ut~kTyb5ljCY#IsG@{dg0eKl$cjC z?nfBU7M_z=YM~=kY#krj4B17DR8dbnC{L~q7dng3ebC1~Dqx?ZZ3wNZMle1yqm{wH zPG`;PQu%x)!APP%cT(FBrenIpUv~K4JxC^^wf6P)-XkiL*o+Wh&Kml-^!j!zWcZB$ z!-(L8p+~z0J^Y|zinHaA7S<&s)3HW82l+ANM(?Lrp^b`Vkem1Q)v*(dmzbd-_@dY-J?c?&)lSiQd!Qd9*h*k?T)_y~L zA}HYJf~fE%Bs5WcnQX&_U-|`EhJQTY|ESqh3`KrE@0@rxJi+pPqG@>I@j`qI5wkH# zhe@LOaFnOG#b)d~U-*nSD*%KS35$>%O_^DASLvN;ei$d(Y<9Mh4yYx`!_{m4ADuiB zKO6V*btjK7(fCXF{g;q7LQ#ezqJ9ZrV9_**K%pz2{`{BQHDHfAkJWh0+XGWXm1GQ) zkCTBXG#oKbB`+a`WhwF18Xn^LNV$}XfKbQLIU4(z8ujZS{PT-?Q8-_c=hobsFRfpK z=vanj|7`}+uy0G;L3JTV4h_GOP_S8-o_#q9D1BtySyz|e`12(iK*;)I3^9~A#C+e! zkVUJHgpW*fmm=c){MdN6An&%7MPpf#Y(oCr@VGGw$d* z*>5lCBzN)}X)x)4kUlG4c!$yWqwhoE)6#-f?$7TI%V0WArh;$4XBr-dkN%HG|K~z7 zj$egzM9trVv3(-6nESKSy9byzff3}?4 zSXs01<1Su(Zb@gTP?t26T(EL+s5ey=ds~Fy*;!5OFkxfDg@(&V?Ja4DuiT z*GE?W@h&YP`5*@-Jnt!HSf2+A=zXi4g#oa>_gxnj!ub-!vcpg^>gbc4S;|V4lalAIq}cpCVtrUaMtse(VyJP7nXKhNqOIbPmwJd!DOjO=#}Q)a0ld6Dfvu^>T{ag4j}>29Zh{t+B{Qll&>;R z*ZDylf1CQr`}U0d-)4l$t9{CT*nOy}*Jy1Zv9L_PqfO#i(N+7Vwe2c>h4Cm161&6D z!XhnOfk^cNRDK(gzx^?sc9qMBm)06v*E>dC!gjJ|WzfE6aF+0YwUso{EpM&8yEl8# z6CxpImOfzi6Sfs4X<%$$42G2U4+eVR*VCU^#X@6jGS&iY6!&1 zW-x$+gd{qbYjM+t-GHb}^M(vQ(XIsZ_Ab9~UPTna)5D|P_Y~tSt+8?<9pljF5Z4dq zAgsXWPHNas+Hc+A{{3T9N5{T*?b*#Ay_EKfgs%c)iqt<=3X{d?MtISkY(EN%8GWo8 zO_v9MW{r&L`~d@tDZ?r+X%;@SS0||*Lm*(1HjtjPot7}-;LiJgPbhfr>g+WlT1IdT z+dcUI1zqX_7Vh}@kFxYcP6)9*NtbQZ7FJS)%^7B?N0JF`?0bf3HUuF@fsR9$+3`Bf zHdSF;O)7*uX7g|`aT_*y7(Xz7)D~O6df+~R>gO)&M+f%3onAZVmcH=L11Q)L{&G5e z;RDaF2&0ZST|046_29a;n}o6Z4+jBn{}+j5^1-Av+OgL4K%4;F=c4yv1Aiv8X{o?l z%Xjlf(huEkM9n==f1d%z$8W43WTM$ni7U+Tj;DH5<8Z7~D-GR)^Ly?Y@WSwMSPuub zf3YY39eawgkh|EvygVc6xQ%xK%jH~%-M2h-8P)2hLOaD@*!M?}DYPi$mE+#@cm0Tn zc6gM*64k=o-2u$z zdw&_U+Ae3;OEDzq`CQ<#I?A+>c56RxAkY_sz#SVg+%dy(Ef`hwR%S=wA(XqFcFvpK z74S;8_ACoz{0280%xK5>$in68IP;0hrU&fS@)cYn^%t3EE~a+(AN2CBhd)!#4u2)> zEPKl$pTKyPQ}Cx{75A&nZ^2wKdN76@QN0I}lS0xiQ{_?f@xHZ!nFKul3Fd=Djs#gw z$djNV#m^k0M!(AQ=8*A}j4Pw_x$$a6u)yuf#8+-l)yID*hQIDyz!9j3{Itl2>eQ^A z-ibKx+FI55KLg-gtIK|bX~8YCv-;^P^jrpKtSwonfY{CYK4Y1C1N48iw!?#aRPv*W z;`}Z9=NI)j<~B{tH7yvRU&Dob`#*d#*fdGF2{tL# z+vvYh?*`BMJSvKDw|9hMC6=cK(3Z(RzNawPN6WBKd}akz6>C7ksL`8CfZDk%OQEgE z@x$UD1$sWj>kg;0u6nVS&AAYc-tDd>d5ehaENm&|BK(RZckuiYM)CDpUrC`~5={$?k{fZ{(YbtXXJ4_`+ z__)&~;IEs!YK{r+s7;JWYnrLQGPc{ZYtqv$DR+0-TR~+mtLLqGhIM} zO-_1yL@v+z*T=l%vTF7t_`YX*Pc8fT2~y)@sCrp?0T?qI8os@M`t$LF?x7DbFVQfa z4vhz9RKe%YAfB3O=t5c?G=m`;+Z#ZyFdAtiBYjxCvq@5&TeSN6jrL5KObu&dCzh z0;>Oo>i%<3Hb_D<+~36mFDenoYmnGQB1;6|a5T^n)01<*;AslI*OUq6%}_GBn*%&1 zIXAKUw#!M~LkC7$w^78Z zI0H8pV;J%fItef+$Y~smT=ok;4&+PVVhVslEGc`SktM;W9zb&O~>^To1vM3gf0BF0v_XqM~%EP93~G5(m$n`kt1tg850_Zl=yK z7Pc_3h3A|Yrhbt__ZRqh)e@T*a@U!VOF~+H_Fitc7}Q&<6QpiGg0w_Y*vbRy5Xz2? zj^<>Z9Jx&t%;cx|HzR=XVFCLvqY37f+d|Q>zRzb8%Xkj4rcVg>LSOb6YFS0Xys?I7 zSGnjD)!(xfxkUM4h@Gz8<7#m(aDWVd*rn3(r;9utJdrG1u9d>3{Rw@i1 zVxnzaTZB|e>=D77trKKo?c$9skZIGAwvK@HXyjl7X$L}Y7 zpeCx9BOW!ubjZJ!ORIc7GtZ%cU4IyJ*M&iN{rzQ_O$Mi9mbUq4#ETcgj54v{f6&~3B=v$dH z!2U+}eL*F`Zsp5Jtg}i)NA*E1jI_fj%#1ch*aB`L0_J#SBqBXxN%$ubbG(3Dt!e-8 zV`eWw-a5^`>g7*p87F}nv%+H-*&5;f5^P|&ly-60xee`vp!c4-`A}gEhSs-DVqW3z zE5rDX5D=_Rx9F{r)5BdhBLxpy)T-a@S5f`0J-QTEkwQEpw^k|Lp?jv^>1m>^vWDj*V)iptsYNZZBm}9hsWwc#;FS#=dj;zk>!5|W>k(95bk+l+pDOyz(Hswjoh8>ud=nF zGWA15N9j^w;jO=HZ43pu)@g%87p#fMJK#Ay7KYyCIz#el-!D|Bfihztvjm*cuaUdF zUx4iL91UtzG)ad@CNH--;pq*qi>N7d2%XmIyyv=e7c{EnWZ~)NyM0M~@x96tA@62P z%25o))9k}D?A-oN8l=PhMMgp`Uyr;^xvNx%8s}Fpp-#&^J!pD8oSTXz!jap?mxHW9 ze&o|@C8DdLgFWpzRoUy<5^0b{{tUHW(ecdt-%pF^c?sgECmF5@QKcT7x)Oob$Ge6? z^pF9jjLU;8%L_X&?-jLi^j)a(&lA&b(&eZa-mOtthN`C$BIP^mwFXwn2oxN-#U#|^ zkO^%8Q~6|U1_=Dgrw7B>=hu-1O?ruVCLH>5dJQRCFxV`Fj^7@GrTLT(Bm+FHb+ypt z5{rfDFDKe|Ssia>ySbYr0pt>5Gy326U;hA1m(Xo4`1E4z`y{q31|!boa+%Ztmnm7q+LXP=&yh-X7GE5)p$v9bwdLZIBROFEdmA7a2nW!$socwgqp6C zv5CV>A`4JkS>5{pB&0Az^dA`X?|VDu0Ni5bU)s+Cn2eok0nLKQCZ+GLV~)=Y9gB(p z2*y9=7d^)XVvg1;?H!xZ)qg(lMoj>g+LH&UHJsin*&`1sJ z_hLp9R!1$1X({#1R$geWeWb*jtz#CS<#wb3sJSK7$_(5b?Qu)3J3OX+dRkD?NfZjM zwLf{b3syIz_UkQyszD>SzZHo5A#tMEd{j#O1ac+{BiUg~pdfRJ>8sg^0_mq2{w1ul z0r&)X($=JU_)lr1^nD6a9f+f5_LGOt#{sxn-^Ca zYk&=)b7>;&RF6=j3}SSn`V&%Qn5EH~Y?AD}xf{-~ww7}v-vl%k{De4!Ky?c_Ln}Cy zUlVW;jdFF;5Y^O>jrFfwoJe@0ERTp|(#!29Oh|D9x2@8KN~Aymj@nRE_y+y&`PeWi z;9H;pFaBVI5vFh`I6XG z6Oj&>?$WUPOX6DE6)b-EhKkWpl5nSAr$&=dt5d= zkr`xpWpM&Fyb9d%dM~3r&c`!;?XN?rhfW_=f%Fl941VvmG!5x&ZasCxD)~QPsB#CF z-^YD_*zG=h!tpDSBWB4ZqZxXC;;(>px2)DALk;blO$agYpj-)w(?XTs=oO! zWK+#UGCT_HNHi4Ez9A&-yb}-!6M&Uh-e2(j4O`rWIdFmD0P8&p#==D zy8R@tzr!;K)x`4`kj$a#MSQ>X?z=;5J^3%GITKReMD%UM_%$M{ z>+^xxm&xODC(uZGL_m6f{a5ObCapj#`M7U0C^A|4SyUCdN7Fc%r2R5a1fuUbgj`Rm zwn5B(h6~zYmmE~24Xi=DQ#+>*&bS4Pv&iQqr-e|7R4=nL%&qDDV~;jGBDM+-eO4f%Hv#Y-ZKjE%YfUde^;0w~_b! zw*wyD{>x5G7Pm|w>@pGAiKckn^nH8OPfWiNa^9Gs7;_6{sDIbrEm-83>veSApD3iq zM4)lS_?{0a1Y@gv0%oAQF9{m(@Xj}axbO)I!)~9$Q^xi$hoE-Uzj!ZW@#k9+3ncX* zn)|#rkQp1wMvt@M#Pqws$!OW{P#3&!6!Dux$FpBGL(tV`HcZ7H>9I6|xHj^T%Vb%O zx^zOo$7}(N@$tM6_$mR!o<7ok6Xu{Kb7mH^&G#U(Lh8;W>`_2e^nc3ZXE_8u25j+u zfd~)vAF%)FGyee~-6H+8AA-`BNC=me<8}Mgru>i$#`^`T-uBl2jj;b(FL?@DCX7AD z_TIPDzEkN{S5U^PsByfTBUQKnNS%AhW7rl1;gEcLbFT)OL}eFn^7apZ#oD|o9*OBI zt0HLfk?cxih;(ArPM#P`>Bpkj6XgiA0DOZJnSepbP-i!rouwSiZv`p|={`G33 z>I@cP!(!OB7)59o+@0L7?r|3l%CrGJTghECx55$JkD5*AhrPQ!J)afa^U}LR5JW*O z(v`BLz4;dcML4R30s$jg8L?zWa>a-@kiq#xS`9t;R`X&O=&aA}D=(X}D*L+@@r<*P z5YzJQ{gW$qpjkjXhnbLi`OiFHb$_S8#>X|Qxn(u#2XLrfR^sReI&LsBm$wxV3mT2x zz^Ij`na(l1@uya}yp6NF-}M=6ojilEg+-iNC6gi=+)9&Xjp);epf>Wid73crgAPUH zIc&us)m&$FbtdI;s93+gV#KMPdR#;#Qnk=}Z}3@vRSxOcYE{*ckob4aSNj6k6bi@N zFBWrj_1yD6d^u#epXGkD(~(%h`rD!Q$i;@b@Y8v#9u6M0W6L*hzN?#>*Ny6%Qat^^ z_1up8e(m|1hVh4rfCbkn?y>k|z+|L3@$oU0sC#43&m~_iP3^?HgE|`ZyQxBN>^xzR zSUXTFp;J0?l7@ttiatB{#Zg`ga(am^B>&6*h$E!r1S*ZwtF!{1@REc3NJw$-hp+RH zQXTZ#g8nCQibM>Qc)iDN$n53TIj(Yc?&ec%=11-f> zeZYBcwDpZR>sgnNDbEVb8lvtj(K$b%e3PUc$pMWx_@acnWtM3P(?YhrXHvC`u6+DSwIm2m zm6DFhSj{^Pn;ufWwlt;+6PB#STI}$C1UDO)TB2HZVvG)S7zb*e!79E31>;MJj%@ z`>v|ZG!K0+*LrW+Q+$0bv#zk|f%9a~34k#(bwG^4r?dz_oJ@?;dgTJNxMQ?Rsy_B8 zBxKJ|%B+>1)91J{WwwwAkW*=d$I7v8+}zFU`s-f*`qS$zMfv`0FqFaHCAaCf?6`Y) z>C3K%tn48V;h#_80E{pydT^h8;~j;Vo4}}*u6lhm+i9v-H=q9zo=Ydj+Y4*o&y@cj z&#A5$=>`d8bMbYf^@7?Et|!zOjtdp@(04j+IpIYd9QJ*ZX)k?gmO1j@HX`LofSq^!BD_N6=j}aJ62-*ipXZ@Gr!Xn8 z#Tybsjq1VI*neA=Eu@`rp4bekX~^L$P3LZbM%Pp4tYkM%7J(rf%6*tvZY%Q0&UC*V z!Jd4NRTm?Yv9^pS{nY`s2Ce~h=~*^%2TLd5Z)Nr3lXHz^Ao zN??*WJk#6;ucNPmjlJ-pm~8V*A9VG8PNk8dTZWr8JL4VXg&kEH1-41zt(n0$M6IS# zP`g}QJqm?4mBv0F45_J#)!=R_h=iCN+PIs4T7lHv2Mwf&9QdK29Arq! zw%ws5EX_~xtS-&vbqmJVADiudA;8a{fXmRjJoT0Ix1pJ#uxB&CTvJ?&Xk4iXN_<-# zu(uZ4>zssx9xz~^DM`eWemNgqdTG@)tx}{3eRduN>`yLv&p-W)jpw|!!iyNPe|*j{ zdk@cf>3ex$?viY-1<{(MjGVwPAn-wtX%$@;-!}Se|NHj?=p~33U>%QMZN~iTVqO%Zrq44B7LTQ|OY3#lnh#~}l0JY$|Sehti}o_BtJ{>h^0-rZSj?d)C>1bF#X ze81@LMfk%b?|Y-7<;gP&cz3#{6i4y8BBP;j=}o~5~PP!|4ifJ)4V?cX-)B($%E4k%l;Q$>^8<+?g#! ztl<>z&S|d~U?!)hf^A=Y3#@}0oL2cnVWZz`|IZ8i``z3aI3yIDp)58bzVOuRlM+M@ zQx5J68|H!jo+O-fS~;EDTSznaf{$9_GWDkFkcZooQRwsj51;sZHS2#Jz?$%5p_<6- zLQ~dk{)Wd_HLrL3nhXEC6HpEKC)Yv#z-g=E_a|S z!^Cjoqa+?sC5ReeGUaJW{PuUcP)Y+(00(LRHgbA7P;{oNT@={{LbX#oRGd&y1uIWy?TFm^cGCLrMX(h zXkS&;_jLU#j|ZJ)7mm989M|kDbxPlTLiIIOX(K(a_UEeg)vVoIT7uozcuKzQOjS+v zSIgAVr|v*$>q0^FGN$dioGOV|KIWF>`6D-*14UY;I|ZETkRW*nL#R z=%jKM21|oU+Xjb2Q6hncoc`WL*folZI(PowW*f`>KmU9b3EpUmoUonc%yH-s60xpr zROUFs22{__6t}%nNpqkf1)T;&it$U>fIVCZfDo#tTsm}RNXCrn!cJJd&VR1n!DKSr z(tKKAAgw^?1%3ENiSwjidUvE6;OUQu9P*ObjqWShmf!FBuSebqaN2JjGiXg$=Rqxc z8p9W%D3WmD{?3ch>|&17P({<8Z3iO5abKFfNVOsjT~>|mrus&{$}xIt++Aps>$&a$ z{W#Xxfsgt)KVPuBUZm|l3=C-2s7;RcU>f6M$QtX&iARH4Gwm)iy`nq-!H+D&_H;%^ zb<1wxZ&T288Q=yLy}j_KAI!p``+Mc4lVPcpN#RfW#nne%m}Kyx1p0#pCcYf*|7u)V zc2Uu4j5NOu+{gZ@%sF&+)=^yn%t|wrx1jZKq!$)!&H&_HR9)@x_3uGh8UCZ9%`SM~ z7g{>Ex(E>!J7AQ5xB}&QIdqmw&!*ikBUL)-I1~@SF;X^7wBcPUQQ^Yuj61s5JyB zqj{ZnJ5)5pYwt0z?~ID}BWq2R3ufCkZy3tFgY}&Cb4|MSv!K*UD&MYJPzVw-`Ps8l z5o@kjp_hItQ`e8{vwd5arYLNdrhLI8X`D;yBqKmjp;dHkVyca1om-^)5jefSOVa(P0}rWjrW1!(VVC5R#(Ju^u2E?m;l$3#Z*T$9YgS>cmBGUdk#0KaO{=)^VX&JH zTEM>QrP4TX%1*+6FHh7!`F>GHIW)*Oq2V&&fa3F0O$$eD>3Yp2%)h+!wTGp7>r!_8 z6BJRt0TA>A28Gn1#_HnQIxv8d4{KMiUI=jDSh$8xycc;+B6v0{tG! zJXfQXKb1NqLPPB4IBA-0?2#4;-PrTKD^^X7VSb7EU13qa5KXEA+#tZNxF^-(k#%RT z$%8D@IjC!09IB5DGO@%QKf6oW0+R7c2tr?r>$-Cd)`m8mXlb|+MNZJk2|BrX$=N{? z%3BFk-Q9!7c^Syh@PHMXk=?R|T=gh83X_kt|9ZfS)xkE1km8soE#PQ`__z($?L(op z{PqPn3>*+o(J+lcq2Ni0!_XVSNd z3CDz~ghn9rZQ}qH1ka7Ncc*b^mMcTe=`pu%>4NPt=s5RZ3Fj0vC~%#>7XR!SGZXMu z$JwklZ}K=hT=qHF7m9GV&A>to5X9rzKOcenuGeML=D&GJ|KQ{`4#QiSNzwr>YN1*t z7p6;J0bE4#p+&K5`q2#SqIxPcm1&nG{_f+`eo)Gbn;q{IND1IKsd@KvqMQAO>*&W5 z;o_c^w9LYah1WhYIV}LM=3RZHm^9hhuK6DWwO3f)H040H%F&Xf6d>9M3NuA$xW%Rr z$}NMcfss=VQ=lDJKN2jI5M&atZXY6lk%-Q`MVAG@aS1Tuc~mJ zr_@cx6Y|9~yarw;k-Sij6kSZtY%PN%aqwQ^9Y4M~AGTbFM!L zOH9W{WhrZAYbM8!#P}==uWYBWwg(#W=0tg)Fo62(UiZW+H3yM!o>ODCX}`s?VSci~ z8sVJ^X9Ks8)`RyOAgo?3q&lSuPd;&eShAVf{%Mtk8Q}G6UacVP%5RV<5BaUBtrnt zIZ7X>h_4C`y9m^qDDL82WLq`3JkeFKTCsiqsY^hFx%EUgC#kH_3!9}^5op%|0gi3F zWzP=r6@xqLa7@geYr5->F4sZ%()7a}#c{kB{(hJv3VHtuVGpKm*YKrer0RB{VvryL z4~#ILyQ_|jWS$1fM(ZqjzXSe_yRm5`xrQd z2Mj0QXIl7j>AHq;>jtK2nHInZ#(R@f-~f!l6C0MbzC~L-&rh+KHA5qDdBRG{k2bi{ zefdpKA2bhqcElJpLwVn-u7ALR^+t)^^3Ms?Or6u~j$K8zw&Ss%B5=NCz(3jf%+&l5 zf2{J!%KKjUYN8_oOMb1$O~r#cL=JX#DZvF}X?BP+EbHe}mziv5J{h%Ywk&FNI+u?P zxGcl+{@0x@qbU{#~hemLvq8l~@o zXQjk$j8FIFy3ARos%Pn)(e3E(LSyMwOC2rNb$~#_1_BdruEC@y3lN!@!0vt1*{lz4 z>0;Uw;?l0l|hVCn{d*!*zBC7lP#N4Fu6`5>-1ye7-6|LS(NKZb^S zk)U0q4DcMo1g`rmD^qqFF5{USKyZa&LqH`wXc<#4Y1F<)K&ur)V695PWLRug*mWp`D&mp$cF=kDGWo#utd z?OOuJ?K2n$RUoyL&z|EDlYth0<%qTPQ94WW>Z4^?PMA1b>NwuPN65i#ZAnvFoEZ`s z(3P#Yn$eSZ=mmI@chGgfs;0x#@yFkx*s3m2f{pk=R2f#f2DUkEJH#M%QLB}BGa2dxH^5i&ed*5XWH~snnZ3_a) zNibpJ%mjLpGK9a^D)a96w^bs(K#R0DThw7Nkb$TH3HZ8k2prKA#U#A38<(I#qBkA_ zdhuF|Hapqk&(&bji~&WphnD*a_jew7SxJ5V)nS)lKVHO-I@zxB{M6;r8xu_5TNChh zz-c=j&+*_u7jPE!<43!}Yrj-R6hA(W_yXHLJ?cFt!#+M0irJ$;RFvXglB_iUQkrn| z3GqX$p&*_1h=;6Igs{!y8xvjcfzo%t(E`q{G-v#PT8qTzrMWSfsIp#G7b(_GMQKSt z4aVdt%H#6St2f0-8@VVMtG^h9z<5svM`G*n?A(ZzR;;2DWO=*r5`A0s+~=^M=(jaq zUt4`MhJ6*prIr5(9Nl7r=?&~RHo7=7?LPJVDg+2e21N3p%xkPIwUxG|xp}2-$*>4= zQ?Wb&k&T5V7LeYc(iriid3M?T4OhfBnv07Zw{|_VudUC0@O)2WG>v&nw1A%#Il

    f@md6p3SRM@8L&e;~8irL4IX0dnAWFBIx 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

    Z|>RsK38qm5_F7@ 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 z%0D+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 Date: Thu, 27 Jun 2024 17:07:06 -0700 Subject: [PATCH 200/234] support/storage: Add GetLatestLedgerSequence method to Archive interface (#5362) --- .../ledgerexporter/internal/config.go | 6 +-- .../ledgerexporter/internal/config_test.go | 28 ++++++---- historyarchive/archive.go | 11 ++++ historyarchive/archive_pool.go | 10 ++++ historyarchive/mocks.go | 5 ++ support/datastore/history_archive.go | 53 ------------------- support/datastore/resumablemanager_test.go | 8 ++- support/datastore/resumeablemanager.go | 4 +- 8 files changed, 56 insertions(+), 69 deletions(-) delete mode 100644 support/datastore/history_archive.go diff --git a/exp/services/ledgerexporter/internal/config.go b/exp/services/ledgerexporter/internal/config.go index d5aad53256..013a3ef8d7 100644 --- a/exp/services/ledgerexporter/internal/config.go +++ b/exp/services/ledgerexporter/internal/config.go @@ -124,8 +124,8 @@ func (config *Config) ValidateAndSetLedgerRange(ctx context.Context, archive his return errors.New("invalid end value, must be greater than start") } - latestNetworkLedger, err := datastore.GetLatestLedgerSequenceFromHistoryArchives(archive) - latestNetworkLedger = latestNetworkLedger + (datastore.GetHistoryArchivesCheckPointFrequency() * 2) + latestNetworkLedger, err := archive.GetLatestLedgerSequence() + latestNetworkLedger = latestNetworkLedger + (archive.GetCheckpointManager().GetCheckpointFrequency() * 2) if err != nil { return errors.Wrap(err, "Failed to retrieve the latest ledger sequence from history archives.") @@ -189,7 +189,7 @@ func (config *Config) GenerateCaptiveCoreConfig(coreBinFromPath string) (ledgerb BinaryPath: config.StellarCoreConfig.StellarCoreBinaryPath, NetworkPassphrase: params.NetworkPassphrase, HistoryArchiveURLs: params.HistoryArchiveURLs, - CheckpointFrequency: datastore.GetHistoryArchivesCheckPointFrequency(), + CheckpointFrequency: historyarchive.DefaultCheckpointFrequency, Log: logger.WithField("subservice", "stellar-core"), Toml: captiveCoreToml, UserAgent: "ledger-exporter", diff --git a/exp/services/ledgerexporter/internal/config_test.go b/exp/services/ledgerexporter/internal/config_test.go index f782de5ea4..d1c24cb198 100644 --- a/exp/services/ledgerexporter/internal/config_test.go +++ b/exp/services/ledgerexporter/internal/config_test.go @@ -5,19 +5,20 @@ import ( "fmt" "testing" + "github.com/stellar/go/historyarchive" "github.com/stellar/go/network" - "github.com/stellar/go/support/datastore" "github.com/stretchr/testify/require" - - "github.com/stellar/go/historyarchive" ) func TestNewConfig(t *testing.T) { ctx := context.Background() mockArchive := &historyarchive.MockArchive{} - mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: 5}, nil).Once() + mockArchive.On("GetLatestLedgerSequence").Return(uint32(5), nil).Once() + mockArchive.On("GetCheckpointManager"). + Return(historyarchive.NewCheckpointManager( + historyarchive.DefaultCheckpointFrequency)).Once() config, err := NewConfig( RuntimeSettings{StartLedger: 2, EndLedger: 3, ConfigFilePath: "test/test.toml", Mode: Append}, nil) @@ -198,7 +199,7 @@ func TestInvalidCaptiveCoreTomlPath(t *testing.T) { func TestValidateStartAndEndLedger(t *testing.T) { latestNetworkLedger := uint32(20000) - latestNetworkLedgerPadding := datastore.GetHistoryArchivesCheckPointFrequency() * 2 + latestNetworkLedgerPadding := historyarchive.DefaultCheckpointFrequency * 2 tests := []struct { name string @@ -282,7 +283,10 @@ func TestValidateStartAndEndLedger(t *testing.T) { ctx := context.Background() mockArchive := &historyarchive.MockArchive{} - mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: latestNetworkLedger}, nil) + mockArchive.On("GetLatestLedgerSequence").Return(latestNetworkLedger, nil) + mockArchive.On("GetCheckpointManager"). + Return(historyarchive.NewCheckpointManager( + historyarchive.DefaultCheckpointFrequency)) mockedHasCtr := 0 for _, tt := range tests { @@ -302,7 +306,7 @@ func TestValidateStartAndEndLedger(t *testing.T) { } }) } - mockArchive.AssertNumberOfCalls(t, "GetRootHAS", mockedHasCtr) + mockArchive.AssertExpectations(t) } func TestAdjustedLedgerRangeBoundedMode(t *testing.T) { @@ -358,7 +362,10 @@ func TestAdjustedLedgerRangeBoundedMode(t *testing.T) { ctx := context.Background() mockArchive := &historyarchive.MockArchive{} - mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: 500}, nil).Times(len(tests)) + mockArchive.On("GetLatestLedgerSequence").Return(uint32(500), nil) + mockArchive.On("GetCheckpointManager"). + Return(historyarchive.NewCheckpointManager( + historyarchive.DefaultCheckpointFrequency)) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -421,7 +428,10 @@ func TestAdjustedLedgerRangeUnBoundedMode(t *testing.T) { ctx := context.Background() mockArchive := &historyarchive.MockArchive{} - mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: 500}, nil).Times(len(tests)) + mockArchive.On("GetLatestLedgerSequence").Return(uint32(500), nil) + mockArchive.On("GetCheckpointManager"). + Return(historyarchive.NewCheckpointManager( + historyarchive.DefaultCheckpointFrequency)) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/historyarchive/archive.go b/historyarchive/archive.go index 4f9e14380f..d97471b42f 100644 --- a/historyarchive/archive.go +++ b/historyarchive/archive.go @@ -71,6 +71,7 @@ type ArchiveInterface interface { GetLedgerHeader(chk uint32) (xdr.LedgerHeaderHistoryEntry, error) GetRootHAS() (HistoryArchiveState, error) GetLedgers(start, end uint32) (map[uint32]*Ledger, error) + GetLatestLedgerSequence() (uint32, error) GetCheckpointHAS(chk uint32) (HistoryArchiveState, error) PutCheckpointHAS(chk uint32, has HistoryArchiveState, opts *CommandOptions) error PutRootHAS(has HistoryArchiveState, opts *CommandOptions) error @@ -176,6 +177,16 @@ func (a *Archive) PutPathHAS(path string, has HistoryArchiveState, opts *Command return a.backend.PutFile(path, io.NopCloser(bytes.NewReader(buf))) } +func (a *Archive) GetLatestLedgerSequence() (uint32, error) { + has, err := a.GetRootHAS() + if err != nil { + log.Error("Error getting root HAS from archive", err) + return 0, errors.Wrap(err, "failed to retrieve the latest ledger sequence from history archive") + } + + return has.CurrentLedger, nil +} + func (a *Archive) BucketExists(bucket Hash) (bool, error) { return a.cachedExists(BucketPath(bucket)) } diff --git a/historyarchive/archive_pool.go b/historyarchive/archive_pool.go index 48178ade26..28967d8aa6 100644 --- a/historyarchive/archive_pool.go +++ b/historyarchive/archive_pool.go @@ -204,6 +204,16 @@ func (pa *ArchivePool) GetCheckpointHAS(chk uint32) (HistoryArchiveState, error) }) } +func (pa *ArchivePool) GetLatestLedgerSequence() (uint32, error) { + has, err := pa.GetRootHAS() + if err != nil { + log.Error("Error getting root HAS from archive", err) + return 0, errors.Wrap(err, "failed to retrieve the latest ledger sequence from history archive") + } + + return has.CurrentLedger, nil +} + func (pa *ArchivePool) PutCheckpointHAS(chk uint32, has HistoryArchiveState, opts *CommandOptions) error { return pa.runRoundRobin(func(ai ArchiveInterface) error { return ai.PutCheckpointHAS(chk, has, opts) diff --git a/historyarchive/mocks.go b/historyarchive/mocks.go index fa5716e5de..efe333cd33 100644 --- a/historyarchive/mocks.go +++ b/historyarchive/mocks.go @@ -10,6 +10,11 @@ type MockArchive struct { mock.Mock } +func (m *MockArchive) GetLatestLedgerSequence() (uint32, error) { + a := m.Called() + return a.Get(0).(uint32), a.Error(1) +} + func (m *MockArchive) GetCheckpointManager() CheckpointManager { a := m.Called() return a.Get(0).(CheckpointManager) diff --git a/support/datastore/history_archive.go b/support/datastore/history_archive.go deleted file mode 100644 index 9fd291bac7..0000000000 --- a/support/datastore/history_archive.go +++ /dev/null @@ -1,53 +0,0 @@ -package datastore - -import ( - "context" - - log "github.com/sirupsen/logrus" - - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/network" - "github.com/stellar/go/support/errors" - supportlog "github.com/stellar/go/support/log" - "github.com/stellar/go/support/storage" -) - -const ( - Pubnet = "pubnet" - Testnet = "testnet" -) - -func CreateHistoryArchiveFromNetworkName(ctx context.Context, networkName string, userAgent string, logger *supportlog.Entry) (historyarchive.ArchiveInterface, error) { - var historyArchiveUrls []string - switch networkName { - case Pubnet: - historyArchiveUrls = network.PublicNetworkhistoryArchiveURLs - case Testnet: - historyArchiveUrls = network.TestNetworkhistoryArchiveURLs - default: - return nil, errors.Errorf("Invalid network name %s", networkName) - } - - return historyarchive.NewArchivePool(historyArchiveUrls, historyarchive.ArchiveOptions{ - Logger: logger, - ConnectOptions: storage.ConnectOptions{ - UserAgent: userAgent, - Context: ctx, - }, - }) -} - -func GetLatestLedgerSequenceFromHistoryArchives(archive historyarchive.ArchiveInterface) (uint32, error) { - has, err := archive.GetRootHAS() - if err != nil { - log.Error("Error getting root HAS from archives", err) - return 0, errors.Wrap(err, "failed to retrieve the latest ledger sequence from any history archive") - } - - return has.CurrentLedger, nil -} - -func GetHistoryArchivesCheckPointFrequency() uint32 { - // this could evolve to use other sources for checkpoint freq - return historyarchive.DefaultCheckpointFrequency -} diff --git a/support/datastore/resumablemanager_test.go b/support/datastore/resumablemanager_test.go index 4616f9e4ae..4fc8738b08 100644 --- a/support/datastore/resumablemanager_test.go +++ b/support/datastore/resumablemanager_test.go @@ -282,8 +282,12 @@ func TestResumability(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { mockArchive := &historyarchive.MockArchive{} - mockArchive.On("GetRootHAS").Return(historyarchive.HistoryArchiveState{CurrentLedger: tt.latestLedger}, tt.archiveError).Once() - + mockArchive.On("GetLatestLedgerSequence").Return(tt.latestLedger, tt.archiveError).Once() + if tt.archiveError == nil { + mockArchive.On("GetCheckpointManager"). + Return(historyarchive.NewCheckpointManager( + historyarchive.DefaultCheckpointFrequency)).Once() + } mockDataStore := &MockDataStore{} tt.registerMockCalls(mockDataStore) diff --git a/support/datastore/resumeablemanager.go b/support/datastore/resumeablemanager.go index 35031d73f6..7e6b03df99 100644 --- a/support/datastore/resumeablemanager.go +++ b/support/datastore/resumeablemanager.go @@ -62,12 +62,12 @@ func (rm resumableManagerService) FindStart(ctx context.Context, start, end uint networkLatest := uint32(0) if end < 1 { var latestErr error - networkLatest, latestErr = GetLatestLedgerSequenceFromHistoryArchives(rm.archive) + networkLatest, latestErr = rm.archive.GetLatestLedgerSequence() if latestErr != nil { err := errors.Wrap(latestErr, "Resumability of requested export ledger range, was not able to get latest ledger from network") return 0, false, err } - networkLatest = networkLatest + (GetHistoryArchivesCheckPointFrequency() * 2) + networkLatest = networkLatest + (rm.archive.GetCheckpointManager().GetCheckpointFrequency() * 2) log.Infof("Resumability computed effective latest network ledger including padding of checkpoint frequency to be %d", networkLatest) if start > networkLatest { From 01dc1762d6be40d84829476ee154834d88490155 Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 28 Jun 2024 06:16:20 +0100 Subject: [PATCH 201/234] sevices/friendbot: Allow friendbot to include cloudflare derived IP address in request logs (#5359) --- services/friendbot/main.go | 17 ++++++++++--- services/friendbot/router_test.go | 33 +++++++++++++++++++++++++ support/http/logging_middleware.go | 2 ++ support/http/logging_middleware_test.go | 9 ++++--- support/http/mux.go | 1 + 5 files changed, 55 insertions(+), 7 deletions(-) create mode 100644 services/friendbot/router_test.go diff --git a/services/friendbot/main.go b/services/friendbot/main.go index 22a04b0bed..22e7d4c44d 100644 --- a/services/friendbot/main.go +++ b/services/friendbot/main.go @@ -8,6 +8,7 @@ import ( "github.com/go-chi/chi" "github.com/spf13/cobra" + "github.com/stellar/go/services/friendbot/internal" "github.com/stellar/go/support/app" "github.com/stellar/go/support/config" @@ -29,6 +30,7 @@ type Config struct { BaseFee int64 `toml:"base_fee" valid:"optional"` MinionBatchSize int `toml:"minion_batch_size" valid:"optional"` SubmitTxRetriesAllowed int `toml:"submit_tx_retries_allowed" valid:"optional"` + UseCloudflareIP bool `toml:"use_cloudflare_ip" valid:"optional"` } func main() { @@ -68,7 +70,7 @@ func run(cmd *cobra.Command, args []string) { log.Error(err) os.Exit(1) } - router := initRouter(fb) + router := initRouter(cfg, fb) registerProblems() addr := fmt.Sprintf("0.0.0.0:%d", cfg.Port) @@ -84,8 +86,8 @@ func run(cmd *cobra.Command, args []string) { }) } -func initRouter(fb *internal.Bot) *chi.Mux { - mux := http.NewAPIMux(log.DefaultLogger) +func initRouter(cfg Config, fb *internal.Bot) *chi.Mux { + mux := newMux(cfg) handler := &internal.FriendbotHandler{Friendbot: fb} mux.Get("/", handler.Handle) @@ -97,6 +99,15 @@ func initRouter(fb *internal.Bot) *chi.Mux { return mux } +func newMux(cfg Config) *chi.Mux { + mux := chi.NewRouter() + // first apply XFFMiddleware so we can have the real ip in the subsequent + // middlewares + mux.Use(http.XFFMiddleware(http.XFFMiddlewareConfig{BehindCloudflare: cfg.UseCloudflareIP})) + mux.Use(http.NewAPIMux(log.DefaultLogger).Middlewares()...) + return mux +} + func registerProblems() { problem.RegisterError(sql.ErrNoRows, problem.NotFound) diff --git a/services/friendbot/router_test.go b/services/friendbot/router_test.go new file mode 100644 index 0000000000..292a3253ca --- /dev/null +++ b/services/friendbot/router_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/stellar/go/support/log" +) + +func TestIPLogging(t *testing.T) { + done := log.DefaultLogger.StartTest(log.InfoLevel) + + mux := newMux(Config{UseCloudflareIP: true}) + mux.Get("/", func(w http.ResponseWriter, request *http.Request) { + w.WriteHeader(http.StatusOK) + }) + recorder := httptest.NewRecorder() + request := httptest.NewRequest("GET", "/", nil) + ipAddress := "255.128.255.128" + request.Header.Set("CF-Connecting-IP", ipAddress) + mux.ServeHTTP(recorder, request) + require.Equal(t, http.StatusOK, recorder.Code) + + logged := done() + require.Len(t, logged, 2) + require.Equal(t, "starting request", logged[0].Message) + require.Equal(t, ipAddress, logged[0].Data["ip"]) + require.Equal(t, "finished request", logged[1].Message) + require.Equal(t, ipAddress, logged[1].Data["ip"]) +} diff --git a/support/http/logging_middleware.go b/support/http/logging_middleware.go index 2cc957ac68..540dbd1243 100644 --- a/support/http/logging_middleware.go +++ b/support/http/logging_middleware.go @@ -8,6 +8,7 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" + "github.com/stellar/go/support/http/mutil" "github.com/stellar/go/support/log" ) @@ -136,6 +137,7 @@ func logEndOfRequest( "subsys": "http", "path": r.URL.String(), "method": r.Method, + "ip": r.RemoteAddr, "status": mw.Status(), "bytes": mw.BytesWritten(), "duration": duration, diff --git a/support/http/logging_middleware_test.go b/support/http/logging_middleware_test.go index 0e2eb45bb2..3ba4d651db 100644 --- a/support/http/logging_middleware_test.go +++ b/support/http/logging_middleware_test.go @@ -6,9 +6,10 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" + "github.com/stretchr/testify/assert" + "github.com/stellar/go/support/http/httptest" "github.com/stellar/go/support/log" - "github.com/stretchr/testify/assert" ) // setXFFMiddleware sets "X-Forwarded-For" header to test LoggingMiddlewareWithOptions. @@ -143,7 +144,7 @@ func TestHTTPMiddlewareWithOptions(t *testing.T) { assert.Equal(t, req1, logged[2].Data["req"]) assert.Equal(t, "/path/1234", logged[2].Data["path"]) assert.Equal(t, "/path/{value}", logged[2].Data["route"]) - assert.Equal(t, 9, len(logged[2].Data)) + assert.Equal(t, 10, len(logged[2].Data)) assert.Equal(t, "starting request", logged[3].Message) assert.Equal(t, "http", logged[3].Data["subsys"]) @@ -162,7 +163,7 @@ func TestHTTPMiddlewareWithOptions(t *testing.T) { assert.Equal(t, req2, logged[4].Data["req"]) assert.Equal(t, "/not_found", logged[4].Data["path"]) assert.Equal(t, "/not_found", logged[4].Data["route"]) - assert.Equal(t, 9, len(logged[4].Data)) + assert.Equal(t, 10, len(logged[4].Data)) assert.Equal(t, "starting request", logged[5].Message) assert.Equal(t, "http", logged[5].Data["subsys"]) @@ -181,7 +182,7 @@ func TestHTTPMiddlewareWithOptions(t *testing.T) { assert.Equal(t, req3, logged[6].Data["req"]) assert.Equal(t, "/really_not_found", logged[6].Data["path"]) assert.Equal(t, "", logged[6].Data["route"]) - assert.Equal(t, 9, len(logged[6].Data)) + assert.Equal(t, 10, len(logged[6].Data)) } } diff --git a/support/http/mux.go b/support/http/mux.go index ca041d3797..1d64f99812 100644 --- a/support/http/mux.go +++ b/support/http/mux.go @@ -4,6 +4,7 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/rs/cors" + "github.com/stellar/go/support/log" ) From b589529f102f0b92c106e1015a80531f52623036 Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 28 Jun 2024 07:30:57 +0100 Subject: [PATCH 202/234] ingest/ledgerbackend: Refactor captive core process manager (#5360) --- ingest/ledgerbackend/captive_core_backend.go | 34 +- .../captive_core_backend_test.go | 64 +- ingest/ledgerbackend/catchup.go | 76 +++ ingest/ledgerbackend/cmd.go | 157 +++++ ...llar_core_runner_posix.go => cmd_posix.go} | 11 +- ..._core_runner_windows.go => cmd_windows.go} | 4 +- ingest/ledgerbackend/core_log.go | 82 +++ ingest/ledgerbackend/dir.go | 109 ++++ ingest/ledgerbackend/main.go | 92 --- ingest/ledgerbackend/mock_cmd_test.go | 32 +- .../ledgerbackend/mock_system_caller_test.go | 5 +- ingest/ledgerbackend/run_from.go | 140 +++++ ingest/ledgerbackend/stellar_core_runner.go | 565 +++--------------- .../ledgerbackend/stellar_core_runner_test.go | 16 +- 14 files changed, 738 insertions(+), 649 deletions(-) create mode 100644 ingest/ledgerbackend/catchup.go create mode 100644 ingest/ledgerbackend/cmd.go rename ingest/ledgerbackend/{stellar_core_runner_posix.go => cmd_posix.go} (75%) rename ingest/ledgerbackend/{stellar_core_runner_windows.go => cmd_windows.go} (85%) create mode 100644 ingest/ledgerbackend/core_log.go create mode 100644 ingest/ledgerbackend/dir.go delete mode 100644 ingest/ledgerbackend/main.go create mode 100644 ingest/ledgerbackend/run_from.go diff --git a/ingest/ledgerbackend/captive_core_backend.go b/ingest/ledgerbackend/captive_core_backend.go index 9d1f7f7fc0..c8f28974f5 100644 --- a/ingest/ledgerbackend/captive_core_backend.go +++ b/ingest/ledgerbackend/captive_core_backend.go @@ -349,11 +349,11 @@ func (c *CaptiveStellarCore) openOfflineReplaySubprocess(from, to uint32) error ) } - c.stellarCoreRunner = c.stellarCoreRunnerFactory() - err = c.stellarCoreRunner.catchup(from, to) - if err != nil { + stellarCoreRunner := c.stellarCoreRunnerFactory() + if err = stellarCoreRunner.catchup(from, to); err != nil { return errors.Wrap(err, "error running stellar-core") } + c.stellarCoreRunner = stellarCoreRunner // The next ledger should be the first ledger of the checkpoint containing // the requested ledger @@ -375,11 +375,11 @@ func (c *CaptiveStellarCore) openOnlineReplaySubprocess(ctx context.Context, fro return errors.Wrap(err, "error calculating ledger and hash for stellar-core run") } - c.stellarCoreRunner = c.stellarCoreRunnerFactory() - err = c.stellarCoreRunner.runFrom(runFrom, ledgerHash) - if err != nil { + stellarCoreRunner := c.stellarCoreRunnerFactory() + if err = stellarCoreRunner.runFrom(runFrom, ledgerHash); err != nil { return errors.Wrap(err, "error running stellar-core") } + c.stellarCoreRunner = stellarCoreRunner // In the online mode we update nextLedger after streaming the first ledger. // This is to support versions before and after/including v17.1.0 that @@ -556,7 +556,7 @@ func (c *CaptiveStellarCore) isPrepared(ledgerRange Range) bool { return false } - if exited, _ := c.stellarCoreRunner.getProcessExitError(); exited { + if _, exited := c.stellarCoreRunner.getProcessExitError(); exited { return false } @@ -627,9 +627,6 @@ func (c *CaptiveStellarCore) GetLedger(ctx context.Context, sequence uint32) (xd if c.stellarCoreRunner == nil { return xdr.LedgerCloseMeta{}, errors.New("stellar-core cannot be nil, call PrepareRange first") } - if c.closed { - return xdr.LedgerCloseMeta{}, errors.New("stellar-core has an error, call PrepareRange first") - } if sequence < c.nextExpectedSequence() { return xdr.LedgerCloseMeta{}, errors.Errorf( @@ -647,12 +644,17 @@ func (c *CaptiveStellarCore) GetLedger(ctx context.Context, sequence uint32) (xd ) } + ch, ok := c.stellarCoreRunner.getMetaPipe() + if !ok { + return xdr.LedgerCloseMeta{}, errors.New("stellar-core is not running, call PrepareRange first") + } + // Now loop along the range until we find the ledger we want. for { select { case <-ctx.Done(): return xdr.LedgerCloseMeta{}, ctx.Err() - case result, ok := <-c.stellarCoreRunner.getMetaPipe(): + case result, ok := <-ch: found, ledger, err := c.handleMetaPipeResult(sequence, result, ok) if found || err != nil { return ledger, err @@ -732,7 +734,7 @@ func (c *CaptiveStellarCore) checkMetaPipeResult(result metaResult, ok bool) err return err } if !ok || result.err != nil { - exited, err := c.stellarCoreRunner.getProcessExitError() + err, exited := c.stellarCoreRunner.getProcessExitError() if exited && err != nil { // Case 2 - The stellar core process exited unexpectedly with an error message return errors.Wrap(err, "stellar core exited unexpectedly") @@ -775,12 +777,12 @@ func (c *CaptiveStellarCore) GetLatestLedgerSequence(ctx context.Context) (uint3 if c.stellarCoreRunner == nil { return 0, errors.New("stellar-core cannot be nil, call PrepareRange first") } - if c.closed { - return 0, errors.New("stellar-core is closed, call PrepareRange first") - + ch, ok := c.stellarCoreRunner.getMetaPipe() + if !ok { + return 0, errors.New("stellar-core is not running, call PrepareRange first") } if c.lastLedger == nil { - return c.nextExpectedSequence() - 1 + uint32(len(c.stellarCoreRunner.getMetaPipe())), nil + return c.nextExpectedSequence() - 1 + uint32(len(ch)), nil } return *c.lastLedger, nil } diff --git a/ingest/ledgerbackend/captive_core_backend_test.go b/ingest/ledgerbackend/captive_core_backend_test.go index 76319c2f77..f8161aec25 100644 --- a/ingest/ledgerbackend/captive_core_backend_test.go +++ b/ingest/ledgerbackend/captive_core_backend_test.go @@ -42,14 +42,14 @@ func (m *stellarCoreRunnerMock) runFrom(from uint32, hash string) error { return a.Error(0) } -func (m *stellarCoreRunnerMock) getMetaPipe() <-chan metaResult { +func (m *stellarCoreRunnerMock) getMetaPipe() (<-chan metaResult, bool) { a := m.Called() - return a.Get(0).(<-chan metaResult) + return a.Get(0).(<-chan metaResult), a.Bool(1) } -func (m *stellarCoreRunnerMock) getProcessExitError() (bool, error) { +func (m *stellarCoreRunnerMock) getProcessExitError() (error, bool) { a := m.Called() - return a.Bool(0), a.Error(1) + return a.Error(0), a.Bool(1) } func (m *stellarCoreRunnerMock) close() error { @@ -213,7 +213,7 @@ func TestCaptivePrepareRange(t *testing.T) { ctx := context.Background() mockRunner := &stellarCoreRunnerMock{} mockRunner.On("catchup", uint32(100), uint32(200)).Return(nil).Once() - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) mockArchive := &historyarchive.MockArchive{} @@ -251,8 +251,8 @@ func TestCaptivePrepareRangeCrash(t *testing.T) { ctx := context.Background() mockRunner := &stellarCoreRunnerMock{} mockRunner.On("catchup", uint32(100), uint32(200)).Return(nil).Once() - mockRunner.On("getProcessExitError").Return(true, errors.New("exit code -1")) - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getProcessExitError").Return(errors.New("exit code -1"), true) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("close").Return(nil).Once() mockRunner.On("context").Return(ctx) @@ -292,7 +292,7 @@ func TestCaptivePrepareRangeTerminated(t *testing.T) { ctx := context.Background() mockRunner := &stellarCoreRunnerMock{} mockRunner.On("catchup", uint32(100), uint32(200)).Return(nil).Once() - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) mockArchive := &historyarchive.MockArchive{} @@ -328,7 +328,7 @@ func TestCaptivePrepareRangeCloseNotFullyTerminated(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) mockRunner := &stellarCoreRunnerMock{} mockRunner.On("catchup", uint32(100), uint32(200)).Return(nil).Twice() - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) mockRunner.On("close").Return(nil) @@ -364,7 +364,7 @@ func TestCaptivePrepareRange_ErrClosingSession(t *testing.T) { ctx := context.Background() mockRunner := &stellarCoreRunnerMock{} mockRunner.On("close").Return(fmt.Errorf("transient error")) - mockRunner.On("getProcessExitError").Return(false, nil) + mockRunner.On("getProcessExitError").Return(nil, false) mockRunner.On("context").Return(ctx) captiveBackend := CaptiveStellarCore{ @@ -440,7 +440,7 @@ func TestCaptivePrepareRange_FromIsAheadOfRootHAS(t *testing.T) { } mockRunner.On("runFrom", uint32(63), "0000000000000000000000000000000000000000000000000000000000000000").Return(nil).Once() - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) assert.NoError(t, captiveBackend.PrepareRange(ctx, UnboundedRange(100))) @@ -481,7 +481,7 @@ func TestCaptivePrepareRangeWithDB_FromIsAheadOfRootHAS(t *testing.T) { LedgerCloseMeta: &meta, } mockRunner.On("runFrom", uint32(99), "").Return(nil).Once() - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) assert.NoError(t, captiveBackend.PrepareRange(ctx, UnboundedRange(100))) @@ -517,7 +517,6 @@ func TestCaptivePrepareRange_ToIsAheadOfRootHAS(t *testing.T) { func TestCaptivePrepareRange_ErrCatchup(t *testing.T) { mockRunner := &stellarCoreRunnerMock{} mockRunner.On("catchup", uint32(100), uint32(192)).Return(errors.New("transient error")).Once() - mockRunner.On("close").Return(nil).Once() mockArchive := &historyarchive.MockArchive{} mockArchive. @@ -552,7 +551,6 @@ func TestCaptivePrepareRange_ErrCatchup(t *testing.T) { func TestCaptivePrepareRangeUnboundedRange_ErrRunFrom(t *testing.T) { mockRunner := &stellarCoreRunnerMock{} mockRunner.On("runFrom", uint32(126), "0000000000000000000000000000000000000000000000000000000000000000").Return(errors.New("transient error")).Once() - mockRunner.On("close").Return(nil).Once() mockArchive := &historyarchive.MockArchive{} mockArchive. @@ -604,9 +602,9 @@ func TestCaptivePrepareRangeUnboundedRange_ReuseSession(t *testing.T) { ctx := context.Background() mockRunner := &stellarCoreRunnerMock{} mockRunner.On("runFrom", uint32(64), "0000000000000000000000000000000000000000000000000000000000000000").Return(nil).Once() - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) - mockRunner.On("getProcessExitError").Return(false, nil) + mockRunner.On("getProcessExitError").Return(nil, false) mockArchive := &historyarchive.MockArchive{} mockArchive. @@ -653,7 +651,7 @@ func TestGetLatestLedgerSequence(t *testing.T) { ctx := context.Background() mockRunner := &stellarCoreRunnerMock{} mockRunner.On("runFrom", uint32(63), "0000000000000000000000000000000000000000000000000000000000000000").Return(nil).Once() - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) mockArchive := &historyarchive.MockArchive{} @@ -699,7 +697,7 @@ func TestGetLatestLedgerSequenceRaceCondition(t *testing.T) { } ctx, cancel := context.WithCancel(context.Background()) mockRunner := &stellarCoreRunnerMock{} - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) mockRunner.On("runFrom", mock.Anything, mock.Anything).Return(nil) @@ -766,9 +764,9 @@ func TestCaptiveGetLedger(t *testing.T) { ctx, cancel := context.WithCancel(ctx) mockRunner := &stellarCoreRunnerMock{} mockRunner.On("catchup", uint32(65), uint32(66)).Return(nil) - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) - mockRunner.On("getProcessExitError").Return(false, nil) + mockRunner.On("getProcessExitError").Return(nil, false) mockArchive := &historyarchive.MockArchive{} mockArchive. @@ -857,7 +855,7 @@ func TestCaptiveGetLedgerCacheLatestLedger(t *testing.T) { defer cancel() mockRunner := &stellarCoreRunnerMock{} mockRunner.On("runFrom", uint32(65), "0101010100000000000000000000000000000000000000000000000000000000").Return(nil).Once() - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) mockArchive := &historyarchive.MockArchive{} @@ -919,7 +917,7 @@ func TestCaptiveGetLedger_NextLedgerIsDifferentToLedgerFromBuffer(t *testing.T) ctx := context.Background() mockRunner := &stellarCoreRunnerMock{} mockRunner.On("catchup", uint32(65), uint32(66)).Return(nil) - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) mockRunner.On("close").Return(nil) @@ -965,7 +963,7 @@ func TestCaptiveGetLedger_NextLedger0RangeFromIsSmallerThanLedgerFromBuffer(t *t ctx := context.Background() mockRunner := &stellarCoreRunnerMock{} mockRunner.On("runFrom", uint32(64), mock.Anything).Return(nil) - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) mockRunner.On("close").Return(nil) @@ -1067,13 +1065,13 @@ func TestCaptiveGetLedger_ErrReadingMetaResult(t *testing.T) { ctx := context.Background() mockRunner := &stellarCoreRunnerMock{} mockRunner.On("catchup", uint32(65), uint32(66)).Return(nil) - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) ctx, cancel := context.WithCancel(ctx) mockRunner.On("context").Return(ctx) mockRunner.On("close").Return(nil).Run(func(args mock.Arguments) { cancel() }).Once() - mockRunner.On("getProcessExitError").Return(false, nil) + mockRunner.On("getProcessExitError").Return(nil, false) // even if the request to fetch the latest checkpoint succeeds, we should fail at creating the subprocess mockArchive := &historyarchive.MockArchive{} @@ -1125,7 +1123,7 @@ func TestCaptiveGetLedger_ErrClosingAfterLastLedger(t *testing.T) { ctx := context.Background() mockRunner := &stellarCoreRunnerMock{} mockRunner.On("catchup", uint32(65), uint32(66)).Return(nil) - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) mockRunner.On("close").Return(fmt.Errorf("transient error")).Once() @@ -1167,7 +1165,7 @@ func TestCaptiveAfterClose(t *testing.T) { mockRunner := &stellarCoreRunnerMock{} ctx, cancel := context.WithCancel(context.Background()) mockRunner.On("catchup", uint32(65), uint32(66)).Return(nil) - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) mockRunner.On("close").Return(nil).Once() @@ -1222,7 +1220,7 @@ func TestGetLedgerBoundsCheck(t *testing.T) { ctx := context.Background() mockRunner := &stellarCoreRunnerMock{} mockRunner.On("catchup", uint32(128), uint32(130)).Return(nil).Once() - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) mockArchive := &historyarchive.MockArchive{} @@ -1346,9 +1344,9 @@ func TestCaptiveGetLedgerTerminatedUnexpectedly(t *testing.T) { ctx := testCase.ctx mockRunner := &stellarCoreRunnerMock{} mockRunner.On("catchup", uint32(64), uint32(100)).Return(nil).Once() - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) - mockRunner.On("getProcessExitError").Return(testCase.processExited, testCase.processExitedError) + mockRunner.On("getProcessExitError").Return(testCase.processExitedError, testCase.processExited) mockRunner.On("close").Return(nil).Once() mockArchive := &historyarchive.MockArchive{} @@ -1514,7 +1512,7 @@ func TestCaptiveRunFromParams(t *testing.T) { func TestCaptiveIsPrepared(t *testing.T) { mockRunner := &stellarCoreRunnerMock{} mockRunner.On("context").Return(context.Background()).Maybe() - mockRunner.On("getProcessExitError").Return(false, nil) + mockRunner.On("getProcessExitError").Return(nil, false) // c.prepared == nil captiveBackend := CaptiveStellarCore{ @@ -1578,7 +1576,7 @@ func TestCaptiveIsPreparedCoreContextCancelled(t *testing.T) { mockRunner := &stellarCoreRunnerMock{} ctx, cancel := context.WithCancel(context.Background()) mockRunner.On("context").Return(ctx).Maybe() - mockRunner.On("getProcessExitError").Return(false, nil) + mockRunner.On("getProcessExitError").Return(nil, false) rang := UnboundedRange(100) captiveBackend := CaptiveStellarCore{ @@ -1630,7 +1628,7 @@ func TestCaptivePreviousLedgerCheck(t *testing.T) { ctx := context.Background() mockRunner := &stellarCoreRunnerMock{} mockRunner.On("runFrom", uint32(254), "0101010100000000000000000000000000000000000000000000000000000000").Return(nil).Once() - mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan)) + mockRunner.On("getMetaPipe").Return((<-chan metaResult)(metaChan), true) mockRunner.On("context").Return(ctx) mockRunner.On("close").Return(nil).Once() diff --git a/ingest/ledgerbackend/catchup.go b/ingest/ledgerbackend/catchup.go new file mode 100644 index 0000000000..2cd12df0f3 --- /dev/null +++ b/ingest/ledgerbackend/catchup.go @@ -0,0 +1,76 @@ +package ledgerbackend + +import ( + "context" + "fmt" + + "github.com/stellar/go/support/log" +) + +type catchupStream struct { + dir workingDir + from uint32 + to uint32 + coreCmdFactory coreCmdFactory + log *log.Entry + useDB bool +} + +func newCatchupStream(r *stellarCoreRunner, from, to uint32) catchupStream { + // We want to use ephemeral directories in running the catchup command + // (used for the reingestion use case) because it's possible to run parallel + // reingestion where multiple captive cores are active on the same machine. + // Having ephemeral directories will ensure that each ingestion worker will + // have a separate working directory + dir := newWorkingDir(r, true) + return catchupStream{ + dir: dir, + from: from, + to: to, + coreCmdFactory: newCoreCmdFactory(r, dir), + log: r.log, + useDB: r.useDB, + } +} + +func (s catchupStream) getWorkingDir() workingDir { + return s.dir +} + +func (s catchupStream) start(ctx context.Context) (cmdI, pipe, error) { + var err error + var cmd cmdI + var captiveCorePipe pipe + + rangeArg := fmt.Sprintf("%d/%d", s.to, s.to-s.from+1) + params := []string{"catchup", rangeArg, "--metadata-output-stream", s.coreCmdFactory.getPipeName()} + + // horizon operator has specified to use external storage for captive core ledger state + // instruct captive core invocation to not use memory, and in that case + // cc will look at DATABASE property in cfg toml for the external storage source to use. + // when using external storage of ledgers, use new-db to first set the state of + // remote db storage to genesis to purge any prior state and reset. + if s.useDB { + cmd, err = s.coreCmdFactory.newCmd(ctx, stellarCoreRunnerModeOffline, true, "new-db") + if err != nil { + return nil, pipe{}, fmt.Errorf("error creating command: %w", err) + } + if err = cmd.Run(); err != nil { + return nil, pipe{}, fmt.Errorf("error initializing core db: %w", err) + } + } else { + params = append(params, "--in-memory") + } + + cmd, err = s.coreCmdFactory.newCmd(ctx, stellarCoreRunnerModeOffline, true, params...) + if err != nil { + return nil, pipe{}, fmt.Errorf("error creating command: %w", err) + } + + captiveCorePipe, err = s.coreCmdFactory.startCaptiveCore(cmd) + if err != nil { + return nil, pipe{}, fmt.Errorf("error starting `stellar-core run` subprocess: %w", err) + } + + return cmd, captiveCorePipe, nil +} diff --git a/ingest/ledgerbackend/cmd.go b/ingest/ledgerbackend/cmd.go new file mode 100644 index 0000000000..8af729f0a6 --- /dev/null +++ b/ingest/ledgerbackend/cmd.go @@ -0,0 +1,157 @@ +package ledgerbackend + +import ( + "context" + "fmt" + "io/fs" + "io/ioutil" + "math/rand" + "os" + "os/exec" + "time" + + "github.com/stellar/go/support/log" +) + +type isDir interface { + IsDir() bool +} + +type systemCaller interface { + removeAll(path string) error + writeFile(filename string, data []byte, perm fs.FileMode) error + mkdirAll(path string, perm os.FileMode) error + stat(name string) (isDir, error) + command(ctx context.Context, name string, arg ...string) cmdI +} + +type realSystemCaller struct{} + +func (realSystemCaller) removeAll(path string) error { + return os.RemoveAll(path) +} + +func (realSystemCaller) writeFile(filename string, data []byte, perm fs.FileMode) error { + return ioutil.WriteFile(filename, data, perm) +} + +func (realSystemCaller) mkdirAll(path string, perm os.FileMode) error { + return os.MkdirAll(path, perm) +} + +func (realSystemCaller) stat(name string) (isDir, error) { + return os.Stat(name) +} + +func (realSystemCaller) command(ctx context.Context, name string, arg ...string) cmdI { + cmd := exec.CommandContext(ctx, name, arg...) + cmd.Cancel = func() error { + return cmd.Process.Signal(os.Interrupt) + } + cmd.WaitDelay = time.Second * 10 + return &realCmd{Cmd: cmd} +} + +type cmdI interface { + Output() ([]byte, error) + Wait() error + Start() error + Run() error + setDir(dir string) + setLogLineWriter(logWriter *logLineWriter) + setExtraFiles([]*os.File) +} + +type realCmd struct { + *exec.Cmd + logWriter *logLineWriter +} + +func (r *realCmd) setDir(dir string) { + r.Cmd.Dir = dir +} + +func (r *realCmd) setLogLineWriter(logWriter *logLineWriter) { + r.logWriter = logWriter +} + +func (r *realCmd) setExtraFiles(extraFiles []*os.File) { + r.ExtraFiles = extraFiles +} + +func (r *realCmd) Start() error { + if r.logWriter != nil { + r.Cmd.Stdout = r.logWriter + r.Cmd.Stderr = r.logWriter + r.logWriter.Start() + } + err := r.Cmd.Start() + if err != nil && r.logWriter != nil { + r.logWriter.Close() + } + return err +} + +func (r *realCmd) Run() error { + if r.logWriter != nil { + r.Cmd.Stdout = r.logWriter + r.Cmd.Stderr = r.logWriter + r.logWriter.Start() + } + err := r.Cmd.Run() + if r.logWriter != nil { + r.logWriter.Close() + } + return err +} + +func (r *realCmd) Wait() error { + err := r.Cmd.Wait() + if r.logWriter != nil { + r.logWriter.Close() + } + return err +} + +type coreCmdFactory struct { + log *log.Entry + systemCaller systemCaller + executablePath string + dir workingDir + nonce string +} + +func newCoreCmdFactory(r *stellarCoreRunner, dir workingDir) coreCmdFactory { + return coreCmdFactory{ + log: r.log, + systemCaller: r.systemCaller, + executablePath: r.executablePath, + dir: dir, + nonce: fmt.Sprintf( + "captive-stellar-core-%x", + rand.New(rand.NewSource(time.Now().UnixNano())).Uint64(), + ), + } +} + +func (c coreCmdFactory) newCmd(ctx context.Context, mode stellarCoreRunnerMode, redirectOutputToLogs bool, params ...string) (cmdI, error) { + if err := c.dir.createIfNotExists(); err != nil { + return nil, err + } + + if err := c.dir.writeConf(mode); err != nil { + return nil, fmt.Errorf("error writing configuration: %w", err) + } + + allParams := []string{"--conf", c.dir.getConfFileName()} + if redirectOutputToLogs { + allParams = append(allParams, "--console") + } + allParams = append(allParams, params...) + cmd := c.systemCaller.command(ctx, c.executablePath, allParams...) + cmd.setDir(c.dir.path) + if redirectOutputToLogs { + cmd.setLogLineWriter(newLogLineWriter(c.log)) + } + return cmd, nil +} diff --git a/ingest/ledgerbackend/stellar_core_runner_posix.go b/ingest/ledgerbackend/cmd_posix.go similarity index 75% rename from ingest/ledgerbackend/stellar_core_runner_posix.go rename to ingest/ledgerbackend/cmd_posix.go index 6f34a49a75..c36dc208ee 100644 --- a/ingest/ledgerbackend/stellar_core_runner_posix.go +++ b/ingest/ledgerbackend/cmd_posix.go @@ -4,26 +4,25 @@ package ledgerbackend import ( + "fmt" "os" - - "github.com/pkg/errors" ) // Posix-specific methods for the StellarCoreRunner type. -func (c *stellarCoreRunner) getPipeName() string { +func (c coreCmdFactory) getPipeName() string { // The exec.Cmd.ExtraFiles field carries *io.File values that are assigned // to child process fds counting from 3, and we'll be passing exactly one // fd: the write end of the anonymous pipe below. return "fd:3" } -func (c *stellarCoreRunner) start(cmd cmdI) (pipe, error) { +func (c coreCmdFactory) startCaptiveCore(cmd cmdI) (pipe, error) { // First make an anonymous pipe. // Note io.File objects close-on-finalization. readFile, writeFile, err := os.Pipe() if err != nil { - return pipe{}, errors.Wrap(err, "error making a pipe") + return pipe{}, fmt.Errorf("error making a pipe: %w", err) } p := pipe{Reader: readFile, File: writeFile} @@ -34,7 +33,7 @@ func (c *stellarCoreRunner) start(cmd cmdI) (pipe, error) { if err != nil { writeFile.Close() readFile.Close() - return pipe{}, errors.Wrap(err, "error starting stellar-core") + return pipe{}, fmt.Errorf("error starting stellar-core: %w", err) } return p, nil diff --git a/ingest/ledgerbackend/stellar_core_runner_windows.go b/ingest/ledgerbackend/cmd_windows.go similarity index 85% rename from ingest/ledgerbackend/stellar_core_runner_windows.go rename to ingest/ledgerbackend/cmd_windows.go index 47368a55b6..6eb6e4d0d2 100644 --- a/ingest/ledgerbackend/stellar_core_runner_windows.go +++ b/ingest/ledgerbackend/cmd_windows.go @@ -11,11 +11,11 @@ import ( // Windows-specific methods for the stellarCoreRunner type. -func (c *stellarCoreRunner) getPipeName() string { +func (c coreCmdFactory) getPipeName() string { return fmt.Sprintf(`\\.\pipe\%s`, c.nonce) } -func (c *stellarCoreRunner) start(cmd cmdI) (pipe, error) { +func (c coreCmdFactory) startCaptiveCore(cmd cmdI) (pipe, error) { // First set up the server pipe. listener, err := winio.ListenPipe(c.getPipeName(), nil) if err != nil { diff --git a/ingest/ledgerbackend/core_log.go b/ingest/ledgerbackend/core_log.go new file mode 100644 index 0000000000..438bc136b0 --- /dev/null +++ b/ingest/ledgerbackend/core_log.go @@ -0,0 +1,82 @@ +package ledgerbackend + +import ( + "bufio" + "io" + "regexp" + "strings" + "sync" + + "github.com/stellar/go/support/log" +) + +type logLineWriter struct { + pipeReader *io.PipeReader + pipeWriter *io.PipeWriter + wg sync.WaitGroup + log *log.Entry +} + +func newLogLineWriter(log *log.Entry) *logLineWriter { + rd, wr := io.Pipe() + return &logLineWriter{ + pipeReader: rd, + pipeWriter: wr, + log: log, + } +} + +func (l *logLineWriter) Write(p []byte) (n int, err error) { + return l.pipeWriter.Write(p) +} + +func (l *logLineWriter) Close() error { + err := l.pipeWriter.Close() + l.wg.Wait() + return err +} + +func (l *logLineWriter) Start() { + br := bufio.NewReader(l.pipeReader) + l.wg.Add(1) + go func() { + defer l.wg.Done() + dateRx := regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3} `) + levelRx := regexp.MustCompile(`\[(\w+) ([A-Z]+)\] (.*)`) + for { + line, err := br.ReadString('\n') + if err != nil { + break + } + line = dateRx.ReplaceAllString(line, "") + line = strings.TrimSpace(line) + + if line == "" { + continue + } + + matches := levelRx.FindStringSubmatch(line) + if len(matches) >= 4 { + // Extract the substrings from the log entry and trim it + category, level := matches[1], matches[2] + line = matches[3] + + levelMapping := map[string]func(string, ...interface{}){ + "FATAL": l.log.Errorf, + "ERROR": l.log.Errorf, + "WARNING": l.log.Warnf, + "INFO": l.log.Infof, + "DEBUG": l.log.Debugf, + } + + writer := l.log.Infof + if f, ok := levelMapping[strings.ToUpper(level)]; ok { + writer = f + } + writer("%s: %s", category, line) + } else { + l.log.Info(line) + } + } + }() +} diff --git a/ingest/ledgerbackend/dir.go b/ingest/ledgerbackend/dir.go new file mode 100644 index 0000000000..d26835936c --- /dev/null +++ b/ingest/ledgerbackend/dir.go @@ -0,0 +1,109 @@ +package ledgerbackend + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/stellar/go/support/log" +) + +type workingDir struct { + ephemeral bool + path string + log *log.Entry + toml *CaptiveCoreToml + systemCaller systemCaller +} + +func newWorkingDir(r *stellarCoreRunner, ephemeral bool) workingDir { + var path string + if ephemeral { + path = filepath.Join(r.storagePath, "captive-core-"+createRandomHexString(8)) + } else { + path = filepath.Join(r.storagePath, "captive-core") + } + return workingDir{ + ephemeral: ephemeral, + path: path, + log: r.log, + toml: r.toml, + systemCaller: r.systemCaller, + } +} + +func (w workingDir) createIfNotExists() error { + info, err := w.systemCaller.stat(w.path) + if os.IsNotExist(err) { + innerErr := w.systemCaller.mkdirAll(w.path, os.FileMode(int(0755))) // rwx|rx|rx + if innerErr != nil { + return fmt.Errorf("failed to create storage directory (%s): %w", w.path, innerErr) + } + } else if !info.IsDir() { + return fmt.Errorf("%s is not a directory", w.path) + } else if err != nil { + return fmt.Errorf("error accessing storage directory (%s): %w", w.path, err) + } + + return nil +} + +func (w workingDir) writeConf(mode stellarCoreRunnerMode) error { + text, err := generateConfig(w.toml, mode) + if err != nil { + return err + } + + w.log.Debugf("captive core config file contents:\n%s", string(text)) + return w.systemCaller.writeFile(w.getConfFileName(), text, 0644) +} + +func (w workingDir) cleanup(coreExitError error) error { + if w.ephemeral || (coreExitError != nil && !errors.Is(coreExitError, context.Canceled)) { + return w.remove() + } + return nil +} + +func (w workingDir) remove() error { + return w.systemCaller.removeAll(w.path) +} + +func generateConfig(captiveCoreToml *CaptiveCoreToml, mode stellarCoreRunnerMode) ([]byte, error) { + if mode == stellarCoreRunnerModeOffline { + var err error + captiveCoreToml, err = captiveCoreToml.CatchupToml() + if err != nil { + return nil, fmt.Errorf("could not generate catch up config: %w", err) + } + } + + if !captiveCoreToml.QuorumSetIsConfigured() { + return nil, fmt.Errorf("captive-core config file does not define any quorum set") + } + + text, err := captiveCoreToml.Marshal() + if err != nil { + return nil, fmt.Errorf("could not marshal captive core config: %w", err) + } + return text, nil +} + +func (w workingDir) getConfFileName() string { + joinedPath := filepath.Join(w.path, "stellar-core.conf") + + // Given that `storagePath` can be anything, we need the full, absolute path + // here so that everything Core needs is created under the storagePath + // subdirectory. + // + // If the path *can't* be absolutely resolved (bizarre), we can still try + // recovering by using the path the user specified directly. + path, err := filepath.Abs(joinedPath) + if err != nil { + w.log.Warnf("Failed to resolve %s as an absolute path: %s", joinedPath, err) + return joinedPath + } + return path +} diff --git a/ingest/ledgerbackend/main.go b/ingest/ledgerbackend/main.go deleted file mode 100644 index 6029b8a1f6..0000000000 --- a/ingest/ledgerbackend/main.go +++ /dev/null @@ -1,92 +0,0 @@ -package ledgerbackend - -import ( - "io/fs" - "io/ioutil" - "os" - "os/exec" -) - -type isDir interface { - IsDir() bool -} - -type systemCaller interface { - removeAll(path string) error - writeFile(filename string, data []byte, perm fs.FileMode) error - mkdirAll(path string, perm os.FileMode) error - stat(name string) (isDir, error) - command(name string, arg ...string) cmdI -} - -type realSystemCaller struct{} - -func (realSystemCaller) removeAll(path string) error { - return os.RemoveAll(path) -} - -func (realSystemCaller) writeFile(filename string, data []byte, perm fs.FileMode) error { - return ioutil.WriteFile(filename, data, perm) -} - -func (realSystemCaller) mkdirAll(path string, perm os.FileMode) error { - return os.MkdirAll(path, perm) -} - -func (realSystemCaller) stat(name string) (isDir, error) { - return os.Stat(name) -} - -func (realSystemCaller) command(name string, arg ...string) cmdI { - cmd := exec.Command(name, arg...) - return &realCmd{Cmd: cmd} -} - -type cmdI interface { - Output() ([]byte, error) - Wait() error - Start() error - Run() error - setDir(dir string) - setStdout(stdout *logLineWriter) - getStdout() *logLineWriter - setStderr(stderr *logLineWriter) - getStderr() *logLineWriter - getProcess() *os.Process - setExtraFiles([]*os.File) -} - -type realCmd struct { - *exec.Cmd - stdout, stderr *logLineWriter -} - -func (r *realCmd) setDir(dir string) { - r.Cmd.Dir = dir -} - -func (r *realCmd) setStdout(stdout *logLineWriter) { - r.stdout = stdout - r.Cmd.Stdout = stdout -} - -func (r *realCmd) getStdout() *logLineWriter { - return r.stdout -} - -func (r *realCmd) setStderr(stderr *logLineWriter) { - r.stderr = stderr - r.Cmd.Stderr = stderr -} - -func (r *realCmd) getStderr() *logLineWriter { - return r.stderr -} - -func (r *realCmd) getProcess() *os.Process { - return r.Cmd.Process -} - -func (r *realCmd) setExtraFiles(extraFiles []*os.File) { - r.ExtraFiles = extraFiles -} diff --git a/ingest/ledgerbackend/mock_cmd_test.go b/ingest/ledgerbackend/mock_cmd_test.go index bf06a9ae86..8be533e8b1 100644 --- a/ingest/ledgerbackend/mock_cmd_test.go +++ b/ingest/ledgerbackend/mock_cmd_test.go @@ -1,7 +1,6 @@ package ledgerbackend import ( - "io" "os" "github.com/stretchr/testify/mock" @@ -35,27 +34,8 @@ func (m *mockCmd) setDir(dir string) { m.Called(dir) } -func (m *mockCmd) setStdout(stdout *logLineWriter) { - m.Called(stdout) -} - -func (m *mockCmd) getStdout() *logLineWriter { - args := m.Called() - return args.Get(0).(*logLineWriter) -} - -func (m *mockCmd) setStderr(stderr *logLineWriter) { - m.Called(stderr) -} - -func (m *mockCmd) getStderr() *logLineWriter { - args := m.Called() - return args.Get(0).(*logLineWriter) -} - -func (m *mockCmd) getProcess() *os.Process { - args := m.Called() - return args.Get(0).(*os.Process) +func (m *mockCmd) setLogLineWriter(logWriter *logLineWriter) { + m.Called(logWriter) } func (m *mockCmd) setExtraFiles(files []*os.File) { @@ -63,15 +43,9 @@ func (m *mockCmd) setExtraFiles(files []*os.File) { } func simpleCommandMock() *mockCmd { - _, writer := io.Pipe() - llw := logLineWriter{pipeWriter: writer} cmdMock := &mockCmd{} cmdMock.On("setDir", mock.Anything) - cmdMock.On("setStdout", mock.Anything) - cmdMock.On("getStdout").Return(&llw) - cmdMock.On("setStderr", mock.Anything) - cmdMock.On("getStderr").Return(&llw) - cmdMock.On("getProcess").Return(&os.Process{}).Maybe() + cmdMock.On("setLogLineWriter", mock.Anything) cmdMock.On("setExtraFiles", mock.Anything) cmdMock.On("Start").Return(nil) return cmdMock diff --git a/ingest/ledgerbackend/mock_system_caller_test.go b/ingest/ledgerbackend/mock_system_caller_test.go index 99e1faede9..7878e39f34 100644 --- a/ingest/ledgerbackend/mock_system_caller_test.go +++ b/ingest/ledgerbackend/mock_system_caller_test.go @@ -1,6 +1,7 @@ package ledgerbackend import ( + "context" "io/fs" "os" @@ -37,8 +38,8 @@ func (m *mockSystemCaller) stat(name string) (isDir, error) { return args.Get(0).(isDir), args.Error(1) } -func (m *mockSystemCaller) command(name string, arg ...string) cmdI { - a := []interface{}{name} +func (m *mockSystemCaller) command(ctx context.Context, name string, arg ...string) cmdI { + a := []interface{}{ctx, name} for _, ar := range arg { a = append(a, ar) } diff --git a/ingest/ledgerbackend/run_from.go b/ingest/ledgerbackend/run_from.go new file mode 100644 index 0000000000..2d02322519 --- /dev/null +++ b/ingest/ledgerbackend/run_from.go @@ -0,0 +1,140 @@ +package ledgerbackend + +import ( + "context" + "encoding/json" + "fmt" + "runtime" + + "github.com/stellar/go/protocols/stellarcore" + "github.com/stellar/go/support/log" +) + +type runFromStream struct { + dir workingDir + from uint32 + hash string + coreCmdFactory coreCmdFactory + log *log.Entry + useDB bool +} + +func newRunFromStream(r *stellarCoreRunner, from uint32, hash string) runFromStream { + // We only use ephemeral directories on windows because there is + // no way to terminate captive core gracefully on windows. + // Having an ephemeral directory ensures that it is wiped out + // whenever we terminate captive core + dir := newWorkingDir(r, runtime.GOOS == "windows") + return runFromStream{ + dir: dir, + from: from, + hash: hash, + coreCmdFactory: newCoreCmdFactory(r, dir), + log: r.log, + useDB: r.useDB, + } +} + +func (s runFromStream) getWorkingDir() workingDir { + return s.dir +} + +func (s runFromStream) offlineInfo(ctx context.Context) (stellarcore.InfoResponse, error) { + cmd, err := s.coreCmdFactory.newCmd(ctx, stellarCoreRunnerModeOnline, false, "offline-info") + if err != nil { + return stellarcore.InfoResponse{}, fmt.Errorf("error creating offline-info cmd: %w", err) + } + output, err := cmd.Output() + if err != nil { + return stellarcore.InfoResponse{}, fmt.Errorf("error executing offline-info cmd: %w", err) + } + var info stellarcore.InfoResponse + err = json.Unmarshal(output, &info) + if err != nil { + return stellarcore.InfoResponse{}, fmt.Errorf("invalid output of offline-info cmd: %w", err) + } + return info, nil +} + +func (s runFromStream) start(ctx context.Context) (cmd cmdI, captiveCorePipe pipe, returnErr error) { + var err error + var createNewDB bool + defer func() { + if returnErr != nil && createNewDB { + // if we could not start captive core remove the new db we created + s.dir.remove() + } + }() + if s.useDB { + // Check if on-disk core DB exists and what's the LCL there. If not what + // we need remove storage dir and start from scratch. + var info stellarcore.InfoResponse + info, err = s.offlineInfo(ctx) + if err != nil { + s.log.Infof("Error running offline-info: %v, removing existing storage-dir contents", err) + createNewDB = true + } else if info.Info.Ledger.Num <= 1 || uint32(info.Info.Ledger.Num) > s.from { + s.log.Infof("Unexpected LCL in Stellar-Core DB: %d (want: %d), removing existing storage-dir contents", info.Info.Ledger.Num, s.from) + createNewDB = true + } + + if createNewDB { + if err = s.dir.remove(); err != nil { + return nil, pipe{}, fmt.Errorf("error removing existing storage-dir contents: %w", err) + } + + cmd, err = s.coreCmdFactory.newCmd(ctx, stellarCoreRunnerModeOnline, true, "new-db") + if err != nil { + return nil, pipe{}, fmt.Errorf("error creating command: %w", err) + } + + if err = cmd.Run(); err != nil { + return nil, pipe{}, fmt.Errorf("error initializing core db: %w", err) + } + + // Do a quick catch-up to set the LCL in core to be our expected starting + // point. + if s.from > 2 { + cmd, err = s.coreCmdFactory.newCmd(ctx, stellarCoreRunnerModeOnline, true, "catchup", fmt.Sprintf("%d/0", s.from-1)) + } else { + cmd, err = s.coreCmdFactory.newCmd(ctx, stellarCoreRunnerModeOnline, true, "catchup", "2/0") + } + if err != nil { + return nil, pipe{}, fmt.Errorf("error creating command: %w", err) + } + + if err = cmd.Run(); err != nil { + return nil, pipe{}, fmt.Errorf("error runing stellar-core catchup: %w", err) + } + } + + cmd, err = s.coreCmdFactory.newCmd( + ctx, + stellarCoreRunnerModeOnline, + true, + "run", + "--metadata-output-stream", s.coreCmdFactory.getPipeName(), + ) + } else { + cmd, err = s.coreCmdFactory.newCmd( + ctx, + stellarCoreRunnerModeOnline, + true, + "run", + "--in-memory", + "--start-at-ledger", fmt.Sprintf("%d", s.from), + "--start-at-hash", s.hash, + "--metadata-output-stream", s.coreCmdFactory.getPipeName(), + ) + } + if err != nil { + return nil, pipe{}, fmt.Errorf("error creating command: %w", err) + } + + captiveCorePipe, err = s.coreCmdFactory.startCaptiveCore(cmd) + if err != nil { + return nil, pipe{}, fmt.Errorf("error starting `stellar-core run` subprocess: %w", err) + } + + return cmd, captiveCorePipe, nil +} diff --git a/ingest/ledgerbackend/stellar_core_runner.go b/ingest/ledgerbackend/stellar_core_runner.go index 57e8c1c0f9..5245051dce 100644 --- a/ingest/ledgerbackend/stellar_core_runner.go +++ b/ingest/ledgerbackend/stellar_core_runner.go @@ -1,33 +1,21 @@ package ledgerbackend import ( - "bufio" "context" - "encoding/json" "fmt" "io" "math/rand" - "os" - "path" - "path/filepath" - "regexp" - "runtime" - "strings" "sync" - "time" - "github.com/pkg/errors" - - "github.com/stellar/go/protocols/stellarcore" "github.com/stellar/go/support/log" ) type stellarCoreRunnerInterface interface { catchup(from, to uint32) error runFrom(from uint32, hash string) error - getMetaPipe() <-chan metaResult + getMetaPipe() (<-chan metaResult, bool) context() context.Context - getProcessExitError() (bool, error) + getProcessExitError() (error, bool) close() error } @@ -51,29 +39,33 @@ type pipe struct { File io.Closer } +type executionState struct { + cmd cmdI + workingDir workingDir + ledgerBuffer *bufferedLedgerMetaReader + pipe pipe + wg sync.WaitGroup + processExitedLock sync.RWMutex + processExited bool + processExitError error + log *log.Entry +} + type stellarCoreRunner struct { executablePath string - - started bool - cmd cmdI - wg sync.WaitGroup - ctx context.Context - cancel context.CancelFunc - ledgerBuffer *bufferedLedgerMetaReader - pipe pipe - mode stellarCoreRunnerMode + ctx context.Context + cancel context.CancelFunc systemCaller systemCaller - lock sync.Mutex - closeOnce sync.Once - processExited bool - processExitError error + stateLock sync.Mutex + state *executionState + + closeOnce sync.Once storagePath string toml *CaptiveCoreToml useDB bool - nonce string log *log.Entry } @@ -96,12 +88,8 @@ func newStellarCoreRunner(config CaptiveCoreConfig) *stellarCoreRunner { cancel: cancel, storagePath: config.StoragePath, useDB: config.UseDB, - nonce: fmt.Sprintf( - "captive-stellar-core-%x", - rand.New(rand.NewSource(time.Now().UnixNano())).Uint64(), - ), - log: config.Log, - toml: config.Toml, + log: config.Log, + toml: config.Toml, systemCaller: realSystemCaller{}, } @@ -109,356 +97,54 @@ func newStellarCoreRunner(config CaptiveCoreConfig) *stellarCoreRunner { return runner } -func (r *stellarCoreRunner) getFullStoragePath() string { - if runtime.GOOS == "windows" || r.mode == stellarCoreRunnerModeOffline { - // On Windows, first we ALWAYS append something to the base storage path, - // because we will delete the directory entirely when Horizon stops. We also - // add a random suffix in order to ensure that there aren't naming - // conflicts. - // This is done because it's impossible to send SIGINT on Windows so - // buckets can become corrupted. - // We also want to use random directories in offline mode (reingestion) - // because it's possible it's running multiple Stellar-Cores on a single - // machine. - return path.Join(r.storagePath, "captive-core-"+createRandomHexString(8)) - } else { - // Use the specified directory to store Captive Core's data: - // https://github.com/stellar/go/issues/3437 - // but be sure to re-use rather than replace it: - // https://github.com/stellar/go/issues/3631 - return path.Join(r.storagePath, "captive-core") - } -} - -func (r *stellarCoreRunner) establishStorageDirectory() error { - info, err := r.systemCaller.stat(r.storagePath) - if os.IsNotExist(err) { - innerErr := r.systemCaller.mkdirAll(r.storagePath, os.FileMode(int(0755))) // rwx|rx|rx - if innerErr != nil { - return errors.Wrap(innerErr, fmt.Sprintf( - "failed to create storage directory (%s)", r.storagePath)) - } - } else if !info.IsDir() { - return errors.New(fmt.Sprintf("%s is not a directory", r.storagePath)) - } else if err != nil { - return errors.Wrap(err, fmt.Sprintf( - "error accessing storage directory (%s)", r.storagePath)) - } - - return nil -} - -func (r *stellarCoreRunner) writeConf() (string, error) { - text, err := generateConfig(r.toml, r.mode) - if err != nil { - return "", err - } - - return string(text), r.systemCaller.writeFile(r.getConfFileName(), text, 0644) -} - -func generateConfig(captiveCoreToml *CaptiveCoreToml, mode stellarCoreRunnerMode) ([]byte, error) { - if mode == stellarCoreRunnerModeOffline { - var err error - captiveCoreToml, err = captiveCoreToml.CatchupToml() - if err != nil { - return nil, errors.Wrap(err, "could not generate catch up config") - } - } - - if !captiveCoreToml.QuorumSetIsConfigured() { - return nil, errors.New("captive-core config file does not define any quorum set") - } - - text, err := captiveCoreToml.Marshal() - if err != nil { - return nil, errors.Wrap(err, "could not marshal captive core config") - } - return text, nil -} - -func (r *stellarCoreRunner) getConfFileName() string { - joinedPath := filepath.Join(r.storagePath, "stellar-core.conf") - - // Given that `storagePath` can be anything, we need the full, absolute path - // here so that everything Core needs is created under the storagePath - // subdirectory. - // - // If the path *can't* be absolutely resolved (bizarre), we can still try - // recovering by using the path the user specified directly. - path, err := filepath.Abs(joinedPath) - if err != nil { - r.log.Warnf("Failed to resolve %s as an absolute path: %s", joinedPath, err) - return joinedPath - } - return path -} - -type logLineWriter struct { - pipeWriter *io.PipeWriter - wg sync.WaitGroup -} - -func (l *logLineWriter) Write(p []byte) (n int, err error) { - return l.pipeWriter.Write(p) -} - -func (l *logLineWriter) Close() error { - err := l.pipeWriter.Close() - l.wg.Wait() - return err -} - -func (r *stellarCoreRunner) getLogLineWriter() *logLineWriter { - rd, wr := io.Pipe() - br := bufio.NewReader(rd) - result := &logLineWriter{ - pipeWriter: wr, - } - // Strip timestamps from log lines from captive stellar-core. We emit our own. - dateRx := regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3} `) - result.wg.Add(1) - go func() { - defer result.wg.Done() - levelRx := regexp.MustCompile(`\[(\w+) ([A-Z]+)\] (.*)`) - for { - line, err := br.ReadString('\n') - if err != nil { - break - } - line = dateRx.ReplaceAllString(line, "") - line = strings.TrimSpace(line) - - if line == "" { - continue - } - - matches := levelRx.FindStringSubmatch(line) - if len(matches) >= 4 { - // Extract the substrings from the log entry and trim it - category, level := matches[1], matches[2] - line = matches[3] - - levelMapping := map[string]func(string, ...interface{}){ - "FATAL": r.log.Errorf, - "ERROR": r.log.Errorf, - "WARNING": r.log.Warnf, - "INFO": r.log.Infof, - "DEBUG": r.log.Debugf, - } - - writer := r.log.Infof - if f, ok := levelMapping[strings.ToUpper(level)]; ok { - writer = f - } - writer("%s: %s", category, line) - } else { - r.log.Info(line) - } - } - }() - return result -} - -func (r *stellarCoreRunner) offlineInfo() (stellarcore.InfoResponse, error) { - allParams := []string{"--conf", r.getConfFileName(), "offline-info"} - cmd := r.systemCaller.command(r.executablePath, allParams...) - cmd.setDir(r.storagePath) - output, err := cmd.Output() - if err != nil { - return stellarcore.InfoResponse{}, errors.Wrap(err, "error executing offline-info cmd") - } - var info stellarcore.InfoResponse - err = json.Unmarshal(output, &info) - if err != nil { - return stellarcore.InfoResponse{}, errors.Wrap(err, "invalid output of offline-info cmd") - } - return info, nil -} - -func (r *stellarCoreRunner) createCmd(params ...string) (cmdI, error) { - err := r.establishStorageDirectory() - if err != nil { - return nil, err - } - - if conf, err := r.writeConf(); err != nil { - return nil, errors.Wrap(err, "error writing configuration") - } else { - r.log.Debugf("captive core config file contents:\n%s", conf) - } - - allParams := append([]string{"--conf", r.getConfFileName(), "--console"}, params...) - cmd := r.systemCaller.command(r.executablePath, allParams...) - cmd.setDir(r.storagePath) - cmd.setStdout(r.getLogLineWriter()) - cmd.setStderr(r.getLogLineWriter()) - return cmd, nil -} - // context returns the context.Context instance associated with the running captive core instance func (r *stellarCoreRunner) context() context.Context { return r.ctx } +// runFrom executes the run command with a starting ledger on the captive core subprocess +func (r *stellarCoreRunner) runFrom(from uint32, hash string) error { + return r.startMetaStream(newRunFromStream(r, from, hash)) +} + // catchup executes the catchup command on the captive core subprocess func (r *stellarCoreRunner) catchup(from, to uint32) error { - r.lock.Lock() - defer r.lock.Unlock() - - // check if we have already been closed - if r.ctx.Err() != nil { - return r.ctx.Err() - } - - if r.started { - return errors.New("runner already started") - } - - r.mode = stellarCoreRunnerModeOffline - r.storagePath = r.getFullStoragePath() - - rangeArg := fmt.Sprintf("%d/%d", to, to-from+1) - params := []string{"catchup", rangeArg, "--metadata-output-stream", r.getPipeName()} - - // horizon operator has specified to use external storage for captive core ledger state - // instruct captive core invocation to not use memory, and in that case - // cc will look at DATABASE property in cfg toml for the external storage source to use. - // when using external storage of ledgers, use new-db to first set the state of - // remote db storage to genesis to purge any prior state and reset. - if r.useDB { - cmd, err := r.createCmd("new-db") - if err != nil { - return errors.Wrap(err, "error creating command") - } - if err := cmd.Run(); err != nil { - return errors.Wrap(err, "error initializing core db") - } - } else { - params = append(params, "--in-memory") - } - - var err error - r.cmd, err = r.createCmd(params...) - if err != nil { - return errors.Wrap(err, "error creating command") - } - - r.pipe, err = r.start(r.cmd) - if err != nil { - r.closeLogLineWriters(r.cmd) - return errors.Wrap(err, "error starting `stellar-core catchup` subprocess") - } - - r.started = true - r.ledgerBuffer = newBufferedLedgerMetaReader(r.pipe.Reader) - go r.ledgerBuffer.start() - - if binaryWatcher, err := newFileWatcher(r); err != nil { - r.log.Warnf("could not create captive core binary watcher: %v", err) - } else { - go binaryWatcher.loop() - } - - r.wg.Add(1) - go r.handleExit() + return r.startMetaStream(newCatchupStream(r, from, to)) +} - return nil +type metaStream interface { + getWorkingDir() workingDir + start(ctx context.Context) (cmdI, pipe, error) } -// runFrom executes the run command with a starting ledger on the captive core subprocess -func (r *stellarCoreRunner) runFrom(from uint32, hash string) error { - r.lock.Lock() - defer r.lock.Unlock() +func (r *stellarCoreRunner) startMetaStream(stream metaStream) error { + r.stateLock.Lock() + defer r.stateLock.Unlock() // check if we have already been closed if r.ctx.Err() != nil { return r.ctx.Err() } - if r.started { - return errors.New("runner already started") + if r.state != nil { + return fmt.Errorf("runner already started") } - r.mode = stellarCoreRunnerModeOnline - r.storagePath = r.getFullStoragePath() - - var err error - - if r.useDB { - // Check if on-disk core DB exists and what's the LCL there. If not what - // we need remove storage dir and start from scratch. - removeStorageDir := false - var info stellarcore.InfoResponse - info, err = r.offlineInfo() - if err != nil { - r.log.Infof("Error running offline-info: %v, removing existing storage-dir contents", err) - removeStorageDir = true - } else if uint32(info.Info.Ledger.Num) > from { - r.log.Infof("Unexpected LCL in Stellar-Core DB: %d (want: %d), removing existing storage-dir contents", info.Info.Ledger.Num, from) - removeStorageDir = true - } - - if removeStorageDir { - if err = r.systemCaller.removeAll(r.storagePath); err != nil { - return errors.Wrap(err, "error removing existing storage-dir contents") - } - - var cmd cmdI - cmd, err = r.createCmd("new-db") - if err != nil { - return errors.Wrap(err, "error creating command") - } - - if err = cmd.Run(); err != nil { - return errors.Wrap(err, "error initializing core db") - } - - // Do a quick catch-up to set the LCL in core to be our expected starting - // point. - if from > 2 { - cmd, err = r.createCmd("catchup", fmt.Sprintf("%d/0", from-1)) - } else { - cmd, err = r.createCmd("catchup", "2/0") - } - - if err != nil { - return errors.Wrap(err, "error creating command") - } - - if err = cmd.Run(); err != nil { - return errors.Wrap(err, "error runing stellar-core catchup") - } - } - - r.cmd, err = r.createCmd( - "run", - "--metadata-output-stream", - r.getPipeName(), - ) - } else { - r.cmd, err = r.createCmd( - "run", - "--in-memory", - "--start-at-ledger", fmt.Sprintf("%d", from), - "--start-at-hash", hash, - "--metadata-output-stream", r.getPipeName(), - ) + state := &executionState{ + workingDir: stream.getWorkingDir(), + log: r.log, } + cmd, p, err := stream.start(r.ctx) if err != nil { - return errors.Wrap(err, "error creating command") + state.workingDir.cleanup(nil) + return err } - r.pipe, err = r.start(r.cmd) - if err != nil { - r.closeLogLineWriters(r.cmd) - return errors.Wrap(err, "error starting `stellar-core run` subprocess") - } - - r.started = true - r.ledgerBuffer = newBufferedLedgerMetaReader(r.pipe.Reader) - go r.ledgerBuffer.start() + state.cmd = cmd + state.pipe = p + state.ledgerBuffer = newBufferedLedgerMetaReader(state.pipe.Reader) + go state.ledgerBuffer.start() if binaryWatcher, err := newFileWatcher(r); err != nil { r.log.Warnf("could not create captive core binary watcher: %v", err) @@ -466,101 +152,78 @@ func (r *stellarCoreRunner) runFrom(from uint32, hash string) error { go binaryWatcher.loop() } - r.wg.Add(1) - go r.handleExit() + state.wg.Add(1) + go state.handleExit() + r.state = state return nil } -func (r *stellarCoreRunner) handleExit() { - defer r.wg.Done() - - // Pattern recommended in: - // https://github.com/golang/go/blob/cacac8bdc5c93e7bc71df71981fdf32dded017bf/src/cmd/go/script_test.go#L1091-L1098 - interrupt := os.Interrupt - if runtime.GOOS == "windows" { - // Per https://golang.org/pkg/os/#Signal, “Interrupt is not implemented on - // Windows; using it with os.Process.Signal will return an error.” - // Fall back to Kill instead. - interrupt = os.Kill - } +func (r *stellarCoreRunner) getExecutionState() *executionState { + r.stateLock.Lock() + defer r.stateLock.Unlock() + return r.state +} - errc := make(chan error) - go func() { - select { - case errc <- nil: - return - case <-r.ctx.Done(): - } +func (state *executionState) handleExit() { + defer state.wg.Done() - err := r.cmd.getProcess().Signal(interrupt) - if err == nil { - err = r.ctx.Err() // Report ctx.Err() as the reason we interrupted. - } else if err.Error() == "os: process already finished" { - errc <- nil - return - } + waitErr := state.cmd.Wait() - timer := time.NewTimer(10 * time.Second) - select { - // Report ctx.Err() as the reason we interrupted the process... - case errc <- r.ctx.Err(): - timer.Stop() - return - // ...but after killDelay has elapsed, fall back to a stronger signal. - case <-timer.C: - } + // By closing the pipe file we will send an EOF to the pipe reader used by ledgerBuffer. + if err := state.pipe.File.Close(); err != nil { + state.log.WithError(err).Warn("could not close captive core write pipe") + } - // Wait still hasn't returned. - // Kill the process harder to make sure that it exits. - // - // Ignore any error: if cmd.Process has already terminated, we still - // want to send ctx.Err() (or the error from the Interrupt call) - // to properly attribute the signal that may have terminated it. - _ = r.cmd.getProcess().Kill() + state.processExitedLock.Lock() + defer state.processExitedLock.Unlock() + state.processExited = true + state.processExitError = waitErr +} - errc <- err - }() +func (state *executionState) getProcessExitError() (error, bool) { + state.processExitedLock.RLock() + defer state.processExitedLock.RUnlock() + return state.processExitError, state.processExited +} - waitErr := r.cmd.Wait() - r.closeLogLineWriters(r.cmd) +func (state *executionState) cleanup() error { + // wait for the stellar core process to terminate + state.wg.Wait() - r.lock.Lock() - defer r.lock.Unlock() + // drain meta pipe channel to make sure the ledger buffer goroutine exits + for range state.ledgerBuffer.getChannel() { - // By closing the pipe file we will send an EOF to the pipe reader used by ledgerBuffer. - // We need to do this operation with the lock to ensure that the processExitError is available - // when the ledgerBuffer channel is closed - if closeErr := r.pipe.File.Close(); closeErr != nil { - r.log.WithError(closeErr).Warn("could not close captive core write pipe") } - r.processExited = true - if interruptErr := <-errc; interruptErr != nil { - r.processExitError = interruptErr - } else { - r.processExitError = waitErr + // now it's safe to close the pipe reader + // because the ledger buffer is no longer reading from it + if err := state.pipe.Reader.Close(); err != nil { + state.log.WithError(err).Warn("could not close captive core read pipe") } -} -// closeLogLineWriters closes the go routines created by getLogLineWriter() -func (r *stellarCoreRunner) closeLogLineWriters(cmd cmdI) { - cmd.getStdout().Close() - cmd.getStderr().Close() + processExitError, _ := state.getProcessExitError() + return state.workingDir.cleanup(processExitError) } // getMetaPipe returns a channel which contains ledgers streamed from the captive core subprocess -func (r *stellarCoreRunner) getMetaPipe() <-chan metaResult { - return r.ledgerBuffer.getChannel() +func (r *stellarCoreRunner) getMetaPipe() (<-chan metaResult, bool) { + state := r.getExecutionState() + if state == nil { + return nil, false + } + return state.ledgerBuffer.getChannel(), true } // getProcessExitError returns an exit error (can be nil) of the process and a bool indicating // if the process has exited yet // getProcessExitError is thread safe -func (r *stellarCoreRunner) getProcessExitError() (bool, error) { - r.lock.Lock() - defer r.lock.Unlock() - return r.processExited, r.processExitError +func (r *stellarCoreRunner) getProcessExitError() (error, bool) { + state := r.getExecutionState() + if state == nil { + return nil, false + } + return state.getProcessExitError() } // close kills the captive core process if it is still running and performs @@ -569,43 +232,11 @@ func (r *stellarCoreRunner) getProcessExitError() (bool, error) { func (r *stellarCoreRunner) close() error { var closeError error r.closeOnce.Do(func() { - r.lock.Lock() - // we cancel the context while holding the lock in order to guarantee that - // this captive core instance cannot start once the lock is released. - // catchup() and runFrom() can only execute while holding the lock and if - // the context is canceled both catchup() and runFrom() will abort early - // without performing any side effects (e.g. state mutations). r.cancel() - r.lock.Unlock() - - // only reap captive core sub process and related go routines if we've started - // otherwise, just cleanup the temp dir - if r.started { - // wait for the stellar core process to terminate - r.wg.Wait() - - // drain meta pipe channel to make sure the ledger buffer goroutine exits - for range r.getMetaPipe() { - - } - - // now it's safe to close the pipe reader - // because the ledger buffer is no longer reading from it - r.pipe.Reader.Close() - } - - if r.mode != 0 && (runtime.GOOS == "windows" || - (r.processExitError != nil && r.processExitError != context.Canceled) || - r.mode == stellarCoreRunnerModeOffline) { - // It's impossible to send SIGINT on Windows so buckets can become - // corrupted. If we can't reuse it, then remove it. - // We also remove the storage path if there was an error terminating the - // process (files can be corrupted). - // We remove all files when reingesting to save disk space. - closeError = r.systemCaller.removeAll(r.storagePath) - return + state := r.getExecutionState() + if state != nil { + closeError = state.cleanup() } }) - return closeError } diff --git a/ingest/ledgerbackend/stellar_core_runner_test.go b/ingest/ledgerbackend/stellar_core_runner_test.go index 00cb29137b..f53cd88328 100644 --- a/ingest/ledgerbackend/stellar_core_runner_test.go +++ b/ingest/ledgerbackend/stellar_core_runner_test.go @@ -37,6 +37,7 @@ func TestCloseOffline(t *testing.T) { scMock.On("stat", mock.Anything).Return(isDirImpl(true), nil) scMock.On("writeFile", mock.Anything, mock.Anything, mock.Anything).Return(nil) scMock.On("command", + runner.ctx, "/usr/bin/stellar-core", "--conf", mock.Anything, @@ -78,6 +79,7 @@ func TestCloseOnline(t *testing.T) { scMock.On("stat", mock.Anything).Return(isDirImpl(true), nil) scMock.On("writeFile", mock.Anything, mock.Anything, mock.Anything).Return(nil) scMock.On("command", + runner.ctx, "/usr/bin/stellar-core", "--conf", mock.Anything, @@ -121,6 +123,7 @@ func TestCloseOnlineWithError(t *testing.T) { scMock.On("stat", mock.Anything).Return(isDirImpl(true), nil) scMock.On("writeFile", mock.Anything, mock.Anything, mock.Anything).Return(nil) scMock.On("command", + runner.ctx, "/usr/bin/stellar-core", "--conf", mock.Anything, @@ -141,7 +144,7 @@ func TestCloseOnlineWithError(t *testing.T) { // Wait with calling close until r.processExitError is set to Wait() error for { - _, err := runner.getProcessExitError() + err, _ := runner.getProcessExitError() if err != nil { break } @@ -175,6 +178,7 @@ func TestCloseConcurrency(t *testing.T) { scMock.On("stat", mock.Anything).Return(isDirImpl(true), nil) scMock.On("writeFile", mock.Anything, mock.Anything, mock.Anything).Return(nil) scMock.On("command", + runner.ctx, "/usr/bin/stellar-core", "--conf", mock.Anything, @@ -196,7 +200,7 @@ func TestCloseConcurrency(t *testing.T) { go func() { defer wg.Done() assert.NoError(t, runner.close()) - exited, err := runner.getProcessExitError() + err, exited := runner.getProcessExitError() assert.True(t, exited) assert.Error(t, err) }() @@ -238,12 +242,14 @@ func TestRunFromUseDBLedgersMatch(t *testing.T) { scMock.On("stat", mock.Anything).Return(isDirImpl(true), nil) scMock.On("writeFile", mock.Anything, mock.Anything, mock.Anything).Return(nil) scMock.On("command", + runner.ctx, "/usr/bin/stellar-core", "--conf", mock.Anything, "offline-info", ).Return(offlineInfoCmdMock) scMock.On("command", + runner.ctx, "/usr/bin/stellar-core", "--conf", mock.Anything, @@ -299,12 +305,14 @@ func TestRunFromUseDBLedgersBehind(t *testing.T) { scMock.On("stat", mock.Anything).Return(isDirImpl(true), nil) scMock.On("writeFile", mock.Anything, mock.Anything, mock.Anything).Return(nil) scMock.On("command", + runner.ctx, "/usr/bin/stellar-core", "--conf", mock.Anything, "offline-info", ).Return(offlineInfoCmdMock) scMock.On("command", + runner.ctx, "/usr/bin/stellar-core", "--conf", mock.Anything, @@ -360,12 +368,14 @@ func TestRunFromUseDBLedgersInFront(t *testing.T) { scMock.On("stat", mock.Anything).Return(isDirImpl(true), nil) scMock.On("writeFile", mock.Anything, mock.Anything, mock.Anything).Return(nil) scMock.On("command", + runner.ctx, "/usr/bin/stellar-core", "--conf", mock.Anything, "offline-info", ).Return(offlineInfoCmdMock) scMock.On("command", + runner.ctx, "/usr/bin/stellar-core", "--conf", mock.Anything, @@ -373,6 +383,7 @@ func TestRunFromUseDBLedgersInFront(t *testing.T) { "new-db", ).Return(newDBCmdMock) scMock.On("command", + runner.ctx, "/usr/bin/stellar-core", "--conf", mock.Anything, @@ -381,6 +392,7 @@ func TestRunFromUseDBLedgersInFront(t *testing.T) { "99/0", ).Return(catchupCmdMock) scMock.On("command", + runner.ctx, "/usr/bin/stellar-core", "--conf", mock.Anything, From 7060fdd35a670ec34a05f5154705d450f5d40680 Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 28 Jun 2024 14:20:30 +0100 Subject: [PATCH 203/234] Remove dump-ledger-state script (#5363) --- .github/workflows/horizon-master.yml | 28 -- exp/tools/dump-ledger-state/Dockerfile | 45 --- exp/tools/dump-ledger-state/README.md | 14 - exp/tools/dump-ledger-state/diff_test.sh | 36 -- .../dump-ledger-state/docker-entrypoint.sh | 39 -- exp/tools/dump-ledger-state/dump_core_db.sh | 27 -- exp/tools/dump-ledger-state/main.go | 366 ------------------ exp/tools/dump-ledger-state/run_test.sh | 39 -- .../stellar-core-testnet.cfg | 39 -- exp/tools/dump-ledger-state/stellar-core.cfg | 201 ---------- 10 files changed, 834 deletions(-) delete mode 100644 .github/workflows/horizon-master.yml delete mode 100644 exp/tools/dump-ledger-state/Dockerfile delete mode 100644 exp/tools/dump-ledger-state/README.md delete mode 100755 exp/tools/dump-ledger-state/diff_test.sh delete mode 100755 exp/tools/dump-ledger-state/docker-entrypoint.sh delete mode 100755 exp/tools/dump-ledger-state/dump_core_db.sh delete mode 100644 exp/tools/dump-ledger-state/main.go delete mode 100755 exp/tools/dump-ledger-state/run_test.sh delete mode 100644 exp/tools/dump-ledger-state/stellar-core-testnet.cfg delete mode 100644 exp/tools/dump-ledger-state/stellar-core.cfg diff --git a/.github/workflows/horizon-master.yml b/.github/workflows/horizon-master.yml deleted file mode 100644 index e2487a0d64..0000000000 --- a/.github/workflows/horizon-master.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Horizon master - -on: - push: - branches: [master] - -jobs: - - push-state-diff-image: - name: Push stellar/ledger-state-diff:{sha,latest} to DockerHub - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v3 - - - name: Login to DockerHub - uses: docker/login-action@bb984efc561711aaa26e433c32c3521176eae55b - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push to DockerHub - uses: docker/build-push-action@7f9d37fa544684fb73bfe4835ed7214c255ce02b - with: - push: true - tags: stellar/ledger-state-diff:${{ github.sha }},stellar/ledger-state-diff:latest - file: exp/tools/dump-ledger-state/Dockerfile - build-args: GITCOMMIT=${{ github.sha }} - no-cache: true diff --git a/exp/tools/dump-ledger-state/Dockerfile b/exp/tools/dump-ledger-state/Dockerfile deleted file mode 100644 index 5ffcb9c0a2..0000000000 --- a/exp/tools/dump-ledger-state/Dockerfile +++ /dev/null @@ -1,45 +0,0 @@ -FROM ubuntu:22.04 - -ENV STELLAR_CORE_VERSION=21.0.0-1872.c6f474133.focal -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl wget gnupg apt-utils -RUN wget -qO - https://apt.stellar.org/SDF.asc | APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=true apt-key add - -RUN echo "deb https://apt.stellar.org focal stable" >/etc/apt/sources.list.d/SDF.list -# RUN echo "deb https://apt.stellar.org focal unstable" >/etc/apt/sources.list.d/SDF-unstable.list -RUN apt-get update -y - -RUN apt-get install -y stellar-core=${STELLAR_CORE_VERSION} jq -RUN apt-get clean -RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ $(env -i bash -c '. /etc/os-release; echo $VERSION_CODENAME')-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list && \ - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ - apt-get update && \ - DEBIAN_FRONTEND="noninteractive" apt-get install -y postgresql-9.6 postgresql-contrib-9.6 postgresql-client-9.6 - -# Create a PostgreSQL role named `circleci` and then create a database `core` owned by the `circleci` role. -RUN su - postgres -c "/etc/init.d/postgresql start && psql --command \"CREATE USER circleci WITH SUPERUSER;\" && createdb -O circleci core" - -# Adjust PostgreSQL configuration so that remote connections to the -# database are possible. -RUN echo "host all all all trust" > /etc/postgresql/9.6/main/pg_hba.conf - -# And add `listen_addresses` to `/etc/postgresql/9.6/main/postgresql.conf` -RUN echo "listen_addresses='*'" >> /etc/postgresql/9.6/main/postgresql.conf - -COPY --from=golang:1.22-bullseye /usr/local/go/ /usr/local/go/ -RUN ln -s /usr/local/go/bin/go /usr/local/bin/go -WORKDIR /go/src/github.com/stellar/go -COPY go.mod go.sum ./ -RUN go mod download -COPY . ./ - -ENV PGPORT=5432 -ENV PGUSER=circleci -ENV PGHOST=localhost - -WORKDIR /go/src/github.com/stellar/go/exp/tools/dump-ledger-state - -ARG GITCOMMIT -ENV GITCOMMIT=${GITCOMMIT} - -ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/exp/tools/dump-ledger-state/README.md b/exp/tools/dump-ledger-state/README.md deleted file mode 100644 index 17376bd17d..0000000000 --- a/exp/tools/dump-ledger-state/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# dump-ledger-state - -This tool dumps the state from history archive buckets to 4 separate files: -* accounts.csv -* accountdata.csv -* offers.csv -* trustlines.csv -* claimablebalances.csv - -It's primary use is to test `SingleLedgerStateReader`. To run the test (`run_test.sh`) it: -1. Runs `dump-ledger-state`. -2. Syncs stellar-core to the same checkpoint: `stellar-core catchup [ledger]/1`. -3. Dumps stellar-core DB by using `dump_core_db.sh` script. -4. Diffs results by using `diff_test.sh` script. diff --git a/exp/tools/dump-ledger-state/diff_test.sh b/exp/tools/dump-ledger-state/diff_test.sh deleted file mode 100755 index 69295b2a82..0000000000 --- a/exp/tools/dump-ledger-state/diff_test.sh +++ /dev/null @@ -1,36 +0,0 @@ -ENTRIES=(accounts accountdata offers trustlines claimablebalances pools) - -echo "Sorting dump-ledger-state output files..." -for i in "${ENTRIES[@]}" -do - if test -f "${i}_sorted.csv"; then - echo "Skipping, ${i}_sorted.csv exists (remove if out of date to sort again)" - continue - fi - wc -l ${i}.csv - sort -S 500M -o ${i}_sorted.csv ${i}.csv -done - -echo "Sorting stellar-core output files..." -for i in "${ENTRIES[@]}" -do - if test -f "${i}_core_sorted.csv"; then - echo "Skipping, ${i}_core_sorted.csv exists (remove if out of date to sort again)" - continue - fi - wc -l ${i}_core.csv - sort -S 500M -o ${i}_core_sorted.csv ${i}_core.csv -done - -echo "Checking diffs..." -for type in "${ENTRIES[@]}" -do - diff -q ${type}_core_sorted.csv ${type}_sorted.csv - if [ "$?" -ne "0" ] - then - echo "ERROR: $type does NOT match"; - exit -1 - else - echo "$type OK"; - fi -done diff --git a/exp/tools/dump-ledger-state/docker-entrypoint.sh b/exp/tools/dump-ledger-state/docker-entrypoint.sh deleted file mode 100755 index f1451c2ad5..0000000000 --- a/exp/tools/dump-ledger-state/docker-entrypoint.sh +++ /dev/null @@ -1,39 +0,0 @@ -#! /bin/bash -set -e - -/etc/init.d/postgresql start - -while ! psql -U circleci -d core -h localhost -p 5432 -c 'select 1' >/dev/null 2>&1; do - echo "Waiting for postgres to be available..." - sleep 1 -done - -echo "using version $(stellar-core version)" - -if [ -z ${TESTNET+x} ]; then - stellar-core --conf ./stellar-core.cfg new-db -else - stellar-core --conf ./stellar-core-testnet.cfg new-db -fi - -if [ -z ${LATEST_LEDGER+x} ]; then - # Get latest ledger - echo "Getting latest checkpoint ledger..." - if [ -z ${TESTNET+x} ]; then - export LATEST_LEDGER=`curl -s http://history.stellar.org/prd/core-live/core_live_001/.well-known/stellar-history.json | jq -r '.currentLedger'` - else - export LATEST_LEDGER=`curl -s http://history.stellar.org/prd/core-testnet/core_testnet_001/.well-known/stellar-history.json | jq -r '.currentLedger'` - fi -fi - -if [[ -z "${LATEST_LEDGER}" ]]; then - echo "could not obtain latest ledger" - exit 1 -fi - -echo "Latest ledger: $LATEST_LEDGER" - -if ! ./run_test.sh; then - echo "ingestion dump (git commit \`$GITCOMMIT\`) of ledger \`$LATEST_LEDGER\` does not match stellar core db." - exit 1 -fi \ No newline at end of file diff --git a/exp/tools/dump-ledger-state/dump_core_db.sh b/exp/tools/dump-ledger-state/dump_core_db.sh deleted file mode 100755 index ebd8871a47..0000000000 --- a/exp/tools/dump-ledger-state/dump_core_db.sh +++ /dev/null @@ -1,27 +0,0 @@ -# Get state from stellar-core DB, colums match CSV printer -# FETCH_COUNT is there for circleci to use cursor-based method of getting rows (less RAM usage): -# https://dba.stackexchange.com/a/101510 - -echo "Fetching accounts from stellar-core DB..." -psql -d core -t -A -F"," --variable="FETCH_COUNT=10000" -c "select accountid, balance, seqnum, numsubentries, inflationdest, homedomain, thresholds, flags, COALESCE(extension, 'AAAAAA=='), signers, ledgerext from accounts" > accounts_core.csv -rm accounts_core_sorted.csv || true # Remove if exist in case original files are rebuilt - -echo "Fetching accountdata from stellar-core DB..." -psql -d core -t -A -F"," --variable="FETCH_COUNT=10000" -c "select accountid, dataname, datavalue, COALESCE(extension, 'AAAAAA=='), ledgerext from accountdata" > accountdata_core.csv -rm accountdata_core_sorted.csv || true # Remove if exist in case original files are rebuilt - -echo "Fetching offers from stellar-core DB..." -psql -d core -t -A -F"," --variable="FETCH_COUNT=10000" -c "select sellerid, offerid, sellingasset, buyingasset, amount, pricen, priced, flags, COALESCE(extension, 'AAAAAA=='), ledgerext from offers" > offers_core.csv -rm offers_core_sorted.csv || true # Remove if exist in case original files are rebuilt - -echo "Fetching trustlines from stellar-core DB..." -psql -d core -t -A -F"," --variable="FETCH_COUNT=10000" -c "select ledgerentry from trustlines" > trustlines_core.csv -rm trustlines_core_sorted.csv || true # Remove if exist in case original files are rebuilt - -echo "Fetching claimable balances from stellar-core DB..." -psql -d core -t -A -F"," --variable="FETCH_COUNT=10000" -c "select balanceid, ledgerentry from claimablebalance" > claimablebalances_core.csv -rm claimablebalances_core_sorted.csv || true # Remove if exist in case original files are rebuilt - -echo "Fetching liquidity pools from stellar-core DB..." -psql -d core -t -A -F"," --variable="FETCH_COUNT=10000" -c "select ledgerentry from liquiditypool" > pools_core.csv -rm pools_core_sorted.csv || true # Remove if exist in case original files are rebuilt \ No newline at end of file diff --git a/exp/tools/dump-ledger-state/main.go b/exp/tools/dump-ledger-state/main.go deleted file mode 100644 index 26f59348a7..0000000000 --- a/exp/tools/dump-ledger-state/main.go +++ /dev/null @@ -1,366 +0,0 @@ -package main - -import ( - "context" - "encoding/base64" - "encoding/csv" - "flag" - "io" - "os" - "runtime" - "strconv" - "time" - - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/ingest" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/log" - "github.com/stellar/go/support/storage" - "github.com/stellar/go/xdr" -) - -// csvMap maintains a mapping from ledger entry type to csv file -type csvMap struct { - files map[xdr.LedgerEntryType]*os.File - writers map[xdr.LedgerEntryType]*csv.Writer -} - -// newCSVMap constructs an empty csvMap instance -func newCSVMap() csvMap { - return csvMap{ - files: map[xdr.LedgerEntryType]*os.File{}, - writers: map[xdr.LedgerEntryType]*csv.Writer{}, - } -} - -// put creates a new file with the given file name and links that file to the -// given ledger entry type -func (c csvMap) put(entryType xdr.LedgerEntryType, fileName string) error { - if _, ok := c.files[entryType]; ok { - return errors.Errorf("entry type %s is already present in the file set", fileName) - } - - file, err := os.Create(fileName) - if err != nil { - return errors.Wrapf(err, "could not open file %s", fileName) - } - - c.files[entryType] = file - c.writers[entryType] = csv.NewWriter(file) - - return nil -} - -// get returns a csv writer for the given ledger entry type if it exists in the mapping -func (c csvMap) get(entryType xdr.LedgerEntryType) (*csv.Writer, bool) { - writer, ok := c.writers[entryType] - return writer, ok -} - -// close will close all files contained in the mapping -func (c csvMap) close() { - for entryType, file := range c.files { - if err := file.Close(); err != nil { - log.WithField("type", entryType.String()).Warn("could not close csv file") - } - delete(c.files, entryType) - delete(c.writers, entryType) - } -} - -type csvProcessor struct { - files csvMap - changeStats *ingest.StatsChangeProcessor -} - -func (processor csvProcessor) ProcessChange(change ingest.Change) error { - csvWriter, ok := processor.files.get(change.Type) - if !ok { - return nil - } - if err := processor.changeStats.ProcessChange(context.Background(), change); err != nil { - return err - } - - legerExt, err := xdr.MarshalBase64(change.Post.Ext) - if err != nil { - return err - } - - switch change.Type { - case xdr.LedgerEntryTypeAccount: - account := change.Post.Data.MustAccount() - - inflationDest := "" - if account.InflationDest != nil { - inflationDest = account.InflationDest.Address() - } - - var signers string - if len(account.Signers) > 0 { - var err error - signers, err = xdr.MarshalBase64(account.Signers) - if err != nil { - return err - } - } - - accountExt, err := xdr.MarshalBase64(account.Ext) - if err != nil { - return err - } - - csvWriter.Write([]string{ - account.AccountId.Address(), - strconv.FormatInt(int64(account.Balance), 10), - strconv.FormatInt(int64(account.SeqNum), 10), - strconv.FormatInt(int64(account.NumSubEntries), 10), - inflationDest, - base64.StdEncoding.EncodeToString([]byte(account.HomeDomain)), - base64.StdEncoding.EncodeToString(account.Thresholds[:]), - strconv.FormatInt(int64(account.Flags), 10), - accountExt, - signers, - legerExt, - }) - case xdr.LedgerEntryTypeTrustline: - ledgerEntry, err := xdr.MarshalBase64(change.Post) - if err != nil { - return err - } - csvWriter.Write([]string{ - ledgerEntry, - }) - case xdr.LedgerEntryTypeOffer: - offer := change.Post.Data.MustOffer() - - selling, err := xdr.MarshalBase64(offer.Selling) - if err != nil { - return err - } - - buying, err := xdr.MarshalBase64(offer.Buying) - if err != nil { - return err - } - - offerExt, err := xdr.MarshalBase64(offer.Ext) - if err != nil { - return err - } - - csvWriter.Write([]string{ - offer.SellerId.Address(), - strconv.FormatInt(int64(offer.OfferId), 10), - selling, - buying, - strconv.FormatInt(int64(offer.Amount), 10), - strconv.FormatInt(int64(offer.Price.N), 10), - strconv.FormatInt(int64(offer.Price.D), 10), - strconv.FormatInt(int64(offer.Flags), 10), - offerExt, - legerExt, - }) - case xdr.LedgerEntryTypeData: - accountData := change.Post.Data.MustData() - accountDataExt, err := xdr.MarshalBase64(accountData.Ext) - if err != nil { - return err - } - - csvWriter.Write([]string{ - accountData.AccountId.Address(), - base64.StdEncoding.EncodeToString([]byte(accountData.DataName)), - base64.StdEncoding.EncodeToString(accountData.DataValue), - accountDataExt, - legerExt, - }) - case xdr.LedgerEntryTypeClaimableBalance: - claimableBalance := change.Post.Data.MustClaimableBalance() - - ledgerEntry, err := xdr.MarshalBase64(change.Post) - if err != nil { - return err - } - - balanceID, err := xdr.MarshalBase64(claimableBalance.BalanceId) - if err != nil { - return err - } - - csvWriter.Write([]string{ - balanceID, - ledgerEntry, - }) - case xdr.LedgerEntryTypeLiquidityPool: - ledgerEntry, err := xdr.MarshalBase64(change.Post) - if err != nil { - return err - } - csvWriter.Write([]string{ - ledgerEntry, - }) - default: - return errors.Errorf("Invalid LedgerEntryType: %d", change.Type) - } - - if err := csvWriter.Error(); err != nil { - return errors.Wrap(err, "Error during csv.Writer.Write") - } - - csvWriter.Flush() - - if err := csvWriter.Error(); err != nil { - return errors.Wrap(err, "Error during csv.Writer.Flush") - } - return nil -} - -func main() { - testnet := flag.Bool("testnet", false, "connect to the Stellar test network") - flag.Parse() - - archive, err := archive(*testnet) - if err != nil { - panic(err) - } - log.SetLevel(log.InfoLevel) - - files := newCSVMap() - defer files.close() - - for entryType, fileName := range map[xdr.LedgerEntryType]string{ - xdr.LedgerEntryTypeAccount: "./accounts.csv", - xdr.LedgerEntryTypeData: "./accountdata.csv", - xdr.LedgerEntryTypeOffer: "./offers.csv", - xdr.LedgerEntryTypeTrustline: "./trustlines.csv", - xdr.LedgerEntryTypeClaimableBalance: "./claimablebalances.csv", - xdr.LedgerEntryTypeLiquidityPool: "./pools.csv", - } { - if err = files.put(entryType, fileName); err != nil { - log.WithField("err", err). - WithField("file", fileName). - Fatal("cannot create csv file") - } - } - - ledgerSequenceString := os.Getenv("LATEST_LEDGER") - ledgerSequence, err := strconv.Atoi(ledgerSequenceString) - if err != nil { - log.WithField("ledger", ledgerSequenceString). - WithField("err", err). - Fatal("cannot parse latest ledger") - } - log.WithField("ledger", ledgerSequence). - Info("Processing entries from History Archive Snapshot") - - changeReader, err := ingest.NewCheckpointChangeReader( - context.Background(), - archive, - uint32(ledgerSequence), - ) - if err != nil { - log.WithField("err", err).Fatal("cannot construct change reader") - } - defer changeReader.Close() - - changeStats := &ingest.StatsChangeProcessor{} - doneStats := printPipelineStats(changeStats) - changeProcessor := csvProcessor{files: files, changeStats: changeStats} - logFatalError := func(err error) { - log.WithField("err", err).Fatal("could not process all changes from HAS") - } - for { - change, err := changeReader.Read() - if err == io.EOF { - break - } - if err != nil { - logFatalError(errors.Wrap(err, "could not read transaction")) - } - - if err = changeProcessor.ProcessChange(change); err != nil { - logFatalError(errors.Wrap(err, "could not process change")) - } - } - - // Remove sorted files - sortedFiles := []string{ - "./accounts_sorted.csv", - "./accountdata_sorted.csv", - "./offers_sorted.csv", - "./trustlines_sorted.csv", - "./claimablebalances_sort.csv", - } - for _, file := range sortedFiles { - err := os.Remove(file) - // Ignore not exist errors - if err != nil && !os.IsNotExist(err) { - panic(err) - } - } - - doneStats <- true -} - -func archive(testnet bool) (*historyarchive.Archive, error) { - if testnet { - return historyarchive.Connect( - "https://history.stellar.org/prd/core-testnet/core_testnet_001", - historyarchive.ArchiveOptions{ - ConnectOptions: storage.ConnectOptions{ - UserAgent: "dump-ledger-state", - }, - }, - ) - } - - return historyarchive.Connect( - "https://history.stellar.org/prd/core-live/core_live_001/", - historyarchive.ArchiveOptions{ - ConnectOptions: storage.ConnectOptions{ - UserAgent: "dump-ledger-state", - }, - }, - ) -} - -func printPipelineStats(reporter *ingest.StatsChangeProcessor) chan<- bool { - startTime := time.Now() - done := make(chan bool) - ticker := time.NewTicker(10 * time.Second) - - go func() { - defer ticker.Stop() - - for { - var m runtime.MemStats - runtime.ReadMemStats(&m) - results := reporter.GetResults() - stats := log.F(results.Map()) - stats["Alloc"] = bToMb(m.Alloc) - stats["HeapAlloc"] = bToMb(m.HeapAlloc) - stats["Sys"] = bToMb(m.Sys) - stats["NumGC"] = m.NumGC - stats["Goroutines"] = runtime.NumGoroutine() - stats["NumCPU"] = runtime.NumCPU() - stats["Duration"] = time.Since(startTime) - - log.WithFields(stats).Info("Current Job Status") - - select { - case <-ticker.C: - continue - case <-done: - // Pipeline done - return - } - } - }() - - return done -} - -func bToMb(b uint64) uint64 { - return b / 1024 / 1024 -} diff --git a/exp/tools/dump-ledger-state/run_test.sh b/exp/tools/dump-ledger-state/run_test.sh deleted file mode 100755 index ef2b56356c..0000000000 --- a/exp/tools/dump-ledger-state/run_test.sh +++ /dev/null @@ -1,39 +0,0 @@ -#! /bin/bash -set -e - -if [ -z ${LATEST_LEDGER+x} ]; then - # Get latest ledger - echo "Getting latest checkpoint ledger..." - if [ -z ${TESTNET+x} ]; then - export LATEST_LEDGER=`curl -s http://history.stellar.org/prd/core-live/core_live_001/.well-known/stellar-history.json | jq -r '.currentLedger'` - else - export LATEST_LEDGER=`curl -s http://history.stellar.org/prd/core-testnet/core_testnet_001/.well-known/stellar-history.json | jq -r '.currentLedger'` - fi - echo "Latest ledger: $LATEST_LEDGER" -fi - -# Dump state using Golang -if [ -z ${TESTNET+x} ]; then - echo "Dumping pubnet state using ingest..." - go run ./main.go -else - echo "Dumping testnet state using ingest..." - go run ./main.go --testnet -fi -echo "State dumped..." - -# Catchup core -if [ -z ${TESTNET+x} ]; then - echo "Catch up from pubnet" - stellar-core --conf ./stellar-core.cfg catchup $LATEST_LEDGER/1 -else - echo "Catch up from testnet" - stellar-core --conf ./stellar-core-testnet.cfg catchup $LATEST_LEDGER/1 -fi - -echo "Dumping state from stellar-core..." -./dump_core_db.sh -echo "State dumped..." - -echo "Comparing state dumps..." -./diff_test.sh diff --git a/exp/tools/dump-ledger-state/stellar-core-testnet.cfg b/exp/tools/dump-ledger-state/stellar-core-testnet.cfg deleted file mode 100644 index a02221e795..0000000000 --- a/exp/tools/dump-ledger-state/stellar-core-testnet.cfg +++ /dev/null @@ -1,39 +0,0 @@ -HTTP_PORT=11626 -PUBLIC_HTTP_PORT=true -LOG_FILE_PATH="" - -DATABASE="postgresql://dbname=core host=localhost user=circleci" -NETWORK_PASSPHRASE="Test SDF Network ; September 2015" -UNSAFE_QUORUM=true -FAILURE_SAFETY=1 -CATCHUP_RECENT=8640 - -EXPERIMENTAL_BUCKETLIST_DB=true - -[HISTORY.cache] -get="cp /opt/stellar/history-cache/{0} {1}" - -[[HOME_DOMAINS]] -HOME_DOMAIN="testnet.stellar.org" -QUALITY="HIGH" - -[[VALIDATORS]] -NAME="sdf_testnet_1" -HOME_DOMAIN="testnet.stellar.org" -PUBLIC_KEY="GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" -ADDRESS="core-testnet1.stellar.org" -HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_001/{0} -o {1}" - -[[VALIDATORS]] -NAME="sdf_testnet_2" -HOME_DOMAIN="testnet.stellar.org" -PUBLIC_KEY="GCUCJTIYXSOXKBSNFGNFWW5MUQ54HKRPGJUTQFJ5RQXZXNOLNXYDHRAP" -ADDRESS="core-testnet2.stellar.org" -HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_002/{0} -o {1}" - -[[VALIDATORS]] -NAME="sdf_testnet_3" -HOME_DOMAIN="testnet.stellar.org" -PUBLIC_KEY="GC2V2EFSXN6SQTWVYA5EPJPBWWIMSD2XQNKUOHGEKB535AQE2I6IXV2Z" -ADDRESS="core-testnet3.stellar.org" -HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_003/{0} -o {1}" \ No newline at end of file diff --git a/exp/tools/dump-ledger-state/stellar-core.cfg b/exp/tools/dump-ledger-state/stellar-core.cfg deleted file mode 100644 index 0d97346ce6..0000000000 --- a/exp/tools/dump-ledger-state/stellar-core.cfg +++ /dev/null @@ -1,201 +0,0 @@ -HTTP_PORT=11626 -PUBLIC_HTTP_PORT=true -LOG_FILE_PATH="" - -DATABASE="postgresql://dbname=core host=localhost user=circleci" -NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" -CATCHUP_RECENT=1 - -EXPERIMENTAL_BUCKETLIST_DB=true - -[HISTORY.cache] -get="cp /opt/stellar/history-cache/{0} {1}" - -[[HOME_DOMAINS]] -HOME_DOMAIN="publicnode.org" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="lobstr.co" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="www.franklintempleton.com" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="satoshipay.io" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="whalestack.com" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="www.stellar.org" -QUALITY="HIGH" - -[[HOME_DOMAINS]] -HOME_DOMAIN="stellar.blockdaemon.com" -QUALITY="HIGH" - -[[VALIDATORS]] -NAME="Boötes" -PUBLIC_KEY="GCVJ4Z6TI6Z2SOGENSPXDQ2U4RKH3CNQKYUHNSSPYFPNWTLGS6EBH7I2" -ADDRESS="bootes.publicnode.org:11625" -HISTORY="curl -sf https://bootes-history.publicnode.org/{0} -o {1}" -HOME_DOMAIN="publicnode.org" - -[[VALIDATORS]] -NAME="Lyra by BP Ventures" -PUBLIC_KEY="GCIXVKNFPKWVMKJKVK2V4NK7D4TC6W3BUMXSIJ365QUAXWBRPPJXIR2Z" -ADDRESS="lyra.publicnode.org:11625" -HISTORY="curl -sf https://lyra-history.publicnode.org/{0} -o {1}" -HOME_DOMAIN="publicnode.org" - -[[VALIDATORS]] -NAME="Hercules by OG Technologies" -PUBLIC_KEY="GBLJNN3AVZZPG2FYAYTYQKECNWTQYYUUY2KVFN2OUKZKBULXIXBZ4FCT" -ADDRESS="hercules.publicnode.org:11625" -HISTORY="curl -sf https://hercules-history.publicnode.org/{0} -o {1}" -HOME_DOMAIN="publicnode.org" - -[[VALIDATORS]] -NAME="LOBSTR 3 (North America)" -PUBLIC_KEY="GD5QWEVV4GZZTQP46BRXV5CUMMMLP4JTGFD7FWYJJWRL54CELY6JGQ63" -ADDRESS="v3.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v3.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="LOBSTR 1 (Europe)" -PUBLIC_KEY="GCFONE23AB7Y6C5YZOMKUKGETPIAJA4QOYLS5VNS4JHBGKRZCPYHDLW7" -ADDRESS="v1.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v1.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="LOBSTR 2 (Europe)" -PUBLIC_KEY="GCB2VSADESRV2DDTIVTFLBDI562K6KE3KMKILBHUHUWFXCUBHGQDI7VL" -ADDRESS="v2.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v2.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="LOBSTR 4 (Asia)" -PUBLIC_KEY="GA7TEPCBDQKI7JQLQ34ZURRMK44DVYCIGVXQQWNSWAEQR6KB4FMCBT7J" -ADDRESS="v4.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v4.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="LOBSTR 5 (India)" -PUBLIC_KEY="GA5STBMV6QDXFDGD62MEHLLHZTPDI77U3PFOD2SELU5RJDHQWBR5NNK7" -ADDRESS="v5.stellar.lobstr.co:11625" -HISTORY="curl -sf https://archive.v5.stellar.lobstr.co/{0} -o {1}" -HOME_DOMAIN="lobstr.co" - -[[VALIDATORS]] -NAME="FT SCV 2" -PUBLIC_KEY="GCMSM2VFZGRPTZKPH5OABHGH4F3AVS6XTNJXDGCZ3MKCOSUBH3FL6DOB" -ADDRESS="stellar2.franklintempleton.com:11625" -HISTORY="curl -sf https://stellar-history-usc.franklintempleton.com/azuscshf401/{0} -o {1}" -HOME_DOMAIN="www.franklintempleton.com" - -[[VALIDATORS]] -NAME="FT SCV 3" -PUBLIC_KEY="GA7DV63PBUUWNUFAF4GAZVXU2OZMYRATDLKTC7VTCG7AU4XUPN5VRX4A" -ADDRESS="stellar3.franklintempleton.com:11625" -HISTORY="curl -sf https://stellar-history-ins.franklintempleton.com/azinsshf401/{0} -o {1}" -HOME_DOMAIN="www.franklintempleton.com" - -[[VALIDATORS]] -NAME="FT SCV 1" -PUBLIC_KEY="GARYGQ5F2IJEBCZJCBNPWNWVDOFK7IBOHLJKKSG2TMHDQKEEC6P4PE4V" -ADDRESS="stellar1.franklintempleton.com:11625" -HISTORY="curl -sf https://stellar-history-usw.franklintempleton.com/azuswshf401/{0} -o {1}" -HOME_DOMAIN="www.franklintempleton.com" - -[[VALIDATORS]] -NAME="SatoshiPay Frankfurt" -PUBLIC_KEY="GC5SXLNAM3C4NMGK2PXK4R34B5GNZ47FYQ24ZIBFDFOCU6D4KBN4POAE" -ADDRESS="stellar-de-fra.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-de-fra.satoshipay.io/{0} -o {1}" -HOME_DOMAIN="satoshipay.io" - -[[VALIDATORS]] -NAME="SatoshiPay Singapore" -PUBLIC_KEY="GBJQUIXUO4XSNPAUT6ODLZUJRV2NPXYASKUBY4G5MYP3M47PCVI55MNT" -ADDRESS="stellar-sg-sin.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-sg-sin.satoshipay.io/{0} -o {1}" -HOME_DOMAIN="satoshipay.io" - -[[VALIDATORS]] -NAME="SatoshiPay Iowa" -PUBLIC_KEY="GAK6Z5UVGUVSEK6PEOCAYJISTT5EJBB34PN3NOLEQG2SUKXRVV2F6HZY" -ADDRESS="stellar-us-iowa.satoshipay.io:11625" -HISTORY="curl -sf https://stellar-history-us-iowa.satoshipay.io/{0} -o {1}" -HOME_DOMAIN="satoshipay.io" - -[[VALIDATORS]] -NAME="Whalestack (Germany)" -PUBLIC_KEY="GD6SZQV3WEJUH352NTVLKEV2JM2RH266VPEM7EH5QLLI7ZZAALMLNUVN" -ADDRESS="germany.stellar.whalestack.com:11625" -HISTORY="curl -sf https://germany.stellar.whalestack.com/history/{0} -o {1}" -HOME_DOMAIN="whalestack.com" - -[[VALIDATORS]] -NAME="Whalestack (Hong Kong)" -PUBLIC_KEY="GAZ437J46SCFPZEDLVGDMKZPLFO77XJ4QVAURSJVRZK2T5S7XUFHXI2Z" -ADDRESS="hongkong.stellar.whalestack.com:11625" -HISTORY="curl -sf https://hongkong.stellar.whalestack.com/history/{0} -o {1}" -HOME_DOMAIN="whalestack.com" - -[[VALIDATORS]] -NAME="Whalestack (Finland)" -PUBLIC_KEY="GADLA6BJK6VK33EM2IDQM37L5KGVCY5MSHSHVJA4SCNGNUIEOTCR6J5T" -ADDRESS="finland.stellar.whalestack.com:11625" -HISTORY="curl -sf https://finland.stellar.whalestack.com/history/{0} -o {1}" -HOME_DOMAIN="whalestack.com" - -[[VALIDATORS]] -NAME="SDF 2" -PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" -ADDRESS="core-live-b.stellar.org:11625" -HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" -HOME_DOMAIN="www.stellar.org" - -[[VALIDATORS]] -NAME="SDF 1" -PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" -ADDRESS="core-live-a.stellar.org:11625" -HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" -HOME_DOMAIN="www.stellar.org" - -[[VALIDATORS]] -NAME="SDF 3" -PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" -ADDRESS="core-live-c.stellar.org:11625" -HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" -HOME_DOMAIN="www.stellar.org" - -[[VALIDATORS]] -NAME="Blockdaemon Validator 3" -PUBLIC_KEY="GAYXZ4PZ7P6QOX7EBHPIZXNWY4KCOBYWJCA4WKWRKC7XIUS3UJPT6EZ4" -ADDRESS="stellar-full-validator3.bdnodes.net:11625" -HISTORY="curl -sf https://stellar-full-history3.bdnodes.net/{0} -o {1}" -HOME_DOMAIN="stellar.blockdaemon.com" - -[[VALIDATORS]] -NAME="Blockdaemon Validator 2" -PUBLIC_KEY="GAVXB7SBJRYHSG6KSQHY74N7JAFRL4PFVZCNWW2ARI6ZEKNBJSMSKW7C" -ADDRESS="stellar-full-validator2.bdnodes.net:11625" -HISTORY="curl -sf https://stellar-full-history2.bdnodes.net/{0} -o {1}" -HOME_DOMAIN="stellar.blockdaemon.com" - -[[VALIDATORS]] -NAME="Blockdaemon Validator 1" -PUBLIC_KEY="GAAV2GCVFLNN522ORUYFV33E76VPC22E72S75AQ6MBR5V45Z5DWVPWEU" -ADDRESS="stellar-full-validator1.bdnodes.net:11625" -HISTORY="curl -sf https://stellar-full-history1.bdnodes.net/{0} -o {1}" -HOME_DOMAIN="stellar.blockdaemon.com" \ No newline at end of file From a5e50c4cf3998fd6c9ba7e11cf25b1ebe24d368c Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 1 Jul 2024 20:13:55 +0100 Subject: [PATCH 204/234] Add flag to enable / disable reaping of lookup tables (#5366) --- services/horizon/internal/config.go | 2 ++ services/horizon/internal/flags.go | 8 ++++++++ services/horizon/internal/ingest/main.go | 4 ++-- services/horizon/internal/ingest/resume_state_test.go | 4 ++-- services/horizon/internal/init.go | 2 +- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/services/horizon/internal/config.go b/services/horizon/internal/config.go index 3414b1aaea..b0e1ecb9df 100644 --- a/services/horizon/internal/config.go +++ b/services/horizon/internal/config.go @@ -85,6 +85,8 @@ type Config struct { // If ReapFrequency is set to 2 history is reaped after ingesting every two ledgers. // etc... ReapFrequency uint + // ReapLookupTables enables the reaping of history lookup tables + ReapLookupTables bool // StaleThreshold represents the number of ledgers a history database may be // out-of-date by before horizon begins to respond with an error to history // requests. diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index 7321d07fb2..9930fee69f 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -687,6 +687,14 @@ func Flags() (*Config, support.ConfigOptions) { return nil }, }, + { + Name: "reap-lookup-tables", + ConfigKey: &config.ReapLookupTables, + OptType: types.Bool, + FlagDefault: true, + Usage: "enables the reaping of history lookup tables.", + UsedInCommands: IngestionCommands, + }, &support.ConfigOption{ Name: "history-stale-threshold", ConfigKey: &config.StaleThreshold, diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 5bff414ac3..650a08b426 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -95,7 +95,7 @@ type Config struct { HistoryArchiveCaching bool DisableStateVerification bool - EnableReapLookupTables bool + ReapLookupTables bool EnableExtendedLogLedgerStats bool MaxReingestRetries int @@ -757,7 +757,7 @@ func (s *system) maybeVerifyState(lastIngestedLedger uint32, expectedBucketListH } func (s *system) maybeReapLookupTables(lastIngestedLedger uint32) { - if !s.config.EnableReapLookupTables { + if !s.config.ReapLookupTables { return } diff --git a/services/horizon/internal/ingest/resume_state_test.go b/services/horizon/internal/ingest/resume_state_test.go index 5167eb195a..feb5e13bb0 100644 --- a/services/horizon/internal/ingest/resume_state_test.go +++ b/services/horizon/internal/ingest/resume_state_test.go @@ -402,9 +402,9 @@ func (s *ResumeTestTestSuite) TestReapingObjectsDisabled() { } func (s *ResumeTestTestSuite) TestErrorReapingObjectsIgnored() { - s.system.config.EnableReapLookupTables = true + s.system.config.ReapLookupTables = true defer func() { - s.system.config.EnableReapLookupTables = false + s.system.config.ReapLookupTables = false }() s.historyQ.On("Begin", s.ctx).Return(nil).Once() s.historyQ.On("GetLastLedgerIngest", s.ctx).Return(uint32(100), nil).Once() diff --git a/services/horizon/internal/init.go b/services/horizon/internal/init.go index 137ff26aff..c245970117 100644 --- a/services/horizon/internal/init.go +++ b/services/horizon/internal/init.go @@ -99,7 +99,7 @@ func initIngester(app *App) { DisableStateVerification: app.config.IngestDisableStateVerification, StateVerificationCheckpointFrequency: uint32(app.config.IngestStateVerificationCheckpointFrequency), StateVerificationTimeout: app.config.IngestStateVerificationTimeout, - EnableReapLookupTables: app.config.HistoryRetentionCount > 0, + ReapLookupTables: app.config.ReapLookupTables && app.config.HistoryRetentionCount > 0, EnableExtendedLogLedgerStats: app.config.IngestEnableExtendedLogLedgerStats, RoundingSlippageFilter: app.config.RoundingSlippageFilter, SkipTxmeta: app.config.SkipTxmeta, From 6eae5691367912a2bb6aa4cc246be947ebe87593 Mon Sep 17 00:00:00 2001 From: shawn Date: Tue, 2 Jul 2024 08:56:51 -0700 Subject: [PATCH 205/234] Port 2.31.0 release notes to mainline (#5365) --- services/horizon/CHANGELOG.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 6f127b9968..501ad51847 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -3,17 +3,31 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## Unreleased +## 2.31.0 ### Breaking Changes - Change ingestion filtering logic to store transactions if any filter matches on it. ([5303](https://github.com/stellar/go/pull/5303)) - - The previous behaviour was to store a tx only if both asset and account filters match together. So even if a tx matched an account filter but failed to match an asset filter, it would not be stored by Horizon. - + - The previous behaviour was to store a tx only if both asset and account filters match together. So even if a tx matched an account filter but failed to match an asset filter, it would not be stored by Horizon. - Captive-core configuration parameters updated to align with [stellar-core v21](https://github.com/stellar/stellar-core/issues/3811) ([5333](https://github.com/stellar/go/pull/5333)) - - BucketlistDB is now the default database for stellar-core, deprecating `EXPERIMENTAL_BUCKETLIST_DB`. - - A new mandatory parameter `DEPRECATED_SQL_LEDGER_STATE` (default: false) is required by stellar-core on its configuration toml file. if the toml provided by `CAPTIVE_CORE_CONFIG_PATH` does not have this new setting, Horizon will add it automatically, therefore, no action required. - - If using `EXPERIMENTAL_BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT` or `EXPERIMENTAL_BUCKETLIST_DB_INDEX_CUTOFF`, they have been renamed to `BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT` and `BUCKETLIST_DB_INDEX_CUTOFF` respectively. + - BucketlistDB is now the default database for stellar-core, deprecating the usage of `EXPERIMENTAL_BUCKETLIST_DB` in captive core configuration toml. + - A new mandatory parameter `DEPRECATED_SQL_LEDGER_STATE` (default: false) is required by stellar-core on its captive core configuration toml file. if the toml provided by `CAPTIVE_CORE_CONFIG_PATH` does not have this new setting, Horizon will add it automatically, therefore, no action required. + - If using `EXPERIMENTAL_BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT` or `EXPERIMENTAL_BUCKETLIST_DB_INDEX_CUTOFF` in captive core configuration toml, they must be renamed to `BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT` and `BUCKETLIST_DB_INDEX_CUTOFF` respectively. + +### Added + +- Bump XDR definitions ([5289](https://github.com/stellar/go/pull/5289)), ([5330](https://github.com/stellar/go/pull/5330)) +- Add new async transaction submission endpoint ([5188](https://github.com/stellar/go/pull/5188)) +- Add `horizon_ingest_errors_total` metric key ([5302](https://github.com/stellar/go/pull/5302)) +- Add transaction hash to txsub timeout response ([5328](https://github.com/stellar/go/pull/5328)) +- Add new captive-core flags for V1 Meta ([5309](https://github.com/stellar/go/pull/5309)) +- Add version check for protocol 21 ([5346](https://github.com/stellar/go/pull/5346)) +- Improve horizon history reaper ([5331](https://github.com/stellar/go/pull/5331)). New reaper configuration flags `REAP_FREQUENCY` - the frequency in units of ledgers for how often history is reaped. + +### Fixed + +- Fix the following ingestion error: `error preparing range: error starting prepare range: the previous Stellar-Core instance is still running` ([5307](https://github.com/stellar/go/pull/5307)) + ## 2.30.0 **This release adds support for Protocol 21** From 188558412d74c122f9cbc6f76ff575d12f9a396d Mon Sep 17 00:00:00 2001 From: urvisavla Date: Tue, 2 Jul 2024 11:58:26 -0700 Subject: [PATCH 206/234] exp/services/ledgerexporter: create CI workflow for ledger exporter release (#5368) --- .github/workflows/horizon.yml | 27 ---------------- .github/workflows/ledgerexporter-release.yml | 33 ++++++++++++++++++++ .github/workflows/ledgerexporter.yml | 24 ++++++++++++++ exp/services/ledgerexporter/Makefile | 2 +- 4 files changed, 58 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/ledgerexporter-release.yml create mode 100644 .github/workflows/ledgerexporter.yml diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index 3ea92c8b17..cc3cf644f3 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -149,30 +149,3 @@ jobs: name: Push to DockerHub run: docker push stellar/horizon-verify-range:latest - ledger-exporter: - name: Test and push the Ledger Exporter images - runs-on: ubuntu-latest - env: - STELLAR_CORE_VERSION: 21.0.0-1872.c6f474133.focal - steps: - - uses: actions/checkout@v3 - with: - # For pull requests, build and test the PR head not a merge of the PR with the destination. - ref: ${{ github.event.pull_request.head.sha || github.ref }} - - name: Build Ledger Exporter docker - run: make -C exp/services/ledgerexporter docker-build - - - name: Run Ledger Exporter test - run: make -C exp/services/ledgerexporter docker-test - - # Push images - - if: github.ref == 'refs/heads/master' - name: Login to DockerHub - uses: docker/login-action@bb984efc561711aaa26e433c32c3521176eae55b - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - if: github.ref == 'refs/heads/master' - name: Push to DockerHub - run: make -C exp/services/ledgerexporter docker-push diff --git a/.github/workflows/ledgerexporter-release.yml b/.github/workflows/ledgerexporter-release.yml new file mode 100644 index 0000000000..9b23695c4a --- /dev/null +++ b/.github/workflows/ledgerexporter-release.yml @@ -0,0 +1,33 @@ +name: Ledger Exporter release + +on: + push: + tags: ['ledgerexporter-v*'] + +jobs: + + publish-docker: + name: Test and push the Ledger Exporter images + runs-on: ubuntu-latest + env: + STELLAR_CORE_VERSION: 21.1.0-1921.b3aeb14cc.focal + VERSION: ${GITHUB_REF_NAME#ledgerexporter-v} + steps: + - uses: actions/checkout@v3 + with: + ref: github.sha + - name: Build Ledger Exporter docker + run: make -C exp/services/ledgerexporter docker-build + + - name: Run Ledger Exporter test + run: make -C exp/services/ledgerexporter docker-test + + # Push images + - name: Login to DockerHub + uses: docker/login-action@bb984efc561711aaa26e433c32c3521176eae55b + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Push to DockerHub + run: make -C exp/services/ledgerexporter docker-push diff --git a/.github/workflows/ledgerexporter.yml b/.github/workflows/ledgerexporter.yml new file mode 100644 index 0000000000..63ae5e8128 --- /dev/null +++ b/.github/workflows/ledgerexporter.yml @@ -0,0 +1,24 @@ +name: LedgerExporter + +on: + push: + branches: [master] + pull_request: + +jobs: + ledger-exporter: + name: Build and test Ledger Exporter image + runs-on: ubuntu-latest + env: + STELLAR_CORE_VERSION: 21.1.0-1921.b3aeb14cc.focal + steps: + - uses: actions/checkout@v3 + with: + # For pull requests, build and test the PR head not a merge of the PR with the destination. + ref: ${{ github.event.pull_request.head.sha || github.ref }} + - name: Build Ledger Exporter docker + run: make -C exp/services/ledgerexporter docker-build + + - name: Run Ledger Exporter test + run: make -C exp/services/ledgerexporter docker-test + diff --git a/exp/services/ledgerexporter/Makefile b/exp/services/ledgerexporter/Makefile index 971fc3eb25..0d90a282c3 100644 --- a/exp/services/ledgerexporter/Makefile +++ b/exp/services/ledgerexporter/Makefile @@ -2,7 +2,7 @@ SUDO := $(shell docker version >/dev/null 2>&1 || echo "sudo") # https://github.com/opencontainers/image-spec/blob/master/annotations.md BUILD_DATE := $(shell date -u +%FT%TZ) -VERSION ?= 1.0.0-$(shell git rev-parse --short HEAD) +VERSION ?= $(shell git rev-parse --short HEAD) DOCKER_IMAGE := stellar/ledger-exporter docker-build: From 67f77f3b824d0d03dfeb80fd997ce6d62db4fae2 Mon Sep 17 00:00:00 2001 From: shawn Date: Mon, 8 Jul 2024 09:15:43 -0700 Subject: [PATCH 207/234] exp/services/ledgerexporter: create go integration tests for sub commands (#5370) --- .github/workflows/horizon.yml | 1 - .github/workflows/ledgerexporter-release.yml | 31 +- .github/workflows/ledgerexporter.yml | 35 +- .../ledgerexporter/DEVELOPER_GUIDE.md | 21 +- exp/services/ledgerexporter/Makefile | 2 +- exp/services/ledgerexporter/README.md | 3 +- exp/services/ledgerexporter/internal/app.go | 2 +- .../ledgerexporter/internal/config.go | 14 +- .../internal/integration_test.go | 349 ++++++++++++++++++ exp/services/ledgerexporter/internal/main.go | 22 +- .../ledgerexporter/internal/main_test.go | 11 +- .../test/integration_captive_core.cfg | 21 ++ .../test/integration_config_template.toml | 15 + go.mod | 72 ++-- go.sum | 174 +++++---- 15 files changed, 647 insertions(+), 126 deletions(-) create mode 100644 exp/services/ledgerexporter/internal/integration_test.go create mode 100644 exp/services/ledgerexporter/internal/test/integration_captive_core.cfg create mode 100644 exp/services/ledgerexporter/internal/test/integration_config_template.toml diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index cc3cf644f3..117e11bb6f 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -148,4 +148,3 @@ jobs: - if: github.ref == 'refs/heads/master' name: Push to DockerHub run: docker push stellar/horizon-verify-range:latest - diff --git a/.github/workflows/ledgerexporter-release.yml b/.github/workflows/ledgerexporter-release.yml index 9b23695c4a..2e66847286 100644 --- a/.github/workflows/ledgerexporter-release.yml +++ b/.github/workflows/ledgerexporter-release.yml @@ -10,17 +10,40 @@ jobs: name: Test and push the Ledger Exporter images runs-on: ubuntu-latest env: + LEDGEREXPORTER_INTEGRATION_TESTS_ENABLED: "true" + LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN: /usr/bin/stellar-core + # this pins to a version of quickstart:testing that has the same version as STELLAR_CORE_VERSION + # this is the multi-arch index sha, get it by 'docker buildx imagetools inspect stellar/quickstart:testing' + LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE: docker.io/stellar/quickstart:testing@sha256:03c6679f838a92b1eda4cd3a9e2bdee4c3586e278a138a0acf36a9bc99a0041f + LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL: "false" STELLAR_CORE_VERSION: 21.1.0-1921.b3aeb14cc.focal - VERSION: ${GITHUB_REF_NAME#ledgerexporter-v} + VERSION: ${GITHUB_REF_NAME#ledgerexporter-v} steps: - uses: actions/checkout@v3 with: ref: github.sha - - name: Build Ledger Exporter docker - run: make -C exp/services/ledgerexporter docker-build + - name: Pull Quickstart image + shell: bash + run: | + docker pull "$LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE" + - name: Install captive core + run: | + # Workaround for https://github.com/actions/virtual-environments/issues/5245, + # libc++1-8 won't be installed if another version is installed (but apt won't give you a helpul + # message about why the installation fails) + sudo apt list --installed | grep libc++ + sudo apt-get remove -y libc++1-* libc++abi1-* || true + + sudo wget -qO - https://apt.stellar.org/SDF.asc | APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=true sudo apt-key add - + sudo bash -c 'echo "deb https://apt.stellar.org focal unstable" > /etc/apt/sources.list.d/SDF-unstable.list' + sudo apt-get update && sudo apt-get install -y stellar-core="$STELLAR_CORE_VERSION" + echo "Using stellar core version $(stellar-core version)" - name: Run Ledger Exporter test - run: make -C exp/services/ledgerexporter docker-test + run: go test -v -race -run TestLedgerExporterTestSuite ./exp/services/ledgerexporter/... + + - name: Build Ledger Exporter docker + run: make -C exp/services/ledgerexporter docker-build # Push images - name: Login to DockerHub diff --git a/.github/workflows/ledgerexporter.yml b/.github/workflows/ledgerexporter.yml index 63ae5e8128..c80a367771 100644 --- a/.github/workflows/ledgerexporter.yml +++ b/.github/workflows/ledgerexporter.yml @@ -7,18 +7,39 @@ on: jobs: ledger-exporter: - name: Build and test Ledger Exporter image + name: Test Ledger Exporter runs-on: ubuntu-latest env: - STELLAR_CORE_VERSION: 21.1.0-1921.b3aeb14cc.focal + CAPTIVE_CORE_DEBIAN_PKG_VERSION: 21.1.0-1921.b3aeb14cc.focal + LEDGEREXPORTER_INTEGRATION_TESTS_ENABLED: "true" + LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN: /usr/bin/stellar-core + # this pins to a version of quickstart:testing that has the same version as LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN + # this is the multi-arch index sha, get it by 'docker buildx imagetools inspect stellar/quickstart:testing' + LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE: docker.io/stellar/quickstart:testing@sha256:03c6679f838a92b1eda4cd3a9e2bdee4c3586e278a138a0acf36a9bc99a0041f + LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL: "false" steps: + - name: Install captive core + run: | + # Workaround for https://github.com/actions/virtual-environments/issues/5245, + # libc++1-8 won't be installed if another version is installed (but apt won't give you a helpul + # message about why the installation fails) + sudo apt list --installed | grep libc++ + sudo apt-get remove -y libc++1-* libc++abi1-* || true + + sudo wget -qO - https://apt.stellar.org/SDF.asc | APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=true sudo apt-key add - + sudo bash -c 'echo "deb https://apt.stellar.org focal unstable" > /etc/apt/sources.list.d/SDF-unstable.list' + sudo apt-get update && sudo apt-get install -y stellar-core="$CAPTIVE_CORE_DEBIAN_PKG_VERSION" + echo "Using stellar core version $(stellar-core version)" + + - name: Pull Quickstart image + shell: bash + run: | + docker pull "$LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE" + - uses: actions/checkout@v3 with: # For pull requests, build and test the PR head not a merge of the PR with the destination. ref: ${{ github.event.pull_request.head.sha || github.ref }} - - name: Build Ledger Exporter docker - run: make -C exp/services/ledgerexporter docker-build - + - name: Run Ledger Exporter test - run: make -C exp/services/ledgerexporter docker-test - + run: go test -v -race -run TestLedgerExporterTestSuite ./exp/services/ledgerexporter/... diff --git a/exp/services/ledgerexporter/DEVELOPER_GUIDE.md b/exp/services/ledgerexporter/DEVELOPER_GUIDE.md index ef81553e37..28a16ec1b0 100644 --- a/exp/services/ledgerexporter/DEVELOPER_GUIDE.md +++ b/exp/services/ledgerexporter/DEVELOPER_GUIDE.md @@ -32,14 +32,31 @@ To achieve its goals, the ledger exporter uses the following architecture, which - An example implementation of `DataStore` for GCS, Google Cloud Storage. This plugin is located in the [support](https://github.com/stellar/go/tree/master/support/datastore) package. - The ledger exporter currently implements the interface only for Google Cloud Storage (GCS). The [GCS plugin](https://github.com/stellar/go/blob/master/support/datastore/gcs_datastore.go) uses GCS-specific behaviors like conditional puts, automatic retry, metadata, and CRC checksum. -## Build, Run and Test using Docker +## Build and Run using Docker The Dockerfile contains all the necessary dependencies (e.g., Stellar-core) required to run the ledger exporter. - Build: To build the Docker container, use the provided [Makefile](./Makefile). Simply run make `make docker-build` to build a new container after making any changes. - Run: For instructions on running the Docker container, refer to the [Installation Guide](./README.md). -- Test: To test the Docker container, refer to the [docker-test](./Makefile) command for an example of how to use the [GCS emulator](https://github.com/fsouza/fake-gcs-server) for local testing. +- Run ledgerexporter with a local, fake GCS backend: Requires `make docker-build` first, then run `make docker-test-fake-gcs`. This will run the ledger exporter against `testnet` and export to the 'fake' GCS instance started in the container. + +## Running Integration Tests: +from top directory of stellar/go repo, run go test to launch ledger exporter integration +tests. + +`LEDGEREXPORTER_INTEGRATION_TESTS_ENABLED=true` is required environment variable to allow +tests to run. + +Optional, tests will try to run `stellar-core` from o/s PATH for captive core, if not resolvable, then set `LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN=/path/to/stellar-core` + +Optional, can override the version of quickstart used to run standalone stellar network, `LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE=docker.io/stellar/quickstart:`. By default it will try to docker pull `stellar/quickstart:testing` image to local host's docker image store. Set `LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL=false` to skip the pull, if you know host has up to date image. + +Note, the version of stellar core in `LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE` and `LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN` needs to be on the same major rev or the captive core process may not be able to join or parse ledger meta from the `local` network created by `LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE` + +``` +$ LEDGEREXPORTER_INTEGRATION_TESTS_ENABLED=true go test -v -race -run TestLedgerExporterTestSuite ./exp/services/ledgerexporter/... +``` ## Adding support for a new storage type Support for different data storage types are encapsulated as 'plugins', which are implementation of `DataStore` interface in a go package. To add a data storage plugin based on a new storage type (e.g. AWS S3), follow these steps: diff --git a/exp/services/ledgerexporter/Makefile b/exp/services/ledgerexporter/Makefile index 0d90a282c3..6561c4f24c 100644 --- a/exp/services/ledgerexporter/Makefile +++ b/exp/services/ledgerexporter/Makefile @@ -20,7 +20,7 @@ docker-clean: $(SUDO) rm -rf ${PWD}/storage || true $(SUDO) docker network rm test-network || true -docker-test: docker-clean +docker-test-fake-gcs: docker-clean # Create temp storage dir $(SUDO) mkdir -p ${PWD}/storage/exporter-test diff --git a/exp/services/ledgerexporter/README.md b/exp/services/ledgerexporter/README.md index 008981b551..8308bfdaeb 100644 --- a/exp/services/ledgerexporter/README.md +++ b/exp/services/ledgerexporter/README.md @@ -126,5 +126,4 @@ docker run --platform linux/amd64 -d \ Arguments: - `--start ` (required): The starting ledger sequence number in the range to export. - `--end ` (required): The ending ledger sequence number in the range. -- `--config-file ` (optional): The path to your configuration file, containing details like GCS bucket information. If not provided, the exporter will look for config.toml in the directory where you run the command. - +- `--config-file ` (optional): The path to your configuration file, containing details like GCS bucket information. If not provided, the exporter will look for config.toml in the directory where you run the command. \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/app.go b/exp/services/ledgerexporter/internal/app.go index 40cdf90f0e..00382f00e4 100644 --- a/exp/services/ledgerexporter/internal/app.go +++ b/exp/services/ledgerexporter/internal/app.go @@ -186,7 +186,7 @@ func newAdminServer(adminPort int, prometheusRegistry *prometheus.Registry) *htt } func (a *App) Run(runtimeSettings RuntimeSettings) error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(runtimeSettings.Ctx) defer cancel() if err := a.init(ctx, runtimeSettings); err != nil { diff --git a/exp/services/ledgerexporter/internal/config.go b/exp/services/ledgerexporter/internal/config.go index 013a3ef8d7..196327e229 100644 --- a/exp/services/ledgerexporter/internal/config.go +++ b/exp/services/ledgerexporter/internal/config.go @@ -48,6 +48,7 @@ type RuntimeSettings struct { EndLedger uint32 ConfigFilePath string Mode Mode + Ctx context.Context } type StellarCoreConfig struct { @@ -56,6 +57,8 @@ type StellarCoreConfig struct { HistoryArchiveUrls []string `toml:"history_archive_urls"` StellarCoreBinaryPath string `toml:"stellar_core_binary_path"` CaptiveCoreTomlPath string `toml:"captive_core_toml_path"` + CheckpointFrequency uint32 `toml:"checkpoint_frequency"` + StoragePath string `toml:"storage_path"` } type Config struct { @@ -98,8 +101,8 @@ func NewConfig(settings RuntimeSettings, getCoreVersionFn ledgerbackend.CoreBuil } logger.Infof("Network Config Archive URLs: %v", config.StellarCoreConfig.HistoryArchiveUrls) logger.Infof("Network Config Archive Passphrase: %v", config.StellarCoreConfig.NetworkPassphrase) - logger.Infof("Network Config Archive Stellar Core Binary Path: %v", config.StellarCoreConfig.StellarCoreBinaryPath) - logger.Infof("Network Config Archive Stellar Core Toml Config: %v", string(config.SerializedCaptiveCoreToml)) + logger.Infof("Network Config Stellar Core Binary Path: %v", config.StellarCoreConfig.StellarCoreBinaryPath) + logger.Infof("Network Config Stellar Core Toml Config: %v", string(config.SerializedCaptiveCoreToml)) return config, nil } @@ -185,15 +188,20 @@ func (config *Config) GenerateCaptiveCoreConfig(coreBinFromPath string) (ledgerb return ledgerbackend.CaptiveCoreConfig{}, errors.Wrap(err, "Failed to create captive-core toml") } + checkpointFrequency := historyarchive.DefaultCheckpointFrequency + if config.StellarCoreConfig.CheckpointFrequency > 0 { + checkpointFrequency = config.StellarCoreConfig.CheckpointFrequency + } return ledgerbackend.CaptiveCoreConfig{ BinaryPath: config.StellarCoreConfig.StellarCoreBinaryPath, NetworkPassphrase: params.NetworkPassphrase, HistoryArchiveURLs: params.HistoryArchiveURLs, - CheckpointFrequency: historyarchive.DefaultCheckpointFrequency, + CheckpointFrequency: checkpointFrequency, Log: logger.WithField("subservice", "stellar-core"), Toml: captiveCoreToml, UserAgent: "ledger-exporter", UseDB: true, + StoragePath: config.StellarCoreConfig.StoragePath, }, nil } diff --git a/exp/services/ledgerexporter/internal/integration_test.go b/exp/services/ledgerexporter/internal/integration_test.go new file mode 100644 index 0000000000..dab2e5b5f8 --- /dev/null +++ b/exp/services/ledgerexporter/internal/integration_test.go @@ -0,0 +1,349 @@ +package ledgerexporter + +import ( + "bytes" + "context" + "io" + "os" + "os/signal" + "path/filepath" + "testing" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/stdcopy" + "github.com/docker/go-connections/nat" + "github.com/pkg/errors" + + "github.com/pelletier/go-toml" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + + "github.com/fsouza/fake-gcs-server/fakestorage" + "github.com/stellar/go/historyarchive" + "github.com/stellar/go/support/datastore" + "github.com/stellar/go/support/storage" +) + +const ( + maxWaitForCoreStartup = (180 * time.Second) + coreStartupPingInterval = time.Second + // set the max ledger we want the standalone network to emit + // tests then refer to ledger sequences only up to this, therefore + // don't have to do complex waiting within test for a sequence to exist. + waitForCoreLedgerSequence = 16 +) + +func TestLedgerExporterTestSuite(t *testing.T) { + if os.Getenv("LEDGEREXPORTER_INTEGRATION_TESTS_ENABLED") != "true" { + t.Skip("skipping integration test: LEDGEREXPORTER_INTEGRATION_TESTS_ENABLED not true") + } + + ledgerExporterSuite := &LedgerExporterTestSuite{} + suite.Run(t, ledgerExporterSuite) +} + +type LedgerExporterTestSuite struct { + suite.Suite + tempConfigFile string + ctx context.Context + ctxStop context.CancelFunc + coreContainerID string + dockerCli *client.Client + gcsServer *fakestorage.Server + finishedSetup bool +} + +func (s *LedgerExporterTestSuite) TestScanAndFill() { + require := s.Require() + + rootCmd := defineCommands() + + rootCmd.SetArgs([]string{"scan-and-fill", "--start", "4", "--end", "5", "--config-file", s.tempConfigFile}) + var errWriter bytes.Buffer + var outWriter bytes.Buffer + rootCmd.SetErr(&errWriter) + rootCmd.SetOut(&outWriter) + err := rootCmd.ExecuteContext(s.ctx) + require.NoError(err) + + output := outWriter.String() + errOutput := errWriter.String() + s.T().Log(output) + s.T().Log(errOutput) + + datastore, err := datastore.NewGCSDataStore(s.ctx, "integration-test/standalone") + require.NoError(err) + + _, err = datastore.GetFile(s.ctx, "FFFFFFFF--0-9/FFFFFFFA--5.xdr.zstd") + require.NoError(err) +} + +func (s *LedgerExporterTestSuite) TestAppend() { + require := s.Require() + + // first populate ledgers 4-5 + rootCmd := defineCommands() + rootCmd.SetArgs([]string{"scan-and-fill", "--start", "6", "--end", "7", "--config-file", s.tempConfigFile}) + err := rootCmd.ExecuteContext(s.ctx) + require.NoError(err) + + // now run an append of overalapping range, it will resume past existing ledgers + rootCmd.SetArgs([]string{"append", "--start", "6", "--end", "9", "--config-file", s.tempConfigFile}) + var errWriter bytes.Buffer + var outWriter bytes.Buffer + rootCmd.SetErr(&errWriter) + rootCmd.SetOut(&outWriter) + err = rootCmd.ExecuteContext(s.ctx) + require.NoError(err) + + output := outWriter.String() + errOutput := errWriter.String() + s.T().Log(output) + s.T().Log(errOutput) + + datastore, err := datastore.NewGCSDataStore(s.ctx, "integration-test/standalone") + require.NoError(err) + + _, err = datastore.GetFile(s.ctx, "FFFFFFFF--0-9/FFFFFFF6--9.xdr.zstd") + require.NoError(err) +} + +func (s *LedgerExporterTestSuite) TestAppendUnbounded() { + require := s.Require() + + rootCmd := defineCommands() + rootCmd.SetArgs([]string{"append", "--start", "10", "--config-file", s.tempConfigFile}) + var errWriter bytes.Buffer + var outWriter bytes.Buffer + rootCmd.SetErr(&errWriter) + rootCmd.SetOut(&outWriter) + + appendCtx, cancel := context.WithCancel(s.ctx) + syn := make(chan struct{}) + defer func() { <-syn }() + defer cancel() + go func() { + defer close(syn) + require.NoError(rootCmd.ExecuteContext(appendCtx)) + output := outWriter.String() + errOutput := errWriter.String() + s.T().Log(output) + s.T().Log(errOutput) + }() + + datastore, err := datastore.NewGCSDataStore(s.ctx, "integration-test/standalone") + require.NoError(err) + + require.EventuallyWithT(func(c *assert.CollectT) { + // this checks every 50ms up to 180s total + assert := assert.New(c) + _, err = datastore.GetFile(s.ctx, "FFFFFFF5--10-19/FFFFFFF0--15.xdr.zstd") + assert.NoError(err) + }, 180*time.Second, 50*time.Millisecond, "append unbounded did not work") +} + +func (s *LedgerExporterTestSuite) SetupSuite() { + var err error + t := s.T() + + s.ctx, s.ctxStop = signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) + + defer func() { + if !s.finishedSetup { + s.TearDownSuite() + } + }() + testTempDir := t.TempDir() + + ledgerExporterConfigTemplate, err := toml.LoadFile("test/integration_config_template.toml") + if err != nil { + t.Fatalf("unable to load config template file %v", err) + } + + // if LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN not specified, + // ledgerexporter will attempt resolve core bin using 'stellar-core' from OS path + ledgerExporterConfigTemplate.Set("stellar_core_config.stellar_core_binary_path", + os.Getenv("LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN")) + + ledgerExporterConfigTemplate.Set("stellar_core_config.storage_path", filepath.Join(testTempDir, "captive-core")) + + tomlBytes, err := toml.Marshal(ledgerExporterConfigTemplate) + if err != nil { + t.Fatalf("unable to load config file %v", err) + } + + tempSeedDataPath := filepath.Join(testTempDir, "data") + if err = os.MkdirAll(filepath.Join(tempSeedDataPath, "integration-test"), 0777); err != nil { + t.Fatalf("unable to create seed data in temp path, %v", err) + } + + s.tempConfigFile = filepath.Join(testTempDir, "config.toml") + err = os.WriteFile(s.tempConfigFile, tomlBytes, 0777) + if err != nil { + t.Fatalf("unable to write temp config file %v, %v", s.tempConfigFile, err) + } + + testWriter := &testWriter{test: t} + opts := fakestorage.Options{ + Scheme: "http", + Host: "127.0.0.1", + Port: uint16(0), + Writer: testWriter, + Seed: tempSeedDataPath, + StorageRoot: filepath.Join(testTempDir, "bucket"), + PublicHost: "127.0.0.1", + } + + s.gcsServer, err = fakestorage.NewServerWithOptions(opts) + + if err != nil { + t.Fatalf("couldn't start the fake gcs http server %v", err) + } + + t.Logf("fake gcs server started at %v", s.gcsServer.URL()) + t.Setenv("STORAGE_EMULATOR_HOST", s.gcsServer.URL()) + + quickstartImage := os.Getenv("LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE") + if quickstartImage == "" { + quickstartImage = "stellar/quickstart:testing" + } + pullQuickStartImage := true + if os.Getenv("LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL") == "false" { + pullQuickStartImage = false + } + + s.mustStartCore(t, quickstartImage, pullQuickStartImage) + s.mustWaitForCore(t, ledgerExporterConfigTemplate.GetArray("stellar_core_config.history_archive_urls").([]string), + ledgerExporterConfigTemplate.Get("stellar_core_config.network_passphrase").(string)) + s.finishedSetup = true +} + +func (s *LedgerExporterTestSuite) TearDownSuite() { + if s.coreContainerID != "" { + s.T().Logf("Stopping the quickstart container %v", s.coreContainerID) + containerLogs, err := s.dockerCli.ContainerLogs(s.ctx, s.coreContainerID, container.LogsOptions{ShowStdout: true, ShowStderr: true}) + + if err == nil { + var errWriter bytes.Buffer + var outWriter bytes.Buffer + stdcopy.StdCopy(&outWriter, &errWriter, containerLogs) + s.T().Log(outWriter.String()) + s.T().Log(errWriter.String()) + } + if err := s.dockerCli.ContainerStop(context.Background(), s.coreContainerID, container.StopOptions{}); err != nil { + s.T().Logf("unable to stop core container, %v, %v", s.coreContainerID, err) + } + } + if s.dockerCli != nil { + s.dockerCli.Close() + } + if s.gcsServer != nil { + s.gcsServer.Stop() + } + s.ctxStop() +} + +func (s *LedgerExporterTestSuite) mustStartCore(t *testing.T, quickstartImage string, pullImage bool) { + var err error + s.dockerCli, err = client.NewClientWithOpts(client.WithAPIVersionNegotiation()) + if err != nil { + t.Fatalf("could not create docker client, %v", err) + } + + if pullImage { + imgReader, imgErr := s.dockerCli.ImagePull(s.ctx, quickstartImage, image.PullOptions{}) + if imgErr != nil { + t.Fatalf("could not pull docker image, %v, %v", quickstartImage, imgErr) + } + // ImagePull is asynchronous. + // The reader needs to be read completely for the pull operation to complete. + _, err = io.Copy(io.Discard, imgReader) + if err != nil { + t.Fatalf("could not pull docker image, %v, %v", quickstartImage, err) + } + + err = imgReader.Close() + if err != nil { + t.Fatalf("could not download all of docker image bytes after pull, %v, %v", quickstartImage, err) + } + } + + resp, err := s.dockerCli.ContainerCreate(s.ctx, + &container.Config{ + Image: quickstartImage, + // only run tge core service(no horizon, rpc, etc) and don't spend any time upgrading + // the core with newer soroban limits + Cmd: []string{"--enable", "core,,", "--limits", "default", "--local"}, + ExposedPorts: nat.PortSet{ + nat.Port("1570/tcp"): {}, + nat.Port("11625/tcp"): {}, + }, + }, + + &container.HostConfig{ + PortBindings: nat.PortMap{ + nat.Port("1570/tcp"): {nat.PortBinding{HostIP: "127.0.0.1", HostPort: "1570"}}, + nat.Port("11625/tcp"): {nat.PortBinding{HostIP: "127.0.0.1", HostPort: "11625"}}, + }, + AutoRemove: true, + }, + nil, nil, "") + + if err != nil { + t.Fatalf("could not create quickstart docker container, %v, error %v", quickstartImage, err) + } + s.coreContainerID = resp.ID + + if err := s.dockerCli.ContainerStart(s.ctx, resp.ID, container.StartOptions{}); err != nil { + t.Fatalf("could not run quickstart docker container, %v, error %v", quickstartImage, err) + } + t.Logf("Started quickstart container %v", s.coreContainerID) +} + +func (s *LedgerExporterTestSuite) mustWaitForCore(t *testing.T, archiveUrls []string, passphrase string) { + t.Log("Waiting for core to be up...") + startTime := time.Now() + infoTime := startTime + archive, err := historyarchive.NewArchivePool(archiveUrls, historyarchive.ArchiveOptions{ + NetworkPassphrase: passphrase, + // due to ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING that is done by quickstart's local network + CheckpointFrequency: 8, + ConnectOptions: storage.ConnectOptions{ + Context: s.ctx, + }, + }) + if err != nil { + t.Fatalf("unable to create archive pool against core, %v", err) + } + for time.Since(startTime) < maxWaitForCoreStartup { + if durationSince := time.Since(infoTime); durationSince < coreStartupPingInterval { + time.Sleep(coreStartupPingInterval - durationSince) + } + infoTime = time.Now() + has, requestErr := archive.GetRootHAS() + if errors.Is(requestErr, context.Canceled) { + break + } + if requestErr != nil { + t.Logf("request to fetch checkpoint failed: %v", requestErr) + continue + } + latestCheckpoint := has.CurrentLedger + if latestCheckpoint >= waitForCoreLedgerSequence { + return + } + } + t.Fatalf("core did not progress ledgers within %v seconds", maxWaitForCoreStartup) +} + +type testWriter struct { + test *testing.T +} + +func (w *testWriter) Write(p []byte) (n int, err error) { + w.test.Log(string(p)) + return len(p), nil +} diff --git a/exp/services/ledgerexporter/internal/main.go b/exp/services/ledgerexporter/internal/main.go index d1409eb89c..f0ff076ae3 100644 --- a/exp/services/ledgerexporter/internal/main.go +++ b/exp/services/ledgerexporter/internal/main.go @@ -1,6 +1,7 @@ package ledgerexporter import ( + "context" "fmt" "github.com/spf13/cobra" @@ -14,16 +15,15 @@ var ( app := NewApp() return app.Run(runtimeSettings) } - rootCmd, scanAndFillCmd, appendCmd *cobra.Command ) func Execute() error { - defineCommands() + rootCmd := defineCommands() return rootCmd.Execute() } -func defineCommands() { - rootCmd = &cobra.Command{ +func defineCommands() *cobra.Command { + var rootCmd = &cobra.Command{ Use: "ledgerexporter", Short: "Export Stellar network ledger data to a remote data store", Long: "Converts ledger meta data from Stellar network into static data and exports it remote data storage.", @@ -32,7 +32,7 @@ func defineCommands() { return fmt.Errorf("please specify one of the availble sub-commands to initiate export") }, } - scanAndFillCmd = &cobra.Command{ + var scanAndFillCmd = &cobra.Command{ Use: "scan-and-fill", Short: "scans the entire bounded requested range between 'start' and 'end' flags and exports only the ledgers which are missing from the data lake.", Long: "scans the entire bounded requested range between 'start' and 'end' flags and exports only the ledgers which are missing from the data lake.", @@ -42,10 +42,14 @@ func defineCommands() { cmd.PersistentFlags().Lookup("config-file"), ) settings.Mode = ScanFill + settings.Ctx = cmd.Context() + if settings.Ctx == nil { + settings.Ctx = context.Background() + } return ledgerExporterCmdRunner(settings) }, } - appendCmd = &cobra.Command{ + var appendCmd = &cobra.Command{ Use: "append", Short: "export ledgers beginning with the first missing ledger after the specified 'start' ledger and resumes exporting from there", Long: "export ledgers beginning with the first missing ledger after the specified 'start' ledger and resumes exporting from there", @@ -55,6 +59,10 @@ func defineCommands() { cmd.PersistentFlags().Lookup("config-file"), ) settings.Mode = Append + settings.Ctx = cmd.Context() + if settings.Ctx == nil { + settings.Ctx = context.Background() + } return ledgerExporterCmdRunner(settings) }, } @@ -73,6 +81,8 @@ func defineCommands() { "If 'end' is absent or '0' means unbounded mode, exporter will continue to run indefintely and export the latest closed ledgers from network as they are generated in real time.") appendCmd.PersistentFlags().String("config-file", "config.toml", "Path to the TOML config file. Defaults to 'config.toml' on runtime working directory path.") viper.BindPFlags(appendCmd.PersistentFlags()) + + return rootCmd } func bindCliParameters(startFlag *pflag.Flag, endFlag *pflag.Flag, configFileFlag *pflag.Flag) RuntimeSettings { diff --git a/exp/services/ledgerexporter/internal/main_test.go b/exp/services/ledgerexporter/internal/main_test.go index 4c9e5412f3..c9fe0f853d 100644 --- a/exp/services/ledgerexporter/internal/main_test.go +++ b/exp/services/ledgerexporter/internal/main_test.go @@ -2,6 +2,7 @@ package ledgerexporter import ( "bytes" + "context" "io" "testing" @@ -20,6 +21,8 @@ func TestFlagsOutput(t *testing.T) { return errors.New("test error") } + ctx := context.Background() + testCases := []struct { name string commandArgs []string @@ -42,6 +45,7 @@ func TestFlagsOutput(t *testing.T) { EndLedger: 5, ConfigFilePath: "myfile", Mode: Append, + Ctx: ctx, }, }, { @@ -54,6 +58,7 @@ func TestFlagsOutput(t *testing.T) { EndLedger: 0, ConfigFilePath: "myfile", Mode: Append, + Ctx: ctx, }, }, { @@ -72,6 +77,7 @@ func TestFlagsOutput(t *testing.T) { EndLedger: 5, ConfigFilePath: "myfile", Mode: ScanFill, + Ctx: ctx, }, }, { @@ -84,6 +90,7 @@ func TestFlagsOutput(t *testing.T) { EndLedger: 0, ConfigFilePath: "myfile", Mode: ScanFill, + Ctx: ctx, }, }, { @@ -97,13 +104,13 @@ func TestFlagsOutput(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { // mock the ledger exporter's cmd runner to be this test's mock routine instead of real app ledgerExporterCmdRunner = testCase.appRunner - defineCommands() + rootCmd := defineCommands() rootCmd.SetArgs(testCase.commandArgs) var errWriter io.Writer = &bytes.Buffer{} var outWriter io.Writer = &bytes.Buffer{} rootCmd.SetErr(errWriter) rootCmd.SetOut(outWriter) - rootCmd.Execute() + rootCmd.ExecuteContext(ctx) errOutput := errWriter.(*bytes.Buffer).String() if testCase.expectedErrOutput != "" { diff --git a/exp/services/ledgerexporter/internal/test/integration_captive_core.cfg b/exp/services/ledgerexporter/internal/test/integration_captive_core.cfg new file mode 100644 index 0000000000..14e3772542 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/integration_captive_core.cfg @@ -0,0 +1,21 @@ +# this is based on quickstart --local network settings +ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING = true +DATABASE = "sqlite3://stellar.db" +DEPRECATED_SQL_LEDGER_STATE = false +ENABLE_SOROBAN_DIAGNOSTIC_EVENTS = true +FAILURE_SAFETY = 0 +HTTP_PORT = 0 +PUBLIC_HTTP_PORT = false +# avoid colliding with peer port from standalone core instance on same host +PEER_PORT = 15625 +LOG_FILE_PATH = "" +NETWORK_PASSPHRASE = "Standalone Network ; February 2017" +UNSAFE_QUORUM = true + +[[VALIDATORS]] + ADDRESS = "localhost:11625" + HISTORY = "curl -sf http://localhost:1570/{0} -o {1}" + HOME_DOMAIN = "core.local" + NAME = "local_core" + PUBLIC_KEY = "GCTI6HMWRH2QGMFKWVU5M5ZSOTKL7P7JAHZDMJJBKDHGWTEC4CJ7O3DU" + QUALITY = "MEDIUM" diff --git a/exp/services/ledgerexporter/internal/test/integration_config_template.toml b/exp/services/ledgerexporter/internal/test/integration_config_template.toml new file mode 100644 index 0000000000..410aa0f7e5 --- /dev/null +++ b/exp/services/ledgerexporter/internal/test/integration_config_template.toml @@ -0,0 +1,15 @@ +[datastore_config] +type = "GCS" + +[datastore_config.params] +destination_bucket_path = "integration-test/standalone" + +[datastore_config.schema] +ledgers_per_file = 1 +files_per_partition = 10 + +[stellar_core_config] +captive_core_toml_path = "test/integration_captive_core.cfg" +history_archive_urls = ["http://localhost:1570"] +network_passphrase = "Standalone Network ; February 2017" +checkpoint_frequency = 8 \ No newline at end of file diff --git a/go.mod b/go.mod index 9e1caca7aa..531a7ecd3f 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.1 require ( cloud.google.com/go/firestore v1.15.0 // indirect - cloud.google.com/go/storage v1.40.0 + cloud.google.com/go/storage v1.42.0 firebase.google.com/go v3.12.0+incompatible github.com/2opremio/pretty v0.2.2-0.20230601220618-e1d5758b2a95 github.com/BurntSushi/toml v1.3.2 @@ -52,34 +52,40 @@ require ( github.com/stretchr/testify v1.9.0 github.com/tyler-smith/go-bip39 v0.0.0-20180618194314-52158e4697b8 github.com/xdrpp/goxdr v0.1.1 - google.golang.org/api v0.177.0 + google.golang.org/api v0.183.0 gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0 gopkg.in/square/go-jose.v2 v2.4.1 gopkg.in/tylerb/graceful.v1 v1.2.15 ) require ( - github.com/cenkalti/backoff/v4 v4.2.1 - github.com/fsouza/fake-gcs-server v1.49.0 + github.com/cenkalti/backoff/v4 v4.3.0 + github.com/docker/docker v27.0.3+incompatible + github.com/docker/go-connections v0.5.0 + github.com/fsouza/fake-gcs-server v1.49.2 ) require ( - cloud.google.com/go/auth v0.3.0 // indirect + cloud.google.com/go/auth v0.5.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect cloud.google.com/go/compute/metadata v0.3.0 // indirect cloud.google.com/go/iam v1.1.8 // indirect - cloud.google.com/go/longrunning v0.5.6 // indirect - cloud.google.com/go/pubsub v1.37.0 // indirect + cloud.google.com/go/longrunning v0.5.7 // indirect + cloud.google.com/go/pubsub v1.38.0 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/containerd/log v0.1.0 // indirect github.com/creachadair/mds v0.0.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gobuffalo/packd v1.0.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/renameio/v2 v2.0.0 // indirect github.com/google/s2a-go v0.1.7 // indirect @@ -87,7 +93,12 @@ require ( github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/nxadm/tail v1.4.8 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pkg/xattr v0.4.9 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect @@ -98,22 +109,25 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect + go.opentelemetry.io/otel v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect + go.opentelemetry.io/otel/metric v1.28.0 // indirect + go.opentelemetry.io/otel/sdk v1.28.0 // indirect + go.opentelemetry.io/otel/trace v1.28.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.13.0 // indirect + golang.org/x/mod v0.17.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/tools v0.14.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect gopkg.in/djherbis/atime.v1 v1.0.0 // indirect gopkg.in/djherbis/stream.v1 v1.3.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gotest.tools/v3 v3.5.1 // indirect ) require ( - cloud.google.com/go v0.112.2 // indirect + cloud.google.com/go v0.114.0 // indirect github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/buger/goreplay v1.3.2 @@ -124,7 +138,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5 // indirect - github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/googleapis/gax-go/v2 v2.12.4 // indirect github.com/hashicorp/golang-lru v1.0.2 github.com/imkira/go-interpol v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -146,26 +160,26 @@ require ( github.com/stretchr/objx v0.5.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.34.0 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20151027082146-e0fe6f683076 // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20150808065054-e02fc20de94c // indirect - github.com/xeipuuv/gojsonschema v0.0.0-20161231055540-f06f290571ce // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/yalp/jsonpath v0.0.0-20150812003900-31a79c7593bb // indirect github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d // indirect github.com/yudai/golcs v0.0.0-20150405163532-d1c525dea8ce // indirect github.com/yudai/pp v2.0.1+incompatible // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.22.0 // indirect + golang.org/x/crypto v0.24.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d - golang.org/x/net v0.24.0 // indirect - golang.org/x/oauth2 v0.20.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/term v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/oauth2 v0.21.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect - google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.34.1 // indirect + google.golang.org/genproto v0.0.0-20240528184218-531527333157 // indirect + google.golang.org/grpc v1.64.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ab95f599dd..13d3a0acf0 100644 --- a/go.sum +++ b/go.sum @@ -17,10 +17,10 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw= -cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= -cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs= -cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= +cloud.google.com/go v0.114.0 h1:OIPFAdfrFDFO2ve2U7r/H5SwSbBzEdrBdE7xkgwc+kY= +cloud.google.com/go v0.114.0/go.mod h1:ZV9La5YYxctro1HTPug5lXH/GefROyW8PPD4T8n9J8E= +cloud.google.com/go/auth v0.5.1 h1:0QNO7VThG54LUzKiQxv8C6x1YX7lUrzlAa1nVLF8CIw= +cloud.google.com/go/auth v0.5.1/go.mod h1:vbZT8GjzDf3AVqCcQmqeeM32U9HBFc32vVVAbwDsa6s= cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -37,29 +37,31 @@ cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBp cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0= cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE= -cloud.google.com/go/kms v1.15.8 h1:szIeDCowID8th2i8XE4uRev5PMxQFqW+JjwYxL9h6xs= -cloud.google.com/go/kms v1.15.8/go.mod h1:WoUHcDjD9pluCg7pNds131awnH429QGvRM3N/4MyoVs= -cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE= -cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA= +cloud.google.com/go/kms v1.17.1 h1:5k0wXqkxL+YcXd4viQzTqCgzzVKKxzgrK+rCZJytEQs= +cloud.google.com/go/kms v1.17.1/go.mod h1:DCMnCF/apA6fZk5Cj4XsD979OyHAqFasPuA5Sd0kGlQ= +cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= +cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.37.0 h1:0uEEfaB1VIJzabPpwpZf44zWAKAme3zwKKxHk7vJQxQ= -cloud.google.com/go/pubsub v1.37.0/go.mod h1:YQOQr1uiUM092EXwKs56OPT650nwnawc+8/IjoUeGzQ= +cloud.google.com/go/pubsub v1.38.0 h1:J1OT7h51ifATIedjqk/uBNPh+1hkvUaH4VKbz4UuAsc= +cloud.google.com/go/pubsub v1.38.0/go.mod h1:IPMJSWSus/cu57UyR01Jqa/bNOQA+XnPF6Z4dKW4fAA= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw= -cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= +cloud.google.com/go/storage v1.42.0 h1:4QtGpplCVt1wz6g5o1ifXd656P5z+yNgzdw1tVfp0cU= +cloud.google.com/go/storage v1.42.0/go.mod h1:HjMXRFq65pGKFn6hxj6x3HCyR41uSB72Z0SO/Vn6JFQ= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= firebase.google.com/go v3.12.0+incompatible h1:q70KCp/J0oOL8kJ8oV2j3646kV4TB8Y5IvxXC0WT1bo= firebase.google.com/go v3.12.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs= github.com/2opremio/pretty v0.2.2-0.20230601220618-e1d5758b2a95 h1:vvMDiVd621MU1Djr7Ep7OXu8gHOtsdwrI4tjnIGvpTg= github.com/2opremio/pretty v0.2.2-0.20230601220618-e1d5758b2a95/go.mod h1:Gv4NIpY67KDahg+DtIG5/2Ok4l8vzYEekiirSCH+IGA= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -88,8 +90,8 @@ github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENU github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/buger/goreplay v1.3.2 h1:MFAStZZCsHMPeN5xJ11rhUtV4ZctFRgzSHTfWSWOJsg= github.com/buger/goreplay v1.3.2/go.mod h1:EyAKHxJR6K6phd0NaoPETSDbJRB/ogIw3Y15UlSbVBM= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s= github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= @@ -102,6 +104,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/jrpc2 v1.1.0 h1:SgpJf0v1rVCZx68+4APv6dgsTFsIHlpgFD1NlQAWA0A= github.com/creachadair/jrpc2 v1.1.0/go.mod h1:5jN7MKwsm8qvgfTsTzLX3JIfidsAkZ1c8DZSQmp+g38= @@ -112,8 +116,16 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/fscache v0.10.1 h1:hDv+RGyvD+UDKyRYuLoVNbuRTnf2SrA2K3VyR1br9lk= github.com/djherbis/fscache v0.10.1/go.mod h1:yyPYtkNnnPXsW+81lAcQS6yab3G2CRfnPLotBvtbf0c= +github.com/docker/docker v27.0.3+incompatible h1:aBGI9TeQ4MPlhquTQKq9XbK79rKFVwXNUAYz9aXyEBE= +github.com/docker/docker v27.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -140,8 +152,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/fsouza/fake-gcs-server v1.49.0 h1:4x1RxKuqoqhZrXogtj5nInQnIjQylxld43tKrkPHnmE= -github.com/fsouza/fake-gcs-server v1.49.0/go.mod h1:FJYZxdHQk2nGxrczFjLbDv8h6SnYXxSxcnM14eeespA= +github.com/fsouza/fake-gcs-server v1.49.2 h1:fukDqzEQM50QkA0jAbl6cLqeDu3maQjwZBuys759TR4= +github.com/fsouza/fake-gcs-server v1.49.2/go.mod h1:17SYzJEXRcaAA5ATwwvgBkSIqIy7r1icnGM0y/y4foY= github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955 h1:gmtGRvSexPU4B1T/yYo0sLOKzER1YT+b4kPxPpm0Ty4= github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955/go.mod h1:vmp8DIyckQMXOPl0AQVHt+7n5h7Gb7hS6CUydiV8QeA= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= @@ -156,8 +168,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -174,6 +186,8 @@ github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XE github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -230,8 +244,8 @@ github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPg github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -256,8 +270,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= -github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg= +github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -269,6 +283,8 @@ github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/guregu/null v4.0.0+incompatible h1:4zw0ckM7ECd6FNNddc3Fu4aty9nTlpkkzH7dPn4/4Gw= github.com/guregu/null v4.0.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -305,6 +321,7 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= @@ -351,12 +368,18 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.70 h1:1u9NtMgfK1U42kUxcsl5v0yj6TEOPR497OAQxpJnn2g= -github.com/minio/minio-go/v7 v7.0.70/go.mod h1:4yBA8v80xGA30cfM3fz0DKYMXunWl/AV/6tWEs9ryzo= +github.com/minio/minio-go/v7 v7.0.71 h1:No9XfOKTYi6i0GnBj+WZwD8WP5GZfL7n7GOjRqCdAjA= +github.com/minio/minio-go/v7 v7.0.71/go.mod h1:4yBA8v80xGA30cfM3fz0DKYMXunWl/AV/6tWEs9ryzo= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db h1:eZgFHVkk9uOTaOQLC6tgjkzdp7Ays8eEVecBcfHZlJQ= github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -372,6 +395,10 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= @@ -450,6 +477,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -475,12 +503,12 @@ github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhe github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xdrpp/goxdr v0.1.1 h1:E1B2c6E8eYhOVyd7yEpOyopzTPirUeF6mVOfXfGyJyc= github.com/xdrpp/goxdr v0.1.1/go.mod h1:dXo1scL/l6s7iME1gxHWo2XCppbHEKZS7m/KyYWkNzA= -github.com/xeipuuv/gojsonpointer v0.0.0-20151027082146-e0fe6f683076 h1:KM4T3G70MiR+JtqplcYkNVoNz7pDwYaBxWBXQK804So= -github.com/xeipuuv/gojsonpointer v0.0.0-20151027082146-e0fe6f683076/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20150808065054-e02fc20de94c h1:XZWnr3bsDQWAZg4Ne+cPoXRPILrNlPNQfxBuwLl43is= -github.com/xeipuuv/gojsonreference v0.0.0-20150808065054-e02fc20de94c/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v0.0.0-20161231055540-f06f290571ce h1:cVSRGH8cOveJNwFEEZLXtB+XMnRqKLjUP6V/ZFYQCXI= -github.com/xeipuuv/gojsonschema v0.0.0-20161231055540-f06f290571ce/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yalp/jsonpath v0.0.0-20150812003900-31a79c7593bb h1:06WAhQa+mYv7BiOk13B/ywyTlkoE/S7uu6TBKU6FHnE= github.com/yalp/jsonpath v0.0.0-20150812003900-31a79c7593bb/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d h1:yJIizrfO599ot2kQ6Af1enICnwBD3XoxgX3MrMwot2M= @@ -494,8 +522,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.einride.tech/aip v0.66.0 h1:XfV+NQX6L7EOYK11yoHHFtndeaWh3KbD9/cN/6iWEt8= -go.einride.tech/aip v0.66.0/go.mod h1:qAhMsfT7plxBX+Oy7Huol6YUvZ0ZzdUz26yZsQwfl1M= +go.einride.tech/aip v0.67.1 h1:d/4TW92OxXBngkSOwWS2CH5rez869KpKMaN44mdxkFI= +go.einride.tech/aip v0.67.1/go.mod h1:ZGX4/zKw8dcgzdLsrvpOOGxfxI2QSk12SlP7d6c0/XI= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -508,14 +536,20 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.4 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= +go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= +go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= +go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= +go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= +go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= +go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= +go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -528,8 +562,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -566,8 +600,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -606,8 +640,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -617,8 +651,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -682,13 +716,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -699,8 +733,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -744,6 +778,7 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -753,11 +788,12 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= -golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -783,8 +819,8 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.177.0 h1:8a0p/BbPa65GlqGWtUKxot4p0TV8OGOfyTjtmkXNXmk= -google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw= +google.golang.org/api v0.183.0 h1:PNMeRDwo1pJdgNcFQ9GstuLe/noWKIc89pRWRLMvLwE= +google.golang.org/api v0.183.0/go.mod h1:q43adC5/pHoSZTx5h2mSmdF7NcyfW9JuDyIOJAgS9ZQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -830,12 +866,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw= -google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw= -google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 h1:DTJM0R8LECCgFeUwApvcEJHz85HLagW8uRENYxHh1ww= -google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6/go.mod h1:10yRODfgim2/T8csjQsMPgZOMvtytXKTDRzH6HRGzRw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/genproto v0.0.0-20240528184218-531527333157 h1:u7WMYrIrVvs0TF5yaKwKNbcJyySYf+HAIFXxWltJOXE= +google.golang.org/genproto v0.0.0-20240528184218-531527333157/go.mod h1:ubQlAQnzejB8uZzszhrTCU2Fyp6Vi7ZE5nn0c3W8+qQ= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= +google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -852,8 +888,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -866,8 +902,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -903,6 +939,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 8e86d00c7668e5732bf278c46dffb983b8b55776 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Fri, 12 Jul 2024 15:33:59 -0700 Subject: [PATCH 208/234] exp/servies/ledgerexporter: Fix github release workflow (#5387) --- .github/workflows/ledgerexporter-release.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ledgerexporter-release.yml b/.github/workflows/ledgerexporter-release.yml index 2e66847286..8738f53def 100644 --- a/.github/workflows/ledgerexporter-release.yml +++ b/.github/workflows/ledgerexporter-release.yml @@ -17,15 +17,18 @@ jobs: LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE: docker.io/stellar/quickstart:testing@sha256:03c6679f838a92b1eda4cd3a9e2bdee4c3586e278a138a0acf36a9bc99a0041f LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL: "false" STELLAR_CORE_VERSION: 21.1.0-1921.b3aeb14cc.focal - VERSION: ${GITHUB_REF_NAME#ledgerexporter-v} steps: + - name: Set VERSION + run: | + echo "VERSION=${GITHUB_REF_NAME#ledgerexporter-v}" >> $GITHUB_ENV + - uses: actions/checkout@v3 with: - ref: github.sha + ref: ${{ github.sha }} - name: Pull Quickstart image shell: bash run: | - docker pull "$LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE" + docker pull "$LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE" - name: Install captive core run: | # Workaround for https://github.com/actions/virtual-environments/issues/5245, @@ -40,7 +43,7 @@ jobs: echo "Using stellar core version $(stellar-core version)" - name: Run Ledger Exporter test - run: go test -v -race -run TestLedgerExporterTestSuite ./exp/services/ledgerexporter/... + run: go test -v -race -run TestLedgerExporterTestSuite ./exp/services/ledgerexporter/... - name: Build Ledger Exporter docker run: make -C exp/services/ledgerexporter docker-build From 0e8321510004ebfc6a460a3d8ab5bcf42662c69a Mon Sep 17 00:00:00 2001 From: tamirms Date: Tue, 16 Jul 2024 22:59:17 +0100 Subject: [PATCH 209/234] services/horizon/internal/ingest: Add more fine-grained reap metrics (#5385) --- services/horizon/internal/db2/history/main.go | 38 +++++++++-------- .../horizon/internal/db2/history/reap_test.go | 20 ++++----- services/horizon/internal/ingest/main.go | 41 +++++++++++-------- services/horizon/internal/ingest/main_test.go | 11 ++--- 4 files changed, 60 insertions(+), 50 deletions(-) diff --git a/services/horizon/internal/db2/history/main.go b/services/horizon/internal/db2/history/main.go index 35f32f5434..47e8952b07 100644 --- a/services/horizon/internal/db2/history/main.go +++ b/services/horizon/internal/db2/history/main.go @@ -282,7 +282,7 @@ type IngestionQ interface { NewTradeBatchInsertBuilder() TradeBatchInsertBuilder RebuildTradeAggregationTimes(ctx context.Context, from, to strtime.Millis, roundingSlippageFilter int) error RebuildTradeAggregationBuckets(ctx context.Context, fromLedger, toLedger uint32, roundingSlippageFilter int) error - ReapLookupTables(ctx context.Context, offsets map[string]int64) (map[string]int64, map[string]int64, error) + ReapLookupTables(ctx context.Context, offsets map[string]int64) (map[string]LookupTableReapResult, error) CreateAssets(ctx context.Context, assets []xdr.Asset, batchSize int) (map[string]Asset, error) QTransactions QTrustLines @@ -971,27 +971,27 @@ type tableObjectFieldPair struct { objectField string } +type LookupTableReapResult struct { + Offset int64 + RowsDeleted int64 + Duration time.Duration +} + // ReapLookupTables removes rows from lookup tables like history_claimable_balances // which aren't used (orphaned), i.e. history entries for them were reaped. // This method must be executed inside ingestion transaction. Otherwise it may // create invalid state in lookup and history tables. func (q Q) ReapLookupTables(ctx context.Context, offsets map[string]int64) ( - map[string]int64, // deleted rows count - map[string]int64, // new offsets + map[string]LookupTableReapResult, error, ) { if q.GetTx() == nil { - return nil, nil, errors.New("cannot be called outside of an ingestion transaction") + return nil, errors.New("cannot be called outside of an ingestion transaction") } const batchSize = 1000 - deletedCount := make(map[string]int64) - - if offsets == nil { - offsets = make(map[string]int64) - } - + results := map[string]LookupTableReapResult{} for table, historyTables := range map[string][]tableObjectFieldPair{ "history_accounts": { { @@ -1054,9 +1054,10 @@ func (q Q) ReapLookupTables(ctx context.Context, offsets map[string]int64) ( }, }, } { + startTime := time.Now() query, err := constructReapLookupTablesQuery(table, historyTables, batchSize, offsets[table]) if err != nil { - return nil, nil, errors.Wrap(err, "error constructing a query") + return nil, errors.Wrap(err, "error constructing a query") } // Find new offset before removing the rows @@ -1066,7 +1067,7 @@ func (q Q) ReapLookupTables(ctx context.Context, offsets map[string]int64) ( if q.NoRows(err) { newOffset = 0 } else { - return nil, nil, err + return nil, err } } @@ -1075,18 +1076,21 @@ func (q Q) ReapLookupTables(ctx context.Context, offsets map[string]int64) ( query, ) if err != nil { - return nil, nil, errors.Wrapf(err, "error running query: %s", query) + return nil, errors.Wrapf(err, "error running query: %s", query) } rows, err := res.RowsAffected() if err != nil { - return nil, nil, errors.Wrapf(err, "error running RowsAffected after query: %s", query) + return nil, errors.Wrapf(err, "error running RowsAffected after query: %s", query) } - deletedCount[table] = rows - offsets[table] = newOffset + results[table] = LookupTableReapResult{ + Offset: newOffset, + RowsDeleted: rows, + Duration: time.Since(startTime), + } } - return deletedCount, offsets, nil + return results, nil } // constructReapLookupTablesQuery creates a query like (using history_claimable_balances diff --git a/services/horizon/internal/db2/history/reap_test.go b/services/horizon/internal/db2/history/reap_test.go index 508041eb22..0f033c3629 100644 --- a/services/horizon/internal/db2/history/reap_test.go +++ b/services/horizon/internal/db2/history/reap_test.go @@ -52,7 +52,7 @@ func TestReapLookupTables(t *testing.T) { err = q.Begin(tt.Ctx) tt.Require.NoError(err) - deletedCount, newOffsets, err := q.ReapLookupTables(tt.Ctx, nil) + results, err := q.ReapLookupTables(tt.Ctx, nil) tt.Require.NoError(err) err = q.Commit() @@ -77,23 +77,23 @@ func TestReapLookupTables(t *testing.T) { tt.Assert.Equal(25, prevAccounts, "prevAccounts") tt.Assert.Equal(1, curAccounts, "curAccounts") - tt.Assert.Equal(int64(24), deletedCount["history_accounts"], `deletedCount["history_accounts"]`) + tt.Assert.Equal(int64(24), results["history_accounts"].RowsDeleted, `deletedCount["history_accounts"]`) tt.Assert.Equal(7, prevAssets, "prevAssets") tt.Assert.Equal(0, curAssets, "curAssets") - tt.Assert.Equal(int64(7), deletedCount["history_assets"], `deletedCount["history_assets"]`) + tt.Assert.Equal(int64(7), results["history_assets"].RowsDeleted, `deletedCount["history_assets"]`) tt.Assert.Equal(1, prevClaimableBalances, "prevClaimableBalances") tt.Assert.Equal(0, curClaimableBalances, "curClaimableBalances") - tt.Assert.Equal(int64(1), deletedCount["history_claimable_balances"], `deletedCount["history_claimable_balances"]`) + tt.Assert.Equal(int64(1), results["history_claimable_balances"].RowsDeleted, `deletedCount["history_claimable_balances"]`) tt.Assert.Equal(1, prevLiquidityPools, "prevLiquidityPools") tt.Assert.Equal(0, curLiquidityPools, "curLiquidityPools") - tt.Assert.Equal(int64(1), deletedCount["history_liquidity_pools"], `deletedCount["history_liquidity_pools"]`) + tt.Assert.Equal(int64(1), results["history_liquidity_pools"].RowsDeleted, `deletedCount["history_liquidity_pools"]`) - tt.Assert.Len(newOffsets, 4) - tt.Assert.Equal(int64(0), newOffsets["history_accounts"]) - tt.Assert.Equal(int64(0), newOffsets["history_assets"]) - tt.Assert.Equal(int64(0), newOffsets["history_claimable_balances"]) - tt.Assert.Equal(int64(0), newOffsets["history_liquidity_pools"]) + tt.Assert.Len(results, 4) + tt.Assert.Equal(int64(0), results["history_accounts"].Offset) + tt.Assert.Equal(int64(0), results["history_assets"].Offset) + tt.Assert.Equal(int64(0), results["history_claimable_balances"].Offset) + tt.Assert.Equal(int64(0), results["history_liquidity_pools"].Offset) } diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 650a08b426..dec3123f34 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -145,9 +145,8 @@ type Metrics struct { // duration of rebuilding trade aggregation buckets. LedgerIngestionTradeAggregationDuration prometheus.Summary - // LedgerIngestionReapLookupTablesDuration exposes timing metrics about the rate and - // duration of reaping lookup tables. - LedgerIngestionReapLookupTablesDuration prometheus.Summary + ReapDurationByLookupTable *prometheus.SummaryVec + RowsReapedByLookupTable *prometheus.SummaryVec // StateVerifyDuration exposes timing metrics about the rate and // duration of state verification. @@ -228,7 +227,7 @@ type system struct { runStateVerificationOnLedger func(uint32) bool - reapOffsets map[string]int64 + reapOffsetByTable map[string]int64 maxLedgerPerFlush uint32 reaper *Reaper @@ -327,6 +326,7 @@ func NewSystem(config Config) (System, error) { config.ReapConfig, config.HistorySession, ), + reapOffsetByTable: map[string]int64{}, } system.initMetrics() @@ -367,11 +367,17 @@ func (s *system) initMetrics() { Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, }) - s.metrics.LedgerIngestionReapLookupTablesDuration = prometheus.NewSummary(prometheus.SummaryOpts{ - Namespace: "horizon", Subsystem: "ingest", Name: "ledger_ingestion_reap_lookup_tables_duration_seconds", - Help: "ledger ingestion reap lookup tables durations, sliding window = 10m", + s.metrics.ReapDurationByLookupTable = prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Namespace: "horizon", Subsystem: "ingest", Name: "reap_lookup_tables_duration_seconds", + Help: "reap lookup tables durations, sliding window = 10m", Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }) + }, []string{"table"}) + + s.metrics.RowsReapedByLookupTable = prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Namespace: "horizon", Subsystem: "ingest", Name: "reap_lookup_tables_rows_reaped", + Help: "rows deleted during lookup tables reap, sliding window = 10m", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, []string{"table"}) s.metrics.StateVerifyDuration = prometheus.NewSummary(prometheus.SummaryOpts{ Namespace: "horizon", Subsystem: "ingest", Name: "state_verify_duration_seconds", @@ -490,7 +496,8 @@ func (s *system) RegisterMetrics(registry *prometheus.Registry) { registry.MustRegister(s.metrics.LocalLatestLedger) registry.MustRegister(s.metrics.LedgerIngestionDuration) registry.MustRegister(s.metrics.LedgerIngestionTradeAggregationDuration) - registry.MustRegister(s.metrics.LedgerIngestionReapLookupTablesDuration) + registry.MustRegister(s.metrics.ReapDurationByLookupTable) + registry.MustRegister(s.metrics.RowsReapedByLookupTable) registry.MustRegister(s.metrics.StateVerifyDuration) registry.MustRegister(s.metrics.StateInvalidGauge) registry.MustRegister(s.metrics.LedgerStatsCounter) @@ -793,7 +800,7 @@ func (s *system) maybeReapLookupTables(lastIngestedLedger uint32) { defer cancel() reapStart := time.Now() - deletedCount, newOffsets, err := s.historyQ.ReapLookupTables(ctx, s.reapOffsets) + results, err := s.historyQ.ReapLookupTables(ctx, s.reapOffsetByTable) if err != nil { log.WithError(err).Warn("Error reaping lookup tables") return @@ -807,18 +814,20 @@ func (s *system) maybeReapLookupTables(lastIngestedLedger uint32) { totalDeleted := int64(0) reapLog := log - for table, c := range deletedCount { - totalDeleted += c - reapLog = reapLog.WithField(table, c) + for table, result := range results { + totalDeleted += result.RowsDeleted + reapLog = reapLog.WithField(table, result) + s.reapOffsetByTable[table] = result.Offset + s.Metrics().RowsReapedByLookupTable.With(prometheus.Labels{"table": table}).Observe(float64(result.RowsDeleted)) + s.Metrics().ReapDurationByLookupTable.With(prometheus.Labels{"table": table}).Observe(result.Duration.Seconds()) } if totalDeleted > 0 { reapLog.Info("Reaper deleted rows from lookup tables") } - s.reapOffsets = newOffsets - reapDuration := time.Since(reapStart).Seconds() - s.Metrics().LedgerIngestionReapLookupTablesDuration.Observe(float64(reapDuration)) + s.Metrics().RowsReapedByLookupTable.With(prometheus.Labels{"table": "total"}).Observe(float64(totalDeleted)) + s.Metrics().ReapDurationByLookupTable.With(prometheus.Labels{"table": "total"}).Observe(time.Since(reapStart).Seconds()) } func (s *system) incrementStateVerificationErrors() int { diff --git a/services/horizon/internal/ingest/main_test.go b/services/horizon/internal/ingest/main_test.go index 4f0e220ebe..fde8e40a9c 100644 --- a/services/horizon/internal/ingest/main_test.go +++ b/services/horizon/internal/ingest/main_test.go @@ -562,16 +562,13 @@ func (m *mockDBQ) NewTradeBatchInsertBuilder() history.TradeBatchInsertBuilder { return args.Get(0).(history.TradeBatchInsertBuilder) } -func (m *mockDBQ) ReapLookupTables(ctx context.Context, offsets map[string]int64) (map[string]int64, map[string]int64, error) { +func (m *mockDBQ) ReapLookupTables(ctx context.Context, offsets map[string]int64) (map[string]history.LookupTableReapResult, error) { args := m.Called(ctx, offsets) - var r1, r2 map[string]int64 + var r1 map[string]history.LookupTableReapResult if args.Get(0) != nil { - r1 = args.Get(0).(map[string]int64) + r1 = args.Get(0).(map[string]history.LookupTableReapResult) } - if args.Get(1) != nil { - r1 = args.Get(1).(map[string]int64) - } - return r1, r2, args.Error(2) + return r1, args.Error(2) } func (m *mockDBQ) RebuildTradeAggregationTimes(ctx context.Context, from, to strtime.Millis, roundingSlippageFilter int) error { From d5767df5b21a1c34a5cb05b158d9b0962c94e7bf Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 18 Jul 2024 09:32:22 -0700 Subject: [PATCH 210/234] services/horizon/ingest: Use buffered storage backend for reingest command (#5374) --- .github/workflows/ledgerexporter.yml | 5 +- .../internal/integration_test.go | 17 +- .../ledgerbackend/buffered_storage_backend.go | 20 +- .../buffered_storage_backend_test.go | 64 +- ingest/ledgerbackend/ledger_buffer.go | 6 +- services/horizon/CHANGELOG.md | 11 + services/horizon/cmd/db.go | 742 ++++++++++-------- services/horizon/cmd/db_test.go | 266 +++++++ services/horizon/cmd/ingest.go | 11 +- services/horizon/cmd/root.go | 62 +- services/horizon/internal/flags.go | 129 ++- services/horizon/internal/flags_test.go | 99 ++- services/horizon/internal/ingest/README.md | 42 +- services/horizon/internal/ingest/main.go | 87 +- .../testdata/config.storagebackend.toml | 19 + .../horizon/internal/integration/db_test.go | 156 +++- .../internal/integration/parameters_test.go | 89 +-- .../testbucket/FFFFFC18--999.xdr.zstd | Bin 0 -> 1547 bytes .../testbucket/FFFFFC19--998.xdr.zstd | Bin 0 -> 2791 bytes .../testbucket/FFFFFC1A--997.xdr.zstd | Bin 0 -> 2814 bytes .../internal/test/integration/integration.go | 149 ++-- support/datastore/datastore.go | 3 +- support/datastore/gcs_datastore.go | 16 +- support/datastore/gcs_test.go | 16 +- support/datastore/mocks.go | 5 + 25 files changed, 1327 insertions(+), 687 deletions(-) create mode 100644 services/horizon/cmd/db_test.go create mode 100644 services/horizon/internal/ingest/testdata/config.storagebackend.toml create mode 100644 services/horizon/internal/integration/testdata/testbucket/FFFFFC18--999.xdr.zstd create mode 100644 services/horizon/internal/integration/testdata/testbucket/FFFFFC19--998.xdr.zstd create mode 100644 services/horizon/internal/integration/testdata/testbucket/FFFFFC1A--997.xdr.zstd diff --git a/.github/workflows/ledgerexporter.yml b/.github/workflows/ledgerexporter.yml index c80a367771..ac1e265582 100644 --- a/.github/workflows/ledgerexporter.yml +++ b/.github/workflows/ledgerexporter.yml @@ -13,9 +13,10 @@ jobs: CAPTIVE_CORE_DEBIAN_PKG_VERSION: 21.1.0-1921.b3aeb14cc.focal LEDGEREXPORTER_INTEGRATION_TESTS_ENABLED: "true" LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN: /usr/bin/stellar-core - # this pins to a version of quickstart:testing that has the same version as LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN + # this pins to a version of quickstart:testing that has the same version of core + # as specified on LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN # this is the multi-arch index sha, get it by 'docker buildx imagetools inspect stellar/quickstart:testing' - LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE: docker.io/stellar/quickstart:testing@sha256:03c6679f838a92b1eda4cd3a9e2bdee4c3586e278a138a0acf36a9bc99a0041f + LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE: docker.io/stellar/quickstart:testing@sha256:5c8186f53cc98571749054dd782dce33b0aca2d1a622a7610362f7c15b79b1bf LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL: "false" steps: - name: Install captive core diff --git a/exp/services/ledgerexporter/internal/integration_test.go b/exp/services/ledgerexporter/internal/integration_test.go index dab2e5b5f8..ccc5463908 100644 --- a/exp/services/ledgerexporter/internal/integration_test.go +++ b/exp/services/ledgerexporter/internal/integration_test.go @@ -34,6 +34,7 @@ const ( // tests then refer to ledger sequences only up to this, therefore // don't have to do complex waiting within test for a sequence to exist. waitForCoreLedgerSequence = 16 + configTemplate = "test/integration_config_template.toml" ) func TestLedgerExporterTestSuite(t *testing.T) { @@ -54,6 +55,7 @@ type LedgerExporterTestSuite struct { dockerCli *client.Client gcsServer *fakestorage.Server finishedSetup bool + config Config } func (s *LedgerExporterTestSuite) TestScanAndFill() { @@ -74,7 +76,7 @@ func (s *LedgerExporterTestSuite) TestScanAndFill() { s.T().Log(output) s.T().Log(errOutput) - datastore, err := datastore.NewGCSDataStore(s.ctx, "integration-test/standalone") + datastore, err := datastore.NewDataStore(s.ctx, s.config.DataStoreConfig) require.NoError(err) _, err = datastore.GetFile(s.ctx, "FFFFFFFF--0-9/FFFFFFFA--5.xdr.zstd") @@ -104,7 +106,7 @@ func (s *LedgerExporterTestSuite) TestAppend() { s.T().Log(output) s.T().Log(errOutput) - datastore, err := datastore.NewGCSDataStore(s.ctx, "integration-test/standalone") + datastore, err := datastore.NewDataStore(s.ctx, s.config.DataStoreConfig) require.NoError(err) _, err = datastore.GetFile(s.ctx, "FFFFFFFF--0-9/FFFFFFF6--9.xdr.zstd") @@ -134,7 +136,7 @@ func (s *LedgerExporterTestSuite) TestAppendUnbounded() { s.T().Log(errOutput) }() - datastore, err := datastore.NewGCSDataStore(s.ctx, "integration-test/standalone") + datastore, err := datastore.NewDataStore(s.ctx, s.config.DataStoreConfig) require.NoError(err) require.EventuallyWithT(func(c *assert.CollectT) { @@ -158,9 +160,9 @@ func (s *LedgerExporterTestSuite) SetupSuite() { }() testTempDir := t.TempDir() - ledgerExporterConfigTemplate, err := toml.LoadFile("test/integration_config_template.toml") + ledgerExporterConfigTemplate, err := toml.LoadFile(configTemplate) if err != nil { - t.Fatalf("unable to load config template file %v", err) + t.Fatalf("unable to load config template file %v, %v", configTemplate, err) } // if LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN not specified, @@ -172,7 +174,10 @@ func (s *LedgerExporterTestSuite) SetupSuite() { tomlBytes, err := toml.Marshal(ledgerExporterConfigTemplate) if err != nil { - t.Fatalf("unable to load config file %v", err) + t.Fatalf("unable to parse config file toml %v, %v", configTemplate, err) + } + if err = toml.Unmarshal(tomlBytes, &s.config); err != nil { + t.Fatalf("unable to marshal config file toml into struct, %v", err) } tempSeedDataPath := filepath.Join(testTempDir, "data") diff --git a/ingest/ledgerbackend/buffered_storage_backend.go b/ingest/ledgerbackend/buffered_storage_backend.go index 4a353bfe22..aa70336295 100644 --- a/ingest/ledgerbackend/buffered_storage_backend.go +++ b/ingest/ledgerbackend/buffered_storage_backend.go @@ -18,12 +18,10 @@ import ( var _ LedgerBackend = (*BufferedStorageBackend)(nil) type BufferedStorageBackendConfig struct { - LedgerBatchConfig datastore.DataStoreSchema - DataStore datastore.DataStore - BufferSize uint32 - NumWorkers uint32 - RetryLimit uint32 - RetryWait time.Duration + BufferSize uint32 `toml:"buffer_size"` + NumWorkers uint32 `toml:"num_workers"` + RetryLimit uint32 `toml:"retry_limit"` + RetryWait time.Duration `toml:"retry_wait"` } // BufferedStorageBackend is a ledger backend that reads from a storage service. @@ -45,7 +43,7 @@ type BufferedStorageBackend struct { } // NewBufferedStorageBackend returns a new BufferedStorageBackend instance. -func NewBufferedStorageBackend(ctx context.Context, config BufferedStorageBackendConfig) (*BufferedStorageBackend, error) { +func NewBufferedStorageBackend(config BufferedStorageBackendConfig, dataStore datastore.DataStore) (*BufferedStorageBackend, error) { if config.BufferSize == 0 { return nil, errors.New("buffer size must be > 0") } @@ -54,17 +52,13 @@ func NewBufferedStorageBackend(ctx context.Context, config BufferedStorageBacken return nil, errors.New("number of workers must be <= BufferSize") } - if config.DataStore == nil { - return nil, errors.New("no DataStore provided") - } - - if config.LedgerBatchConfig.LedgersPerFile <= 0 { + if dataStore.GetSchema().LedgersPerFile <= 0 { return nil, errors.New("ledgersPerFile must be > 0") } bsBackend := &BufferedStorageBackend{ config: config, - dataStore: config.DataStore, + dataStore: dataStore, } return bsBackend, nil diff --git a/ingest/ledgerbackend/buffered_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go index f18329fffa..ca2711c40d 100644 --- a/ingest/ledgerbackend/buffered_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -44,29 +44,21 @@ func createBufferedStorageBackendConfigForTesting() BufferedStorageBackendConfig param := make(map[string]string) param["destination_bucket_path"] = "testURL" - ledgerBatchConfig := datastore.DataStoreSchema{ - LedgersPerFile: 1, - FilesPerPartition: 64000, - } - - dataStore := new(datastore.MockDataStore) - return BufferedStorageBackendConfig{ - LedgerBatchConfig: ledgerBatchConfig, - DataStore: dataStore, - BufferSize: 100, - NumWorkers: 5, - RetryLimit: 3, - RetryWait: time.Microsecond, + BufferSize: 100, + NumWorkers: 5, + RetryLimit: 3, + RetryWait: time.Microsecond, } } func createBufferedStorageBackendForTesting() BufferedStorageBackend { config := createBufferedStorageBackendConfigForTesting() + dataStore := new(datastore.MockDataStore) return BufferedStorageBackend{ config: config, - dataStore: config.DataStore, + dataStore: dataStore, } } @@ -86,6 +78,10 @@ func createMockdataStore(t *testing.T, start, end, partitionSize, count uint32) } mockDataStore.On("GetFile", mock.Anything, objectName).Return(readCloser, nil) } + mockDataStore.On("GetSchema").Return(datastore.DataStoreSchema{ + LedgersPerFile: count, + FilesPerPartition: partitionSize, + }) t.Cleanup(func() { mockDataStore.AssertExpectations(t) @@ -126,15 +122,18 @@ func createLCMBatchReader(start, end, count uint32) io.ReadCloser { } func TestNewBufferedStorageBackend(t *testing.T) { - ctx := context.Background() config := createBufferedStorageBackendConfigForTesting() - - bsb, err := NewBufferedStorageBackend(ctx, config) + mockDataStore := new(datastore.MockDataStore) + mockDataStore.On("GetSchema").Return(datastore.DataStoreSchema{ + LedgersPerFile: uint32(1), + FilesPerPartition: partitionSize, + }) + bsb, err := NewBufferedStorageBackend(config, mockDataStore) assert.NoError(t, err) - assert.Equal(t, bsb.dataStore, config.DataStore) - assert.Equal(t, uint32(1), bsb.config.LedgerBatchConfig.LedgersPerFile) - assert.Equal(t, uint32(64000), bsb.config.LedgerBatchConfig.FilesPerPartition) + assert.Equal(t, bsb.dataStore, mockDataStore) + assert.Equal(t, uint32(1), bsb.dataStore.GetSchema().LedgersPerFile) + assert.Equal(t, uint32(64000), bsb.dataStore.GetSchema().FilesPerPartition) assert.Equal(t, uint32(100), bsb.config.BufferSize) assert.Equal(t, uint32(5), bsb.config.NumWorkers) assert.Equal(t, uint32(3), bsb.config.RetryLimit) @@ -210,12 +209,14 @@ func TestCloudStorageGetLedger_MultipleLedgerPerFile(t *testing.T) { lcmArray := createLCMForTesting(startLedger, endLedger) bsb := createBufferedStorageBackendForTesting() ctx := context.Background() - bsb.config.LedgerBatchConfig.LedgersPerFile = uint32(2) ledgerRange := BoundedRange(startLedger, endLedger) mockDataStore := createMockdataStore(t, startLedger, endLedger, partitionSize, 2) bsb.dataStore = mockDataStore - + mockDataStore.On("GetSchema").Return(datastore.DataStoreSchema{ + LedgersPerFile: uint32(2), + FilesPerPartition: partitionSize, + }) assert.NoError(t, bsb.PrepareRange(ctx, ledgerRange)) assert.Eventually(t, func() bool { return len(bsb.ledgerBuffer.ledgerQueue) == 2 }, time.Second*5, time.Millisecond*50) @@ -451,6 +452,10 @@ func TestLedgerBufferClose(t *testing.T) { mockDataStore := new(datastore.MockDataStore) partition := ledgerPerFileCount*partitionSize - 1 + mockDataStore.On("GetSchema").Return(datastore.DataStoreSchema{ + LedgersPerFile: ledgerPerFileCount, + FilesPerPartition: partitionSize, + }) objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.zstd", partition, math.MaxUint32-3, 3) afterPrepareRange := make(chan struct{}) @@ -483,7 +488,10 @@ func TestLedgerBufferBoundedObjectNotFound(t *testing.T) { mockDataStore := new(datastore.MockDataStore) partition := ledgerPerFileCount*partitionSize - 1 - + mockDataStore.On("GetSchema").Return(datastore.DataStoreSchema{ + LedgersPerFile: ledgerPerFileCount, + FilesPerPartition: partitionSize, + }) objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.zstd", partition, math.MaxUint32-3, 3) mockDataStore.On("GetFile", mock.Anything, objectName).Return(io.NopCloser(&bytes.Buffer{}), os.ErrNotExist).Once() t.Cleanup(func() { @@ -509,7 +517,10 @@ func TestLedgerBufferUnboundedObjectNotFound(t *testing.T) { mockDataStore := new(datastore.MockDataStore) partition := ledgerPerFileCount*partitionSize - 1 - + mockDataStore.On("GetSchema").Return(datastore.DataStoreSchema{ + LedgersPerFile: ledgerPerFileCount, + FilesPerPartition: partitionSize, + }) objectName := fmt.Sprintf("FFFFFFFF--0-%d/%08X--%d.xdr.zstd", partition, math.MaxUint32-3, 3) iteration := &atomic.Int32{} cancelAfter := int32(bsb.config.RetryLimit) + 2 @@ -551,7 +562,10 @@ func TestLedgerBufferRetryLimit(t *testing.T) { }) bsb.dataStore = mockDataStore - + mockDataStore.On("GetSchema").Return(datastore.DataStoreSchema{ + LedgersPerFile: ledgerPerFileCount, + FilesPerPartition: partitionSize, + }) assert.NoError(t, bsb.PrepareRange(context.Background(), ledgerRange)) bsb.ledgerBuffer.wg.Wait() diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index 5b2ec57ffc..6965461bba 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -95,7 +95,7 @@ func (lb *ledgerBuffer) pushTaskQueue() { return } lb.taskQueue <- lb.nextTaskLedger - lb.nextTaskLedger += lb.config.LedgerBatchConfig.LedgersPerFile + lb.nextTaskLedger += lb.dataStore.GetSchema().LedgersPerFile } // sleepWithContext returns true upon sleeping without interruption from the context @@ -163,7 +163,7 @@ func (lb *ledgerBuffer) worker(ctx context.Context) { } func (lb *ledgerBuffer) downloadLedgerObject(ctx context.Context, sequence uint32) ([]byte, error) { - objectKey := lb.config.LedgerBatchConfig.GetObjectKeyFromSequenceNumber(sequence) + objectKey := lb.dataStore.GetSchema().GetObjectKeyFromSequenceNumber(sequence) reader, err := lb.dataStore.GetFile(ctx, objectKey) if err != nil { @@ -198,7 +198,7 @@ func (lb *ledgerBuffer) storeObject(ledgerObject []byte, sequence uint32) { for lb.ledgerPriorityQueue.Len() > 0 && lb.currentLedger == uint32(lb.ledgerPriorityQueue.Peek().startLedger) { item := lb.ledgerPriorityQueue.Pop() lb.ledgerQueue <- item.payload - lb.currentLedger += lb.config.LedgerBatchConfig.LedgersPerFile + lb.currentLedger += lb.dataStore.GetSchema().LedgersPerFile } } diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 501ad51847..cd5d8af57b 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## Pending + +### Added + +- Reingest from pre-computed tx meta on remote cloud storage. ([4911](https://github.com/stellar/go/issues/4911)), ([5374](https://github.com/stellar/go/pull/5374)) + - Configure horizon reingestion to obtain ledger tx meta in pre-computed files from a Google Cloud Storage(GCS) location. + - Using this option will no longer require a captive core binary be present and it no longer runs a captive core sub-process, instead obtaining the tx meta from the GCS backend. + - Horizon supports this new feature with two new parameters `ledgerbackend` and `datastore-config` on the `reingest` command. Refer to [Reingestion README](./internal/ingest/README.md#reingestion). + + + ## 2.31.0 ### Breaking Changes diff --git a/services/horizon/cmd/db.go b/services/horizon/cmd/db.go index e2589a7385..92a732e002 100644 --- a/services/horizon/cmd/db.go +++ b/services/horizon/cmd/db.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" + "github.com/pelletier/go-toml" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -18,28 +19,43 @@ import ( "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/services/horizon/internal/db2/schema" "github.com/stellar/go/services/horizon/internal/ingest" + "github.com/stellar/go/support/config" support "github.com/stellar/go/support/config" "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" hlog "github.com/stellar/go/support/log" ) -var dbCmd = &cobra.Command{ - Use: "db [command]", - Short: "commands to manage horizon's postgres db", -} - -var dbMigrateCmd = &cobra.Command{ - Use: "migrate [command]", - Short: "commands to run schema migrations on horizon's postgres db", -} +var ( + runDBReingestRangeFn = runDBReingestRange + dbCmd *cobra.Command + dbMigrateCmd *cobra.Command + dbInitCmd *cobra.Command + dbMigrateDownCmd *cobra.Command + dbMigrateRedoCmd *cobra.Command + dbMigrateStatusCmd *cobra.Command + dbMigrateUpCmd *cobra.Command + dbReapCmd *cobra.Command + dbReingestCmd *cobra.Command + dbReingestRangeCmd *cobra.Command + dbFillGapsCmd *cobra.Command + dbDetectGapsCmd *cobra.Command + reingestForce bool + parallelWorkers uint + parallelJobSize uint32 + retries uint + retryBackoffSeconds uint + ledgerBackendStr string + storageBackendConfigPath string + ledgerBackendType ingest.LedgerBackendType +) -func requireAndSetFlags(names ...string) error { +func requireAndSetFlags(horizonFlags config.ConfigOptions, names ...string) error { set := map[string]bool{} for _, name := range names { set[name] = true } - for _, flag := range globalFlags { + for _, flag := range horizonFlags { if set[flag.Name] { flag.Require() if err := flag.SetValue(); err != nil { @@ -58,44 +74,17 @@ func requireAndSetFlags(names ...string) error { return fmt.Errorf("could not find %s flags", strings.Join(missing, ",")) } -var dbInitCmd = &cobra.Command{ - Use: "init", - Short: "install schema", - Long: "init initializes the postgres database used by horizon.", - RunE: func(cmd *cobra.Command, args []string) error { - if err := requireAndSetFlags(horizon.DatabaseURLFlagName, horizon.IngestFlagName); err != nil { - return err - } - - db, err := sql.Open("postgres", globalConfig.DatabaseURL) - if err != nil { - return err - } - - numMigrationsRun, err := schema.Migrate(db, schema.MigrateUp, 0) - if err != nil { - return err - } - - if numMigrationsRun == 0 { - log.Println("No migrations applied.") - } else { - log.Printf("Successfully applied %d migrations.\n", numMigrationsRun) - } - return nil - }, -} - -func migrate(dir schema.MigrateDir, count int) error { - if !globalConfig.Ingest { +func migrate(dir schema.MigrateDir, count int, horizonConfig *horizon.Config) error { + if !horizonConfig.Ingest { log.Println("Skipping migrations because ingest flag is not enabled") return nil } - dbConn, err := db.Open("postgres", globalConfig.DatabaseURL) + dbConn, err := db.Open("postgres", horizonConfig.DatabaseURL) if err != nil { return err } + defer dbConn.Close() numMigrationsRun, err := schema.Migrate(dbConn.DB.DB, dir, count) if err != nil { @@ -110,160 +99,6 @@ func migrate(dir schema.MigrateDir, count int) error { return nil } -var dbMigrateDownCmd = &cobra.Command{ - Use: "down COUNT", - Short: "run downwards db schema migrations", - Long: "performs a downards schema migration command", - RunE: func(cmd *cobra.Command, args []string) error { - if err := requireAndSetFlags(horizon.DatabaseURLFlagName, horizon.IngestFlagName); err != nil { - return err - } - - // Only allow invocations with 1 args. - if len(args) != 1 { - return ErrUsage{cmd} - } - - count, err := strconv.Atoi(args[0]) - if err != nil { - log.Println(err) - return ErrUsage{cmd} - } - - return migrate(schema.MigrateDown, count) - }, -} - -var dbMigrateRedoCmd = &cobra.Command{ - Use: "redo COUNT", - Short: "redo db schema migrations", - Long: "performs a redo schema migration command", - RunE: func(cmd *cobra.Command, args []string) error { - if err := requireAndSetFlags(horizon.DatabaseURLFlagName, horizon.IngestFlagName); err != nil { - return err - } - - // Only allow invocations with 1 args. - if len(args) != 1 { - return ErrUsage{cmd} - } - - count, err := strconv.Atoi(args[0]) - if err != nil { - log.Println(err) - return ErrUsage{cmd} - } - - return migrate(schema.MigrateRedo, count) - }, -} - -var dbMigrateStatusCmd = &cobra.Command{ - Use: "status", - Short: "print current database migration status", - Long: "print current database migration status", - RunE: func(cmd *cobra.Command, args []string) error { - if err := requireAndSetFlags(horizon.DatabaseURLFlagName); err != nil { - return err - } - - // Only allow invocations with 0 args. - if len(args) != 0 { - fmt.Println(args) - return ErrUsage{cmd} - } - - dbConn, err := db.Open("postgres", globalConfig.DatabaseURL) - if err != nil { - return err - } - - status, err := schema.Status(dbConn.DB.DB) - if err != nil { - return err - } - - fmt.Println(status) - return nil - }, -} - -var dbMigrateUpCmd = &cobra.Command{ - Use: "up [COUNT]", - Short: "run upwards db schema migrations", - Long: "performs an upwards schema migration command", - RunE: func(cmd *cobra.Command, args []string) error { - if err := requireAndSetFlags(horizon.DatabaseURLFlagName, horizon.IngestFlagName); err != nil { - return err - } - - // Only allow invocations with 0-1 args. - if len(args) > 1 { - return ErrUsage{cmd} - } - - count := 0 - if len(args) == 1 { - var err error - count, err = strconv.Atoi(args[0]) - if err != nil { - log.Println(err) - return ErrUsage{cmd} - } - } - - return migrate(schema.MigrateUp, count) - }, -} - -var dbReapCmd = &cobra.Command{ - Use: "reap", - Short: "reaps (i.e. removes) any reapable history data", - Long: "reap removes any historical data that is earlier than the configured retention cutoff", - RunE: func(cmd *cobra.Command, args []string) error { - - err := horizon.ApplyFlags(globalConfig, globalFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false, AlwaysIngest: false}) - if err != nil { - return err - } - - session, err := db.Open("postgres", globalConfig.DatabaseURL) - if err != nil { - return fmt.Errorf("cannot open Horizon DB: %v", err) - } - defer session.Close() - - reaper := ingest.NewReaper( - ingest.ReapConfig{ - RetentionCount: uint32(globalConfig.HistoryRetentionCount), - BatchSize: uint32(globalConfig.HistoryRetentionReapCount), - }, - session, - ) - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) - defer cancel() - return reaper.DeleteUnretainedHistory(ctx) - }, -} - -var dbReingestCmd = &cobra.Command{ - Use: "reingest", - Short: "reingest commands", - Long: "reingest ingests historical data for every ledger or ledgers specified by subcommand", - RunE: func(cmd *cobra.Command, args []string) error { - fmt.Println("Use one of the subcomands...") - return ErrUsage{cmd} - }, -} - -var ( - reingestForce bool - parallelWorkers uint - parallelJobSize uint32 - retries uint - retryBackoffSeconds uint -) - func ingestRangeCmdOpts() support.ConfigOptions { return support.ConfigOptions{ { @@ -307,108 +142,43 @@ func ingestRangeCmdOpts() support.ConfigOptions { FlagDefault: uint(5), Usage: "[optional] backoff seconds between reingest retries", }, + { + Name: "ledgerbackend", + ConfigKey: &ledgerBackendStr, + OptType: types.String, + Required: false, + FlagDefault: ingest.CaptiveCoreBackend.String(), + Usage: fmt.Sprintf("[optional] Specify the ledger backend type: '%s' (default) or '%s'", + ingest.CaptiveCoreBackend.String(), + ingest.BufferedStorageBackend.String()), + CustomSetValue: func(co *support.ConfigOption) error { + val := viper.GetString(co.Name) + switch val { + case ingest.CaptiveCoreBackend.String(): + ledgerBackendType = ingest.CaptiveCoreBackend + case ingest.BufferedStorageBackend.String(): + ledgerBackendType = ingest.BufferedStorageBackend + default: + return fmt.Errorf("invalid ledger backend: %s, must be 'captive-core' or 'datastore'", val) + } + *co.ConfigKey.(*string) = val + return nil + }, + }, + { + Name: "datastore-config", + ConfigKey: &storageBackendConfigPath, + OptType: types.String, + Required: false, + Usage: "[optional] Specify the path to the datastore config file (required for datastore backend)", + }, } } var dbReingestRangeCmdOpts = ingestRangeCmdOpts() -var dbReingestRangeCmd = &cobra.Command{ - Use: "range [Start sequence number] [End sequence number]", - Short: "reingests ledgers within a range", - Long: "reingests ledgers between X and Y sequence number (closed intervals)", - RunE: func(cmd *cobra.Command, args []string) error { - if err := dbReingestRangeCmdOpts.RequireE(); err != nil { - return err - } - if err := dbReingestRangeCmdOpts.SetValues(); err != nil { - return err - } - - if len(args) != 2 { - return ErrUsage{cmd} - } - - argsUInt32 := make([]uint32, 2) - for i, arg := range args { - if seq, err := strconv.ParseUint(arg, 10, 32); err != nil { - cmd.Usage() - return fmt.Errorf(`invalid sequence number "%s"`, arg) - } else { - argsUInt32[i] = uint32(seq) - } - } - - err := horizon.ApplyFlags(globalConfig, globalFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false, AlwaysIngest: true}) - if err != nil { - return err - } - return runDBReingestRange( - []history.LedgerRange{{StartSequence: argsUInt32[0], EndSequence: argsUInt32[1]}}, - reingestForce, - parallelWorkers, - *globalConfig, - ) - }, -} - var dbFillGapsCmdOpts = ingestRangeCmdOpts() -var dbFillGapsCmd = &cobra.Command{ - Use: "fill-gaps [Start sequence number] [End sequence number]", - Short: "Ingests any gaps found in the horizon db", - Long: "Ingests any gaps found in the horizon db. The command takes an optional start and end parameters which restrict the range of ledgers ingested.", - RunE: func(cmd *cobra.Command, args []string) error { - if err := dbFillGapsCmdOpts.RequireE(); err != nil { - return err - } - if err := dbFillGapsCmdOpts.SetValues(); err != nil { - return err - } - - if len(args) != 0 && len(args) != 2 { - hlog.Errorf("Expected either 0 arguments or 2 but found %v arguments", len(args)) - return ErrUsage{cmd} - } - var start, end uint64 - var withRange bool - if len(args) == 2 { - var err error - start, err = strconv.ParseUint(args[0], 10, 32) - if err != nil { - cmd.Usage() - return fmt.Errorf(`invalid sequence number "%s"`, args[0]) - } - end, err = strconv.ParseUint(args[1], 10, 32) - if err != nil { - cmd.Usage() - return fmt.Errorf(`invalid sequence number "%s"`, args[1]) - } - withRange = true - } - - err := horizon.ApplyFlags(globalConfig, globalFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false, AlwaysIngest: true}) - if err != nil { - return err - } - var gaps []history.LedgerRange - if withRange { - gaps, err = runDBDetectGapsInRange(*globalConfig, uint32(start), uint32(end)) - if err != nil { - return err - } - hlog.Infof("found gaps %v within range [%v, %v]", gaps, start, end) - } else { - gaps, err = runDBDetectGaps(*globalConfig) - if err != nil { - return err - } - hlog.Infof("found gaps %v", gaps) - } - - return runDBReingestRange(gaps, reingestForce, parallelWorkers, *globalConfig) - }, -} - -func runDBReingestRange(ledgerRanges []history.LedgerRange, reingestForce bool, parallelWorkers uint, config horizon.Config) error { +func runDBReingestRange(ledgerRanges []history.LedgerRange, reingestForce bool, parallelWorkers uint, config horizon.Config, storageBackendConfig ingest.StorageBackendConfig) error { var err error if reingestForce && parallelWorkers > 1 { @@ -435,6 +205,8 @@ func runDBReingestRange(ledgerRanges []history.LedgerRange, reingestForce bool, RoundingSlippageFilter: config.RoundingSlippageFilter, MaxLedgerPerFlush: maxLedgersPerFlush, SkipTxmeta: config.SkipTxmeta, + LedgerBackendType: ledgerBackendType, + StorageBackendConfig: storageBackendConfig, } if ingestConfig.HistorySession, err = db.Open("postgres", config.DatabaseURL); err != nil { @@ -476,35 +248,6 @@ the reingest command completes.`) return nil } -var dbDetectGapsCmd = &cobra.Command{ - Use: "detect-gaps", - Short: "detects ingestion gaps in Horizon's database", - Long: "detects ingestion gaps in Horizon's database and prints a list of reingest commands needed to fill the gaps", - RunE: func(cmd *cobra.Command, args []string) error { - if err := requireAndSetFlags(horizon.DatabaseURLFlagName); err != nil { - return err - } - - if len(args) != 0 { - return ErrUsage{cmd} - } - gaps, err := runDBDetectGaps(*globalConfig) - if err != nil { - return err - } - if len(gaps) == 0 { - hlog.Info("No gaps found") - return nil - } - fmt.Println("Horizon commands to run in order to fill in the gaps:") - cmdname := os.Args[0] - for _, g := range gaps { - fmt.Printf("%s db reingest range %d %d\n", cmdname, g.StartSequence, g.EndSequence) - } - return nil - }, -} - func runDBDetectGaps(config horizon.Config) ([]history.LedgerRange, error) { horizonSession, err := db.Open("postgres", config.DatabaseURL) if err != nil { @@ -525,7 +268,340 @@ func runDBDetectGapsInRange(config horizon.Config, start, end uint32) ([]history return q.GetLedgerGapsInRange(context.Background(), start, end) } -func init() { +func DefineDBCommands(rootCmd *cobra.Command, horizonConfig *horizon.Config, horizonFlags config.ConfigOptions) { + dbCmd = &cobra.Command{ + Use: "db [command]", + Short: "commands to manage horizon's postgres db", + } + + dbMigrateCmd = &cobra.Command{ + Use: "migrate [command]", + Short: "commands to run schema migrations on horizon's postgres db", + } + + dbInitCmd = &cobra.Command{ + Use: "init", + Short: "install schema", + Long: "init initializes the postgres database used by horizon.", + RunE: func(cmd *cobra.Command, args []string) error { + if err := requireAndSetFlags(horizonFlags, horizon.DatabaseURLFlagName, horizon.IngestFlagName); err != nil { + return err + } + + db, err := sql.Open("postgres", horizonConfig.DatabaseURL) + if err != nil { + return err + } + + numMigrationsRun, err := schema.Migrate(db, schema.MigrateUp, 0) + if err != nil { + return err + } + + if numMigrationsRun == 0 { + log.Println("No migrations applied.") + } else { + log.Printf("Successfully applied %d migrations.\n", numMigrationsRun) + } + return nil + }, + } + + dbMigrateDownCmd = &cobra.Command{ + Use: "down COUNT", + Short: "run downwards db schema migrations", + Long: "performs a downards schema migration command", + RunE: func(cmd *cobra.Command, args []string) error { + if err := requireAndSetFlags(horizonFlags, horizon.DatabaseURLFlagName, horizon.IngestFlagName); err != nil { + return err + } + + // Only allow invocations with 1 args. + if len(args) != 1 { + return ErrUsage{cmd} + } + + count, err := strconv.Atoi(args[0]) + if err != nil { + log.Println(err) + return ErrUsage{cmd} + } + + return migrate(schema.MigrateDown, count, horizonConfig) + }, + } + + dbMigrateRedoCmd = &cobra.Command{ + Use: "redo COUNT", + Short: "redo db schema migrations", + Long: "performs a redo schema migration command", + RunE: func(cmd *cobra.Command, args []string) error { + if err := requireAndSetFlags(horizonFlags, horizon.DatabaseURLFlagName, horizon.IngestFlagName); err != nil { + return err + } + + // Only allow invocations with 1 args. + if len(args) != 1 { + return ErrUsage{cmd} + } + + count, err := strconv.Atoi(args[0]) + if err != nil { + log.Println(err) + return ErrUsage{cmd} + } + + return migrate(schema.MigrateRedo, count, horizonConfig) + }, + } + + dbMigrateStatusCmd = &cobra.Command{ + Use: "status", + Short: "print current database migration status", + Long: "print current database migration status", + RunE: func(cmd *cobra.Command, args []string) error { + if err := requireAndSetFlags(horizonFlags, horizon.DatabaseURLFlagName); err != nil { + return err + } + + // Only allow invocations with 0 args. + if len(args) != 0 { + fmt.Println(args) + return ErrUsage{cmd} + } + + dbConn, err := db.Open("postgres", horizonConfig.DatabaseURL) + if err != nil { + return err + } + + status, err := schema.Status(dbConn.DB.DB) + if err != nil { + return err + } + + fmt.Println(status) + return nil + }, + } + + dbMigrateUpCmd = &cobra.Command{ + Use: "up [COUNT]", + Short: "run upwards db schema migrations", + Long: "performs an upwards schema migration command", + RunE: func(cmd *cobra.Command, args []string) error { + if err := requireAndSetFlags(horizonFlags, horizon.DatabaseURLFlagName, horizon.IngestFlagName); err != nil { + return err + } + + // Only allow invocations with 0-1 args. + if len(args) > 1 { + return ErrUsage{cmd} + } + + count := 0 + if len(args) == 1 { + var err error + count, err = strconv.Atoi(args[0]) + if err != nil { + log.Println(err) + return ErrUsage{cmd} + } + } + + return migrate(schema.MigrateUp, count, horizonConfig) + }, + } + + dbReapCmd = &cobra.Command{ + Use: "reap", + Short: "reaps (i.e. removes) any reapable history data", + Long: "reap removes any historical data that is earlier than the configured retention cutoff", + RunE: func(cmd *cobra.Command, args []string) error { + + err := horizon.ApplyFlags(horizonConfig, horizonFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false}) + if err != nil { + return err + } + + session, err := db.Open("postgres", horizonConfig.DatabaseURL) + if err != nil { + return fmt.Errorf("cannot open Horizon DB: %v", err) + } + defer session.Close() + + reaper := ingest.NewReaper( + ingest.ReapConfig{ + RetentionCount: uint32(horizonConfig.HistoryRetentionCount), + BatchSize: uint32(horizonConfig.HistoryRetentionReapCount), + }, + session, + ) + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) + defer cancel() + return reaper.DeleteUnretainedHistory(ctx) + }, + } + + dbReingestCmd = &cobra.Command{ + Use: "reingest", + Short: "reingest commands", + Long: "reingest ingests historical data for every ledger or ledgers specified by subcommand", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("Use one of the subcomands...") + return ErrUsage{cmd} + }, + } + + dbReingestRangeCmd = &cobra.Command{ + Use: "range [Start sequence number] [End sequence number]", + Short: "reingests ledgers within a range", + Long: "reingests ledgers between X and Y sequence number (closed intervals)", + RunE: func(cmd *cobra.Command, args []string) error { + if err := dbReingestRangeCmdOpts.RequireE(); err != nil { + return err + } + if err := dbReingestRangeCmdOpts.SetValues(); err != nil { + return err + } + + if len(args) != 2 { + return ErrUsage{cmd} + } + + argsUInt32 := make([]uint32, 2) + for i, arg := range args { + if seq, err := strconv.ParseUint(arg, 10, 32); err != nil { + cmd.Usage() + return fmt.Errorf(`invalid sequence number "%s"`, arg) + } else { + argsUInt32[i] = uint32(seq) + } + } + + var err error + var storageBackendConfig ingest.StorageBackendConfig + options := horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false} + if ledgerBackendType == ingest.BufferedStorageBackend { + if storageBackendConfig, err = loadStorageBackendConfig(storageBackendConfigPath); err != nil { + return err + } + // when using buffered storage, performance observations have noted optimal parallel batch size + // of 100, apply that as default if the flag was absent. + if !viper.IsSet("parallel-job-size") { + parallelJobSize = 100 + } + options.NoCaptiveCore = true + } + + if err = horizon.ApplyFlags(horizonConfig, horizonFlags, options); err != nil { + return err + } + return runDBReingestRangeFn( + []history.LedgerRange{{StartSequence: argsUInt32[0], EndSequence: argsUInt32[1]}}, + reingestForce, + parallelWorkers, + *horizonConfig, + storageBackendConfig, + ) + }, + } + + dbFillGapsCmd = &cobra.Command{ + Use: "fill-gaps [Start sequence number] [End sequence number]", + Short: "Ingests any gaps found in the horizon db", + Long: "Ingests any gaps found in the horizon db. The command takes an optional start and end parameters which restrict the range of ledgers ingested.", + RunE: func(cmd *cobra.Command, args []string) error { + if err := dbFillGapsCmdOpts.RequireE(); err != nil { + return err + } + if err := dbFillGapsCmdOpts.SetValues(); err != nil { + return err + } + + if len(args) != 0 && len(args) != 2 { + hlog.Errorf("Expected either 0 arguments or 2 but found %v arguments", len(args)) + return ErrUsage{cmd} + } + + var start, end uint64 + var withRange bool + if len(args) == 2 { + var err error + start, err = strconv.ParseUint(args[0], 10, 32) + if err != nil { + cmd.Usage() + return fmt.Errorf(`invalid sequence number "%s"`, args[0]) + } + end, err = strconv.ParseUint(args[1], 10, 32) + if err != nil { + cmd.Usage() + return fmt.Errorf(`invalid sequence number "%s"`, args[1]) + } + withRange = true + } + + var err error + var storageBackendConfig ingest.StorageBackendConfig + options := horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false} + if ledgerBackendType == ingest.BufferedStorageBackend { + if storageBackendConfig, err = loadStorageBackendConfig(storageBackendConfigPath); err != nil { + return err + } + options.NoCaptiveCore = true + } + + if err = horizon.ApplyFlags(horizonConfig, horizonFlags, options); err != nil { + return err + } + var gaps []history.LedgerRange + if withRange { + gaps, err = runDBDetectGapsInRange(*horizonConfig, uint32(start), uint32(end)) + if err != nil { + return err + } + hlog.Infof("found gaps %v within range [%v, %v]", gaps, start, end) + } else { + gaps, err = runDBDetectGaps(*horizonConfig) + if err != nil { + return err + } + hlog.Infof("found gaps %v", gaps) + } + + return runDBReingestRangeFn(gaps, reingestForce, parallelWorkers, *horizonConfig, storageBackendConfig) + }, + } + + dbDetectGapsCmd = &cobra.Command{ + Use: "detect-gaps", + Short: "detects ingestion gaps in Horizon's database", + Long: "detects ingestion gaps in Horizon's database and prints a list of reingest commands needed to fill the gaps", + RunE: func(cmd *cobra.Command, args []string) error { + if err := requireAndSetFlags(horizonFlags, horizon.DatabaseURLFlagName); err != nil { + return err + } + + if len(args) != 0 { + return ErrUsage{cmd} + } + gaps, err := runDBDetectGaps(*horizonConfig) + if err != nil { + return err + } + if len(gaps) == 0 { + hlog.Info("No gaps found") + return nil + } + fmt.Println("Horizon commands to run in order to fill in the gaps:") + cmdname := os.Args[0] + for _, g := range gaps { + fmt.Printf("%s db reingest range %d %d\n", cmdname, g.StartSequence, g.EndSequence) + } + return nil + }, + } + if err := dbReingestRangeCmdOpts.Init(dbReingestRangeCmd); err != nil { log.Fatal(err.Error()) } @@ -536,7 +612,7 @@ func init() { viper.BindPFlags(dbReingestRangeCmd.PersistentFlags()) viper.BindPFlags(dbFillGapsCmd.PersistentFlags()) - RootCmd.AddCommand(dbCmd) + rootCmd.AddCommand(dbCmd) dbCmd.AddCommand( dbInitCmd, dbMigrateCmd, @@ -553,3 +629,23 @@ func init() { ) dbReingestCmd.AddCommand(dbReingestRangeCmd) } + +func loadStorageBackendConfig(storageBackendConfigPath string) (ingest.StorageBackendConfig, error) { + if storageBackendConfigPath == "" { + return ingest.StorageBackendConfig{}, errors.New("datastore config file is required for datastore ledgerbackend type") + } + cfg, err := toml.LoadFile(storageBackendConfigPath) + if err != nil { + return ingest.StorageBackendConfig{}, fmt.Errorf("failed to load datastore ledgerbackend config file %v: %w", storageBackendConfigPath, err) + } + var storageBackendConfig ingest.StorageBackendConfig + if err = cfg.Unmarshal(&storageBackendConfig); err != nil { + return ingest.StorageBackendConfig{}, fmt.Errorf("error unmarshalling datastore ledgerbackend TOML config: %w", err) + } + + return storageBackendConfig, nil +} + +func init() { + DefineDBCommands(RootCmd, globalConfig, globalFlags) +} diff --git a/services/horizon/cmd/db_test.go b/services/horizon/cmd/db_test.go new file mode 100644 index 0000000000..6a00576bd3 --- /dev/null +++ b/services/horizon/cmd/db_test.go @@ -0,0 +1,266 @@ +package cmd + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + horizon "github.com/stellar/go/services/horizon/internal" + "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/services/horizon/internal/ingest" + "github.com/stellar/go/support/db/dbtest" +) + +func TestDBCommandsTestSuite(t *testing.T) { + dbCmdSuite := &DBCommandsTestSuite{} + suite.Run(t, dbCmdSuite) +} + +type DBCommandsTestSuite struct { + suite.Suite + db *dbtest.DB + rootCmd *cobra.Command +} + +func (s *DBCommandsTestSuite) SetupSuite() { + runDBReingestRangeFn = func([]history.LedgerRange, bool, uint, + horizon.Config, ingest.StorageBackendConfig) error { + return nil + } + + s.db = dbtest.Postgres(s.T()) + + RootCmd.SetArgs([]string{ + "db", "migrate", "up", "--db-url", s.db.DSN}) + require.NoError(s.T(), RootCmd.Execute()) +} + +func (s *DBCommandsTestSuite) TearDownSuite() { + s.db.Close() +} + +func (s *DBCommandsTestSuite) BeforeTest(suiteName string, testName string) { + s.rootCmd = NewRootCmd() +} + +func (s *DBCommandsTestSuite) TestDefaultParallelJobSizeForBufferedBackend() { + s.rootCmd.SetArgs([]string{ + "db", "reingest", "range", + "--db-url", s.db.DSN, + "--network", "testnet", + "--parallel-workers", "2", + "--ledgerbackend", "datastore", + "--datastore-config", "../internal/ingest/testdata/config.storagebackend.toml", + "2", + "10"}) + + require.NoError(s.T(), s.rootCmd.Execute()) + require.Equal(s.T(), parallelJobSize, uint32(100)) +} + +func (s *DBCommandsTestSuite) TestDefaultParallelJobSizeForCaptiveBackend() { + s.rootCmd.SetArgs([]string{ + "db", "reingest", "range", + "--db-url", s.db.DSN, + "--network", "testnet", + "--stellar-core-binary-path", "/test/core/bin/path", + "--parallel-workers", "2", + "--ledgerbackend", "captive-core", + "2", + "10"}) + + require.NoError(s.T(), s.rootCmd.Execute()) + require.Equal(s.T(), parallelJobSize, uint32(100_000)) +} + +func (s *DBCommandsTestSuite) TestUsesParallelJobSizeWhenSetForCaptive() { + s.rootCmd.SetArgs([]string{ + "db", "reingest", "range", + "--db-url", s.db.DSN, + "--network", "testnet", + "--stellar-core-binary-path", "/test/core/bin/path", + "--parallel-workers", "2", + "--parallel-job-size", "5", + "--ledgerbackend", "captive-core", + "2", + "10"}) + + require.NoError(s.T(), s.rootCmd.Execute()) + require.Equal(s.T(), parallelJobSize, uint32(5)) +} + +func (s *DBCommandsTestSuite) TestUsesParallelJobSizeWhenSetForBuffered() { + s.rootCmd.SetArgs([]string{ + "db", "reingest", "range", + "--db-url", s.db.DSN, + "--network", "testnet", + "--parallel-workers", "2", + "--parallel-job-size", "5", + "--ledgerbackend", "datastore", + "--datastore-config", "../internal/ingest/testdata/config.storagebackend.toml", + "2", + "10"}) + + require.NoError(s.T(), s.rootCmd.Execute()) + require.Equal(s.T(), parallelJobSize, uint32(5)) +} + +func (s *DBCommandsTestSuite) TestDbReingestAndFillGapsCmds() { + tests := []struct { + name string + args []string + ledgerBackend ingest.LedgerBackendType + expectError bool + errorMessage string + }{ + { + name: "default; w/ individual network flags", + args: []string{ + "1", "100", + "--network-passphrase", "passphrase", + "--history-archive-urls", "[]", + }, + expectError: false, + }, + { + name: "default; w/o individual network flags", + args: []string{ + "1", "100", + }, + expectError: true, + errorMessage: "network-passphrase must be set", + }, + { + name: "default; no history-archive-urls flag", + args: []string{ + "1", "100", + "--network-passphrase", "passphrase", + }, + expectError: true, + errorMessage: "history-archive-urls must be set", + }, + { + name: "default; w/ network parameter", + args: []string{ + "1", "100", + "--network", "testnet", + }, + expectError: false, + }, + { + name: "datastore; w/ individual network flags", + args: []string{ + "1", "100", + "--ledgerbackend", "datastore", + "--datastore-config", "../internal/ingest/testdata/config.storagebackend.toml", + "--network-passphrase", "passphrase", + "--history-archive-urls", "[]", + }, + expectError: false, + }, + { + name: "datastore; w/o individual network flags", + args: []string{ + "1", "100", + "--ledgerbackend", "datastore", + "--datastore-config", "../internal/ingest/testdata/config.storagebackend.toml", + }, + expectError: true, + errorMessage: "network-passphrase must be set", + }, + { + name: "datastore; no history-archive-urls flag", + args: []string{ + "1", "100", + "--ledgerbackend", "datastore", + "--datastore-config", "../internal/ingest/testdata/config.storagebackend.toml", + "--network-passphrase", "passphrase", + }, + expectError: true, + errorMessage: "history-archive-urls must be set", + }, + { + name: "captive-core; valid", + args: []string{ + "1", "100", + "--network", "testnet", + "--ledgerbackend", "captive-core", + }, + expectError: false, + }, + { + name: "invalid datastore", + args: []string{ + "1", "100", + "--network", "testnet", + "--ledgerbackend", "unknown", + }, + expectError: true, + errorMessage: "invalid ledger backend: unknown, must be 'captive-core' or 'datastore'", + }, + { + name: "datastore; missing config file", + args: []string{ + "1", "100", + "--network", "testnet", + "--ledgerbackend", "datastore", + "--datastore-config", "invalid.config.toml", + }, + expectError: true, + errorMessage: "failed to load datastore ledgerbackend config file", + }, + { + name: "datastore; w/ config", + args: []string{ + "1", "100", + "--network", "testnet", + "--ledgerbackend", "datastore", + "--datastore-config", "../internal/ingest/testdata/config.storagebackend.toml", + }, + expectError: false, + }, + { + name: "datastore; w/o config", + args: []string{ + "1", "100", + "--network", "testnet", + "--ledgerbackend", "datastore", + }, + expectError: true, + errorMessage: "datastore config file is required for datastore ledgerbackend type", + }, + } + + commands := []struct { + cmd []string + name string + }{ + {[]string{"db", "reingest", "range"}, "TestDbReingestRangeCmd"}, + {[]string{"db", "fill-gaps"}, "TestDbFillGapsCmd"}, + } + + for _, command := range commands { + for _, tt := range tests { + s.T().Run(tt.name+"_"+command.name, func(t *testing.T) { + + rootCmd := NewRootCmd() + var args []string + args = append(command.cmd, tt.args...) + rootCmd.SetArgs(append([]string{ + "--db-url", s.db.DSN, + "--stellar-core-binary-path", "/test/core/bin/path", + }, args...)) + + if tt.expectError { + err := rootCmd.Execute() + require.Error(t, err) + require.Contains(t, err.Error(), tt.errorMessage) + } else { + require.NoError(t, rootCmd.Execute()) + } + }) + } + } +} diff --git a/services/horizon/cmd/ingest.go b/services/horizon/cmd/ingest.go index 864067da8f..f6b94a8f52 100644 --- a/services/horizon/cmd/ingest.go +++ b/services/horizon/cmd/ingest.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/stellar/go/historyarchive" horizon "github.com/stellar/go/services/horizon/internal" "github.com/stellar/go/services/horizon/internal/db2/history" @@ -94,7 +95,7 @@ var ingestVerifyRangeCmd = &cobra.Command{ co.SetValue() } - if err := horizon.ApplyFlags(globalConfig, globalFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false, AlwaysIngest: true}); err != nil { + if err := horizon.ApplyFlags(globalConfig, globalFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false}); err != nil { return err } @@ -189,7 +190,7 @@ var ingestStressTestCmd = &cobra.Command{ co.SetValue() } - if err := horizon.ApplyFlags(globalConfig, globalFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false, AlwaysIngest: true}); err != nil { + if err := horizon.ApplyFlags(globalConfig, globalFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false}); err != nil { return err } @@ -239,7 +240,7 @@ var ingestTriggerStateRebuildCmd = &cobra.Command{ Short: "updates a database to trigger state rebuild, state will be rebuilt by a running Horizon instance, DO NOT RUN production DB, some endpoints will be unavailable until state is rebuilt", RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - if err := horizon.ApplyFlags(globalConfig, globalFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false, AlwaysIngest: true}); err != nil { + if err := horizon.ApplyFlags(globalConfig, globalFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false}); err != nil { return err } @@ -263,7 +264,7 @@ var ingestInitGenesisStateCmd = &cobra.Command{ Short: "ingests genesis state (ledger 1)", RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - if err := horizon.ApplyFlags(globalConfig, globalFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false, AlwaysIngest: true}); err != nil { + if err := horizon.ApplyFlags(globalConfig, globalFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false}); err != nil { return err } @@ -320,7 +321,7 @@ var ingestBuildStateCmd = &cobra.Command{ co.SetValue() } - if err := horizon.ApplyFlags(globalConfig, globalFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false, AlwaysIngest: true}); err != nil { + if err := horizon.ApplyFlags(globalConfig, globalFlags, horizon.ApplyOptions{RequireCaptiveCoreFullConfig: false}); err != nil { return err } diff --git a/services/horizon/cmd/root.go b/services/horizon/cmd/root.go index d2900496d4..099979c97b 100644 --- a/services/horizon/cmd/root.go +++ b/services/horizon/cmd/root.go @@ -12,7 +12,13 @@ import ( var ( globalConfig, globalFlags = horizon.Flags() - RootCmd = &cobra.Command{ + RootCmd = createRootCmd(globalConfig, globalFlags) + originalHelpFunc = RootCmd.HelpFunc() + originalUsageFunc = RootCmd.UsageFunc() +) + +func createRootCmd(horizonConfig *horizon.Config, configOptions config.ConfigOptions) *cobra.Command { + return &cobra.Command{ Use: "horizon", Short: "client-facing api server for the Stellar network", SilenceErrors: true, @@ -23,16 +29,44 @@ var ( "DEPRECATED - the use of command-line flags has been deprecated in favor of environment variables. Please" + "consult our Configuring section in the developer documentation on how to use them - https://developers.stellar.org/docs/run-api-server/configuring", RunE: func(cmd *cobra.Command, args []string) error { - app, err := horizon.NewAppFromFlags(globalConfig, globalFlags) + app, err := horizon.NewAppFromFlags(horizonConfig, configOptions) if err != nil { return err } return app.Serve() }, } - originalHelpFunc = RootCmd.HelpFunc() - originalUsageFunc = RootCmd.UsageFunc() -) +} + +func initRootCmd(cmd *cobra.Command, + originalHelpFn func(*cobra.Command, []string), + originalUsageFn func(*cobra.Command) error, + horizonGlobalFlags config.ConfigOptions) { + // override the default help output, apply further filtering on which global flags + // will be shown on the help outout dependent on the command help was issued upon. + cmd.SetHelpFunc(func(c *cobra.Command, args []string) { + enableGlobalOptionsInHelp(c, horizonGlobalFlags) + originalHelpFn(c, args) + }) + + cmd.SetUsageFunc(func(c *cobra.Command) error { + enableGlobalOptionsInHelp(c, horizonGlobalFlags) + return originalUsageFn(c) + }) + + err := horizonGlobalFlags.Init(cmd) + if err != nil { + stdLog.Fatal(err.Error()) + } +} + +func NewRootCmd() *cobra.Command { + horizonGlobalConfig, horizonGlobalFlags := horizon.Flags() + cmd := createRootCmd(horizonGlobalConfig, horizonGlobalFlags) + initRootCmd(cmd, cmd.HelpFunc(), cmd.UsageFunc(), horizonGlobalFlags) + DefineDBCommands(cmd, horizonGlobalConfig, horizonGlobalFlags) + return cmd +} // ErrUsage indicates we should print the usage string and exit with code 1 type ErrUsage struct { @@ -51,23 +85,7 @@ func (e ErrExitCode) Error() string { } func init() { - - // override the default help output, apply further filtering on which global flags - // will be shown on the help outout dependent on the command help was issued upon. - RootCmd.SetHelpFunc(func(c *cobra.Command, args []string) { - enableGlobalOptionsInHelp(c, globalFlags) - originalHelpFunc(c, args) - }) - - RootCmd.SetUsageFunc(func(c *cobra.Command) error { - enableGlobalOptionsInHelp(c, globalFlags) - return originalUsageFunc(c) - }) - - err := globalFlags.Init(RootCmd) - if err != nil { - stdLog.Fatal(err.Error()) - } + initRootCmd(RootCmd, originalHelpFunc, originalUsageFunc, globalFlags) } func Execute() error { diff --git a/services/horizon/internal/flags.go b/services/horizon/internal/flags.go index 9930fee69f..38fca67576 100644 --- a/services/horizon/internal/flags.go +++ b/services/horizon/internal/flags.go @@ -832,7 +832,7 @@ func Flags() (*Config, support.ConfigOptions) { // NewAppFromFlags constructs a new Horizon App from the given command line flags func NewAppFromFlags(config *Config, flags support.ConfigOptions) (*App, error) { - err := ApplyFlags(config, flags, ApplyOptions{RequireCaptiveCoreFullConfig: true, AlwaysIngest: false}) + err := ApplyFlags(config, flags, ApplyOptions{RequireCaptiveCoreFullConfig: true}) if err != nil { return nil, err } @@ -850,30 +850,10 @@ func NewAppFromFlags(config *Config, flags support.ConfigOptions) (*App, error) } type ApplyOptions struct { - AlwaysIngest bool RequireCaptiveCoreFullConfig bool + NoCaptiveCore bool } -type networkConfig struct { - defaultConfig []byte - HistoryArchiveURLs []string - NetworkPassphrase string -} - -var ( - PubnetConf = networkConfig{ - defaultConfig: ledgerbackend.PubnetDefaultConfig, - HistoryArchiveURLs: network.PublicNetworkhistoryArchiveURLs, - NetworkPassphrase: network.PublicNetworkPassphrase, - } - - TestnetConf = networkConfig{ - defaultConfig: ledgerbackend.TestnetDefaultConfig, - HistoryArchiveURLs: network.TestNetworkhistoryArchiveURLs, - NetworkPassphrase: network.TestNetworkPassphrase, - } -) - // getCaptiveCoreBinaryPath retrieves the path of the Captive Core binary // Returns the path or an error if the binary is not found func getCaptiveCoreBinaryPath() (string, error) { @@ -884,69 +864,32 @@ func getCaptiveCoreBinaryPath() (string, error) { return result, nil } -// getCaptiveCoreConfigFromNetworkParameter returns the default Captive Core configuration based on the network. -func getCaptiveCoreConfigFromNetworkParameter(config *Config) (networkConfig, error) { - var defaultNetworkConfig networkConfig - - if config.NetworkPassphrase != "" { - return defaultNetworkConfig, fmt.Errorf("invalid config: %s parameter not allowed with the %s parameter", - NetworkPassphraseFlagName, NetworkFlagName) - } - - if len(config.HistoryArchiveURLs) > 0 { - return defaultNetworkConfig, fmt.Errorf("invalid config: %s parameter not allowed with the %s parameter", - HistoryArchiveURLsFlagName, NetworkFlagName) - } - - switch config.Network { - case StellarPubnet: - defaultNetworkConfig = PubnetConf - case StellarTestnet: - defaultNetworkConfig = TestnetConf - default: - return defaultNetworkConfig, fmt.Errorf("no default configuration found for network %s", config.Network) - } - - return defaultNetworkConfig, nil -} - // setCaptiveCoreConfiguration prepares configuration for the Captive Core func setCaptiveCoreConfiguration(config *Config, options ApplyOptions) error { stdLog.Println("Preparing captive core...") + var err error // If the user didn't specify a Stellar Core binary, we can check the // $PATH and possibly fill it in for them. if config.CaptiveCoreBinaryPath == "" { - var err error if config.CaptiveCoreBinaryPath, err = getCaptiveCoreBinaryPath(); err != nil { return fmt.Errorf("captive core requires %s", StellarCoreBinaryPathName) } } - var defaultNetworkConfig networkConfig - if config.Network != "" { - var err error - defaultNetworkConfig, err = getCaptiveCoreConfigFromNetworkParameter(config) - if err != nil { - return err - } - config.NetworkPassphrase = defaultNetworkConfig.NetworkPassphrase - config.HistoryArchiveURLs = defaultNetworkConfig.HistoryArchiveURLs - } else { - if config.NetworkPassphrase == "" { - return fmt.Errorf("%s must be set", NetworkPassphraseFlagName) - } + var defaultCaptiveCoreConfig []byte + switch config.Network { + case StellarPubnet: + defaultCaptiveCoreConfig = ledgerbackend.PubnetDefaultConfig + case StellarTestnet: - if len(config.HistoryArchiveURLs) == 0 { - return fmt.Errorf("%s must be set", HistoryArchiveURLsFlagName) - } + defaultCaptiveCoreConfig = ledgerbackend.TestnetDefaultConfig } config.CaptiveCoreTomlParams.CoreBinaryPath = config.CaptiveCoreBinaryPath config.CaptiveCoreTomlParams.HistoryArchiveURLs = config.HistoryArchiveURLs config.CaptiveCoreTomlParams.NetworkPassphrase = config.NetworkPassphrase - var err error if config.CaptiveCoreConfigPath != "" { config.CaptiveCoreToml, err = ledgerbackend.NewCaptiveCoreTomlFromFile(config.CaptiveCoreConfigPath, config.CaptiveCoreTomlParams) @@ -960,8 +903,8 @@ func setCaptiveCoreConfiguration(config *Config, options ApplyOptions) error { if err != nil { return errors.Wrap(err, "invalid captive core toml file") } - } else if len(defaultNetworkConfig.defaultConfig) != 0 { - config.CaptiveCoreToml, err = ledgerbackend.NewCaptiveCoreTomlFromData(defaultNetworkConfig.defaultConfig, + } else if len(defaultCaptiveCoreConfig) != 0 { + config.CaptiveCoreToml, err = ledgerbackend.NewCaptiveCoreTomlFromData(defaultCaptiveCoreConfig, config.CaptiveCoreTomlParams) if err != nil { return errors.Wrap(err, "invalid captive core toml file") @@ -1004,10 +947,6 @@ func ApplyFlags(config *Config, flags support.ConfigOptions, options ApplyOption return err } - if options.AlwaysIngest { - config.Ingest = true - } - if config.Ingest { // Migrations should be checked as early as possible. Apply and check // only on ingesting instances which are required to have write-access @@ -1023,9 +962,15 @@ func ApplyFlags(config *Config, flags support.ConfigOptions, options ApplyOption return err } - err := setCaptiveCoreConfiguration(config, options) - if err != nil { - return errors.Wrap(err, "error generating captive core configuration") + if err := setNetworkConfiguration(config); err != nil { + return err + } + + if !options.NoCaptiveCore { + err := setCaptiveCoreConfiguration(config, options) + if err != nil { + return errors.Wrap(err, "error generating captive core configuration") + } } } @@ -1061,3 +1006,37 @@ func ApplyFlags(config *Config, flags support.ConfigOptions, options ApplyOption return nil } + +func setNetworkConfiguration(config *Config) error { + if config.Network != "" { + if config.NetworkPassphrase != "" { + return fmt.Errorf("invalid config: %s parameter not allowed with the %s parameter", + NetworkPassphraseFlagName, NetworkFlagName) + } + + if len(config.HistoryArchiveURLs) > 0 { + return fmt.Errorf("invalid config: %s parameter not allowed with the %s parameter", + HistoryArchiveURLsFlagName, NetworkFlagName) + } + + switch config.Network { + case StellarPubnet: + config.NetworkPassphrase = network.PublicNetworkPassphrase + config.HistoryArchiveURLs = network.PublicNetworkhistoryArchiveURLs + case StellarTestnet: + config.NetworkPassphrase = network.TestNetworkPassphrase + config.HistoryArchiveURLs = network.TestNetworkhistoryArchiveURLs + default: + return fmt.Errorf("no default configuration found for network %s", config.Network) + } + } + + if config.NetworkPassphrase == "" { + return fmt.Errorf("%s must be set", NetworkPassphraseFlagName) + } + + if len(config.HistoryArchiveURLs) == 0 { + return fmt.Errorf("%s must be set", HistoryArchiveURLsFlagName) + } + return nil +} diff --git a/services/horizon/internal/flags_test.go b/services/horizon/internal/flags_test.go index 76ec1ffd8d..4d8d352080 100644 --- a/services/horizon/internal/flags_test.go +++ b/services/horizon/internal/flags_test.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" + "github.com/stellar/go/network" "github.com/stellar/go/services/horizon/internal/test" "github.com/stretchr/testify/assert" @@ -29,16 +30,16 @@ func Test_createCaptiveCoreDefaultConfig(t *testing.T) { config: Config{Network: StellarTestnet, CaptiveCoreBinaryPath: "/path/to/captive-core/binary", }, - networkPassphrase: TestnetConf.NetworkPassphrase, - historyArchiveURLs: TestnetConf.HistoryArchiveURLs, + networkPassphrase: network.TestNetworkPassphrase, + historyArchiveURLs: network.TestNetworkhistoryArchiveURLs, }, { name: "pubnet default config", config: Config{Network: StellarPubnet, CaptiveCoreBinaryPath: "/path/to/captive-core/binary", }, - networkPassphrase: PubnetConf.NetworkPassphrase, - historyArchiveURLs: PubnetConf.HistoryArchiveURLs, + networkPassphrase: network.PublicNetworkPassphrase, + historyArchiveURLs: network.PublicNetworkhistoryArchiveURLs, }, { name: "testnet validation; history archive urls supplied", @@ -83,18 +84,41 @@ func Test_createCaptiveCoreDefaultConfig(t *testing.T) { }, errStr: "no default configuration found for network unknown", }, + { + name: "no network specified; passphrase not supplied", + config: Config{ + HistoryArchiveURLs: []string{"HistoryArchiveURLs"}, + CaptiveCoreBinaryPath: "/path/to/captive-core/binary", + }, + errStr: fmt.Sprintf("%s must be set", NetworkPassphraseFlagName), + }, + { + name: "no network specified; history archive urls not supplied", + config: Config{ + NetworkPassphrase: "NetworkPassphrase", + CaptiveCoreBinaryPath: "/path/to/captive-core/binary", + }, + errStr: fmt.Sprintf("%s must be set", HistoryArchiveURLsFlagName), + }, + + { + name: "unknown network specified", + config: Config{Network: "unknown", + NetworkPassphrase: "", + HistoryArchiveURLs: []string{}, + CaptiveCoreBinaryPath: "/path/to/captive-core/binary", + }, + errStr: "no default configuration found for network unknown", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.config.CaptiveCoreTomlParams.UseDB = true - e := setCaptiveCoreConfiguration(&tt.config, - ApplyOptions{RequireCaptiveCoreFullConfig: true}) + e := setNetworkConfiguration(&tt.config) if tt.errStr == "" { assert.NoError(t, e) assert.Equal(t, tt.networkPassphrase, tt.config.NetworkPassphrase) assert.Equal(t, tt.historyArchiveURLs, tt.config.HistoryArchiveURLs) - assert.Equal(t, tt.networkPassphrase, tt.config.CaptiveCoreTomlParams.NetworkPassphrase) - assert.Equal(t, tt.historyArchiveURLs, tt.config.CaptiveCoreTomlParams.HistoryArchiveURLs) } else { assert.Equal(t, tt.errStr, e.Error()) } @@ -102,53 +126,50 @@ func Test_createCaptiveCoreDefaultConfig(t *testing.T) { } } -func Test_createCaptiveCoreConfig(t *testing.T) { - - var errorMsgConfig = "%s must be set" +func TestSetCaptiveCoreConfig(t *testing.T) { tests := []struct { name string requireCaptiveCoreConfig bool config Config - networkPassphrase string - historyArchiveURLs []string errStr string }{ { - name: "no network specified; valid parameters", + name: "testnet default config", requireCaptiveCoreConfig: true, config: Config{ - NetworkPassphrase: PubnetConf.NetworkPassphrase, - HistoryArchiveURLs: PubnetConf.HistoryArchiveURLs, - CaptiveCoreConfigPath: "../../../ingest/ledgerbackend/configs/captive-core-pubnet.cfg", + Network: StellarTestnet, + NetworkPassphrase: network.TestNetworkPassphrase, + HistoryArchiveURLs: network.TestNetworkhistoryArchiveURLs, CaptiveCoreBinaryPath: "/path/to/captive-core/binary", }, - networkPassphrase: PubnetConf.NetworkPassphrase, - historyArchiveURLs: PubnetConf.HistoryArchiveURLs, }, { - name: "no network specified; passphrase not supplied", + name: "pubnet default config", requireCaptiveCoreConfig: true, config: Config{ - HistoryArchiveURLs: []string{"HistoryArchiveURLs"}, + Network: StellarPubnet, + NetworkPassphrase: network.PublicNetworkPassphrase, + HistoryArchiveURLs: network.PublicNetworkhistoryArchiveURLs, CaptiveCoreBinaryPath: "/path/to/captive-core/binary", }, - errStr: fmt.Sprintf(errorMsgConfig, NetworkPassphraseFlagName), }, { - name: "no network specified; history archive urls not supplied", + name: "no network specified; valid parameters", requireCaptiveCoreConfig: true, config: Config{ - NetworkPassphrase: "NetworkPassphrase", + NetworkPassphrase: network.PublicNetworkPassphrase, + HistoryArchiveURLs: network.PublicNetworkhistoryArchiveURLs, + CaptiveCoreConfigPath: "../../../ingest/ledgerbackend/configs/captive-core-pubnet.cfg", CaptiveCoreBinaryPath: "/path/to/captive-core/binary", }, - errStr: fmt.Sprintf(errorMsgConfig, HistoryArchiveURLsFlagName), }, + { name: "no network specified; captive-core-config-path not supplied", requireCaptiveCoreConfig: true, config: Config{ - NetworkPassphrase: PubnetConf.NetworkPassphrase, - HistoryArchiveURLs: PubnetConf.HistoryArchiveURLs, + NetworkPassphrase: network.PublicNetworkPassphrase, + HistoryArchiveURLs: network.PublicNetworkhistoryArchiveURLs, CaptiveCoreBinaryPath: "/path/to/captive-core/binary", }, errStr: fmt.Sprintf("invalid config: captive core requires that --%s is set or "+ @@ -158,8 +179,8 @@ func Test_createCaptiveCoreConfig(t *testing.T) { name: "no network specified; captive-core-config-path invalid file", requireCaptiveCoreConfig: true, config: Config{ - NetworkPassphrase: PubnetConf.NetworkPassphrase, - HistoryArchiveURLs: PubnetConf.HistoryArchiveURLs, + NetworkPassphrase: network.PublicNetworkPassphrase, + HistoryArchiveURLs: network.PublicNetworkhistoryArchiveURLs, CaptiveCoreConfigPath: "xyz.cfg", CaptiveCoreBinaryPath: "/path/to/captive-core/binary", }, @@ -170,25 +191,21 @@ func Test_createCaptiveCoreConfig(t *testing.T) { name: "no network specified; captive-core-config-path incorrect config", requireCaptiveCoreConfig: true, config: Config{ - NetworkPassphrase: PubnetConf.NetworkPassphrase, - HistoryArchiveURLs: PubnetConf.HistoryArchiveURLs, + NetworkPassphrase: network.PublicNetworkPassphrase, + HistoryArchiveURLs: network.PublicNetworkhistoryArchiveURLs, CaptiveCoreConfigPath: "../../../ingest/ledgerbackend/configs/captive-core-testnet.cfg", CaptiveCoreBinaryPath: "/path/to/captive-core/binary", }, errStr: fmt.Sprintf("invalid captive core toml file: invalid captive core toml: "+ "NETWORK_PASSPHRASE in captive core config file: %s does not match Horizon "+ - "network-passphrase flag: %s", TestnetConf.NetworkPassphrase, PubnetConf.NetworkPassphrase), + "network-passphrase flag: %s", network.TestNetworkPassphrase, network.PublicNetworkPassphrase), }, { - name: "no network specified; captive-core-config not required", + name: "no network specified; full captive-core-config not required", requireCaptiveCoreConfig: false, config: Config{ - NetworkPassphrase: PubnetConf.NetworkPassphrase, - HistoryArchiveURLs: PubnetConf.HistoryArchiveURLs, CaptiveCoreBinaryPath: "/path/to/captive-core/binary", }, - networkPassphrase: PubnetConf.NetworkPassphrase, - historyArchiveURLs: PubnetConf.HistoryArchiveURLs, }, } for _, tt := range tests { @@ -198,10 +215,6 @@ func Test_createCaptiveCoreConfig(t *testing.T) { ApplyOptions{RequireCaptiveCoreFullConfig: tt.requireCaptiveCoreConfig}) if tt.errStr == "" { assert.NoError(t, e) - assert.Equal(t, tt.networkPassphrase, tt.config.NetworkPassphrase) - assert.Equal(t, tt.historyArchiveURLs, tt.config.HistoryArchiveURLs) - assert.Equal(t, tt.networkPassphrase, tt.config.CaptiveCoreTomlParams.NetworkPassphrase) - assert.Equal(t, tt.historyArchiveURLs, tt.config.CaptiveCoreTomlParams.HistoryArchiveURLs) } else { require.Error(t, e) assert.Equal(t, tt.errStr, e.Error()) @@ -261,7 +274,7 @@ func TestClientQueryTimeoutFlag(t *testing.T) { if err := flags.Init(horizonCmd); err != nil { require.NoError(t, err) } - if err := ApplyFlags(config, flags, ApplyOptions{RequireCaptiveCoreFullConfig: true, AlwaysIngest: false}); err != nil { + if err := ApplyFlags(config, flags, ApplyOptions{RequireCaptiveCoreFullConfig: true}); err != nil { require.EqualError(t, err, testCase.err) } else { require.Empty(t, testCase.err) @@ -293,7 +306,7 @@ func TestEnvironmentVariables(t *testing.T) { if err := flags.Init(horizonCmd); err != nil { fmt.Println(err) } - if err := ApplyFlags(config, flags, ApplyOptions{RequireCaptiveCoreFullConfig: true, AlwaysIngest: false}); err != nil { + if err := ApplyFlags(config, flags, ApplyOptions{RequireCaptiveCoreFullConfig: true}); err != nil { fmt.Println(err) } assert.Equal(t, config.Ingest, false) diff --git a/services/horizon/internal/ingest/README.md b/services/horizon/internal/ingest/README.md index a0874a0b43..12982b5047 100644 --- a/services/horizon/internal/ingest/README.md +++ b/services/horizon/internal/ingest/README.md @@ -140,8 +140,46 @@ This pauses the state machine for 10 seconds then tries again, in hopes that a n **Next state**: [`start`](#start-state) -# Ingestion -TODO +# Reingestion +Horizon supports running reingestion by executing a sub command `db reingest range ` which will execute as an o/s process and will be synchronous, exiting the process only after the complete reingestion range is finished or an error is encountered. + +By default this sub-command will attempt to use captive core configuration in the form of stellar core binary(`--stellar-core-binary-path`) and stellar core config(`--captive-core-config-path`) to obtain ledger tx meta from a stellar network to be ingested. + +The `db reingest range` sub-command can optionally be configured to consume pre-computed ledger tx meta files from a Google Cloud Storage(GCS) location instead of running captive core on host machine. +Pre-requirements: + - Have a GCS account. + - Run the [ledgerexporter] to publish ledger tx meta files to your GCS bucket location. +Run the `db reingest` sub-command, configured to import tx meta from your GCS bucket: + ```$ DATABASE_URL= \ + NETWORK=testnet \ + stellar-horizon db reingest range \ + --parallel-workers 2 \ + --ledgerbackend "datastore" \ + --datastore-config "config.storagebackend.toml" \ + 100 200 + ``` +Notice, even though we no longer need to provide stellar-core related config for binary or config file, we do still need to provide network related config, using convenience parameter `NETWORK=testnet|pubnet` or directly with `NETWORK_PASSPHRASE` and `HISTORY_ARCHIVE_URLS` + +The `--datastore-config` must point to a new toml config file that will provide the necessary parameters for ingestion to work with remote GCS storage. + +example config toml: +``` +# Datastore Configuration +[datastore_config] +# Specifies the type of datastore. +# Currently, only Google Cloud Storage (GCS) is supported. +type = "GCS" + +[datastore_config.params] +# The Google Cloud Storage bucket path for storing data, with optional subpaths for organization. +destination_bucket_path = "path/to/my/bucket" + +[datastore_config.schema] +# Configuration for data organization of the remote files +ledgers_per_file = 1 # Number of ledgers stored in each file. +files_per_partition = 64000 # Number of files per partition/directory. + +``` # Range Preparation TODO: See `maybePrepareRange` diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index dec3123f34..1a54e6843c 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -20,6 +20,7 @@ import ( "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/services/horizon/internal/ingest/filters" apkg "github.com/stellar/go/support/app" + "github.com/stellar/go/support/datastore" "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" logpkg "github.com/stellar/go/support/log" @@ -82,6 +83,28 @@ const ( var log = logpkg.DefaultLogger.WithField("service", "ingest") +type LedgerBackendType uint + +const ( + CaptiveCoreBackend LedgerBackendType = iota + BufferedStorageBackend +) + +func (s LedgerBackendType) String() string { + switch s { + case CaptiveCoreBackend: + return "captive-core" + case BufferedStorageBackend: + return "datastore" + } + return "" +} + +type StorageBackendConfig struct { + DataStoreConfig datastore.DataStoreConfig `toml:"datastore_config"` + BufferedStorageBackendConfig ledgerbackend.BufferedStorageBackendConfig `toml:"buffered_storage_backend_config"` +} + type Config struct { StellarCoreURL string CaptiveCoreBinaryPath string @@ -115,6 +138,9 @@ type Config struct { CoreBuildVersionFn ledgerbackend.CoreBuildVersionFunc ReapConfig ReapConfig + + LedgerBackendType LedgerBackendType + StorageBackendConfig StorageBackendConfig } const ( @@ -261,29 +287,46 @@ func NewSystem(config Config) (System, error) { cancel() return nil, errors.Wrap(err, "error creating history archive") } + var ledgerBackend ledgerbackend.LedgerBackend - // the only ingest option is local captive core config - logger := log.WithField("subservice", "stellar-core") - ledgerBackend, err := ledgerbackend.NewCaptive( - ledgerbackend.CaptiveCoreConfig{ - BinaryPath: config.CaptiveCoreBinaryPath, - StoragePath: config.CaptiveCoreStoragePath, - UseDB: config.CaptiveCoreConfigUseDB, - Toml: config.CaptiveCoreToml, - NetworkPassphrase: config.NetworkPassphrase, - HistoryArchiveURLs: config.HistoryArchiveURLs, - CheckpointFrequency: config.CheckpointFrequency, - LedgerHashStore: ledgerbackend.NewHorizonDBLedgerHashStore(config.HistorySession), - Log: logger, - Context: ctx, - UserAgent: fmt.Sprintf("captivecore horizon/%s golang/%s", apkg.Version(), runtime.Version()), - CoreProtocolVersionFn: config.CoreProtocolVersionFn, - CoreBuildVersionFn: config.CoreBuildVersionFn, - }, - ) - if err != nil { - cancel() - return nil, errors.Wrap(err, "error creating captive core backend") + if config.LedgerBackendType == BufferedStorageBackend { + // Ingest from datastore + var dataStore datastore.DataStore + dataStore, err = datastore.NewDataStore(context.Background(), config.StorageBackendConfig.DataStoreConfig) + if err != nil { + cancel() + return nil, fmt.Errorf("failed to create datastore: %w", err) + } + ledgerBackend, err = ledgerbackend.NewBufferedStorageBackend(config.StorageBackendConfig.BufferedStorageBackendConfig, dataStore) + if err != nil { + cancel() + return nil, fmt.Errorf("failed to create buffered storage backend: %w", err) + } + } else { + // Ingest from local captive core + + logger := log.WithField("subservice", "stellar-core") + ledgerBackend, err = ledgerbackend.NewCaptive( + ledgerbackend.CaptiveCoreConfig{ + BinaryPath: config.CaptiveCoreBinaryPath, + StoragePath: config.CaptiveCoreStoragePath, + UseDB: config.CaptiveCoreConfigUseDB, + Toml: config.CaptiveCoreToml, + NetworkPassphrase: config.NetworkPassphrase, + HistoryArchiveURLs: config.HistoryArchiveURLs, + CheckpointFrequency: config.CheckpointFrequency, + LedgerHashStore: ledgerbackend.NewHorizonDBLedgerHashStore(config.HistorySession), + Log: logger, + Context: ctx, + UserAgent: fmt.Sprintf("captivecore horizon/%s golang/%s", apkg.Version(), runtime.Version()), + CoreProtocolVersionFn: config.CoreProtocolVersionFn, + CoreBuildVersionFn: config.CoreBuildVersionFn, + }, + ) + if err != nil { + cancel() + return nil, errors.Wrap(err, "error creating captive core backend") + } } historyQ := &history.Q{config.HistorySession.Clone()} diff --git a/services/horizon/internal/ingest/testdata/config.storagebackend.toml b/services/horizon/internal/ingest/testdata/config.storagebackend.toml new file mode 100644 index 0000000000..538b793b54 --- /dev/null +++ b/services/horizon/internal/ingest/testdata/config.storagebackend.toml @@ -0,0 +1,19 @@ +[buffered_storage_backend_config] +buffer_size = 5 # The size of the buffer +num_workers = 5 # Number of workers +retry_limit = 3 # Number of retries allowed +retry_wait = "30s" # Duration to wait before retrying in seconds + +# Datastore Configuration +[datastore_config] +# Specifies the type of datastore. Currently, only Google Cloud Storage (GCS) is supported. +type = "GCS" + +[datastore_config.params] +# The Google Cloud Storage bucket path for storing data, with optional subpaths for organization. +destination_bucket_path = "path/to/my/bucket" + +[datastore_config.schema] +# Configuration for data organization +ledgers_per_file = 1 # Number of ledgers stored in each file. +files_per_partition = 64000 # Number of files per partition/directory. diff --git a/services/horizon/internal/integration/db_test.go b/services/horizon/internal/integration/db_test.go index 1f1d2277ec..5a2b03e48b 100644 --- a/services/horizon/internal/integration/db_test.go +++ b/services/horizon/internal/integration/db_test.go @@ -3,12 +3,16 @@ package integration import ( "context" "fmt" + "io/fs" + "os" "path/filepath" "strconv" "testing" "time" + "github.com/fsouza/fake-gcs-server/fakestorage" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/historyarchive" @@ -485,7 +489,8 @@ func TestReingestDB(t *testing.T) { horizonConfig := itest.GetHorizonIngestConfig() t.Run("validate parallel range", func(t *testing.T) { - horizoncmd.RootCmd.SetArgs(command(t, horizonConfig, + var rootCmd = horizoncmd.NewRootCmd() + rootCmd.SetArgs(command(t, horizonConfig, "db", "reingest", "range", @@ -494,7 +499,7 @@ func TestReingestDB(t *testing.T) { "2", )) - assert.EqualError(t, horizoncmd.RootCmd.Execute(), "Invalid range: {10 2} from > to") + assert.EqualError(t, rootCmd.Execute(), "Invalid range: {10 2} from > to") }) t.Logf("reached ledger is %v", reachedLedger) @@ -537,7 +542,8 @@ func TestReingestDB(t *testing.T) { "captive-core-reingest-range-integration-tests.cfg", ) - horizoncmd.RootCmd.SetArgs(command(t, horizonConfig, "db", + var rootCmd = horizoncmd.NewRootCmd() + rootCmd.SetArgs(command(t, horizonConfig, "db", "reingest", "range", "--parallel-workers=1", @@ -545,8 +551,84 @@ func TestReingestDB(t *testing.T) { fmt.Sprintf("%d", toLedger), )) - tt.NoError(horizoncmd.RootCmd.Execute()) - tt.NoError(horizoncmd.RootCmd.Execute(), "Repeat the same reingest range against db, should not have errors.") + tt.NoError(rootCmd.Execute()) + tt.NoError(rootCmd.Execute(), "Repeat the same reingest range against db, should not have errors.") +} + +func TestReingestDatastore(t *testing.T) { + test := integration.NewTest(t, integration.Config{ + SkipHorizonStart: true, + SkipCoreContainerCreation: true, + }) + err := test.StartHorizon(false) + assert.NoError(t, err) + test.WaitForHorizonWeb() + + testTempDir := t.TempDir() + fakeBucketFilesSource := "testdata/testbucket" + fakeBucketFiles := []fakestorage.Object{} + + if err = filepath.WalkDir(fakeBucketFilesSource, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + + if entry.Type().IsRegular() { + contents, err := os.ReadFile(fmt.Sprintf("%s/%s", fakeBucketFilesSource, entry.Name())) + if err != nil { + return err + } + + fakeBucketFiles = append(fakeBucketFiles, fakestorage.Object{ + ObjectAttrs: fakestorage.ObjectAttrs{ + BucketName: "path", + Name: fmt.Sprintf("to/my/bucket/FFFFFFFF--0-63999/%s", entry.Name()), + }, + Content: contents, + }) + } + return nil + }); err != nil { + t.Fatalf("unable to setup fake bucket files: %v", err) + } + + testWriter := &testWriter{test: t} + opts := fakestorage.Options{ + Scheme: "http", + Host: "127.0.0.1", + Port: uint16(0), + Writer: testWriter, + StorageRoot: filepath.Join(testTempDir, "bucket"), + PublicHost: "127.0.0.1", + InitialObjects: fakeBucketFiles, + } + + gcsServer, err := fakestorage.NewServerWithOptions(opts) + + if err != nil { + t.Fatalf("couldn't start the fake gcs http server %v", err) + } + + defer gcsServer.Stop() + t.Logf("fake gcs server started at %v", gcsServer.URL()) + t.Setenv("STORAGE_EMULATOR_HOST", gcsServer.URL()) + + rootCmd := horizoncmd.NewRootCmd() + rootCmd.SetArgs([]string{"db", + "reingest", + "range", + "--db-url", test.GetTestDB().DSN, + "--network", "testnet", + "--parallel-workers", "1", + "--ledgerbackend", "datastore", + "--datastore-config", "../ingest/testdata/config.storagebackend.toml", + "997", + "999"}) + + require.NoError(t, rootCmd.Execute()) + + _, err = test.Client().LedgerDetail(998) + require.NoError(t, err) } func TestReingestDBWithFilterRules(t *testing.T) { @@ -648,22 +730,24 @@ func TestReingestDBWithFilterRules(t *testing.T) { itest.StopHorizon() // clear the db with reaping all ledgers - horizoncmd.RootCmd.SetArgs(command(t, itest.GetHorizonIngestConfig(), "db", + var rootCmd = horizoncmd.NewRootCmd() + rootCmd.SetArgs(command(t, itest.GetHorizonIngestConfig(), "db", "reap", "--history-retention-count=1", )) - tt.NoError(horizoncmd.RootCmd.Execute()) + tt.NoError(rootCmd.Execute()) // repopulate the db with reingestion which should catchup using core reapply filter rules // correctly on reingestion ranged - horizoncmd.RootCmd.SetArgs(command(t, itest.GetHorizonIngestConfig(), "db", + rootCmd = horizoncmd.NewRootCmd() + rootCmd.SetArgs(command(t, itest.GetHorizonIngestConfig(), "db", "reingest", "range", "1", fmt.Sprintf("%d", reachedLedger), )) - tt.NoError(horizoncmd.RootCmd.Execute()) + tt.NoError(rootCmd.Execute()) // bring up horizon, just the api server no ingestion, to query // for tx's that should have been repopulated on db from reingestion per @@ -678,7 +762,7 @@ func TestReingestDBWithFilterRules(t *testing.T) { }() // wait until the web server is up before continuing to test requests - itest.WaitForHorizon() + itest.WaitForHorizonIngest() // Make sure that a tx from non-whitelisted account is not stored after reingestion _, err = itest.Client().TransactionDetail(nonWhiteListTxResp.Hash) @@ -733,12 +817,13 @@ func TestMigrateIngestIsTrueByDefault(t *testing.T) { newDB := dbtest.Postgres(t) freshHorizonPostgresURL := newDB.DSN - horizoncmd.RootCmd.SetArgs([]string{ + rootCmd := horizoncmd.NewRootCmd() + rootCmd.SetArgs([]string{ // ingest is set to true by default "--db-url", freshHorizonPostgresURL, "db", "migrate", "up", }) - tt.NoError(horizoncmd.RootCmd.Execute()) + tt.NoError(rootCmd.Execute()) dbConn, err := db.Open("postgres", freshHorizonPostgresURL) tt.NoError(err) @@ -754,12 +839,13 @@ func TestMigrateChecksIngestFlag(t *testing.T) { newDB := dbtest.Postgres(t) freshHorizonPostgresURL := newDB.DSN - horizoncmd.RootCmd.SetArgs([]string{ + rootCmd := horizoncmd.NewRootCmd() + rootCmd.SetArgs([]string{ "--ingest=false", "--db-url", freshHorizonPostgresURL, "db", "migrate", "up", }) - tt.NoError(horizoncmd.RootCmd.Execute()) + tt.NoError(rootCmd.Execute()) dbConn, err := db.Open("postgres", freshHorizonPostgresURL) tt.NoError(err) @@ -802,7 +888,8 @@ func TestFillGaps(t *testing.T) { tt.NoError(err) t.Run("validate parallel range", func(t *testing.T) { - horizoncmd.RootCmd.SetArgs(command(t, horizonConfig, + var rootCmd = horizoncmd.NewRootCmd() + rootCmd.SetArgs(command(t, horizonConfig, "db", "fill-gaps", "--parallel-workers=2", @@ -810,7 +897,7 @@ func TestFillGaps(t *testing.T) { "2", )) - assert.EqualError(t, horizoncmd.RootCmd.Execute(), "Invalid range: {10 2} from > to") + assert.EqualError(t, rootCmd.Execute(), "Invalid range: {10 2} from > to") }) // make sure a full checkpoint has elapsed otherwise there will be nothing to reingest @@ -842,21 +929,25 @@ func TestFillGaps(t *testing.T) { filepath.Dir(horizonConfig.CaptiveCoreConfigPath), "captive-core-reingest-range-integration-tests.cfg", ) - horizoncmd.RootCmd.SetArgs(command(t, horizonConfig, "db", "fill-gaps", "--parallel-workers=1")) - tt.NoError(horizoncmd.RootCmd.Execute()) + + rootCmd := horizoncmd.NewRootCmd() + rootCmd.SetArgs(command(t, horizonConfig, "db", "fill-gaps", "--parallel-workers=1")) + tt.NoError(rootCmd.Execute()) tt.NoError(historyQ.LatestLedger(context.Background(), &latestLedger)) tt.Equal(int64(0), latestLedger) - horizoncmd.RootCmd.SetArgs(command(t, horizonConfig, "db", "fill-gaps", "3", "4")) - tt.NoError(horizoncmd.RootCmd.Execute()) + rootCmd = horizoncmd.NewRootCmd() + rootCmd.SetArgs(command(t, horizonConfig, "db", "fill-gaps", "3", "4")) + tt.NoError(rootCmd.Execute()) tt.NoError(historyQ.LatestLedger(context.Background(), &latestLedger)) tt.NoError(historyQ.ElderLedger(context.Background(), &oldestLedger)) tt.Equal(int64(3), oldestLedger) tt.Equal(int64(4), latestLedger) - horizoncmd.RootCmd.SetArgs(command(t, horizonConfig, "db", "fill-gaps", "6", "7")) - tt.NoError(horizoncmd.RootCmd.Execute()) + rootCmd = horizoncmd.NewRootCmd() + rootCmd.SetArgs(command(t, horizonConfig, "db", "fill-gaps", "6", "7")) + tt.NoError(rootCmd.Execute()) tt.NoError(historyQ.LatestLedger(context.Background(), &latestLedger)) tt.NoError(historyQ.ElderLedger(context.Background(), &oldestLedger)) tt.Equal(int64(3), oldestLedger) @@ -866,8 +957,9 @@ func TestFillGaps(t *testing.T) { tt.NoError(err) tt.Equal([]history.LedgerRange{{StartSequence: 5, EndSequence: 5}}, gaps) - horizoncmd.RootCmd.SetArgs(command(t, horizonConfig, "db", "fill-gaps")) - tt.NoError(horizoncmd.RootCmd.Execute()) + rootCmd = horizoncmd.NewRootCmd() + rootCmd.SetArgs(command(t, horizonConfig, "db", "fill-gaps")) + tt.NoError(rootCmd.Execute()) tt.NoError(historyQ.LatestLedger(context.Background(), &latestLedger)) tt.NoError(historyQ.ElderLedger(context.Background(), &oldestLedger)) tt.Equal(int64(3), oldestLedger) @@ -876,8 +968,9 @@ func TestFillGaps(t *testing.T) { tt.NoError(err) tt.Empty(gaps) - horizoncmd.RootCmd.SetArgs(command(t, horizonConfig, "db", "fill-gaps", "2", "8")) - tt.NoError(horizoncmd.RootCmd.Execute()) + rootCmd = horizoncmd.NewRootCmd() + rootCmd.SetArgs(command(t, horizonConfig, "db", "fill-gaps", "2", "8")) + tt.NoError(rootCmd.Execute()) tt.NoError(historyQ.LatestLedger(context.Background(), &latestLedger)) tt.NoError(historyQ.ElderLedger(context.Background(), &oldestLedger)) tt.Equal(int64(2), oldestLedger) @@ -892,7 +985,7 @@ func TestResumeFromInitializedDB(t *testing.T) { tt := assert.New(t) // Stop the integration test, and restart it with the same database - err := itest.RestartHorizon() + err := itest.RestartHorizon(true) tt.NoError(err) successfullyResumed := func() bool { @@ -905,3 +998,12 @@ func TestResumeFromInitializedDB(t *testing.T) { tt.Eventually(successfullyResumed, 1*time.Minute, 1*time.Second) } + +type testWriter struct { + test *testing.T +} + +func (w *testWriter) Write(p []byte) (n int, err error) { + w.test.Log(string(p)) + return len(p), nil +} diff --git a/services/horizon/internal/integration/parameters_test.go b/services/horizon/internal/integration/parameters_test.go index 133950d6f3..c7e0d0c75b 100644 --- a/services/horizon/internal/integration/parameters_test.go +++ b/services/horizon/internal/integration/parameters_test.go @@ -16,6 +16,7 @@ import ( "github.com/spf13/cobra" + "github.com/stellar/go/network" "github.com/stellar/go/services/horizon/internal/paths" "github.com/stellar/go/services/horizon/internal/simplepath" @@ -75,13 +76,10 @@ func TestBucketDirDisallowed(t *testing.T) { horizon.StellarCoreBinaryPathName: os.Getenv("CAPTIVE_CORE_BIN"), } test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() + err := test.StartHorizon(true) assert.Equal(t, err.Error(), integration.HorizonInitErrStr+": error generating captive core configuration:"+ " invalid captive core toml file: could not unmarshal captive core toml: setting BUCKET_DIR_PATH is disallowed"+ " for Captive Core, use CAPTIVE_CORE_STORAGE_PATH instead") - time.Sleep(1 * time.Second) - test.StopHorizon() - test.Shutdown() } func TestEnvironmentPreserved(t *testing.T) { @@ -109,9 +107,9 @@ func TestEnvironmentPreserved(t *testing.T) { } test := integration.NewTest(t, *testConfig) - err = test.StartHorizon() + err = test.StartHorizon(true) assert.NoError(t, err) - test.WaitForHorizon() + test.WaitForHorizonIngest() envValue := os.Getenv("STELLAR_CORE_URL") assert.Equal(t, StellarCoreURL, envValue) @@ -126,8 +124,7 @@ func TestEnvironmentPreserved(t *testing.T) { // using NETWORK environment variables, history archive urls or network passphrase // parameters are also set. func TestInvalidNetworkParameters(t *testing.T) { - var captiveCoreConfigErrMsg = integration.HorizonInitErrStr + ": error generating captive " + - "core configuration: invalid config: %s parameter not allowed with the %s parameter" + var captiveCoreConfigErrMsg = integration.HorizonInitErrStr + ": invalid config: %s parameter not allowed with the %s parameter" testCases := []struct { name string errMsg string @@ -160,12 +157,11 @@ func TestInvalidNetworkParameters(t *testing.T) { testConfig.SkipCoreContainerCreation = true testConfig.HorizonIngestParameters = localParams test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() + err := test.StartHorizon(true) // Adding sleep as a workaround for the race condition in the ingestion system. // https://github.com/stellar/go/issues/5005 time.Sleep(2 * time.Second) assert.Equal(t, testCase.errMsg, err.Error()) - test.Shutdown() }) } } @@ -186,13 +182,13 @@ func TestNetworkParameter(t *testing.T) { }{ { networkValue: horizon.StellarTestnet, - networkPassphrase: horizon.TestnetConf.NetworkPassphrase, - historyArchiveURLs: horizon.TestnetConf.HistoryArchiveURLs, + networkPassphrase: network.TestNetworkPassphrase, + historyArchiveURLs: network.TestNetworkhistoryArchiveURLs, }, { networkValue: horizon.StellarPubnet, - networkPassphrase: horizon.PubnetConf.NetworkPassphrase, - historyArchiveURLs: horizon.PubnetConf.HistoryArchiveURLs, + networkPassphrase: network.PublicNetworkPassphrase, + historyArchiveURLs: network.PublicNetworkhistoryArchiveURLs, }, } for _, tt := range testCases { @@ -204,15 +200,13 @@ func TestNetworkParameter(t *testing.T) { testConfig.SkipCoreContainerCreation = true testConfig.HorizonIngestParameters = localParams test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() + err := test.StartHorizon(true) // Adding sleep as a workaround for the race condition in the ingestion system. // https://github.com/stellar/go/issues/5005 time.Sleep(2 * time.Second) assert.NoError(t, err) assert.Equal(t, test.GetHorizonIngestConfig().HistoryArchiveURLs, tt.historyArchiveURLs) assert.Equal(t, test.GetHorizonIngestConfig().NetworkPassphrase, tt.networkPassphrase) - - test.Shutdown() }) } } @@ -247,12 +241,11 @@ func TestNetworkEnvironmentVariable(t *testing.T) { testConfig.HorizonIngestParameters = networkParamArgs testConfig.HorizonEnvironment = map[string]string{"NETWORK": networkValue} test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() - // Adding sleep here as a workaround for the race condition in the ingestion system. - // More details can be found at https://github.com/stellar/go/issues/5005 + err := test.StartHorizon(true) + // Adding sleep as a workaround for the race condition in the ingestion system. + // https://github.com/stellar/go/issues/5005 time.Sleep(2 * time.Second) assert.NoError(t, err) - test.Shutdown() }) } } @@ -270,9 +263,9 @@ func TestCaptiveCoreConfigFilesystemState(t *testing.T) { testConfig.HorizonIngestParameters = localParams test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() + err := test.StartHorizon(true) assert.NoError(t, err) - test.WaitForHorizon() + test.WaitForHorizonIngest() t.Run("disk state", func(t *testing.T) { validateCaptiveCoreDiskState(test, storagePath) @@ -286,9 +279,9 @@ func TestCaptiveCoreConfigFilesystemState(t *testing.T) { func TestMaxAssetsForPathRequests(t *testing.T) { t.Run("default", func(t *testing.T) { test := integration.NewTest(t, *integration.GetTestConfig()) - err := test.StartHorizon() + err := test.StartHorizon(true) assert.NoError(t, err) - test.WaitForHorizon() + test.WaitForHorizonIngest() assert.Equal(t, test.HorizonIngest().Config().MaxAssetsPerPathRequest, 15) test.Shutdown() }) @@ -296,20 +289,19 @@ func TestMaxAssetsForPathRequests(t *testing.T) { testConfig := integration.GetTestConfig() testConfig.HorizonIngestParameters = map[string]string{"max-assets-per-path-request": "2"} test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() + err := test.StartHorizon(true) assert.NoError(t, err) - test.WaitForHorizon() + test.WaitForHorizonIngest() assert.Equal(t, test.HorizonIngest().Config().MaxAssetsPerPathRequest, 2) - test.Shutdown() }) } func TestMaxPathFindingRequests(t *testing.T) { t.Run("default", func(t *testing.T) { test := integration.NewTest(t, *integration.GetTestConfig()) - err := test.StartHorizon() + err := test.StartHorizon(true) assert.NoError(t, err) - test.WaitForHorizon() + test.WaitForHorizonIngest() assert.Equal(t, test.HorizonIngest().Config().MaxPathFindingRequests, uint(0)) _, ok := test.HorizonIngest().Paths().(simplepath.InMemoryFinder) assert.True(t, ok) @@ -319,37 +311,34 @@ func TestMaxPathFindingRequests(t *testing.T) { testConfig := integration.GetTestConfig() testConfig.HorizonIngestParameters = map[string]string{"max-path-finding-requests": "5"} test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() + err := test.StartHorizon(true) assert.NoError(t, err) - test.WaitForHorizon() + test.WaitForHorizonIngest() assert.Equal(t, test.HorizonIngest().Config().MaxPathFindingRequests, uint(5)) finder, ok := test.HorizonIngest().Paths().(*paths.RateLimitedFinder) assert.True(t, ok) assert.Equal(t, finder.Limit(), 5) - test.Shutdown() }) } func TestDisablePathFinding(t *testing.T) { t.Run("default", func(t *testing.T) { test := integration.NewTest(t, *integration.GetTestConfig()) - err := test.StartHorizon() + err := test.StartHorizon(true) assert.NoError(t, err) - test.WaitForHorizon() + test.WaitForHorizonIngest() assert.Equal(t, test.HorizonIngest().Config().MaxPathFindingRequests, uint(0)) _, ok := test.HorizonIngest().Paths().(simplepath.InMemoryFinder) assert.True(t, ok) - test.Shutdown() }) t.Run("set to true", func(t *testing.T) { testConfig := integration.GetTestConfig() testConfig.HorizonIngestParameters = map[string]string{"disable-path-finding": "true"} test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() + err := test.StartHorizon(true) assert.NoError(t, err) - test.WaitForHorizon() + test.WaitForHorizonIngest() assert.Nil(t, test.HorizonIngest().Paths()) - test.Shutdown() }) } @@ -364,9 +353,8 @@ func TestDisableTxSub(t *testing.T) { testConfig.HorizonIngestParameters = localParams testConfig.SkipCoreContainerCreation = true test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() + err := test.StartHorizon(true) assert.ErrorContains(t, err, "cannot initialize Horizon: flag --stellar-core-url cannot be empty") - test.Shutdown() }) t.Run("horizon starts successfully when DISABLE_TX_SUB=false, INGEST=false and stellar-core-url is provided", func(t *testing.T) { localParams := integration.MergeMaps(networkParamArgs, map[string]string{ @@ -379,9 +367,8 @@ func TestDisableTxSub(t *testing.T) { testConfig.HorizonIngestParameters = localParams testConfig.SkipCoreContainerCreation = true test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() + err := test.StartHorizon(true) assert.NoError(t, err) - test.Shutdown() }) t.Run("horizon starts successfully when DISABLE_TX_SUB=true and INGEST=true", func(t *testing.T) { testConfig := integration.GetTestConfig() @@ -390,10 +377,9 @@ func TestDisableTxSub(t *testing.T) { "ingest": "true", } test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() + err := test.StartHorizon(true) assert.NoError(t, err) - test.WaitForHorizon() - test.Shutdown() + test.WaitForHorizonIngest() }) t.Run("do not require stellar-core-url when both DISABLE_TX_SUB=true and INGEST=false", func(t *testing.T) { localParams := integration.MergeMaps(networkParamArgs, map[string]string{ @@ -405,9 +391,8 @@ func TestDisableTxSub(t *testing.T) { testConfig.HorizonIngestParameters = localParams testConfig.SkipCoreContainerCreation = true test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() + err := test.StartHorizon(true) assert.NoError(t, err) - test.Shutdown() }) } @@ -421,9 +406,9 @@ func TestDeprecatedOutputs(t *testing.T) { testConfig := integration.GetTestConfig() testConfig.HorizonIngestParameters = map[string]string{"exp-enable-ingestion-filtering": "false"} test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() + err := test.StartHorizon(true) assert.NoError(t, err) - test.WaitForHorizon() + test.WaitForHorizonIngest() // Use a wait group to wait for the goroutine to finish before proceeding var wg sync.WaitGroup @@ -507,9 +492,9 @@ func TestDeprecatedOutputs(t *testing.T) { testConfig := integration.GetTestConfig() testConfig.HorizonIngestParameters = map[string]string{"captive-core-use-db": "true"} test := integration.NewTest(t, *testConfig) - err := test.StartHorizon() + err := test.StartHorizon(true) assert.NoError(t, err) - test.WaitForHorizon() + test.WaitForHorizonIngest() // Use a wait group to wait for the goroutine to finish before proceeding var wg sync.WaitGroup diff --git a/services/horizon/internal/integration/testdata/testbucket/FFFFFC18--999.xdr.zstd b/services/horizon/internal/integration/testdata/testbucket/FFFFFC18--999.xdr.zstd new file mode 100644 index 0000000000000000000000000000000000000000..b2627e7fc1a95a8a8b50255573b00837a9139fe2 GIT binary patch literal 1547 zcmV+m2K4zTwJ-f-d>HL70F+1o00ZX$0RW*Kpt{u7lbA9>L6Ecqj14{q<4!*qZuzMh zP0G2{H#7hM02H>0jR*+|S#fT3INCscK`Rw*TZaIx?A)0;VAeka0*M=yO9G9GLnHah zP^Fg$Z+jGgxkxm!bJ+0wCgOL7yaE6K003pef#CoF00000)pBXsVo>)E?%)BC-?e*|Cc2Ut*|;5);s=4qnfnhdCe9!Po4Lk zqK=;@G4?uQ92zh!?$qQ>EM=9Ot%0I7lI}id!z0bFbBbkHEhZ|%4XTvg000B$4dAx3 zr(^&rm_q;p0001F08C3j0001K+>^52wkQEkIjEE&{m$XZ)I?UvZ z0096100IC211VD9PxAEIZ=qko>q8obZG~TL#Ji@V^J;a^e=|9(E&vZgKmY&$nE?=e z(Q7O?Gf09ifgbm&&lJ&^if;f^a#Yhk9&LtJ;GFH!OPLr*dqp@jAUQN`PR1){Gk*_o z*GGK$*%)$y*X{rS02fGh=mG!$K#<4UO%36E$PtF+i~Lo5dgNEzn2DcL>fYWwxQLg_ z{@?46%)TPmL~W8sKr&r}OMK@?Tk)Q+Zh&2W21dwn2|&G~StAvNt?Gc*&g*Kn4m_1* z9^Y)vkLy&~9K$3>^~6$tbtDr+1UCEiDWF4;P<|U~0}|eth9{&AdqGiFs}E+SJC=tU zS!h2gDSHvpIb;XuYu~v2iHUsP;P=;f=3LCuaVZNr-8PG4e1-(M2d5X_F$Jb_cs>CM zHcU#))F{XR06=?@pT1XownH^QSmiQm)6msx@ohNO(3R&m3xIna2RZ+LiIbW2)oeiV zMv!gx`K<^GZYf6{m4RBYY)DlZ*a0pTSX!~Bla9h=+MbGE)9?J1C7NRX-jyPPWHl+) zGI{jSfdd_wwVPu#dg+r72loo6_ZR)_TidAm8VW{gDFw>kGX5baq!4r)kF90Ho2Y)d z5CtZ5RFNRF?8eO%a1Lg$=00001R9aI258vo`qwkTqK}Y<4^3hWeao;6d zi&BJdQukz1Z!wN2fByhWMN(HrRZ~PoRZ>p@06=%We)E72A2sa7WzrbhJjaiBda00g z!J9qIp(4A`a$>uUm}HXGjpGe00U*hc9~ze0|Eo) z0b&yK$R!%U0001M00G-^=iGm;G627vdj?J9FctKR3_&ZvaoI*l$#oHx)vyl{5m8et zcgVra!Jg;EH#bUG@-=2JT4=MFJzf)}?*1qE(<^Ie5hPwrDq>e~B$t!{^aTb20008| z@PGjT0Pc(d0R;gA0RaF30RjR80|W#G00t&70000000000fUcm?O%e(KBJxr(-~s^v zDG~?TFbF0fDf}k?gVF+O2r94XOQ5WP8iL9@{qki6)DWl))5lGjQ-uuUfG2Qy+VMhX ze-Ps8($IM1*wJ4ue4QPC#)cDoF;Jg$t8ah8{Q#NQ$MxS3J9t?%gb_wR*#o>V8NhNV z^AR2kcy{uJ<*qrK`&e=QxRLuYqfBPN;bd}FAW)H-jO(IvnnL x4AGaJAPA6rf30ZM&bU4~TeL*V~Zd-={t?b;H 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 Nk6k%^FvPX4{(mc#_qG54 literal 0 HcmV?d00001 diff --git a/services/horizon/internal/test/integration/integration.go b/services/horizon/internal/test/integration/integration.go index 0402301a44..9884470d70 100644 --- a/services/horizon/internal/test/integration/integration.go +++ b/services/horizon/internal/test/integration/integration.go @@ -23,11 +23,13 @@ import ( "github.com/creachadair/jrpc2/jhttp" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" sdk "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/clients/stellarcore" "github.com/stellar/go/keypair" proto "github.com/stellar/go/protocols/horizon" + horizoncmd "github.com/stellar/go/services/horizon/cmd" horizon "github.com/stellar/go/services/horizon/internal" "github.com/stellar/go/services/horizon/internal/ingest" "github.com/stellar/go/support/config" @@ -91,6 +93,7 @@ type Test struct { coreConfig CaptiveConfig horizonIngestConfig horizon.Config horizonWebConfig horizon.Config + testDB *dbtest.DB environment *test.EnvironmentManager horizonClient *sdk.Client @@ -170,11 +173,11 @@ func NewTest(t *testing.T, config Config) *Test { } if !config.SkipHorizonStart { - if innerErr := i.StartHorizon(); innerErr != nil { + if innerErr := i.StartHorizon(true); innerErr != nil { t.Fatalf("Failed to start Horizon: %v", innerErr) } - i.WaitForHorizon() + i.WaitForHorizonIngest() } return i @@ -297,14 +300,15 @@ func (i *Test) prepareShutdownHandlers() { }() } -func (i *Test) RestartHorizon() error { +// if startIngestProcess=true, will restart the ingest sub process also +func (i *Test) RestartHorizon(restartIngestProcess bool) error { i.StopHorizon() - if err := i.StartHorizon(); err != nil { + if err := i.StartHorizon(restartIngestProcess); err != nil { return err } - i.WaitForHorizon() + i.WaitForHorizonIngest() return nil } @@ -316,6 +320,10 @@ func (i *Test) GetHorizonWebConfig() horizon.Config { return i.horizonWebConfig } +func (i *Test) GetTestDB() *dbtest.DB { + return i.testDB +} + // Shutdown stops the integration tests and destroys all its associated // resources. It will be implicitly called when the calling test (i.e. the // `testing.Test` passed to `New()`) is finished if it hasn't been explicitly @@ -329,68 +337,91 @@ func (i *Test) Shutdown() { }) } -// StartHorizon initializes and starts the Horizon client-facing API server and the ingest server. -func (i *Test) StartHorizon() error { - postgres := dbtest.Postgres(i.t) +// StartHorizon initializes and starts the Horizon client-facing API server. +// When startIngestProcess=true, start a second process for ingest server +func (i *Test) StartHorizon(startIngestProcess bool) error { + i.testDB = dbtest.Postgres(i.t) i.shutdownCalls = append(i.shutdownCalls, func() { + if i.appStopped == nil { + // appStopped is nil when the horizon cmd.Execute creates an App, but gets an intentional error and StartHorizon + // never gets to point of running App.Serve() which would have closed the db conn eventually + // since it wires up listener to App.Close() invocation, so, we must manually detect this edge case and + // close the app's db here to clean up + if i.webNode != nil { + i.webNode.CloseDB() + } + if i.ingestNode != nil { + i.ingestNode.CloseDB() + } + } i.StopHorizon() - postgres.Close() + i.testDB.Close() }) + var err error // To facilitate custom runs of Horizon, we merge a default set of // parameters with the tester-supplied ones (if any). - mergedWebArgs := MergeMaps(i.getDefaultWebArgs(postgres), i.config.HorizonWebParameters) - webArgs := mapToFlags(mergedWebArgs) - i.t.Log("Horizon command line webArgs:", webArgs) - - mergedIngestArgs := MergeMaps(i.getDefaultIngestArgs(postgres), i.config.HorizonIngestParameters) - ingestArgs := mapToFlags(mergedIngestArgs) - i.t.Log("Horizon command line ingestArgs:", ingestArgs) - - // setup Horizon web command - var err error - webConfig, webConfigOpts := horizon.Flags() - webCmd := i.createWebCommand(webConfig, webConfigOpts) - webCmd.SetArgs(webArgs) - if err = webConfigOpts.Init(webCmd); err != nil { - return errors.Wrap(err, "cannot initialize params") - } + mergedWebArgs := MergeMaps(i.getDefaultWebArgs(), i.config.HorizonWebParameters) + mergedIngestArgs := MergeMaps(i.getDefaultIngestArgs(), i.config.HorizonIngestParameters) - // setup Horizon ingest command - ingestConfig, ingestConfigOpts := horizon.Flags() - ingestCmd := i.createIngestCommand(ingestConfig, ingestConfigOpts) - ingestCmd.SetArgs(ingestArgs) - if err = ingestConfigOpts.Init(ingestCmd); err != nil { - return errors.Wrap(err, "cannot initialize params") + // Set up Horizon clients + i.setupHorizonClient(mergedWebArgs) + if err = i.setupHorizonAdminClient(mergedIngestArgs); err != nil { + return err } if err = i.initializeEnvironmentVariables(); err != nil { return err } - if err = ingestCmd.Execute(); err != nil { - return errors.Wrap(err, HorizonInitErrStr) + // setup Horizon web process + webArgs := mapToFlags(mergedWebArgs) + i.t.Log("Horizon command line webArgs:", webArgs) + webConfig, webConfigOpts := horizon.Flags() + webCmd := i.createWebCommand(webConfig, webConfigOpts) + webCmd.SetArgs(webArgs) + if err = webConfigOpts.Init(webCmd); err != nil { + return errors.Wrap(err, "cannot initialize params") } - if err = webCmd.Execute(); err != nil { return errors.Wrap(err, HorizonInitErrStr) } + i.horizonWebConfig = *webConfig - // Set up Horizon clients - i.setupHorizonClient(mergedWebArgs) - if err = i.setupHorizonAdminClient(mergedIngestArgs); err != nil { - return err + // setup horizon ingest process + if startIngestProcess { + ingestArgs := mapToFlags(mergedIngestArgs) + i.t.Log("Horizon command line ingestArgs:", ingestArgs) + // setup Horizon ingest command + ingestConfig, ingestConfigOpts := horizon.Flags() + ingestCmd := i.createIngestCommand(ingestConfig, ingestConfigOpts) + ingestCmd.SetArgs(ingestArgs) + if err = ingestConfigOpts.Init(ingestCmd); err != nil { + return errors.Wrap(err, "cannot initialize params") + } + if err = ingestCmd.Execute(); err != nil { + return errors.Wrap(err, HorizonInitErrStr) + } + i.horizonIngestConfig = *ingestConfig + } else { + // not running ingestion, normally that process would do migration through --apply-migrations + // so migrage the empty in any case directly + var rootCmd = horizoncmd.NewRootCmd() + rootCmd.SetArgs([]string{ + "db", "migrate", "up", "--db-url", i.testDB.DSN}) + require.NoError(i.t, rootCmd.Execute()) } - i.horizonIngestConfig = *ingestConfig - i.horizonWebConfig = *webConfig - i.appStopped = &sync.WaitGroup{} - i.appStopped.Add(2) - go func() { - _ = i.ingestNode.Serve() - i.appStopped.Done() - }() + if i.ingestNode != nil { + i.appStopped.Add(1) + go func() { + _ = i.ingestNode.Serve() + i.appStopped.Done() + }() + } + + i.appStopped.Add(1) go func() { _ = i.webNode.Serve() i.appStopped.Done() @@ -399,13 +430,13 @@ func (i *Test) StartHorizon() error { return nil } -func (i *Test) getDefaultArgs(postgres *dbtest.DB) map[string]string { +func (i *Test) getDefaultArgs() map[string]string { // TODO: Ideally, we'd be pulling host/port information from the Docker // Compose YAML file itself rather than hardcoding it. return map[string]string{ "ingest": "false", "history-archive-urls": fmt.Sprintf("http://%s:%d", "localhost", historyArchivePort), - "db-url": postgres.RO_DSN, + "db-url": i.testDB.RO_DSN, "stellar-core-url": i.coreClient.URL, "network-passphrase": i.passPhrase, "apply-migrations": "true", @@ -417,15 +448,15 @@ func (i *Test) getDefaultArgs(postgres *dbtest.DB) map[string]string { } } -func (i *Test) getDefaultWebArgs(postgres *dbtest.DB) map[string]string { - return MergeMaps(i.getDefaultArgs(postgres), map[string]string{"admin-port": "0"}) +func (i *Test) getDefaultWebArgs() map[string]string { + return MergeMaps(i.getDefaultArgs(), map[string]string{"admin-port": "0"}) } -func (i *Test) getDefaultIngestArgs(postgres *dbtest.DB) map[string]string { - return MergeMaps(i.getDefaultArgs(postgres), map[string]string{ +func (i *Test) getDefaultIngestArgs() map[string]string { + return MergeMaps(i.getDefaultArgs(), map[string]string{ "admin-port": strconv.Itoa(i.AdminPort()), "port": "8001", - "db-url": postgres.DSN, + "db-url": i.testDB.DSN, "stellar-core-binary-path": i.coreConfig.binaryPath, "captive-core-config-path": i.coreConfig.configPath, "captive-core-http-port": "21626", @@ -816,7 +847,17 @@ func (i *Test) UpgradeProtocol(version uint32) { i.t.Fatalf("could not upgrade protocol in 10s") } -func (i *Test) WaitForHorizon() { +func (i *Test) WaitForHorizonWeb() { + // wait until the web server is up before continuing to test requests + require.Eventually(i.t, func() bool { + if _, horizonErr := i.Client().Root(); horizonErr != nil { + return false + } + return true + }, time.Second*15, time.Millisecond*100) +} + +func (i *Test) WaitForHorizonIngest() { for t := 60; t >= 0; t -= 1 { time.Sleep(time.Second) diff --git a/support/datastore/datastore.go b/support/datastore/datastore.go index e7e999345d..961ba99545 100644 --- a/support/datastore/datastore.go +++ b/support/datastore/datastore.go @@ -21,6 +21,7 @@ type DataStore interface { PutFileIfNotExists(ctx context.Context, path string, in io.WriterTo, metaData map[string]string) (bool, error) Exists(ctx context.Context, path string) (bool, error) Size(ctx context.Context, path string) (int64, error) + GetSchema() DataStoreSchema Close() error } @@ -32,7 +33,7 @@ func NewDataStore(ctx context.Context, datastoreConfig DataStoreConfig) (DataSto if !ok { return nil, errors.Errorf("Invalid GCS config, no destination_bucket_path") } - return NewGCSDataStore(ctx, destinationBucketPath) + return NewGCSDataStore(ctx, destinationBucketPath, datastoreConfig.Schema) default: return nil, errors.Errorf("Invalid datastore type %v, not supported", datastoreConfig.Type) } diff --git a/support/datastore/gcs_datastore.go b/support/datastore/gcs_datastore.go index cdedea086d..ab1bc669b5 100644 --- a/support/datastore/gcs_datastore.go +++ b/support/datastore/gcs_datastore.go @@ -24,18 +24,19 @@ type GCSDataStore struct { client *storage.Client bucket *storage.BucketHandle prefix string + schema DataStoreSchema } -func NewGCSDataStore(ctx context.Context, bucketPath string) (DataStore, error) { +func NewGCSDataStore(ctx context.Context, bucketPath string, schema DataStoreSchema) (DataStore, error) { client, err := storage.NewClient(ctx) if err != nil { return nil, err } - return FromGCSClient(ctx, client, bucketPath) + return FromGCSClient(ctx, client, bucketPath, schema) } -func FromGCSClient(ctx context.Context, client *storage.Client, bucketPath string) (DataStore, error) { +func FromGCSClient(ctx context.Context, client *storage.Client, bucketPath string, schema DataStoreSchema) (DataStore, error) { // append the gcs:// scheme to enable usage of the url package reliably to // get parse bucket name which is first path segment as URL.Host gcsBucketURL := fmt.Sprintf("gcs://%s", bucketPath) @@ -55,7 +56,8 @@ func FromGCSClient(ctx context.Context, client *storage.Client, bucketPath strin return nil, fmt.Errorf("failed to retrieve bucket attributes: %w", err) } - return &GCSDataStore{client: client, bucket: bucket, prefix: prefix}, nil + // TODO: Datastore schema to be fetched from the datastore https://stellarorg.atlassian.net/browse/HUBBLE-397 + return &GCSDataStore{client: client, bucket: bucket, prefix: prefix, schema: schema}, nil } // GetFileMetadata retrieves the metadata for the specified file in the GCS bucket. @@ -177,3 +179,9 @@ func (b GCSDataStore) putFile(ctx context.Context, filePath string, in io.Writer } return w.Close() } + +// GetSchema returns the schema information which defines the structure +// and organization of data in the datastore. +func (b GCSDataStore) GetSchema() DataStoreSchema { + return b.schema +} diff --git a/support/datastore/gcs_test.go b/support/datastore/gcs_test.go index 8838e8dadb..618b5d602a 100644 --- a/support/datastore/gcs_test.go +++ b/support/datastore/gcs_test.go @@ -24,7 +24,7 @@ func TestGCSExists(t *testing.T) { }) defer server.Stop() - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet", DataStoreSchema{}) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) @@ -52,7 +52,7 @@ func TestGCSSize(t *testing.T) { }) defer server.Stop() - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet", DataStoreSchema{}) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) @@ -86,7 +86,7 @@ func TestGCSPutFile(t *testing.T) { DefaultEventBasedHold: false, }) - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet", DataStoreSchema{}) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) @@ -138,7 +138,7 @@ func TestGCSPutFileIfNotExists(t *testing.T) { }) defer server.Stop() - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet", DataStoreSchema{}) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) @@ -187,7 +187,7 @@ func TestGCSPutFileWithMetadata(t *testing.T) { DefaultEventBasedHold: false, }) - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet", DataStoreSchema{}) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) @@ -255,7 +255,7 @@ func TestGCSPutFileIfNotExistsWithMetadata(t *testing.T) { }) defer server.Stop() - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet", DataStoreSchema{}) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) @@ -323,7 +323,7 @@ func TestGCSGetNonExistentFile(t *testing.T) { }) defer server.Stop() - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet", DataStoreSchema{}) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) @@ -365,7 +365,7 @@ func TestGCSGetFileValidatesCRC32C(t *testing.T) { }) defer server.Stop() - store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet") + store, err := FromGCSClient(context.Background(), server.Client(), "test-bucket/objects/testnet", DataStoreSchema{}) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, store.Close()) diff --git a/support/datastore/mocks.go b/support/datastore/mocks.go index 96c15c1371..2fa39a4712 100644 --- a/support/datastore/mocks.go +++ b/support/datastore/mocks.go @@ -47,6 +47,11 @@ func (m *MockDataStore) Close() error { return args.Error(0) } +func (m *MockDataStore) GetSchema() DataStoreSchema { + args := m.Called() + return args.Get(0).(DataStoreSchema) +} + type MockResumableManager struct { mock.Mock } From a3fae02c3b634b353dc4c37597ccc1da78f46317 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Thu, 18 Jul 2024 22:10:46 -0700 Subject: [PATCH 211/234] services/ledgerexporter: Move ledgerexporter out of experimental (#5341) * services/ledgerexporter: Move ledgerexporter out of experimental * Add CHANGELOG.md * Fix git workflow * Rename Ledgerexporter to Galexie --- ...porter-release.yml => galexie-release.yml} | 28 +++++----- .../{ledgerexporter.yml => galexie.yml} | 23 ++++---- Makefile | 4 +- services/galexie/CHANGELOG.md | 0 .../galexie}/DEVELOPER_GUIDE.md | 34 ++++++------ .../galexie}/Makefile | 24 ++++----- .../galexie}/README.md | 48 ++++++++--------- .../galexie}/architecture.png | Bin .../galexie}/config.example.toml | 0 .../galexie}/docker/Dockerfile | 6 +-- .../galexie}/docker/config.test.toml | 0 .../galexie}/internal/app.go | 17 +++--- .../galexie}/internal/app_test.go | 2 +- .../galexie}/internal/config.go | 6 +-- .../galexie}/internal/config_test.go | 4 +- .../galexie}/internal/exportmanager.go | 4 +- .../galexie}/internal/exportmanager_test.go | 2 +- .../galexie}/internal/integration_test.go | 50 +++++++++--------- .../galexie}/internal/ledger_meta_archive.go | 2 +- .../internal/ledger_meta_archive_test.go | 2 +- .../galexie}/internal/main.go | 10 ++-- .../galexie}/internal/main_test.go | 6 +-- .../galexie}/internal/queue.go | 4 +- .../galexie}/internal/queue_test.go | 2 +- .../galexie}/internal/test/10perfile.toml | 0 .../galexie}/internal/test/15perfile.toml | 0 .../galexie}/internal/test/1perfile.toml | 0 .../galexie}/internal/test/64perfile.toml | 0 .../internal/test/captive-core-test.cfg | 0 .../test/integration_captive_core.cfg | 0 .../test/integration_config_template.toml | 0 .../test/invalid_captive_core_toml_path.toml | 0 .../galexie}/internal/test/invalid_empty.toml | 0 .../test/invalid_preconfigured_network.toml | 0 .../galexie}/internal/test/no_core_bin.toml | 0 .../galexie/internal/test/no_network.toml | 11 ++++ .../galexie}/internal/test/test.toml | 0 .../galexie}/internal/test/useragent.toml | 0 .../test/valid_captive_core_manual.toml | 0 .../test/valid_captive_core_override.toml | 0 .../valid_captive_core_override_archives.toml | 0 .../valid_captive_core_preconfigured.toml | 0 .../internal/test/validate_start_end.toml | 0 .../galexie}/internal/uploader.go | 8 +-- .../galexie}/internal/uploader_test.go | 2 +- .../galexie}/main.go | 4 +- 46 files changed, 157 insertions(+), 146 deletions(-) rename .github/workflows/{ledgerexporter-release.yml => galexie-release.yml} (63%) rename .github/workflows/{ledgerexporter.yml => galexie.yml} (66%) create mode 100644 services/galexie/CHANGELOG.md rename {exp/services/ledgerexporter => services/galexie}/DEVELOPER_GUIDE.md (54%) rename {exp/services/ledgerexporter => services/galexie}/Makefile (69%) rename {exp/services/ledgerexporter => services/galexie}/README.md (76%) rename {exp/services/ledgerexporter => services/galexie}/architecture.png (100%) rename {exp/services/ledgerexporter => services/galexie}/config.example.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/docker/Dockerfile (84%) rename {exp/services/ledgerexporter => services/galexie}/docker/config.test.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/app.go (96%) rename {exp/services/ledgerexporter => services/galexie}/internal/app_test.go (99%) rename {exp/services/ledgerexporter => services/galexie}/internal/config.go (99%) rename {exp/services/ledgerexporter => services/galexie}/internal/config_test.go (99%) rename {exp/services/ledgerexporter => services/galexie}/internal/exportmanager.go (97%) rename {exp/services/ledgerexporter => services/galexie}/internal/exportmanager_test.go (99%) rename {exp/services/ledgerexporter => services/galexie}/internal/integration_test.go (84%) rename {exp/services/ledgerexporter => services/galexie}/internal/ledger_meta_archive.go (98%) rename {exp/services/ledgerexporter => services/galexie}/internal/ledger_meta_archive_test.go (98%) rename {exp/services/ledgerexporter => services/galexie}/internal/main.go (95%) rename {exp/services/ledgerexporter => services/galexie}/internal/main_test.go (95%) rename {exp/services/ledgerexporter => services/galexie}/internal/queue.go (96%) rename {exp/services/ledgerexporter => services/galexie}/internal/queue_test.go (98%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/10perfile.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/15perfile.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/1perfile.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/64perfile.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/captive-core-test.cfg (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/integration_captive_core.cfg (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/integration_config_template.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/invalid_captive_core_toml_path.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/invalid_empty.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/invalid_preconfigured_network.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/no_core_bin.toml (100%) create mode 100644 services/galexie/internal/test/no_network.toml rename {exp/services/ledgerexporter => services/galexie}/internal/test/test.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/useragent.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/valid_captive_core_manual.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/valid_captive_core_override.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/valid_captive_core_override_archives.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/valid_captive_core_preconfigured.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/test/validate_start_end.toml (100%) rename {exp/services/ledgerexporter => services/galexie}/internal/uploader.go (94%) rename {exp/services/ledgerexporter => services/galexie}/internal/uploader_test.go (99%) rename {exp/services/ledgerexporter => services/galexie}/main.go (55%) diff --git a/.github/workflows/ledgerexporter-release.yml b/.github/workflows/galexie-release.yml similarity index 63% rename from .github/workflows/ledgerexporter-release.yml rename to .github/workflows/galexie-release.yml index 8738f53def..45e8152690 100644 --- a/.github/workflows/ledgerexporter-release.yml +++ b/.github/workflows/galexie-release.yml @@ -1,26 +1,26 @@ -name: Ledger Exporter release +name: Galexie Release on: push: - tags: ['ledgerexporter-v*'] + tags: ['galexie-v*'] jobs: publish-docker: - name: Test and push the Ledger Exporter images + name: Test and push docker image runs-on: ubuntu-latest env: - LEDGEREXPORTER_INTEGRATION_TESTS_ENABLED: "true" - LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN: /usr/bin/stellar-core + GALEXIE_INTEGRATION_TESTS_ENABLED: "true" + GALEXIE_INTEGRATION_TESTS_CAPTIVE_CORE_BIN: /usr/bin/stellar-core # this pins to a version of quickstart:testing that has the same version as STELLAR_CORE_VERSION # this is the multi-arch index sha, get it by 'docker buildx imagetools inspect stellar/quickstart:testing' - LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE: docker.io/stellar/quickstart:testing@sha256:03c6679f838a92b1eda4cd3a9e2bdee4c3586e278a138a0acf36a9bc99a0041f - LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL: "false" + GALEXIE_INTEGRATION_TESTS_QUICKSTART_IMAGE: docker.io/stellar/quickstart:testing@sha256:03c6679f838a92b1eda4cd3a9e2bdee4c3586e278a138a0acf36a9bc99a0041f + GALEXIE_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL: "false" STELLAR_CORE_VERSION: 21.1.0-1921.b3aeb14cc.focal steps: - name: Set VERSION run: | - echo "VERSION=${GITHUB_REF_NAME#ledgerexporter-v}" >> $GITHUB_ENV + echo "VERSION=${GITHUB_REF_NAME#galexie-v}" >> $GITHUB_ENV - uses: actions/checkout@v3 with: @@ -28,7 +28,7 @@ jobs: - name: Pull Quickstart image shell: bash run: | - docker pull "$LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE" + docker pull "$GALEXIE_INTEGRATION_TESTS_QUICKSTART_IMAGE" - name: Install captive core run: | # Workaround for https://github.com/actions/virtual-environments/issues/5245, @@ -42,11 +42,11 @@ jobs: sudo apt-get update && sudo apt-get install -y stellar-core="$STELLAR_CORE_VERSION" echo "Using stellar core version $(stellar-core version)" - - name: Run Ledger Exporter test - run: go test -v -race -run TestLedgerExporterTestSuite ./exp/services/ledgerexporter/... + - name: Run tests + run: go test -v -race -run TestGalexieTestSuite ./exp/services/galexie/... - - name: Build Ledger Exporter docker - run: make -C exp/services/ledgerexporter docker-build + - name: Build docker + run: make -C exp/services/galexie docker-build # Push images - name: Login to DockerHub @@ -56,4 +56,4 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Push to DockerHub - run: make -C exp/services/ledgerexporter docker-push + run: make -C exp/services/galexie docker-push diff --git a/.github/workflows/ledgerexporter.yml b/.github/workflows/galexie.yml similarity index 66% rename from .github/workflows/ledgerexporter.yml rename to .github/workflows/galexie.yml index ac1e265582..458f23ca37 100644 --- a/.github/workflows/ledgerexporter.yml +++ b/.github/workflows/galexie.yml @@ -1,4 +1,4 @@ -name: LedgerExporter +name: Galexie on: push: @@ -6,18 +6,17 @@ on: pull_request: jobs: - ledger-exporter: - name: Test Ledger Exporter + galexie: + name: Test runs-on: ubuntu-latest env: CAPTIVE_CORE_DEBIAN_PKG_VERSION: 21.1.0-1921.b3aeb14cc.focal - LEDGEREXPORTER_INTEGRATION_TESTS_ENABLED: "true" - LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN: /usr/bin/stellar-core - # this pins to a version of quickstart:testing that has the same version of core - # as specified on LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN + GALEXIE_INTEGRATION_TESTS_ENABLED: "true" + GALEXIE_INTEGRATION_TESTS_CAPTIVE_CORE_BIN: /usr/bin/stellar-core + # this pins to a version of quickstart:testing that has the same version as GALEXIE_INTEGRATION_TESTS_CAPTIVE_CORE_BIN # this is the multi-arch index sha, get it by 'docker buildx imagetools inspect stellar/quickstart:testing' - LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE: docker.io/stellar/quickstart:testing@sha256:5c8186f53cc98571749054dd782dce33b0aca2d1a622a7610362f7c15b79b1bf - LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL: "false" + GALEXIE_INTEGRATION_TESTS_QUICKSTART_IMAGE: docker.io/stellar/quickstart:testing@sha256:5c8186f53cc98571749054dd782dce33b0aca2d1a622a7610362f7c15b79b1bf + GALEXIE_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL: "false" steps: - name: Install captive core run: | @@ -35,12 +34,12 @@ jobs: - name: Pull Quickstart image shell: bash run: | - docker pull "$LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE" + docker pull "$GALEXIE_INTEGRATION_TESTS_QUICKSTART_IMAGE" - uses: actions/checkout@v3 with: # For pull requests, build and test the PR head not a merge of the PR with the destination. ref: ${{ github.event.pull_request.head.sha || github.ref }} - - name: Run Ledger Exporter test - run: go test -v -race -run TestLedgerExporterTestSuite ./exp/services/ledgerexporter/... + - name: Run test + run: go test -v -race -run TestGalexieTestSuite ./services/galexie/... diff --git a/Makefile b/Makefile index 69d46f68c3..6e41e653bb 100644 --- a/Makefile +++ b/Makefile @@ -34,8 +34,8 @@ friendbot: horizon: $(MAKE) -C services/horizon/ binary-build -ledger-exporter: - $(MAKE) -C exp/services/ledgerexporter/ docker-build +galexie: + $(MAKE) -C services/galexie/ docker-build webauth: $(MAKE) -C exp/services/webauth/ docker-build diff --git a/services/galexie/CHANGELOG.md b/services/galexie/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/exp/services/ledgerexporter/DEVELOPER_GUIDE.md b/services/galexie/DEVELOPER_GUIDE.md similarity index 54% rename from exp/services/ledgerexporter/DEVELOPER_GUIDE.md rename to services/galexie/DEVELOPER_GUIDE.md index 28a16ec1b0..da7a659c46 100644 --- a/exp/services/ledgerexporter/DEVELOPER_GUIDE.md +++ b/services/galexie/DEVELOPER_GUIDE.md @@ -1,28 +1,28 @@ -# Ledger Exporter Developer Guide -The ledger exporter is a tool to export Stellar network transaction data to cloud storage in a way that is easy to access. +# Galexie Developer Guide +Galexie is a tool to export Stellar network transaction data to cloud storage in a way that is easy to access. ## Prerequisites -This document assumes that you have installed and can run the ledger exporter, and that you have familiarity with its CLI and configuration. If not, please refer to the [Installation Guide](./README.md). +This document assumes that you have installed and can run Galexie, and that you have familiarity with its CLI and configuration. If not, please refer to the [Installation Guide](./README.md). ## Goal -The goal of the ledger exporter is to build an easy-to-use tool to export Stellar network ledger data to a configurable remote data store, such as cloud blob storage. +The goal of Galexie is to build an easy-to-use tool to export Stellar network ledger data to a configurable remote data store, such as cloud blob storage. - Use cloud storage optimally - Minimize network usage to export - Make it easy and fast to search for a specific ledger or ledger range ## Architecture -To achieve its goals, the ledger exporter uses the following architecture, which consists of the 3 main components: +To achieve its goals, Galexie uses the following architecture, which consists of the 3 main components: - Captive-core to extract raw transaction metadata from the Stellar Network. - Export manager to bundles and organizes the ledgers to get them ready for export. - The cloud storage plugin writes to the cloud storage. This is specific to the type of cloud storage, GCS in this case. -![ledgerexporter-architecture](./architecture.png) +![Architecture](./architecture.png) ## Data Format -- Ledger exporter uses a compact and efficient data format called [XDR](https://developers.stellar.org/docs/learn/encyclopedia/data-format/xdr) (External Data Representation), which is a compact binary format. A Stellar Captive Core instance emits data in this format and the data structure is referred to as `LedgerCloseMeta`. The exporter bundles multiple `LedgerCloseMeta`'s into a single object using a custom XDR structure called `LedgerCloseMetaBatch` which is defined in [Stellar-exporter.x](https://github.com/stellar/go/blob/master/xdr/Stellar-exporter.x). +- Galexie uses a compact and efficient data format called [XDR](https://developers.stellar.org/docs/learn/encyclopedia/data-format/xdr) (External Data Representation), which is a compact binary format. A Stellar Captive Core instance emits data in this format and the data structure is referred to as `LedgerCloseMeta`. The exporter bundles multiple `LedgerCloseMeta`'s into a single object using a custom XDR structure called `LedgerCloseMetaBatch` which is defined in [Stellar-exporter.x](https://github.com/stellar/go/blob/master/xdr/Stellar-exporter.x). - The metadata for the same batch is also stored alongside each exported object. Supported metadata is defined in [metadata.go](https://github.com/stellar/go/blob/master/support/datastore/metadata.go). @@ -30,32 +30,32 @@ To achieve its goals, the ledger exporter uses the following architecture, which ## Data Storage - An example implementation of `DataStore` for GCS, Google Cloud Storage. This plugin is located in the [support](https://github.com/stellar/go/tree/master/support/datastore) package. -- The ledger exporter currently implements the interface only for Google Cloud Storage (GCS). The [GCS plugin](https://github.com/stellar/go/blob/master/support/datastore/gcs_datastore.go) uses GCS-specific behaviors like conditional puts, automatic retry, metadata, and CRC checksum. +- Galexie currently implements the interface only for Google Cloud Storage (GCS). The [GCS plugin](https://github.com/stellar/go/blob/master/support/datastore/gcs_datastore.go) uses GCS-specific behaviors like conditional puts, automatic retry, metadata, and CRC checksum. ## Build and Run using Docker -The Dockerfile contains all the necessary dependencies (e.g., Stellar-core) required to run the ledger exporter. +The Dockerfile contains all the necessary dependencies (e.g., Stellar-core) required to run Galexie. - Build: To build the Docker container, use the provided [Makefile](./Makefile). Simply run make `make docker-build` to build a new container after making any changes. - Run: For instructions on running the Docker container, refer to the [Installation Guide](./README.md). -- Run ledgerexporter with a local, fake GCS backend: Requires `make docker-build` first, then run `make docker-test-fake-gcs`. This will run the ledger exporter against `testnet` and export to the 'fake' GCS instance started in the container. +- Run Galexie with a local, fake GCS backend: Requires `make docker-build` first, then run `make docker-test-fake-gcs`. This will run it against `testnet` and export to the 'fake' GCS instance started in the container. ## Running Integration Tests: -from top directory of stellar/go repo, run go test to launch ledger exporter integration +from top directory of stellar/go repo, run go test to launch Galexie integration tests. -`LEDGEREXPORTER_INTEGRATION_TESTS_ENABLED=true` is required environment variable to allow +`GALEXIE_INTEGRATION_TESTS_ENABLED=true` is required environment variable to allow tests to run. -Optional, tests will try to run `stellar-core` from o/s PATH for captive core, if not resolvable, then set `LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN=/path/to/stellar-core` +Optional, tests will try to run `stellar-core` from o/s PATH for captive core, if not resolvable, then set `GALEXIE_INTEGRATION_TESTS_CAPTIVE_CORE_BIN=/path/to/stellar-core` -Optional, can override the version of quickstart used to run standalone stellar network, `LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE=docker.io/stellar/quickstart:`. By default it will try to docker pull `stellar/quickstart:testing` image to local host's docker image store. Set `LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL=false` to skip the pull, if you know host has up to date image. +Optional, can override the version of quickstart used to run standalone stellar network, `GALEXIE_INTEGRATION_TESTS_QUICKSTART_IMAGE=docker.io/stellar/quickstart:`. By default it will try to docker pull `stellar/quickstart:testing` image to local host's docker image store. Set `GALEXIE_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL=false` to skip the pull, if you know host has up to date image. -Note, the version of stellar core in `LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE` and `LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN` needs to be on the same major rev or the captive core process may not be able to join or parse ledger meta from the `local` network created by `LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE` +Note, the version of stellar core in `GALEXIE_INTEGRATION_TESTS_QUICKSTART_IMAGE` and `GALEXIE_INTEGRATION_TESTS_CAPTIVE_CORE_BIN` needs to be on the same major rev or the captive core process may not be able to join or parse ledger meta from the `local` network created by `GALEXIE_INTEGRATION_TESTS_QUICKSTART_IMAGE` ``` -$ LEDGEREXPORTER_INTEGRATION_TESTS_ENABLED=true go test -v -race -run TestLedgerExporterTestSuite ./exp/services/ledgerexporter/... +$ GALEXIE_INTEGRATION_TESTS_ENABLED=true go test -v -race -run TestGalexieTestSuite ./services/galexie/... ``` ## Adding support for a new storage type @@ -71,7 +71,7 @@ Support for different data storage types are encapsulated as 'plugins', which ar - An emulator such as a GCS emulator [fake-gcs-server](https://github.com/fsouza/fake-gcs-server) can be used for testing without connecting to real cloud storage. ### Design DOs and DONTs -- Multiple exporters should be able to run in parallel without the need for explicit locking or synchronization. +- Multiple Galexie instances should be able to run in parallel without the need for explicit locking or synchronization. - Exporters when restarted do not have any memory of prior operation and rely on the already exported data as much as possible to decide where to resume. ## Using exported data diff --git a/exp/services/ledgerexporter/Makefile b/services/galexie/Makefile similarity index 69% rename from exp/services/ledgerexporter/Makefile rename to services/galexie/Makefile index 6561c4f24c..f501300e11 100644 --- a/exp/services/ledgerexporter/Makefile +++ b/services/galexie/Makefile @@ -3,14 +3,14 @@ SUDO := $(shell docker version >/dev/null 2>&1 || echo "sudo") # https://github.com/opencontainers/image-spec/blob/master/annotations.md BUILD_DATE := $(shell date -u +%FT%TZ) VERSION ?= $(shell git rev-parse --short HEAD) -DOCKER_IMAGE := stellar/ledger-exporter +DOCKER_IMAGE := stellar/stellar-galexie docker-build: - cd ../../../ && \ + cd ../../ && \ $(SUDO) docker build --platform linux/amd64 --pull --label org.opencontainers.image.created="$(BUILD_DATE)" \ - --build-arg GOFLAGS="-ldflags=-X=github.com/stellar/go/exp/services/ledgerexporter/internal.version=$(VERSION)" \ + --build-arg GOFLAGS="-ldflags=-X=github.com/stellar/go/services/galexie/internal.version=$(VERSION)" \ $(if $(STELLAR_CORE_VERSION), --build-arg STELLAR_CORE_VERSION=$(STELLAR_CORE_VERSION)) \ - -f exp/services/ledgerexporter/docker/Dockerfile \ + -f services/galexie/docker/Dockerfile \ -t $(DOCKER_IMAGE):$(VERSION) \ -t $(DOCKER_IMAGE):latest . @@ -22,7 +22,7 @@ docker-clean: docker-test-fake-gcs: docker-clean # Create temp storage dir - $(SUDO) mkdir -p ${PWD}/storage/exporter-test + $(SUDO) mkdir -p ${PWD}/storage/galexie-test # Create test network for docker $(SUDO) docker network create test-network @@ -31,13 +31,13 @@ docker-test-fake-gcs: docker-clean $(SUDO) docker run -d --name fake-gcs-server -p 4443:4443 \ -v ${PWD}/storage:/data --network test-network fsouza/fake-gcs-server -scheme http - # Run the ledger-exporter - $(SUDO) docker run --platform linux/amd64 -t --network test-network\ - -v ${PWD}/exp/services/ledgerexporter/docker/config.test.toml:/config.toml \ - -e STORAGE_EMULATOR_HOST=http://fake-gcs-server:4443 \ - $(DOCKER_IMAGE):$(VERSION) \ - scan-and-fill --start 1000 --end 2000 - + # Run + $(SUDO) docker run --platform linux/amd64 -t --network test-network \ + -v ${PWD}/exp/services/galexie/docker/config.test.toml:/config.toml \ + -e STORAGE_EMULATOR_HOST=http://fake-gcs-server:4443 \ + $(DOCKER_IMAGE):$(VERSION) \ + scan-and-fill --start 1000 --end 2000 + $(MAKE) docker-clean docker-push: diff --git a/exp/services/ledgerexporter/README.md b/services/galexie/README.md similarity index 76% rename from exp/services/ledgerexporter/README.md rename to services/galexie/README.md index 8308bfdaeb..0c4527e637 100644 --- a/exp/services/ledgerexporter/README.md +++ b/services/galexie/README.md @@ -1,23 +1,23 @@ -## Ledger Exporter: Installation and Usage Guide +## Galexie: Installation and Usage Guide -This guide provides step-by-step instructions on installing and using the Ledger Exporter, a tool that exports Stellar network ledger data to a Google Cloud Storage (GCS) bucket for efficient analysis and storage. +This guide provides step-by-step instructions on installing and using the Galexie - Ledger Exporter, a tool that exports Stellar network ledger data to a Google Cloud Storage (GCS) bucket for efficient analysis and storage. * [Prerequisites](#prerequisites) * [Setup](#setup) * [Set Up GCP Credentials](#set-up-gcp-credentials) * [Create a GCS Bucket for Storage](#create-a-gcs-bucket-for-storage) -* [Running the Ledger Exporter](#running-the-ledger-exporter) +* [Running Galexie](#running-galexie) * [Pull the Docker Image](#1-pull-the-docker-image) - * [Configure the Exporter](#2-configure-the-exporter-configtoml) - * [Run the Exporter](#3-run-the-exporter) + * [Configure](#2-configure-configtoml) + * [Run](#3-run) * [Command Line Interface (CLI)](#command-line-interface-cli) - 1. [scan-and-fill: Fill Data Gaps](#1-scan-and-fill-fill-data-gaps) - 2. [append: Continuously Export New Data](#2-append-continuously-export-new-data) + 1. [append: Continuously Export New Data](#1-append-continuously-export-new-data) + 2. [scan-and-fill: Fill Data Gaps](#2-scan-and-fill-fill-data-gaps) ## Prerequisites * **Google Cloud Platform (GCP) Account:** You will need a GCP account to create a GCS bucket for storing the exported data. -* **Docker:** Allows you to run the Ledger Exporter in a self-contained environment. The official Docker installation guide: [https://docs.docker.com/engine/install/](https://docs.docker.com/engine/install/) +* **Docker:** Allows you to run the Galexie in a self-contained environment. The official Docker installation guide: [https://docs.docker.com/engine/install/](https://docs.docker.com/engine/install/) ## Setup @@ -37,34 +37,34 @@ For detailed instructions, refer to the [Providing Credentials for Application D 3. **Note down the bucket name** as you'll need it later in the configuration process. -## Running the Ledger Exporter +## Running Galexie ### 1. Pull the Docker Image -Open a terminal window and download the Stellar Ledger Exporter Docker image using the following command: +Open a terminal window and download the Stellar Galexie Docker image using the following command: ```bash -docker pull stellar/ledger-exporter +docker pull stellar/stellar-galexie ``` -### 2. Configure the Exporter (config.toml) -The Ledger Exporter relies on a configuration file (config.toml) to connect to your specific environment. This file defines details like: +### 2. Configure (config.toml) +Galexie relies on a configuration file (config.toml) to connect to your specific environment. This file defines details like: - Your Google Cloud Storage (GCS) bucket where exported ledger data will be stored. - Stellar network settings, such as the network you're using (testnet or pubnet). - Datastore schema to control data organization. A sample configuration file [config.example.toml](config.example.toml) is provided. Copy and rename it to config.toml for customization. Edit the copied file (config.toml) to replace placeholders with your specific details. -### 3. Run the Exporter +### 3. Run -The following command demonstrates how to run the Ledger Exporter: +The following command demonstrates how to run the Galexie: ```bash docker run --platform linux/amd64 \ -v "$HOME/.config/gcloud/application_default_credentials.json":/.config/gcp/credentials.json:ro \ -e GOOGLE_APPLICATION_CREDENTIALS=/.config/gcp/credentials.json \ -v ${PWD}/config.toml:/config.toml \ - stellar/ledger-exporter [options] + stellar/stellar-galexie [options] ``` **Explanation:** @@ -74,12 +74,12 @@ docker run --platform linux/amd64 \ * `$HOME/.config/gcloud/application_default_credentials.json`: Your local GCP credentials file. * `${PWD}/config.toml`: Your local configuration file. * `-e GOOGLE_APPLICATION_CREDENTIALS=/.config/gcp/credentials.json`: Sets the environment variable for credentials within the container. -* `stellar/ledger-exporter`: The Docker image name. -* ``: The Stellar Ledger Exporter command: [append](#1-append-continuously-export-new-data), [scan-and-fill](#2-scan-and-fill-fill-data-gaps)) +* `stellar/stellar-galexie`: The Docker image name. +* ``: The Stellar Galexie command: [append](#1-append-continuously-export-new-data), [scan-and-fill](#2-scan-and-fill-fill-data-gaps)) ## Command Line Interface (CLI) -The Ledger Exporter offers two mode of operation for exporting ledger data: +Galexie offers two mode of operation for exporting ledger data: ### 1. append: Continuously Export New Data @@ -99,14 +99,14 @@ docker run --platform linux/amd64 -d \ -v "$HOME/.config/gcloud/application_default_credentials.json":/.config/gcp/credentials.json:ro \ -e GOOGLE_APPLICATION_CREDENTIALS=/.config/gcp/credentials.json \ -v ${PWD}/config.toml:/config.toml \ - stellar/ledger-exporter \ + stellar/stellar-galexie \ append --start [--end ] [--config-file ] ``` Arguments: - `--start ` (required): The starting ledger sequence number for the export process. -- `--end ` (optional): The ending ledger sequence number. If omitted or set to 0, the exporter will continuously export new ledgers as they appear on the network. -- `--config-file ` (optional): The path to your configuration file, containing details like GCS bucket information. If not provided, the exporter will look for config.toml in the directory where you run the command. +- `--end ` (optional): The ending ledger sequence number. If omitted or set to 0, it will continuously export new ledgers as they appear on the network. +- `--config-file ` (optional): The path to your configuration file, containing details like GCS bucket information. If not provided, it will look for config.toml in the directory where you run the command. ### 2. scan-and-fill: Fill Data Gaps @@ -119,11 +119,11 @@ docker run --platform linux/amd64 -d \ -v "$HOME/.config/gcloud/application_default_credentials.json":/.config/gcp/credentials.json:ro \ -e GOOGLE_APPLICATION_CREDENTIALS=/.config/gcp/credentials.json \ -v ${PWD}/config.toml:/config.toml \ - stellar/ledger-exporter \ + stellar/stellar-galexie \ scan-and-fill --start --end [--config-file ] ``` Arguments: - `--start ` (required): The starting ledger sequence number in the range to export. - `--end ` (required): The ending ledger sequence number in the range. -- `--config-file ` (optional): The path to your configuration file, containing details like GCS bucket information. If not provided, the exporter will look for config.toml in the directory where you run the command. \ No newline at end of file +- `--config-file ` (optional): The path to your configuration file, containing details like GCS bucket information. If not provided, the exporter will look for config.toml in the directory where you run the command. diff --git a/exp/services/ledgerexporter/architecture.png b/services/galexie/architecture.png similarity index 100% rename from exp/services/ledgerexporter/architecture.png rename to services/galexie/architecture.png diff --git a/exp/services/ledgerexporter/config.example.toml b/services/galexie/config.example.toml similarity index 100% rename from exp/services/ledgerexporter/config.example.toml rename to services/galexie/config.example.toml diff --git a/exp/services/ledgerexporter/docker/Dockerfile b/services/galexie/docker/Dockerfile similarity index 84% rename from exp/services/ledgerexporter/docker/Dockerfile rename to services/galexie/docker/Dockerfile index 7144800d87..6614a40692 100644 --- a/exp/services/ledgerexporter/docker/Dockerfile +++ b/services/galexie/docker/Dockerfile @@ -10,7 +10,7 @@ RUN go mod download COPY . ./ ARG GOFLAGS -RUN go install github.com/stellar/go/exp/services/ledgerexporter +RUN go install github.com/stellar/go/services/galexie FROM ubuntu:22.04 ARG STELLAR_CORE_VERSION @@ -26,9 +26,9 @@ RUN echo "deb https://apt.stellar.org focal unstable" >/etc/apt/sources.list.d/S RUN apt-get update && apt-get install -y stellar-core=${STELLAR_CORE_VERSION} RUN apt-get clean -COPY --from=builder /go/bin/ledgerexporter /usr/bin/ledgerexporter +COPY --from=builder /go/bin/galexie /usr/bin/galexie -ENTRYPOINT ["/usr/bin/ledgerexporter"] +ENTRYPOINT ["/usr/bin/galexie"] CMD ["--help"] diff --git a/exp/services/ledgerexporter/docker/config.test.toml b/services/galexie/docker/config.test.toml similarity index 100% rename from exp/services/ledgerexporter/docker/config.test.toml rename to services/galexie/docker/config.test.toml diff --git a/exp/services/ledgerexporter/internal/app.go b/services/galexie/internal/app.go similarity index 96% rename from exp/services/ledgerexporter/internal/app.go rename to services/galexie/internal/app.go index 00382f00e4..7117526302 100644 --- a/exp/services/ledgerexporter/internal/app.go +++ b/services/galexie/internal/app.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "context" @@ -35,10 +35,11 @@ const ( // size in our metrics that is an indication that uploads to the // data store have degraded uploadQueueCapacity = 128 + nameSpace = "galexie" ) var ( - logger = log.New().WithField("service", "ledger-exporter") + logger = log.New().WithField("service", nameSpace) version = "develop" ) @@ -96,11 +97,11 @@ func (a *App) init(ctx context.Context, runtimeSettings RuntimeSettings) error { var err error var archive historyarchive.ArchiveInterface - logger.Infof("Starting Ledger Exporter with version %s", version) + logger.Infof("Starting Galexie with version %s", version) registry := prometheus.NewRegistry() registry.MustRegister( - collectors.NewProcessCollector(collectors.ProcessCollectorOpts{Namespace: "ledger_exporter"}), + collectors.NewProcessCollector(collectors.ProcessCollectorOpts{Namespace: nameSpace}), collectors.NewGoCollector(), ) @@ -193,10 +194,10 @@ func (a *App) Run(runtimeSettings RuntimeSettings) error { var dataAlreadyExported *DataAlreadyExportedError if errors.As(err, &dataAlreadyExported) { logger.Info(err.Error()) - logger.Info("Shutting down ledger-exporter") + logger.Info("Shutting down Galexie") return nil } - logger.WithError(err).Error("Stopping ledger-exporter") + logger.WithError(err).Error("Stopping Galexie") return err } defer a.close() @@ -250,7 +251,7 @@ func (a *App) Run(runtimeSettings RuntimeSettings) error { }() wg.Wait() - logger.Info("Shutting down ledger-exporter") + logger.Info("Shutting down Galexie") if a.adminServer != nil { serverShutdownCtx, serverShutdownCancel := context.WithTimeout(context.Background(), adminServerShutdownTimeout) @@ -280,7 +281,7 @@ func newLedgerBackend(config *Config, prometheusRegistry *prometheus.Registry) ( if err != nil { return nil, errors.Wrap(err, "Failed to create captive-core instance") } - backend = ledgerbackend.WithMetrics(backend, prometheusRegistry, "ledger_exporter") + backend = ledgerbackend.WithMetrics(backend, prometheusRegistry, nameSpace) return backend, nil } diff --git a/exp/services/ledgerexporter/internal/app_test.go b/services/galexie/internal/app_test.go similarity index 99% rename from exp/services/ledgerexporter/internal/app_test.go rename to services/galexie/internal/app_test.go index bb715baa6f..205869ae63 100644 --- a/exp/services/ledgerexporter/internal/app_test.go +++ b/services/galexie/internal/app_test.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "context" diff --git a/exp/services/ledgerexporter/internal/config.go b/services/galexie/internal/config.go similarity index 99% rename from exp/services/ledgerexporter/internal/config.go rename to services/galexie/internal/config.go index 196327e229..563686033e 100644 --- a/exp/services/ledgerexporter/internal/config.go +++ b/services/galexie/internal/config.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "context" @@ -22,7 +22,7 @@ import ( const ( Pubnet = "pubnet" Testnet = "testnet" - UserAgent = "ledgerexporter" + UserAgent = "galexie" ) type Mode int @@ -199,7 +199,7 @@ func (config *Config) GenerateCaptiveCoreConfig(coreBinFromPath string) (ledgerb CheckpointFrequency: checkpointFrequency, Log: logger.WithField("subservice", "stellar-core"), Toml: captiveCoreToml, - UserAgent: "ledger-exporter", + UserAgent: config.UserAgent, UseDB: true, StoragePath: config.StellarCoreConfig.StoragePath, }, nil diff --git a/exp/services/ledgerexporter/internal/config_test.go b/services/galexie/internal/config_test.go similarity index 99% rename from exp/services/ledgerexporter/internal/config_test.go rename to services/galexie/internal/config_test.go index d1c24cb198..3925c5b0de 100644 --- a/exp/services/ledgerexporter/internal/config_test.go +++ b/services/galexie/internal/config_test.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "context" @@ -30,7 +30,7 @@ func TestNewConfig(t *testing.T) { require.Equal(t, config.DataStoreConfig.Type, "ABC") require.Equal(t, config.DataStoreConfig.Schema.FilesPerPartition, uint32(1)) require.Equal(t, config.DataStoreConfig.Schema.LedgersPerFile, uint32(3)) - require.Equal(t, config.UserAgent, "ledgerexporter") + require.Equal(t, config.UserAgent, "galexie") require.True(t, config.Resumable()) url, ok := config.DataStoreConfig.Params["destination_bucket_path"] require.True(t, ok) diff --git a/exp/services/ledgerexporter/internal/exportmanager.go b/services/galexie/internal/exportmanager.go similarity index 97% rename from exp/services/ledgerexporter/internal/exportmanager.go rename to services/galexie/internal/exportmanager.go index af2633d6be..f6ea27d1e2 100644 --- a/exp/services/ledgerexporter/internal/exportmanager.go +++ b/services/galexie/internal/exportmanager.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "context" @@ -34,7 +34,7 @@ func NewExportManager(dataStoreSchema datastore.DataStoreSchema, } latestLedgerMetric := prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Namespace: "ledger_exporter", Subsystem: "export_manager", Name: "latest_ledger", + Namespace: nameSpace, Subsystem: "export_manager", Name: "latest_ledger", Help: "sequence number of the latest ledger consumed by the export manager", }, []string{"start_ledger", "end_ledger"}) prometheusRegistry.MustRegister(latestLedgerMetric) diff --git a/exp/services/ledgerexporter/internal/exportmanager_test.go b/services/galexie/internal/exportmanager_test.go similarity index 99% rename from exp/services/ledgerexporter/internal/exportmanager_test.go rename to services/galexie/internal/exportmanager_test.go index 7845186c06..08829a8f8e 100644 --- a/exp/services/ledgerexporter/internal/exportmanager_test.go +++ b/services/galexie/internal/exportmanager_test.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "context" diff --git a/exp/services/ledgerexporter/internal/integration_test.go b/services/galexie/internal/integration_test.go similarity index 84% rename from exp/services/ledgerexporter/internal/integration_test.go rename to services/galexie/internal/integration_test.go index ccc5463908..7c84e13ea0 100644 --- a/exp/services/ledgerexporter/internal/integration_test.go +++ b/services/galexie/internal/integration_test.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "bytes" @@ -37,16 +37,16 @@ const ( configTemplate = "test/integration_config_template.toml" ) -func TestLedgerExporterTestSuite(t *testing.T) { - if os.Getenv("LEDGEREXPORTER_INTEGRATION_TESTS_ENABLED") != "true" { - t.Skip("skipping integration test: LEDGEREXPORTER_INTEGRATION_TESTS_ENABLED not true") +func TestGalexieTestSuite(t *testing.T) { + if os.Getenv("GALEXIE_INTEGRATION_TESTS_ENABLED") != "true" { + t.Skip("skipping integration test: GALEXIE_INTEGRATION_TESTS_ENABLED not true") } - ledgerExporterSuite := &LedgerExporterTestSuite{} - suite.Run(t, ledgerExporterSuite) + galexieSuite := &GalexieTestSuite{} + suite.Run(t, galexieSuite) } -type LedgerExporterTestSuite struct { +type GalexieTestSuite struct { suite.Suite tempConfigFile string ctx context.Context @@ -58,7 +58,7 @@ type LedgerExporterTestSuite struct { config Config } -func (s *LedgerExporterTestSuite) TestScanAndFill() { +func (s *GalexieTestSuite) TestScanAndFill() { require := s.Require() rootCmd := defineCommands() @@ -83,7 +83,7 @@ func (s *LedgerExporterTestSuite) TestScanAndFill() { require.NoError(err) } -func (s *LedgerExporterTestSuite) TestAppend() { +func (s *GalexieTestSuite) TestAppend() { require := s.Require() // first populate ledgers 4-5 @@ -113,7 +113,7 @@ func (s *LedgerExporterTestSuite) TestAppend() { require.NoError(err) } -func (s *LedgerExporterTestSuite) TestAppendUnbounded() { +func (s *GalexieTestSuite) TestAppendUnbounded() { require := s.Require() rootCmd := defineCommands() @@ -147,7 +147,7 @@ func (s *LedgerExporterTestSuite) TestAppendUnbounded() { }, 180*time.Second, 50*time.Millisecond, "append unbounded did not work") } -func (s *LedgerExporterTestSuite) SetupSuite() { +func (s *GalexieTestSuite) SetupSuite() { var err error t := s.T() @@ -160,19 +160,19 @@ func (s *LedgerExporterTestSuite) SetupSuite() { }() testTempDir := t.TempDir() - ledgerExporterConfigTemplate, err := toml.LoadFile(configTemplate) + galexieConfigTemplate, err := toml.LoadFile(configTemplate) if err != nil { t.Fatalf("unable to load config template file %v, %v", configTemplate, err) } - // if LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN not specified, - // ledgerexporter will attempt resolve core bin using 'stellar-core' from OS path - ledgerExporterConfigTemplate.Set("stellar_core_config.stellar_core_binary_path", - os.Getenv("LEDGEREXPORTER_INTEGRATION_TESTS_CAPTIVE_CORE_BIN")) + // if GALEXIE_INTEGRATION_TESTS_CAPTIVE_CORE_BIN not specified, + // galexie will attempt resolve core bin using 'stellar-core' from OS path + galexieConfigTemplate.Set("stellar_core_config.stellar_core_binary_path", + os.Getenv("GALEXIE_INTEGRATION_TESTS_CAPTIVE_CORE_BIN")) - ledgerExporterConfigTemplate.Set("stellar_core_config.storage_path", filepath.Join(testTempDir, "captive-core")) + galexieConfigTemplate.Set("stellar_core_config.storage_path", filepath.Join(testTempDir, "captive-core")) - tomlBytes, err := toml.Marshal(ledgerExporterConfigTemplate) + tomlBytes, err := toml.Marshal(galexieConfigTemplate) if err != nil { t.Fatalf("unable to parse config file toml %v, %v", configTemplate, err) } @@ -211,22 +211,22 @@ func (s *LedgerExporterTestSuite) SetupSuite() { t.Logf("fake gcs server started at %v", s.gcsServer.URL()) t.Setenv("STORAGE_EMULATOR_HOST", s.gcsServer.URL()) - quickstartImage := os.Getenv("LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE") + quickstartImage := os.Getenv("GALEXIE_INTEGRATION_TESTS_QUICKSTART_IMAGE") if quickstartImage == "" { quickstartImage = "stellar/quickstart:testing" } pullQuickStartImage := true - if os.Getenv("LEDGEREXPORTER_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL") == "false" { + if os.Getenv("GALEXIE_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL") == "false" { pullQuickStartImage = false } s.mustStartCore(t, quickstartImage, pullQuickStartImage) - s.mustWaitForCore(t, ledgerExporterConfigTemplate.GetArray("stellar_core_config.history_archive_urls").([]string), - ledgerExporterConfigTemplate.Get("stellar_core_config.network_passphrase").(string)) + s.mustWaitForCore(t, galexieConfigTemplate.GetArray("stellar_core_config.history_archive_urls").([]string), + galexieConfigTemplate.Get("stellar_core_config.network_passphrase").(string)) s.finishedSetup = true } -func (s *LedgerExporterTestSuite) TearDownSuite() { +func (s *GalexieTestSuite) TearDownSuite() { if s.coreContainerID != "" { s.T().Logf("Stopping the quickstart container %v", s.coreContainerID) containerLogs, err := s.dockerCli.ContainerLogs(s.ctx, s.coreContainerID, container.LogsOptions{ShowStdout: true, ShowStderr: true}) @@ -251,7 +251,7 @@ func (s *LedgerExporterTestSuite) TearDownSuite() { s.ctxStop() } -func (s *LedgerExporterTestSuite) mustStartCore(t *testing.T, quickstartImage string, pullImage bool) { +func (s *GalexieTestSuite) mustStartCore(t *testing.T, quickstartImage string, pullImage bool) { var err error s.dockerCli, err = client.NewClientWithOpts(client.WithAPIVersionNegotiation()) if err != nil { @@ -308,7 +308,7 @@ func (s *LedgerExporterTestSuite) mustStartCore(t *testing.T, quickstartImage st t.Logf("Started quickstart container %v", s.coreContainerID) } -func (s *LedgerExporterTestSuite) mustWaitForCore(t *testing.T, archiveUrls []string, passphrase string) { +func (s *GalexieTestSuite) mustWaitForCore(t *testing.T, archiveUrls []string, passphrase string) { t.Log("Waiting for core to be up...") startTime := time.Now() infoTime := startTime diff --git a/exp/services/ledgerexporter/internal/ledger_meta_archive.go b/services/galexie/internal/ledger_meta_archive.go similarity index 98% rename from exp/services/ledgerexporter/internal/ledger_meta_archive.go rename to services/galexie/internal/ledger_meta_archive.go index f63a77f28b..e2d0368a5e 100644 --- a/exp/services/ledgerexporter/internal/ledger_meta_archive.go +++ b/services/galexie/internal/ledger_meta_archive.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "github.com/stellar/go/support/compressxdr" diff --git a/exp/services/ledgerexporter/internal/ledger_meta_archive_test.go b/services/galexie/internal/ledger_meta_archive_test.go similarity index 98% rename from exp/services/ledgerexporter/internal/ledger_meta_archive_test.go rename to services/galexie/internal/ledger_meta_archive_test.go index 152f2c6496..36240f6a3a 100644 --- a/exp/services/ledgerexporter/internal/ledger_meta_archive_test.go +++ b/services/galexie/internal/ledger_meta_archive_test.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "testing" diff --git a/exp/services/ledgerexporter/internal/main.go b/services/galexie/internal/main.go similarity index 95% rename from exp/services/ledgerexporter/internal/main.go rename to services/galexie/internal/main.go index f0ff076ae3..21db0dadbd 100644 --- a/exp/services/ledgerexporter/internal/main.go +++ b/services/galexie/internal/main.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "context" @@ -11,7 +11,7 @@ import ( ) var ( - ledgerExporterCmdRunner = func(runtimeSettings RuntimeSettings) error { + galexieCmdRunner = func(runtimeSettings RuntimeSettings) error { app := NewApp() return app.Run(runtimeSettings) } @@ -24,7 +24,7 @@ func Execute() error { func defineCommands() *cobra.Command { var rootCmd = &cobra.Command{ - Use: "ledgerexporter", + Use: "galexie", Short: "Export Stellar network ledger data to a remote data store", Long: "Converts ledger meta data from Stellar network into static data and exports it remote data storage.", @@ -46,7 +46,7 @@ func defineCommands() *cobra.Command { if settings.Ctx == nil { settings.Ctx = context.Background() } - return ledgerExporterCmdRunner(settings) + return galexieCmdRunner(settings) }, } var appendCmd = &cobra.Command{ @@ -63,7 +63,7 @@ func defineCommands() *cobra.Command { if settings.Ctx == nil { settings.Ctx = context.Background() } - return ledgerExporterCmdRunner(settings) + return galexieCmdRunner(settings) }, } diff --git a/exp/services/ledgerexporter/internal/main_test.go b/services/galexie/internal/main_test.go similarity index 95% rename from exp/services/ledgerexporter/internal/main_test.go rename to services/galexie/internal/main_test.go index c9fe0f853d..f86dbc1b58 100644 --- a/exp/services/ledgerexporter/internal/main_test.go +++ b/services/galexie/internal/main_test.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "bytes" @@ -102,8 +102,8 @@ func TestFlagsOutput(t *testing.T) { } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - // mock the ledger exporter's cmd runner to be this test's mock routine instead of real app - ledgerExporterCmdRunner = testCase.appRunner + // mock galexie's cmd runner to be this test's mock routine instead of real app + galexieCmdRunner = testCase.appRunner rootCmd := defineCommands() rootCmd.SetArgs(testCase.commandArgs) var errWriter io.Writer = &bytes.Buffer{} diff --git a/exp/services/ledgerexporter/internal/queue.go b/services/galexie/internal/queue.go similarity index 96% rename from exp/services/ledgerexporter/internal/queue.go rename to services/galexie/internal/queue.go index 372ccb0056..13b3d6aba2 100644 --- a/exp/services/ledgerexporter/internal/queue.go +++ b/services/galexie/internal/queue.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "context" @@ -15,7 +15,7 @@ type UploadQueue struct { // NewUploadQueue constructs a new UploadQueue func NewUploadQueue(size int, prometheusRegistry *prometheus.Registry) UploadQueue { queueLengthMetric := prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: "ledger_exporter", + Namespace: nameSpace, Subsystem: "upload_queue", Name: "length", Help: "The number of objects queued for upload", diff --git a/exp/services/ledgerexporter/internal/queue_test.go b/services/galexie/internal/queue_test.go similarity index 98% rename from exp/services/ledgerexporter/internal/queue_test.go rename to services/galexie/internal/queue_test.go index 0676f3594f..9318aca439 100644 --- a/exp/services/ledgerexporter/internal/queue_test.go +++ b/services/galexie/internal/queue_test.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "context" diff --git a/exp/services/ledgerexporter/internal/test/10perfile.toml b/services/galexie/internal/test/10perfile.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/10perfile.toml rename to services/galexie/internal/test/10perfile.toml diff --git a/exp/services/ledgerexporter/internal/test/15perfile.toml b/services/galexie/internal/test/15perfile.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/15perfile.toml rename to services/galexie/internal/test/15perfile.toml diff --git a/exp/services/ledgerexporter/internal/test/1perfile.toml b/services/galexie/internal/test/1perfile.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/1perfile.toml rename to services/galexie/internal/test/1perfile.toml diff --git a/exp/services/ledgerexporter/internal/test/64perfile.toml b/services/galexie/internal/test/64perfile.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/64perfile.toml rename to services/galexie/internal/test/64perfile.toml diff --git a/exp/services/ledgerexporter/internal/test/captive-core-test.cfg b/services/galexie/internal/test/captive-core-test.cfg similarity index 100% rename from exp/services/ledgerexporter/internal/test/captive-core-test.cfg rename to services/galexie/internal/test/captive-core-test.cfg diff --git a/exp/services/ledgerexporter/internal/test/integration_captive_core.cfg b/services/galexie/internal/test/integration_captive_core.cfg similarity index 100% rename from exp/services/ledgerexporter/internal/test/integration_captive_core.cfg rename to services/galexie/internal/test/integration_captive_core.cfg diff --git a/exp/services/ledgerexporter/internal/test/integration_config_template.toml b/services/galexie/internal/test/integration_config_template.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/integration_config_template.toml rename to services/galexie/internal/test/integration_config_template.toml diff --git a/exp/services/ledgerexporter/internal/test/invalid_captive_core_toml_path.toml b/services/galexie/internal/test/invalid_captive_core_toml_path.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/invalid_captive_core_toml_path.toml rename to services/galexie/internal/test/invalid_captive_core_toml_path.toml diff --git a/exp/services/ledgerexporter/internal/test/invalid_empty.toml b/services/galexie/internal/test/invalid_empty.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/invalid_empty.toml rename to services/galexie/internal/test/invalid_empty.toml diff --git a/exp/services/ledgerexporter/internal/test/invalid_preconfigured_network.toml b/services/galexie/internal/test/invalid_preconfigured_network.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/invalid_preconfigured_network.toml rename to services/galexie/internal/test/invalid_preconfigured_network.toml diff --git a/exp/services/ledgerexporter/internal/test/no_core_bin.toml b/services/galexie/internal/test/no_core_bin.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/no_core_bin.toml rename to services/galexie/internal/test/no_core_bin.toml diff --git a/services/galexie/internal/test/no_network.toml b/services/galexie/internal/test/no_network.toml new file mode 100644 index 0000000000..1cb591cdd4 --- /dev/null +++ b/services/galexie/internal/test/no_network.toml @@ -0,0 +1,11 @@ + +[datastore_config] +type = "ABC" + +[datastore_config.params] +destination_bucket_path = "your-bucket-name/subpath" + +[exporter_config] +ledgers_per_file = 3 +files_per_partition = 1 +file_suffix = ".xdr.gz" \ No newline at end of file diff --git a/exp/services/ledgerexporter/internal/test/test.toml b/services/galexie/internal/test/test.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/test.toml rename to services/galexie/internal/test/test.toml diff --git a/exp/services/ledgerexporter/internal/test/useragent.toml b/services/galexie/internal/test/useragent.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/useragent.toml rename to services/galexie/internal/test/useragent.toml diff --git a/exp/services/ledgerexporter/internal/test/valid_captive_core_manual.toml b/services/galexie/internal/test/valid_captive_core_manual.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/valid_captive_core_manual.toml rename to services/galexie/internal/test/valid_captive_core_manual.toml diff --git a/exp/services/ledgerexporter/internal/test/valid_captive_core_override.toml b/services/galexie/internal/test/valid_captive_core_override.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/valid_captive_core_override.toml rename to services/galexie/internal/test/valid_captive_core_override.toml diff --git a/exp/services/ledgerexporter/internal/test/valid_captive_core_override_archives.toml b/services/galexie/internal/test/valid_captive_core_override_archives.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/valid_captive_core_override_archives.toml rename to services/galexie/internal/test/valid_captive_core_override_archives.toml diff --git a/exp/services/ledgerexporter/internal/test/valid_captive_core_preconfigured.toml b/services/galexie/internal/test/valid_captive_core_preconfigured.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/valid_captive_core_preconfigured.toml rename to services/galexie/internal/test/valid_captive_core_preconfigured.toml diff --git a/exp/services/ledgerexporter/internal/test/validate_start_end.toml b/services/galexie/internal/test/validate_start_end.toml similarity index 100% rename from exp/services/ledgerexporter/internal/test/validate_start_end.toml rename to services/galexie/internal/test/validate_start_end.toml diff --git a/exp/services/ledgerexporter/internal/uploader.go b/services/galexie/internal/uploader.go similarity index 94% rename from exp/services/ledgerexporter/internal/uploader.go rename to services/galexie/internal/uploader.go index 6d35d2920f..52712da280 100644 --- a/exp/services/ledgerexporter/internal/uploader.go +++ b/services/galexie/internal/uploader.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "context" @@ -30,7 +30,7 @@ func NewUploader( ) Uploader { uploadDurationMetric := prometheus.NewSummaryVec( prometheus.SummaryOpts{ - Namespace: "ledger_exporter", Subsystem: "uploader", Name: "put_duration_seconds", + Namespace: nameSpace, Subsystem: "uploader", Name: "put_duration_seconds", Help: "duration for uploading a ledger batch, sliding window = 10m", Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, }, @@ -38,14 +38,14 @@ func NewUploader( ) objectSizeMetrics := prometheus.NewSummaryVec( prometheus.SummaryOpts{ - Namespace: "ledger_exporter", Subsystem: "uploader", Name: "object_size_bytes", + Namespace: nameSpace, Subsystem: "uploader", Name: "object_size_bytes", Help: "size of a ledger batch in bytes, sliding window = 10m", Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, }, []string{"ledgers", "already_exists", "compression"}, ) latestLedgerMetric := prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: "ledger_exporter", Subsystem: "uploader", Name: "latest_ledger", + Namespace: nameSpace, Subsystem: "uploader", Name: "latest_ledger", Help: "sequence number of the latest ledger uploaded", }) prometheusRegistry.MustRegister(uploadDurationMetric, objectSizeMetrics, latestLedgerMetric) diff --git a/exp/services/ledgerexporter/internal/uploader_test.go b/services/galexie/internal/uploader_test.go similarity index 99% rename from exp/services/ledgerexporter/internal/uploader_test.go rename to services/galexie/internal/uploader_test.go index 612c9e8740..81eb252755 100644 --- a/exp/services/ledgerexporter/internal/uploader_test.go +++ b/services/galexie/internal/uploader_test.go @@ -1,4 +1,4 @@ -package ledgerexporter +package galexie import ( "bytes" diff --git a/exp/services/ledgerexporter/main.go b/services/galexie/main.go similarity index 55% rename from exp/services/ledgerexporter/main.go rename to services/galexie/main.go index 6d38c69df9..adfc1bb89d 100644 --- a/exp/services/ledgerexporter/main.go +++ b/services/galexie/main.go @@ -4,11 +4,11 @@ import ( "fmt" "os" - exporter "github.com/stellar/go/exp/services/ledgerexporter/internal" + galexie "github.com/stellar/go/services/galexie/internal" ) func main() { - err := exporter.Execute() + err := galexie.Execute() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) From 2674e204f27ef9057c8b38a3ca0446c6beca6d27 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Mon, 22 Jul 2024 14:20:06 -0700 Subject: [PATCH 212/234] Fix CI test workflow errors in RecoverySigner unit tests (#5398) * Fix race condition in unit tests * addressing review comment --- support/db/dbtest/db.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/support/db/dbtest/db.go b/support/db/dbtest/db.go index 2caf1fee6a..18de073224 100644 --- a/support/db/dbtest/db.go +++ b/support/db/dbtest/db.go @@ -118,7 +118,7 @@ func checkReadOnly(t testing.TB, DSN string) { require.NoError(t, err) defer conn.Close() - tx, err := conn.BeginTx(context.Background(), &sql.TxOptions{Isolation: sql.LevelSerializable}) + tx, err := conn.BeginTx(context.Background(), &sql.TxOptions{}) require.NoError(t, err) defer tx.Rollback() @@ -127,6 +127,12 @@ func checkReadOnly(t testing.TB, DSN string) { if !rows.Next() { _, err = tx.Exec("CREATE ROLE user_ro WITH LOGIN PASSWORD 'user_ro';") + if err != nil { + // Handle race condition by ignoring the error if it's a duplicate key violation + if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "23505" { + return + } + } require.NoError(t, err) } From 31fc8f4236388f12fc609228b7a7f5494867a1f9 Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Thu, 25 Jul 2024 11:49:50 +1000 Subject: [PATCH 213/234] services/friendbot: support funding existing accounts (#5399) --- services/friendbot/init_friendbot.go | 1 + services/friendbot/internal/friendbot_test.go | 122 +++++++++++++++++- services/friendbot/internal/minion.go | 117 ++++++++++++++++- services/friendbot/internal/minion_test.go | 10 ++ services/friendbot/main.go | 5 +- 5 files changed, 248 insertions(+), 7 deletions(-) diff --git a/services/friendbot/init_friendbot.go b/services/friendbot/init_friendbot.go index ef7a21253c..bbfd1ded16 100644 --- a/services/friendbot/init_friendbot.go +++ b/services/friendbot/init_friendbot.go @@ -104,6 +104,7 @@ func createMinionAccounts(botAccount internal.Account, botKeypair *keypair.Full, StartingBalance: newAccountBalance, SubmitTransaction: internal.SubmitTransaction, CheckSequenceRefresh: internal.CheckSequenceRefresh, + CheckAccountExists: internal.CheckAccountExists, BaseFee: baseFee, }) diff --git a/services/friendbot/internal/friendbot_test.go b/services/friendbot/internal/friendbot_test.go index a9f2864c83..6313dd52dc 100644 --- a/services/friendbot/internal/friendbot_test.go +++ b/services/friendbot/internal/friendbot_test.go @@ -12,13 +12,17 @@ import ( "github.com/stretchr/testify/assert" ) -func TestFriendbot_Pay(t *testing.T) { +func TestFriendbot_Pay_accountDoesNotExist(t *testing.T) { mockSubmitTransaction := func(minion *Minion, hclient horizonclient.ClientInterface, tx string) (*hProtocol.Transaction, error) { // Instead of submitting the tx, we emulate a success. txSuccess := hProtocol.Transaction{EnvelopeXdr: tx, Successful: true} return &txSuccess, nil } + mockCheckAccountExists := func(minion *Minion, hclient horizonclient.ClientInterface, destAddress string) (bool, string, error) { + return false, "0", nil + } + // Public key: GD25B4QI6KWVDWXDW25CIM7EKR6A6PBSWE2RCNSAC4NJQDQJXZJYMMKR botSeed := "SCWNLYELENPBXN46FHYXETT5LJCYBZD5VUQQVW4KZPHFO2YTQJUWT4D5" botKeypair, err := keypair.Parse(botSeed) @@ -46,6 +50,7 @@ func TestFriendbot_Pay(t *testing.T) { StartingBalance: "10000.00", SubmitTransaction: mockSubmitTransaction, CheckSequenceRefresh: CheckSequenceRefresh, + CheckAccountExists: mockCheckAccountExists, BaseFee: txnbuild.MinBaseFee, } fb := &Bot{Minions: []Minion{minion}} @@ -73,3 +78,118 @@ func TestFriendbot_Pay(t *testing.T) { }() wg.Wait() } + +func TestFriendbot_Pay_accountExists(t *testing.T) { + mockSubmitTransaction := func(minion *Minion, hclient horizonclient.ClientInterface, tx string) (*hProtocol.Transaction, error) { + // Instead of submitting the tx, we emulate a success. + txSuccess := hProtocol.Transaction{EnvelopeXdr: tx, Successful: true} + return &txSuccess, nil + } + + mockCheckAccountExists := func(minion *Minion, hclient horizonclient.ClientInterface, destAddress string) (bool, string, error) { + return true, "0", nil + } + + // Public key: GD25B4QI6KWVDWXDW25CIM7EKR6A6PBSWE2RCNSAC4NJQDQJXZJYMMKR + botSeed := "SCWNLYELENPBXN46FHYXETT5LJCYBZD5VUQQVW4KZPHFO2YTQJUWT4D5" + botKeypair, err := keypair.Parse(botSeed) + if !assert.NoError(t, err) { + return + } + botAccount := Account{AccountID: botKeypair.Address()} + + // Public key: GD4AGPPDFFHKK3Z2X4XZDRXX6GZQKP4FMLVQ5T55NDEYGG3GIP7BQUHM + minionSeed := "SDTNSEERJPJFUE2LSDNYBFHYGVTPIWY7TU2IOJZQQGLWO2THTGB7NU5A" + minionKeypair, err := keypair.Parse(minionSeed) + if !assert.NoError(t, err) { + return + } + + minion := Minion{ + Account: Account{ + AccountID: minionKeypair.Address(), + Sequence: 1, + }, + Keypair: minionKeypair.(*keypair.Full), + BotAccount: botAccount, + BotKeypair: botKeypair.(*keypair.Full), + Network: "Test SDF Network ; September 2015", + StartingBalance: "10000.00", + SubmitTransaction: mockSubmitTransaction, + CheckSequenceRefresh: CheckSequenceRefresh, + CheckAccountExists: mockCheckAccountExists, + BaseFee: txnbuild.MinBaseFee, + } + fb := &Bot{Minions: []Minion{minion}} + + recipientAddress := "GDJIN6W6PLTPKLLM57UW65ZH4BITUXUMYQHIMAZFYXF45PZVAWDBI77Z" + txSuccess, err := fb.Pay(recipientAddress) + if !assert.NoError(t, err) { + return + } + expectedTxn := "AAAAAgAAAAD4Az3jKU6lbzq/L5HG9/GzBT+FYusOz71oyYMbZkP+GAAAAGQAAAAAAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAPXQ8gjyrVHa47a6JDPkVHwPPDKxNRE2QBcamA4JvlOGAAAAAQAAAADShvreeub1LWzv6W93J+BROl6MxA6GAyXFy86/NQWGFAAAAAAAAAAXSHboAAAAAAAAAAACZkP+GAAAAEBAwm/hWuu/ZHHQWRD9oF/cnSwQyTZpHQoTlPlVSFH4g12HR2nbzOI9wC5Z5bt0ueXny4UNFS5QhUvnzdb2FMsDCb5ThgAAAED1HzWPW6lKBxBi6MTSwM/POytPSfL87taiarpTIk5naoqXPLpM0YBBaf5uH8de5Id1KSCP/g8tdeCxvrT053kJ" + assert.Equal(t, expectedTxn, txSuccess.EnvelopeXdr) + + // Don't assert on tx values below, since the completion order is unknown. + var wg sync.WaitGroup + wg.Add(2) + go func() { + _, err := fb.Pay(recipientAddress) + assert.NoError(t, err) + wg.Done() + }() + go func() { + _, err := fb.Pay(recipientAddress) + assert.NoError(t, err) + wg.Done() + }() + wg.Wait() +} + +func TestFriendbot_Pay_accountExistsAlreadyFunded(t *testing.T) { + mockSubmitTransaction := func(minion *Minion, hclient horizonclient.ClientInterface, tx string) (*hProtocol.Transaction, error) { + // Instead of submitting the tx, we emulate a success. + txSuccess := hProtocol.Transaction{EnvelopeXdr: tx, Successful: true} + return &txSuccess, nil + } + + mockCheckAccountExists := func(minion *Minion, hclient horizonclient.ClientInterface, destAddress string) (bool, string, error) { + return true, "10000.00", nil + } + + // Public key: GD25B4QI6KWVDWXDW25CIM7EKR6A6PBSWE2RCNSAC4NJQDQJXZJYMMKR + botSeed := "SCWNLYELENPBXN46FHYXETT5LJCYBZD5VUQQVW4KZPHFO2YTQJUWT4D5" + botKeypair, err := keypair.Parse(botSeed) + if !assert.NoError(t, err) { + return + } + botAccount := Account{AccountID: botKeypair.Address()} + + // Public key: GD4AGPPDFFHKK3Z2X4XZDRXX6GZQKP4FMLVQ5T55NDEYGG3GIP7BQUHM + minionSeed := "SDTNSEERJPJFUE2LSDNYBFHYGVTPIWY7TU2IOJZQQGLWO2THTGB7NU5A" + minionKeypair, err := keypair.Parse(minionSeed) + if !assert.NoError(t, err) { + return + } + + minion := Minion{ + Account: Account{ + AccountID: minionKeypair.Address(), + Sequence: 1, + }, + Keypair: minionKeypair.(*keypair.Full), + BotAccount: botAccount, + BotKeypair: botKeypair.(*keypair.Full), + Network: "Test SDF Network ; September 2015", + StartingBalance: "10000.00", + SubmitTransaction: mockSubmitTransaction, + CheckSequenceRefresh: CheckSequenceRefresh, + CheckAccountExists: mockCheckAccountExists, + BaseFee: txnbuild.MinBaseFee, + } + fb := &Bot{Minions: []Minion{minion}} + + recipientAddress := "GDJIN6W6PLTPKLLM57UW65ZH4BITUXUMYQHIMAZFYXF45PZVAWDBI77Z" + _, err = fb.Pay(recipientAddress) + assert.ErrorIs(t, err, ErrAccountFunded) +} diff --git a/services/friendbot/internal/minion.go b/services/friendbot/internal/minion.go index b8359b2c23..697806a1f9 100644 --- a/services/friendbot/internal/minion.go +++ b/services/friendbot/internal/minion.go @@ -3,6 +3,7 @@ package internal import ( "fmt" + "github.com/stellar/go/amount" "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/keypair" hProtocol "github.com/stellar/go/protocols/horizon" @@ -14,6 +15,8 @@ const createAccountAlreadyExistXDR = "AAAAAAAAAGT/////AAAAAQAAAAAAAAAA/////AAAAA var ErrAccountExists error = errors.New(fmt.Sprintf("createAccountAlreadyExist (%s)", createAccountAlreadyExistXDR)) +var ErrAccountFunded error = errors.New("account already funded to starting balance") + // Minion contains a Stellar channel account and Go channels to communicate with friendbot. type Minion struct { Account Account @@ -28,6 +31,7 @@ type Minion struct { // Mockable functions SubmitTransaction func(minion *Minion, hclient horizonclient.ClientInterface, tx string) (*hProtocol.Transaction, error) CheckSequenceRefresh func(minion *Minion, hclient horizonclient.ClientInterface) error + CheckAccountExists func(minion *Minion, hclient horizonclient.ClientInterface, destAddress string) (bool, string, error) // Uninitialized. forceRefreshSequence bool @@ -44,7 +48,23 @@ func (minion *Minion) Run(destAddress string, resultChan chan SubmitResult) { } return } - txHash, txStr, err := minion.makeTx(destAddress) + exists, balance, err := minion.CheckAccountExists(minion, minion.Horizon, destAddress) + if err != nil { + resultChan <- SubmitResult{ + maybeTransactionSuccess: nil, + maybeErr: errors.Wrap(err, "checking account exists"), + } + return + } + err = minion.checkBalance(balance) + if err != nil { + resultChan <- SubmitResult{ + maybeTransactionSuccess: nil, + maybeErr: errors.Wrap(err, "account already funded"), + } + return + } + txHash, txStr, err := minion.makeTx(destAddress, exists) if err != nil { resultChan <- SubmitResult{ maybeTransactionSuccess: nil, @@ -52,6 +72,14 @@ func (minion *Minion) Run(destAddress string, resultChan chan SubmitResult) { } return } + _, err = minion.Account.IncrementSequenceNumber() + if err != nil { + resultChan <- SubmitResult{ + maybeTransactionSuccess: nil, + maybeErr: errors.Wrap(err, "incrementing submitters sequence number"), + } + return + } succ, err := minion.SubmitTransaction(minion, minion.Horizon, txStr) resultChan <- SubmitResult{ maybeTransactionSuccess: succ, @@ -96,6 +124,30 @@ func CheckSequenceRefresh(minion *Minion, hclient horizonclient.ClientInterface) return nil } +// CheckAccountExists checks if the specified address exists as a Stellar account. +// And returns the current native balance of the account also. +// This should also be passed to the minion. +func CheckAccountExists(minion *Minion, hclient horizonclient.ClientInterface, address string) (bool, string, error) { + accountRequest := horizonclient.AccountRequest{AccountID: address} + accountDetails, err := hclient.AccountDetail(accountRequest) + switch e := err.(type) { + case nil: + balance := "0" + for _, b := range accountDetails.Balances { + if b.Type == "native" { + balance = b.Balance + break + } + } + return true, balance, nil + case *horizonclient.Error: + if e.Response.StatusCode == 404 { + return false, "0", nil + } + } + return false, "0", err +} + func (minion *Minion) checkHandleBadSequence(err *horizonclient.Error) { resCode, e := err.ResultCodes() isTxBadSeqCode := e == nil && resCode.TransactionCode == "tx_bad_seq" @@ -105,7 +157,30 @@ func (minion *Minion) checkHandleBadSequence(err *horizonclient.Error) { minion.forceRefreshSequence = true } -func (minion *Minion) makeTx(destAddress string) ([32]byte, string, error) { +func (minion *Minion) checkBalance(balance string) error { + bal, err := amount.ParseInt64(balance) + if err != nil { + return errors.Wrap(err, "cannot parse account balance") + } + starting, err := amount.ParseInt64(minion.StartingBalance) + if err != nil { + return errors.Wrap(err, "cannot parse starting balance") + } + if bal >= starting { + return ErrAccountFunded + } + return nil +} + +func (minion *Minion) makeTx(destAddress string, exists bool) ([32]byte, string, error) { + if exists { + return minion.makePaymentTx(destAddress) + } else { + return minion.makeCreateTx(destAddress) + } +} + +func (minion *Minion) makeCreateTx(destAddress string) ([32]byte, string, error) { createAccountOp := txnbuild.CreateAccount{ Destination: destAddress, SourceAccount: minion.BotAccount.GetAccountID(), @@ -138,11 +213,43 @@ func (minion *Minion) makeTx(destAddress string) ([32]byte, string, error) { if err != nil { return [32]byte{}, "", errors.Wrap(err, "unable to hash") } + return txh, txe, err +} - // Increment the in-memory sequence number, since the tx will be submitted. - _, err = minion.Account.IncrementSequenceNumber() +func (minion *Minion) makePaymentTx(destAddress string) ([32]byte, string, error) { + paymentOp := txnbuild.Payment{ + SourceAccount: minion.BotAccount.GetAccountID(), + Destination: destAddress, + Asset: txnbuild.NativeAsset{}, + Amount: minion.StartingBalance, + } + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: minion.Account, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{&paymentOp}, + BaseFee: minion.BaseFee, + Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewInfiniteTimeout()}, + }, + ) if err != nil { - return [32]byte{}, "", errors.Wrap(err, "incrementing minion seq") + return [32]byte{}, "", errors.Wrap(err, "unable to build tx") } + + tx, err = tx.Sign(minion.Network, minion.Keypair, minion.BotKeypair) + if err != nil { + return [32]byte{}, "", errors.Wrap(err, "unable to sign tx") + } + + txe, err := tx.Base64() + if err != nil { + return [32]byte{}, "", errors.Wrap(err, "unable to serialize") + } + + txh, err := tx.Hash(minion.Network) + if err != nil { + return [32]byte{}, "", errors.Wrap(err, "unable to hash") + } + return txh, txe, err } diff --git a/services/friendbot/internal/minion_test.go b/services/friendbot/internal/minion_test.go index 6099d4b3d6..272bb64afb 100644 --- a/services/friendbot/internal/minion_test.go +++ b/services/friendbot/internal/minion_test.go @@ -24,6 +24,10 @@ func TestMinion_NoChannelErrors(t *testing.T) { return errors.New("could not refresh sequence") } + mockCheckAccountExists := func(minion *Minion, hclient horizonclient.ClientInterface, destAddress string) (bool, string, error) { + return false, "0", nil + } + // Public key: GD25B4QI6KWVDWXDW25CIM7EKR6A6PBSWE2RCNSAC4NJQDQJXZJYMMKR botSeed := "SCWNLYELENPBXN46FHYXETT5LJCYBZD5VUQQVW4KZPHFO2YTQJUWT4D5" botKeypair, err := keypair.Parse(botSeed) @@ -51,6 +55,7 @@ func TestMinion_NoChannelErrors(t *testing.T) { StartingBalance: "10000.00", SubmitTransaction: mockSubmitTransaction, CheckSequenceRefresh: mockCheckSequenceRefresh, + CheckAccountExists: mockCheckAccountExists, BaseFee: txnbuild.MinBaseFee, } fb := &Bot{Minions: []Minion{minion}} @@ -89,6 +94,10 @@ func TestMinion_CorrectNumberOfTxSubmissions(t *testing.T) { return nil } + mockCheckAccountExists := func(minion *Minion, hclient horizonclient.ClientInterface, destAddress string) (bool, string, error) { + return false, "0", nil + } + // Public key: GD25B4QI6KWVDWXDW25CIM7EKR6A6PBSWE2RCNSAC4NJQDQJXZJYMMKR botSeed := "SCWNLYELENPBXN46FHYXETT5LJCYBZD5VUQQVW4KZPHFO2YTQJUWT4D5" botKeypair, err := keypair.Parse(botSeed) @@ -116,6 +125,7 @@ func TestMinion_CorrectNumberOfTxSubmissions(t *testing.T) { StartingBalance: "10000.00", SubmitTransaction: mockSubmitTransaction, CheckSequenceRefresh: mockCheckSequenceRefresh, + CheckAccountExists: mockCheckAccountExists, BaseFee: txnbuild.MinBaseFee, } fb := &Bot{Minions: []Minion{minion}} diff --git a/services/friendbot/main.go b/services/friendbot/main.go index 22e7d4c44d..c5933c2fc3 100644 --- a/services/friendbot/main.go +++ b/services/friendbot/main.go @@ -34,7 +34,6 @@ type Config struct { } func main() { - rootCmd := &cobra.Command{ Use: "friendbot", Short: "friendbot for the Stellar Test Network", @@ -114,4 +113,8 @@ func registerProblems() { accountExistsProblem := problem.BadRequest accountExistsProblem.Detail = internal.ErrAccountExists.Error() problem.RegisterError(internal.ErrAccountExists, accountExistsProblem) + + accountFundedProblem := problem.BadRequest + accountFundedProblem.Detail = internal.ErrAccountFunded.Error() + problem.RegisterError(internal.ErrAccountFunded, accountFundedProblem) } From ecd28b61d224bae9b43e2d4ac92c968b9707c53e Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 29 Jul 2024 10:04:59 +0100 Subject: [PATCH 214/234] services/horizon/internal/db2/history: Optimize query for reaping lookup tables (#5393) --- .../horizon/internal/db2/history/key_value.go | 45 ++++ services/horizon/internal/db2/history/main.go | 225 +++++++++--------- .../horizon/internal/db2/history/main_test.go | 46 +--- .../horizon/internal/db2/history/reap_test.go | 120 +++++++--- services/horizon/internal/ingest/main.go | 12 +- services/horizon/internal/ingest/main_test.go | 4 +- 6 files changed, 259 insertions(+), 193 deletions(-) diff --git a/services/horizon/internal/db2/history/key_value.go b/services/horizon/internal/db2/history/key_value.go index a2a170a4b1..3d23451937 100644 --- a/services/horizon/internal/db2/history/key_value.go +++ b/services/horizon/internal/db2/history/key_value.go @@ -3,9 +3,12 @@ package history import ( "context" "database/sql" + "fmt" "strconv" + "strings" sq "github.com/Masterminds/squirrel" + "github.com/stellar/go/support/errors" ) @@ -18,6 +21,7 @@ const ( stateInvalid = "exp_state_invalid" offerCompactionSequence = "offer_compaction_sequence" liquidityPoolCompactionSequence = "liquidity_pool_compaction_sequence" + lookupTableReapOffsetSuffix = "_reap_offset" ) // GetLastLedgerIngestNonBlocking works like GetLastLedgerIngest but @@ -203,6 +207,47 @@ func (q *Q) getValueFromStore(ctx context.Context, key string, forUpdate bool) ( return value, nil } +type KeyValuePair struct { + Key string `db:"key"` + Value string `db:"value"` +} + +func (q *Q) getLookupTableReapOffsets(ctx context.Context) (map[string]int64, error) { + keys := make([]string, 0, len(historyLookupTables)) + for table := range historyLookupTables { + keys = append(keys, table+lookupTableReapOffsetSuffix) + } + offsets := map[string]int64{} + var pairs []KeyValuePair + query := sq.Select("key", "value"). + From("key_value_store"). + Where(map[string]interface{}{ + "key": keys, + }) + err := q.Select(ctx, &pairs, query) + if err != nil { + return nil, err + } + for _, pair := range pairs { + table := strings.TrimSuffix(pair.Key, lookupTableReapOffsetSuffix) + if _, ok := historyLookupTables[table]; !ok { + return nil, fmt.Errorf("invalid key: %s", pair.Key) + } + + var offset int64 + offset, err = strconv.ParseInt(pair.Value, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid offset: %s", pair.Value) + } + offsets[table] = offset + } + return offsets, err +} + +func (q *Q) updateLookupTableReapOffset(ctx context.Context, table string, offset int64) error { + return q.updateValueInStore(ctx, table+lookupTableReapOffsetSuffix, strconv.FormatInt(offset, 10)) +} + // updateValueInStore updates a value for a given key in KV store func (q *Q) updateValueInStore(ctx context.Context, key, value string) error { query := sq.Insert("key_value_store"). diff --git a/services/horizon/internal/db2/history/main.go b/services/horizon/internal/db2/history/main.go index 47e8952b07..e9d8ffb185 100644 --- a/services/horizon/internal/db2/history/main.go +++ b/services/horizon/internal/db2/history/main.go @@ -282,7 +282,7 @@ type IngestionQ interface { NewTradeBatchInsertBuilder() TradeBatchInsertBuilder RebuildTradeAggregationTimes(ctx context.Context, from, to strtime.Millis, roundingSlippageFilter int) error RebuildTradeAggregationBuckets(ctx context.Context, fromLedger, toLedger uint32, roundingSlippageFilter int) error - ReapLookupTables(ctx context.Context, offsets map[string]int64) (map[string]LookupTableReapResult, error) + ReapLookupTables(ctx context.Context, batchSize int) (map[string]LookupTableReapResult, error) CreateAssets(ctx context.Context, assets []xdr.Asset, batchSize int) (map[string]Asset, error) QTransactions QTrustLines @@ -981,7 +981,7 @@ type LookupTableReapResult struct { // which aren't used (orphaned), i.e. history entries for them were reaped. // This method must be executed inside ingestion transaction. Otherwise it may // create invalid state in lookup and history tables. -func (q Q) ReapLookupTables(ctx context.Context, offsets map[string]int64) ( +func (q *Q) ReapLookupTables(ctx context.Context, batchSize int) ( map[string]LookupTableReapResult, error, ) { @@ -989,80 +989,19 @@ func (q Q) ReapLookupTables(ctx context.Context, offsets map[string]int64) ( return nil, errors.New("cannot be called outside of an ingestion transaction") } - const batchSize = 1000 + offsets, err := q.getLookupTableReapOffsets(ctx) + if err != nil { + return nil, fmt.Errorf("could not obtain offsets: %w", err) + } results := map[string]LookupTableReapResult{} - for table, historyTables := range map[string][]tableObjectFieldPair{ - "history_accounts": { - { - name: "history_effects", - objectField: "history_account_id", - }, - { - name: "history_operation_participants", - objectField: "history_account_id", - }, - { - name: "history_trades", - objectField: "base_account_id", - }, - { - name: "history_trades", - objectField: "counter_account_id", - }, - { - name: "history_transaction_participants", - objectField: "history_account_id", - }, - }, - "history_assets": { - { - name: "history_trades", - objectField: "base_asset_id", - }, - { - name: "history_trades", - objectField: "counter_asset_id", - }, - { - name: "history_trades_60000", - objectField: "base_asset_id", - }, - { - name: "history_trades_60000", - objectField: "counter_asset_id", - }, - }, - "history_claimable_balances": { - { - name: "history_operation_claimable_balances", - objectField: "history_claimable_balance_id", - }, - { - name: "history_transaction_claimable_balances", - objectField: "history_claimable_balance_id", - }, - }, - "history_liquidity_pools": { - { - name: "history_operation_liquidity_pools", - objectField: "history_liquidity_pool_id", - }, - { - name: "history_transaction_liquidity_pools", - objectField: "history_liquidity_pool_id", - }, - }, - } { + for table, historyTables := range historyLookupTables { startTime := time.Now() - query, err := constructReapLookupTablesQuery(table, historyTables, batchSize, offsets[table]) - if err != nil { - return nil, errors.Wrap(err, "error constructing a query") - } + query := constructReapLookupTablesQuery(table, historyTables, batchSize, offsets[table]) // Find new offset before removing the rows var newOffset int64 - err = q.GetRaw(ctx, &newOffset, fmt.Sprintf("SELECT id FROM %s where id >= %d limit 1 offset %d", table, offsets[table], batchSize)) + err := q.GetRaw(ctx, &newOffset, fmt.Sprintf("SELECT id FROM %s where id >= %d limit 1 offset %d", table, offsets[table], batchSize)) if err != nil { if q.NoRows(err) { newOffset = 0 @@ -1079,6 +1018,10 @@ func (q Q) ReapLookupTables(ctx context.Context, offsets map[string]int64) ( return nil, errors.Wrapf(err, "error running query: %s", query) } + if err = q.updateLookupTableReapOffset(ctx, table, newOffset); err != nil { + return nil, fmt.Errorf("error updating offset: %w", err) + } + rows, err := res.RowsAffected() if err != nil { return nil, errors.Wrapf(err, "error running RowsAffected after query: %s", query) @@ -1093,22 +1036,86 @@ func (q Q) ReapLookupTables(ctx context.Context, offsets map[string]int64) ( return results, nil } +var historyLookupTables = map[string][]tableObjectFieldPair{ + "history_accounts": { + { + name: "history_transaction_participants", + objectField: "history_account_id", + }, + + { + name: "history_effects", + objectField: "history_account_id", + }, + { + name: "history_operation_participants", + objectField: "history_account_id", + }, + { + name: "history_trades", + objectField: "base_account_id", + }, + { + name: "history_trades", + objectField: "counter_account_id", + }, + }, + "history_assets": { + { + name: "history_trades", + objectField: "base_asset_id", + }, + { + name: "history_trades", + objectField: "counter_asset_id", + }, + { + name: "history_trades_60000", + objectField: "base_asset_id", + }, + { + name: "history_trades_60000", + objectField: "counter_asset_id", + }, + }, + "history_claimable_balances": { + { + name: "history_transaction_claimable_balances", + objectField: "history_claimable_balance_id", + }, + { + name: "history_operation_claimable_balances", + objectField: "history_claimable_balance_id", + }, + }, + "history_liquidity_pools": { + { + name: "history_transaction_liquidity_pools", + objectField: "history_liquidity_pool_id", + }, + { + name: "history_operation_liquidity_pools", + objectField: "history_liquidity_pool_id", + }, + }, +} + // constructReapLookupTablesQuery creates a query like (using history_claimable_balances // as an example): // -// delete from history_claimable_balances where id in +// delete from history_claimable_balances where id in ( // -// (select id from -// (select id, -// (select 1 from history_operation_claimable_balances -// where history_claimable_balance_id = hcb.id limit 1) as c1, -// (select 1 from history_transaction_claimable_balances -// where history_claimable_balance_id = hcb.id limit 1) as c2, -// 1 as cx, -// from history_claimable_balances hcb where id > 1000 order by id limit 100) -// as sub where c1 IS NULL and c2 IS NULL and 1=1); +// WITH ha_batch AS ( +// SELECT id +// FROM history_claimable_balances +// WHERE id >= 1000 +// ORDER BY id limit 1000 +// ) SELECT e1.id as id FROM ha_batch e1 +// WHERE NOT EXISTS (SELECT 1 FROM history_transaction_claimable_balances WHERE history_transaction_claimable_balances.history_claimable_balance_id = id limit 1) +// AND NOT EXISTS (SELECT 1 FROM history_operation_claimable_balances WHERE history_operation_claimable_balances.history_claimable_balance_id = id limit 1) +// ) // -// In short it checks the 100 rows omitting 1000 row of history_claimable_balances +// In short it checks the 1000 rows omitting 1000 row of history_claimable_balances // and counts occurrences of each row in corresponding history tables. // If there are no history rows for a given id, the row in // history_claimable_balances is removed. @@ -1118,45 +1125,29 @@ func (q Q) ReapLookupTables(ctx context.Context, offsets map[string]int64) ( // possible that rows will be skipped from deletion. But offset is reset // when it reaches the table size so eventually all orphaned rows are // deleted. -func constructReapLookupTablesQuery(table string, historyTables []tableObjectFieldPair, batchSize, offset int64) (string, error) { - var sb strings.Builder - var err error - _, err = fmt.Fprintf(&sb, "delete from %s where id IN (select id from (select id, ", table) - if err != nil { - return "", err - } - - for i, historyTable := range historyTables { - _, err = fmt.Fprintf( - &sb, - `(select 1 from %s where %s = hcb.id limit 1) as c%d, `, - historyTable.name, - historyTable.objectField, - i, +func constructReapLookupTablesQuery(table string, historyTables []tableObjectFieldPair, batchSize int, offset int64) string { + var conditions []string + + for _, historyTable := range historyTables { + conditions = append( + conditions, + fmt.Sprintf( + "NOT EXISTS ( SELECT 1 as row FROM %s WHERE %s.%s = id LIMIT 1)", + historyTable.name, + historyTable.name, historyTable.objectField, + ), ) - if err != nil { - return "", err - } } - _, err = fmt.Fprintf(&sb, "1 as cx from %s hcb where id >= %d order by id limit %d) as sub where ", table, offset, batchSize) - if err != nil { - return "", err - } - - for i := range historyTables { - _, err = fmt.Fprintf(&sb, "c%d IS NULL and ", i) - if err != nil { - return "", err - } - } - - _, err = sb.WriteString("1=1);") - if err != nil { - return "", err - } - - return sb.String(), nil + return fmt.Sprintf( + "DELETE FROM %s WHERE id IN ("+ + "WITH ha_batch AS (SELECT id FROM %s WHERE id >= %d ORDER BY id limit %d) "+ + "SELECT e1.id as id FROM ha_batch e1 WHERE ", + table, + table, + offset, + batchSize, + ) + strings.Join(conditions, " AND ") + ")" } // DeleteRangeAll deletes a range of rows from all history tables between diff --git a/services/horizon/internal/db2/history/main_test.go b/services/horizon/internal/db2/history/main_test.go index 792f9826aa..1a28b9e584 100644 --- a/services/horizon/internal/db2/history/main_test.go +++ b/services/horizon/internal/db2/history/main_test.go @@ -4,9 +4,9 @@ import ( "testing" "time" - "github.com/stellar/go/services/horizon/internal/test" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + + "github.com/stellar/go/services/horizon/internal/test" ) func TestLatestLedger(t *testing.T) { @@ -70,43 +70,19 @@ func TestElderLedger(t *testing.T) { } func TestConstructReapLookupTablesQuery(t *testing.T) { - query, err := constructReapLookupTablesQuery( + query := constructReapLookupTablesQuery( "history_accounts", - []tableObjectFieldPair{ - { - name: "history_effects", - objectField: "history_account_id", - }, - { - name: "history_operation_participants", - objectField: "history_account_id", - }, - { - name: "history_trades", - objectField: "base_account_id", - }, - { - name: "history_trades", - objectField: "counter_account_id", - }, - { - name: "history_transaction_participants", - objectField: "history_account_id", - }, - }, + historyLookupTables["history_accounts"], 10, 0, ) - require.NoError(t, err) assert.Equal(t, - "delete from history_accounts where id IN "+ - "(select id from "+ - "(select id, (select 1 from history_effects where history_account_id = hcb.id limit 1) as c0, "+ - "(select 1 from history_operation_participants where history_account_id = hcb.id limit 1) as c1, "+ - "(select 1 from history_trades where base_account_id = hcb.id limit 1) as c2, "+ - "(select 1 from history_trades where counter_account_id = hcb.id limit 1) as c3, "+ - "(select 1 from history_transaction_participants where history_account_id = hcb.id limit 1) as c4, "+ - "1 as cx from history_accounts hcb where id >= 0 order by id limit 10) as sub "+ - "where c0 IS NULL and c1 IS NULL and c2 IS NULL and c3 IS NULL and c4 IS NULL and 1=1);", query) + "DELETE FROM history_accounts WHERE id IN ("+ + "WITH ha_batch AS (SELECT id FROM history_accounts WHERE id >= 0 ORDER BY id limit 10) SELECT e1.id as id FROM ha_batch e1 "+ + "WHERE NOT EXISTS ( SELECT 1 as row FROM history_transaction_participants WHERE history_transaction_participants.history_account_id = id LIMIT 1) "+ + "AND NOT EXISTS ( SELECT 1 as row FROM history_effects WHERE history_effects.history_account_id = id LIMIT 1) "+ + "AND NOT EXISTS ( SELECT 1 as row FROM history_operation_participants WHERE history_operation_participants.history_account_id = id LIMIT 1) "+ + "AND NOT EXISTS ( SELECT 1 as row FROM history_trades WHERE history_trades.base_account_id = id LIMIT 1) "+ + "AND NOT EXISTS ( SELECT 1 as row FROM history_trades WHERE history_trades.counter_account_id = id LIMIT 1))", query) } diff --git a/services/horizon/internal/db2/history/reap_test.go b/services/horizon/internal/db2/history/reap_test.go index 0f033c3629..5601cd19b6 100644 --- a/services/horizon/internal/db2/history/reap_test.go +++ b/services/horizon/internal/db2/history/reap_test.go @@ -30,21 +30,18 @@ func TestReapLookupTables(t *testing.T) { prevLiquidityPools, curLiquidityPools int ) - // Prev - { - err := db.GetRaw(tt.Ctx, &prevLedgers, `SELECT COUNT(*) FROM history_ledgers`) - tt.Require.NoError(err) - err = db.GetRaw(tt.Ctx, &prevAccounts, `SELECT COUNT(*) FROM history_accounts`) - tt.Require.NoError(err) - err = db.GetRaw(tt.Ctx, &prevAssets, `SELECT COUNT(*) FROM history_assets`) - tt.Require.NoError(err) - err = db.GetRaw(tt.Ctx, &prevClaimableBalances, `SELECT COUNT(*) FROM history_claimable_balances`) - tt.Require.NoError(err) - err = db.GetRaw(tt.Ctx, &prevLiquidityPools, `SELECT COUNT(*) FROM history_liquidity_pools`) - tt.Require.NoError(err) - } - - err := reaper.DeleteUnretainedHistory(tt.Ctx) + err := db.GetRaw(tt.Ctx, &prevLedgers, `SELECT COUNT(*) FROM history_ledgers`) + tt.Require.NoError(err) + err = db.GetRaw(tt.Ctx, &prevAccounts, `SELECT COUNT(*) FROM history_accounts`) + tt.Require.NoError(err) + err = db.GetRaw(tt.Ctx, &prevAssets, `SELECT COUNT(*) FROM history_assets`) + tt.Require.NoError(err) + err = db.GetRaw(tt.Ctx, &prevClaimableBalances, `SELECT COUNT(*) FROM history_claimable_balances`) + tt.Require.NoError(err) + err = db.GetRaw(tt.Ctx, &prevLiquidityPools, `SELECT COUNT(*) FROM history_liquidity_pools`) + tt.Require.NoError(err) + + err = reaper.DeleteUnretainedHistory(tt.Ctx) tt.Require.NoError(err) q := &history.Q{tt.HorizonSession()} @@ -52,36 +49,33 @@ func TestReapLookupTables(t *testing.T) { err = q.Begin(tt.Ctx) tt.Require.NoError(err) - results, err := q.ReapLookupTables(tt.Ctx, nil) + results, err := q.ReapLookupTables(tt.Ctx, 5) tt.Require.NoError(err) err = q.Commit() tt.Require.NoError(err) - // cur - { - err := db.GetRaw(tt.Ctx, &curLedgers, `SELECT COUNT(*) FROM history_ledgers`) - tt.Require.NoError(err) - err = db.GetRaw(tt.Ctx, &curAccounts, `SELECT COUNT(*) FROM history_accounts`) - tt.Require.NoError(err) - err = db.GetRaw(tt.Ctx, &curAssets, `SELECT COUNT(*) FROM history_assets`) - tt.Require.NoError(err) - err = db.GetRaw(tt.Ctx, &curClaimableBalances, `SELECT COUNT(*) FROM history_claimable_balances`) - tt.Require.NoError(err) - err = db.GetRaw(tt.Ctx, &curLiquidityPools, `SELECT COUNT(*) FROM history_liquidity_pools`) - tt.Require.NoError(err) - } + err = db.GetRaw(tt.Ctx, &curLedgers, `SELECT COUNT(*) FROM history_ledgers`) + tt.Require.NoError(err) + err = db.GetRaw(tt.Ctx, &curAccounts, `SELECT COUNT(*) FROM history_accounts`) + tt.Require.NoError(err) + err = db.GetRaw(tt.Ctx, &curAssets, `SELECT COUNT(*) FROM history_assets`) + tt.Require.NoError(err) + err = db.GetRaw(tt.Ctx, &curClaimableBalances, `SELECT COUNT(*) FROM history_claimable_balances`) + tt.Require.NoError(err) + err = db.GetRaw(tt.Ctx, &curLiquidityPools, `SELECT COUNT(*) FROM history_liquidity_pools`) + tt.Require.NoError(err) tt.Assert.Equal(61, prevLedgers, "prevLedgers") tt.Assert.Equal(1, curLedgers, "curLedgers") tt.Assert.Equal(25, prevAccounts, "prevAccounts") - tt.Assert.Equal(1, curAccounts, "curAccounts") - tt.Assert.Equal(int64(24), results["history_accounts"].RowsDeleted, `deletedCount["history_accounts"]`) + tt.Assert.Equal(21, curAccounts, "curAccounts") + tt.Assert.Equal(int64(4), results["history_accounts"].RowsDeleted, `deletedCount["history_accounts"]`) tt.Assert.Equal(7, prevAssets, "prevAssets") - tt.Assert.Equal(0, curAssets, "curAssets") - tt.Assert.Equal(int64(7), results["history_assets"].RowsDeleted, `deletedCount["history_assets"]`) + tt.Assert.Equal(2, curAssets, "curAssets") + tt.Assert.Equal(int64(5), results["history_assets"].RowsDeleted, `deletedCount["history_assets"]`) tt.Assert.Equal(1, prevClaimableBalances, "prevClaimableBalances") tt.Assert.Equal(0, curClaimableBalances, "curClaimableBalances") @@ -91,6 +85,66 @@ func TestReapLookupTables(t *testing.T) { tt.Assert.Equal(0, curLiquidityPools, "curLiquidityPools") tt.Assert.Equal(int64(1), results["history_liquidity_pools"].RowsDeleted, `deletedCount["history_liquidity_pools"]`) + tt.Assert.Len(results, 4) + tt.Assert.Equal(int64(6), results["history_accounts"].Offset) + tt.Assert.Equal(int64(6), results["history_assets"].Offset) + tt.Assert.Equal(int64(0), results["history_claimable_balances"].Offset) + tt.Assert.Equal(int64(0), results["history_liquidity_pools"].Offset) + + err = q.Begin(tt.Ctx) + tt.Require.NoError(err) + + results, err = q.ReapLookupTables(tt.Ctx, 5) + tt.Require.NoError(err) + + err = q.Commit() + tt.Require.NoError(err) + + err = db.GetRaw(tt.Ctx, &curAccounts, `SELECT COUNT(*) FROM history_accounts`) + tt.Require.NoError(err) + err = db.GetRaw(tt.Ctx, &curAssets, `SELECT COUNT(*) FROM history_assets`) + tt.Require.NoError(err) + + tt.Assert.Equal(16, curAccounts, "curAccounts") + tt.Assert.Equal(int64(5), results["history_accounts"].RowsDeleted, `deletedCount["history_accounts"]`) + + tt.Assert.Equal(0, curAssets, "curAssets") + tt.Assert.Equal(int64(2), results["history_assets"].RowsDeleted, `deletedCount["history_assets"]`) + + tt.Assert.Equal(int64(0), results["history_claimable_balances"].RowsDeleted, `deletedCount["history_claimable_balances"]`) + + tt.Assert.Equal(int64(0), results["history_liquidity_pools"].RowsDeleted, `deletedCount["history_liquidity_pools"]`) + + tt.Assert.Len(results, 4) + tt.Assert.Equal(int64(11), results["history_accounts"].Offset) + tt.Assert.Equal(int64(0), results["history_assets"].Offset) + tt.Assert.Equal(int64(0), results["history_claimable_balances"].Offset) + tt.Assert.Equal(int64(0), results["history_liquidity_pools"].Offset) + + err = q.Begin(tt.Ctx) + tt.Require.NoError(err) + + results, err = q.ReapLookupTables(tt.Ctx, 1000) + tt.Require.NoError(err) + + err = q.Commit() + tt.Require.NoError(err) + + err = db.GetRaw(tt.Ctx, &curAccounts, `SELECT COUNT(*) FROM history_accounts`) + tt.Require.NoError(err) + err = db.GetRaw(tt.Ctx, &curAssets, `SELECT COUNT(*) FROM history_assets`) + tt.Require.NoError(err) + + tt.Assert.Equal(1, curAccounts, "curAccounts") + tt.Assert.Equal(int64(15), results["history_accounts"].RowsDeleted, `deletedCount["history_accounts"]`) + + tt.Assert.Equal(0, curAssets, "curAssets") + tt.Assert.Equal(int64(0), results["history_assets"].RowsDeleted, `deletedCount["history_assets"]`) + + tt.Assert.Equal(int64(0), results["history_claimable_balances"].RowsDeleted, `deletedCount["history_claimable_balances"]`) + + tt.Assert.Equal(int64(0), results["history_liquidity_pools"].RowsDeleted, `deletedCount["history_liquidity_pools"]`) + tt.Assert.Len(results, 4) tt.Assert.Equal(int64(0), results["history_accounts"].Offset) tt.Assert.Equal(int64(0), results["history_assets"].Offset) diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 1a54e6843c..64e4558723 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -73,12 +73,13 @@ const ( // * Reaping (requires 2 connections, the extra connection is used for holding the advisory lock) MaxDBConnections = 5 - defaultCoreCursorName = "HORIZON" stateVerificationErrorThreshold = 3 // 100 ledgers per flush has shown in stress tests // to be best point on performance curve, default to that. MaxLedgersPerFlush uint32 = 100 + + reapLookupTablesBatchSize = 1000 ) var log = logpkg.DefaultLogger.WithField("service", "ingest") @@ -253,7 +254,6 @@ type system struct { runStateVerificationOnLedger func(uint32) bool - reapOffsetByTable map[string]int64 maxLedgerPerFlush uint32 reaper *Reaper @@ -369,7 +369,6 @@ func NewSystem(config Config) (System, error) { config.ReapConfig, config.HistorySession, ), - reapOffsetByTable: map[string]int64{}, } system.initMetrics() @@ -843,7 +842,7 @@ func (s *system) maybeReapLookupTables(lastIngestedLedger uint32) { defer cancel() reapStart := time.Now() - results, err := s.historyQ.ReapLookupTables(ctx, s.reapOffsetByTable) + results, err := s.historyQ.ReapLookupTables(ctx, reapLookupTablesBatchSize) if err != nil { log.WithError(err).Warn("Error reaping lookup tables") return @@ -859,8 +858,9 @@ func (s *system) maybeReapLookupTables(lastIngestedLedger uint32) { reapLog := log for table, result := range results { totalDeleted += result.RowsDeleted - reapLog = reapLog.WithField(table, result) - s.reapOffsetByTable[table] = result.Offset + reapLog = reapLog.WithField(table+"_offset", result.Offset) + reapLog = reapLog.WithField(table+"_duration", result.Duration) + reapLog = reapLog.WithField(table+"_rows_deleted", result.RowsDeleted) s.Metrics().RowsReapedByLookupTable.With(prometheus.Labels{"table": table}).Observe(float64(result.RowsDeleted)) s.Metrics().ReapDurationByLookupTable.With(prometheus.Labels{"table": table}).Observe(result.Duration.Seconds()) } diff --git a/services/horizon/internal/ingest/main_test.go b/services/horizon/internal/ingest/main_test.go index fde8e40a9c..d5733ee5e4 100644 --- a/services/horizon/internal/ingest/main_test.go +++ b/services/horizon/internal/ingest/main_test.go @@ -562,8 +562,8 @@ func (m *mockDBQ) NewTradeBatchInsertBuilder() history.TradeBatchInsertBuilder { return args.Get(0).(history.TradeBatchInsertBuilder) } -func (m *mockDBQ) ReapLookupTables(ctx context.Context, offsets map[string]int64) (map[string]history.LookupTableReapResult, error) { - args := m.Called(ctx, offsets) +func (m *mockDBQ) ReapLookupTables(ctx context.Context, batchSize int) (map[string]history.LookupTableReapResult, error) { + args := m.Called(ctx, batchSize) var r1 map[string]history.LookupTableReapResult if args.Get(0) != nil { r1 = args.Get(0).(map[string]history.LookupTableReapResult) From f692f1246b01fb09af2c232630d4ad31025de747 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Wed, 31 Jul 2024 14:33:24 -0700 Subject: [PATCH 215/234] services/galexie: merge galexie v1.0.0 release branch to master (#5406) * services/galexie: Add Changelog entry * Fix Galexie release workflow --- .github/workflows/galexie-release.yml | 6 +++--- services/galexie/CHANGELOG.md | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/galexie-release.yml b/.github/workflows/galexie-release.yml index 45e8152690..18c441eae3 100644 --- a/.github/workflows/galexie-release.yml +++ b/.github/workflows/galexie-release.yml @@ -43,10 +43,10 @@ jobs: echo "Using stellar core version $(stellar-core version)" - name: Run tests - run: go test -v -race -run TestGalexieTestSuite ./exp/services/galexie/... + run: go test -v -race -run TestGalexieTestSuite ./services/galexie/... - name: Build docker - run: make -C exp/services/galexie docker-build + run: make -C services/galexie docker-build # Push images - name: Login to DockerHub @@ -56,4 +56,4 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Push to DockerHub - run: make -C exp/services/galexie docker-push + run: make -C services/galexie docker-push diff --git a/services/galexie/CHANGELOG.md b/services/galexie/CHANGELOG.md index e69de29bb2..8040d43cb8 100644 --- a/services/galexie/CHANGELOG.md +++ b/services/galexie/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +All notable changes to this project will be documented in this +file. This project adheres to [Semantic Versioning](http://semver.org/). + +## [v1.0.0] + +- 🎉 First release! From 81dbb68fb71cb09e434e1544fcabd0110021c894 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Fri, 2 Aug 2024 23:10:27 -0700 Subject: [PATCH 216/234] services/horizon: migrate to docker compose v2 (#5409) --- services/horizon/internal/test/integration/integration.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/horizon/internal/test/integration/integration.go b/services/horizon/internal/test/integration/integration.go index 9884470d70..107cb33759 100644 --- a/services/horizon/internal/test/integration/integration.go +++ b/services/horizon/internal/test/integration/integration.go @@ -224,7 +224,8 @@ func (i *Test) runComposeCommand(args ...string) { cmdline = append([]string{"-f", integrationSorobanRPCYaml}, cmdline...) } cmdline = append([]string{"-f", integrationYaml}, cmdline...) - cmd := exec.Command("docker-compose", cmdline...) + cmdline = append([]string{"compose"}, cmdline...) + cmd := exec.Command("docker", cmdline...) coreImageOverride := "" if i.config.CoreDockerImage != "" { coreImageOverride = i.config.CoreDockerImage From 9b925b19a8fd01ac837efbd0a083940ab98e02e8 Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 5 Aug 2024 21:18:39 +0100 Subject: [PATCH 217/234] services/horizon/internal/actions: populate default cursor value for history endpoints (#5410) --- services/horizon/internal/actions/effects.go | 2 +- services/horizon/internal/actions/helpers.go | 14 ++++ .../horizon/internal/actions/helpers_test.go | 58 +++++++++++++++ services/horizon/internal/actions/ledger.go | 2 +- .../horizon/internal/actions/operation.go | 2 +- .../internal/actions/operation_test.go | 71 +++++++++++++------ services/horizon/internal/actions/trade.go | 2 +- .../horizon/internal/actions/transaction.go | 2 +- .../internal/actions/transaction_test.go | 15 +++- 9 files changed, 138 insertions(+), 30 deletions(-) diff --git a/services/horizon/internal/actions/effects.go b/services/horizon/internal/actions/effects.go index cc7406d7bc..e04997aca5 100644 --- a/services/horizon/internal/actions/effects.go +++ b/services/horizon/internal/actions/effects.go @@ -51,7 +51,7 @@ type GetEffectsHandler struct { } func (handler GetEffectsHandler) GetResourcePage(w HeaderWriter, r *http.Request) ([]hal.Pageable, error) { - pq, err := GetPageQuery(handler.LedgerState, r) + pq, err := GetPageQuery(handler.LedgerState, r, DefaultTOID) if err != nil { return nil, err } diff --git a/services/horizon/internal/actions/helpers.go b/services/horizon/internal/actions/helpers.go index 579e1056ff..2575b13251 100644 --- a/services/horizon/internal/actions/helpers.go +++ b/services/horizon/internal/actions/helpers.go @@ -21,6 +21,7 @@ import ( "github.com/stellar/go/services/horizon/internal/ledger" hProblem "github.com/stellar/go/services/horizon/internal/render/problem" "github.com/stellar/go/support/errors" + "github.com/stellar/go/support/ordered" "github.com/stellar/go/support/render/problem" "github.com/stellar/go/toid" "github.com/stellar/go/xdr" @@ -44,6 +45,8 @@ type Opt int const ( // DisableCursorValidation disables cursor validation in GetPageQuery DisableCursorValidation Opt = iota + // DefaultTOID sets a default cursor value in GetPageQuery based on the ledger state + DefaultTOID Opt = iota ) // HeaderWriter is an interface for setting HTTP response headers @@ -182,10 +185,14 @@ func getLimit(r *http.Request, name string, def uint64, max uint64) (uint64, err // using the results from a call to GetPagingParams() func GetPageQuery(ledgerState *ledger.State, r *http.Request, opts ...Opt) (db2.PageQuery, error) { disableCursorValidation := false + defaultTOID := false for _, opt := range opts { if opt == DisableCursorValidation { disableCursorValidation = true } + if opt == DefaultTOID { + defaultTOID = true + } } cursor, err := getCursor(ledgerState, r, ParamCursor) @@ -214,6 +221,13 @@ func GetPageQuery(ledgerState *ledger.State, r *http.Request, opts ...Opt) (db2. return db2.PageQuery{}, err } + if cursor == "" && defaultTOID { + if pageQuery.Order == db2.OrderAscending { + pageQuery.Cursor = toid.AfterLedger( + ordered.Max(0, ledgerState.CurrentStatus().HistoryElder-1), + ).String() + } + } return pageQuery, nil } diff --git a/services/horizon/internal/actions/helpers_test.go b/services/horizon/internal/actions/helpers_test.go index 445862c25e..05e840577e 100644 --- a/services/horizon/internal/actions/helpers_test.go +++ b/services/horizon/internal/actions/helpers_test.go @@ -8,6 +8,7 @@ import ( "net/url" "strings" "testing" + "time" "github.com/go-chi/chi" "github.com/stretchr/testify/assert" @@ -290,6 +291,63 @@ func TestGetPageQuery(t *testing.T) { tt.Assert.Error(err) } +func TestGetPageQueryCursorDefaultTOID(t *testing.T) { + ascReq := makeTestActionRequest("/foo-bar/blah?limit=2", testURLParams()) + descReq := makeTestActionRequest("/foo-bar/blah?limit=2&order=desc", testURLParams()) + + ledgerState := &ledger.State{} + ledgerState.SetHorizonStatus(ledger.HorizonStatus{ + HistoryLatest: 7000, + HistoryLatestClosedAt: time.Now(), + HistoryElder: 300, + ExpHistoryLatest: 7000, + }) + + pq, err := GetPageQuery(ledgerState, ascReq, DefaultTOID) + assert.NoError(t, err) + assert.Equal(t, toid.AfterLedger(299).String(), pq.Cursor) + assert.Equal(t, uint64(2), pq.Limit) + assert.Equal(t, "asc", pq.Order) + + pq, err = GetPageQuery(ledgerState, descReq, DefaultTOID) + assert.NoError(t, err) + assert.Equal(t, "", pq.Cursor) + assert.Equal(t, uint64(2), pq.Limit) + assert.Equal(t, "desc", pq.Order) + + pq, err = GetPageQuery(ledgerState, ascReq) + assert.NoError(t, err) + assert.Empty(t, pq.Cursor) + assert.Equal(t, uint64(2), pq.Limit) + assert.Equal(t, "asc", pq.Order) + + pq, err = GetPageQuery(ledgerState, descReq) + assert.NoError(t, err) + assert.Empty(t, pq.Cursor) + assert.Equal(t, "", pq.Cursor) + assert.Equal(t, "desc", pq.Order) + + ledgerState.SetHorizonStatus(ledger.HorizonStatus{ + HistoryLatest: 7000, + HistoryLatestClosedAt: time.Now(), + HistoryElder: 0, + ExpHistoryLatest: 7000, + }) + + pq, err = GetPageQuery(ledgerState, ascReq, DefaultTOID) + assert.NoError(t, err) + assert.Equal(t, toid.AfterLedger(0).String(), pq.Cursor) + assert.Equal(t, uint64(2), pq.Limit) + assert.Equal(t, "asc", pq.Order) + + pq, err = GetPageQuery(ledgerState, descReq, DefaultTOID) + assert.NoError(t, err) + assert.Equal(t, "", pq.Cursor) + assert.Equal(t, uint64(2), pq.Limit) + assert.Equal(t, "desc", pq.Order) + +} + func TestGetString(t *testing.T) { tt := test.Start(t) defer tt.Finish() diff --git a/services/horizon/internal/actions/ledger.go b/services/horizon/internal/actions/ledger.go index 37fddb5bd9..0b3d51b8e1 100644 --- a/services/horizon/internal/actions/ledger.go +++ b/services/horizon/internal/actions/ledger.go @@ -17,7 +17,7 @@ type GetLedgersHandler struct { } func (handler GetLedgersHandler) GetResourcePage(w HeaderWriter, r *http.Request) ([]hal.Pageable, error) { - pq, err := GetPageQuery(handler.LedgerState, r) + pq, err := GetPageQuery(handler.LedgerState, r, DefaultTOID) if err != nil { return nil, err } diff --git a/services/horizon/internal/actions/operation.go b/services/horizon/internal/actions/operation.go index f59191aee0..9ccadb272e 100644 --- a/services/horizon/internal/actions/operation.go +++ b/services/horizon/internal/actions/operation.go @@ -72,7 +72,7 @@ type GetOperationsHandler struct { func (handler GetOperationsHandler) GetResourcePage(w HeaderWriter, r *http.Request) ([]hal.Pageable, error) { ctx := r.Context() - pq, err := GetPageQuery(handler.LedgerState, r) + pq, err := GetPageQuery(handler.LedgerState, r, DefaultTOID) if err != nil { return nil, err } diff --git a/services/horizon/internal/actions/operation_test.go b/services/horizon/internal/actions/operation_test.go index 9aa033fd24..aaa21ef2a3 100644 --- a/services/horizon/internal/actions/operation_test.go +++ b/services/horizon/internal/actions/operation_test.go @@ -10,6 +10,8 @@ import ( "time" "github.com/guregu/null" + "github.com/stretchr/testify/assert" + "github.com/stellar/go/ingest" "github.com/stellar/go/protocols/horizon/operations" "github.com/stellar/go/services/horizon/internal/db2/history" @@ -19,7 +21,6 @@ import ( supportProblem "github.com/stellar/go/support/render/problem" "github.com/stellar/go/toid" "github.com/stellar/go/xdr" - "github.com/stretchr/testify/assert" ) func TestInvokeHostFnDetailsInPaymentOperations(t *testing.T) { @@ -28,8 +29,14 @@ func TestInvokeHostFnDetailsInPaymentOperations(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) q := &history.Q{tt.HorizonSession()} - handler := GetOperationsHandler{OnlyPayments: true} - + handler := GetOperationsHandler{OnlyPayments: true, + LedgerState: &ledger.State{}, + } + handler.LedgerState.SetHorizonStatus(ledger.HorizonStatus{ + HistoryLatest: 56, + HistoryElder: 56, + ExpHistoryLatest: 56, + }) txIndex := int32(1) sequence := int32(56) txID := toid.New(sequence, txIndex, 0).ToInt64() @@ -178,10 +185,12 @@ func TestInvokeHostFnDetailsInPaymentOperations(t *testing.T) { func TestGetOperationsWithoutFilter(t *testing.T) { tt := test.Start(t) defer tt.Finish() - tt.Scenario("base") q := &history.Q{tt.HorizonSession()} - handler := GetOperationsHandler{} + handler := GetOperationsHandler{ + LedgerState: &ledger.State{}, + } + handler.LedgerState.SetStatus(tt.Scenario("base")) records, err := handler.GetResourcePage( httptest.NewRecorder(), @@ -196,10 +205,12 @@ func TestGetOperationsWithoutFilter(t *testing.T) { func TestGetOperationsExclusiveFilters(t *testing.T) { tt := test.Start(t) defer tt.Finish() - tt.Scenario("base") q := &history.Q{tt.HorizonSession()} - handler := GetOperationsHandler{} + handler := GetOperationsHandler{ + LedgerState: &ledger.State{}, + } + handler.LedgerState.SetStatus(tt.Scenario("base")) testCases := []struct { desc string @@ -255,10 +266,12 @@ func TestGetOperationsByLiquidityPool(t *testing.T) { func TestGetOperationsFilterByAccountID(t *testing.T) { tt := test.Start(t) defer tt.Finish() - tt.Scenario("base") q := &history.Q{tt.HorizonSession()} - handler := GetOperationsHandler{} + handler := GetOperationsHandler{ + LedgerState: &ledger.State{}, + } + handler.LedgerState.SetStatus(tt.Scenario("base")) testCases := []struct { accountID string @@ -296,10 +309,12 @@ func TestGetOperationsFilterByAccountID(t *testing.T) { func TestGetOperationsFilterByTxID(t *testing.T) { tt := test.Start(t) defer tt.Finish() - tt.Scenario("base") q := &history.Q{tt.HorizonSession()} - handler := GetOperationsHandler{} + handler := GetOperationsHandler{ + LedgerState: &ledger.State{}, + } + handler.LedgerState.SetStatus(tt.Scenario("base")) testCases := []struct { desc string @@ -370,10 +385,12 @@ func TestGetOperationsFilterByTxID(t *testing.T) { func TestGetOperationsIncludeFailed(t *testing.T) { tt := test.Start(t) defer tt.Finish() - tt.Scenario("failed_transactions") q := &history.Q{tt.HorizonSession()} - handler := GetOperationsHandler{} + handler := GetOperationsHandler{ + LedgerState: &ledger.State{}, + } + handler.LedgerState.SetStatus(tt.Scenario("failed_transactions")) records, err := handler.GetResourcePage( httptest.NewRecorder(), @@ -500,10 +517,12 @@ func TestGetOperationsIncludeFailed(t *testing.T) { func TestGetOperationsFilterByLedgerID(t *testing.T) { tt := test.Start(t) defer tt.Finish() - tt.Scenario("base") q := &history.Q{tt.HorizonSession()} - handler := GetOperationsHandler{} + handler := GetOperationsHandler{ + LedgerState: &ledger.State{}, + } + handler.LedgerState.SetStatus(tt.Scenario("base")) testCases := []struct { ledgerID string @@ -571,12 +590,13 @@ func TestGetOperationsFilterByLedgerID(t *testing.T) { func TestGetOperationsOnlyPayments(t *testing.T) { tt := test.Start(t) defer tt.Finish() - tt.Scenario("base") q := &history.Q{tt.HorizonSession()} handler := GetOperationsHandler{ + LedgerState: &ledger.State{}, OnlyPayments: true, } + handler.LedgerState.SetStatus(tt.Scenario("base")) records, err := handler.GetResourcePage( httptest.NewRecorder(), @@ -620,7 +640,7 @@ func TestGetOperationsOnlyPayments(t *testing.T) { tt.Assert.NoError(err) tt.Assert.Len(records, 1) - tt.Scenario("pathed_payment") + handler.LedgerState.SetStatus(tt.Scenario("pathed_payment")) records, err = handler.GetResourcePage( httptest.NewRecorder(), @@ -651,10 +671,12 @@ func TestGetOperationsOnlyPayments(t *testing.T) { func TestOperation_CreatedAt(t *testing.T) { tt := test.Start(t) defer tt.Finish() - tt.Scenario("base") q := &history.Q{tt.HorizonSession()} - handler := GetOperationsHandler{} + handler := GetOperationsHandler{ + LedgerState: &ledger.State{}, + } + handler.LedgerState.SetStatus(tt.Scenario("base")) records, err := handler.GetResourcePage( httptest.NewRecorder(), @@ -676,12 +698,12 @@ func TestOperation_CreatedAt(t *testing.T) { func TestGetOperationsPagination(t *testing.T) { tt := test.Start(t) defer tt.Finish() - tt.Scenario("base") q := &history.Q{tt.HorizonSession()} handler := GetOperationsHandler{ LedgerState: &ledger.State{}, } + handler.LedgerState.SetStatus(tt.Scenario("base")) records, err := handler.GetResourcePage( httptest.NewRecorder(), @@ -735,10 +757,12 @@ func TestGetOperationsPagination(t *testing.T) { func TestGetOperations_IncludeTransactions(t *testing.T) { tt := test.Start(t) defer tt.Finish() - tt.Scenario("failed_transactions") q := &history.Q{tt.HorizonSession()} - handler := GetOperationsHandler{} + handler := GetOperationsHandler{ + LedgerState: &ledger.State{}, + } + handler.LedgerState.SetStatus(tt.Scenario("failed_transactions")) _, err := handler.GetResourcePage( httptest.NewRecorder(), @@ -828,11 +852,12 @@ func TestGetOperation(t *testing.T) { func TestOperation_IncludeTransaction(t *testing.T) { tt := test.Start(t) defer tt.Finish() - tt.Scenario("kahuna") handler := GetOperationByIDHandler{ LedgerState: &ledger.State{}, } + handler.LedgerState.SetStatus(tt.Scenario("kahuna")) + record, err := handler.GetResource( httptest.NewRecorder(), makeRequest( diff --git a/services/horizon/internal/actions/trade.go b/services/horizon/internal/actions/trade.go index d6bf415424..b29ad64715 100644 --- a/services/horizon/internal/actions/trade.go +++ b/services/horizon/internal/actions/trade.go @@ -159,7 +159,7 @@ type GetTradesHandler struct { func (handler GetTradesHandler) GetResourcePage(w HeaderWriter, r *http.Request) ([]hal.Pageable, error) { ctx := r.Context() - pq, err := GetPageQuery(handler.LedgerState, r) + pq, err := GetPageQuery(handler.LedgerState, r, DefaultTOID) if err != nil { return nil, err } diff --git a/services/horizon/internal/actions/transaction.go b/services/horizon/internal/actions/transaction.go index 6903d5db2f..6a2fd1c6e2 100644 --- a/services/horizon/internal/actions/transaction.go +++ b/services/horizon/internal/actions/transaction.go @@ -98,7 +98,7 @@ type GetTransactionsHandler struct { func (handler GetTransactionsHandler) GetResourcePage(w HeaderWriter, r *http.Request) ([]hal.Pageable, error) { ctx := r.Context() - pq, err := GetPageQuery(handler.LedgerState, r) + pq, err := GetPageQuery(handler.LedgerState, r, DefaultTOID) if err != nil { return nil, err } diff --git a/services/horizon/internal/actions/transaction_test.go b/services/horizon/internal/actions/transaction_test.go index b76cf1b0bf..d501e38213 100644 --- a/services/horizon/internal/actions/transaction_test.go +++ b/services/horizon/internal/actions/transaction_test.go @@ -6,6 +6,7 @@ import ( "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/services/horizon/internal/db2/history" + "github.com/stellar/go/services/horizon/internal/ledger" "github.com/stellar/go/services/horizon/internal/test" supportProblem "github.com/stellar/go/support/render/problem" ) @@ -16,7 +17,10 @@ func TestGetTransactionsHandler(t *testing.T) { defer tt.Finish() q := &history.Q{tt.HorizonSession()} - handler := GetTransactionsHandler{} + handler := GetTransactionsHandler{ + LedgerState: &ledger.State{}, + } + handler.LedgerState.SetStatus(tt.Scenario("base")) // filter by account records, err := handler.GetResourcePage( @@ -155,7 +159,14 @@ func TestFeeBumpTransactionPage(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) q := &history.Q{tt.HorizonSession()} fixture := history.FeeBumpScenario(tt, q, true) - handler := GetTransactionsHandler{} + handler := GetTransactionsHandler{ + LedgerState: &ledger.State{}, + } + handler.LedgerState.SetHorizonStatus(ledger.HorizonStatus{ + HistoryLatest: fixture.Ledger.Sequence, + HistoryElder: fixture.Ledger.Sequence, + ExpHistoryLatest: uint32(fixture.Ledger.Sequence), + }) records, err := handler.GetResourcePage( httptest.NewRecorder(), From cabbd30486ee22c50648166040dfb311e4912c7e Mon Sep 17 00:00:00 2001 From: shawn Date: Thu, 8 Aug 2024 13:54:10 -0700 Subject: [PATCH 218/234] Update GH repo PR template checklist for CHANGELOG expectations (#5416) --- .github/pull_request_template.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0c4a54c7ef..59905a4548 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -23,8 +23,7 @@ change is, and why it is being made, with enough context for anyone to understan ### Release planning -* [ ] I've updated the relevant CHANGELOG ([here](services/horizon/CHANGELOG.md) for Horizon) if - needed with deprecations, added features, breaking changes, and DB schema changes. +* [ ] I've reviewed the changes in this PR and if I consider them worthwhile for being mentioned on release notes then I have updated the relevant `CHANGELOG.md` within the component folder structure. For example, if I changed horizon, then I updated ([services/horizon/CHANGELOG.md](services/horizon/CHANGELOG.md). I add a new line item describing the change and reference to this PR. If I don't update a CHANGELOG, I acknowledge this PR's change may not be mentioned in future release notes. * [ ] I've decided if this PR requires a new major/minor version according to [semver](https://semver.org/), or if it's mainly a patch change. The PR is targeted at the next release branch if it's not a patch change. From f2f4a0e085ed09426df7da6e13c299f8365b1b40 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Thu, 15 Aug 2024 09:05:49 -0700 Subject: [PATCH 219/234] ingest: Add BufferedStorate ledger backend in README (#5427) --- ingest/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ingest/README.md b/ingest/README.md index 277807f978..bace92c2d0 100644 --- a/ingest/README.md +++ b/ingest/README.md @@ -23,9 +23,9 @@ From a high level, the ingestion library is broken down into a few modular compo [ Ledger Backend ] | - | - Captive - Core + |---+---| + Captive Buffered + Core Storage ``` This is described in a little more detail in [`doc.go`](./doc.go), its accompanying examples, the documentation within this package, and the rest of this tutorial. From 26ab1acd813b9f2df77e8a4a139cbab90f1c952e Mon Sep 17 00:00:00 2001 From: urvisavla Date: Mon, 19 Aug 2024 16:55:51 -0700 Subject: [PATCH 220/234] Fix inconsistent debian version in Dockerfile (#5432) --- services/galexie/docker/Dockerfile | 2 +- services/horizon/docker/Dockerfile.dev | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/galexie/docker/Dockerfile b/services/galexie/docker/Dockerfile index 6614a40692..42ff2e43d9 100644 --- a/services/galexie/docker/Dockerfile +++ b/services/galexie/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.22-bullseye AS builder +FROM golang:1.22-bookworm AS builder WORKDIR /go/src/github.com/stellar/go diff --git a/services/horizon/docker/Dockerfile.dev b/services/horizon/docker/Dockerfile.dev index 5cef8d89d1..91b0f8aeea 100644 --- a/services/horizon/docker/Dockerfile.dev +++ b/services/horizon/docker/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM golang:1.22-bullseye AS builder +FROM golang:1.22-bookworm AS builder ARG VERSION="devel" WORKDIR /go/src/github.com/stellar/go From dfabe31b5b89cb235e81732d708a3f5bdeff507a Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:19:02 +1000 Subject: [PATCH 221/234] Remove CLA reference from contributing document (#5435) * Remove CLA reference from contributing document * Update CONTRIBUTING.md --- services/horizon/CONTRIBUTING.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/services/horizon/CONTRIBUTING.md b/services/horizon/CONTRIBUTING.md index 26ba4d1ad0..c8c598382d 100644 --- a/services/horizon/CONTRIBUTING.md +++ b/services/horizon/CONTRIBUTING.md @@ -1,6 +1,3 @@ # How to contribute -Please read the [Contribution Guide](https://github.com/stellar/docs/blob/master/CONTRIBUTING.md). - -Then please [sign the Contributor License Agreement](https://docs.google.com/forms/d/1g7EF6PERciwn7zfmfke5Sir2n10yddGGSXyZsq98tVY/viewform?usp=send_form). - +Please read the [Contribution Guide](https://github.com/stellar/.github/blob/master/CONTRIBUTING.md). From 8257415ed3f62654d03f979132c8637632c6e230 Mon Sep 17 00:00:00 2001 From: mlo Date: Thu, 22 Aug 2024 13:52:21 -0700 Subject: [PATCH 222/234] Update Core version to v21.3.1 across CI (#5439) * Bump core versions to v21.3.0 * Update RPC image and incorporate into cache-busting * Remove unsupported Horizon / Captive Core flag in config --------- Co-authored-by: George --- .github/workflows/galexie-release.yml | 2 +- .github/workflows/galexie.yml | 2 +- .github/workflows/horizon.yml | 10 +++++----- .../captive-core-integration-tests.soroban-rpc.cfg | 2 +- services/horizon/internal/docs/GUIDE_FOR_DEVELOPERS.md | 4 ++-- services/horizon/internal/docs/TESTING_NOTES.md | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/galexie-release.yml b/.github/workflows/galexie-release.yml index 18c441eae3..b84f3c1cf0 100644 --- a/.github/workflows/galexie-release.yml +++ b/.github/workflows/galexie-release.yml @@ -16,7 +16,7 @@ jobs: # this is the multi-arch index sha, get it by 'docker buildx imagetools inspect stellar/quickstart:testing' GALEXIE_INTEGRATION_TESTS_QUICKSTART_IMAGE: docker.io/stellar/quickstart:testing@sha256:03c6679f838a92b1eda4cd3a9e2bdee4c3586e278a138a0acf36a9bc99a0041f GALEXIE_INTEGRATION_TESTS_QUICKSTART_IMAGE_PULL: "false" - STELLAR_CORE_VERSION: 21.1.0-1921.b3aeb14cc.focal + STELLAR_CORE_VERSION: 21.3.1-2007.4ede19620.focal steps: - name: Set VERSION run: | diff --git a/.github/workflows/galexie.yml b/.github/workflows/galexie.yml index 458f23ca37..6406a7a897 100644 --- a/.github/workflows/galexie.yml +++ b/.github/workflows/galexie.yml @@ -10,7 +10,7 @@ jobs: name: Test runs-on: ubuntu-latest env: - CAPTIVE_CORE_DEBIAN_PKG_VERSION: 21.1.0-1921.b3aeb14cc.focal + CAPTIVE_CORE_DEBIAN_PKG_VERSION: 21.3.1-2007.4ede19620.focal GALEXIE_INTEGRATION_TESTS_ENABLED: "true" GALEXIE_INTEGRATION_TESTS_CAPTIVE_CORE_BIN: /usr/bin/stellar-core # this pins to a version of quickstart:testing that has the same version as GALEXIE_INTEGRATION_TESTS_CAPTIVE_CORE_BIN diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index 117e11bb6f..f315e27791 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -33,9 +33,9 @@ jobs: HORIZON_INTEGRATION_TESTS_ENABLED: true HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL: ${{ matrix.protocol-version }} HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_USE_DB: true - PROTOCOL_21_CORE_DEBIAN_PKG_VERSION: 21.0.0-1872.c6f474133.focal - PROTOCOL_21_CORE_DOCKER_IMG: stellar/stellar-core:21 - PROTOCOL_21_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:21.0.0-rc2-73 + PROTOCOL_21_CORE_DEBIAN_PKG_VERSION: 21.3.1-2007.4ede19620.focal + PROTOCOL_21_CORE_DOCKER_IMG: stellar/stellar-core:21.3.1-2007.4ede19620.focal + PROTOCOL_21_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:21.4.1 PGHOST: localhost PGPORT: 5432 PGUSER: postgres @@ -98,7 +98,7 @@ jobs: - name: Calculate the source hash id: calculate_source_hash run: | - combined_hash=$(echo "horizon-hash-${{ hashFiles('./horizon') }}-${{ hashFiles('./clients/horizonclient/**') }}-${{ hashFiles('./protocols/horizon/**') }}-${{ hashFiles('./txnbuild/**') }}-${{ hashFiles('./ingest/**') }}-${{ hashFiles('./xdr/**') }}-${{ hashFiles('./services/**') }}-${{ env.PROTOCOL_21_CORE_DOCKER_IMG }}-${{ env.PREFIX }}" | sha256sum | cut -d ' ' -f 1) + combined_hash=$(echo "horizon-hash-${{ hashFiles('./horizon') }}-${{ hashFiles('./clients/horizonclient/**') }}-${{ hashFiles('./protocols/horizon/**') }}-${{ hashFiles('./txnbuild/**') }}-${{ hashFiles('./ingest/**') }}-${{ hashFiles('./xdr/**') }}-${{ hashFiles('./services/**') }}-${{ env.PROTOCOL_21_CORE_DOCKER_IMG }}-${{ env.PROTOCOL_21_RPC_DOCKER_IMG }}-${{ env.PROTOCOL_21_CORE_DEBIAN_PKG_VERSION }}-${{ env.PREFIX }}" | sha256sum | cut -d ' ' -f 1) echo "COMBINED_SOURCE_HASH=$combined_hash" >> "$GITHUB_ENV" - name: Restore Horizon binary and integration tests source hash to cache @@ -123,7 +123,7 @@ jobs: name: Test (and push) verify-range image runs-on: ubuntu-22.04 env: - STELLAR_CORE_VERSION: 21.0.0-1872.c6f474133.focal + STELLAR_CORE_VERSION: 21.3.1-2007.4ede19620.focal CAPTIVE_CORE_STORAGE_PATH: /tmp steps: - uses: actions/checkout@v3 diff --git a/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg b/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg index 8d76504de2..0ce81a7f5c 100644 --- a/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg +++ b/services/horizon/docker/captive-core-integration-tests.soroban-rpc.cfg @@ -1,4 +1,4 @@ -EXPERIMENTAL_BUCKETLIST_DB = true +DEPRECATED_SQL_LEDGER_STATE=false PEER_PORT=11725 ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true diff --git a/services/horizon/internal/docs/GUIDE_FOR_DEVELOPERS.md b/services/horizon/internal/docs/GUIDE_FOR_DEVELOPERS.md index cdbc1b97c7..9464aea2a5 100644 --- a/services/horizon/internal/docs/GUIDE_FOR_DEVELOPERS.md +++ b/services/horizon/internal/docs/GUIDE_FOR_DEVELOPERS.md @@ -171,14 +171,14 @@ By default, the Docker Compose file is configured to use version 21 of Protocol ```bash export PROTOCOL_VERSION="21" export CORE_IMAGE="stellar/stellar-core:21" -export STELLAR_CORE_VERSION="21.0.0-1872.c6f474133.focal" +export STELLAR_CORE_VERSION="21.3.1-2007.4ede19620.focal" ``` Example: Runs Stellar Protocol and Core version 21, for any mode of testnet, standalone, pubnet ```bash -PROTOCOL_VERSION=21 CORE_IMAGE=stellar/stellar-core:21 STELLAR_CORE_VERSION=21.0.0-1872.c6f474133.focal ./start.sh [standalone|pubnet] +PROTOCOL_VERSION=21 CORE_IMAGE=stellar/stellar-core:21 STELLAR_CORE_VERSION=21.3.1-2007.4ede19620.focal ./start.sh [standalone|pubnet] ``` ## **Logging** diff --git a/services/horizon/internal/docs/TESTING_NOTES.md b/services/horizon/internal/docs/TESTING_NOTES.md index c7db9cd465..4dcc5ce6f2 100644 --- a/services/horizon/internal/docs/TESTING_NOTES.md +++ b/services/horizon/internal/docs/TESTING_NOTES.md @@ -16,7 +16,7 @@ Before running integration tests, you also need to set some environment variable ```bash export HORIZON_INTEGRATION_TESTS_ENABLED=true export HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL=21 -export HORIZON_INTEGRATION_TESTS_DOCKER_IMG=stellar/stellar-core:21.0.0-1872.c6f474133.focal +export HORIZON_INTEGRATION_TESTS_DOCKER_IMG=stellar/stellar-core:21.3.1-2007.4ede19620.focal ``` Make sure to check [horizon.yml](/.github/workflows/horizon.yml) for the latest core image version. From 2349c8fbd5f3d51df3c81b2d25f0c8e93f579cc0 Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 23 Aug 2024 23:31:37 +0100 Subject: [PATCH 223/234] services/horizon/internal/db2/history: Insert and query rows from history lookup tables with one query (#5415) --- .../internal/db2/history/account_loader.go | 336 ++++++++++++------ .../db2/history/account_loader_test.go | 33 +- .../internal/db2/history/asset_loader.go | 207 +++-------- .../internal/db2/history/asset_loader_test.go | 56 ++- .../db2/history/claimable_balance_loader.go | 151 +------- .../history/claimable_balance_loader_test.go | 41 ++- .../db2/history/liquidity_pool_loader.go | 151 +------- .../db2/history/liquidity_pool_loader_test.go | 37 +- 8 files changed, 466 insertions(+), 546 deletions(-) diff --git a/services/horizon/internal/db2/history/account_loader.go b/services/horizon/internal/db2/history/account_loader.go index e7e7e90854..9e15920609 100644 --- a/services/horizon/internal/db2/history/account_loader.go +++ b/services/horizon/internal/db2/history/account_loader.go @@ -1,6 +1,7 @@ package history import ( + "cmp" "context" "database/sql/driver" "fmt" @@ -12,37 +13,29 @@ import ( "github.com/stellar/go/support/collections/set" "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/ordered" ) +var errSealed = errors.New("cannot register more entries to Loader after calling Exec()") + +// LoaderStats describes the result of executing a history lookup id Loader +type LoaderStats struct { + // Total is the number of elements registered to the Loader + Total int + // Inserted is the number of elements inserted into the lookup table + Inserted int +} + // FutureAccountID represents a future history account. // A FutureAccountID is created by an AccountLoader and // the account id is available after calling Exec() on // the AccountLoader. -type FutureAccountID struct { - address string - loader *AccountLoader -} - -const loaderLookupBatchSize = 50000 - -// Value implements the database/sql/driver Valuer interface. -func (a FutureAccountID) Value() (driver.Value, error) { - return a.loader.GetNow(a.address) -} +type FutureAccountID = future[string, Account] // AccountLoader will map account addresses to their history // account ids. If there is no existing mapping for a given address, // the AccountLoader will insert into the history_accounts table to // establish a mapping. -type AccountLoader struct { - sealed bool - set set.Set[string] - ids map[string]int64 - stats LoaderStats -} - -var errSealed = errors.New("cannot register more entries to loader after calling Exec()") +type AccountLoader = loader[string, Account] // NewAccountLoader will construct a new AccountLoader instance. func NewAccountLoader() *AccountLoader { @@ -51,141 +44,222 @@ func NewAccountLoader() *AccountLoader { set: set.Set[string]{}, ids: map[string]int64{}, stats: LoaderStats{}, + name: "AccountLoader", + table: "history_accounts", + columnsForKeys: func(addresses []string) []columnValues { + return []columnValues{ + { + name: "address", + dbType: "character varying(64)", + objects: addresses, + }, + } + }, + mappingFromRow: func(account Account) (string, int64) { + return account.Address, account.ID + }, + less: cmp.Less[string], } } -// GetFuture registers the given account address into the loader and -// returns a FutureAccountID which will hold the history account id for -// the address after Exec() is called. -func (a *AccountLoader) GetFuture(address string) FutureAccountID { - if a.sealed { +type loader[K comparable, T any] struct { + sealed bool + set set.Set[K] + ids map[K]int64 + stats LoaderStats + name string + table string + columnsForKeys func([]K) []columnValues + mappingFromRow func(T) (K, int64) + less func(K, K) bool +} + +type future[K comparable, T any] struct { + key K + loader *loader[K, T] +} + +// Value implements the database/sql/driver Valuer interface. +func (f future[K, T]) Value() (driver.Value, error) { + return f.loader.GetNow(f.key) +} + +// GetFuture registers the given key into the Loader and +// returns a future which will hold the history id for +// the key after Exec() is called. +func (l *loader[K, T]) GetFuture(key K) future[K, T] { + if l.sealed { panic(errSealed) } - a.set.Add(address) - return FutureAccountID{ - address: address, - loader: a, + l.set.Add(key) + return future[K, T]{ + key: key, + loader: l, } } -// GetNow returns the history account id for the given address. +// GetNow returns the history id for the given key. // GetNow should only be called on values which were registered by // GetFuture() calls. Also, Exec() must be called before any GetNow // call can succeed. -func (a *AccountLoader) GetNow(address string) (int64, error) { - if !a.sealed { - return 0, fmt.Errorf(`invalid account loader state, - Exec was not called yet to properly seal and resolve %v id`, address) +func (l *loader[K, T]) GetNow(key K) (int64, error) { + if !l.sealed { + return 0, fmt.Errorf(`invalid loader state, + Exec was not called yet to properly seal and resolve %v id`, key) } - if internalID, ok := a.ids[address]; !ok { - return 0, fmt.Errorf(`account loader address %q was not found`, address) + if internalID, ok := l.ids[key]; !ok { + return 0, fmt.Errorf(`loader key %v was not found`, key) } else { return internalID, nil } } -func (a *AccountLoader) lookupKeys(ctx context.Context, q *Q, addresses []string) error { - for i := 0; i < len(addresses); i += loaderLookupBatchSize { - end := ordered.Min(len(addresses), i+loaderLookupBatchSize) +// Exec will look up all the history ids for the keys registered in the Loader. +// If there are no history ids for a given set of keys, Exec will insert rows +// into the corresponding history table to establish a mapping between each key and its history id. +func (l *loader[K, T]) Exec(ctx context.Context, session db.SessionInterface) error { + l.sealed = true + if len(l.set) == 0 { + return nil + } + q := &Q{session} + keys := make([]K, 0, len(l.set)) + for key := range l.set { + keys = append(keys, key) + } + // sort entries before inserting rows to prevent deadlocks on acquiring a ShareLock + // https://github.com/stellar/go/issues/2370 + sort.Slice(keys, func(i, j int) bool { + return l.less(keys[i], keys[j]) + }) - var accounts []Account - if err := q.AccountsByAddresses(ctx, &accounts, addresses[i:end]); err != nil { - return errors.Wrap(err, "could not select accounts") - } + if count, err := l.insert(ctx, q, keys); err != nil { + return err + } else { + l.stats.Total += count + l.stats.Inserted += count + } - for _, account := range accounts { - a.ids[account.Address] = account.ID - } + if count, err := l.query(ctx, q, keys); err != nil { + return err + } else { + l.stats.Total += count } + return nil } -// LoaderStats describes the result of executing a history lookup id loader -type LoaderStats struct { - // Total is the number of elements registered to the loader - Total int - // Inserted is the number of elements inserted into the lookup table - Inserted int +// Stats returns the number of addresses registered in the Loader and the number of rows +// inserted into the history table. +func (l *loader[K, T]) Stats() LoaderStats { + return l.stats } -// Exec will look up all the history account ids for the addresses registered in the loader. -// If there are no history account ids for a given set of addresses, Exec will insert rows -// into the history_accounts table to establish a mapping between address and history account id. -func (a *AccountLoader) Exec(ctx context.Context, session db.SessionInterface) error { - a.sealed = true - if len(a.set) == 0 { - return nil - } - q := &Q{session} - addresses := make([]string, 0, len(a.set)) - for address := range a.set { - addresses = append(addresses, address) - } +func (l *loader[K, T]) Name() string { + return l.name +} - if err := a.lookupKeys(ctx, q, addresses); err != nil { - return err +func (l *loader[K, T]) filter(keys []K) []K { + if len(l.ids) == 0 { + return keys } - a.stats.Total += len(addresses) - insert := 0 - for _, address := range addresses { - if _, ok := a.ids[address]; ok { + remaining := make([]K, 0, len(keys)) + for _, key := range keys { + if _, ok := l.ids[key]; ok { continue } - addresses[insert] = address - insert++ + remaining = append(remaining, key) } - if insert == 0 { - return nil + return remaining +} + +func (l *loader[K, T]) updateMap(rows []T) { + for _, row := range rows { + key, id := l.mappingFromRow(row) + l.ids[key] = id + } +} + +func (l *loader[K, T]) insert(ctx context.Context, q *Q, keys []K) (int, error) { + keys = l.filter(keys) + if len(keys) == 0 { + return 0, nil } - addresses = addresses[:insert] - // sort entries before inserting rows to prevent deadlocks on acquiring a ShareLock - // https://github.com/stellar/go/issues/2370 - sort.Strings(addresses) + var rows []T err := bulkInsert( ctx, q, - "history_accounts", - []string{"address"}, - []bulkInsertField{ - { - name: "address", - dbType: "character varying(64)", - objects: addresses, - }, - }, + l.table, + l.columnsForKeys(keys), + &rows, ) if err != nil { - return err + return 0, err } - a.stats.Inserted += insert - return a.lookupKeys(ctx, q, addresses) + l.updateMap(rows) + return len(rows), nil } -// Stats returns the number of addresses registered in the loader and the number of addresses -// inserted into the history_accounts table. -func (a *AccountLoader) Stats() LoaderStats { - return a.stats -} +func (l *loader[K, T]) query(ctx context.Context, q *Q, keys []K) (int, error) { + keys = l.filter(keys) + if len(keys) == 0 { + return 0, nil + } -func (a *AccountLoader) Name() string { - return "AccountLoader" + var rows []T + err := bulkGet( + ctx, + q, + l.table, + l.columnsForKeys(keys), + &rows, + ) + if err != nil { + return 0, err + } + + l.updateMap(rows) + return len(rows), nil } -type bulkInsertField struct { +type columnValues struct { name string dbType string objects []string } -func bulkInsert(ctx context.Context, q *Q, table string, conflictFields []string, fields []bulkInsertField) error { +func bulkInsert(ctx context.Context, q *Q, table string, fields []columnValues, response interface{}) error { unnestPart := make([]string, 0, len(fields)) insertFieldsPart := make([]string, 0, len(fields)) pqArrays := make([]interface{}, 0, len(fields)) + // In the code below we are building the bulk insert query which looks like: + // + // WITH rows AS + // (SELECT + // /* unnestPart */ + // unnest(?::type1[]), /* field1 */ + // unnest(?::type2[]), /* field2 */ + // ... + // ) + // INSERT INTO table ( + // /* insertFieldsPart */ + // field1, + // field2, + // ... + // ) + // SELECT * FROM rows ON CONFLICT (field1, field2, ...) DO NOTHING RETURNING * + // + // Using unnest allows to get around the maximum limit of 65,535 query parameters, + // see https://www.postgresql.org/docs/12/limits.html and + // https://klotzandrew.com/blog/postgres-passing-65535-parameter-limit/ + // + // Without using unnest we would have to use multiple insert statements to insert + // all the rows for large datasets. for _, field := range fields { unnestPart = append( unnestPart, @@ -200,21 +274,69 @@ func bulkInsert(ctx context.Context, q *Q, table string, conflictFields []string pq.Array(field.objects), ) } + columns := strings.Join(insertFieldsPart, ",") sql := ` - WITH r AS + WITH rows AS (SELECT ` + strings.Join(unnestPart, ",") + `) INSERT INTO ` + table + ` - (` + strings.Join(insertFieldsPart, ",") + `) - SELECT * from r - ON CONFLICT (` + strings.Join(conflictFields, ",") + `) DO NOTHING` + (` + columns + `) + SELECT * FROM rows + ON CONFLICT (` + columns + `) DO NOTHING + RETURNING *` + + return q.SelectRaw( + ctx, + response, + sql, + pqArrays..., + ) +} + +func bulkGet(ctx context.Context, q *Q, table string, fields []columnValues, response interface{}) error { + unnestPart := make([]string, 0, len(fields)) + columns := make([]string, 0, len(fields)) + pqArrays := make([]interface{}, 0, len(fields)) + + // In the code below we are building the bulk get query which looks like: + // + // SELECT * FROM table WHERE (field1, field2, ...) IN + // (SELECT + // /* unnestPart */ + // unnest(?::type1[]), /* field1 */ + // unnest(?::type2[]), /* field2 */ + // ... + // ) + // + // Using unnest allows to get around the maximum limit of 65,535 query parameters, + // see https://www.postgresql.org/docs/12/limits.html and + // https://klotzandrew.com/blog/postgres-passing-65535-parameter-limit/ + // + // Without using unnest we would have to use multiple select statements to obtain + // all the rows for large datasets. + for _, field := range fields { + unnestPart = append( + unnestPart, + fmt.Sprintf("unnest(?::%s[]) /* %s */", field.dbType, field.name), + ) + columns = append( + columns, + field.name, + ) + pqArrays = append( + pqArrays, + pq.Array(field.objects), + ) + } + sql := `SELECT * FROM ` + table + ` WHERE (` + strings.Join(columns, ",") + `) IN + (SELECT ` + strings.Join(unnestPart, ",") + `)` - _, err := q.ExecRaw( - context.WithValue(ctx, &db.QueryTypeContextKey, db.UpsertQueryType), + return q.SelectRaw( + ctx, + response, sql, pqArrays..., ) - return err } // AccountLoaderStub is a stub wrapper around AccountLoader which allows diff --git a/services/horizon/internal/db2/history/account_loader_test.go b/services/horizon/internal/db2/history/account_loader_test.go index ed30b43bd9..9a9fb30445 100644 --- a/services/horizon/internal/db2/history/account_loader_test.go +++ b/services/horizon/internal/db2/history/account_loader_test.go @@ -26,7 +26,7 @@ func TestAccountLoader(t *testing.T) { future := loader.GetFuture(address) _, err := future.Value() assert.Error(t, err) - assert.Contains(t, err.Error(), `invalid account loader state,`) + assert.Contains(t, err.Error(), `invalid loader state,`) duplicateFuture := loader.GetFuture(address) assert.Equal(t, future, duplicateFuture) } @@ -55,4 +55,35 @@ func TestAccountLoader(t *testing.T) { _, err = loader.GetNow("not present") assert.Error(t, err) assert.Contains(t, err.Error(), `was not found`) + + // check that Loader works when all the previous values are already + // present in the db and also add 10 more rows to insert + loader = NewAccountLoader() + for i := 0; i < 10; i++ { + addresses = append(addresses, keypair.MustRandom().Address()) + } + + for _, address := range addresses { + future := loader.GetFuture(address) + _, err = future.Value() + assert.Error(t, err) + assert.Contains(t, err.Error(), `invalid loader state,`) + } + + assert.NoError(t, loader.Exec(context.Background(), session)) + assert.Equal(t, LoaderStats{ + Total: 110, + Inserted: 10, + }, loader.Stats()) + + for _, address := range addresses { + var internalId int64 + internalId, err = loader.GetNow(address) + assert.NoError(t, err) + var account Account + assert.NoError(t, q.AccountByAddress(context.Background(), &account, address)) + assert.Equal(t, account.ID, internalId) + assert.Equal(t, account.Address, address) + } + } diff --git a/services/horizon/internal/db2/history/asset_loader.go b/services/horizon/internal/db2/history/asset_loader.go index fe17dc17be..cdd2a0d714 100644 --- a/services/horizon/internal/db2/history/asset_loader.go +++ b/services/horizon/internal/db2/history/asset_loader.go @@ -1,16 +1,9 @@ package history import ( - "context" - "database/sql/driver" - "fmt" - "sort" "strings" "github.com/stellar/go/support/collections/set" - "github.com/stellar/go/support/db" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/ordered" "github.com/stellar/go/xdr" ) @@ -40,26 +33,13 @@ func AssetKeyFromXDR(asset xdr.Asset) AssetKey { // A FutureAssetID is created by an AssetLoader and // the asset id is available after calling Exec() on // the AssetLoader. -type FutureAssetID struct { - asset AssetKey - loader *AssetLoader -} - -// Value implements the database/sql/driver Valuer interface. -func (a FutureAssetID) Value() (driver.Value, error) { - return a.loader.GetNow(a.asset) -} +type FutureAssetID = future[AssetKey, Asset] // AssetLoader will map assets to their history // asset ids. If there is no existing mapping for a given sset, // the AssetLoader will insert into the history_assets table to // establish a mapping. -type AssetLoader struct { - sealed bool - set set.Set[AssetKey] - ids map[AssetKey]int64 - stats LoaderStats -} +type AssetLoader = loader[AssetKey, Asset] // NewAssetLoader will construct a new AssetLoader instance. func NewAssetLoader() *AssetLoader { @@ -68,152 +48,47 @@ func NewAssetLoader() *AssetLoader { set: set.Set[AssetKey]{}, ids: map[AssetKey]int64{}, stats: LoaderStats{}, - } -} - -// GetFuture registers the given asset into the loader and -// returns a FutureAssetID which will hold the history asset id for -// the asset after Exec() is called. -func (a *AssetLoader) GetFuture(asset AssetKey) FutureAssetID { - if a.sealed { - panic(errSealed) - } - a.set.Add(asset) - return FutureAssetID{ - asset: asset, - loader: a, - } -} - -// GetNow returns the history asset id for the given asset. -// GetNow should only be called on values which were registered by -// GetFuture() calls. Also, Exec() must be called before any GetNow -// call can succeed. -func (a *AssetLoader) GetNow(asset AssetKey) (int64, error) { - if !a.sealed { - return 0, fmt.Errorf(`invalid asset loader state, - Exec was not called yet to properly seal and resolve %v id`, asset) - } - if internalID, ok := a.ids[asset]; !ok { - return 0, fmt.Errorf(`asset loader id %v was not found`, asset) - } else { - return internalID, nil - } -} - -func (a *AssetLoader) lookupKeys(ctx context.Context, q *Q, keys []AssetKey) error { - var rows []Asset - for i := 0; i < len(keys); i += loaderLookupBatchSize { - end := ordered.Min(len(keys), i+loaderLookupBatchSize) - subset := keys[i:end] - args := make([]interface{}, 0, 3*len(subset)) - placeHolders := make([]string, 0, len(subset)) - for _, key := range subset { - args = append(args, key.Code, key.Type, key.Issuer) - placeHolders = append(placeHolders, "(?, ?, ?)") - } - rawSQL := fmt.Sprintf( - "SELECT * FROM history_assets WHERE (asset_code, asset_type, asset_issuer) in (%s)", - strings.Join(placeHolders, ", "), - ) - err := q.SelectRaw(ctx, &rows, rawSQL, args...) - if err != nil { - return errors.Wrap(err, "could not select assets") - } - - for _, row := range rows { - a.ids[AssetKey{ - Type: row.Type, - Code: row.Code, - Issuer: row.Issuer, - }] = row.ID - } - } - return nil -} - -// Exec will look up all the history asset ids for the assets registered in the loader. -// If there are no history asset ids for a given set of assets, Exec will insert rows -// into the history_assets table. -func (a *AssetLoader) Exec(ctx context.Context, session db.SessionInterface) error { - a.sealed = true - if len(a.set) == 0 { - return nil - } - q := &Q{session} - keys := make([]AssetKey, 0, len(a.set)) - for key := range a.set { - keys = append(keys, key) - } - - if err := a.lookupKeys(ctx, q, keys); err != nil { - return err - } - a.stats.Total += len(keys) - - assetTypes := make([]string, 0, len(a.set)-len(a.ids)) - assetCodes := make([]string, 0, len(a.set)-len(a.ids)) - assetIssuers := make([]string, 0, len(a.set)-len(a.ids)) - // sort entries before inserting rows to prevent deadlocks on acquiring a ShareLock - // https://github.com/stellar/go/issues/2370 - sort.Slice(keys, func(i, j int) bool { - return keys[i].String() < keys[j].String() - }) - insert := 0 - for _, key := range keys { - if _, ok := a.ids[key]; ok { - continue - } - assetTypes = append(assetTypes, key.Type) - assetCodes = append(assetCodes, key.Code) - assetIssuers = append(assetIssuers, key.Issuer) - keys[insert] = key - insert++ - } - if insert == 0 { - return nil - } - keys = keys[:insert] - - err := bulkInsert( - ctx, - q, - "history_assets", - []string{"asset_code", "asset_type", "asset_issuer"}, - []bulkInsertField{ - { - name: "asset_code", - dbType: "character varying(12)", - objects: assetCodes, - }, - { - name: "asset_issuer", - dbType: "character varying(56)", - objects: assetIssuers, - }, - { - name: "asset_type", - dbType: "character varying(64)", - objects: assetTypes, - }, + name: "AssetLoader", + table: "history_assets", + columnsForKeys: func(keys []AssetKey) []columnValues { + assetTypes := make([]string, 0, len(keys)) + assetCodes := make([]string, 0, len(keys)) + assetIssuers := make([]string, 0, len(keys)) + for _, key := range keys { + assetTypes = append(assetTypes, key.Type) + assetCodes = append(assetCodes, key.Code) + assetIssuers = append(assetIssuers, key.Issuer) + } + + return []columnValues{ + { + name: "asset_code", + dbType: "character varying(12)", + objects: assetCodes, + }, + { + name: "asset_type", + dbType: "character varying(64)", + objects: assetTypes, + }, + { + name: "asset_issuer", + dbType: "character varying(56)", + objects: assetIssuers, + }, + } + }, + mappingFromRow: func(asset Asset) (AssetKey, int64) { + return AssetKey{ + Type: asset.Type, + Code: asset.Code, + Issuer: asset.Issuer, + }, asset.ID + }, + less: func(a AssetKey, b AssetKey) bool { + return a.String() < b.String() }, - ) - if err != nil { - return err } - a.stats.Inserted += insert - - return a.lookupKeys(ctx, q, keys) -} - -// Stats returns the number of assets registered in the loader and the number of assets -// inserted into the history_assets table. -func (a *AssetLoader) Stats() LoaderStats { - return a.stats -} - -func (a *AssetLoader) Name() string { - return "AssetLoader" } // AssetLoaderStub is a stub wrapper around AssetLoader which allows diff --git a/services/horizon/internal/db2/history/asset_loader_test.go b/services/horizon/internal/db2/history/asset_loader_test.go index f097561e4a..ca65cebb7e 100644 --- a/services/horizon/internal/db2/history/asset_loader_test.go +++ b/services/horizon/internal/db2/history/asset_loader_test.go @@ -71,7 +71,7 @@ func TestAssetLoader(t *testing.T) { future := loader.GetFuture(key) _, err := future.Value() assert.Error(t, err) - assert.Contains(t, err.Error(), `invalid asset loader state,`) + assert.Contains(t, err.Error(), `invalid loader state,`) duplicateFuture := loader.GetFuture(key) assert.Equal(t, future, duplicateFuture) } @@ -106,4 +106,58 @@ func TestAssetLoader(t *testing.T) { _, err = loader.GetNow(AssetKey{}) assert.Error(t, err) assert.Contains(t, err.Error(), `was not found`) + + // check that Loader works when all the previous values are already + // present in the db and also add 10 more rows to insert + loader = NewAssetLoader() + for i := 0; i < 10; i++ { + var key AssetKey + if i%2 == 0 { + code := [4]byte{0, 0, 0, 0} + copy(code[:], fmt.Sprintf("ab%d", i)) + key = AssetKeyFromXDR(xdr.Asset{ + Type: xdr.AssetTypeAssetTypeCreditAlphanum4, + AlphaNum4: &xdr.AlphaNum4{ + AssetCode: code, + Issuer: xdr.MustAddress(keypair.MustRandom().Address())}}) + } else { + code := [12]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + copy(code[:], fmt.Sprintf("abcdef%d", i)) + key = AssetKeyFromXDR(xdr.Asset{ + Type: xdr.AssetTypeAssetTypeCreditAlphanum12, + AlphaNum12: &xdr.AlphaNum12{ + AssetCode: code, + Issuer: xdr.MustAddress(keypair.MustRandom().Address())}}) + + } + keys = append(keys, key) + } + + for _, key := range keys { + future := loader.GetFuture(key) + _, err = future.Value() + assert.Error(t, err) + assert.Contains(t, err.Error(), `invalid loader state,`) + } + assert.NoError(t, loader.Exec(context.Background(), session)) + assert.Equal(t, LoaderStats{ + Total: 110, + Inserted: 10, + }, loader.Stats()) + + for _, key := range keys { + var internalID int64 + internalID, err = loader.GetNow(key) + assert.NoError(t, err) + var assetXDR xdr.Asset + if key.Type == "native" { + assetXDR = xdr.MustNewNativeAsset() + } else { + assetXDR = xdr.MustNewCreditAsset(key.Code, key.Issuer) + } + var assetID int64 + assetID, err = q.GetAssetID(context.Background(), assetXDR) + assert.NoError(t, err) + assert.Equal(t, assetID, internalID) + } } diff --git a/services/horizon/internal/db2/history/claimable_balance_loader.go b/services/horizon/internal/db2/history/claimable_balance_loader.go index ef18683cb6..f775ea4b24 100644 --- a/services/horizon/internal/db2/history/claimable_balance_loader.go +++ b/services/horizon/internal/db2/history/claimable_balance_loader.go @@ -1,41 +1,22 @@ package history import ( - "context" - "database/sql/driver" - "fmt" - "sort" + "cmp" "github.com/stellar/go/support/collections/set" - "github.com/stellar/go/support/db" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/ordered" ) // FutureClaimableBalanceID represents a future history claimable balance. // A FutureClaimableBalanceID is created by a ClaimableBalanceLoader and // the claimable balance id is available after calling Exec() on // the ClaimableBalanceLoader. -type FutureClaimableBalanceID struct { - id string - loader *ClaimableBalanceLoader -} - -// Value implements the database/sql/driver Valuer interface. -func (a FutureClaimableBalanceID) Value() (driver.Value, error) { - return a.loader.getNow(a.id) -} +type FutureClaimableBalanceID = future[string, HistoryClaimableBalance] // ClaimableBalanceLoader will map claimable balance ids to their internal // history ids. If there is no existing mapping for a given claimable balance id, // the ClaimableBalanceLoader will insert into the history_claimable_balances table to // establish a mapping. -type ClaimableBalanceLoader struct { - sealed bool - set set.Set[string] - ids map[string]int64 - stats LoaderStats -} +type ClaimableBalanceLoader = loader[string, HistoryClaimableBalance] // NewClaimableBalanceLoader will construct a new ClaimableBalanceLoader instance. func NewClaimableBalanceLoader() *ClaimableBalanceLoader { @@ -44,118 +25,20 @@ func NewClaimableBalanceLoader() *ClaimableBalanceLoader { set: set.Set[string]{}, ids: map[string]int64{}, stats: LoaderStats{}, - } -} - -// GetFuture registers the given claimable balance into the loader and -// returns a FutureClaimableBalanceID which will hold the internal history id for -// the claimable balance after Exec() is called. -func (a *ClaimableBalanceLoader) GetFuture(id string) FutureClaimableBalanceID { - if a.sealed { - panic(errSealed) - } - - a.set.Add(id) - return FutureClaimableBalanceID{ - id: id, - loader: a, - } -} - -// getNow returns the internal history id for the given claimable balance. -// getNow should only be called on values which were registered by -// GetFuture() calls. Also, Exec() must be called before any getNow -// call can succeed. -func (a *ClaimableBalanceLoader) getNow(id string) (int64, error) { - if !a.sealed { - return 0, fmt.Errorf(`invalid claimable balance loader state, - Exec was not called yet to properly seal and resolve %v id`, id) - } - if internalID, ok := a.ids[id]; !ok { - return 0, fmt.Errorf(`claimable balance loader id %q was not found`, id) - } else { - return internalID, nil - } -} - -func (a *ClaimableBalanceLoader) lookupKeys(ctx context.Context, q *Q, ids []string) error { - for i := 0; i < len(ids); i += loaderLookupBatchSize { - end := ordered.Min(len(ids), i+loaderLookupBatchSize) - - cbs, err := q.ClaimableBalancesByIDs(ctx, ids[i:end]) - if err != nil { - return errors.Wrap(err, "could not select claimable balances") - } - - for _, cb := range cbs { - a.ids[cb.BalanceID] = cb.InternalID - } - } - return nil -} - -// Exec will look up all the internal history ids for the claimable balances registered in the loader. -// If there are no internal ids for a given set of claimable balances, Exec will insert rows -// into the history_claimable_balances table. -func (a *ClaimableBalanceLoader) Exec(ctx context.Context, session db.SessionInterface) error { - a.sealed = true - if len(a.set) == 0 { - return nil - } - q := &Q{session} - ids := make([]string, 0, len(a.set)) - for id := range a.set { - ids = append(ids, id) - } - - if err := a.lookupKeys(ctx, q, ids); err != nil { - return err - } - a.stats.Total += len(ids) - - insert := 0 - for _, id := range ids { - if _, ok := a.ids[id]; ok { - continue - } - ids[insert] = id - insert++ - } - if insert == 0 { - return nil - } - ids = ids[:insert] - // sort entries before inserting rows to prevent deadlocks on acquiring a ShareLock - // https://github.com/stellar/go/issues/2370 - sort.Strings(ids) - - err := bulkInsert( - ctx, - q, - "history_claimable_balances", - []string{"claimable_balance_id"}, - []bulkInsertField{ - { - name: "claimable_balance_id", - dbType: "text", - objects: ids, - }, + name: "ClaimableBalanceLoader", + table: "history_claimable_balances", + columnsForKeys: func(keys []string) []columnValues { + return []columnValues{ + { + name: "claimable_balance_id", + dbType: "text", + objects: keys, + }, + } }, - ) - if err != nil { - return err + mappingFromRow: func(row HistoryClaimableBalance) (string, int64) { + return row.BalanceID, row.InternalID + }, + less: cmp.Less[string], } - a.stats.Inserted += insert - - return a.lookupKeys(ctx, q, ids) -} - -// Stats returns the number of claimable balances registered in the loader and the number of claimable balances -// inserted into the history_claimable_balances table. -func (a *ClaimableBalanceLoader) Stats() LoaderStats { - return a.stats -} - -func (a *ClaimableBalanceLoader) Name() string { - return "ClaimableBalanceLoader" } diff --git a/services/horizon/internal/db2/history/claimable_balance_loader_test.go b/services/horizon/internal/db2/history/claimable_balance_loader_test.go index aaf91ccdcc..f5759015c7 100644 --- a/services/horizon/internal/db2/history/claimable_balance_loader_test.go +++ b/services/horizon/internal/db2/history/claimable_balance_loader_test.go @@ -35,7 +35,7 @@ func TestClaimableBalanceLoader(t *testing.T) { futures = append(futures, future) _, err := future.Value() assert.Error(t, err) - assert.Contains(t, err.Error(), `invalid claimable balance loader state,`) + assert.Contains(t, err.Error(), `invalid loader state,`) duplicateFuture := loader.GetFuture(id) assert.Equal(t, future, duplicateFuture) } @@ -63,8 +63,45 @@ func TestClaimableBalanceLoader(t *testing.T) { assert.Equal(t, cb.InternalID, internalID) } - futureCb := &FutureClaimableBalanceID{id: "not-present", loader: loader} + futureCb := &FutureClaimableBalanceID{key: "not-present", loader: loader} _, err = futureCb.Value() assert.Error(t, err) assert.Contains(t, err.Error(), `was not found`) + + // check that Loader works when all the previous values are already + // present in the db and also add 10 more rows to insert + loader = NewClaimableBalanceLoader() + for i := 100; i < 110; i++ { + balanceID := xdr.ClaimableBalanceId{ + Type: xdr.ClaimableBalanceIdTypeClaimableBalanceIdTypeV0, + V0: &xdr.Hash{byte(i)}, + } + var id string + id, err = xdr.MarshalHex(balanceID) + tt.Assert.NoError(err) + ids = append(ids, id) + } + + for _, id := range ids { + future := loader.GetFuture(id) + _, err = future.Value() + assert.Error(t, err) + assert.Contains(t, err.Error(), `invalid loader state,`) + } + + assert.NoError(t, loader.Exec(context.Background(), session)) + assert.Equal(t, LoaderStats{ + Total: 110, + Inserted: 10, + }, loader.Stats()) + + for _, id := range ids { + internalID, err := loader.GetNow(id) + assert.NoError(t, err) + var cb HistoryClaimableBalance + cb, err = q.ClaimableBalanceByID(context.Background(), id) + assert.NoError(t, err) + assert.Equal(t, cb.BalanceID, id) + assert.Equal(t, cb.InternalID, internalID) + } } diff --git a/services/horizon/internal/db2/history/liquidity_pool_loader.go b/services/horizon/internal/db2/history/liquidity_pool_loader.go index d619fa3bb4..a03caaa988 100644 --- a/services/horizon/internal/db2/history/liquidity_pool_loader.go +++ b/services/horizon/internal/db2/history/liquidity_pool_loader.go @@ -1,41 +1,22 @@ package history import ( - "context" - "database/sql/driver" - "fmt" - "sort" + "cmp" "github.com/stellar/go/support/collections/set" - "github.com/stellar/go/support/db" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/ordered" ) // FutureLiquidityPoolID represents a future history liquidity pool. // A FutureLiquidityPoolID is created by an LiquidityPoolLoader and // the liquidity pool id is available after calling Exec() on // the LiquidityPoolLoader. -type FutureLiquidityPoolID struct { - id string - loader *LiquidityPoolLoader -} - -// Value implements the database/sql/driver Valuer interface. -func (a FutureLiquidityPoolID) Value() (driver.Value, error) { - return a.loader.GetNow(a.id) -} +type FutureLiquidityPoolID = future[string, HistoryLiquidityPool] // LiquidityPoolLoader will map liquidity pools to their internal // history ids. If there is no existing mapping for a given liquidity pool, // the LiquidityPoolLoader will insert into the history_liquidity_pools table to // establish a mapping. -type LiquidityPoolLoader struct { - sealed bool - set set.Set[string] - ids map[string]int64 - stats LoaderStats -} +type LiquidityPoolLoader = loader[string, HistoryLiquidityPool] // NewLiquidityPoolLoader will construct a new LiquidityPoolLoader instance. func NewLiquidityPoolLoader() *LiquidityPoolLoader { @@ -44,120 +25,22 @@ func NewLiquidityPoolLoader() *LiquidityPoolLoader { set: set.Set[string]{}, ids: map[string]int64{}, stats: LoaderStats{}, - } -} - -// GetFuture registers the given liquidity pool into the loader and -// returns a FutureLiquidityPoolID which will hold the internal history id for -// the liquidity pool after Exec() is called. -func (a *LiquidityPoolLoader) GetFuture(id string) FutureLiquidityPoolID { - if a.sealed { - panic(errSealed) - } - - a.set.Add(id) - return FutureLiquidityPoolID{ - id: id, - loader: a, - } -} - -// GetNow returns the internal history id for the given liquidity pool. -// GetNow should only be called on values which were registered by -// GetFuture() calls. Also, Exec() must be called before any GetNow -// call can succeed. -func (a *LiquidityPoolLoader) GetNow(id string) (int64, error) { - if !a.sealed { - return 0, fmt.Errorf(`invalid liquidity pool loader state, - Exec was not called yet to properly seal and resolve %v id`, id) - } - if internalID, ok := a.ids[id]; !ok { - return 0, fmt.Errorf(`liquidity pool loader id %q was not found`, id) - } else { - return internalID, nil - } -} - -func (a *LiquidityPoolLoader) lookupKeys(ctx context.Context, q *Q, ids []string) error { - for i := 0; i < len(ids); i += loaderLookupBatchSize { - end := ordered.Min(len(ids), i+loaderLookupBatchSize) - - lps, err := q.LiquidityPoolsByIDs(ctx, ids[i:end]) - if err != nil { - return errors.Wrap(err, "could not select accounts") - } - - for _, lp := range lps { - a.ids[lp.PoolID] = lp.InternalID - } - } - return nil -} - -// Exec will look up all the internal history ids for the liquidity pools registered in the loader. -// If there are no internal history ids for a given set of liquidity pools, Exec will insert rows -// into the history_liquidity_pools table. -func (a *LiquidityPoolLoader) Exec(ctx context.Context, session db.SessionInterface) error { - a.sealed = true - if len(a.set) == 0 { - return nil - } - q := &Q{session} - ids := make([]string, 0, len(a.set)) - for id := range a.set { - ids = append(ids, id) - } - - if err := a.lookupKeys(ctx, q, ids); err != nil { - return err - } - a.stats.Total += len(ids) - - insert := 0 - for _, id := range ids { - if _, ok := a.ids[id]; ok { - continue - } - ids[insert] = id - insert++ - } - if insert == 0 { - return nil - } - ids = ids[:insert] - // sort entries before inserting rows to prevent deadlocks on acquiring a ShareLock - // https://github.com/stellar/go/issues/2370 - sort.Strings(ids) - - err := bulkInsert( - ctx, - q, - "history_liquidity_pools", - []string{"liquidity_pool_id"}, - []bulkInsertField{ - { - name: "liquidity_pool_id", - dbType: "text", - objects: ids, - }, + name: "LiquidityPoolLoader", + table: "history_liquidity_pools", + columnsForKeys: func(keys []string) []columnValues { + return []columnValues{ + { + name: "liquidity_pool_id", + dbType: "text", + objects: keys, + }, + } }, - ) - if err != nil { - return err + mappingFromRow: func(row HistoryLiquidityPool) (string, int64) { + return row.PoolID, row.InternalID + }, + less: cmp.Less[string], } - a.stats.Inserted += insert - - return a.lookupKeys(ctx, q, ids) -} - -// Stats returns the number of liquidity pools registered in the loader and the number of liquidity pools -// inserted into the history_liquidity_pools table. -func (a *LiquidityPoolLoader) Stats() LoaderStats { - return a.stats -} - -func (a *LiquidityPoolLoader) Name() string { - return "LiquidityPoolLoader" } // LiquidityPoolLoaderStub is a stub wrapper around LiquidityPoolLoader which allows diff --git a/services/horizon/internal/db2/history/liquidity_pool_loader_test.go b/services/horizon/internal/db2/history/liquidity_pool_loader_test.go index 25ca80826c..aec2fcd886 100644 --- a/services/horizon/internal/db2/history/liquidity_pool_loader_test.go +++ b/services/horizon/internal/db2/history/liquidity_pool_loader_test.go @@ -29,7 +29,7 @@ func TestLiquidityPoolLoader(t *testing.T) { future := loader.GetFuture(id) _, err := future.Value() assert.Error(t, err) - assert.Contains(t, err.Error(), `invalid liquidity pool loader state,`) + assert.Contains(t, err.Error(), `invalid loader state,`) duplicateFuture := loader.GetFuture(id) assert.Equal(t, future, duplicateFuture) } @@ -59,4 +59,39 @@ func TestLiquidityPoolLoader(t *testing.T) { _, err = loader.GetNow("not present") assert.Error(t, err) assert.Contains(t, err.Error(), `was not found`) + + // check that Loader works when all the previous values are already + // present in the db and also add 10 more rows to insert + loader = NewLiquidityPoolLoader() + for i := 100; i < 110; i++ { + poolID := xdr.PoolId{byte(i)} + var id string + id, err = xdr.MarshalHex(poolID) + tt.Assert.NoError(err) + ids = append(ids, id) + } + + for _, id := range ids { + future := loader.GetFuture(id) + _, err = future.Value() + assert.Error(t, err) + assert.Contains(t, err.Error(), `invalid loader state,`) + } + + assert.NoError(t, loader.Exec(context.Background(), session)) + assert.Equal(t, LoaderStats{ + Total: 110, + Inserted: 10, + }, loader.Stats()) + + for _, id := range ids { + var internalID int64 + internalID, err = loader.GetNow(id) + assert.NoError(t, err) + var lp HistoryLiquidityPool + lp, err = q.LiquidityPoolByID(context.Background(), id) + assert.NoError(t, err) + assert.Equal(t, lp.PoolID, id) + assert.Equal(t, lp.InternalID, internalID) + } } From f10ea0f79143c1b5594f5a91c5aeb4a8ae39f2cc Mon Sep 17 00:00:00 2001 From: George Date: Tue, 27 Aug 2024 13:24:25 -0700 Subject: [PATCH 224/234] ingest: Add verbosity to missing file error for `BufferedStorageBackend` (#5442) * Specify object key in 404 error message * Update tests to match new error message --- ingest/ledgerbackend/buffered_storage_backend_test.go | 9 +++++++-- ingest/ledgerbackend/ledger_buffer.go | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ingest/ledgerbackend/buffered_storage_backend_test.go b/ingest/ledgerbackend/buffered_storage_backend_test.go index ca2711c40d..6c95b465f7 100644 --- a/ingest/ledgerbackend/buffered_storage_backend_test.go +++ b/ingest/ledgerbackend/buffered_storage_backend_test.go @@ -505,7 +505,9 @@ func TestLedgerBufferBoundedObjectNotFound(t *testing.T) { bsb.ledgerBuffer.wg.Wait() _, err := bsb.GetLedger(ctx, 3) - assert.EqualError(t, err, "failed getting next ledger batch from queue: ledger object containing sequence 3 is missing: file does not exist") + assert.ErrorContains(t, err, "ledger object containing sequence 3 is missing") + assert.ErrorContains(t, err, objectName) + assert.ErrorContains(t, err, "file does not exist") } func TestLedgerBufferUnboundedObjectNotFound(t *testing.T) { @@ -571,5 +573,8 @@ func TestLedgerBufferRetryLimit(t *testing.T) { bsb.ledgerBuffer.wg.Wait() _, err := bsb.GetLedger(context.Background(), 3) - assert.EqualError(t, err, "failed getting next ledger batch from queue: maximum retries exceeded for downloading object containing sequence 3: transient error") + assert.ErrorContains(t, err, "failed getting next ledger batch from queue") + assert.ErrorContains(t, err, "maximum retries exceeded for downloading object containing sequence 3") + assert.ErrorContains(t, err, objectName) + assert.ErrorContains(t, err, "transient error") } diff --git a/ingest/ledgerbackend/ledger_buffer.go b/ingest/ledgerbackend/ledger_buffer.go index 6965461bba..d23bf0bfbd 100644 --- a/ingest/ledgerbackend/ledger_buffer.go +++ b/ingest/ledgerbackend/ledger_buffer.go @@ -167,7 +167,7 @@ func (lb *ledgerBuffer) downloadLedgerObject(ctx context.Context, sequence uint3 reader, err := lb.dataStore.GetFile(ctx, objectKey) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "unable to retrieve file: %s", objectKey) } defer reader.Close() From 61bbeb8124724ff30318d6e4348a26787b8033ad Mon Sep 17 00:00:00 2001 From: tamirms Date: Tue, 3 Sep 2024 08:02:19 +0100 Subject: [PATCH 225/234] ingest/ledgerbackend: Add prometheus metrics to track captive core startup time (#5449) --- ingest/ledgerbackend/captive_core_backend.go | 40 +++++++-- .../captive_core_backend_test.go | 81 ++++++++++++------- ingest/ledgerbackend/file_watcher_test.go | 4 +- ingest/ledgerbackend/run_from.go | 33 +++++--- ingest/ledgerbackend/stellar_core_runner.go | 11 ++- .../ledgerbackend/stellar_core_runner_test.go | 36 +++++++-- 6 files changed, 146 insertions(+), 59 deletions(-) diff --git a/ingest/ledgerbackend/captive_core_backend.go b/ingest/ledgerbackend/captive_core_backend.go index c8f28974f5..dc5365809b 100644 --- a/ingest/ledgerbackend/captive_core_backend.go +++ b/ingest/ledgerbackend/captive_core_backend.go @@ -112,9 +112,11 @@ type CaptiveStellarCore struct { lastLedger *uint32 // end of current segment if offline, nil if online previousLedgerHash *string - config CaptiveCoreConfig - stellarCoreClient *stellarcore.Client - captiveCoreVersion string // Updates when captive-core restarts + config CaptiveCoreConfig + captiveCoreStartDuration prometheus.Summary + captiveCoreNewDBCounter prometheus.Counter + stellarCoreClient *stellarcore.Client + captiveCoreVersion string // Updates when captive-core restarts } // CaptiveCoreConfig contains all the parameters required to create a CaptiveStellarCore instance @@ -230,7 +232,7 @@ func NewCaptive(config CaptiveCoreConfig) (*CaptiveStellarCore, error) { c.stellarCoreRunnerFactory = func() stellarCoreRunnerInterface { c.setCoreVersion() - return newStellarCoreRunner(config) + return newStellarCoreRunner(config, c.captiveCoreNewDBCounter) } if config.Toml != nil && config.Toml.HTTPPort != 0 { @@ -315,7 +317,27 @@ func (c *CaptiveStellarCore) registerMetrics(registry *prometheus.Registry, name return float64(latest) }, ) - registry.MustRegister(coreSynced, supportedProtocolVersion, latestLedger) + c.captiveCoreStartDuration = prometheus.NewSummary(prometheus.SummaryOpts{ + Namespace: namespace, + Subsystem: "ingest", + Name: "captive_stellar_core_start_duration_seconds", + Help: "duration of start up time when running captive core on an unbounded range, sliding window = 10m", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }) + c.captiveCoreNewDBCounter = prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: namespace, + Subsystem: "ingest", + Name: "captive_stellar_core_new_db", + Help: "counter for the number of times we start up captive core with a new buckets db, sliding window = 10m", + }) + + registry.MustRegister( + coreSynced, + supportedProtocolVersion, + latestLedger, + c.captiveCoreStartDuration, + c.captiveCoreNewDBCounter, + ) } func (c *CaptiveStellarCore) getLatestCheckpointSequence() (uint32, error) { @@ -521,12 +543,14 @@ func (c *CaptiveStellarCore) startPreparingRange(ctx context.Context, ledgerRang // Please note that using a BoundedRange, currently, requires a full-trust on // history archive. This issue is being fixed in Stellar-Core. func (c *CaptiveStellarCore) PrepareRange(ctx context.Context, ledgerRange Range) error { + startTime := time.Now() if alreadyPrepared, err := c.startPreparingRange(ctx, ledgerRange); err != nil { return errors.Wrap(err, "error starting prepare range") } else if alreadyPrepared { return nil } + var reportedStartTime bool // the prepared range might be below ledgerRange.from so we // need to seek ahead until we reach ledgerRange.from for seq := c.prepared.from; seq <= ledgerRange.from; seq++ { @@ -534,6 +558,12 @@ func (c *CaptiveStellarCore) PrepareRange(ctx context.Context, ledgerRange Range if err != nil { return errors.Wrapf(err, "Error fast-forwarding to %d", ledgerRange.from) } + if !reportedStartTime { + reportedStartTime = true + if c.captiveCoreStartDuration != nil && !ledgerRange.bounded { + c.captiveCoreStartDuration.Observe(time.Since(startTime).Seconds()) + } + } } return nil diff --git a/ingest/ledgerbackend/captive_core_backend_test.go b/ingest/ledgerbackend/captive_core_backend_test.go index f8161aec25..cb7e8dba7e 100644 --- a/ingest/ledgerbackend/captive_core_backend_test.go +++ b/ingest/ledgerbackend/captive_core_backend_test.go @@ -10,6 +10,8 @@ import ( "sync" "testing" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -224,7 +226,7 @@ func TestCaptivePrepareRange(t *testing.T) { }, nil) cancelCalled := false - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -234,6 +236,7 @@ func TestCaptivePrepareRange(t *testing.T) { cancelCalled = true }), } + captiveBackend.registerMetrics(prometheus.NewRegistry(), "test") err := captiveBackend.PrepareRange(ctx, BoundedRange(100, 200)) assert.NoError(t, err) @@ -243,6 +246,8 @@ func TestCaptivePrepareRange(t *testing.T) { assert.True(t, cancelCalled) mockRunner.AssertExpectations(t) mockArchive.AssertExpectations(t) + + assert.Equal(t, uint64(0), getStartDurationMetric(captiveBackend).GetSampleCount()) } func TestCaptivePrepareRangeCrash(t *testing.T) { @@ -263,7 +268,7 @@ func TestCaptivePrepareRangeCrash(t *testing.T) { CurrentLedger: uint32(200), }, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -302,7 +307,7 @@ func TestCaptivePrepareRangeTerminated(t *testing.T) { CurrentLedger: uint32(200), }, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -339,7 +344,7 @@ func TestCaptivePrepareRangeCloseNotFullyTerminated(t *testing.T) { CurrentLedger: uint32(200), }, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -367,7 +372,7 @@ func TestCaptivePrepareRange_ErrClosingSession(t *testing.T) { mockRunner.On("getProcessExitError").Return(nil, false) mockRunner.On("context").Return(ctx) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ nextLedger: 300, stellarCoreRunner: mockRunner, } @@ -388,7 +393,7 @@ func TestCaptivePrepareRange_ErrGettingRootHAS(t *testing.T) { On("GetRootHAS"). Return(historyarchive.HistoryArchiveState{}, errors.New("transient error")) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, } @@ -412,7 +417,7 @@ func TestCaptivePrepareRange_FromIsAheadOfRootHAS(t *testing.T) { CurrentLedger: uint32(64), }, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -460,7 +465,7 @@ func TestCaptivePrepareRangeWithDB_FromIsAheadOfRootHAS(t *testing.T) { CurrentLedger: uint32(64), }, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, useDB: true, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { @@ -499,7 +504,7 @@ func TestCaptivePrepareRange_ToIsAheadOfRootHAS(t *testing.T) { CurrentLedger: uint32(192), }, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -527,7 +532,7 @@ func TestCaptivePrepareRange_ErrCatchup(t *testing.T) { ctx := context.Background() cancelCalled := false - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -565,7 +570,7 @@ func TestCaptivePrepareRangeUnboundedRange_ErrRunFrom(t *testing.T) { ctx := context.Background() cancelCalled := false - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -575,10 +580,13 @@ func TestCaptivePrepareRangeUnboundedRange_ErrRunFrom(t *testing.T) { cancelCalled = true }), } + captiveBackend.registerMetrics(prometheus.NewRegistry(), "test") err := captiveBackend.PrepareRange(ctx, UnboundedRange(128)) assert.EqualError(t, err, "error starting prepare range: opening subprocess: error running stellar-core: transient error") + assert.Equal(t, uint64(0), getStartDurationMetric(captiveBackend).GetSampleCount()) + // make sure we can Close without errors assert.NoError(t, captiveBackend.Close()) assert.True(t, cancelCalled) @@ -587,6 +595,15 @@ func TestCaptivePrepareRangeUnboundedRange_ErrRunFrom(t *testing.T) { mockRunner.AssertExpectations(t) } +func getStartDurationMetric(captiveCore *CaptiveStellarCore) *dto.Summary { + value := &dto.Metric{} + err := captiveCore.captiveCoreStartDuration.Write(value) + if err != nil { + panic(err) + } + return value.GetSummary() +} + func TestCaptivePrepareRangeUnboundedRange_ReuseSession(t *testing.T) { metaChan := make(chan metaResult, 100) @@ -617,21 +634,27 @@ func TestCaptivePrepareRangeUnboundedRange_ReuseSession(t *testing.T) { On("GetLedgerHeader", uint32(65)). Return(xdr.LedgerHeaderHistoryEntry{}, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner }, checkpointManager: historyarchive.NewCheckpointManager(64), } + captiveBackend.registerMetrics(prometheus.NewRegistry(), "test") err := captiveBackend.PrepareRange(ctx, UnboundedRange(65)) assert.NoError(t, err) + assert.Equal(t, uint64(1), getStartDurationMetric(captiveBackend).GetSampleCount()) + assert.Greater(t, getStartDurationMetric(captiveBackend).GetSampleSum(), float64(0)) + captiveBackend.nextLedger = 64 err = captiveBackend.PrepareRange(ctx, UnboundedRange(65)) assert.NoError(t, err) + assert.Equal(t, uint64(1), getStartDurationMetric(captiveBackend).GetSampleCount()) + mockArchive.AssertExpectations(t) mockRunner.AssertExpectations(t) } @@ -665,7 +688,7 @@ func TestGetLatestLedgerSequence(t *testing.T) { On("GetLedgerHeader", uint32(64)). Return(xdr.LedgerHeaderHistoryEntry{}, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -712,7 +735,7 @@ func TestGetLatestLedgerSequenceRaceCondition(t *testing.T) { On("GetLedgerHeader", mock.Anything). Return(xdr.LedgerHeaderHistoryEntry{}, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -775,7 +798,7 @@ func TestCaptiveGetLedger(t *testing.T) { CurrentLedger: uint32(200), }, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -873,7 +896,7 @@ func TestCaptiveGetLedgerCacheLatestLedger(t *testing.T) { }, }, nil).Once() - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -928,7 +951,7 @@ func TestCaptiveGetLedger_NextLedgerIsDifferentToLedgerFromBuffer(t *testing.T) CurrentLedger: uint32(200), }, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -978,7 +1001,7 @@ func TestCaptiveGetLedger_NextLedger0RangeFromIsSmallerThanLedgerFromBuffer(t *t On("GetLedgerHeader", uint32(65)). Return(xdr.LedgerHeaderHistoryEntry{}, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -1081,7 +1104,7 @@ func TestCaptiveGetLedger_ErrReadingMetaResult(t *testing.T) { CurrentLedger: uint32(200), }, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -1134,7 +1157,7 @@ func TestCaptiveGetLedger_ErrClosingAfterLastLedger(t *testing.T) { CurrentLedger: uint32(200), }, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -1176,7 +1199,7 @@ func TestCaptiveAfterClose(t *testing.T) { CurrentLedger: uint32(200), }, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -1230,7 +1253,7 @@ func TestGetLedgerBoundsCheck(t *testing.T) { CurrentLedger: uint32(200), }, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -1356,7 +1379,7 @@ func TestCaptiveGetLedgerTerminatedUnexpectedly(t *testing.T) { CurrentLedger: uint32(200), }, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner @@ -1410,7 +1433,7 @@ func TestCaptiveUseOfLedgerHashStore(t *testing.T) { Return("mnb", true, nil).Once() cancelCalled := false - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, ledgerHashStore: mockLedgerHashStore, checkpointManager: historyarchive.NewCheckpointManager(64), @@ -1493,7 +1516,7 @@ func TestCaptiveRunFromParams(t *testing.T) { CurrentLedger: uint32(255), }, nil) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, checkpointManager: historyarchive.NewCheckpointManager(64), } @@ -1515,7 +1538,7 @@ func TestCaptiveIsPrepared(t *testing.T) { mockRunner.On("getProcessExitError").Return(nil, false) // c.prepared == nil - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ nextLedger: 0, } @@ -1548,7 +1571,7 @@ func TestCaptiveIsPrepared(t *testing.T) { for _, tc := range tests { t.Run(fmt.Sprintf("next_%d_last_%d_cached_%d_range_%v", tc.nextLedger, tc.lastLedger, tc.cachedLedger, tc.ledgerRange), func(t *testing.T) { - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ stellarCoreRunner: mockRunner, nextLedger: tc.nextLedger, prepared: &tc.preparedRange, @@ -1579,7 +1602,7 @@ func TestCaptiveIsPreparedCoreContextCancelled(t *testing.T) { mockRunner.On("getProcessExitError").Return(nil, false) rang := UnboundedRange(100) - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ nextLedger: 100, prepared: &rang, stellarCoreRunner: mockRunner, @@ -1650,7 +1673,7 @@ func TestCaptivePreviousLedgerCheck(t *testing.T) { mockLedgerHashStore.On("GetLedgerHash", ctx, uint32(299)). Return("", false, nil).Once() - captiveBackend := CaptiveStellarCore{ + captiveBackend := &CaptiveStellarCore{ archive: mockArchive, stellarCoreRunnerFactory: func() stellarCoreRunnerInterface { return mockRunner diff --git a/ingest/ledgerbackend/file_watcher_test.go b/ingest/ledgerbackend/file_watcher_test.go index 7e84bbfcf2..c3e85d643f 100644 --- a/ingest/ledgerbackend/file_watcher_test.go +++ b/ingest/ledgerbackend/file_watcher_test.go @@ -65,7 +65,7 @@ func createFWFixtures(t *testing.T) (*mockHash, *stellarCoreRunner, *fileWatcher Context: context.Background(), Toml: captiveCoreToml, StoragePath: storagePath, - }) + }, nil) fw, err := newFileWatcherWithOptions(runner, ms.hashFile, time.Millisecond) assert.NoError(t, err) @@ -96,7 +96,7 @@ func TestNewFileWatcherError(t *testing.T) { Context: context.Background(), Toml: captiveCoreToml, StoragePath: storagePath, - }) + }, nil) _, err = newFileWatcherWithOptions(runner, ms.hashFile, time.Millisecond) assert.EqualError(t, err, "could not hash captive core binary: test error") diff --git a/ingest/ledgerbackend/run_from.go b/ingest/ledgerbackend/run_from.go index 2d02322519..8a424da105 100644 --- a/ingest/ledgerbackend/run_from.go +++ b/ingest/ledgerbackend/run_from.go @@ -6,32 +6,36 @@ import ( "fmt" "runtime" + "github.com/prometheus/client_golang/prometheus" + "github.com/stellar/go/protocols/stellarcore" "github.com/stellar/go/support/log" ) type runFromStream struct { - dir workingDir - from uint32 - hash string - coreCmdFactory coreCmdFactory - log *log.Entry - useDB bool + dir workingDir + from uint32 + hash string + coreCmdFactory coreCmdFactory + log *log.Entry + useDB bool + captiveCoreNewDBCounter prometheus.Counter } -func newRunFromStream(r *stellarCoreRunner, from uint32, hash string) runFromStream { +func newRunFromStream(r *stellarCoreRunner, from uint32, hash string, captiveCoreNewDBCounter prometheus.Counter) runFromStream { // We only use ephemeral directories on windows because there is // no way to terminate captive core gracefully on windows. // Having an ephemeral directory ensures that it is wiped out // whenever we terminate captive core dir := newWorkingDir(r, runtime.GOOS == "windows") return runFromStream{ - dir: dir, - from: from, - hash: hash, - coreCmdFactory: newCoreCmdFactory(r, dir), - log: r.log, - useDB: r.useDB, + dir: dir, + from: from, + hash: hash, + coreCmdFactory: newCoreCmdFactory(r, dir), + log: r.log, + useDB: r.useDB, + captiveCoreNewDBCounter: captiveCoreNewDBCounter, } } @@ -79,6 +83,9 @@ func (s runFromStream) start(ctx context.Context) (cmd cmdI, captiveCorePipe pip } if createNewDB { + if s.captiveCoreNewDBCounter != nil { + s.captiveCoreNewDBCounter.Inc() + } if err = s.dir.remove(); err != nil { return nil, pipe{}, fmt.Errorf("error removing existing storage-dir contents: %w", err) } diff --git a/ingest/ledgerbackend/stellar_core_runner.go b/ingest/ledgerbackend/stellar_core_runner.go index 5245051dce..4f95e94f45 100644 --- a/ingest/ledgerbackend/stellar_core_runner.go +++ b/ingest/ledgerbackend/stellar_core_runner.go @@ -7,6 +7,8 @@ import ( "math/rand" "sync" + "github.com/prometheus/client_golang/prometheus" + "github.com/stellar/go/support/log" ) @@ -67,6 +69,8 @@ type stellarCoreRunner struct { toml *CaptiveCoreToml useDB bool + captiveCoreNewDBCounter prometheus.Counter + log *log.Entry } @@ -79,7 +83,7 @@ func createRandomHexString(n int) string { return string(b) } -func newStellarCoreRunner(config CaptiveCoreConfig) *stellarCoreRunner { +func newStellarCoreRunner(config CaptiveCoreConfig, captiveCoreNewDBCounter prometheus.Counter) *stellarCoreRunner { ctx, cancel := context.WithCancel(config.Context) runner := &stellarCoreRunner{ @@ -91,7 +95,8 @@ func newStellarCoreRunner(config CaptiveCoreConfig) *stellarCoreRunner { log: config.Log, toml: config.Toml, - systemCaller: realSystemCaller{}, + captiveCoreNewDBCounter: captiveCoreNewDBCounter, + systemCaller: realSystemCaller{}, } return runner @@ -104,7 +109,7 @@ func (r *stellarCoreRunner) context() context.Context { // runFrom executes the run command with a starting ledger on the captive core subprocess func (r *stellarCoreRunner) runFrom(from uint32, hash string) error { - return r.startMetaStream(newRunFromStream(r, from, hash)) + return r.startMetaStream(newRunFromStream(r, from, hash, r.captiveCoreNewDBCounter)) } // catchup executes the catchup command on the captive core subprocess diff --git a/ingest/ledgerbackend/stellar_core_runner_test.go b/ingest/ledgerbackend/stellar_core_runner_test.go index f53cd88328..06b9c85fce 100644 --- a/ingest/ledgerbackend/stellar_core_runner_test.go +++ b/ingest/ledgerbackend/stellar_core_runner_test.go @@ -7,6 +7,8 @@ import ( "testing" "time" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -26,7 +28,7 @@ func TestCloseOffline(t *testing.T) { Context: context.Background(), Toml: captiveCoreToml, StoragePath: "/tmp/captive-core", - }) + }, nil) cmdMock := simpleCommandMock() cmdMock.On("Wait").Return(nil) @@ -68,7 +70,7 @@ func TestCloseOnline(t *testing.T) { Context: context.Background(), Toml: captiveCoreToml, StoragePath: "/tmp/captive-core", - }) + }, nil) cmdMock := simpleCommandMock() cmdMock.On("Wait").Return(nil) @@ -112,7 +114,7 @@ func TestCloseOnlineWithError(t *testing.T) { Context: context.Background(), Toml: captiveCoreToml, StoragePath: "/tmp/captive-core", - }) + }, nil) cmdMock := simpleCommandMock() cmdMock.On("Wait").Return(errors.New("wait error")) @@ -166,7 +168,7 @@ func TestCloseConcurrency(t *testing.T) { Context: context.Background(), Toml: captiveCoreToml, StoragePath: "/tmp/captive-core", - }) + }, nil) cmdMock := simpleCommandMock() cmdMock.On("Wait").Return(errors.New("wait error")).WaitUntil(time.After(time.Millisecond * 300)) @@ -223,7 +225,7 @@ func TestRunFromUseDBLedgersMatch(t *testing.T) { Toml: captiveCoreToml, StoragePath: "/tmp/captive-core", UseDB: true, - }) + }, createNewDBCounter()) cmdMock := simpleCommandMock() cmdMock.On("Wait").Return(nil) @@ -263,6 +265,8 @@ func TestRunFromUseDBLedgersMatch(t *testing.T) { assert.NoError(t, runner.runFrom(100, "hash")) assert.NoError(t, runner.close()) + + assert.Equal(t, float64(0), getNewDBCounterMetric(runner)) } func TestRunFromUseDBLedgersBehind(t *testing.T) { @@ -279,7 +283,7 @@ func TestRunFromUseDBLedgersBehind(t *testing.T) { Toml: captiveCoreToml, StoragePath: "/tmp/captive-core", UseDB: true, - }) + }, createNewDBCounter()) newDBCmdMock := simpleCommandMock() newDBCmdMock.On("Run").Return(nil) @@ -325,6 +329,23 @@ func TestRunFromUseDBLedgersBehind(t *testing.T) { assert.NoError(t, runner.runFrom(100, "hash")) assert.NoError(t, runner.close()) + + assert.Equal(t, float64(0), getNewDBCounterMetric(runner)) +} + +func createNewDBCounter() prometheus.Counter { + return prometheus.NewCounter(prometheus.CounterOpts{ + Namespace: "test", Subsystem: "captive_core", Name: "new_db_counter", + }) +} + +func getNewDBCounterMetric(runner *stellarCoreRunner) float64 { + value := &dto.Metric{} + err := runner.captiveCoreNewDBCounter.Write(value) + if err != nil { + panic(err) + } + return value.GetCounter().GetValue() } func TestRunFromUseDBLedgersInFront(t *testing.T) { @@ -341,7 +362,7 @@ func TestRunFromUseDBLedgersInFront(t *testing.T) { Toml: captiveCoreToml, StoragePath: "/tmp/captive-core", UseDB: true, - }) + }, createNewDBCounter()) newDBCmdMock := simpleCommandMock() newDBCmdMock.On("Run").Return(nil) @@ -405,4 +426,5 @@ func TestRunFromUseDBLedgersInFront(t *testing.T) { assert.NoError(t, runner.runFrom(100, "hash")) assert.NoError(t, runner.close()) + assert.Equal(t, float64(1), getNewDBCounterMetric(runner)) } From eb4b2ab750b840f96339ecbc017a68a0ef5272ff Mon Sep 17 00:00:00 2001 From: tamirms Date: Fri, 6 Sep 2024 07:44:26 +0100 Subject: [PATCH 226/234] services/horizon/internal/ingest: reap lookup tables without blocking ingestion (#5405) --- .../internal/db2/history/account_loader.go | 303 +-------------- .../db2/history/account_loader_test.go | 12 +- .../internal/db2/history/asset_loader.go | 5 +- .../internal/db2/history/asset_loader_test.go | 11 +- .../db2/history/claimable_balance_loader.go | 5 +- .../history/claimable_balance_loader_test.go | 11 +- .../effect_batch_insert_builder_test.go | 2 +- .../internal/db2/history/effect_test.go | 6 +- .../internal/db2/history/fee_bump_scenario.go | 2 +- .../horizon/internal/db2/history/key_value.go | 44 +-- .../db2/history/liquidity_pool_loader.go | 7 +- .../db2/history/liquidity_pool_loader_test.go | 11 +- .../horizon/internal/db2/history/loader.go | 365 ++++++++++++++++++ .../db2/history/loader_concurrency_test.go | 197 ++++++++++ services/horizon/internal/db2/history/main.go | 194 ++++++---- .../horizon/internal/db2/history/main_test.go | 24 +- ...n_participant_batch_insert_builder_test.go | 2 +- .../internal/db2/history/operation_test.go | 2 +- .../internal/db2/history/participants_test.go | 2 +- .../horizon/internal/db2/history/reap_test.go | 57 +-- .../internal/db2/history/transaction_test.go | 8 +- .../internal/db2/history/verify_lock.go | 10 +- services/horizon/internal/ingest/main.go | 83 +--- services/horizon/internal/ingest/main_test.go | 20 +- .../internal/ingest/processor_runner.go | 18 +- .../internal/ingest/processor_runner_test.go | 2 +- ...ble_balances_transaction_processor_test.go | 2 +- .../processors/effects_processor_test.go | 12 +- ...uidity_pools_transaction_processor_test.go | 2 +- .../processors/participants_processor_test.go | 2 +- services/horizon/internal/ingest/reap.go | 116 ++++++ .../internal/ingest/resume_state_test.go | 10 - 32 files changed, 984 insertions(+), 563 deletions(-) create mode 100644 services/horizon/internal/db2/history/loader.go create mode 100644 services/horizon/internal/db2/history/loader_concurrency_test.go diff --git a/services/horizon/internal/db2/history/account_loader.go b/services/horizon/internal/db2/history/account_loader.go index 9e15920609..f69e7d7f48 100644 --- a/services/horizon/internal/db2/history/account_loader.go +++ b/services/horizon/internal/db2/history/account_loader.go @@ -2,29 +2,10 @@ package history import ( "cmp" - "context" - "database/sql/driver" - "fmt" - "sort" - "strings" - - "github.com/lib/pq" "github.com/stellar/go/support/collections/set" - "github.com/stellar/go/support/db" - "github.com/stellar/go/support/errors" ) -var errSealed = errors.New("cannot register more entries to Loader after calling Exec()") - -// LoaderStats describes the result of executing a history lookup id Loader -type LoaderStats struct { - // Total is the number of elements registered to the Loader - Total int - // Inserted is the number of elements inserted into the lookup table - Inserted int -} - // FutureAccountID represents a future history account. // A FutureAccountID is created by an AccountLoader and // the account id is available after calling Exec() on @@ -38,7 +19,7 @@ type FutureAccountID = future[string, Account] type AccountLoader = loader[string, Account] // NewAccountLoader will construct a new AccountLoader instance. -func NewAccountLoader() *AccountLoader { +func NewAccountLoader(concurrencyMode ConcurrencyMode) *AccountLoader { return &AccountLoader{ sealed: false, set: set.Set[string]{}, @@ -58,287 +39,11 @@ func NewAccountLoader() *AccountLoader { mappingFromRow: func(account Account) (string, int64) { return account.Address, account.ID }, - less: cmp.Less[string], + less: cmp.Less[string], + concurrencyMode: concurrencyMode, } } -type loader[K comparable, T any] struct { - sealed bool - set set.Set[K] - ids map[K]int64 - stats LoaderStats - name string - table string - columnsForKeys func([]K) []columnValues - mappingFromRow func(T) (K, int64) - less func(K, K) bool -} - -type future[K comparable, T any] struct { - key K - loader *loader[K, T] -} - -// Value implements the database/sql/driver Valuer interface. -func (f future[K, T]) Value() (driver.Value, error) { - return f.loader.GetNow(f.key) -} - -// GetFuture registers the given key into the Loader and -// returns a future which will hold the history id for -// the key after Exec() is called. -func (l *loader[K, T]) GetFuture(key K) future[K, T] { - if l.sealed { - panic(errSealed) - } - - l.set.Add(key) - return future[K, T]{ - key: key, - loader: l, - } -} - -// GetNow returns the history id for the given key. -// GetNow should only be called on values which were registered by -// GetFuture() calls. Also, Exec() must be called before any GetNow -// call can succeed. -func (l *loader[K, T]) GetNow(key K) (int64, error) { - if !l.sealed { - return 0, fmt.Errorf(`invalid loader state, - Exec was not called yet to properly seal and resolve %v id`, key) - } - if internalID, ok := l.ids[key]; !ok { - return 0, fmt.Errorf(`loader key %v was not found`, key) - } else { - return internalID, nil - } -} - -// Exec will look up all the history ids for the keys registered in the Loader. -// If there are no history ids for a given set of keys, Exec will insert rows -// into the corresponding history table to establish a mapping between each key and its history id. -func (l *loader[K, T]) Exec(ctx context.Context, session db.SessionInterface) error { - l.sealed = true - if len(l.set) == 0 { - return nil - } - q := &Q{session} - keys := make([]K, 0, len(l.set)) - for key := range l.set { - keys = append(keys, key) - } - // sort entries before inserting rows to prevent deadlocks on acquiring a ShareLock - // https://github.com/stellar/go/issues/2370 - sort.Slice(keys, func(i, j int) bool { - return l.less(keys[i], keys[j]) - }) - - if count, err := l.insert(ctx, q, keys); err != nil { - return err - } else { - l.stats.Total += count - l.stats.Inserted += count - } - - if count, err := l.query(ctx, q, keys); err != nil { - return err - } else { - l.stats.Total += count - } - - return nil -} - -// Stats returns the number of addresses registered in the Loader and the number of rows -// inserted into the history table. -func (l *loader[K, T]) Stats() LoaderStats { - return l.stats -} - -func (l *loader[K, T]) Name() string { - return l.name -} - -func (l *loader[K, T]) filter(keys []K) []K { - if len(l.ids) == 0 { - return keys - } - - remaining := make([]K, 0, len(keys)) - for _, key := range keys { - if _, ok := l.ids[key]; ok { - continue - } - remaining = append(remaining, key) - } - return remaining -} - -func (l *loader[K, T]) updateMap(rows []T) { - for _, row := range rows { - key, id := l.mappingFromRow(row) - l.ids[key] = id - } -} - -func (l *loader[K, T]) insert(ctx context.Context, q *Q, keys []K) (int, error) { - keys = l.filter(keys) - if len(keys) == 0 { - return 0, nil - } - - var rows []T - err := bulkInsert( - ctx, - q, - l.table, - l.columnsForKeys(keys), - &rows, - ) - if err != nil { - return 0, err - } - - l.updateMap(rows) - return len(rows), nil -} - -func (l *loader[K, T]) query(ctx context.Context, q *Q, keys []K) (int, error) { - keys = l.filter(keys) - if len(keys) == 0 { - return 0, nil - } - - var rows []T - err := bulkGet( - ctx, - q, - l.table, - l.columnsForKeys(keys), - &rows, - ) - if err != nil { - return 0, err - } - - l.updateMap(rows) - return len(rows), nil -} - -type columnValues struct { - name string - dbType string - objects []string -} - -func bulkInsert(ctx context.Context, q *Q, table string, fields []columnValues, response interface{}) error { - unnestPart := make([]string, 0, len(fields)) - insertFieldsPart := make([]string, 0, len(fields)) - pqArrays := make([]interface{}, 0, len(fields)) - - // In the code below we are building the bulk insert query which looks like: - // - // WITH rows AS - // (SELECT - // /* unnestPart */ - // unnest(?::type1[]), /* field1 */ - // unnest(?::type2[]), /* field2 */ - // ... - // ) - // INSERT INTO table ( - // /* insertFieldsPart */ - // field1, - // field2, - // ... - // ) - // SELECT * FROM rows ON CONFLICT (field1, field2, ...) DO NOTHING RETURNING * - // - // Using unnest allows to get around the maximum limit of 65,535 query parameters, - // see https://www.postgresql.org/docs/12/limits.html and - // https://klotzandrew.com/blog/postgres-passing-65535-parameter-limit/ - // - // Without using unnest we would have to use multiple insert statements to insert - // all the rows for large datasets. - for _, field := range fields { - unnestPart = append( - unnestPart, - fmt.Sprintf("unnest(?::%s[]) /* %s */", field.dbType, field.name), - ) - insertFieldsPart = append( - insertFieldsPart, - field.name, - ) - pqArrays = append( - pqArrays, - pq.Array(field.objects), - ) - } - columns := strings.Join(insertFieldsPart, ",") - - sql := ` - WITH rows AS - (SELECT ` + strings.Join(unnestPart, ",") + `) - INSERT INTO ` + table + ` - (` + columns + `) - SELECT * FROM rows - ON CONFLICT (` + columns + `) DO NOTHING - RETURNING *` - - return q.SelectRaw( - ctx, - response, - sql, - pqArrays..., - ) -} - -func bulkGet(ctx context.Context, q *Q, table string, fields []columnValues, response interface{}) error { - unnestPart := make([]string, 0, len(fields)) - columns := make([]string, 0, len(fields)) - pqArrays := make([]interface{}, 0, len(fields)) - - // In the code below we are building the bulk get query which looks like: - // - // SELECT * FROM table WHERE (field1, field2, ...) IN - // (SELECT - // /* unnestPart */ - // unnest(?::type1[]), /* field1 */ - // unnest(?::type2[]), /* field2 */ - // ... - // ) - // - // Using unnest allows to get around the maximum limit of 65,535 query parameters, - // see https://www.postgresql.org/docs/12/limits.html and - // https://klotzandrew.com/blog/postgres-passing-65535-parameter-limit/ - // - // Without using unnest we would have to use multiple select statements to obtain - // all the rows for large datasets. - for _, field := range fields { - unnestPart = append( - unnestPart, - fmt.Sprintf("unnest(?::%s[]) /* %s */", field.dbType, field.name), - ) - columns = append( - columns, - field.name, - ) - pqArrays = append( - pqArrays, - pq.Array(field.objects), - ) - } - sql := `SELECT * FROM ` + table + ` WHERE (` + strings.Join(columns, ",") + `) IN - (SELECT ` + strings.Join(unnestPart, ",") + `)` - - return q.SelectRaw( - ctx, - response, - sql, - pqArrays..., - ) -} - // AccountLoaderStub is a stub wrapper around AccountLoader which allows // you to manually configure the mapping of addresses to history account ids type AccountLoaderStub struct { @@ -347,7 +52,7 @@ type AccountLoaderStub struct { // NewAccountLoaderStub returns a new AccountLoaderStub instance func NewAccountLoaderStub() AccountLoaderStub { - return AccountLoaderStub{Loader: NewAccountLoader()} + return AccountLoaderStub{Loader: NewAccountLoader(ConcurrentInserts)} } // Insert updates the wrapped AccountLoader so that the given account diff --git a/services/horizon/internal/db2/history/account_loader_test.go b/services/horizon/internal/db2/history/account_loader_test.go index 9a9fb30445..83b172b40b 100644 --- a/services/horizon/internal/db2/history/account_loader_test.go +++ b/services/horizon/internal/db2/history/account_loader_test.go @@ -8,6 +8,7 @@ import ( "github.com/stellar/go/keypair" "github.com/stellar/go/services/horizon/internal/test" + "github.com/stellar/go/support/db" ) func TestAccountLoader(t *testing.T) { @@ -16,12 +17,18 @@ func TestAccountLoader(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) session := tt.HorizonSession() + testAccountLoader(t, session, ConcurrentInserts) + test.ResetHorizonDB(t, tt.HorizonDB) + testAccountLoader(t, session, ConcurrentDeletes) +} + +func testAccountLoader(t *testing.T, session *db.Session, mode ConcurrencyMode) { var addresses []string for i := 0; i < 100; i++ { addresses = append(addresses, keypair.MustRandom().Address()) } - loader := NewAccountLoader() + loader := NewAccountLoader(mode) for _, address := range addresses { future := loader.GetFuture(address) _, err := future.Value() @@ -58,7 +65,7 @@ func TestAccountLoader(t *testing.T) { // check that Loader works when all the previous values are already // present in the db and also add 10 more rows to insert - loader = NewAccountLoader() + loader = NewAccountLoader(mode) for i := 0; i < 10; i++ { addresses = append(addresses, keypair.MustRandom().Address()) } @@ -85,5 +92,4 @@ func TestAccountLoader(t *testing.T) { assert.Equal(t, account.ID, internalId) assert.Equal(t, account.Address, address) } - } diff --git a/services/horizon/internal/db2/history/asset_loader.go b/services/horizon/internal/db2/history/asset_loader.go index cdd2a0d714..33c5c333dd 100644 --- a/services/horizon/internal/db2/history/asset_loader.go +++ b/services/horizon/internal/db2/history/asset_loader.go @@ -42,7 +42,7 @@ type FutureAssetID = future[AssetKey, Asset] type AssetLoader = loader[AssetKey, Asset] // NewAssetLoader will construct a new AssetLoader instance. -func NewAssetLoader() *AssetLoader { +func NewAssetLoader(concurrencyMode ConcurrencyMode) *AssetLoader { return &AssetLoader{ sealed: false, set: set.Set[AssetKey]{}, @@ -88,6 +88,7 @@ func NewAssetLoader() *AssetLoader { less: func(a AssetKey, b AssetKey) bool { return a.String() < b.String() }, + concurrencyMode: concurrencyMode, } } @@ -99,7 +100,7 @@ type AssetLoaderStub struct { // NewAssetLoaderStub returns a new AssetLoaderStub instance func NewAssetLoaderStub() AssetLoaderStub { - return AssetLoaderStub{Loader: NewAssetLoader()} + return AssetLoaderStub{Loader: NewAssetLoader(ConcurrentInserts)} } // Insert updates the wrapped AssetLoaderStub so that the given asset diff --git a/services/horizon/internal/db2/history/asset_loader_test.go b/services/horizon/internal/db2/history/asset_loader_test.go index ca65cebb7e..e7a0495cad 100644 --- a/services/horizon/internal/db2/history/asset_loader_test.go +++ b/services/horizon/internal/db2/history/asset_loader_test.go @@ -9,6 +9,7 @@ import ( "github.com/stellar/go/keypair" "github.com/stellar/go/services/horizon/internal/test" + "github.com/stellar/go/support/db" "github.com/stellar/go/xdr" ) @@ -40,6 +41,12 @@ func TestAssetLoader(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) session := tt.HorizonSession() + testAssetLoader(t, session, ConcurrentInserts) + test.ResetHorizonDB(t, tt.HorizonDB) + testAssetLoader(t, session, ConcurrentDeletes) +} + +func testAssetLoader(t *testing.T, session *db.Session, mode ConcurrencyMode) { var keys []AssetKey for i := 0; i < 100; i++ { var key AssetKey @@ -66,7 +73,7 @@ func TestAssetLoader(t *testing.T) { keys = append(keys, key) } - loader := NewAssetLoader() + loader := NewAssetLoader(mode) for _, key := range keys { future := loader.GetFuture(key) _, err := future.Value() @@ -109,7 +116,7 @@ func TestAssetLoader(t *testing.T) { // check that Loader works when all the previous values are already // present in the db and also add 10 more rows to insert - loader = NewAssetLoader() + loader = NewAssetLoader(mode) for i := 0; i < 10; i++ { var key AssetKey if i%2 == 0 { diff --git a/services/horizon/internal/db2/history/claimable_balance_loader.go b/services/horizon/internal/db2/history/claimable_balance_loader.go index f775ea4b24..9107d4fb9f 100644 --- a/services/horizon/internal/db2/history/claimable_balance_loader.go +++ b/services/horizon/internal/db2/history/claimable_balance_loader.go @@ -19,7 +19,7 @@ type FutureClaimableBalanceID = future[string, HistoryClaimableBalance] type ClaimableBalanceLoader = loader[string, HistoryClaimableBalance] // NewClaimableBalanceLoader will construct a new ClaimableBalanceLoader instance. -func NewClaimableBalanceLoader() *ClaimableBalanceLoader { +func NewClaimableBalanceLoader(concurrencyMode ConcurrencyMode) *ClaimableBalanceLoader { return &ClaimableBalanceLoader{ sealed: false, set: set.Set[string]{}, @@ -39,6 +39,7 @@ func NewClaimableBalanceLoader() *ClaimableBalanceLoader { mappingFromRow: func(row HistoryClaimableBalance) (string, int64) { return row.BalanceID, row.InternalID }, - less: cmp.Less[string], + less: cmp.Less[string], + concurrencyMode: concurrencyMode, } } diff --git a/services/horizon/internal/db2/history/claimable_balance_loader_test.go b/services/horizon/internal/db2/history/claimable_balance_loader_test.go index f5759015c7..490d5a0f70 100644 --- a/services/horizon/internal/db2/history/claimable_balance_loader_test.go +++ b/services/horizon/internal/db2/history/claimable_balance_loader_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stellar/go/services/horizon/internal/test" + "github.com/stellar/go/support/db" "github.com/stellar/go/xdr" ) @@ -17,6 +18,12 @@ func TestClaimableBalanceLoader(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) session := tt.HorizonSession() + testCBLoader(t, tt, session, ConcurrentInserts) + test.ResetHorizonDB(t, tt.HorizonDB) + testCBLoader(t, tt, session, ConcurrentDeletes) +} + +func testCBLoader(t *testing.T, tt *test.T, session *db.Session, mode ConcurrencyMode) { var ids []string for i := 0; i < 100; i++ { balanceID := xdr.ClaimableBalanceId{ @@ -28,7 +35,7 @@ func TestClaimableBalanceLoader(t *testing.T) { ids = append(ids, id) } - loader := NewClaimableBalanceLoader() + loader := NewClaimableBalanceLoader(mode) var futures []FutureClaimableBalanceID for _, id := range ids { future := loader.GetFuture(id) @@ -70,7 +77,7 @@ func TestClaimableBalanceLoader(t *testing.T) { // check that Loader works when all the previous values are already // present in the db and also add 10 more rows to insert - loader = NewClaimableBalanceLoader() + loader = NewClaimableBalanceLoader(mode) for i := 100; i < 110; i++ { balanceID := xdr.ClaimableBalanceId{ Type: xdr.ClaimableBalanceIdTypeClaimableBalanceIdTypeV0, diff --git a/services/horizon/internal/db2/history/effect_batch_insert_builder_test.go b/services/horizon/internal/db2/history/effect_batch_insert_builder_test.go index e1ac998953..e917983b8f 100644 --- a/services/horizon/internal/db2/history/effect_batch_insert_builder_test.go +++ b/services/horizon/internal/db2/history/effect_batch_insert_builder_test.go @@ -20,7 +20,7 @@ func TestAddEffect(t *testing.T) { address := "GAQAA5L65LSYH7CQ3VTJ7F3HHLGCL3DSLAR2Y47263D56MNNGHSQSTVY" muxedAddres := "MAQAA5L65LSYH7CQ3VTJ7F3HHLGCL3DSLAR2Y47263D56MNNGHSQSAAAAAAAAAAE2LP26" - accountLoader := NewAccountLoader() + accountLoader := NewAccountLoader(ConcurrentInserts) builder := q.NewEffectBatchInsertBuilder() sequence := int32(56) diff --git a/services/horizon/internal/db2/history/effect_test.go b/services/horizon/internal/db2/history/effect_test.go index 19af0ceff8..ba59cf3fd4 100644 --- a/services/horizon/internal/db2/history/effect_test.go +++ b/services/horizon/internal/db2/history/effect_test.go @@ -23,7 +23,7 @@ func TestEffectsForLiquidityPool(t *testing.T) { // Insert Effect address := "GAQAA5L65LSYH7CQ3VTJ7F3HHLGCL3DSLAR2Y47263D56MNNGHSQSTVY" muxedAddres := "MAQAA5L65LSYH7CQ3VTJ7F3HHLGCL3DSLAR2Y47263D56MNNGHSQSAAAAAAAAAAE2LP26" - accountLoader := NewAccountLoader() + accountLoader := NewAccountLoader(ConcurrentInserts) builder := q.NewEffectBatchInsertBuilder() sequence := int32(56) @@ -47,7 +47,7 @@ func TestEffectsForLiquidityPool(t *testing.T) { // Insert Liquidity Pool history liquidityPoolID := "abcde" - lpLoader := NewLiquidityPoolLoader() + lpLoader := NewLiquidityPoolLoader(ConcurrentInserts) operationBuilder := q.NewOperationLiquidityPoolBatchInsertBuilder() tt.Assert.NoError(operationBuilder.Add(opID, lpLoader.GetFuture(liquidityPoolID))) @@ -78,7 +78,7 @@ func TestEffectsForTrustlinesSponsorshipEmptyAssetType(t *testing.T) { address := "GAQAA5L65LSYH7CQ3VTJ7F3HHLGCL3DSLAR2Y47263D56MNNGHSQSTVY" muxedAddres := "MAQAA5L65LSYH7CQ3VTJ7F3HHLGCL3DSLAR2Y47263D56MNNGHSQSAAAAAAAAAAE2LP26" - accountLoader := NewAccountLoader() + accountLoader := NewAccountLoader(ConcurrentInserts) builder := q.NewEffectBatchInsertBuilder() sequence := int32(56) diff --git a/services/horizon/internal/db2/history/fee_bump_scenario.go b/services/horizon/internal/db2/history/fee_bump_scenario.go index da6563c732..e161d686d1 100644 --- a/services/horizon/internal/db2/history/fee_bump_scenario.go +++ b/services/horizon/internal/db2/history/fee_bump_scenario.go @@ -288,7 +288,7 @@ func FeeBumpScenario(tt *test.T, q *Q, successful bool) FeeBumpFixture { details, err = json.Marshal(map[string]interface{}{"new_seq": 98}) tt.Assert.NoError(err) - accountLoader := NewAccountLoader() + accountLoader := NewAccountLoader(ConcurrentInserts) err = effectBuilder.Add( accountLoader.GetFuture(account.Address()), diff --git a/services/horizon/internal/db2/history/key_value.go b/services/horizon/internal/db2/history/key_value.go index 3d23451937..9fee9513c2 100644 --- a/services/horizon/internal/db2/history/key_value.go +++ b/services/horizon/internal/db2/history/key_value.go @@ -5,7 +5,6 @@ import ( "database/sql" "fmt" "strconv" - "strings" sq "github.com/Masterminds/squirrel" @@ -207,41 +206,26 @@ func (q *Q) getValueFromStore(ctx context.Context, key string, forUpdate bool) ( return value, nil } -type KeyValuePair struct { - Key string `db:"key"` - Value string `db:"value"` -} - -func (q *Q) getLookupTableReapOffsets(ctx context.Context) (map[string]int64, error) { - keys := make([]string, 0, len(historyLookupTables)) - for table := range historyLookupTables { - keys = append(keys, table+lookupTableReapOffsetSuffix) - } - offsets := map[string]int64{} - var pairs []KeyValuePair - query := sq.Select("key", "value"). +func (q *Q) getLookupTableReapOffset(ctx context.Context, table string) (int64, error) { + query := sq.Select("value"). From("key_value_store"). Where(map[string]interface{}{ - "key": keys, + "key": table + lookupTableReapOffsetSuffix, }) - err := q.Select(ctx, &pairs, query) + var text string + err := q.Get(ctx, &text, query) if err != nil { - return nil, err - } - for _, pair := range pairs { - table := strings.TrimSuffix(pair.Key, lookupTableReapOffsetSuffix) - if _, ok := historyLookupTables[table]; !ok { - return nil, fmt.Errorf("invalid key: %s", pair.Key) - } - - var offset int64 - offset, err = strconv.ParseInt(pair.Value, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid offset: %s", pair.Value) + if errors.Cause(err) == sql.ErrNoRows { + return 0, nil } - offsets[table] = offset + return 0, err + } + var offset int64 + offset, err = strconv.ParseInt(text, 10, 64) + if err != nil { + return 0, fmt.Errorf("invalid offset: %s for table %s", text, table) } - return offsets, err + return offset, nil } func (q *Q) updateLookupTableReapOffset(ctx context.Context, table string, offset int64) error { diff --git a/services/horizon/internal/db2/history/liquidity_pool_loader.go b/services/horizon/internal/db2/history/liquidity_pool_loader.go index a03caaa988..5da2a7b6fd 100644 --- a/services/horizon/internal/db2/history/liquidity_pool_loader.go +++ b/services/horizon/internal/db2/history/liquidity_pool_loader.go @@ -19,7 +19,7 @@ type FutureLiquidityPoolID = future[string, HistoryLiquidityPool] type LiquidityPoolLoader = loader[string, HistoryLiquidityPool] // NewLiquidityPoolLoader will construct a new LiquidityPoolLoader instance. -func NewLiquidityPoolLoader() *LiquidityPoolLoader { +func NewLiquidityPoolLoader(concurrencyMode ConcurrencyMode) *LiquidityPoolLoader { return &LiquidityPoolLoader{ sealed: false, set: set.Set[string]{}, @@ -39,7 +39,8 @@ func NewLiquidityPoolLoader() *LiquidityPoolLoader { mappingFromRow: func(row HistoryLiquidityPool) (string, int64) { return row.PoolID, row.InternalID }, - less: cmp.Less[string], + less: cmp.Less[string], + concurrencyMode: concurrencyMode, } } @@ -51,7 +52,7 @@ type LiquidityPoolLoaderStub struct { // NewLiquidityPoolLoaderStub returns a new LiquidityPoolLoader instance func NewLiquidityPoolLoaderStub() LiquidityPoolLoaderStub { - return LiquidityPoolLoaderStub{Loader: NewLiquidityPoolLoader()} + return LiquidityPoolLoaderStub{Loader: NewLiquidityPoolLoader(ConcurrentInserts)} } // Insert updates the wrapped LiquidityPoolLoader so that the given liquidity pool diff --git a/services/horizon/internal/db2/history/liquidity_pool_loader_test.go b/services/horizon/internal/db2/history/liquidity_pool_loader_test.go index aec2fcd886..c7a1282760 100644 --- a/services/horizon/internal/db2/history/liquidity_pool_loader_test.go +++ b/services/horizon/internal/db2/history/liquidity_pool_loader_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stellar/go/services/horizon/internal/test" + "github.com/stellar/go/support/db" "github.com/stellar/go/xdr" ) @@ -16,6 +17,12 @@ func TestLiquidityPoolLoader(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) session := tt.HorizonSession() + testLPLoader(t, tt, session, ConcurrentInserts) + test.ResetHorizonDB(t, tt.HorizonDB) + testLPLoader(t, tt, session, ConcurrentDeletes) +} + +func testLPLoader(t *testing.T, tt *test.T, session *db.Session, mode ConcurrencyMode) { var ids []string for i := 0; i < 100; i++ { poolID := xdr.PoolId{byte(i)} @@ -24,7 +31,7 @@ func TestLiquidityPoolLoader(t *testing.T) { ids = append(ids, id) } - loader := NewLiquidityPoolLoader() + loader := NewLiquidityPoolLoader(mode) for _, id := range ids { future := loader.GetFuture(id) _, err := future.Value() @@ -62,7 +69,7 @@ func TestLiquidityPoolLoader(t *testing.T) { // check that Loader works when all the previous values are already // present in the db and also add 10 more rows to insert - loader = NewLiquidityPoolLoader() + loader = NewLiquidityPoolLoader(mode) for i := 100; i < 110; i++ { poolID := xdr.PoolId{byte(i)} var id string diff --git a/services/horizon/internal/db2/history/loader.go b/services/horizon/internal/db2/history/loader.go new file mode 100644 index 0000000000..dc2236accb --- /dev/null +++ b/services/horizon/internal/db2/history/loader.go @@ -0,0 +1,365 @@ +package history + +import ( + "context" + "database/sql/driver" + "fmt" + "sort" + "strings" + + "github.com/lib/pq" + + "github.com/stellar/go/support/collections/set" + "github.com/stellar/go/support/db" +) + +var errSealed = fmt.Errorf("cannot register more entries to Loader after calling Exec()") + +// ConcurrencyMode is used to configure the level of thread-safety for a loader +type ConcurrencyMode int + +func (cm ConcurrencyMode) String() string { + switch cm { + case ConcurrentInserts: + return "ConcurrentInserts" + case ConcurrentDeletes: + return "ConcurrentDeletes" + default: + return "unknown" + } +} + +const ( + _ ConcurrencyMode = iota + // ConcurrentInserts configures the loader to maintain safety when there are multiple loaders + // inserting into the same table concurrently. This ConcurrencyMode is suitable for parallel reingestion. + // Note while ConcurrentInserts is enabled it is not safe to have deletes occurring concurrently on the + // same table. + ConcurrentInserts + // ConcurrentDeletes configures the loader to maintain safety when there is another thread which is invoking + // reapLookupTable() to delete rows from the same table concurrently. This ConcurrencyMode is suitable for + // live ingestion when reaping of lookup tables is enabled. + // Note while ConcurrentDeletes is enabled it is not safe to have multiple threads inserting concurrently to the + // same table. + ConcurrentDeletes +) + +// LoaderStats describes the result of executing a history lookup id Loader +type LoaderStats struct { + // Total is the number of elements registered to the Loader + Total int + // Inserted is the number of elements inserted into the lookup table + Inserted int +} + +type loader[K comparable, T any] struct { + sealed bool + set set.Set[K] + ids map[K]int64 + stats LoaderStats + name string + table string + columnsForKeys func([]K) []columnValues + mappingFromRow func(T) (K, int64) + less func(K, K) bool + concurrencyMode ConcurrencyMode +} + +type future[K comparable, T any] struct { + key K + loader *loader[K, T] +} + +// Value implements the database/sql/driver Valuer interface. +func (f future[K, T]) Value() (driver.Value, error) { + return f.loader.GetNow(f.key) +} + +// GetFuture registers the given key into the Loader and +// returns a future which will hold the history id for +// the key after Exec() is called. +func (l *loader[K, T]) GetFuture(key K) future[K, T] { + if l.sealed { + panic(errSealed) + } + + l.set.Add(key) + return future[K, T]{ + key: key, + loader: l, + } +} + +// GetNow returns the history id for the given key. +// GetNow should only be called on values which were registered by +// GetFuture() calls. Also, Exec() must be called before any GetNow +// call can succeed. +func (l *loader[K, T]) GetNow(key K) (int64, error) { + if !l.sealed { + return 0, fmt.Errorf(`invalid loader state, + Exec was not called yet to properly seal and resolve %v id`, key) + } + if internalID, ok := l.ids[key]; !ok { + return 0, fmt.Errorf(`loader key %v was not found`, key) + } else { + return internalID, nil + } +} + +// Exec will look up all the history ids for the keys registered in the Loader. +// If there are no history ids for a given set of keys, Exec will insert rows +// into the corresponding history table to establish a mapping between each key and its history id. +func (l *loader[K, T]) Exec(ctx context.Context, session db.SessionInterface) error { + l.sealed = true + if len(l.set) == 0 { + return nil + } + q := &Q{session} + keys := make([]K, 0, len(l.set)) + for key := range l.set { + keys = append(keys, key) + } + // sort entries before inserting rows to prevent deadlocks on acquiring a ShareLock + // https://github.com/stellar/go/issues/2370 + sort.Slice(keys, func(i, j int) bool { + return l.less(keys[i], keys[j]) + }) + + if l.concurrencyMode == ConcurrentInserts { + // if there are other ingestion transactions running concurrently, + // we need to first insert the records (with a ON CONFLICT DO NOTHING + // clause). Then, we can query for the remaining records. + // This order (insert first and then query) is important because + // if multiple concurrent transactions try to insert the same record + // only one of them will succeed and the other transactions will omit + // the record from the RETURNING set. + if count, err := l.insert(ctx, q, keys); err != nil { + return err + } else { + l.stats.Total += count + l.stats.Inserted += count + } + + if count, err := l.query(ctx, q, keys, false); err != nil { + return err + } else { + l.stats.Total += count + } + } else if l.concurrencyMode == ConcurrentDeletes { + // if the lookup table reaping transaction is running concurrently, + // we need to lock the rows from the lookup table to ensure that + // the reaper cannot run until after the ingestion transaction has + // been committed. + if count, err := l.query(ctx, q, keys, true); err != nil { + return err + } else { + l.stats.Total += count + } + + // insert whatever records were not found from l.query() + if count, err := l.insert(ctx, q, keys); err != nil { + return err + } else { + l.stats.Total += count + l.stats.Inserted += count + } + } else { + return fmt.Errorf("concurrency mode %v is invalid", l.concurrencyMode) + } + + return nil +} + +// Stats returns the number of addresses registered in the Loader and the number of rows +// inserted into the history table. +func (l *loader[K, T]) Stats() LoaderStats { + return l.stats +} + +func (l *loader[K, T]) Name() string { + return l.name +} + +func (l *loader[K, T]) filter(keys []K) []K { + if len(l.ids) == 0 { + return keys + } + + remaining := make([]K, 0, len(keys)) + for _, key := range keys { + if _, ok := l.ids[key]; ok { + continue + } + remaining = append(remaining, key) + } + return remaining +} + +func (l *loader[K, T]) updateMap(rows []T) { + for _, row := range rows { + key, id := l.mappingFromRow(row) + l.ids[key] = id + } +} + +func (l *loader[K, T]) insert(ctx context.Context, q *Q, keys []K) (int, error) { + keys = l.filter(keys) + if len(keys) == 0 { + return 0, nil + } + + var rows []T + err := bulkInsert( + ctx, + q, + l.table, + l.columnsForKeys(keys), + &rows, + ) + if err != nil { + return 0, err + } + + l.updateMap(rows) + return len(rows), nil +} + +func (l *loader[K, T]) query(ctx context.Context, q *Q, keys []K, lockRows bool) (int, error) { + keys = l.filter(keys) + if len(keys) == 0 { + return 0, nil + } + var suffix string + if lockRows { + suffix = "ORDER BY id ASC FOR KEY SHARE" + } + + var rows []T + err := bulkGet( + ctx, + q, + l.table, + l.columnsForKeys(keys), + &rows, + suffix, + ) + if err != nil { + return 0, err + } + + l.updateMap(rows) + return len(rows), nil +} + +type columnValues struct { + name string + dbType string + objects []string +} + +func bulkInsert(ctx context.Context, q *Q, table string, fields []columnValues, response interface{}) error { + unnestPart := make([]string, 0, len(fields)) + insertFieldsPart := make([]string, 0, len(fields)) + pqArrays := make([]interface{}, 0, len(fields)) + + // In the code below we are building the bulk insert query which looks like: + // + // WITH rows AS + // (SELECT + // /* unnestPart */ + // unnest(?::type1[]), /* field1 */ + // unnest(?::type2[]), /* field2 */ + // ... + // ) + // INSERT INTO table ( + // /* insertFieldsPart */ + // field1, + // field2, + // ... + // ) + // SELECT * FROM rows ON CONFLICT (field1, field2, ...) DO NOTHING RETURNING * + // + // Using unnest allows to get around the maximum limit of 65,535 query parameters, + // see https://www.postgresql.org/docs/12/limits.html and + // https://klotzandrew.com/blog/postgres-passing-65535-parameter-limit/ + // + // Without using unnest we would have to use multiple insert statements to insert + // all the rows for large datasets. + for _, field := range fields { + unnestPart = append( + unnestPart, + fmt.Sprintf("unnest(?::%s[]) /* %s */", field.dbType, field.name), + ) + insertFieldsPart = append( + insertFieldsPart, + field.name, + ) + pqArrays = append( + pqArrays, + pq.Array(field.objects), + ) + } + columns := strings.Join(insertFieldsPart, ",") + + sql := ` + WITH rows AS + (SELECT ` + strings.Join(unnestPart, ",") + `) + INSERT INTO ` + table + ` + (` + columns + `) + SELECT * FROM rows + ON CONFLICT (` + columns + `) DO NOTHING + RETURNING *` + + return q.SelectRaw( + ctx, + response, + sql, + pqArrays..., + ) +} + +func bulkGet(ctx context.Context, q *Q, table string, fields []columnValues, response interface{}, suffix string) error { + unnestPart := make([]string, 0, len(fields)) + columns := make([]string, 0, len(fields)) + pqArrays := make([]interface{}, 0, len(fields)) + + // In the code below we are building the bulk get query which looks like: + // + // SELECT * FROM table WHERE (field1, field2, ...) IN + // (SELECT + // /* unnestPart */ + // unnest(?::type1[]), /* field1 */ + // unnest(?::type2[]), /* field2 */ + // ... + // ) + // + // Using unnest allows to get around the maximum limit of 65,535 query parameters, + // see https://www.postgresql.org/docs/12/limits.html and + // https://klotzandrew.com/blog/postgres-passing-65535-parameter-limit/ + // + // Without using unnest we would have to use multiple select statements to obtain + // all the rows for large datasets. + for _, field := range fields { + unnestPart = append( + unnestPart, + fmt.Sprintf("unnest(?::%s[]) /* %s */", field.dbType, field.name), + ) + columns = append( + columns, + field.name, + ) + pqArrays = append( + pqArrays, + pq.Array(field.objects), + ) + } + sql := `SELECT * FROM ` + table + ` WHERE (` + strings.Join(columns, ",") + `) IN + (SELECT ` + strings.Join(unnestPart, ",") + `) ` + suffix + + return q.SelectRaw( + ctx, + response, + sql, + pqArrays..., + ) +} diff --git a/services/horizon/internal/db2/history/loader_concurrency_test.go b/services/horizon/internal/db2/history/loader_concurrency_test.go new file mode 100644 index 0000000000..e87d901bd8 --- /dev/null +++ b/services/horizon/internal/db2/history/loader_concurrency_test.go @@ -0,0 +1,197 @@ +package history + +import ( + "context" + "database/sql" + "fmt" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/stellar/go/keypair" + "github.com/stellar/go/services/horizon/internal/test" +) + +func TestLoaderConcurrentInserts(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + s1 := tt.HorizonSession() + s2 := s1.Clone() + + for _, testCase := range []struct { + mode ConcurrencyMode + pass bool + }{ + {ConcurrentInserts, true}, + {ConcurrentDeletes, false}, + } { + t.Run(fmt.Sprintf("%v", testCase.mode), func(t *testing.T) { + var addresses []string + for i := 0; i < 10; i++ { + addresses = append(addresses, keypair.MustRandom().Address()) + } + + l1 := NewAccountLoader(testCase.mode) + for _, address := range addresses { + l1.GetFuture(address) + } + + for i := 0; i < 5; i++ { + addresses = append(addresses, keypair.MustRandom().Address()) + } + + l2 := NewAccountLoader(testCase.mode) + for _, address := range addresses { + l2.GetFuture(address) + } + + assert.NoError(t, s1.Begin(context.Background())) + assert.NoError(t, l1.Exec(context.Background(), s1)) + + assert.NoError(t, s2.Begin(context.Background())) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + <-time.After(time.Second * 3) + assert.NoError(t, s1.Commit()) + }() + // l2.Exec(context.Background(), s2) will block until s1 + // is committed because s1 and s2 both attempt to insert common + // accounts and, since s1 executed first, s2 must wait until + // s1 terminates. + assert.NoError(t, l2.Exec(context.Background(), s2)) + assert.NoError(t, s2.Commit()) + wg.Wait() + + assert.Equal(t, LoaderStats{ + Total: 10, + Inserted: 10, + }, l1.Stats()) + + if testCase.pass { + assert.Equal(t, LoaderStats{ + Total: 15, + Inserted: 5, + }, l2.Stats()) + } else { + assert.Equal(t, LoaderStats{ + Total: 5, + Inserted: 5, + }, l2.Stats()) + return + } + + q := &Q{s1} + for _, address := range addresses[:10] { + l1Id, err := l1.GetNow(address) + assert.NoError(t, err) + + l2Id, err := l2.GetNow(address) + assert.NoError(t, err) + assert.Equal(t, l1Id, l2Id) + + var account Account + assert.NoError(t, q.AccountByAddress(context.Background(), &account, address)) + assert.Equal(t, account.ID, l1Id) + assert.Equal(t, account.Address, address) + } + + for _, address := range addresses[10:] { + l2Id, err := l2.GetNow(address) + assert.NoError(t, err) + + _, err = l1.GetNow(address) + assert.ErrorContains(t, err, "was not found") + + var account Account + assert.NoError(t, q.AccountByAddress(context.Background(), &account, address)) + assert.Equal(t, account.ID, l2Id) + assert.Equal(t, account.Address, address) + } + }) + } +} + +func TestLoaderConcurrentDeletes(t *testing.T) { + tt := test.Start(t) + defer tt.Finish() + test.ResetHorizonDB(t, tt.HorizonDB) + s1 := tt.HorizonSession() + s2 := s1.Clone() + + for _, testCase := range []struct { + mode ConcurrencyMode + pass bool + }{ + {ConcurrentInserts, false}, + {ConcurrentDeletes, true}, + } { + t.Run(fmt.Sprintf("%v", testCase.mode), func(t *testing.T) { + var addresses []string + for i := 0; i < 10; i++ { + addresses = append(addresses, keypair.MustRandom().Address()) + } + + loader := NewAccountLoader(testCase.mode) + for _, address := range addresses { + loader.GetFuture(address) + } + assert.NoError(t, loader.Exec(context.Background(), s1)) + + var ids []int64 + for _, address := range addresses { + id, err := loader.GetNow(address) + assert.NoError(t, err) + ids = append(ids, id) + } + + loader = NewAccountLoader(testCase.mode) + for _, address := range addresses { + loader.GetFuture(address) + } + + assert.NoError(t, s1.Begin(context.Background())) + assert.NoError(t, loader.Exec(context.Background(), s1)) + + assert.NoError(t, s2.Begin(context.Background())) + q2 := &Q{s2} + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + <-time.After(time.Second * 3) + + q1 := &Q{s1} + for _, address := range addresses { + id, err := loader.GetNow(address) + assert.NoError(t, err) + + var account Account + err = q1.AccountByAddress(context.Background(), &account, address) + if testCase.pass { + assert.NoError(t, err) + assert.Equal(t, account.ID, id) + assert.Equal(t, account.Address, address) + } else { + assert.ErrorContains(t, err, sql.ErrNoRows.Error()) + } + } + assert.NoError(t, s1.Commit()) + }() + + // the reaper should block until s1 has been committed because s1 has locked + // the orphaned rows + deletedCount, err := q2.reapLookupTable(context.Background(), "history_accounts", ids, 1000) + assert.NoError(t, err) + assert.Equal(t, int64(len(addresses)), deletedCount) + assert.NoError(t, s2.Commit()) + + wg.Wait() + }) + } +} diff --git a/services/horizon/internal/db2/history/main.go b/services/horizon/internal/db2/history/main.go index e9d8ffb185..59d828d091 100644 --- a/services/horizon/internal/db2/history/main.go +++ b/services/horizon/internal/db2/history/main.go @@ -9,6 +9,7 @@ import ( "database/sql/driver" "encoding/json" "fmt" + "strconv" "strings" "sync" "time" @@ -282,7 +283,8 @@ type IngestionQ interface { NewTradeBatchInsertBuilder() TradeBatchInsertBuilder RebuildTradeAggregationTimes(ctx context.Context, from, to strtime.Millis, roundingSlippageFilter int) error RebuildTradeAggregationBuckets(ctx context.Context, fromLedger, toLedger uint32, roundingSlippageFilter int) error - ReapLookupTables(ctx context.Context, batchSize int) (map[string]LookupTableReapResult, error) + ReapLookupTable(ctx context.Context, table string, ids []int64, newOffset int64) (int64, error) + FindLookupTableRowsToReap(ctx context.Context, table string, batchSize int) ([]int64, int64, error) CreateAssets(ctx context.Context, assets []xdr.Asset, batchSize int) (map[string]Asset, error) QTransactions QTrustLines @@ -307,6 +309,7 @@ type IngestionQ interface { GetNextLedgerSequence(context.Context, uint32) (uint32, bool, error) TryStateVerificationLock(context.Context) (bool, error) TryReaperLock(context.Context) (bool, error) + TryLookupTableReaperLock(ctx context.Context) (bool, error) ElderLedger(context.Context, interface{}) error } @@ -977,63 +980,70 @@ type LookupTableReapResult struct { Duration time.Duration } -// ReapLookupTables removes rows from lookup tables like history_claimable_balances -// which aren't used (orphaned), i.e. history entries for them were reaped. -// This method must be executed inside ingestion transaction. Otherwise it may -// create invalid state in lookup and history tables. -func (q *Q) ReapLookupTables(ctx context.Context, batchSize int) ( - map[string]LookupTableReapResult, - error, -) { - if q.GetTx() == nil { - return nil, errors.New("cannot be called outside of an ingestion transaction") +func (q *Q) FindLookupTableRowsToReap(ctx context.Context, table string, batchSize int) ([]int64, int64, error) { + offset, err := q.getLookupTableReapOffset(ctx, table) + if err != nil { + return nil, 0, fmt.Errorf("could not obtain offsets: %w", err) } - offsets, err := q.getLookupTableReapOffsets(ctx) + // Find new offset before removing the rows + var newOffset int64 + err = q.GetRaw( + ctx, + &newOffset, + fmt.Sprintf( + "SELECT id FROM %s WHERE id >= %d ORDER BY id ASC LIMIT 1 OFFSET %d", + table, offset, batchSize, + ), + ) if err != nil { - return nil, fmt.Errorf("could not obtain offsets: %w", err) + if q.NoRows(err) { + newOffset = 0 + } else { + return nil, 0, err + } } - results := map[string]LookupTableReapResult{} - for table, historyTables := range historyLookupTables { - startTime := time.Now() - query := constructReapLookupTablesQuery(table, historyTables, batchSize, offsets[table]) + var ids []int64 + err = q.SelectRaw(ctx, &ids, constructFindReapLookupTablesQuery(table, batchSize, offset)) + if err != nil { + return nil, 0, fmt.Errorf("could not query orphaned rows: %w", err) + } - // Find new offset before removing the rows - var newOffset int64 - err := q.GetRaw(ctx, &newOffset, fmt.Sprintf("SELECT id FROM %s where id >= %d limit 1 offset %d", table, offsets[table], batchSize)) - if err != nil { - if q.NoRows(err) { - newOffset = 0 - } else { - return nil, err - } - } + return ids, newOffset, nil +} - res, err := q.ExecRaw( - context.WithValue(ctx, &db.QueryTypeContextKey, db.DeleteQueryType), - query, - ) - if err != nil { - return nil, errors.Wrapf(err, "error running query: %s", query) - } +func (q *Q) ReapLookupTable(ctx context.Context, table string, ids []int64, newOffset int64) (int64, error) { + if err := q.Begin(ctx); err != nil { + return 0, fmt.Errorf("could not start transaction: %w", err) + } + defer q.Rollback() - if err = q.updateLookupTableReapOffset(ctx, table, newOffset); err != nil { - return nil, fmt.Errorf("error updating offset: %w", err) - } + rowsDeleted, err := q.reapLookupTable(ctx, table, ids, newOffset) + if err != nil { + return 0, err + } - rows, err := res.RowsAffected() - if err != nil { - return nil, errors.Wrapf(err, "error running RowsAffected after query: %s", query) - } + if err := q.Commit(); err != nil { + return 0, fmt.Errorf("could not commit transaction: %w", err) + } + return rowsDeleted, nil +} + +func (q *Q) reapLookupTable(ctx context.Context, table string, ids []int64, newOffset int64) (int64, error) { + if err := q.updateLookupTableReapOffset(ctx, table, newOffset); err != nil { + return 0, fmt.Errorf("error updating offset for table %s: %w ", table, err) + } - results[table] = LookupTableReapResult{ - Offset: newOffset, - RowsDeleted: rows, - Duration: time.Since(startTime), + var rowsDeleted int64 + if len(ids) > 0 { + var err error + rowsDeleted, err = q.deleteLookupTableRows(ctx, table, ids) + if err != nil { + return 0, fmt.Errorf("could not delete orphaned rows: %w", err) } } - return results, nil + return rowsDeleted, nil } var historyLookupTables = map[string][]tableObjectFieldPair{ @@ -1100,54 +1110,100 @@ var historyLookupTables = map[string][]tableObjectFieldPair{ }, } -// constructReapLookupTablesQuery creates a query like (using history_claimable_balances +func (q *Q) deleteLookupTableRows(ctx context.Context, table string, ids []int64) (int64, error) { + deleteQuery := constructDeleteLookupTableRowsQuery(table, ids) + result, err := q.ExecRaw( + context.WithValue(ctx, &db.QueryTypeContextKey, db.DeleteQueryType), + deleteQuery, + ) + if err != nil { + return 0, fmt.Errorf("error running query %s : %w", deleteQuery, err) + } + var deletedCount int64 + deletedCount, err = result.RowsAffected() + if err != nil { + return 0, fmt.Errorf("error getting deleted count: %w", err) + } + return deletedCount, nil +} + +// constructDeleteLookupTableRowsQuery creates a query like (using history_claimable_balances // as an example): // -// delete from history_claimable_balances where id in ( -// -// WITH ha_batch AS ( -// SELECT id -// FROM history_claimable_balances -// WHERE id >= 1000 -// ORDER BY id limit 1000 -// ) SELECT e1.id as id FROM ha_batch e1 +// WITH ha_batch AS ( +// SELECT id +// FROM history_claimable_balances +// WHERE IN ($1, $2, ...) ORDER BY id asc FOR UPDATE +// ) DELETE FROM history_claimable_balances WHERE id IN ( +// SELECT e1.id as id FROM ha_batch e1 // WHERE NOT EXISTS (SELECT 1 FROM history_transaction_claimable_balances WHERE history_transaction_claimable_balances.history_claimable_balance_id = id limit 1) // AND NOT EXISTS (SELECT 1 FROM history_operation_claimable_balances WHERE history_operation_claimable_balances.history_claimable_balance_id = id limit 1) // ) // -// In short it checks the 1000 rows omitting 1000 row of history_claimable_balances +// It checks each of the candidate rows provided in the top level IN clause // and counts occurrences of each row in corresponding history tables. // If there are no history rows for a given id, the row in // history_claimable_balances is removed. // -// The offset param should be increased before each execution. Given that -// some rows will be removed and some will be added by ingestion it's -// possible that rows will be skipped from deletion. But offset is reset -// when it reaches the table size so eventually all orphaned rows are -// deleted. -func constructReapLookupTablesQuery(table string, historyTables []tableObjectFieldPair, batchSize int, offset int64) string { +// Note that the rows are locked using via SELECT FOR UPDATE. The reason +// for that is to maintain safety when ingestion is running concurrently. +// The ingestion loaders will also lock rows from the history lookup tables +// via SELECT FOR KEY SHARE. This will ensure that the reaping transaction +// will block until the ingestion transaction commits (or vice-versa). +func constructDeleteLookupTableRowsQuery(table string, ids []int64) string { + var conditions []string + for _, referencedTable := range historyLookupTables[table] { + conditions = append( + conditions, + fmt.Sprintf( + "NOT EXISTS ( SELECT 1 as row FROM %s WHERE %s.%s = id LIMIT 1)", + referencedTable.name, + referencedTable.name, referencedTable.objectField, + ), + ) + } + + stringIds := make([]string, len(ids)) + for i, id := range ids { + stringIds[i] = strconv.FormatInt(id, 10) + } + innerQuery := fmt.Sprintf( + "SELECT id FROM %s WHERE id IN (%s) ORDER BY id asc FOR UPDATE", + table, + strings.Join(stringIds, ", "), + ) + + deleteQuery := fmt.Sprintf( + "WITH ha_batch AS (%s) DELETE FROM %s WHERE id IN ("+ + "SELECT e1.id as id FROM ha_batch e1 WHERE %s)", + innerQuery, + table, + strings.Join(conditions, " AND "), + ) + return deleteQuery +} + +func constructFindReapLookupTablesQuery(table string, batchSize int, offset int64) string { var conditions []string - for _, historyTable := range historyTables { + for _, referencedTable := range historyLookupTables[table] { conditions = append( conditions, fmt.Sprintf( "NOT EXISTS ( SELECT 1 as row FROM %s WHERE %s.%s = id LIMIT 1)", - historyTable.name, - historyTable.name, historyTable.objectField, + referencedTable.name, + referencedTable.name, referencedTable.objectField, ), ) } return fmt.Sprintf( - "DELETE FROM %s WHERE id IN ("+ - "WITH ha_batch AS (SELECT id FROM %s WHERE id >= %d ORDER BY id limit %d) "+ + "WITH ha_batch AS (SELECT id FROM %s WHERE id >= %d ORDER BY id ASC limit %d) "+ "SELECT e1.id as id FROM ha_batch e1 WHERE ", table, - table, offset, batchSize, - ) + strings.Join(conditions, " AND ") + ")" + ) + strings.Join(conditions, " AND ") } // DeleteRangeAll deletes a range of rows from all history tables between diff --git a/services/horizon/internal/db2/history/main_test.go b/services/horizon/internal/db2/history/main_test.go index 1a28b9e584..a86f4c14a4 100644 --- a/services/horizon/internal/db2/history/main_test.go +++ b/services/horizon/internal/db2/history/main_test.go @@ -69,20 +69,34 @@ func TestElderLedger(t *testing.T) { } } +func TestConstructDeleteLookupTableRowsQuery(t *testing.T) { + query := constructDeleteLookupTableRowsQuery( + "history_accounts", + []int64{100, 20, 30}, + ) + + assert.Equal(t, + "WITH ha_batch AS (SELECT id FROM history_accounts WHERE id IN (100, 20, 30) ORDER BY id asc FOR UPDATE) "+ + "DELETE FROM history_accounts WHERE id IN (SELECT e1.id as id FROM ha_batch e1 "+ + "WHERE NOT EXISTS ( SELECT 1 as row FROM history_transaction_participants WHERE history_transaction_participants.history_account_id = id LIMIT 1) "+ + "AND NOT EXISTS ( SELECT 1 as row FROM history_effects WHERE history_effects.history_account_id = id LIMIT 1) "+ + "AND NOT EXISTS ( SELECT 1 as row FROM history_operation_participants WHERE history_operation_participants.history_account_id = id LIMIT 1) "+ + "AND NOT EXISTS ( SELECT 1 as row FROM history_trades WHERE history_trades.base_account_id = id LIMIT 1) "+ + "AND NOT EXISTS ( SELECT 1 as row FROM history_trades WHERE history_trades.counter_account_id = id LIMIT 1))", query) +} + func TestConstructReapLookupTablesQuery(t *testing.T) { - query := constructReapLookupTablesQuery( + query := constructFindReapLookupTablesQuery( "history_accounts", - historyLookupTables["history_accounts"], 10, 0, ) assert.Equal(t, - "DELETE FROM history_accounts WHERE id IN ("+ - "WITH ha_batch AS (SELECT id FROM history_accounts WHERE id >= 0 ORDER BY id limit 10) SELECT e1.id as id FROM ha_batch e1 "+ + "WITH ha_batch AS (SELECT id FROM history_accounts WHERE id >= 0 ORDER BY id ASC limit 10) SELECT e1.id as id FROM ha_batch e1 "+ "WHERE NOT EXISTS ( SELECT 1 as row FROM history_transaction_participants WHERE history_transaction_participants.history_account_id = id LIMIT 1) "+ "AND NOT EXISTS ( SELECT 1 as row FROM history_effects WHERE history_effects.history_account_id = id LIMIT 1) "+ "AND NOT EXISTS ( SELECT 1 as row FROM history_operation_participants WHERE history_operation_participants.history_account_id = id LIMIT 1) "+ "AND NOT EXISTS ( SELECT 1 as row FROM history_trades WHERE history_trades.base_account_id = id LIMIT 1) "+ - "AND NOT EXISTS ( SELECT 1 as row FROM history_trades WHERE history_trades.counter_account_id = id LIMIT 1))", query) + "AND NOT EXISTS ( SELECT 1 as row FROM history_trades WHERE history_trades.counter_account_id = id LIMIT 1)", query) } diff --git a/services/horizon/internal/db2/history/operation_participant_batch_insert_builder_test.go b/services/horizon/internal/db2/history/operation_participant_batch_insert_builder_test.go index 7e823064f2..eb30fe6659 100644 --- a/services/horizon/internal/db2/history/operation_participant_batch_insert_builder_test.go +++ b/services/horizon/internal/db2/history/operation_participant_batch_insert_builder_test.go @@ -15,7 +15,7 @@ func TestAddOperationParticipants(t *testing.T) { test.ResetHorizonDB(t, tt.HorizonDB) q := &Q{tt.HorizonSession()} - accountLoader := NewAccountLoader() + accountLoader := NewAccountLoader(ConcurrentInserts) address := keypair.MustRandom().Address() tt.Assert.NoError(q.Begin(tt.Ctx)) builder := q.NewOperationParticipantBatchInsertBuilder() diff --git a/services/horizon/internal/db2/history/operation_test.go b/services/horizon/internal/db2/history/operation_test.go index 1d20a9cb10..8899bfcdf6 100644 --- a/services/horizon/internal/db2/history/operation_test.go +++ b/services/horizon/internal/db2/history/operation_test.go @@ -125,7 +125,7 @@ func TestOperationByLiquidityPool(t *testing.T) { // Insert Liquidity Pool history liquidityPoolID := "a2f38836a839de008cf1d782c81f45e1253cc5d3dad9110b872965484fec0a49" - lpLoader := NewLiquidityPoolLoader() + lpLoader := NewLiquidityPoolLoader(ConcurrentInserts) lpOperationBuilder := q.NewOperationLiquidityPoolBatchInsertBuilder() tt.Assert.NoError(lpOperationBuilder.Add(opID1, lpLoader.GetFuture(liquidityPoolID))) diff --git a/services/horizon/internal/db2/history/participants_test.go b/services/horizon/internal/db2/history/participants_test.go index 37f7654abb..15d09da0ac 100644 --- a/services/horizon/internal/db2/history/participants_test.go +++ b/services/horizon/internal/db2/history/participants_test.go @@ -35,7 +35,7 @@ func TestTransactionParticipantsBatch(t *testing.T) { q := &Q{tt.HorizonSession()} batch := q.NewTransactionParticipantsBatchInsertBuilder() - accountLoader := NewAccountLoader() + accountLoader := NewAccountLoader(ConcurrentInserts) transactionID := int64(1) otherTransactionID := int64(2) diff --git a/services/horizon/internal/db2/history/reap_test.go b/services/horizon/internal/db2/history/reap_test.go index 5601cd19b6..af20fbc976 100644 --- a/services/horizon/internal/db2/history/reap_test.go +++ b/services/horizon/internal/db2/history/reap_test.go @@ -1,13 +1,43 @@ package history_test import ( + "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/stellar/go/services/horizon/internal/db2/history" "github.com/stellar/go/services/horizon/internal/ingest" "github.com/stellar/go/services/horizon/internal/test" ) +type reapResult struct { + Offset int64 + RowsDeleted int64 +} + +func reapLookupTables(t *testing.T, q *history.Q, batchSize int) map[string]reapResult { + results := map[string]reapResult{} + + for _, table := range []string{ + "history_accounts", + "history_assets", + "history_claimable_balances", + "history_liquidity_pools", + } { + ids, offset, err := q.FindLookupTableRowsToReap(context.Background(), table, batchSize) + assert.NoError(t, err) + rowsDeleted, err := q.ReapLookupTable(context.Background(), table, ids, offset) + assert.NoError(t, err) + results[table] = reapResult{ + Offset: offset, + RowsDeleted: rowsDeleted, + } + } + + return results +} + func TestReapLookupTables(t *testing.T) { tt := test.Start(t) defer tt.Finish() @@ -46,14 +76,7 @@ func TestReapLookupTables(t *testing.T) { q := &history.Q{tt.HorizonSession()} - err = q.Begin(tt.Ctx) - tt.Require.NoError(err) - - results, err := q.ReapLookupTables(tt.Ctx, 5) - tt.Require.NoError(err) - - err = q.Commit() - tt.Require.NoError(err) + results := reapLookupTables(t, q, 5) err = db.GetRaw(tt.Ctx, &curLedgers, `SELECT COUNT(*) FROM history_ledgers`) tt.Require.NoError(err) @@ -91,14 +114,7 @@ func TestReapLookupTables(t *testing.T) { tt.Assert.Equal(int64(0), results["history_claimable_balances"].Offset) tt.Assert.Equal(int64(0), results["history_liquidity_pools"].Offset) - err = q.Begin(tt.Ctx) - tt.Require.NoError(err) - - results, err = q.ReapLookupTables(tt.Ctx, 5) - tt.Require.NoError(err) - - err = q.Commit() - tt.Require.NoError(err) + results = reapLookupTables(t, q, 5) err = db.GetRaw(tt.Ctx, &curAccounts, `SELECT COUNT(*) FROM history_accounts`) tt.Require.NoError(err) @@ -121,14 +137,7 @@ func TestReapLookupTables(t *testing.T) { tt.Assert.Equal(int64(0), results["history_claimable_balances"].Offset) tt.Assert.Equal(int64(0), results["history_liquidity_pools"].Offset) - err = q.Begin(tt.Ctx) - tt.Require.NoError(err) - - results, err = q.ReapLookupTables(tt.Ctx, 1000) - tt.Require.NoError(err) - - err = q.Commit() - tt.Require.NoError(err) + results = reapLookupTables(t, q, 1000) err = db.GetRaw(tt.Ctx, &curAccounts, `SELECT COUNT(*) FROM history_accounts`) tt.Require.NoError(err) diff --git a/services/horizon/internal/db2/history/transaction_test.go b/services/horizon/internal/db2/history/transaction_test.go index 65c6734644..bd8cb1673c 100644 --- a/services/horizon/internal/db2/history/transaction_test.go +++ b/services/horizon/internal/db2/history/transaction_test.go @@ -79,7 +79,7 @@ func TestTransactionByLiquidityPool(t *testing.T) { // Insert Liquidity Pool history liquidityPoolID := "a2f38836a839de008cf1d782c81f45e1253cc5d3dad9110b872965484fec0a49" - lpLoader := NewLiquidityPoolLoader() + lpLoader := NewLiquidityPoolLoader(ConcurrentInserts) lpTransactionBuilder := q.NewTransactionLiquidityPoolBatchInsertBuilder() tt.Assert.NoError(lpTransactionBuilder.Add(txID, lpLoader.GetFuture(liquidityPoolID))) tt.Assert.NoError(lpLoader.Exec(tt.Ctx, q)) @@ -940,15 +940,15 @@ func TestTransactionQueryBuilder(t *testing.T) { tt.Assert.NoError(q.Begin(tt.Ctx)) address := "GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON" - accountLoader := NewAccountLoader() + accountLoader := NewAccountLoader(ConcurrentInserts) accountLoader.GetFuture(address) cbID := "00000000178826fbfe339e1f5c53417c6fedfe2c05e8bec14303143ec46b38981b09c3f9" - cbLoader := NewClaimableBalanceLoader() + cbLoader := NewClaimableBalanceLoader(ConcurrentInserts) cbLoader.GetFuture(cbID) lpID := "0000a8198b5e25994c1ca5b0556faeb27325ac746296944144e0a7406d501e8a" - lpLoader := NewLiquidityPoolLoader() + lpLoader := NewLiquidityPoolLoader(ConcurrentInserts) lpLoader.GetFuture(lpID) tt.Assert.NoError(accountLoader.Exec(tt.Ctx, q)) diff --git a/services/horizon/internal/db2/history/verify_lock.go b/services/horizon/internal/db2/history/verify_lock.go index 29bc11a473..4d7d1fbde7 100644 --- a/services/horizon/internal/db2/history/verify_lock.go +++ b/services/horizon/internal/db2/history/verify_lock.go @@ -13,9 +13,13 @@ const ( // all ingesting nodes use the same value which is why it's hard coded here.`1 stateVerificationLockId = 73897213 // reaperLockId is the objid for the advisory lock acquired during - // reaping. The value is arbitrary. The only requirement is that + // reaping of history tables. The value is arbitrary. The only requirement is that // all ingesting nodes use the same value which is why it's hard coded here. reaperLockId = 944670730 + // lookupTableReaperLockId is the objid for the advisory lock acquired during + // reaping of lookup tables. The value is arbitrary. The only requirement is that + // all ingesting nodes use the same value which is why it's hard coded here. + lookupTableReaperLockId = 329518896 ) // TryStateVerificationLock attempts to acquire the state verification lock @@ -34,6 +38,10 @@ func (q *Q) TryReaperLock(ctx context.Context) (bool, error) { return q.tryAdvisoryLock(ctx, reaperLockId) } +func (q *Q) TryLookupTableReaperLock(ctx context.Context) (bool, error) { + return q.tryAdvisoryLock(ctx, lookupTableReaperLockId) +} + func (q *Q) tryAdvisoryLock(ctx context.Context, lockId int) (bool, error) { if tx := q.GetTx(); tx == nil { return false, errors.New("cannot be called outside of a transaction") diff --git a/services/horizon/internal/ingest/main.go b/services/horizon/internal/ingest/main.go index 64e4558723..63ee7ba457 100644 --- a/services/horizon/internal/ingest/main.go +++ b/services/horizon/internal/ingest/main.go @@ -70,16 +70,15 @@ const ( // * Ledger ingestion, // * State verifications, // * Metrics updates. - // * Reaping (requires 2 connections, the extra connection is used for holding the advisory lock) - MaxDBConnections = 5 + // * Reaping of history (requires 2 connections, the extra connection is used for holding the advisory lock) + // * Reaping of lookup tables (requires 2 connections, the extra connection is used for holding the advisory lock) + MaxDBConnections = 7 stateVerificationErrorThreshold = 3 // 100 ledgers per flush has shown in stress tests // to be best point on performance curve, default to that. MaxLedgersPerFlush uint32 = 100 - - reapLookupTablesBatchSize = 1000 ) var log = logpkg.DefaultLogger.WithField("service", "ingest") @@ -172,9 +171,6 @@ type Metrics struct { // duration of rebuilding trade aggregation buckets. LedgerIngestionTradeAggregationDuration prometheus.Summary - ReapDurationByLookupTable *prometheus.SummaryVec - RowsReapedByLookupTable *prometheus.SummaryVec - // StateVerifyDuration exposes timing metrics about the rate and // duration of state verification. StateVerifyDuration prometheus.Summary @@ -256,7 +252,8 @@ type system struct { maxLedgerPerFlush uint32 - reaper *Reaper + reaper *Reaper + lookupTableReaper *lookupTableReaper currentStateMutex sync.Mutex currentState State @@ -369,6 +366,7 @@ func NewSystem(config Config) (System, error) { config.ReapConfig, config.HistorySession, ), + lookupTableReaper: newLookupTableReaper(config.HistorySession), } system.initMetrics() @@ -409,18 +407,6 @@ func (s *system) initMetrics() { Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, }) - s.metrics.ReapDurationByLookupTable = prometheus.NewSummaryVec(prometheus.SummaryOpts{ - Namespace: "horizon", Subsystem: "ingest", Name: "reap_lookup_tables_duration_seconds", - Help: "reap lookup tables durations, sliding window = 10m", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }, []string{"table"}) - - s.metrics.RowsReapedByLookupTable = prometheus.NewSummaryVec(prometheus.SummaryOpts{ - Namespace: "horizon", Subsystem: "ingest", Name: "reap_lookup_tables_rows_reaped", - Help: "rows deleted during lookup tables reap, sliding window = 10m", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }, []string{"table"}) - s.metrics.StateVerifyDuration = prometheus.NewSummary(prometheus.SummaryOpts{ Namespace: "horizon", Subsystem: "ingest", Name: "state_verify_duration_seconds", Help: "state verification durations, sliding window = 10m", @@ -538,8 +524,6 @@ func (s *system) RegisterMetrics(registry *prometheus.Registry) { registry.MustRegister(s.metrics.LocalLatestLedger) registry.MustRegister(s.metrics.LedgerIngestionDuration) registry.MustRegister(s.metrics.LedgerIngestionTradeAggregationDuration) - registry.MustRegister(s.metrics.ReapDurationByLookupTable) - registry.MustRegister(s.metrics.RowsReapedByLookupTable) registry.MustRegister(s.metrics.StateVerifyDuration) registry.MustRegister(s.metrics.StateInvalidGauge) registry.MustRegister(s.metrics.LedgerStatsCounter) @@ -552,6 +536,7 @@ func (s *system) RegisterMetrics(registry *prometheus.Registry) { registry.MustRegister(s.metrics.IngestionErrorCounter) s.ledgerBackend = ledgerbackend.WithMetrics(s.ledgerBackend, registry, "horizon") s.reaper.RegisterMetrics(registry) + s.lookupTableReaper.RegisterMetrics(registry) } // Run starts ingestion system. Ingestion system supports distributed ingestion @@ -822,55 +807,11 @@ func (s *system) maybeReapLookupTables(lastIngestedLedger uint32) { return } - err = s.historyQ.Begin(s.ctx) - if err != nil { - log.WithError(err).Error("Error starting a transaction") - return - } - defer s.historyQ.Rollback() - - // If so block ingestion in the cluster to reap tables - _, err = s.historyQ.GetLastLedgerIngest(s.ctx) - if err != nil { - log.WithError(err).Error(getLastIngestedErrMsg) - return - } - - // Make sure reaping will not take more than 5s, which is average ledger - // closing time. - ctx, cancel := context.WithTimeout(s.ctx, 5*time.Second) - defer cancel() - - reapStart := time.Now() - results, err := s.historyQ.ReapLookupTables(ctx, reapLookupTablesBatchSize) - if err != nil { - log.WithError(err).Warn("Error reaping lookup tables") - return - } - - err = s.historyQ.Commit() - if err != nil { - log.WithError(err).Error("Error committing a transaction") - return - } - - totalDeleted := int64(0) - reapLog := log - for table, result := range results { - totalDeleted += result.RowsDeleted - reapLog = reapLog.WithField(table+"_offset", result.Offset) - reapLog = reapLog.WithField(table+"_duration", result.Duration) - reapLog = reapLog.WithField(table+"_rows_deleted", result.RowsDeleted) - s.Metrics().RowsReapedByLookupTable.With(prometheus.Labels{"table": table}).Observe(float64(result.RowsDeleted)) - s.Metrics().ReapDurationByLookupTable.With(prometheus.Labels{"table": table}).Observe(result.Duration.Seconds()) - } - - if totalDeleted > 0 { - reapLog.Info("Reaper deleted rows from lookup tables") - } - - s.Metrics().RowsReapedByLookupTable.With(prometheus.Labels{"table": "total"}).Observe(float64(totalDeleted)) - s.Metrics().ReapDurationByLookupTable.With(prometheus.Labels{"table": "total"}).Observe(time.Since(reapStart).Seconds()) + s.wg.Add(1) + go func() { + defer s.wg.Done() + s.lookupTableReaper.deleteOrphanedRows(s.ctx) + }() } func (s *system) incrementStateVerificationErrors() int { diff --git a/services/horizon/internal/ingest/main_test.go b/services/horizon/internal/ingest/main_test.go index d5733ee5e4..470039fd92 100644 --- a/services/horizon/internal/ingest/main_test.go +++ b/services/horizon/internal/ingest/main_test.go @@ -462,6 +462,11 @@ func (m *mockDBQ) TryReaperLock(ctx context.Context) (bool, error) { return args.Get(0).(bool), args.Error(1) } +func (m *mockDBQ) TryLookupTableReaperLock(ctx context.Context) (bool, error) { + args := m.Called(ctx) + return args.Get(0).(bool), args.Error(1) +} + func (m *mockDBQ) GetNextLedgerSequence(ctx context.Context, start uint32) (uint32, bool, error) { args := m.Called(ctx, start) return args.Get(0).(uint32), args.Get(1).(bool), args.Error(2) @@ -562,13 +567,14 @@ func (m *mockDBQ) NewTradeBatchInsertBuilder() history.TradeBatchInsertBuilder { return args.Get(0).(history.TradeBatchInsertBuilder) } -func (m *mockDBQ) ReapLookupTables(ctx context.Context, batchSize int) (map[string]history.LookupTableReapResult, error) { - args := m.Called(ctx, batchSize) - var r1 map[string]history.LookupTableReapResult - if args.Get(0) != nil { - r1 = args.Get(0).(map[string]history.LookupTableReapResult) - } - return r1, args.Error(2) +func (m *mockDBQ) FindLookupTableRowsToReap(ctx context.Context, table string, batchSize int) ([]int64, int64, error) { + args := m.Called(ctx, table, batchSize) + return args.Get(0).([]int64), args.Get(1).(int64), args.Error(2) +} + +func (m *mockDBQ) ReapLookupTable(ctx context.Context, table string, ids []int64, offset int64) (int64, error) { + args := m.Called(ctx, table, ids, offset) + return args.Get(0).(int64), args.Error(1) } func (m *mockDBQ) RebuildTradeAggregationTimes(ctx context.Context, from, to strtime.Millis, roundingSlippageFilter int) error { diff --git a/services/horizon/internal/ingest/processor_runner.go b/services/horizon/internal/ingest/processor_runner.go index e6f0e0cf74..75b8645953 100644 --- a/services/horizon/internal/ingest/processor_runner.go +++ b/services/horizon/internal/ingest/processor_runner.go @@ -133,11 +133,11 @@ func buildChangeProcessor( }) } -func (s *ProcessorRunner) buildTransactionProcessor(ledgersProcessor *processors.LedgersProcessor) (groupLoaders, *groupTransactionProcessors) { - accountLoader := history.NewAccountLoader() - assetLoader := history.NewAssetLoader() - lpLoader := history.NewLiquidityPoolLoader() - cbLoader := history.NewClaimableBalanceLoader() +func (s *ProcessorRunner) buildTransactionProcessor(ledgersProcessor *processors.LedgersProcessor, concurrencyMode history.ConcurrencyMode) (groupLoaders, *groupTransactionProcessors) { + accountLoader := history.NewAccountLoader(concurrencyMode) + assetLoader := history.NewAssetLoader(concurrencyMode) + lpLoader := history.NewLiquidityPoolLoader(concurrencyMode) + cbLoader := history.NewClaimableBalanceLoader(concurrencyMode) loaders := newGroupLoaders([]horizonLazyLoader{accountLoader, assetLoader, lpLoader, cbLoader}) statsLedgerTransactionProcessor := processors.NewStatsLedgerTransactionProcessor() @@ -366,7 +366,7 @@ func (s *ProcessorRunner) streamLedger(ledger xdr.LedgerCloseMeta, return nil } -func (s *ProcessorRunner) runTransactionProcessorsOnLedger(registry nameRegistry, ledger xdr.LedgerCloseMeta) ( +func (s *ProcessorRunner) runTransactionProcessorsOnLedger(registry nameRegistry, ledger xdr.LedgerCloseMeta, concurrencyMode history.ConcurrencyMode) ( transactionStats processors.StatsLedgerTransactionProcessorResults, transactionDurations runDurations, tradeStats processors.TradeStats, @@ -381,7 +381,7 @@ func (s *ProcessorRunner) runTransactionProcessorsOnLedger(registry nameRegistry groupTransactionFilterers := s.buildTransactionFilterer() // when in online mode, the submission result processor must always run (regardless of whether filter rules exist or not) groupFilteredOutProcessors := s.buildFilteredOutProcessor() - loaders, groupTransactionProcessors := s.buildTransactionProcessor(ledgersProcessor) + loaders, groupTransactionProcessors := s.buildTransactionProcessor(ledgersProcessor, concurrencyMode) if err = registerTransactionProcessors( registry, @@ -494,7 +494,7 @@ func (s *ProcessorRunner) RunTransactionProcessorsOnLedgers(ledgers []xdr.Ledger groupTransactionFilterers := s.buildTransactionFilterer() // intentionally skip filtered out processor groupFilteredOutProcessors := newGroupTransactionProcessors(nil, nil, nil) - loaders, groupTransactionProcessors := s.buildTransactionProcessor(ledgersProcessor) + loaders, groupTransactionProcessors := s.buildTransactionProcessor(ledgersProcessor, history.ConcurrentInserts) startTime := time.Now() curHeap, sysHeap := getMemStats() @@ -611,7 +611,7 @@ func (s *ProcessorRunner) RunAllProcessorsOnLedger(ledger xdr.LedgerCloseMeta) ( return } - transactionStats, transactionDurations, tradeStats, loaderDurations, loaderStats, err := s.runTransactionProcessorsOnLedger(registry, ledger) + transactionStats, transactionDurations, tradeStats, loaderDurations, loaderStats, err := s.runTransactionProcessorsOnLedger(registry, ledger, history.ConcurrentDeletes) stats.changeStats = changeStatsProcessor.GetResults() stats.changeDurations = groupChangeProcessors.processorsRunDurations diff --git a/services/horizon/internal/ingest/processor_runner_test.go b/services/horizon/internal/ingest/processor_runner_test.go index e6ce6b512c..82c712b737 100644 --- a/services/horizon/internal/ingest/processor_runner_test.go +++ b/services/horizon/internal/ingest/processor_runner_test.go @@ -248,7 +248,7 @@ func TestProcessorRunnerBuildTransactionProcessor(t *testing.T) { ledgersProcessor := &processors.LedgersProcessor{} - _, processor := runner.buildTransactionProcessor(ledgersProcessor) + _, processor := runner.buildTransactionProcessor(ledgersProcessor, history.ConcurrentInserts) assert.IsType(t, &groupTransactionProcessors{}, processor) assert.IsType(t, &processors.StatsLedgerTransactionProcessor{}, processor.processors[0]) assert.IsType(t, &processors.EffectProcessor{}, processor.processors[1]) diff --git a/services/horizon/internal/ingest/processors/claimable_balances_transaction_processor_test.go b/services/horizon/internal/ingest/processors/claimable_balances_transaction_processor_test.go index ca918e08ea..5967ef41b1 100644 --- a/services/horizon/internal/ingest/processors/claimable_balances_transaction_processor_test.go +++ b/services/horizon/internal/ingest/processors/claimable_balances_transaction_processor_test.go @@ -44,7 +44,7 @@ func (s *ClaimableBalancesTransactionProcessorTestSuiteLedger) SetupTest() { }, }, } - s.cbLoader = history.NewClaimableBalanceLoader() + s.cbLoader = history.NewClaimableBalanceLoader(history.ConcurrentInserts) s.processor = NewClaimableBalancesTransactionProcessor( s.cbLoader, diff --git a/services/horizon/internal/ingest/processors/effects_processor_test.go b/services/horizon/internal/ingest/processors/effects_processor_test.go index 0243768fde..276f6fcb03 100644 --- a/services/horizon/internal/ingest/processors/effects_processor_test.go +++ b/services/horizon/internal/ingest/processors/effects_processor_test.go @@ -7,11 +7,11 @@ import ( "crypto/rand" "encoding/hex" "encoding/json" + "math/big" + "strings" "testing" "github.com/guregu/null" - "math/big" - "strings" "github.com/stellar/go/keypair" "github.com/stellar/go/protocols/horizon/base" @@ -62,7 +62,7 @@ func TestEffectsProcessorTestSuiteLedger(t *testing.T) { func (s *EffectsProcessorTestSuiteLedger) SetupTest() { s.ctx = context.Background() - s.accountLoader = history.NewAccountLoader() + s.accountLoader = history.NewAccountLoader(history.ConcurrentInserts) s.mockBatchInsertBuilder = &history.MockEffectBatchInsertBuilder{} s.lcm = xdr.LedgerCloseMeta{ @@ -446,7 +446,7 @@ func TestEffectsCoversAllOperationTypes(t *testing.T) { } assert.True(t, err2 != nil || err == nil, s) }() - err = operation.ingestEffects(history.NewAccountLoader(), &history.MockEffectBatchInsertBuilder{}) + err = operation.ingestEffects(history.NewAccountLoader(history.ConcurrentInserts), &history.MockEffectBatchInsertBuilder{}) }() } @@ -468,7 +468,7 @@ func TestEffectsCoversAllOperationTypes(t *testing.T) { ledgerSequence: 1, } // calling effects should error due to the unknown operation - err := operation.ingestEffects(history.NewAccountLoader(), &history.MockEffectBatchInsertBuilder{}) + err := operation.ingestEffects(history.NewAccountLoader(history.ConcurrentInserts), &history.MockEffectBatchInsertBuilder{}) assert.Contains(t, err.Error(), "Unknown operation type") } @@ -2558,7 +2558,7 @@ type effect struct { } func assertIngestEffects(t *testing.T, operation transactionOperationWrapper, expected []effect) { - accountLoader := history.NewAccountLoader() + accountLoader := history.NewAccountLoader(history.ConcurrentInserts) mockBatchInsertBuilder := &history.MockEffectBatchInsertBuilder{} for _, expectedEffect := range expected { diff --git a/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor_test.go b/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor_test.go index 8d08e44d44..cdafc5bcc3 100644 --- a/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor_test.go +++ b/services/horizon/internal/ingest/processors/liquidity_pools_transaction_processor_test.go @@ -44,7 +44,7 @@ func (s *LiquidityPoolsTransactionProcessorTestSuiteLedger) SetupTest() { }, }, } - s.lpLoader = history.NewLiquidityPoolLoader() + s.lpLoader = history.NewLiquidityPoolLoader(history.ConcurrentInserts) s.processor = NewLiquidityPoolsTransactionProcessor( s.lpLoader, diff --git a/services/horizon/internal/ingest/processors/participants_processor_test.go b/services/horizon/internal/ingest/processors/participants_processor_test.go index b81bd22f67..f6154b2b39 100644 --- a/services/horizon/internal/ingest/processors/participants_processor_test.go +++ b/services/horizon/internal/ingest/processors/participants_processor_test.go @@ -86,7 +86,7 @@ func (s *ParticipantsProcessorTestSuiteLedger) SetupTest() { s.thirdTx.Envelope.V1.Tx.SourceAccount = aid.ToMuxedAccount() s.thirdTxID = toid.New(int32(sequence), 3, 0).ToInt64() - s.accountLoader = history.NewAccountLoader() + s.accountLoader = history.NewAccountLoader(history.ConcurrentInserts) s.addressToFuture = map[string]history.FutureAccountID{} for _, address := range s.addresses { s.addressToFuture[address] = s.accountLoader.GetFuture(address) diff --git a/services/horizon/internal/ingest/reap.go b/services/horizon/internal/ingest/reap.go index 07a61a4cde..63b56de993 100644 --- a/services/horizon/internal/ingest/reap.go +++ b/services/horizon/internal/ingest/reap.go @@ -16,6 +16,8 @@ import ( "github.com/stellar/go/toid" ) +const reapLookupTablesBatchSize = 1000 + // Reaper represents the history reaping subsystem of horizon. type Reaper struct { historyQ history.IngestionQ @@ -243,3 +245,117 @@ func (r *Reaper) deleteBatch(ctx context.Context, batchStartSeq, batchEndSeq uin r.deleteBatchDuration.Observe(elapsedSeconds) return count, nil } + +type lookupTableReaper struct { + historyQ history.IngestionQ + reapLockQ history.IngestionQ + pending atomic.Bool + logger *logpkg.Entry + + reapDurationByLookupTable *prometheus.SummaryVec + rowsReapedByLookupTable *prometheus.SummaryVec +} + +func newLookupTableReaper(dbSession db.SessionInterface) *lookupTableReaper { + return &lookupTableReaper{ + historyQ: &history.Q{dbSession.Clone()}, + reapLockQ: &history.Q{dbSession.Clone()}, + pending: atomic.Bool{}, + logger: log.WithField("subservice", "lookuptable-reaper"), + reapDurationByLookupTable: prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Namespace: "horizon", Subsystem: "ingest", Name: "reap_lookup_tables_duration_seconds", + Help: "reap lookup tables durations, sliding window = 10m", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, []string{"table", "type"}), + rowsReapedByLookupTable: prometheus.NewSummaryVec(prometheus.SummaryOpts{ + Namespace: "horizon", Subsystem: "ingest", Name: "reap_lookup_tables_rows_reaped", + Help: "rows deleted during lookup tables reap, sliding window = 10m", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, []string{"table"}), + } +} + +func (r *lookupTableReaper) RegisterMetrics(registry *prometheus.Registry) { + registry.MustRegister( + r.reapDurationByLookupTable, + r.rowsReapedByLookupTable, + ) +} + +func (r *lookupTableReaper) deleteOrphanedRows(ctx context.Context) error { + // check if reap is already in progress on this horizon node + if !r.pending.CompareAndSwap(false, true) { + r.logger.Infof("existing reap already in progress, skipping request to start a new one") + return nil + } + defer r.pending.Store(false) + + if err := r.reapLockQ.Begin(ctx); err != nil { + return errors.Wrap(err, "error while starting reaper lock transaction") + } + defer func() { + if err := r.reapLockQ.Rollback(); err != nil { + r.logger.WithField("error", err).Error("failed to release reaper lock") + } + }() + // check if reap is already in progress on another horizon node + if acquired, err := r.reapLockQ.TryLookupTableReaperLock(ctx); err != nil { + return errors.Wrap(err, "error while acquiring reaper database lock") + } else if !acquired { + r.logger.Info("reap already in progress on another node") + return nil + } + + reapStart := time.Now() + var totalQueryDuration, totalDeleteDuration time.Duration + var totalDeleted int64 + for _, table := range []string{ + "history_accounts", "history_claimable_balances", + "history_assets", "history_liquidity_pools", + } { + startTime := time.Now() + ids, offset, err := r.historyQ.FindLookupTableRowsToReap(ctx, table, reapLookupTablesBatchSize) + if err != nil { + r.logger.WithField("table", table).WithError(err).Warn("Error finding orphaned rows") + return err + } + queryDuration := time.Since(startTime) + totalQueryDuration += queryDuration + + deleteStartTime := time.Now() + var rowsDeleted int64 + rowsDeleted, err = r.historyQ.ReapLookupTable(ctx, table, ids, offset) + if err != nil { + r.logger.WithField("table", table).WithError(err).Warn("Error deleting orphaned rows") + return err + } + deleteDuration := time.Since(deleteStartTime) + totalDeleteDuration += deleteDuration + + r.rowsReapedByLookupTable.With(prometheus.Labels{"table": table}). + Observe(float64(rowsDeleted)) + r.reapDurationByLookupTable.With(prometheus.Labels{"table": table, "type": "query"}). + Observe(float64(queryDuration.Seconds())) + r.reapDurationByLookupTable.With(prometheus.Labels{"table": table, "type": "delete"}). + Observe(float64(deleteDuration.Seconds())) + r.reapDurationByLookupTable.With(prometheus.Labels{"table": table, "type": "total"}). + Observe(float64((queryDuration + deleteDuration).Seconds())) + + r.logger.WithField("table", table). + WithField("offset", offset). + WithField("rows_deleted", rowsDeleted). + WithField("query_duration", queryDuration.Seconds()). + WithField("delete_duration", deleteDuration.Seconds()). + Info("Reaper deleted rows from lookup tables") + } + + r.rowsReapedByLookupTable.With(prometheus.Labels{"table": "total"}). + Observe(float64(totalDeleted)) + r.reapDurationByLookupTable.With(prometheus.Labels{"table": "total", "type": "query"}). + Observe(float64(totalQueryDuration.Seconds())) + r.reapDurationByLookupTable.With(prometheus.Labels{"table": "total", "type": "delete"}). + Observe(float64(totalDeleteDuration.Seconds())) + r.reapDurationByLookupTable.With(prometheus.Labels{"table": "total", "type": "total"}). + Observe(time.Since(reapStart).Seconds()) + return nil +} diff --git a/services/horizon/internal/ingest/resume_state_test.go b/services/horizon/internal/ingest/resume_state_test.go index feb5e13bb0..534ec555f6 100644 --- a/services/horizon/internal/ingest/resume_state_test.go +++ b/services/horizon/internal/ingest/resume_state_test.go @@ -402,10 +402,6 @@ func (s *ResumeTestTestSuite) TestReapingObjectsDisabled() { } func (s *ResumeTestTestSuite) TestErrorReapingObjectsIgnored() { - s.system.config.ReapLookupTables = true - defer func() { - s.system.config.ReapLookupTables = false - }() s.historyQ.On("Begin", s.ctx).Return(nil).Once() s.historyQ.On("GetLastLedgerIngest", s.ctx).Return(uint32(100), nil).Once() s.historyQ.On("GetIngestVersion", s.ctx).Return(CurrentVersion, nil).Once() @@ -425,12 +421,6 @@ func (s *ResumeTestTestSuite) TestErrorReapingObjectsIgnored() { s.historyQ.On("GetExpStateInvalid", s.ctx).Return(false, nil).Once() s.historyQ.On("RebuildTradeAggregationBuckets", s.ctx, uint32(101), uint32(101), 0).Return(nil).Once() - // Reap lookup tables: - s.ledgerBackend.On("GetLatestLedgerSequence", s.ctx).Return(uint32(101), nil) - s.historyQ.On("Begin", s.ctx).Return(nil).Once() - s.historyQ.On("GetLastLedgerIngest", s.ctx).Return(uint32(100), nil).Once() - s.historyQ.On("ReapLookupTables", mock.AnythingOfType("*context.timerCtx"), mock.Anything).Return(nil, nil, errors.New("error reaping objects")).Once() - s.historyQ.On("Rollback").Return(nil).Once() mockStats := &historyarchive.MockArchiveStats{} mockStats.On("GetBackendName").Return("name") mockStats.On("GetDownloads").Return(uint32(0)) From 1cd3bbe735310433d542513f0b2683d23c2347a0 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Fri, 6 Sep 2024 15:18:14 -0700 Subject: [PATCH 227/234] services/horizon: Limit sql queries for history endpoints to retention window (#5448) --- services/horizon/internal/actions/effects.go | 4 +- services/horizon/internal/actions/helpers.go | 55 +++--- .../horizon/internal/actions/helpers_test.go | 168 +++++++++++++++--- services/horizon/internal/actions/ledger.go | 4 +- .../horizon/internal/actions/operation.go | 4 +- services/horizon/internal/actions/trade.go | 9 +- .../horizon/internal/actions/transaction.go | 4 +- 7 files changed, 186 insertions(+), 62 deletions(-) diff --git a/services/horizon/internal/actions/effects.go b/services/horizon/internal/actions/effects.go index e04997aca5..d8620fc11f 100644 --- a/services/horizon/internal/actions/effects.go +++ b/services/horizon/internal/actions/effects.go @@ -51,12 +51,12 @@ type GetEffectsHandler struct { } func (handler GetEffectsHandler) GetResourcePage(w HeaderWriter, r *http.Request) ([]hal.Pageable, error) { - pq, err := GetPageQuery(handler.LedgerState, r, DefaultTOID) + pq, err := GetPageQuery(handler.LedgerState, r) if err != nil { return nil, err } - err = validateCursorWithinHistory(handler.LedgerState, pq) + err = validateAndAdjustCursor(handler.LedgerState, &pq) if err != nil { return nil, err } diff --git a/services/horizon/internal/actions/helpers.go b/services/horizon/internal/actions/helpers.go index 2575b13251..99071bfba7 100644 --- a/services/horizon/internal/actions/helpers.go +++ b/services/horizon/internal/actions/helpers.go @@ -3,6 +3,7 @@ package actions import ( "context" "encoding/hex" + "errors" "fmt" "net/http" "net/url" @@ -20,7 +21,6 @@ import ( "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/services/horizon/internal/ledger" hProblem "github.com/stellar/go/services/horizon/internal/render/problem" - "github.com/stellar/go/support/errors" "github.com/stellar/go/support/ordered" "github.com/stellar/go/support/render/problem" "github.com/stellar/go/toid" @@ -45,8 +45,6 @@ type Opt int const ( // DisableCursorValidation disables cursor validation in GetPageQuery DisableCursorValidation Opt = iota - // DefaultTOID sets a default cursor value in GetPageQuery based on the ledger state - DefaultTOID Opt = iota ) // HeaderWriter is an interface for setting HTTP response headers @@ -171,7 +169,7 @@ func getLimit(r *http.Request, name string, def uint64, max uint64) (uint64, err if asI64 <= 0 { err = errors.New("invalid limit: non-positive value provided") } else if asI64 > int64(max) { - err = errors.Errorf("invalid limit: value provided that is over limit max of %d", max) + err = fmt.Errorf("invalid limit: value provided that is over limit max of %d", max) } if err != nil { @@ -185,14 +183,10 @@ func getLimit(r *http.Request, name string, def uint64, max uint64) (uint64, err // using the results from a call to GetPagingParams() func GetPageQuery(ledgerState *ledger.State, r *http.Request, opts ...Opt) (db2.PageQuery, error) { disableCursorValidation := false - defaultTOID := false for _, opt := range opts { if opt == DisableCursorValidation { disableCursorValidation = true } - if opt == DefaultTOID { - defaultTOID = true - } } cursor, err := getCursor(ledgerState, r, ParamCursor) @@ -221,13 +215,6 @@ func GetPageQuery(ledgerState *ledger.State, r *http.Request, opts ...Opt) (db2. return db2.PageQuery{}, err } - if cursor == "" && defaultTOID { - if pageQuery.Order == db2.OrderAscending { - pageQuery.Cursor = toid.AfterLedger( - ordered.Max(0, ledgerState.CurrentStatus().HistoryElder-1), - ).String() - } - } return pageQuery, nil } @@ -553,19 +540,37 @@ func validateAssetParams(aType, code, issuer, prefix string) error { return nil } -// validateCursorWithinHistory compares the requested page of data against the +// validateAndAdjustCursor compares the requested page of data against the // ledger state of the history database. In the event that the cursor is // guaranteed to return no results, we return a 410 GONE http response. -func validateCursorWithinHistory(ledgerState *ledger.State, pq db2.PageQuery) error { - // an ascending query should never return a gone response: An ascending query - // prior to known history should return results at the beginning of history, - // and an ascending query beyond the end of history should not error out but - // rather return an empty page (allowing code that tracks the procession of - // some resource more easily). - if pq.Order != "desc" { - return nil +// For ascending queries, we adjust the cursor to ensure it starts at +// the oldest available ledger. +func validateAndAdjustCursor(ledgerState *ledger.State, pq *db2.PageQuery) error { + err := validateCursorWithinHistory(ledgerState, *pq) + + if pq.Order == db2.OrderAscending { + // an ascending query should never return a gone response: An ascending query + // prior to known history should return results at the beginning of history, + // and an ascending query beyond the end of history should not error out but + // rather return an empty page (allowing code that tracks the procession of + // some resource more easily). + + // set/modify the cursor for ascending queries to start at the oldest available ledger if it + // precedes the oldest ledger. This avoids inefficient queries caused by index bloat from deleted rows + // that are removed as part of reaping to maintain the retention window. + if pq.Cursor == "" || errors.Is(err, &hProblem.BeforeHistory) { + pq.Cursor = toid.AfterLedger( + ordered.Max(0, ledgerState.CurrentStatus().HistoryElder-1), + ).String() + return nil + } } + return err +} +// validateCursorWithinHistory checks if the cursor is within the known history range. +// If the cursor is before the oldest available ledger, it returns BeforeHistory error. +func validateCursorWithinHistory(ledgerState *ledger.State, pq db2.PageQuery) error { var cursor int64 var err error @@ -596,7 +601,7 @@ func countNonEmpty(params ...interface{}) (int, error) { for _, param := range params { switch param := param.(type) { default: - return 0, errors.Errorf("unexpected type %T", param) + return 0, fmt.Errorf("unexpected type %T", param) case int32: if param != 0 { count++ diff --git a/services/horizon/internal/actions/helpers_test.go b/services/horizon/internal/actions/helpers_test.go index 05e840577e..e393cc357e 100644 --- a/services/horizon/internal/actions/helpers_test.go +++ b/services/horizon/internal/actions/helpers_test.go @@ -16,6 +16,7 @@ import ( horizonContext "github.com/stellar/go/services/horizon/internal/context" "github.com/stellar/go/services/horizon/internal/db2" "github.com/stellar/go/services/horizon/internal/ledger" + hProblem "github.com/stellar/go/services/horizon/internal/render/problem" "github.com/stellar/go/services/horizon/internal/test" "github.com/stellar/go/support/db" "github.com/stellar/go/support/errors" @@ -126,11 +127,21 @@ func TestValidateCursorWithinHistory(t *testing.T) { { cursor: "0", order: "asc", - valid: true, + valid: false, }, { cursor: "0-1234", order: "asc", + valid: false, + }, + { + cursor: "1", + order: "asc", + valid: true, + }, + { + cursor: "1-1234", + order: "asc", valid: true, }, } @@ -291,11 +302,10 @@ func TestGetPageQuery(t *testing.T) { tt.Assert.Error(err) } -func TestGetPageQueryCursorDefaultTOID(t *testing.T) { - ascReq := makeTestActionRequest("/foo-bar/blah?limit=2", testURLParams()) - descReq := makeTestActionRequest("/foo-bar/blah?limit=2&order=desc", testURLParams()) - +func TestPageQueryCursorDefaultOrder(t *testing.T) { ledgerState := &ledger.State{} + + // truncated history ledgerState.SetHorizonStatus(ledger.HorizonStatus{ HistoryLatest: 7000, HistoryLatestClosedAt: time.Now(), @@ -303,30 +313,30 @@ func TestGetPageQueryCursorDefaultTOID(t *testing.T) { ExpHistoryLatest: 7000, }) - pq, err := GetPageQuery(ledgerState, ascReq, DefaultTOID) + req := makeTestActionRequest("/foo-bar/blah?limit=2", testURLParams()) + + // default asc, w/o cursor + pq, err := GetPageQuery(ledgerState, req) assert.NoError(t, err) + assert.Empty(t, pq.Cursor) + assert.NoError(t, validateAndAdjustCursor(ledgerState, &pq)) assert.Equal(t, toid.AfterLedger(299).String(), pq.Cursor) assert.Equal(t, uint64(2), pq.Limit) assert.Equal(t, "asc", pq.Order) - pq, err = GetPageQuery(ledgerState, descReq, DefaultTOID) - assert.NoError(t, err) - assert.Equal(t, "", pq.Cursor) - assert.Equal(t, uint64(2), pq.Limit) - assert.Equal(t, "desc", pq.Order) + cursor := toid.AfterLedger(200).String() + reqWithCursor := makeTestActionRequest(fmt.Sprintf("/foo-bar/blah?cursor=%s&limit=2", cursor), testURLParams()) - pq, err = GetPageQuery(ledgerState, ascReq) + // default asc, w/ cursor + pq, err = GetPageQuery(ledgerState, reqWithCursor) assert.NoError(t, err) - assert.Empty(t, pq.Cursor) + assert.Equal(t, cursor, pq.Cursor) + assert.NoError(t, validateAndAdjustCursor(ledgerState, &pq)) + assert.Equal(t, toid.AfterLedger(299).String(), pq.Cursor) assert.Equal(t, uint64(2), pq.Limit) assert.Equal(t, "asc", pq.Order) - pq, err = GetPageQuery(ledgerState, descReq) - assert.NoError(t, err) - assert.Empty(t, pq.Cursor) - assert.Equal(t, "", pq.Cursor) - assert.Equal(t, "desc", pq.Order) - + // full history ledgerState.SetHorizonStatus(ledger.HorizonStatus{ HistoryLatest: 7000, HistoryLatestClosedAt: time.Now(), @@ -334,18 +344,130 @@ func TestGetPageQueryCursorDefaultTOID(t *testing.T) { ExpHistoryLatest: 7000, }) - pq, err = GetPageQuery(ledgerState, ascReq, DefaultTOID) + // default asc, w/o cursor + pq, err = GetPageQuery(ledgerState, req) assert.NoError(t, err) + assert.Empty(t, pq.Cursor) + assert.NoError(t, validateAndAdjustCursor(ledgerState, &pq)) assert.Equal(t, toid.AfterLedger(0).String(), pq.Cursor) assert.Equal(t, uint64(2), pq.Limit) assert.Equal(t, "asc", pq.Order) - pq, err = GetPageQuery(ledgerState, descReq, DefaultTOID) + // default asc, w/ cursor + pq, err = GetPageQuery(ledgerState, reqWithCursor) assert.NoError(t, err) - assert.Equal(t, "", pq.Cursor) + assert.Equal(t, cursor, pq.Cursor) + assert.NoError(t, validateAndAdjustCursor(ledgerState, &pq)) + assert.Equal(t, cursor, pq.Cursor) assert.Equal(t, uint64(2), pq.Limit) - assert.Equal(t, "desc", pq.Order) + assert.Equal(t, "asc", pq.Order) + +} + +func TestGetPageQueryWithoutCursor(t *testing.T) { + ledgerState := &ledger.State{} + + validateCursor := func(limit uint64, order string, expectedCursor string) { + req := makeTestActionRequest(fmt.Sprintf("/foo-bar/blah?limit=%d&order=%s", limit, order), testURLParams()) + pq, err := GetPageQuery(ledgerState, req) + assert.NoError(t, err) + assert.Empty(t, pq.Cursor) + assert.NoError(t, validateAndAdjustCursor(ledgerState, &pq)) + assert.Equal(t, expectedCursor, pq.Cursor) + assert.Equal(t, limit, pq.Limit) + assert.Equal(t, order, pq.Order) + } + + // truncated history + ledgerState.SetHorizonStatus(ledger.HorizonStatus{ + HistoryLatest: 7000, + HistoryLatestClosedAt: time.Now(), + HistoryElder: 300, + ExpHistoryLatest: 7000, + }) + + validateCursor(2, "asc", toid.AfterLedger(299).String()) + validateCursor(2, "desc", "") + + // full history + ledgerState.SetHorizonStatus(ledger.HorizonStatus{ + HistoryLatest: 7000, + HistoryLatestClosedAt: time.Now(), + HistoryElder: 0, + ExpHistoryLatest: 7000, + }) + + validateCursor(2, "asc", toid.AfterLedger(0).String()) + validateCursor(2, "desc", "") +} + +func TestValidateAndAdjustCursor(t *testing.T) { + ledgerState := &ledger.State{} + + validateCursor := func(cursor string, limit uint64, order string, expectedCursor string, expectedError error) { + pq := db2.PageQuery{Cursor: cursor, + Limit: limit, + Order: order, + } + err := validateAndAdjustCursor(ledgerState, &pq) + if expectedError != nil { + assert.EqualError(t, expectedError, err.Error()) + } else { + assert.NoError(t, err) + } + assert.Equal(t, expectedCursor, pq.Cursor) + assert.Equal(t, limit, pq.Limit) + assert.Equal(t, order, pq.Order) + } + + // full history + ledgerState.SetHorizonStatus(ledger.HorizonStatus{ + HistoryLatest: 7000, + HistoryLatestClosedAt: time.Now(), + HistoryElder: 0, + ExpHistoryLatest: 7000, + }) + + // invalid cursor + validateCursor("blah", 2, "asc", "blah", problem.BadRequest) + validateCursor("blah", 2, "desc", "blah", problem.BadRequest) + + validateCursor(toid.AfterLedger(0).String(), 2, "asc", toid.AfterLedger(0).String(), nil) + validateCursor(toid.AfterLedger(200).String(), 2, "asc", toid.AfterLedger(200).String(), nil) + validateCursor(toid.AfterLedger(7001).String(), 2, "asc", toid.AfterLedger(7001).String(), nil) + + validateCursor(toid.AfterLedger(0).String(), 2, "desc", toid.AfterLedger(0).String(), nil) + validateCursor(toid.AfterLedger(200).String(), 2, "desc", toid.AfterLedger(200).String(), nil) + validateCursor(toid.AfterLedger(7001).String(), 2, "desc", toid.AfterLedger(7001).String(), nil) + + // truncated history + ledgerState.SetHorizonStatus(ledger.HorizonStatus{ + HistoryLatest: 7000, + HistoryLatestClosedAt: time.Now(), + HistoryElder: 300, + ExpHistoryLatest: 7000, + }) + // invalid cursor + validateCursor("blah", 2, "asc", "blah", problem.BadRequest) + validateCursor("blah", 2, "desc", "blah", problem.BadRequest) + + // asc order + validateCursor(toid.AfterLedger(0).String(), 2, "asc", toid.AfterLedger(299).String(), nil) + validateCursor(toid.AfterLedger(200).String(), 2, "asc", toid.AfterLedger(299).String(), nil) + validateCursor(toid.AfterLedger(298).String(), 2, "asc", toid.AfterLedger(299).String(), nil) + validateCursor(toid.AfterLedger(299).String(), 2, "asc", toid.AfterLedger(299).String(), nil) + validateCursor(toid.AfterLedger(300).String(), 2, "asc", toid.AfterLedger(300).String(), nil) + validateCursor(toid.AfterLedger(301).String(), 2, "asc", toid.AfterLedger(301).String(), nil) + validateCursor(toid.AfterLedger(7001).String(), 2, "asc", toid.AfterLedger(7001).String(), nil) + + // desc order + validateCursor(toid.AfterLedger(0).String(), 2, "desc", toid.AfterLedger(0).String(), hProblem.BeforeHistory) + validateCursor(toid.AfterLedger(200).String(), 2, "desc", toid.AfterLedger(200).String(), hProblem.BeforeHistory) + validateCursor(toid.AfterLedger(299).String(), 2, "desc", toid.AfterLedger(299).String(), hProblem.BeforeHistory) + validateCursor(toid.AfterLedger(300).String(), 2, "desc", toid.AfterLedger(300).String(), nil) + validateCursor(toid.AfterLedger(320).String(), 2, "desc", toid.AfterLedger(320).String(), nil) + validateCursor(toid.AfterLedger(7001).String(), 2, "desc", toid.AfterLedger(7001).String(), nil) } func TestGetString(t *testing.T) { diff --git a/services/horizon/internal/actions/ledger.go b/services/horizon/internal/actions/ledger.go index 0b3d51b8e1..0836ba9b10 100644 --- a/services/horizon/internal/actions/ledger.go +++ b/services/horizon/internal/actions/ledger.go @@ -17,12 +17,12 @@ type GetLedgersHandler struct { } func (handler GetLedgersHandler) GetResourcePage(w HeaderWriter, r *http.Request) ([]hal.Pageable, error) { - pq, err := GetPageQuery(handler.LedgerState, r, DefaultTOID) + pq, err := GetPageQuery(handler.LedgerState, r) if err != nil { return nil, err } - err = validateCursorWithinHistory(handler.LedgerState, pq) + err = validateAndAdjustCursor(handler.LedgerState, &pq) if err != nil { return nil, err } diff --git a/services/horizon/internal/actions/operation.go b/services/horizon/internal/actions/operation.go index 9ccadb272e..6b200cba29 100644 --- a/services/horizon/internal/actions/operation.go +++ b/services/horizon/internal/actions/operation.go @@ -72,12 +72,12 @@ type GetOperationsHandler struct { func (handler GetOperationsHandler) GetResourcePage(w HeaderWriter, r *http.Request) ([]hal.Pageable, error) { ctx := r.Context() - pq, err := GetPageQuery(handler.LedgerState, r, DefaultTOID) + pq, err := GetPageQuery(handler.LedgerState, r) if err != nil { return nil, err } - err = validateCursorWithinHistory(handler.LedgerState, pq) + err = validateAndAdjustCursor(handler.LedgerState, &pq) if err != nil { return nil, err } diff --git a/services/horizon/internal/actions/trade.go b/services/horizon/internal/actions/trade.go index b29ad64715..57d97593cf 100644 --- a/services/horizon/internal/actions/trade.go +++ b/services/horizon/internal/actions/trade.go @@ -159,12 +159,12 @@ type GetTradesHandler struct { func (handler GetTradesHandler) GetResourcePage(w HeaderWriter, r *http.Request) ([]hal.Pageable, error) { ctx := r.Context() - pq, err := GetPageQuery(handler.LedgerState, r, DefaultTOID) + pq, err := GetPageQuery(handler.LedgerState, r) if err != nil { return nil, err } - err = validateCursorWithinHistory(handler.LedgerState, pq) + err = validateAndAdjustCursor(handler.LedgerState, &pq) if err != nil { return nil, err } @@ -287,10 +287,7 @@ func (handler GetTradeAggregationsHandler) GetResource(w HeaderWriter, r *http.R if err != nil { return nil, err } - err = validateCursorWithinHistory(handler.LedgerState, pq) - if err != nil { - return nil, err - } + qp := TradeAggregationsQuery{} if err = getParams(&qp, r); err != nil { return nil, err diff --git a/services/horizon/internal/actions/transaction.go b/services/horizon/internal/actions/transaction.go index 6a2fd1c6e2..ed579f9d6f 100644 --- a/services/horizon/internal/actions/transaction.go +++ b/services/horizon/internal/actions/transaction.go @@ -98,12 +98,12 @@ type GetTransactionsHandler struct { func (handler GetTransactionsHandler) GetResourcePage(w HeaderWriter, r *http.Request) ([]hal.Pageable, error) { ctx := r.Context() - pq, err := GetPageQuery(handler.LedgerState, r, DefaultTOID) + pq, err := GetPageQuery(handler.LedgerState, r) if err != nil { return nil, err } - err = validateCursorWithinHistory(handler.LedgerState, pq) + err = validateAndAdjustCursor(handler.LedgerState, &pq) if err != nil { return nil, err } From 3261ecbd36f45d89591c225e61077c360f4cc9c3 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Wed, 11 Sep 2024 15:12:34 -0400 Subject: [PATCH 228/234] update rpc docker image (#5459) --- .github/workflows/horizon.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index f315e27791..affea796ff 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -35,7 +35,7 @@ jobs: HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_USE_DB: true PROTOCOL_21_CORE_DEBIAN_PKG_VERSION: 21.3.1-2007.4ede19620.focal PROTOCOL_21_CORE_DOCKER_IMG: stellar/stellar-core:21.3.1-2007.4ede19620.focal - PROTOCOL_21_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:21.4.1 + PROTOCOL_21_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:21.5.0 PGHOST: localhost PGPORT: 5432 PGUSER: postgres From f668d5b831715d0b55010c4f5b8d7ea2d6313065 Mon Sep 17 00:00:00 2001 From: George Date: Mon, 16 Sep 2024 15:37:42 -0700 Subject: [PATCH 229/234] .github: Bump RPC reference to latest patch, v21.5.1 (#5461) --- .github/workflows/horizon.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index affea796ff..86e68c3d85 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -35,7 +35,7 @@ jobs: HORIZON_INTEGRATION_TESTS_CAPTIVE_CORE_USE_DB: true PROTOCOL_21_CORE_DEBIAN_PKG_VERSION: 21.3.1-2007.4ede19620.focal PROTOCOL_21_CORE_DOCKER_IMG: stellar/stellar-core:21.3.1-2007.4ede19620.focal - PROTOCOL_21_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:21.5.0 + PROTOCOL_21_SOROBAN_RPC_DOCKER_IMG: stellar/soroban-rpc:21.5.1 PGHOST: localhost PGPORT: 5432 PGUSER: postgres From 6fbba33f8123b69f398ac3cd7166ebac9311fde1 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Tue, 24 Sep 2024 01:25:00 -0700 Subject: [PATCH 230/234] fix CI failures due to race condition in recoverysigner unit tests (#5472) * fix CI failures due to race condition in recoverysigner unit tests * Update support/db/dbtest/db.go Co-authored-by: tamirms --- support/db/dbtest/db.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/support/db/dbtest/db.go b/support/db/dbtest/db.go index 18de073224..42f252b6f8 100644 --- a/support/db/dbtest/db.go +++ b/support/db/dbtest/db.go @@ -128,9 +128,11 @@ func checkReadOnly(t testing.TB, DSN string) { if !rows.Next() { _, err = tx.Exec("CREATE ROLE user_ro WITH LOGIN PASSWORD 'user_ro';") if err != nil { - // Handle race condition by ignoring the error if it's a duplicate key violation - if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "23505" { + // Handle race condition by ignoring the error if it's a duplicate key violation or duplicate object error + if pqErr, ok := err.(*pq.Error); ok && (pqErr.Code == "23505" || pqErr.Code == "42710") { return + } else if ok { + t.Logf("pq error code: %s", pqErr.Code) } } require.NoError(t, err) From 9e0c2c0169fe7f260ea1e00d56be74e28f4fdf36 Mon Sep 17 00:00:00 2001 From: mwtzzz <101583293+mwtzzz@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:21:43 -0700 Subject: [PATCH 231/234] updates to horizon docker builds (#5466) --- services/horizon/docker/Dockerfile | 1 + .../horizon/docker/Dockerfile.core-testing | 19 ++++++++++++ services/horizon/docker/Dockerfile.stable | 18 ++++++++++++ services/horizon/docker/Makefile | 29 +++++++++++++++++++ 4 files changed, 67 insertions(+) create mode 100644 services/horizon/docker/Dockerfile.core-testing create mode 100644 services/horizon/docker/Dockerfile.stable diff --git a/services/horizon/docker/Dockerfile b/services/horizon/docker/Dockerfile index 3c090d203c..2d5c7a01a2 100644 --- a/services/horizon/docker/Dockerfile +++ b/services/horizon/docker/Dockerfile @@ -1,3 +1,4 @@ +# install Core from apt "stable" or "unstable" pool, and horizon from apt "testing" pool FROM ubuntu:focal ARG VERSION diff --git a/services/horizon/docker/Dockerfile.core-testing b/services/horizon/docker/Dockerfile.core-testing new file mode 100644 index 0000000000..c0d1dff8e6 --- /dev/null +++ b/services/horizon/docker/Dockerfile.core-testing @@ -0,0 +1,19 @@ +# install both horizon and core from apt "testing" pool +FROM ubuntu:focal + +ARG VERSION +ARG STELLAR_CORE_VERSION +ARG DEBIAN_FRONTEND=noninteractive +ARG ALLOW_CORE_UNSTABLE=no + +RUN apt-get update && apt-get install -y wget apt-transport-https gnupg2 && \ + wget -qO /etc/apt/trusted.gpg.d/SDF.asc https://apt.stellar.org/SDF.asc && \ + echo "deb https://apt.stellar.org focal testing" | tee -a /etc/apt/sources.list.d/SDF.list && \ + cat /etc/apt/sources.list.d/SDF.list && \ + apt-get update && \ + apt-cache madison stellar-core && eval "apt-get install -y stellar-core${STELLAR_CORE_VERSION+=$STELLAR_CORE_VERSION}" && \ + apt-cache madison stellar-horizon && apt-get install -y stellar-horizon=${VERSION} && \ + apt-get clean && rm -rf /var/lib/apt/lists/* /var/log/*.log /var/log/*/*.log + +EXPOSE 8000 +ENTRYPOINT ["/usr/bin/stellar-horizon"] diff --git a/services/horizon/docker/Dockerfile.stable b/services/horizon/docker/Dockerfile.stable new file mode 100644 index 0000000000..63f268873a --- /dev/null +++ b/services/horizon/docker/Dockerfile.stable @@ -0,0 +1,18 @@ +# install Core from apt "stable"and horizon from apt "stable" pool +FROM ubuntu:focal + +ARG VERSION +ARG STELLAR_CORE_VERSION +ARG DEBIAN_FRONTEND=noninteractive +ARG ALLOW_CORE_UNSTABLE=no + +RUN apt-get update && apt-get install -y wget apt-transport-https gnupg2 && \ + wget -qO /etc/apt/trusted.gpg.d/SDF.asc https://apt.stellar.org/SDF.asc && \ + echo "deb https://apt.stellar.org focal stable" | tee -a /etc/apt/sources.list.d/SDF.list && \ + cat /etc/apt/sources.list.d/SDF.list && \ + apt-get update && apt-cache madison stellar-core && eval "apt-get install -y stellar-core${STELLAR_CORE_VERSION+=$STELLAR_CORE_VERSION}" && \ + apt-cache madison stellar-horizon && apt-get install -y stellar-horizon=${VERSION} && \ + apt-get clean && rm -rf /var/lib/apt/lists/* /var/log/*.log /var/log/*/*.log + +EXPOSE 8000 +ENTRYPOINT ["/usr/bin/stellar-horizon"] diff --git a/services/horizon/docker/Makefile b/services/horizon/docker/Makefile index 51ee19f2fe..26d3c892d0 100644 --- a/services/horizon/docker/Makefile +++ b/services/horizon/docker/Makefile @@ -5,6 +5,7 @@ BUILD_DATE := $(shell date -u +%FT%TZ) TAG ?= stellar/stellar-horizon:$(VERSION) +# build with Core from apt "stable" or "unstable", and horizon from apt "testing" docker-build: ifndef VERSION $(error VERSION environment variable must be set. For example VERSION=2.4.1-101 ) @@ -22,6 +23,34 @@ else -t $(TAG) . endif +# build Core and Horizon from apt "testing" +docker-build-core-testing: +ifndef VERSION + $(error VERSION environment variable must be set. For example VERSION=2.4.1-101 ) +endif +ifndef STELLAR_CORE_VERSION + $(error STELLAR_CORE_VERSION environment variable must be set. ) +else + $(SUDO) docker build --file ./Dockerfile.core-testing --pull $(DOCKER_OPTS) \ + --label org.opencontainers.image.created="$(BUILD_DATE)" \ + --build-arg VERSION=$(VERSION) --build-arg STELLAR_CORE_VERSION=$(STELLAR_CORE_VERSION) \ + -t $(TAG) . +endif + +# build Core and Horizon from apt "stable" +docker-build-core-stable: +ifndef VERSION + $(error VERSION environment variable must be set. For example VERSION=2.4.1-101 ) +endif +ifndef STELLAR_CORE_VERSION + $(error STELLAR_CORE_VERSION environment variable must be set. ) +else + $(SUDO) docker build --file ./Dockerfile.stable --pull $(DOCKER_OPTS) \ + --label org.opencontainers.image.created="$(BUILD_DATE)" \ + --build-arg VERSION=$(VERSION) --build-arg STELLAR_CORE_VERSION=$(STELLAR_CORE_VERSION) \ + -t $(TAG) . +endif + docker-push: ifndef TAG $(error Must set VERSION or TAG environment variable. For example VERSION=2.4.1-101 ) From 59f4e2332a6dc67b33fb688ba284f8938544d7c0 Mon Sep 17 00:00:00 2001 From: shawn Date: Tue, 1 Oct 2024 15:05:47 -0700 Subject: [PATCH 232/234] refreshed cdp arch diagram on dev guide (#5483) --- services/galexie/architecture.png | Bin 357065 -> 728351 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/services/galexie/architecture.png b/services/galexie/architecture.png index 85bd6d8b31d6224a6897681351c9b8f90f48d479..c19a313ebef18b2a8b4a74a4b640d4567a2a9151 100644 GIT binary patch literal 728351 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%0DWG3 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≷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Ð2X{ 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⪼gPTa8J)A`tLES-Q>@V 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

    %#R{s&vZ$wQf|b>?5_78mpgDlCf*AVJu&f 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

B21UADOKR@L&*AKHsf z^D*2Ez4l_uw{HeJ)#h=2<(QW=`)_HK_Q6;2Hx|9fxx{|@&C;8HTYXPdFEHR*v?e@c z&Y>oFxJ={Q_%FY{xUtOsbi405Tc!<1gIwRs&P$%T{;BNccuPeF#<*a3YwRE7U_Zlm&F#l?!`dUYHvH80w8=U1lJk4};w92s zCN6aT+S(DhxcKWonIp9yCI;jfwDeCXekRMr!3{JK?EXiE(PH&$TW|Zk^WzkE{W76M zD0j}T;%whP+Y_XNMI7!S`4^T3K(s_|sCikkeq>}xsDGe^M{;ILxJQIbpsg*)Z&25Q z=|8)dPQNDi&T#I>ybG?s>c3`QV?4i7BJ+WT(?@S%$7w1c^{}uY=oX-xL%5$njb~s4 zyAK!^3zN-fuT`Rf0G3f;fZ|7}7$`iMp=kpwM1+4Bn0|d|K-L2_3rxW*K@wvG)e|s2k><;g zU_QtXFn`d}9!mUy5iCdoNK9BLLDDZ|#QfQBPlTLI>GbD#>aSyWEHV5^bxG_reP zVL}UgA^8STmLS{9aA>NC!-aO_Pch4)mAI3S)SYa_j&=u(HLnT zRF1&_a=3xTh$sgcq%M`f(k6;~Kn5TQATeRq;fzyIyn)&ao1rRUVG8CF=ceKv(ENg7 zFOmQf6KWG`eE<@Kr89IlVa;QM#BG#>7sy{I04b1=m~bWN>S5spE_cxKC9)jYEF!`S zUat_WV_{ChoZspW_XEFsmolz@V4ap~e(c4sFNJ5hS8RfsnDQS8fNYpuyg=?hs2nJ~K;f+e zi0DIr+{8g+H-X#^ z3om&393*a|B)mZVKS(^l10TvjiB7l}t~kUr^>wD(xFoM{F371&6nTo zet6$PX1NdA~TJm*DEMt gPMn+OeW0Iqp-9A0K%7{w*UYD diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410361 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410361 deleted file mode 100644 index 198df6f33e92c1e7785c7c42754189c79cd012c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3844 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 Tp>bQF`O+Jhx|>NbA09XWrOuMQ diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410365 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410365 deleted file mode 100644 index e989965ff7636746574cc2e6a5810dc486c57550..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5000 zcmZQzfPl5^fz>IRsZCo89}5bJH}BeQUU;Lt=GcOd-5wo1YtKIcsuE_Kv%+_WdyUM` zl!*;uH(G3qnbXfLX)(*2e(+*c(ImGOt>>nu1#0E>EHB?&x_|dcK4aIA1K}a3zIpE3 z>zL(vU^U35q(x`1Kx|}S1kqQEGH)gYeD};{pZ(~CsgT+G|1U1j(PP`cdZUxP_@8tc zpb`h2Z89t$UmJaJ6mK;6sG?N;-GAMl$SEhx*i^1A3A8)*q{9AJj`rK!ST+UME8@H< zf(xy-uIM@Z<;p+rgipUxPcna|xp*z3eW|Db$kA{g(@6S&RDgXYa2E{aR$)28* z*rMz(`&#kM4E}PLhu2e|ty1b*IQ30aiIGU)$;qmxjIU~icPV*1W{-Zn4dNyS210QK zRL26uOd$1!uN|3ZY*0KijYT#4mCCmiPHtzVP9LQ_&%Bc-O(zH&0+pmN2#B3#V9?|Q zvcd5N(gy-bi_ZN4QXnzTg5m-zV<4~uNg&jL)u;FeK4mJ8<2zREskGzXRnL_90f+DV zS%g^albG^(U$ZYzJySqvP=JpsSTC4JKXom+VisR%@77=WdXu9#+)kBPye`iWurOYy zHFNQ`&tOF}rZqryFhbqxaMS0YRfD#x{I*k#dv-lpE|`2S>$Py*SH|tjFGkzcoA0r{ z^x%=Q-%rho0D;I|%#3eSxA8GKp6i|L(fw3EoYjUGXdXCR2)dcEZ2{1mTN#+X&jac~ z3zxHRfnqFo{G7#14*1=Ba`B?1UX^r?OtFe>%g0RjOS@{k+!SUwK=lzDMqqU>Z7-L< z39!`+=d4R+++nY2du;MEtEFp>&k%TW{{FIz8$dIe978-^f`AMdAR;U!ifV4blruu( z#6dXI)cC|C;Ya_R731u$^Zow9yFxHgQ9g6u+Urw!ue7$T@m_k*UUAxMg-+AZ{Ssvf zQ%+S{sjiZcQ9RXgw~W1v4`?7bEKfuxY^o1kx;cFFJc*t28(z+8&uDsaar%u%k$b#s zEZ8j>Kw&A`Y7J8lbqL76U_TSo4@~kPwISX{V6Be|qs8jiw%+!6=f^4T`ej0iQ0|;v z#o4}pwkJpji#Xf^DiZf7djVDp(g$_}u&lZBY=6MR{wv8k2Zg^>-2RYRJ0;jEP(k74 zu|w+)-7B3PbLvNafNE-Lce2vK!_IC>zWts2}cMQ1%37(52d610LCF z9a=ssW}>i9Xz%oH)webBk=pIK@hVAsK@p4MU$7g1{ymlCY}a4#o8|uM?vFgzel6N? zUHNiqf61NC_LeF=F7jeN`R(f~IBx#zedseMXv9`kku^T zuAYbrz9Qsz%&7Zn-Xl!~kt?5GWLSqBDL!@iwvCd-V~`>sNcj&0KpN&>Mj-bWSO$_t zn1S(c0_8Ih%zF$BVj68~>ww0A^50ybK2W}e1DGX10VF0|8k{EKJYb;?W<%`<)=^8K za*Uv|AEu5-H+j+6O(3^}FetnRo7*S}FHjjujW`5{2~s-)9DPAoQvKcUJ~nD~3yF_w z)_7diwzHho?Z5?(uP3<wYCszeEMBHd&`V>e+UQtCUHqhR#_vU^d=5+dvcrE6FkqFtCEm2YIY2_^p^c@~KY3lf}p z4LzP1UeC=>{%4=$)*toFp%to;@ykx diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410366 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410366 deleted file mode 100644 index 9a1eb601e6eaa7c04c6662fa3cf0e5c48d7895c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4996 zcmZQzfPi{WB!mRC9N-ObX4)n>T;%yX%#QfvSYpvIkbDXr?x8 zEqp8}B;LGhw|U`>@|t4{K6ZO_^sGJq#Kf)dn$@|0N6`i94+2)1O-|&|S4hwC~}jw#J;l4Xcfq-ius+87lbM zZvBLGw=St7pLLVxO-amX{J`p$vA(qRaL9FA9pmE_O)+y%^te0`{%erBb;fLzRc{KB!i z7!V5rP89>`6b2t}2N1m^u!iSGB&%SN;}X3?n?iXRX3uE<<)ohQovrr4kFSe`oyfr0wg4CghCrn( zAoWlHia&5164cMYzynen;teu_f#Jx@Zw0IWMar()!h5OrnMr%(E(hK}Q%l4h;<%SH zYaA|ws5JtseN-4NR=>9Ow$D31PI1>S6H0_~=j(_@MiW`Wz zKnx_n2zEa(4JjLZ7JvWQ{;>V>3IW%o#rs^(yWig$d(~I6nDNcLRIjTQixle{5|5tO z-?VU|!LeJK&3^qpA2d&L1otk#`_W-NKhR8YSmm4fsfb_HkiY&VQ%?TTEuL9RV~+|R z$Sj``XFtb3B0m8dRyHv8P`ALtilBZ51~#A`U|2!?|0{j!bE_qqvG?Da{dh2u+kYFg zrJeOg<_XvJCau|*>6iypEACPD0%{EdBiIc<|0Z6sd~UG+$@;d%9~b1S1bUUZZcp_4 zYH0R4bq5kMh8^LQZUFl?#Xs;V zQ+XWUv1(7H9rvz!rpymGeBaL^#A=_!l+XK`eZl@^VBE^U^nE_a-7trMXkcFd45V1@ z_&JN29PqpMd7&Rx_Nl zE}3zMy{7H4$%Q9{N&17;6@pK6SGGHM6)V1V_S$w6vTYu&2O^)Jl zJ5^%wx;#U`!g!t5%*EG0Zj~sixdl_s2=*T^EMMronW3eS&Hd`{beRc$>WU(JLbsGM zEMZ^z?$etmf`#$+;YPnW`2OCXko@DN(}iwdlPFGwyCOf@)s}qF8S)ofScKrQzG)2U`ltXAl4jM^L^&0Yv0Apn7nbM6>+H z)>qyDv;>sbKzR-sfYJ;zu&l6z2@>O8sY}Hn(DDUdCc*T>0s_cKV!~u0X%xu`3l@gpxF9CY4`8_gD>K0UCBm)n`jMFS z0W?@)^(1nb!qPdt>;>BcZLdI`1*bq^U;^Zk;1;6WQ%oV;PZ$_BF507a6l@=bVL*|+ z$aXV8>k@b%tX`Xvsm7kO?5gq12B{NkgjfQ#@>f_Ct(kE~Z%($I^(2VvG3rTBdj$qy zWvVSqfQU91gY4OqIl%A-wc9}MLvbS#2Z;$Y9cTFkwjWr2U4g2Er8O{@C^wzH`-jGE z0)-VUyx?`vAaNTd;RW&&N?ajvkeJYzL94HTqWJ0{WI3=|M1&W-ydt7K4|5c@@(NT& zkQ-053lqxR1acP~AVm%m6E2OS7AT4w7sz7^3_8z#+Huc5cIndUYd>dM>V(hjaClhv zB8~gzkId@49jsTuCIZ{r|A7F=hS|jkf}^4ht`M`Wz%~Ln%i{bQ7pgON}@LhY3 diff --git a/exp/lighthorizon/index/cmd/testdata/ledgers/1410367 b/exp/lighthorizon/index/cmd/testdata/ledgers/1410367 deleted file mode 100644 index 68ee1497fa693d5940f67a5470fabe7b2a78f7bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6884 zcmZQzfB?<8egX{V-`skpTvUE*$DW{A_hqRyFK@YJ8aOZYI?i7PR3-fG*9MK$Ck-bx z-A+vTw10*~n`-V(mPw&mdGqGaeRsX`usxul(gvVJ&27Aj39c2%2|m$b4%HLHD^UfuUTS!c$L0UriG|&kN)hfr*l6T z1C=;%SAUqpKKshM*u*a(e0i&8H(lG8^uu&}ocFqaZieaMR~E`!Ghd%}=0ej_=Lw!0 zrA%x#OzC+TVt7#V+I#V}tWQ`!NWK5URH-|6O^w#qsQt&Y{;+ku+q&FcUjM>2>7=h> zdc02gdoOL-r+M#$b-FA^ai5>4>V)MX)1&X)xu4V5>#oNj+G@}HVB6;9DIga!AHQ&1 zY7U460jElVbP9uyw*!b?5?I4?Ba&6H$Z?6@p-rK@46|pn|8i1K_|8`Q;7I6{S<`uD z^FQ3$Ew8@NJ!O1VE4)j|<1u^m<82T(F)$E{ zE1)_SAZ7xoPw@|Y%2Xc5cdXh|X~(^*o+tCG%&k%TW{{FIz8$k1z978-^f`AMd zNI!KgxndSyY46rw`FfM1INVN^SiCOJ5U?;_r!{l&wa-9x5=Aw)V9FW6{sX45PIJXL zOT)R}=QmDXym8uRao>qQr%mrH)zH#7Tj#lbm!XK>kJ{F@#bL_-*DJZZwaWA@5{!Py zw8n?Kbk~oQ6Ixh-27<%#&+etuugSeLocl5Fg6psPubI~v&##ood|=`9(OcMYnhH29 zMO#gQ>OtWT1MsjRsGos>2dIfL#M=n0_fcWASpC}8+dl97IK^GROehh`owKVr+xO4* z1nFQA2UuQXVEy_Kq@Ejyxxg9`1S8m8z%Y0yv-W(MNs;A-QqCB=dy96z?SJJTX^<=y zcRS}#W7DcA=nowOSQiSJhIa|w0u^~ zL}8!M-s#<{Z)@ZuwcB&!Rg(6CA{!oFztX2Zw_2hZd;hK3j|UUE{kJh&+F5U8o^V}n z(wcplj(H#h@r4!G{lKtVw>G`|?)Bw8W}+-Y=alR{s;PkSmQU7{WXLl5X&7Bl}rMtd+ zc7LzbjBVOo&gpYDF*(?Qfk9mW==}2x+|?35J>dKUq`?51_dsHt1;qtc#>Qp_kPv~Z z1FI)4AB6_HK+OeFMC2m|h8fcupjsFqZe?(o)idkwj)aUUcUj+**l9mH+O|mOxH^Mv zj_yP2X@9)$$nAK-ap!P)(bYR5`;<1`KYv0^Ytue|`)$XMulm^Lzi~ULj$nx^-R9jM z`G)nw?3zSFHA3phFOBjgsT9@C7cJc8w8;C1LNr?RE`l;mq6J> zx~XCojok!tJ1o2go7*S}uLVGVQ6mn)VS?0d0Y~3)|1z!Oe+_|p5|+K66(sI$$-b=3 z=ECH7T~a-p~qV`>g$|`8c&r|Hyqg*)_m&Mvf8OjqAkRfIU;*j zzmWvS7Z8Bj7+?U4JCrmDk|Cl^22^#b1V{keTC{1GvGtWV0L=%rUqS6yWB{wbtYCt~ zxL4{@aR{(J2DK+Z^)*WQ2nz_HI1&?PI;vWzeo%c6wjY=ek3dzz;vdW<&P_fpGnfPGcw|fB74DhL;LnnXCbJ@dy#K)vQ4-Xew)ma zd+r>+`bbfg)6<=&WRHs|*Kb+Z2~kP7o+M)Y251y`%n&{%LF^a~DfJ&Q?ts>Rpu9?# z`j0p_f$Kk7x(RFjH%QzD%M;+_MxvW&SH}Q{#K36;sje9`Zllaic= 0; i-- { - if beginOp, ok := operations[i].Body.GetBeginSponsoringFutureReservesOp(); ok && - beginOp.SponsoredId.Address() == sponsoree { - participants = append(participants, beginOp.SponsoredId.Address()) - } - } - } - - case xdr.OperationTypeRevokeSponsorship: - op := operation.Body.MustRevokeSponsorshipOp() - switch op.Type { - case xdr.RevokeSponsorshipTypeRevokeSponsorshipLedgerEntry: - participants = append(participants, getLedgerKeyParticipants(*op.LedgerKey)...) - - case xdr.RevokeSponsorshipTypeRevokeSponsorshipSigner: - participants = append(participants, op.Signer.AccountId.Address()) - // We don't add signer as a participant because a signer can be - // arbitrary account. This can spam successful operations - // history of any account. - } - - case xdr.OperationTypeClawback: - op := operation.Body.MustClawbackOp() - participants = append(participants, op.From.ToAccountId().Address()) - - case xdr.OperationTypeSetTrustLineFlags: - op := operation.Body.MustSetTrustLineFlagsOp() - participants = append(participants, op.Trustor.Address()) - - // for the following, the only direct participant is the source_account - case xdr.OperationTypeManageBuyOffer: - case xdr.OperationTypeManageSellOffer: - case xdr.OperationTypeCreatePassiveSellOffer: - case xdr.OperationTypeSetOptions: - case xdr.OperationTypeChangeTrust: - case xdr.OperationTypeInflation: - case xdr.OperationTypeManageData: - case xdr.OperationTypeBumpSequence: - case xdr.OperationTypeClaimClaimableBalance: - case xdr.OperationTypeClawbackClaimableBalance: - case xdr.OperationTypeLiquidityPoolDeposit: - case xdr.OperationTypeLiquidityPoolWithdraw: - - default: - return nil, fmt.Errorf("unknown operation type: %s", operation.Body.Type) - } - return participants, nil -} - -// getLedgerKeyParticipants returns a list of accounts that are considered -// "participants" in a particular ledger entry. -// -// This list will have zero or one element, making it easy to expand via `...`. -func getLedgerKeyParticipants(ledgerKey xdr.LedgerKey) []string { - switch ledgerKey.Type { - case xdr.LedgerEntryTypeAccount: - return []string{ledgerKey.Account.AccountId.Address()} - case xdr.LedgerEntryTypeData: - return []string{ledgerKey.Data.AccountId.Address()} - case xdr.LedgerEntryTypeOffer: - return []string{ledgerKey.Offer.SellerId.Address()} - case xdr.LedgerEntryTypeTrustline: - return []string{ledgerKey.TrustLine.AccountId.Address()} - case xdr.LedgerEntryTypeClaimableBalance: - // nothing to do - } - return []string{} -} - -func getIndex(ledger xdr.LedgerCloseMeta, mode AccountIndexMode) uint32 { - switch mode { - case ByCheckpoint: - return GetCheckpointNumber(ledger.LedgerSequence()) - case ByLedger: - return ledger.LedgerSequence() - default: - return 0 - } -} diff --git a/exp/lighthorizon/index/store.go b/exp/lighthorizon/index/store.go deleted file mode 100644 index de5f4f6f07..0000000000 --- a/exp/lighthorizon/index/store.go +++ /dev/null @@ -1,377 +0,0 @@ -package index - -import ( - "encoding/binary" - "encoding/hex" - "io" - "os" - "sync" - "time" - - "github.com/prometheus/client_golang/prometheus" - backend "github.com/stellar/go/exp/lighthorizon/index/backend" - types "github.com/stellar/go/exp/lighthorizon/index/types" - "github.com/stellar/go/support/log" -) - -type Store interface { - NextActive(account, index string, afterCheckpoint uint32) (uint32, error) - TransactionTOID(hash [32]byte) (int64, error) - - AddTransactionToIndexes(txnTOID int64, hash [32]byte) error - AddParticipantsToIndexes(checkpoint uint32, index string, participants []string) error - AddParticipantsToIndexesNoBackend(checkpoint uint32, index string, participants []string) error - AddParticipantToIndexesNoBackend(participant string, indexes types.NamedIndices) - - Flush() error - FlushAccounts() error - ClearMemory(bool) - - Read(account string) (types.NamedIndices, error) - ReadAccounts() ([]string, error) - ReadTransactions(prefix string) (*types.TrieIndex, error) - - MergeTransactions(prefix string, other *types.TrieIndex) error - - RegisterMetrics(registry *prometheus.Registry) -} - -type StoreConfig struct { - // init time config - // the base url for the store resource - URL string - // optional url path to append to the base url to realize the complete url - URLSubPath string - Workers uint32 - - // runtime config - ClearMemoryOnFlush bool - - // logging & metrics - Log *log.Entry // TODO: unused for now - Metrics *prometheus.Registry -} - -type store struct { - mutex sync.RWMutex - config StoreConfig - - // data - indexes map[string]types.NamedIndices - txIndexes map[string]*types.TrieIndex - backend backend.Backend - - // metrics - indexWorkingSet prometheus.Gauge - indexWorkingSetTime prometheus.Gauge // to check if the above takes too long lmao -} - -func NewStore(backend backend.Backend, config StoreConfig) (Store, error) { - result := &store{ - indexes: map[string]types.NamedIndices{}, - txIndexes: map[string]*types.TrieIndex{}, - backend: backend, - - config: config, - - indexWorkingSet: newHorizonLiteGauge("working_set", - "Approximately how much memory (kiB) are indices using?"), - indexWorkingSetTime: newHorizonLiteGauge("working_set_time", - "How long did it take (μs) to calculate the working set size?"), - } - result.RegisterMetrics(config.Metrics) - - return result, nil -} - -func (s *store) accounts() []string { - accounts := make([]string, 0, len(s.indexes)) - for account := range s.indexes { - accounts = append(accounts, account) - } - return accounts -} - -func (s *store) FlushAccounts() error { - s.mutex.Lock() - defer s.mutex.Unlock() - return s.backend.FlushAccounts(s.accounts()) -} - -func (s *store) Read(account string) (types.NamedIndices, error) { - return s.backend.Read(account) -} - -func (s *store) ReadAccounts() ([]string, error) { - return s.backend.ReadAccounts() -} - -func (s *store) ReadTransactions(prefix string) (*types.TrieIndex, error) { - return s.getCreateTrieIndex(prefix) -} - -func (s *store) MergeTransactions(prefix string, other *types.TrieIndex) error { - defer s.approximateWorkingSet() - - index, err := s.getCreateTrieIndex(prefix) - if err != nil { - return err - } - if err := index.Merge(other); err != nil { - return err - } - - s.mutex.Lock() - defer s.mutex.Unlock() - s.txIndexes[prefix] = index - return nil -} - -func (s *store) approximateWorkingSet() { - if s.config.Metrics == nil { - return - } - - start := time.Now() - approx := float64(0) - - for _, indices := range s.indexes { - firstIndexSize := 0 - for _, index := range indices { - firstIndexSize = index.Size() - break - } - - // There may be multiple indices for each account, but we can do a rough - // approximation for now by just assuming they're all around the same - // size. - approx += float64(len(indices) * firstIndexSize) - } - - for _, trie := range s.txIndexes { - // FIXME: Is this too slow? We probably want a TrieIndex.Size() method, - // but that's not trivial to determine for a trie. - trie.Iterate(func(key, value []byte) { - approx += float64(len(key) + len(value)) - }) - } - - s.indexWorkingSet.Set(approx / 1024) // kiB - s.indexWorkingSetTime.Set(float64(time.Since(start).Microseconds())) // μs -} - -func (s *store) Flush() error { - s.mutex.Lock() - defer s.mutex.Unlock() - defer s.approximateWorkingSet() - - if err := s.backend.Flush(s.indexes); err != nil { - return err - } - - if err := s.backend.FlushAccounts(s.accounts()); err != nil { - return err - } else if s.config.ClearMemoryOnFlush { - s.indexes = map[string]types.NamedIndices{} - } - - if err := s.backend.FlushTransactions(s.txIndexes); err != nil { - return err - } else if s.config.ClearMemoryOnFlush { - s.txIndexes = map[string]*types.TrieIndex{} - } - - return nil -} - -func (s *store) ClearMemory(doClear bool) { - s.config.ClearMemoryOnFlush = doClear -} - -func (s *store) AddTransactionToIndexes(txnTOID int64, hash [32]byte) error { - index, err := s.getCreateTrieIndex(hex.EncodeToString(hash[:1])) - if err != nil { - return err - } - - value := make([]byte, 8) - binary.BigEndian.PutUint64(value, uint64(txnTOID)) - - // We don't have to re-calculate the whole working set size for metrics - // since we're adding a known size. - if _, replaced := index.Upsert(hash[1:], value); !replaced { - s.indexWorkingSet.Add(float64(len(hash) - 1 + len(value))) - } - - return nil -} - -func (s *store) TransactionTOID(hash [32]byte) (int64, error) { - index, err := s.getCreateTrieIndex(hex.EncodeToString(hash[:1])) - if err != nil { - return 0, err - } - - value, ok := index.Get(hash[1:]) - if !ok { - return 0, io.EOF - } - return int64(binary.BigEndian.Uint64(value)), nil -} - -// AddParticipantsToIndexesNoBackend is a temp version of -// AddParticipantsToIndexes that skips backend downloads and it used in AWS -// Batch. Refactoring required to make it better. -func (s *store) AddParticipantsToIndexesNoBackend(checkpoint uint32, index string, participants []string) error { - s.mutex.Lock() - defer s.mutex.Unlock() - defer s.approximateWorkingSet() - - var err error - for _, participant := range participants { - if _, ok := s.indexes[participant]; !ok { - s.indexes[participant] = map[string]*types.BitmapIndex{} - } - - ind, ok := s.indexes[participant][index] - if !ok { - ind = &types.BitmapIndex{} - s.indexes[participant][index] = ind - } - - if innerErr := ind.SetActive(checkpoint); innerErr != nil { - err = innerErr - } - // don't break early, instead try to save as many participants as we can - } - - return err -} - -func (s *store) AddParticipantToIndexesNoBackend(participant string, indexes types.NamedIndices) { - s.mutex.Lock() - defer s.mutex.Unlock() - defer s.approximateWorkingSet() - - s.indexes[participant] = indexes -} - -func (s *store) AddParticipantsToIndexes(checkpoint uint32, index string, participants []string) error { - defer s.approximateWorkingSet() - - for _, participant := range participants { - ind, err := s.getCreateIndex(participant, index) - if err != nil { - return err - } - err = ind.SetActive(checkpoint) - if err != nil { - return err - } - } - return nil -} - -func (s *store) getCreateIndex(account, id string) (*types.BitmapIndex, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - defer s.approximateWorkingSet() - - // Check if we already have it loaded - accountIndexes, ok := s.indexes[account] - if !ok { - accountIndexes = types.NamedIndices{} - } - ind, ok := accountIndexes[id] - if ok { - return ind, nil - } - - // Check if index exists in backend - found, err := s.backend.Read(account) - if err == nil { - accountIndexes = found - } else if !os.IsNotExist(err) { - return nil, err - } - - ind, ok = accountIndexes[id] - if !ok { - // Not found anywhere, make a new one. - ind = &types.BitmapIndex{} - accountIndexes[id] = ind - } - - // We don't want to replace the entire index map in memory (even though we - // read all of it from disk), just the one we loaded from disk. Otherwise, - // we lose in-memory changes to unrelated indices. - if memoryIndices, ok := s.indexes[account]; ok { // account exists in-mem - if memoryIndex, ok2 := memoryIndices[id]; ok2 { // id exists in-mem - if memoryIndex != accountIndexes[id] { // not using in-mem already - memoryIndex.Merge(ind) - s.indexes[account][id] = memoryIndex - } - } - } else { - s.indexes[account] = accountIndexes - } - - return ind, nil -} - -func (s *store) NextActive(account, indexId string, afterCheckpoint uint32) (uint32, error) { - defer s.approximateWorkingSet() - - ind, err := s.getCreateIndex(account, indexId) - if err != nil { - return 0, err - } - return ind.NextActiveBit(afterCheckpoint) -} - -func (s *store) getCreateTrieIndex(prefix string) (*types.TrieIndex, error) { - s.mutex.Lock() - defer s.mutex.Unlock() - defer s.approximateWorkingSet() - - // Check if we already have it loaded - index, ok := s.txIndexes[prefix] - if ok { - return index, nil - } - - // Check if index exists in backend - found, err := s.backend.ReadTransactions(prefix) - if err == nil { - s.txIndexes[prefix] = found - } else if !os.IsNotExist(err) { - return nil, err - } - - index, ok = s.txIndexes[prefix] - if !ok { - // Not found anywhere, make a new one. - index = &types.TrieIndex{} - s.txIndexes[prefix] = index - } - - return index, nil -} - -func (s *store) RegisterMetrics(registry *prometheus.Registry) { - s.config.Metrics = registry - - if registry != nil { - registry.Register(s.indexWorkingSet) - registry.Register(s.indexWorkingSetTime) - } -} - -func newHorizonLiteGauge(name, help string) prometheus.Gauge { - return prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: "horizon_lite", - Subsystem: "index_store", - Name: name, - Help: help, - }) -} diff --git a/exp/lighthorizon/index/types/bitmap.go b/exp/lighthorizon/index/types/bitmap.go deleted file mode 100644 index 171115938b..0000000000 --- a/exp/lighthorizon/index/types/bitmap.go +++ /dev/null @@ -1,367 +0,0 @@ -package index - -import ( - "bytes" - "fmt" - "io" - "strings" - "sync" - - "github.com/stellar/go/support/ordered" - "github.com/stellar/go/xdr" -) - -const BitmapIndexVersion = 1 - -type BitmapIndex struct { - mutex sync.RWMutex - bitmap []byte - firstBit uint32 - lastBit uint32 -} - -type NamedIndices map[string]*BitmapIndex - -func NewBitmapIndex(b []byte) (*BitmapIndex, error) { - xdrBitmap := xdr.BitmapIndex{} - err := xdrBitmap.UnmarshalBinary(b) - if err != nil { - return nil, err - } - - return NewBitmapIndexFromXDR(xdrBitmap), nil -} - -func NewBitmapIndexFromXDR(index xdr.BitmapIndex) *BitmapIndex { - return &BitmapIndex{ - bitmap: index.Bitmap[:], - firstBit: uint32(index.FirstBit), - lastBit: uint32(index.LastBit), - } -} - -func (i *BitmapIndex) Size() int { - return len(i.bitmap) -} - -func (i *BitmapIndex) SetActive(index uint32) error { - i.mutex.Lock() - defer i.mutex.Unlock() - return i.setActive(index) -} - -func (i *BitmapIndex) SetInactive(index uint32) error { - i.mutex.Lock() - defer i.mutex.Unlock() - return i.setInactive(index) -} - -// bitShiftLeft returns a byte with the bit set corresponding to the index. In -// other words, it flips the bit corresponding to the index's "position" mod-8. -func bitShiftLeft(index uint32) byte { - if index%8 == 0 { - return 1 - } else { - return byte(1) << (8 - index%8) - } -} - -// rangeFirstBit returns the index of the first *possible* active bit in the -// bitmap. In other words, if you just have SetActive(12), this will return 9, -// because you have one byte (0b0001_0000) and the *first* value the bitmap can -// represent is 9. -func (i *BitmapIndex) rangeFirstBit() uint32 { - return (i.firstBit-1)/8*8 + 1 -} - -// rangeLastBit returns the index of the last *possible* active bit in the -// bitmap. In other words, if you just have SetActive(12), this will return 16, -// because you have one byte (0b0001_0000) and the *last* value the bitmap can -// represent is 16. -func (i *BitmapIndex) rangeLastBit() uint32 { - return i.rangeFirstBit() + uint32(len(i.bitmap))*8 - 1 -} - -func (i *BitmapIndex) setActive(index uint32) error { - if i.firstBit == 0 { - i.firstBit = index - i.lastBit = index - b := bitShiftLeft(index) - i.bitmap = []byte{b} - } else { - if index >= i.rangeFirstBit() && index <= i.rangeLastBit() { - // Update the bit in existing range - b := bitShiftLeft(index) - loc := (index - i.rangeFirstBit()) / 8 - i.bitmap[loc] = i.bitmap[loc] | b - - if index < i.firstBit { - i.firstBit = index - } - if index > i.lastBit { - i.lastBit = index - } - } else { - // Expand the bitmap - if index < i.rangeFirstBit() { - // ...to the left - newBytes := make([]byte, distance(index, i.rangeFirstBit())) - i.bitmap = append(newBytes, i.bitmap...) - b := bitShiftLeft(index) - i.bitmap[0] = i.bitmap[0] | b - - i.firstBit = index - } else if index > i.rangeLastBit() { - // ... to the right - newBytes := make([]byte, distance(i.rangeLastBit(), index)) - i.bitmap = append(i.bitmap, newBytes...) - b := bitShiftLeft(index) - loc := (index - i.rangeFirstBit()) / 8 - i.bitmap[loc] = i.bitmap[loc] | b - - i.lastBit = index - } - } - } - - return nil -} - -func (i *BitmapIndex) setInactive(index uint32) error { - // Is this index even active in the first place? - if i.firstBit == 0 || index < i.rangeFirstBit() || index > i.rangeLastBit() { - return nil // not really an error - } - - loc := (index - i.rangeFirstBit()) / 8 // which byte? - b := bitShiftLeft(index) // which bit w/in the byte? - i.bitmap[loc] &= ^b // unset only that bit - - // If unsetting this bit made the first byte empty OR we unset the earliest - // set bit, we need to find the next "first" active bit. - if loc == 0 && i.firstBit == index { - // find the next active bit to set as the start - nextBit, err := i.nextActiveBit(index) - if err == io.EOF { - i.firstBit = 0 - i.lastBit = 0 - i.bitmap = []byte{} - } else if err != nil { - return err - } else { - // Trim all (now-)empty bytes off the front. - i.bitmap = i.bitmap[distance(i.firstBit, nextBit):] - i.firstBit = nextBit - } - } else if int(loc) == len(i.bitmap)-1 { - idx := -1 - - if i.bitmap[loc] == 0 { - // find the latest non-empty byte, to set as the new "end" - j := len(i.bitmap) - 1 - for i.bitmap[j] == 0 { - j-- - } - - i.bitmap = i.bitmap[:j+1] - idx = 8 - } else if i.lastBit == index { - // Get the "bit number" of the last active bit (i.e. the one we just - // turned off) to mark the starting point for the search. - idx = 8 - if index%8 != 0 { - idx = int(index % 8) - } - } - - // Do we need to adjust the range? Imagine we had 0b0011_0100 and we - // unset the last active bit. - // ^ - // Then, we need to adjust our internal lastBit tracker to represent the - // ^ bit above. This means finding the first previous set bit. - if idx > -1 { - l := uint32(len(i.bitmap) - 1) - // Imagine we had 0b0011_0100 and we unset the last active bit. - // ^ - // Then, we need to adjust our internal lastBit tracker to represent - // the ^ bit above. This means finding the first previous set bit. - j, ok := int(idx), false - for ; j >= 0 && !ok; j-- { - _, ok = maxBitAfter(i.bitmap[l], uint32(j)) - } - - // We know from the earlier conditional that *some* bit is set, so - // we know that j represents the index of the bit that's the new - // "last active" bit. - firstByte := i.rangeFirstBit() - i.lastBit = firstByte + (l * 8) + uint32(j) + 1 - } - } - - return nil -} - -//lint:ignore U1000 Ignore unused function temporarily -func (i *BitmapIndex) isActive(index uint32) bool { - if index >= i.firstBit && index <= i.lastBit { - b := bitShiftLeft(index) - loc := (index - i.rangeFirstBit()) / 8 - return i.bitmap[loc]&b != 0 - } else { - return false - } -} - -func (i *BitmapIndex) iterate(f func(index uint32)) error { - i.mutex.RLock() - defer i.mutex.RUnlock() - - if i.firstBit == 0 { - return nil - } - - f(i.firstBit) - curr := i.firstBit - - for { - var err error - curr, err = i.nextActiveBit(curr + 1) - if err != nil { - if err == io.EOF { - break - } - return err - } - - f(curr) - } - - return nil -} - -func (i *BitmapIndex) Merge(other *BitmapIndex) error { - i.mutex.Lock() - defer i.mutex.Unlock() - - var err error - other.iterate(func(index uint32) { - if err != nil { - return - } - err = i.setActive(index) - }) - - return err -} - -// NextActiveBit returns the next bit position (inclusive) where this index is -// active. "Inclusive" means that if it's already active at `position`, this -// returns `position`. -func (i *BitmapIndex) NextActiveBit(position uint32) (uint32, error) { - i.mutex.RLock() - defer i.mutex.RUnlock() - return i.nextActiveBit(position) -} - -func (i *BitmapIndex) nextActiveBit(position uint32) (uint32, error) { - if i.firstBit == 0 || position > i.lastBit { - // We're past the end. - // TODO: Should this be an error? or how should we signal NONE here? - return 0, io.EOF - } - - if position < i.firstBit { - position = i.firstBit - } - - // Must be within the range, find the first non-zero after our start - loc := (position - i.rangeFirstBit()) / 8 - - // Is it in the same byte? - if shift, ok := maxBitAfter(i.bitmap[loc], (position-1)%8); ok { - return i.rangeFirstBit() + (loc * 8) + shift, nil - } - - // Scan bytes after - loc++ - for ; loc < uint32(len(i.bitmap)); loc++ { - // Find the offset of the set bit - if shift, ok := maxBitAfter(i.bitmap[loc], 0); ok { - return i.rangeFirstBit() + (loc * 8) + shift, nil - } - } - - // all bits after this were zero - // TODO: Should this be an error? or how should we signal NONE here? - return 0, io.EOF -} - -func (i *BitmapIndex) ToXDR() xdr.BitmapIndex { - i.mutex.RLock() - defer i.mutex.RUnlock() - - return xdr.BitmapIndex{ - FirstBit: xdr.Uint32(i.firstBit), - LastBit: xdr.Uint32(i.lastBit), - Bitmap: i.bitmap, - } -} - -func (i *BitmapIndex) Buffer() *bytes.Buffer { - i.mutex.RLock() - defer i.mutex.RUnlock() - - xdrBitmap := i.ToXDR() - b, err := xdrBitmap.MarshalBinary() - if err != nil { - panic(err) - } - return bytes.NewBuffer(b) -} - -// Flush flushes the index data to byte slice in index format. -func (i *BitmapIndex) Flush() []byte { - return i.Buffer().Bytes() -} - -// DebugCompare returns a string that compares this bitmap to another bitmap -// byte-by-byte in binary form as two columns. -func (i *BitmapIndex) DebugCompare(j *BitmapIndex) string { - output := make([]string, ordered.Max(len(i.bitmap), len(j.bitmap))) - for n := 0; n < len(output); n++ { - if n < len(i.bitmap) { - output[n] += fmt.Sprintf("%08b", i.bitmap[n]) - } else { - output[n] += " " - } - - output[n] += " | " - - if n < len(j.bitmap) { - output[n] += fmt.Sprintf("%08b", j.bitmap[n]) - } - } - - return strings.Join(output, "\n") -} - -func maxBitAfter(b byte, after uint32) (uint32, bool) { - if b == 0 { - // empty byte - return 0, false - } - - for shift := uint32(after); shift < 8; shift++ { - mask := byte(0b1000_0000) >> shift - if mask&b != 0 { - return shift, true - } - } - return 0, false -} - -// distance returns how many bytes occur between the two given indices. Note -// that j >= i, otherwise the result will be negative. -func distance(i, j uint32) int { - return (int(j)-1)/8 - (int(i)-1)/8 -} diff --git a/exp/lighthorizon/index/types/bitmap_test.go b/exp/lighthorizon/index/types/bitmap_test.go deleted file mode 100644 index c5e7864872..0000000000 --- a/exp/lighthorizon/index/types/bitmap_test.go +++ /dev/null @@ -1,382 +0,0 @@ -package index - -import ( - "fmt" - "io" - "math/rand" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewFromBytes(t *testing.T) { - for i := uint32(1); i < 200; i++ { - t.Run(fmt.Sprintf("New%d", i), func(t *testing.T) { - index := &BitmapIndex{} - index.SetActive(i) - b := index.Flush() - newIndex, err := NewBitmapIndex(b) - require.NoError(t, err) - assert.Equal(t, index.firstBit, newIndex.firstBit) - assert.Equal(t, index.lastBit, newIndex.lastBit) - assert.Equal(t, index.bitmap, newIndex.bitmap) - }) - } -} - -func TestSetActive(t *testing.T) { - cases := []struct { - checkpoint uint32 - rangeFirstCheckpoint uint32 - bitmap []byte - }{ - {1, 1, []byte{0b1000_0000}}, - {2, 1, []byte{0b0100_0000}}, - {3, 1, []byte{0b0010_0000}}, - {4, 1, []byte{0b0001_0000}}, - {5, 1, []byte{0b0000_1000}}, - {6, 1, []byte{0b0000_0100}}, - {7, 1, []byte{0b0000_0010}}, - {8, 1, []byte{0b0000_0001}}, - - {9, 9, []byte{0b1000_0000}}, - {10, 9, []byte{0b0100_0000}}, - {11, 9, []byte{0b0010_0000}}, - {12, 9, []byte{0b0001_0000}}, - {13, 9, []byte{0b0000_1000}}, - {14, 9, []byte{0b0000_0100}}, - {15, 9, []byte{0b0000_0010}}, - {16, 9, []byte{0b0000_0001}}, - } - - for _, tt := range cases { - t.Run(fmt.Sprintf("init_%d", tt.checkpoint), func(t *testing.T) { - index := &BitmapIndex{} - index.SetActive(tt.checkpoint) - - assert.Equal(t, tt.bitmap, index.bitmap) - assert.Equal(t, tt.rangeFirstCheckpoint, index.rangeFirstBit()) - assert.Equal(t, tt.checkpoint, index.firstBit) - assert.Equal(t, tt.checkpoint, index.lastBit) - }) - } - - // Update current bitmap right - index := &BitmapIndex{} - index.SetActive(1) - assert.Equal(t, uint32(1), index.firstBit) - assert.Equal(t, uint32(1), index.lastBit) - index.SetActive(8) - assert.Equal(t, []byte{0b1000_0001}, index.bitmap) - assert.Equal(t, uint32(1), index.firstBit) - assert.Equal(t, uint32(8), index.lastBit) - - // Update current bitmap left - index = &BitmapIndex{} - index.SetActive(8) - assert.Equal(t, uint32(8), index.firstBit) - assert.Equal(t, uint32(8), index.lastBit) - index.SetActive(1) - assert.Equal(t, []byte{0b1000_0001}, index.bitmap) - assert.Equal(t, uint32(1), index.firstBit) - assert.Equal(t, uint32(8), index.lastBit) - - index = &BitmapIndex{} - index.SetActive(10) - index.SetActive(9) - index.SetActive(16) - assert.Equal(t, []byte{0b1100_0001}, index.bitmap) - assert.Equal(t, uint32(9), index.firstBit) - assert.Equal(t, uint32(16), index.lastBit) - - // Expand bitmap to the left - index = &BitmapIndex{} - index.SetActive(10) - index.SetActive(1) - assert.Equal(t, []byte{0b1000_0000, 0b0100_0000}, index.bitmap) - assert.Equal(t, uint32(1), index.firstBit) - assert.Equal(t, uint32(10), index.lastBit) - - index = &BitmapIndex{} - index.SetActive(17) - index.SetActive(2) - assert.Equal(t, []byte{0b0100_0000, 0b0000_0000, 0b1000_0000}, index.bitmap) - assert.Equal(t, uint32(2), index.firstBit) - assert.Equal(t, uint32(17), index.lastBit) - - // Expand bitmap to the right - index = &BitmapIndex{} - index.SetActive(1) - index.SetActive(10) - assert.Equal(t, []byte{0b1000_0000, 0b0100_0000}, index.bitmap) - assert.Equal(t, uint32(1), index.firstBit) - assert.Equal(t, uint32(10), index.lastBit) - - index = &BitmapIndex{} - index.SetActive(2) - index.SetActive(17) - assert.Equal(t, []byte{0b0100_0000, 0b0000_0000, 0b1000_0000}, index.bitmap) - assert.Equal(t, uint32(2), index.firstBit) - assert.Equal(t, uint32(17), index.lastBit) - - index = &BitmapIndex{} - index.SetActive(17) - index.SetActive(26) - assert.Equal(t, []byte{0b1000_0000, 0b0100_0000}, index.bitmap) - assert.Equal(t, uint32(17), index.firstBit) - assert.Equal(t, uint32(26), index.lastBit) -} - -// TestSetInactive ensures that you can flip active bits off and the bitmap -// compresses in size accordingly. -func TestSetInactive(t *testing.T) { - index := &BitmapIndex{} - index.SetActive(17) - index.SetActive(17 + 9) - index.SetActive(17 + 9 + 10) - assert.Equal(t, []byte{0b1000_0000, 0b0100_0000, 0b0001_0000}, index.bitmap) - - // disabling bits should work - index.SetInactive(17) - assert.False(t, index.isActive(17)) - - // it should trim off the first byte now - assert.Equal(t, []byte{0b0100_0000, 0b0001_0000}, index.bitmap) - assert.EqualValues(t, 17+9, index.firstBit) - assert.EqualValues(t, 17+9+10, index.lastBit) - - // it should compress empty bytes on shrink - index = &BitmapIndex{} - index.SetActive(1) - index.SetActive(1 + 2) - index.SetActive(1 + 9) - index.SetActive(1 + 9 + 8 + 9) - assert.Equal(t, []byte{0b1010_0000, 0b0100_0000, 0b0000_0000, 0b0010_0000}, index.bitmap) - - // ...from the left - index.SetInactive(1) - assert.Equal(t, []byte{0b0010_0000, 0b0100_0000, 0b0000_0000, 0b0010_0000}, index.bitmap) - index.SetInactive(3) - assert.Equal(t, []byte{0b0100_0000, 0b0000_0000, 0b0010_0000}, index.bitmap) - assert.EqualValues(t, 1+9, index.firstBit) - assert.EqualValues(t, 1+9+8+9, index.lastBit) - - // ...and the right - index.SetInactive(1 + 9 + 8 + 9) - assert.Equal(t, []byte{0b0100_0000}, index.bitmap) - assert.EqualValues(t, 1+9, index.firstBit) - assert.EqualValues(t, 1+9, index.lastBit) - - // ensure right-hand compression it works for multiple bytes, too - index = &BitmapIndex{} - index.SetActive(2) - index.SetActive(2 + 2) - index.SetActive(2 + 9) - index.SetActive(2 + 9 + 8 + 6) - index.SetActive(2 + 9 + 8 + 9) - index.SetActive(2 + 9 + 8 + 10) - assert.Equal(t, []byte{0b0101_0000, 0b0010_0000, 0b0000_0000, 0b1001_1000}, index.bitmap) - - index.setInactive(2 + 9 + 8 + 10) - assert.Equal(t, []byte{0b0101_0000, 0b0010_0000, 0b0000_0000, 0b1001_0000}, index.bitmap) - assert.EqualValues(t, 2+9+8+9, index.lastBit) - - index.setInactive(2 + 9 + 8 + 9) - assert.Equal(t, []byte{0b0101_0000, 0b0010_0000, 0b0000_0000, 0b1000_0000}, index.bitmap) - assert.EqualValues(t, 2+9+8+6, index.lastBit) - - index.setInactive(2 + 9 + 8 + 6) - assert.Equal(t, []byte{0b0101_0000, 0b0010_0000}, index.bitmap) - assert.EqualValues(t, 2, index.firstBit) - assert.EqualValues(t, 2+9, index.lastBit) - - index.setInactive(2 + 2) - assert.Equal(t, []byte{0b0100_0000, 0b0010_0000}, index.bitmap) - assert.EqualValues(t, 2, index.firstBit) - assert.EqualValues(t, 2+9, index.lastBit) - - index.setInactive(1) // should be a no-op - assert.Equal(t, []byte{0b0100_0000, 0b0010_0000}, index.bitmap) - assert.EqualValues(t, 2, index.firstBit) - assert.EqualValues(t, 2+9, index.lastBit) -} - -// TestFuzzerSetInactive attempt to fuzz random bits into two bitmap sets, one -// by addition, and one by subtraction - then, it compares the outcome. -func TestFuzzySetUnset(t *testing.T) { - permLen := uint32(128) // should be a multiple of 8 - setBitsCount := permLen / 2 - - for n := 0; n < 10_000; n++ { - randBits := rand.Perm(int(permLen)) - setBits := randBits[:setBitsCount] - clearBits := randBits[setBitsCount:] - - // set all first, then clear the others - clearBitmap := &BitmapIndex{} - for i := uint32(1); i <= permLen; i++ { - clearBitmap.setActive(i) - } - - setBitmap := &BitmapIndex{} - for i := range setBits { - setBitmap.setActive(uint32(setBits[i]) + 1) - clearBitmap.setInactive(uint32(clearBits[i]) + 1) - } - - require.Equalf(t, setBitmap, clearBitmap, - "bitmaps aren't equal:\n%s", setBitmap.DebugCompare(clearBitmap)) - } -} - -func TestNextActive(t *testing.T) { - t.Run("empty", func(t *testing.T) { - index := &BitmapIndex{} - - i, err := index.NextActiveBit(0) - assert.Equal(t, uint32(0), i) - assert.EqualError(t, err, io.EOF.Error()) - }) - - t.Run("one byte", func(t *testing.T) { - t.Run("after last", func(t *testing.T) { - index := &BitmapIndex{} - index.SetActive(3) - - // 16 is well-past the end - i, err := index.NextActiveBit(16) - assert.Equal(t, uint32(0), i) - assert.EqualError(t, err, io.EOF.Error()) - }) - - t.Run("only one bit in the byte", func(t *testing.T) { - index := &BitmapIndex{} - index.SetActive(1) - - i, err := index.NextActiveBit(1) - assert.NoError(t, err) - assert.Equal(t, uint32(1), i) - }) - - t.Run("only one bit in the byte (offset)", func(t *testing.T) { - index := &BitmapIndex{} - index.SetActive(9) - - i, err := index.NextActiveBit(1) - assert.NoError(t, err) - assert.Equal(t, uint32(9), i) - }) - - severalSet := &BitmapIndex{} - severalSet.SetActive(9) - severalSet.SetActive(11) - - t.Run("several bits set (first)", func(t *testing.T) { - i, err := severalSet.NextActiveBit(9) - assert.NoError(t, err) - assert.Equal(t, uint32(9), i) - }) - - t.Run("several bits set (second)", func(t *testing.T) { - i, err := severalSet.NextActiveBit(10) - assert.NoError(t, err) - assert.Equal(t, uint32(11), i) - }) - - t.Run("several bits set (second, inclusive)", func(t *testing.T) { - i, err := severalSet.NextActiveBit(11) - assert.NoError(t, err) - assert.Equal(t, uint32(11), i) - }) - }) - - t.Run("many bytes", func(t *testing.T) { - index := &BitmapIndex{} - index.SetActive(9) - index.SetActive(129) - - // Before the first - i, err := index.NextActiveBit(8) - assert.NoError(t, err) - assert.Equal(t, uint32(9), i) - - // at the first - i, err = index.NextActiveBit(9) - assert.NoError(t, err) - assert.Equal(t, uint32(9), i) - - // In the middle - i, err = index.NextActiveBit(11) - assert.NoError(t, err) - assert.Equal(t, uint32(129), i) - - // At the end - i, err = index.NextActiveBit(129) - assert.NoError(t, err) - assert.Equal(t, uint32(129), i) - - // after the end - i, err = index.NextActiveBit(130) - assert.EqualError(t, err, io.EOF.Error()) - assert.Equal(t, uint32(0), i) - }) -} - -func TestMaxBitAfter(t *testing.T) { - for _, tc := range []struct { - b byte - after uint32 - shift uint32 - ok bool - }{ - {0b0000_0000, 0, 0, false}, - {0b0000_0000, 1, 0, false}, - {0b1000_0000, 0, 0, true}, - {0b0100_0000, 0, 1, true}, - {0b0100_0000, 1, 1, true}, - {0b0010_1000, 0, 2, true}, - {0b0010_1000, 1, 2, true}, - {0b0010_1000, 2, 2, true}, - {0b0010_1000, 3, 4, true}, - {0b0010_1000, 4, 4, true}, - {0b0000_0001, 7, 7, true}, - } { - t.Run(fmt.Sprintf("0b%b,%d", tc.b, tc.after), func(t *testing.T) { - shift, ok := maxBitAfter(tc.b, tc.after) - assert.Equal(t, tc.ok, ok) - assert.Equal(t, tc.shift, shift) - }) - } -} - -func TestMerge(t *testing.T) { - a := &BitmapIndex{} - require.NoError(t, a.SetActive(9)) - require.NoError(t, a.SetActive(129)) - - b := &BitmapIndex{} - require.NoError(t, b.SetActive(900)) - require.NoError(t, b.SetActive(1000)) - - var checkpoints []uint32 - b.iterate(func(c uint32) { - checkpoints = append(checkpoints, c) - }) - - assert.Equal(t, []uint32{900, 1000}, checkpoints) - - require.NoError(t, a.Merge(b)) - - assert.True(t, a.isActive(9)) - assert.True(t, a.isActive(129)) - assert.True(t, a.isActive(900)) - assert.True(t, a.isActive(1000)) - - checkpoints = []uint32{} - a.iterate(func(c uint32) { - checkpoints = append(checkpoints, c) - }) - - assert.Equal(t, []uint32{9, 129, 900, 1000}, checkpoints) -} diff --git a/exp/lighthorizon/index/types/trie.go b/exp/lighthorizon/index/types/trie.go deleted file mode 100644 index b5fc39c0ca..0000000000 --- a/exp/lighthorizon/index/types/trie.go +++ /dev/null @@ -1,345 +0,0 @@ -package index - -import ( - "bufio" - "encoding" - "io" - "sync" - - "github.com/stellar/go/xdr" -) - -const ( - TrieIndexVersion = 1 - - HeaderHasPrefix = 0b0000_0001 - HeaderHasValue = 0b0000_0010 - HeaderHasChildren = 0b0000_0100 -) - -type TrieIndex struct { - sync.RWMutex - Root *trieNode `json:"root"` -} - -// TODO: Store the suffix here so we can truncate the branches -type trieNode struct { - // Common prefix we ignore - Prefix []byte `json:"prefix,omitempty"` - - // The value of this node. - Value []byte `json:"value,omitempty"` - - // Any children of this node, mapped by the next byte of their path - Children map[byte]*trieNode `json:"children,omitempty"` -} - -func NewTrieIndexFromBytes(r io.Reader) (*TrieIndex, error) { - var index TrieIndex - if _, err := index.ReadFrom(r); err != nil { - return nil, err - } - return &index, nil -} - -func (index *TrieIndex) Upsert(key, value []byte) ([]byte, bool) { - if len(key) == 0 { - panic("len(key) must be > 0") - } - index.Lock() - defer index.Unlock() - return index.doUpsert(key, value) -} - -func (index *TrieIndex) doUpsert(key, value []byte) ([]byte, bool) { - if index.Root == nil { - index.Root = &trieNode{Prefix: key, Value: value} - return nil, false - } - - node := index.Root - var parent *trieNode - var parentIdx byte - splitPos := 0 - for len(key) > 0 { - for splitPos < len(node.Prefix) && len(key) > 0 { - if node.Prefix[splitPos] != key[0] { - break - } - splitPos++ - key = key[1:] - } - if splitPos != len(node.Prefix) { - // split this node - break - } - if len(key) == 0 { - // simple update-in-place at this node - break - } - - // Jump to the next child - parent = node - parentIdx = key[0] - child, ok := node.Children[key[0]] - if !ok { - if node.Children == nil { - node.Children = map[byte]*trieNode{} - } - // child doesn't exist. Insert a new node - node.Children[key[0]] = &trieNode{ - Prefix: key[1:], - Value: value, - } - return nil, false - } - node = child - key = key[1:] - splitPos = 0 - } - - // Key fully consumed just as we reached "node" - if len(key) == 0 { - if splitPos == len(node.Prefix) { - // node prefix matches (or is none), simple update-in-place - prev := node.Value - node.Value = value - return prev, true - } else { - // node has a prefix, so we need to insert a new one here and push it down - splitNode := &trieNode{ - Prefix: node.Prefix[:splitPos], // the matching segment - Value: value, - Children: map[byte]*trieNode{}, - } - splitNode.Children[node.Prefix[splitPos]] = node - node.Prefix = node.Prefix[splitPos+1:] // existing part that didn't match - if parent == nil { - index.Root = splitNode - } else { - parent.Children[parentIdx] = splitNode - } - return nil, false - } - } else { - // leftover key - if splitPos == len(node.Prefix) { - // new child - node.Children[key[0]] = &trieNode{ - Prefix: key[1:], - Value: value, - } - return nil, false - } else { - // Need to split the node - splitNode := &trieNode{ - Prefix: node.Prefix[:splitPos], - Children: map[byte]*trieNode{}, - } - splitNode.Children[node.Prefix[splitPos]] = node - splitNode.Children[key[0]] = &trieNode{Prefix: key[1:], Value: value} - node.Prefix = node.Prefix[splitPos+1:] - if parent == nil { - index.Root = splitNode - } else { - parent.Children[parentIdx] = splitNode - } - return nil, false - } - } -} - -func (index *TrieIndex) Get(key []byte) ([]byte, bool) { - index.RLock() - defer index.RUnlock() - if index.Root == nil { - return nil, false - } - - node := index.Root - splitPos := 0 - for len(key) > 0 { - for splitPos < len(node.Prefix) && len(key) > 0 { - if node.Prefix[splitPos] != key[0] { - break - } - splitPos++ - key = key[1:] - } - if splitPos != len(node.Prefix) { - // split this node - break - } - if len(key) == 0 { - // found it - return node.Value, true - } - - // Jump to the next child - child, ok := node.Children[key[0]] - if !ok { - // child doesn't exist - return nil, false - } - node = child - key = key[1:] - splitPos = 0 - } - - if len(key) == 0 { - return node.Value, true - } - return nil, false -} - -func (index *TrieIndex) Iterate(f func(key, value []byte)) { - index.RLock() - defer index.RUnlock() - if index.Root != nil { - index.Root.iterate(nil, f) - } -} - -func (node *trieNode) iterate(prefix []byte, f func(key, value []byte)) { - key := append(prefix, node.Prefix...) - if len(node.Value) > 0 { - f(key, node.Value) - } - - if node.Children != nil { - for b, child := range node.Children { - child.iterate(append(key, b), f) - } - } -} - -// TODO: For now this ignores duplicates. should it error? -func (i *TrieIndex) Merge(other *TrieIndex) error { - i.Lock() - defer i.Unlock() - - other.Iterate(func(key, value []byte) { - i.doUpsert(key, value) - }) - - return nil -} - -func (i *TrieIndex) MarshalBinary() ([]byte, error) { - i.RLock() - defer i.RUnlock() - - xdrRoot := xdr.TrieNode{} - - // Apparently this is possible? - if i.Root != nil { - xdrRoot.Prefix = i.Root.Prefix - xdrRoot.Value = i.Root.Value - xdrRoot.Children = make([]xdr.TrieNodeChild, 0, len(i.Root.Children)) - - for key, node := range i.Root.Children { - buildXdrTrie(key, node, &xdrRoot) - } - } - - xdrIndex := xdr.TrieIndex{Version: TrieIndexVersion, Root: xdrRoot} - return xdrIndex.MarshalBinary() -} - -func (i *TrieIndex) WriteTo(w io.Writer) (int64, error) { - i.RLock() - defer i.RUnlock() - - bytes, err := i.MarshalBinary() - if err != nil { - return int64(len(bytes)), err - } - - count, err := w.Write(bytes) - return int64(count), err -} - -func (i *TrieIndex) UnmarshalBinary(bytes []byte) error { - i.RLock() - defer i.RUnlock() - - xdrIndex := xdr.TrieIndex{} - err := xdrIndex.UnmarshalBinary(bytes) - if err != nil { - return err - } - - i.Root = &trieNode{ - Prefix: xdrIndex.Root.Prefix, - Value: xdrIndex.Root.Value, - Children: make(map[byte]*trieNode, len(xdrIndex.Root.Children)), - } - - for _, node := range xdrIndex.Root.Children { - buildTrie(&node, i.Root) - } - - return nil -} - -func (i *TrieIndex) ReadFrom(r io.Reader) (int64, error) { - i.RLock() - defer i.RUnlock() - - br := bufio.NewReader(r) - bytes, err := io.ReadAll(br) - if err != nil { - return int64(len(bytes)), err - } - - return int64(len(bytes)), i.UnmarshalBinary(bytes) -} - -// buildTrie recursively builds the equivalent `TrieNode` structure from raw -// XDR, creating the key->value child mapping from the flat list of children. -// Here, `xdrNode` is the node we're processing and `parent` is its non-XDR -// parent (i.e. the parent was already converted from XDR). -// -// This is the opposite of buildXdrTrie. -func buildTrie(xdrNode *xdr.TrieNodeChild, parent *trieNode) { - node := &trieNode{ - Prefix: xdrNode.Node.Prefix, - Value: xdrNode.Node.Value, - Children: make(map[byte]*trieNode, len(xdrNode.Node.Children)), - } - parent.Children[xdrNode.Key[0]] = node - - for _, child := range xdrNode.Node.Children { - buildTrie(&child, node) - } -} - -// buildXdrTrie recursively builds the XDR-equivalent TrieNode structure, where -// `i` is the node we're converting and `parent` is the already-converted -// parent. That is, the non-XDR version of `parent` should have had (`key`, `i`) -// as a child. -// -// This is the opposite of buildTrie. -func buildXdrTrie(key byte, node *trieNode, parent *xdr.TrieNode) { - self := xdr.TrieNode{ - Prefix: node.Prefix, - Value: node.Value, - Children: make([]xdr.TrieNodeChild, 0, len(node.Children)), - } - - for key, node := range node.Children { - buildXdrTrie(key, node, &self) - } - - parent.Children = append(parent.Children, xdr.TrieNodeChild{ - Key: [1]byte{key}, - Node: self, - }) -} - -// Ensure we're compatible with stdlib interfaces. -var _ io.WriterTo = &TrieIndex{} -var _ io.ReaderFrom = &TrieIndex{} - -var _ encoding.BinaryMarshaler = &TrieIndex{} -var _ encoding.BinaryUnmarshaler = &TrieIndex{} diff --git a/exp/lighthorizon/index/types/trie_test.go b/exp/lighthorizon/index/types/trie_test.go deleted file mode 100644 index 8745296429..0000000000 --- a/exp/lighthorizon/index/types/trie_test.go +++ /dev/null @@ -1,297 +0,0 @@ -package index - -import ( - "bytes" - "encoding/binary" - "encoding/hex" - "encoding/json" - "math/rand" - "strconv" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func randomTrie(t *testing.T, index *TrieIndex) (*TrieIndex, map[string]uint32) { - if index == nil { - index = &TrieIndex{} - } - inserts := map[string]uint32{} - numInserts := rand.Intn(100) - for j := 0; j < numInserts; j++ { - ledger := uint32(rand.Int63()) - hashBytes := make([]byte, 32) - if _, err := rand.Read(hashBytes); err != nil { - assert.NoError(t, err) - } - hash := hex.EncodeToString(hashBytes) - - inserts[hash] = ledger - b := make([]byte, 4) - binary.BigEndian.PutUint32(b, ledger) - index.Upsert([]byte(hash), b) - } - return index, inserts -} - -func TestTrieIndex(t *testing.T) { - for i := 0; i < 10_000; i++ { - index, inserts := randomTrie(t, nil) - - for key, expected := range inserts { - value, ok := index.Get([]byte(key)) - require.Truef(t, ok, "Key not found: %s", key) - ledger := binary.BigEndian.Uint32(value) - assert.Equalf(t, expected, ledger, - "Key %s found: %v, expected: %v", key, ledger, expected) - } - } -} - -func TestTrieIndexUpsertBasic(t *testing.T) { - index := &TrieIndex{} - - key := "key" - prev, ok := index.Upsert([]byte(key), []byte("a")) - assert.Nil(t, prev) - assert.Falsef(t, ok, "expected nil, got prev: %q", string(prev)) - - prev, ok = index.Upsert([]byte(key), []byte("b")) - assert.Equal(t, "a", string(prev)) - assert.Truef(t, ok, "expected 'a', got prev: %q", string(prev)) - - prev, ok = index.Upsert([]byte(key), []byte("c")) - assert.Equal(t, "b", string(prev)) - assert.Truef(t, ok, "expected 'b', got prev: %q", string(prev)) -} - -func TestTrieIndexSuffixes(t *testing.T) { - index := &TrieIndex{} - - prev, ok := index.Upsert([]byte("a"), []byte("a")) - require.False(t, ok) - require.Nil(t, prev) - - prev, ok = index.Upsert([]byte("ab"), []byte("ab")) - require.False(t, ok) - require.Nil(t, prev) - - prev, ok = index.Get([]byte("a")) - require.True(t, ok) - require.Equal(t, "a", string(prev)) - - prev, ok = index.Get([]byte("ab")) - require.True(t, ok) - require.Equal(t, "ab", string(prev)) - - prev, ok = index.Upsert([]byte("a"), []byte("b")) - require.True(t, ok) - require.Equal(t, "a", string(prev)) - - prev, ok = index.Get([]byte("a")) - require.True(t, ok) - require.Equal(t, "b", string(prev)) -} - -func TestTrieIndexSerialization(t *testing.T) { - for i := 0; i < 10_000; i++ { - t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { - index, inserts := randomTrie(t, nil) - - // Round-trip it to serialization and back - buf := &bytes.Buffer{} - nWritten, err := index.WriteTo(buf) - assert.NoError(t, err) - - read := &TrieIndex{} - nRead, err := read.ReadFrom(buf) - assert.NoError(t, err) - - assert.Equal(t, nWritten, nRead, "read more or less than we wrote") - - for key, expected := range inserts { - value, ok := read.Get([]byte(key)) - require.Truef(t, ok, "Key not found: %s", key) - - ledger := binary.BigEndian.Uint32(value) - assert.Equal(t, expected, ledger, "for key %s", key) - } - }) - } -} - -func requireEqualNodes(t *testing.T, expectedNode, gotNode *trieNode) { - expectedJSON, err := json.Marshal(expectedNode) - require.NoError(t, err) - expected := map[string]interface{}{} - require.NoError(t, json.Unmarshal(expectedJSON, &expected)) - - gotJSON, err := json.Marshal(gotNode) - require.NoError(t, err) - got := map[string]interface{}{} - require.NoError(t, json.Unmarshal(gotJSON, &got)) - - require.Equal(t, expected, got) -} - -func TestTrieIndexUpsertAdvanced(t *testing.T) { - // TODO: This is janky that we inspect the structure, but I want to make sure - // I've gotten the algorithms correct. - makeBase := func() *TrieIndex { - index := &TrieIndex{} - index.Upsert([]byte("annibale"), []byte{1}) - index.Upsert([]byte("annibalesco"), []byte{2}) - return index - } - - t.Run("base", func(t *testing.T) { - base := makeBase() - - baseExpected := &trieNode{ - Prefix: []byte("annibale"), - Value: []byte{1}, - Children: map[byte]*trieNode{ - byte('s'): { - Prefix: []byte("co"), - Value: []byte{2}, - }, - }, - } - requireEqualNodes(t, baseExpected, base.Root) - }) - - for _, tc := range []struct { - key string - expected *trieNode - }{ - {"annientare", &trieNode{ - Prefix: []byte("anni"), - Children: map[byte]*trieNode{ - 'b': { - Prefix: []byte("ale"), - Value: []byte{1}, - Children: map[byte]*trieNode{ - 's': { - Prefix: []byte("co"), - Value: []byte{2}, - }, - }, - }, - 'e': { - Prefix: []byte("ntare"), - Value: []byte{3}, - }, - }, - }}, - {"annibali", &trieNode{ - Prefix: []byte("annibal"), - Children: map[byte]*trieNode{ - 'e': { - Value: []byte{1}, - Children: map[byte]*trieNode{ - 's': { - Prefix: []byte("co"), - Value: []byte{2}, - }, - }, - }, - 'i': { - Value: []byte{3}, - }, - }, - }}, - {"ago", &trieNode{ - Prefix: []byte("a"), - Children: map[byte]*trieNode{ - 'n': { - Prefix: []byte("nibale"), - Value: []byte{1}, - Children: map[byte]*trieNode{ - 's': { - Prefix: []byte("co"), - Value: []byte{2}, - }, - }, - }, - 'g': { - Prefix: []byte("o"), - Value: []byte{3}, - }, - }, - }}, - {"ciao", &trieNode{ - Children: map[byte]*trieNode{ - 'a': { - Prefix: []byte("nnibale"), - Value: []byte{1}, - Children: map[byte]*trieNode{ - 's': { - Prefix: []byte("co"), - Value: []byte{2}, - }, - }, - }, - 'c': { - Prefix: []byte("iao"), - Value: []byte{3}, - }, - }, - }}, - {"anni", &trieNode{ - Prefix: []byte("anni"), - Value: []byte{3}, - Children: map[byte]*trieNode{ - 'b': { - Prefix: []byte("ale"), - Value: []byte{1}, - Children: map[byte]*trieNode{ - 's': { - Prefix: []byte("co"), - Value: []byte{2}, - }, - }, - }, - }, - }}, - } { - t.Run(tc.key, func(t *testing.T) { - // Do our upsert - index := makeBase() - index.Upsert([]byte(tc.key), []byte{3}) - - // Check the tree is shaped right - requireEqualNodes(t, tc.expected, index.Root) - - // Check the value matches expected - value, ok := index.Get([]byte(tc.key)) - require.True(t, ok) - require.Equal(t, []byte{3}, value) - }) - } -} - -func TestTrieIndexMerge(t *testing.T) { - for i := 0; i < 10_000; i++ { - a, aInserts := randomTrie(t, nil) - b, bInserts := randomTrie(t, nil) - - require.NoError(t, a.Merge(b)) - - // Should still have all the A keys - for key, expected := range aInserts { - value, ok := a.Get([]byte(key)) - require.Truef(t, ok, "Key not found: %s", key) - ledger := binary.BigEndian.Uint32(value) - assert.Equalf(t, expected, ledger, "Key %s found", key) - } - - // Should now also have all the B keys - for key, expected := range bInserts { - value, ok := a.Get([]byte(key)) - require.Truef(t, ok, "Key not found: %s", key) - ledger := binary.BigEndian.Uint32(value) - assert.Equalf(t, expected, ledger, "Key %s found", key) - } - } -} diff --git a/exp/lighthorizon/ingester/ingester.go b/exp/lighthorizon/ingester/ingester.go deleted file mode 100644 index 21bb400b50..0000000000 --- a/exp/lighthorizon/ingester/ingester.go +++ /dev/null @@ -1,55 +0,0 @@ -package ingester - -import ( - "context" - - "github.com/stellar/go/ingest" - "github.com/stellar/go/metaarchive" - - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/xdr" -) - -type IngesterConfig struct { - SourceUrl string - NetworkPassphrase string - - CacheDir string - CacheSize int - - ParallelDownloads uint -} - -type liteIngester struct { - metaarchive.MetaArchive - networkPassphrase string -} - -func (i *liteIngester) PrepareRange(ctx context.Context, r historyarchive.Range) error { - return nil -} - -func (i *liteIngester) NewLedgerTransactionReader( - ledgerCloseMeta xdr.SerializedLedgerCloseMeta, -) (LedgerTransactionReader, error) { - reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta( - i.networkPassphrase, - ledgerCloseMeta.MustV0()) - - return &liteLedgerTransactionReader{reader}, err -} - -type liteLedgerTransactionReader struct { - *ingest.LedgerTransactionReader -} - -func (reader *liteLedgerTransactionReader) Read() (LedgerTransaction, error) { - ingestedTx, err := reader.LedgerTransactionReader.Read() - if err != nil { - return LedgerTransaction{}, err - } - return LedgerTransaction{LedgerTransaction: &ingestedTx}, nil -} - -var _ Ingester = (*liteIngester)(nil) // ensure conformity to the interface -var _ LedgerTransactionReader = (*liteLedgerTransactionReader)(nil) diff --git a/exp/lighthorizon/ingester/main.go b/exp/lighthorizon/ingester/main.go deleted file mode 100644 index a93636c67a..0000000000 --- a/exp/lighthorizon/ingester/main.go +++ /dev/null @@ -1,87 +0,0 @@ -package ingester - -import ( - "context" - "fmt" - "net/url" - - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/ingest" - "github.com/stellar/go/metaarchive" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/log" - "github.com/stellar/go/support/storage" - "github.com/stellar/go/xdr" -) - -// -// LightHorizon data model -// - -// Ingester combines a source of unpacked ledger metadata and a way to create a -// ingestion reader interface on top of it. -type Ingester interface { - metaarchive.MetaArchive - - PrepareRange(ctx context.Context, r historyarchive.Range) error - NewLedgerTransactionReader( - ledgerCloseMeta xdr.SerializedLedgerCloseMeta, - ) (LedgerTransactionReader, error) -} - -// For now, this mirrors the `ingest` library exactly, but it's replicated so -// that we can diverge in the future if necessary. -type LedgerTransaction struct { - *ingest.LedgerTransaction -} - -type LedgerTransactionReader interface { - Read() (LedgerTransaction, error) -} - -func NewIngester(config IngesterConfig) (Ingester, error) { - if config.CacheSize <= 0 { - return nil, fmt.Errorf("invalid cache size: %d", config.CacheSize) - } - - // Now, set up a simple filesystem-like access to the backend and wrap it in - // a local on-disk LRU cache if we can. - source, err := historyarchive.ConnectBackend( - config.SourceUrl, - storage.ConnectOptions{Context: context.Background()}, - ) - if err != nil { - return nil, errors.Wrapf(err, "failed to connect to %s", config.SourceUrl) - } - - parsed, err := url.Parse(config.SourceUrl) - if err != nil { - return nil, errors.Wrapf(err, "%s is not a valid URL", config.SourceUrl) - } - - if parsed.Scheme != "file" { // otherwise, already on-disk - cache, errr := storage.MakeOnDiskCache(source, config.CacheDir, uint(config.CacheSize)) - - if errr != nil { // non-fatal: warn but continue w/o cache - log.WithField("path", config.CacheDir).WithError(errr). - Warnf("Failed to create cached ledger backend") - } else { - log.WithField("path", config.CacheDir). - Infof("On-disk cache configured") - source = cache - } - } - - if config.ParallelDownloads > 1 { - log.Infof("Enabling parallel ledger fetches with %d workers", config.ParallelDownloads) - return NewParallelIngester( - metaarchive.NewMetaArchive(source), - config.NetworkPassphrase, - config.ParallelDownloads), nil - } - - return &liteIngester{ - MetaArchive: metaarchive.NewMetaArchive(source), - networkPassphrase: config.NetworkPassphrase, - }, nil -} diff --git a/exp/lighthorizon/ingester/mock_ingester.go b/exp/lighthorizon/ingester/mock_ingester.go deleted file mode 100644 index 62c377ce78..0000000000 --- a/exp/lighthorizon/ingester/mock_ingester.go +++ /dev/null @@ -1,44 +0,0 @@ -package ingester - -import ( - "context" - - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/mock" -) - -type MockIngester struct { - mock.Mock -} - -func (m *MockIngester) NewLedgerTransactionReader( - ledgerCloseMeta xdr.SerializedLedgerCloseMeta, -) (LedgerTransactionReader, error) { - args := m.Called(ledgerCloseMeta) - return args.Get(0).(LedgerTransactionReader), args.Error(1) -} - -func (m *MockIngester) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - args := m.Called(ctx) - return args.Get(0).(uint32), args.Error(1) -} - -func (m *MockIngester) GetLedger(ctx context.Context, sequence uint32) (xdr.SerializedLedgerCloseMeta, error) { - args := m.Called(ctx, sequence) - return args.Get(0).(xdr.SerializedLedgerCloseMeta), args.Error(1) -} - -func (m *MockIngester) PrepareRange(ctx context.Context, r historyarchive.Range) error { - args := m.Called(ctx, r) - return args.Error(0) -} - -type MockLedgerTransactionReader struct { - mock.Mock -} - -func (m *MockLedgerTransactionReader) Read() (LedgerTransaction, error) { - args := m.Called() - return args.Get(0).(LedgerTransaction), args.Error(1) -} diff --git a/exp/lighthorizon/ingester/parallel_ingester.go b/exp/lighthorizon/ingester/parallel_ingester.go deleted file mode 100644 index 133b0a37c4..0000000000 --- a/exp/lighthorizon/ingester/parallel_ingester.go +++ /dev/null @@ -1,141 +0,0 @@ -package ingester - -import ( - "context" - "sync" - "time" - - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/metaarchive" - "github.com/stellar/go/support/collections/set" - "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" -) - -type parallelIngester struct { - liteIngester - - ledgerFeed sync.Map // thread-safe version of map[uint32]downloadState - ledgerQueue set.ISet[uint32] - - workQueue chan uint32 - signalChan chan error -} - -type downloadState struct { - ledger xdr.SerializedLedgerCloseMeta - err error -} - -// NewParallelIngester creates an ingester on the given `ledgerSource` using the -// given `networkPassphrase` that can download ledgers in parallel via -// `workerCount` workers via `PrepareRange()`. -func NewParallelIngester( - archive metaarchive.MetaArchive, - networkPassphrase string, - workerCount uint, -) *parallelIngester { - self := ¶llelIngester{ - liteIngester: liteIngester{ - MetaArchive: archive, - networkPassphrase: networkPassphrase, - }, - ledgerFeed: sync.Map{}, - ledgerQueue: set.NewSafeSet[uint32](64), - workQueue: make(chan uint32, workerCount), - signalChan: make(chan error), - } - - // These are the workers that download & store ledgers in memory. - for j := uint(0); j < workerCount; j++ { - go func(jj uint) { - for ledgerSeq := range self.workQueue { - start := time.Now() - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - txmeta, err := self.liteIngester.GetLedger(ctx, ledgerSeq) - cancel() - - log.WithField("duration", time.Since(start)). - WithField("worker", jj).WithError(err). - Debugf("Downloaded ledger %d", ledgerSeq) - - self.ledgerFeed.Store(ledgerSeq, downloadState{txmeta, err}) - self.signalChan <- err - } - }(j) - } - - return self -} - -// PrepareRange will create a set of parallel worker routines that feed ledgers -// to a channel in the order they're downloaded and store the results in an -// array. You can use this to download ledgers in parallel to fetching them -// individually via `GetLedger()`. `PrepareRange()` is thread-safe. -// -// Note: The passed in range `r` is inclusive of the boundaries. -func (i *parallelIngester) PrepareRange(ctx context.Context, r historyarchive.Range) error { - // The taskmaster adds ledger sequence numbers to the work queue. - go func() { - start := time.Now() - defer func() { - log.WithField("duration", time.Since(start)). - WithError(ctx.Err()). - Infof("Download of ledger range: [%d, %d] (%d ledgers) complete", - r.Low, r.High, r.Size()) - }() - - for seq := r.Low; seq <= r.High; seq++ { - if ctx.Err() != nil { - log.Warnf("Cancelling remaining downloads ([%d, %d]): %v", - seq, r.High, ctx.Err()) - break - } - - // Adding this to the "set of ledgers being downloaded in parallel" - // means that if a GetLedger() request happens in this range but - // outside of the realm of processing, it can be prioritized by the - // normal, direct download. - i.ledgerQueue.Add(seq) - - i.workQueue <- seq // blocks until there's an available worker - - // We don't remove from the queue here, preferring to remove when - // it's actually pulled from the worker. Removing here would mean - // you could have multiple instances of a ledger download happening. - } - }() - - return nil -} - -func (i *parallelIngester) GetLedger( - ctx context.Context, ledgerSeq uint32, -) (xdr.SerializedLedgerCloseMeta, error) { - // If the requested ledger is out of the queued up ranges, we can fall back - // to the default non-parallel download method. - if !i.ledgerQueue.Contains(ledgerSeq) { - return i.liteIngester.GetLedger(ctx, ledgerSeq) - } - - // If the ledger isn't available yet, wait for the download worker. - var err error - for err == nil { - if iState, ok := i.ledgerFeed.Load(ledgerSeq); ok { - state := iState.(downloadState) - i.ledgerFeed.Delete(ledgerSeq) - i.ledgerQueue.Remove(ledgerSeq) - return state.ledger, state.err - } - - select { - case err = <-i.signalChan: // blocks until another ledger downloads - case <-ctx.Done(): - err = ctx.Err() - } - } - - return xdr.SerializedLedgerCloseMeta{}, err -} - -var _ Ingester = (*parallelIngester)(nil) // ensure conformity to the interface diff --git a/exp/lighthorizon/ingester/participants.go b/exp/lighthorizon/ingester/participants.go deleted file mode 100644 index ebc49173cf..0000000000 --- a/exp/lighthorizon/ingester/participants.go +++ /dev/null @@ -1,35 +0,0 @@ -package ingester - -import ( - "github.com/stellar/go/exp/lighthorizon/index" - "github.com/stellar/go/support/collections/set" - "github.com/stellar/go/xdr" -) - -// GetTransactionParticipants takes a LedgerTransaction and returns a set of all -// participants (accounts) in the transaction. If there is any error, it will -// return nil and the error. -func GetTransactionParticipants(tx LedgerTransaction) (set.Set[string], error) { - participants, err := index.GetTransactionParticipants(*tx.LedgerTransaction) - if err != nil { - return nil, err - } - set := set.NewSet[string](len(participants)) - set.AddSlice(participants) - return set, nil -} - -// GetOperationParticipants takes a LedgerTransaction, the Operation within the -// transaction, and the 0-based index of the operation within the transaction. -// It will return a set of all participants (accounts) in the operation. If -// there is any error, it will return nil and the error. -func GetOperationParticipants(tx LedgerTransaction, op xdr.Operation, opIndex int) (set.Set[string], error) { - participants, err := index.GetOperationParticipants(*tx.LedgerTransaction, op, opIndex) - if err != nil { - return nil, err - } - - set := set.NewSet[string](len(participants)) - set.AddSlice(participants) - return set, nil -} diff --git a/exp/lighthorizon/main.go b/exp/lighthorizon/main.go deleted file mode 100644 index f7c502d465..0000000000 --- a/exp/lighthorizon/main.go +++ /dev/null @@ -1,183 +0,0 @@ -package main - -import ( - "context" - "net/http" - - "github.com/go-chi/chi" - "github.com/prometheus/client_golang/prometheus" - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - - "github.com/stellar/go/exp/lighthorizon/actions" - "github.com/stellar/go/exp/lighthorizon/index" - "github.com/stellar/go/exp/lighthorizon/ingester" - "github.com/stellar/go/exp/lighthorizon/services" - "github.com/stellar/go/exp/lighthorizon/tools" - - "github.com/stellar/go/network" - "github.com/stellar/go/support/log" -) - -const ( - HorizonLiteVersion = "0.0.1-alpha" - defaultCacheSize = (60 * 60 * 24) / 6 // 1 day of ledgers @ 6s each -) - -func main() { - log.SetLevel(logrus.InfoLevel) // default for subcommands - - cmd := &cobra.Command{ - Use: "lighthorizon ", - Long: "Horizon Lite command suite", - RunE: func(cmd *cobra.Command, args []string) error { - return cmd.Usage() // require a subcommand - }, - } - - serve := &cobra.Command{ - Use: "serve ", - Long: `Starts the Horizon Lite server, binding it to port 8080 on all -local interfaces of the host. You can refer to the OpenAPI documentation located -at the /api endpoint to see what endpoints are supported. - -The should be a URL to meta archives from which to read unpacked -ledger files, while the should be a URL containing indices that -break down accounts by active ledgers.`, - Run: func(cmd *cobra.Command, args []string) { - if len(args) != 2 { - cmd.Usage() - return - } - - sourceUrl, indexStoreUrl := args[0], args[1] - - networkPassphrase, _ := cmd.Flags().GetString("network-passphrase") - switch networkPassphrase { - case "testnet": - networkPassphrase = network.TestNetworkPassphrase - case "pubnet": - networkPassphrase = network.PublicNetworkPassphrase - } - - cacheDir, _ := cmd.Flags().GetString("ledger-cache") - cacheSize, _ := cmd.Flags().GetUint("ledger-cache-size") - logLevelParam, _ := cmd.Flags().GetString("log-level") - downloadCount, _ := cmd.Flags().GetUint("parallel-downloads") - - L := log.WithField("service", "horizon-lite") - logLevel, err := logrus.ParseLevel(logLevelParam) - if err != nil { - log.Warnf("Failed to parse log level '%s', defaulting to 'info'.", logLevelParam) - logLevel = log.InfoLevel - } - L.SetLevel(logLevel) - L.Info("Starting lighthorizon!") - - registry := prometheus.NewRegistry() - indexStore, err := index.ConnectWithConfig(index.StoreConfig{ - URL: indexStoreUrl, - Log: L.WithField("service", "index"), - Metrics: registry, - }) - if err != nil { - log.Fatal(err) - return - } - - ingester, err := ingester.NewIngester(ingester.IngesterConfig{ - SourceUrl: sourceUrl, - NetworkPassphrase: networkPassphrase, - CacheDir: cacheDir, - CacheSize: int(cacheSize), - ParallelDownloads: downloadCount, - }) - if err != nil { - log.Fatal(err) - return - } - - latestLedger, err := ingester.GetLatestLedgerSequence(context.Background()) - if err != nil { - log.Fatalf("Failed to retrieve latest ledger from %s: %v", sourceUrl, err) - return - } - log.Infof("The latest ledger stored at %s is %d.", sourceUrl, latestLedger) - - cachePreloadCount, _ := cmd.Flags().GetUint32("ledger-cache-preload") - cachePreloadStart, _ := cmd.Flags().GetUint32("ledger-cache-preload-start") - if cachePreloadCount > 0 { - if cacheDir == "" { - log.Fatalf("--ledger-cache-preload=%d specified but no "+ - "--ledger-cache directory provided.", - cachePreloadCount) - return - } else { - startLedger := int(latestLedger) - int(cachePreloadCount) - if cachePreloadStart > 0 { - startLedger = int(cachePreloadStart) - } - if startLedger <= 0 { - log.Warnf("Starting ledger invalid (%d), defaulting to 2.", - startLedger) - startLedger = 2 - } - - log.Infof("Preloading cache at %s with %d ledgers, starting at ledger %d.", - cacheDir, startLedger, cachePreloadCount) - go func() { - tools.BuildCache(sourceUrl, cacheDir, - uint32(startLedger), cachePreloadCount, false) - }() - } - } - - Config := services.Config{ - Ingester: ingester, - Passphrase: networkPassphrase, - IndexStore: indexStore, - Metrics: services.NewMetrics(registry), - } - - lightHorizon := services.LightHorizon{ - Transactions: &services.TransactionRepository{ - Config: Config, - }, - Operations: &services.OperationRepository{ - Config: Config, - }, - } - - // Inject our config into the root response. - router := lightHorizonHTTPHandler(registry, lightHorizon).(*chi.Mux) - router.MethodFunc(http.MethodGet, "/", actions.Root(actions.RootResponse{ - Version: HorizonLiteVersion, - LedgerSource: sourceUrl, - IndexSource: indexStoreUrl, - - LatestLedger: latestLedger, - })) - - log.Fatal(http.ListenAndServe(":8080", router)) - }, - } - - serve.Flags().String("log-level", "info", - "logging level: 'info', 'debug', 'warn', 'error', 'panic', 'fatal', or 'trace'") - serve.Flags().String("network-passphrase", "pubnet", "network passphrase") - serve.Flags().String("ledger-cache", "", "path to cache frequently-used ledgers; "+ - "if left empty, uses a temporary directory") - serve.Flags().Uint("ledger-cache-size", defaultCacheSize, - "number of ledgers to store in the cache") - serve.Flags().Uint32("ledger-cache-preload", 0, - "should the cache come preloaded with the latest ledgers?") - serve.Flags().Uint32("ledger-cache-preload-start", 0, - "the preload should start at ledger ") - serve.Flags().Uint("parallel-downloads", 1, - "how many workers should download ledgers in parallel?") - - cmd.AddCommand(serve) - tools.AddCacheCommands(cmd) - tools.AddIndexCommands(cmd) - cmd.Execute() -} diff --git a/exp/lighthorizon/services/cursor.go b/exp/lighthorizon/services/cursor.go deleted file mode 100644 index 8f2d2b0b5c..0000000000 --- a/exp/lighthorizon/services/cursor.go +++ /dev/null @@ -1,102 +0,0 @@ -package services - -import ( - "github.com/stellar/go/exp/lighthorizon/index" - "github.com/stellar/go/toid" -) - -// CursorManager describes a way to control how a cursor advances for a -// particular indexing strategy. -type CursorManager interface { - Begin(cursor int64) (int64, error) - Advance(times uint) (int64, error) -} - -type AccountActivityCursorManager struct { - AccountId string - - store index.Store - lastCursor *toid.ID -} - -func NewCursorManagerForAccountActivity(store index.Store, accountId string) *AccountActivityCursorManager { - return &AccountActivityCursorManager{AccountId: accountId, store: store} -} - -func (c *AccountActivityCursorManager) Begin(cursor int64) (int64, error) { - freq := checkpointManager.GetCheckpointFrequency() - id := toid.Parse(cursor) - lastCheckpoint := uint32(0) - if id.LedgerSequence >= int32(checkpointManager.GetCheckpointFrequency()) { - lastCheckpoint = index.GetCheckpointNumber(uint32(id.LedgerSequence)) - } - - // We shouldn't take the provided cursor for granted: instead, we should - // skip ahead to the first active ledger that's >= the given cursor. - // - // For example, someone might say ?cursor=0 but the first active checkpoint - // is actually 40M ledgers in. - firstCheckpoint, err := c.store.NextActive(c.AccountId, allTransactionsIndex, lastCheckpoint) - if err != nil { - return cursor, err - } - - nextLedger := (firstCheckpoint - 1) * freq - - // However, if the given cursor is actually *more* specific than the index - // can give us (e.g. somewhere *within* an active checkpoint range), prefer - // it rather than starting over. - if nextLedger < uint32(id.LedgerSequence) { - better := toid.Parse(cursor) - c.lastCursor = &better - return cursor, nil - } - - c.lastCursor = toid.New(int32(nextLedger), 1, 1) - return c.lastCursor.ToInt64(), nil -} - -func (c *AccountActivityCursorManager) Advance(times uint) (int64, error) { - if c.lastCursor == nil { - panic("invalid cursor, call Begin() first") - } - - // - // Advancing the cursor means deciding whether or not we need to query - // the index. - // - freq := checkpointManager.GetCheckpointFrequency() - - for i := uint(1); i <= times; i++ { - lastLedger := uint32(c.lastCursor.LedgerSequence) - - if checkpointManager.IsCheckpoint(lastLedger) { - // If the last cursor we looked at was a checkpoint ledger, then we - // need to jump ahead to the next checkpoint. Note that NextActive() - // is "inclusive" so if the parameter is an active checkpoint it - // will return itself. - checkpoint := index.GetCheckpointNumber(uint32(c.lastCursor.LedgerSequence)) - checkpoint, err := c.store.NextActive(c.AccountId, allTransactionsIndex, checkpoint+1) - if err != nil { - return c.lastCursor.ToInt64(), err - } - - // We add a -1 here because an active checkpoint indicates that an - // account had activity in the *previous* 64 ledgers, so we need to - // backtrack to that ledger range. - c.lastCursor = toid.New(int32((checkpoint-1)*freq), 1, 1) - } else { - // Otherwise, we can just bump the ledger number. - c.lastCursor = toid.New(int32(lastLedger+1), 1, 1) - } - } - - return c.lastCursor.ToInt64(), nil -} - -var _ CursorManager = (*AccountActivityCursorManager)(nil) // ensure conformity to the interface - -// getLedgerFromCursor is a helpful way to turn a cursor into a ledger number -func getLedgerFromCursor(cursor int64) uint32 { - return uint32(toid.Parse(cursor).LedgerSequence) -} diff --git a/exp/lighthorizon/services/cursor_test.go b/exp/lighthorizon/services/cursor_test.go deleted file mode 100644 index 2112ae3715..0000000000 --- a/exp/lighthorizon/services/cursor_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package services - -import ( - "io" - "testing" - - "github.com/stellar/go/exp/lighthorizon/index" - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/keypair" - "github.com/stellar/go/toid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - checkpointMgr = historyarchive.NewCheckpointManager(0) -) - -func TestAccountTransactionCursorManager(t *testing.T) { - freq := int32(checkpointMgr.GetCheckpointFrequency()) - accountId := keypair.MustRandom().Address() - - // Create an index and fill it with some checkpoint details. - tmp := t.TempDir() - store, err := index.NewFileStore(tmp, - index.StoreConfig{ - URL: "file://" + tmp, - Workers: 4, - }, - ) - require.NoError(t, err) - - for _, checkpoint := range []uint32{1, 5, 10, 12} { - require.NoError(t, store.AddParticipantsToIndexes( - checkpoint, allTransactionsIndex, []string{accountId})) - } - - cursorMgr := NewCursorManagerForAccountActivity(store, accountId) - - cursor := toid.New(1, 1, 1) - var nextCursor int64 - - // first checkpoint works - nextCursor, err = cursorMgr.Begin(cursor.ToInt64()) - require.NoError(t, err) - assert.EqualValues(t, 1, getLedgerFromCursor(nextCursor)) - - // cursor is preserved if mid-active-range - cursor.LedgerSequence = freq / 2 - nextCursor, err = cursorMgr.Begin(cursor.ToInt64()) - require.NoError(t, err) - assert.EqualValues(t, cursor.LedgerSequence, getLedgerFromCursor(nextCursor)) - - // cursor jumps ahead if not active - cursor.LedgerSequence = 2 * freq - nextCursor, err = cursorMgr.Begin(cursor.ToInt64()) - require.NoError(t, err) - assert.EqualValues(t, 4*freq, getLedgerFromCursor(nextCursor)) - - // cursor increments - for i := int32(1); i < freq; i++ { - nextCursor, err = cursorMgr.Advance(1) - require.NoError(t, err) - assert.EqualValues(t, 4*freq+i, getLedgerFromCursor(nextCursor)) - } - - // cursor jumps to next active checkpoint - nextCursor, err = cursorMgr.Advance(1) - require.NoError(t, err) - assert.EqualValues(t, 9*freq, getLedgerFromCursor(nextCursor)) - - // cursor skips - nextCursor, err = cursorMgr.Advance(5) - require.NoError(t, err) - assert.EqualValues(t, 9*freq+5, getLedgerFromCursor(nextCursor)) - - // cursor jumps to next active when skipping - nextCursor, err = cursorMgr.Advance(uint(freq - 5)) - require.NoError(t, err) - assert.EqualValues(t, 11*freq, getLedgerFromCursor(nextCursor)) - - // cursor EOFs at the end - nextCursor, err = cursorMgr.Advance(uint(freq - 1)) - require.NoError(t, err) - assert.EqualValues(t, 12*freq-1, getLedgerFromCursor(nextCursor)) - _, err = cursorMgr.Advance(1) - assert.ErrorIs(t, err, io.EOF) - - // cursor EOFs if skipping past the end - rewind := toid.New(int32(getLedgerFromCursor(nextCursor)-5), 0, 0) - nextCursor, err = cursorMgr.Begin(rewind.ToInt64()) - require.NoError(t, err) - assert.EqualValues(t, rewind.LedgerSequence, getLedgerFromCursor(nextCursor)) - _, err = cursorMgr.Advance(uint(freq)) - assert.ErrorIs(t, err, io.EOF) -} diff --git a/exp/lighthorizon/services/main.go b/exp/lighthorizon/services/main.go deleted file mode 100644 index d391fc8baf..0000000000 --- a/exp/lighthorizon/services/main.go +++ /dev/null @@ -1,216 +0,0 @@ -package services - -import ( - "context" - "io" - "time" - - "github.com/prometheus/client_golang/prometheus" - "golang.org/x/exp/constraints" - - "github.com/stellar/go/exp/lighthorizon/index" - "github.com/stellar/go/exp/lighthorizon/ingester" - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" -) - -const ( - allTransactionsIndex = "all/all" - allPaymentsIndex = "all/payments" - slowFetchDurationThreshold = time.Second -) - -var ( - checkpointManager = historyarchive.NewCheckpointManager(0) -) - -// NewMetrics returns a Metrics instance containing all the prometheus -// metrics necessary for running light horizon services. -func NewMetrics(registry *prometheus.Registry) Metrics { - const minute = 60 - const day = 24 * 60 * minute - responseAgeHistogram := prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Namespace: "horizon_lite", - Subsystem: "services", - Name: "response_age", - Buckets: []float64{ - 5 * minute, - 60 * minute, - day, - 7 * day, - 30 * day, - 90 * day, - 180 * day, - 365 * day, - }, - Help: "Age of the response for each service, sliding window = 10m", - }, - []string{"request", "successful"}, - ) - registry.MustRegister(responseAgeHistogram) - return Metrics{ - ResponseAgeHistogram: responseAgeHistogram, - } -} - -type LightHorizon struct { - Operations OperationService - Transactions TransactionService -} - -type Metrics struct { - ResponseAgeHistogram *prometheus.HistogramVec -} - -type Config struct { - Ingester ingester.Ingester - IndexStore index.Store - Passphrase string - Metrics Metrics -} - -// searchCallback is a generic way for any endpoint to process a transaction and -// its corresponding ledger. It should return whether or not we should stop -// processing (e.g. when a limit is reached) and any error that occurred. -type searchCallback func(ingester.LedgerTransaction, *xdr.LedgerHeader) (finished bool, err error) - -func searchAccountTransactions(ctx context.Context, - cursor int64, - accountId string, - config Config, - callback searchCallback, -) error { - cursorMgr := NewCursorManagerForAccountActivity(config.IndexStore, accountId) - cursor, err := cursorMgr.Begin(cursor) - if err == io.EOF { - return nil - } else if err != nil { - return err - } - nextLedger := getLedgerFromCursor(cursor) - - log.WithField("cursor", cursor). - Debugf("Searching %s for account %s starting at ledger %d", - allTransactionsIndex, accountId, nextLedger) - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - fullStart := time.Now() - fetchDuration := time.Duration(0) - processDuration := time.Duration(0) - indexFetchDuration := time.Duration(0) - count := int64(0) - - defer func() { - log.WithField("ledgers", count). - WithField("ledger-fetch", fetchDuration). - WithField("ledger-process", processDuration). - WithField("index-fetch", indexFetchDuration). - WithField("avg-ledger-fetch", getAverageDuration(fetchDuration, count)). - WithField("avg-ledger-process", getAverageDuration(processDuration, count)). - WithField("avg-index-fetch", getAverageDuration(indexFetchDuration, count)). - WithField("total", time.Since(fullStart)). - Infof("Fulfilled request for account %s at cursor %d", accountId, cursor) - }() - - checkpointMgr := historyarchive.NewCheckpointManager(0) - - for { - if checkpointMgr.IsCheckpoint(nextLedger) { - r := historyarchive.Range{ - Low: nextLedger, - High: checkpointMgr.NextCheckpoint(nextLedger + 1), - } - log.Infof("Preparing ledger range [%d, %d]", r.Low, r.High) - if innerErr := config.Ingester.PrepareRange(ctx, r); innerErr != nil { - log.Errorf("failed to prepare ledger range [%d, %d]: %v", - r.Low, r.High, innerErr) - } - } - - start := time.Now() - ledger, innerErr := config.Ingester.GetLedger(ctx, nextLedger) - - // TODO: We should have helpful error messages when innerErr points to a - // 404 for that particular ledger, since that situation shouldn't happen - // under normal operations, but rather indicates a problem with the - // backing archive. - if innerErr != nil { - return errors.Wrapf(innerErr, - "failed to retrieve ledger %d from archive", nextLedger) - } - count++ - thisFetchDuration := time.Since(start) - if thisFetchDuration > slowFetchDurationThreshold { - log.WithField("duration", thisFetchDuration). - Warnf("Fetching ledger %d was really slow", nextLedger) - } - fetchDuration += thisFetchDuration - - start = time.Now() - reader, innerErr := config.Ingester.NewLedgerTransactionReader(ledger) - if innerErr != nil { - return errors.Wrapf(innerErr, - "failed to read ledger %d", nextLedger) - } - - for { - if ctx.Err() != nil { - return ctx.Err() - } - - tx, readErr := reader.Read() - if readErr == io.EOF { - break - } else if readErr != nil { - return readErr - } - - // Note: If we move to ledger-based indices, we don't need this, - // since we have a guarantee that the transaction will contain - // the account as a participant. - participants, participantErr := ingester.GetTransactionParticipants(tx) - if participantErr != nil { - return participantErr - } - - if _, found := participants[accountId]; found { - finished, callBackErr := callback(tx, &ledger.V0.V0.LedgerHeader.Header) - if callBackErr != nil { - return callBackErr - } else if finished { - processDuration += time.Since(start) - return nil - } - } - } - - processDuration += time.Since(start) - start = time.Now() - - cursor, err = cursorMgr.Advance(1) - if err != nil && err != io.EOF { - return err - } - - nextLedger = getLedgerFromCursor(cursor) - indexFetchDuration += time.Since(start) - if err == io.EOF { - break - } - } - - return nil -} - -func getAverageDuration[ - T constraints.Signed | constraints.Float, -](d time.Duration, count T) time.Duration { - if count == 0 { - return 0 // don't bomb on div-by-zero - } - return time.Duration(int64(float64(d.Nanoseconds()) / float64(count))) -} diff --git a/exp/lighthorizon/services/main_test.go b/exp/lighthorizon/services/main_test.go deleted file mode 100644 index a8a3958214..0000000000 --- a/exp/lighthorizon/services/main_test.go +++ /dev/null @@ -1,250 +0,0 @@ -package services - -import ( - "context" - "io" - "testing" - - "github.com/prometheus/client_golang/prometheus" - - "github.com/stellar/go/exp/lighthorizon/index" - "github.com/stellar/go/exp/lighthorizon/ingester" - "github.com/stellar/go/ingest" - "github.com/stellar/go/toid" - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -var ( - passphrase = "White New England clam chowder" - accountId = "GDCXSQPVE45DVGT2ZRFFIIHSJ2EJED65W6AELGWIDRMPMWNXCEBJ4FKX" - startLedgerSeq = 1586112 -) - -func TestItGetsTransactionsByAccount(t *testing.T) { - ctx := context.Background() - - // this is in the checkpoint range prior to the first active checkpoint - ledgerSeq := checkpointMgr.PrevCheckpoint(uint32(startLedgerSeq)) - cursor := toid.New(int32(ledgerSeq), 1, 1).ToInt64() - - t.Run("first", func(tt *testing.T) { - txService := newTransactionService(ctx) - - txs, err := txService.GetTransactionsByAccount(ctx, cursor, 1, accountId) - require.NoError(tt, err) - require.Len(tt, txs, 1) - require.Equal(tt, txs[0].LedgerHeader.LedgerSeq, xdr.Uint32(1586113)) - require.EqualValues(tt, txs[0].TxIndex, 2) - }) - - t.Run("without cursor", func(tt *testing.T) { - txService := newTransactionService(ctx) - - txs, err := txService.GetTransactionsByAccount(ctx, 0, 1, accountId) - require.NoError(tt, err) - require.Len(tt, txs, 1) - require.Equal(tt, txs[0].LedgerHeader.LedgerSeq, xdr.Uint32(1586113)) - require.EqualValues(tt, txs[0].TxIndex, 2) - }) - - t.Run("with limit", func(tt *testing.T) { - txService := newTransactionService(ctx) - - txs, err := txService.GetTransactionsByAccount(ctx, cursor, 5, accountId) - require.NoError(tt, err) - require.Len(tt, txs, 2) - require.Equal(tt, txs[0].LedgerHeader.LedgerSeq, xdr.Uint32(1586113)) - require.EqualValues(tt, txs[0].TxIndex, 2) - require.Equal(tt, txs[1].LedgerHeader.LedgerSeq, xdr.Uint32(1586114)) - require.EqualValues(tt, txs[1].TxIndex, 1) - }) -} - -func TestItGetsOperationsByAccount(t *testing.T) { - ctx := context.Background() - - // this is in the checkpoint range prior to the first active checkpoint - ledgerSeq := checkpointMgr.PrevCheckpoint(uint32(startLedgerSeq)) - cursor := toid.New(int32(ledgerSeq), 1, 1).ToInt64() - - t.Run("first", func(tt *testing.T) { - opsService := newOperationService(ctx) - - // this should start at next checkpoint - ops, err := opsService.GetOperationsByAccount(ctx, cursor, 1, accountId) - require.NoError(tt, err) - require.Len(tt, ops, 1) - require.Equal(tt, ops[0].LedgerHeader.LedgerSeq, xdr.Uint32(1586113)) - require.Equal(tt, ops[0].TxIndex, int32(2)) - - }) - - t.Run("with limit", func(tt *testing.T) { - opsService := newOperationService(ctx) - - // this should start at next checkpoint - ops, err := opsService.GetOperationsByAccount(ctx, cursor, 5, accountId) - require.NoError(tt, err) - require.Len(tt, ops, 2) - require.Equal(tt, ops[0].LedgerHeader.LedgerSeq, xdr.Uint32(1586113)) - require.Equal(tt, ops[0].TxIndex, int32(2)) - require.Equal(tt, ops[1].LedgerHeader.LedgerSeq, xdr.Uint32(1586114)) - require.Equal(tt, ops[1].TxIndex, int32(1)) - }) -} - -func mockArchiveAndIndex(ctx context.Context) (ingester.Ingester, index.Store) { - mockArchive := &ingester.MockIngester{} - mockReaderLedger1 := &ingester.MockLedgerTransactionReader{} - mockReaderLedger2 := &ingester.MockLedgerTransactionReader{} - mockReaderLedger3 := &ingester.MockLedgerTransactionReader{} - mockReaderLedgerTheRest := &ingester.MockLedgerTransactionReader{} - - expectedLedger1 := testLedger(startLedgerSeq) - expectedLedger2 := testLedger(startLedgerSeq + 1) - expectedLedger3 := testLedger(startLedgerSeq + 2) - - // throw an irrelevant account in there to make sure it's filtered - source := xdr.MustAddress("GCXKG6RN4ONIEPCMNFB732A436Z5PNDSRLGWK7GBLCMQLIFO4S7EYWVU") - source2 := xdr.MustAddress(accountId) - - // assert results iterate sequentially across ops-tx-ledgers - expectedLedger1Tx1 := testLedgerTx(source, 1, 34, 35) - expectedLedger1Tx2 := testLedgerTx(source, 2, 34) - expectedLedger2Tx1 := testLedgerTx(source, 1, 34) - expectedLedger2Tx2 := testLedgerTx(source2, 2, 34) - expectedLedger3Tx1 := testLedgerTx(source2, 1, 34) - expectedLedger3Tx2 := testLedgerTx(source, 2, 34) - - mockReaderLedger1. - On("Read").Return(expectedLedger1Tx1, nil).Once(). - On("Read").Return(expectedLedger1Tx2, nil).Once(). - On("Read").Return(ingester.LedgerTransaction{}, io.EOF).Once() - - mockReaderLedger2. - On("Read").Return(expectedLedger2Tx1, nil).Once(). - On("Read").Return(expectedLedger2Tx2, nil).Once(). - On("Read").Return(ingester.LedgerTransaction{}, io.EOF).Once() - - mockReaderLedger3. - On("Read").Return(expectedLedger3Tx1, nil).Once(). - On("Read").Return(expectedLedger3Tx2, nil).Once(). - On("Read").Return(ingester.LedgerTransaction{}, io.EOF).Once() - - mockReaderLedgerTheRest. - On("Read").Return(ingester.LedgerTransaction{}, io.EOF) - - mockArchive. - On("GetLedger", mock.Anything, uint32(1586112)).Return(expectedLedger1, nil). - On("GetLedger", mock.Anything, uint32(1586113)).Return(expectedLedger2, nil). - On("GetLedger", mock.Anything, uint32(1586114)).Return(expectedLedger3, nil). - On("GetLedger", mock.Anything, mock.AnythingOfType("uint32")). - Return(xdr.SerializedLedgerCloseMeta{}, nil) - - mockArchive. - On("NewLedgerTransactionReader", expectedLedger1).Return(mockReaderLedger1, nil).Once(). - On("NewLedgerTransactionReader", expectedLedger2).Return(mockReaderLedger2, nil).Once(). - On("NewLedgerTransactionReader", expectedLedger3).Return(mockReaderLedger3, nil).Once(). - On("NewLedgerTransactionReader", mock.AnythingOfType("xdr.SerializedLedgerCloseMeta")). - Return(mockReaderLedgerTheRest, nil). - On("PrepareRange", mock.Anything, mock.Anything).Return(nil) - - // should be 24784 - activeChk := uint32(index.GetCheckpointNumber(uint32(startLedgerSeq))) - mockStore := &index.MockStore{} - mockStore. - On("NextActive", accountId, mock.Anything, uint32(0)).Return(activeChk, nil). // start - On("NextActive", accountId, mock.Anything, activeChk-1).Return(activeChk, nil). // prev - On("NextActive", accountId, mock.Anything, activeChk).Return(activeChk, nil). // curr - On("NextActive", accountId, mock.Anything, activeChk+1).Return(uint32(0), io.EOF) // next - - return mockArchive, mockStore -} - -func testLedger(seq int) xdr.SerializedLedgerCloseMeta { - return xdr.SerializedLedgerCloseMeta{ - V: 0, - V0: &xdr.LedgerCloseMeta{ - V0: &xdr.LedgerCloseMetaV0{ - LedgerHeader: xdr.LedgerHeaderHistoryEntry{ - Header: xdr.LedgerHeader{ - LedgerSeq: xdr.Uint32(seq), - }, - }, - }, - }, - } -} - -func testLedgerTx(source xdr.AccountId, txIndex uint32, bumpTos ...int) ingester.LedgerTransaction { - code := xdr.TransactionResultCodeTxSuccess - - operations := []xdr.Operation{} - for _, bumpTo := range bumpTos { - operations = append(operations, xdr.Operation{ - Body: xdr.OperationBody{ - Type: xdr.OperationTypeBumpSequence, - BumpSequenceOp: &xdr.BumpSequenceOp{ - BumpTo: xdr.SequenceNumber(bumpTo), - }, - }, - }) - } - - return ingester.LedgerTransaction{ - LedgerTransaction: &ingest.LedgerTransaction{ - Result: xdr.TransactionResultPair{ - TransactionHash: xdr.Hash{}, - Result: xdr.TransactionResult{ - Result: xdr.TransactionResultResult{ - Code: code, - InnerResultPair: &xdr.InnerTransactionResultPair{}, - Results: &[]xdr.OperationResult{}, - }, - }, - }, - Envelope: xdr.TransactionEnvelope{ - Type: xdr.EnvelopeTypeEnvelopeTypeTx, - V1: &xdr.TransactionV1Envelope{ - Tx: xdr.Transaction{ - SourceAccount: source.ToMuxedAccount(), - Operations: operations, - }, - }, - }, - UnsafeMeta: xdr.TransactionMeta{ - V: 2, - V2: &xdr.TransactionMetaV2{ - Operations: make([]xdr.OperationMeta, len(bumpTos)), - }, - }, - Index: txIndex, - }, - } -} - -func newTransactionService(ctx context.Context) TransactionService { - ingest, store := mockArchiveAndIndex(ctx) - return &TransactionRepository{ - Config: Config{ - Ingester: ingest, - IndexStore: store, - Passphrase: passphrase, - Metrics: NewMetrics(prometheus.NewRegistry()), - }, - } -} - -func newOperationService(ctx context.Context) OperationService { - ingest, store := mockArchiveAndIndex(ctx) - return &OperationRepository{ - Config: Config{ - Ingester: ingest, - IndexStore: store, - Passphrase: passphrase, - Metrics: NewMetrics(prometheus.NewRegistry()), - }, - } -} diff --git a/exp/lighthorizon/services/mock_services.go b/exp/lighthorizon/services/mock_services.go deleted file mode 100644 index be573489e0..0000000000 --- a/exp/lighthorizon/services/mock_services.go +++ /dev/null @@ -1,32 +0,0 @@ -package services - -import ( - "context" - - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stretchr/testify/mock" -) - -type MockTransactionService struct { - mock.Mock -} - -func (m *MockTransactionService) GetTransactionsByAccount(ctx context.Context, - cursor int64, limit uint64, - accountId string, -) ([]common.Transaction, error) { - args := m.Called(ctx, cursor, limit, accountId) - return args.Get(0).([]common.Transaction), args.Error(1) -} - -type MockOperationService struct { - mock.Mock -} - -func (m *MockOperationService) GetOperationsByAccount(ctx context.Context, - cursor int64, limit uint64, - accountId string, -) ([]common.Operation, error) { - args := m.Called(ctx, cursor, limit, accountId) - return args.Get(0).([]common.Operation), args.Error(1) -} diff --git a/exp/lighthorizon/services/operations.go b/exp/lighthorizon/services/operations.go deleted file mode 100644 index 1236bcdb01..0000000000 --- a/exp/lighthorizon/services/operations.go +++ /dev/null @@ -1,90 +0,0 @@ -package services - -import ( - "context" - "strconv" - "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/exp/lighthorizon/ingester" - "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" -) - -type OperationService interface { - GetOperationsByAccount(ctx context.Context, - cursor int64, limit uint64, - accountId string, - ) ([]common.Operation, error) -} - -type OperationRepository struct { - OperationService - Config Config -} - -func (or *OperationRepository) GetOperationsByAccount(ctx context.Context, - cursor int64, limit uint64, - accountId string, -) ([]common.Operation, error) { - ops := []common.Operation{} - - opsCallback := func(tx ingester.LedgerTransaction, ledgerHeader *xdr.LedgerHeader) (bool, error) { - for operationOrder, op := range tx.Envelope.Operations() { - opParticipants, err := ingester.GetOperationParticipants(tx, op, operationOrder) - if err != nil { - return false, err - } - - if _, foundInOp := opParticipants[accountId]; foundInOp { - ops = append(ops, common.Operation{ - TransactionEnvelope: &tx.Envelope, - TransactionResult: &tx.Result.Result, - LedgerHeader: ledgerHeader, - TxIndex: int32(tx.Index), - OpIndex: int32(operationOrder), - }) - - if uint64(len(ops)) == limit { - return true, nil - } - } - } - - return false, nil - } - - err := searchAccountTransactions(ctx, cursor, accountId, or.Config, opsCallback) - if age := operationsResponseAgeSeconds(ops); age >= 0 { - or.Config.Metrics.ResponseAgeHistogram.With(prometheus.Labels{ - "request": "GetOperationsByAccount", - "successful": strconv.FormatBool(err == nil), - }).Observe(age) - } - - return ops, err -} - -func operationsResponseAgeSeconds(ops []common.Operation) float64 { - if len(ops) == 0 { - return -1 - } - - oldest := ops[0].LedgerHeader.ScpValue.CloseTime - for i := 1; i < len(ops); i++ { - if closeTime := ops[i].LedgerHeader.ScpValue.CloseTime; closeTime < oldest { - oldest = closeTime - } - } - - lastCloseTime := time.Unix(int64(oldest), 0).UTC() - now := time.Now().UTC() - if now.Before(lastCloseTime) { - log.Errorf("current time %v is before oldest operation close time %v", now, lastCloseTime) - return -1 - } - return now.Sub(lastCloseTime).Seconds() -} - -var _ OperationService = (*OperationRepository)(nil) // ensure conformity to the interface diff --git a/exp/lighthorizon/services/transactions.go b/exp/lighthorizon/services/transactions.go deleted file mode 100644 index 42d3964614..0000000000 --- a/exp/lighthorizon/services/transactions.go +++ /dev/null @@ -1,76 +0,0 @@ -package services - -import ( - "context" - "strconv" - "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/stellar/go/exp/lighthorizon/common" - "github.com/stellar/go/exp/lighthorizon/ingester" - "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" -) - -type TransactionRepository struct { - TransactionService - Config Config -} - -type TransactionService interface { - GetTransactionsByAccount(ctx context.Context, - cursor int64, limit uint64, - accountId string, - ) ([]common.Transaction, error) -} - -func (tr *TransactionRepository) GetTransactionsByAccount(ctx context.Context, - cursor int64, limit uint64, - accountId string, -) ([]common.Transaction, error) { - txs := []common.Transaction{} - - txsCallback := func(tx ingester.LedgerTransaction, ledgerHeader *xdr.LedgerHeader) (bool, error) { - txs = append(txs, common.Transaction{ - LedgerTransaction: &tx, - LedgerHeader: ledgerHeader, - TxIndex: int32(tx.Index), - NetworkPassphrase: tr.Config.Passphrase, - }) - - return uint64(len(txs)) == limit, nil - } - - err := searchAccountTransactions(ctx, cursor, accountId, tr.Config, txsCallback) - if age := transactionsResponseAgeSeconds(txs); age >= 0 { - tr.Config.Metrics.ResponseAgeHistogram.With(prometheus.Labels{ - "request": "GetTransactionsByAccount", - "successful": strconv.FormatBool(err == nil), - }).Observe(age) - } - - return txs, err -} - -func transactionsResponseAgeSeconds(txs []common.Transaction) float64 { - if len(txs) == 0 { - return -1 - } - - oldest := txs[0].LedgerHeader.ScpValue.CloseTime - for i := 1; i < len(txs); i++ { - if closeTime := txs[i].LedgerHeader.ScpValue.CloseTime; closeTime < oldest { - oldest = closeTime - } - } - - lastCloseTime := time.Unix(int64(oldest), 0).UTC() - now := time.Now().UTC() - if now.Before(lastCloseTime) { - log.Errorf("current time %v is before oldest transaction close time %v", now, lastCloseTime) - return -1 - } - return now.Sub(lastCloseTime).Seconds() -} - -var _ TransactionService = (*TransactionRepository)(nil) // ensure conformity to the interface diff --git a/exp/lighthorizon/tools/cache.go b/exp/lighthorizon/tools/cache.go deleted file mode 100644 index 0290fcb164..0000000000 --- a/exp/lighthorizon/tools/cache.go +++ /dev/null @@ -1,270 +0,0 @@ -package tools - -import ( - "context" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "github.com/spf13/cobra" - - "github.com/stellar/go/metaarchive" - "github.com/stellar/go/support/log" - "github.com/stellar/go/support/storage" -) - -const ( - defaultCacheCount = (60 * 60 * 24) / 5 // ~24hrs worth of ledgers -) - -func AddCacheCommands(parent *cobra.Command) *cobra.Command { - cmd := &cobra.Command{ - Use: "cache", - Long: "Manages the on-disk cache of ledgers.", - Example: ` -cache build --start 1234 --count 1000 s3://txmeta /tmp/example -cache purge /tmp/example 1234 1300 -cache show /tmp/example`, - RunE: func(cmd *cobra.Command, args []string) error { - // require a subcommand - this is just a "category" - return cmd.Help() - }, - } - - purge := &cobra.Command{ - Use: "purge [flags] path ", - Long: "Purges individual ledgers (or ranges) from the cache, or the entire cache.", - Example: ` -purge /tmp/example # empty the whole cache -purge /tmp/example 1000 # purge one ledger -purge /tmp/example 1000 1005 # purge a ledger range`, - RunE: func(cmd *cobra.Command, args []string) error { - // The first parameter must be a valid cache directory. - // You can then pass nothing, a single ledger, or a ledger range. - if len(args) < 1 || len(args) > 3 { - return cmd.Usage() - } - - var err error - var start, end uint64 - if len(args) > 1 { - start, err = strconv.ParseUint(args[1], 10, 32) - if err != nil { - cmd.Printf("Error: '%s' not a ledger sequence: %v\n", args[1], err) - return cmd.Usage() - } - } - end = start // fallback - - if len(args) == 3 { - end, err = strconv.ParseUint(args[2], 10, 32) - if err != nil { - cmd.Printf("Error: '%s' not a ledger sequence: %v\n", args[2], err) - return cmd.Usage() - } else if end < start { - cmd.Printf("Error: end precedes start (%d < %d)\n", end, start) - return cmd.Usage() - } - } - - path := args[0] - if start > 0 { - return PurgeLedgers(path, uint32(start), uint32(end)) - } - return PurgeCache(path) - }, - } - show := &cobra.Command{ - Use: "show ", - Long: "Traverses the on-disk cache and prints out cached ledger ranges.", - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - return cmd.Usage() - } - return ShowCache(args[0]) - }, - } - build := &cobra.Command{ - Use: "build [flags] ", - Example: "See cache --help text", - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 2 { - cmd.Println("Error: 2 positional arguments are required") - return cmd.Usage() - } - - start, err := cmd.Flags().GetUint32("start") - if err != nil || start < 2 { - cmd.Println("--start is required to be a ledger sequence") - return cmd.Usage() - } - - count, err := cmd.Flags().GetUint32("count") - if err != nil || count <= 0 { - cmd.Println("--count should be a positive 32-bit integer") - return cmd.Usage() - } - repair, _ := cmd.Flags().GetBool("repair") - return BuildCache(args[0], args[1], start, count, repair) - }, - } - - build.Flags().Bool("repair", false, "attempt to purge the cache and retry ledgers that error") - build.Flags().Uint32("start", 0, "first ledger to cache (required)") - build.Flags().Uint32("count", defaultCacheCount, "number of ledgers to cache") - - cmd.AddCommand(build, purge, show) - if parent == nil { - return cmd - } - - parent.AddCommand(cmd) - return parent -} - -func BuildCache(ledgerSource, cacheDir string, start uint32, count uint32, repair bool) error { - fullStart := time.Now() - L := log.DefaultLogger - L.SetLevel(log.InfoLevel) - log := L - - ctx := context.Background() - store, err := storage.ConnectBackend(ledgerSource, storage.ConnectOptions{ - Context: ctx, - Wrap: func(store storage.Storage) (storage.Storage, error) { - return storage.MakeOnDiskCache(store, cacheDir, uint(count)) - }, - }) - if err != nil { - log.Errorf("Couldn't create local cache for '%s' at '%s': %v", - ledgerSource, cacheDir, err) - return err - } - - log.Infof("Connected to ledger source at %s", ledgerSource) - log.Infof("Connected to ledger cache at %s", cacheDir) - - source := metaarchive.NewMetaArchive(store) - log.Infof("Filling local cache of ledgers at %s...", cacheDir) - log.Infof("Ledger range: [%d, %d] (%d ledgers)", - start, start+count-1, count) - - successful := uint(0) - for i := uint32(0); i < count; i++ { - ledgerSeq := start + uint32(i) - - // do "best effort" caching, skipping if too slow - dlCtx, dlCancel := context.WithTimeout(ctx, 10*time.Second) - start := time.Now() - - _, err := source.GetLedger(dlCtx, ledgerSeq) // this caches - dlCancel() - - if err != nil { - if repair && strings.Contains(err.Error(), "xdr") { - log.Warnf("Caching ledger %d failed, purging & retrying: %v", ledgerSeq, err) - store.(*storage.OnDiskCache).Evict(fmt.Sprintf("ledgers/%d", ledgerSeq)) - i-- // retry - } else { - log.Warnf("Caching ledger %d failed, skipping: %v", ledgerSeq, err) - log.Warn("If you see an XDR decoding error, the cache may be corrupted.") - log.Warnf("Run '%s purge %d' and try again, or pass --repair", - filepath.Base(os.Args[0]), ledgerSeq) - } - continue - } else { - successful++ - } - - duration := time.Since(start) - if duration > 2*time.Second { - log.WithField("duration", duration). - Warnf("Downloading ledger %d took a while.", ledgerSeq) - } - - log = log.WithField("failures", 1+uint(i)-successful) - if successful%97 == 0 { - log.Infof("Cached %d/%d ledgers (%0.1f%%)", successful, count, - 100*float64(successful)/float64(count)) - } - } - - duration := time.Since(fullStart) - log.WithField("duration", duration). - Infof("Cached %d ledgers into %s", successful, cacheDir) - - return nil -} - -func PurgeLedgers(cacheDir string, start, end uint32) error { - base := filepath.Join(cacheDir, "ledgers") - - successful := 0 - for i := start; i <= end; i++ { - ledgerPath := filepath.Join(base, strconv.FormatUint(uint64(i), 10)) - if err := os.Remove(ledgerPath); err != nil { - log.Warnf("Failed to remove cached ledger %d: %v", i, err) - continue - } - os.Remove(storage.NameLockfile(ledgerPath)) // ignore lockfile errors - log.Debugf("Purged ledger from %s", ledgerPath) - successful++ - } - - log.Infof("Purged %d cached ledgers from %s", successful, cacheDir) - return nil -} - -func PurgeCache(cacheDir string) error { - if err := os.RemoveAll(cacheDir); err != nil { - log.Warnf("Failed to remove cache directory (%s): %v", cacheDir, err) - return err - } - - log.Infof("Purged cache at %s", cacheDir) - return nil -} - -func ShowCache(cacheDir string) error { - files, err := ioutil.ReadDir(filepath.Join(cacheDir, "ledgers")) - if err != nil { - log.Errorf("Failed to read cache: %v", err) - return err - } - - ledgers := make([]uint32, 0, len(files)) - - for _, f := range files { - if f.IsDir() { - continue - } - - // If the name can be converted to a ledger sequence, track it. - if seq, errr := strconv.ParseUint(f.Name(), 10, 32); errr == nil { - ledgers = append(ledgers, uint32(seq)) - } - } - - log.Infof("Analyzed cache at %s: %d cached ledgers.", cacheDir, len(ledgers)) - if len(ledgers) == 0 { - return nil - } - - // Find consecutive ranges of ledgers in the cache - log.Infof("Cached ranges:") - firstSeq, lastSeq := ledgers[0], ledgers[0] - for i := 1; i < len(ledgers); i++ { - if ledgers[i]-1 != lastSeq { - log.Infof(" - [%d, %d]", firstSeq, lastSeq) - firstSeq = ledgers[i] - } - lastSeq = ledgers[i] - } - - log.Infof(" - [%d, %d]", firstSeq, lastSeq) - return nil -} diff --git a/exp/lighthorizon/tools/index.go b/exp/lighthorizon/tools/index.go deleted file mode 100644 index e37a7eb38a..0000000000 --- a/exp/lighthorizon/tools/index.go +++ /dev/null @@ -1,356 +0,0 @@ -package tools - -import ( - "context" - "io" - "os" - "os/signal" - "strconv" - "strings" - "syscall" - "time" - - "github.com/spf13/cobra" - - "github.com/stellar/go/exp/lighthorizon/index" - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/strkey" - "github.com/stellar/go/support/collections/maps" - "github.com/stellar/go/support/collections/set" - "github.com/stellar/go/support/log" - "github.com/stellar/go/support/ordered" -) - -var ( - checkpointMgr = historyarchive.NewCheckpointManager(0) -) - -func AddIndexCommands(parent *cobra.Command) *cobra.Command { - cmd := &cobra.Command{ - Use: "index", - Long: "Lets you view details about an index source and modify it.", - Example: ` -index view file:///tmp/indices -index view file:///tmp/indices GAGJZWQ5QT34VK3U6W6YKRYFIK6YSAXQC6BHIIYLG6X3CE5QW2KAYNJR -index stats file:///tmp/indices`, - RunE: func(cmd *cobra.Command, args []string) error { - // require a subcommand - this is just a "category" - return cmd.Help() - }, - } - - stats := &cobra.Command{ - Use: "stats ", - Long: "Summarize the statistics (like the # of active checkpoints " + - "or accounts). Note that this is a very read-heavy operation and " + - "will incur download bandwidth costs if reading from remote, " + - "billable sources.", - Example: `stats s3://indices`, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - return cmd.Usage() - } - - path := args[0] - start := time.Now() - log.Infof("Analyzing indices at %s", path) - - allCheckpoints := set.Set[uint32]{} - allIndexNames := set.Set[string]{} - accounts := showAccounts(path, 0) - log.Infof("Analyzing indices for %d accounts.", len(accounts)) - - // We want to summarize as much as possible on a Ctrl+C event, so - // this handles that by setting up a context that gets cancelled on - // SIGINT. A second Ctrl+C will kill the process as usual. - // - // https://millhouse.dev/posts/graceful-shutdowns-in-golang-with-signal-notify-context - ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGINT) - defer stop() - go func() { - <-ctx.Done() - stop() - log.WithField("error", ctx.Err()). - Warn("Received interrupt, shutting down gracefully & summarizing findings...") - log.Warn("Press Ctrl+C again to abort.") - }() - - mostActiveAccountChk := 0 - mostActiveAccount := "" - for _, account := range accounts { - if ctx.Err() != nil { - break - } - - activity := getIndex(path, account, "", 0) - allCheckpoints.AddSlice(maps.Keys(activity)) - for _, names := range activity { - allIndexNames.AddSlice(names) - } - - if len(activity) > mostActiveAccountChk { - mostActiveAccount = account - mostActiveAccountChk = len(activity) - } - } - - ledgerCount := len(allCheckpoints) * int(checkpointMgr.GetCheckpointFrequency()) - - log.Info("Done analyzing indices, summarizing...") - log.Infof("") - log.Infof("=== Final Summary ===") - log.Infof("Analysis took %s.", time.Since(start)) - log.Infof("Path: %s", path) - log.Infof("Accounts: %d", len(accounts)) - log.Infof("Smallest checkpoint: %d", ordered.MinSlice(allCheckpoints.Slice())) - log.Infof("Largest checkpoint: %d", ordered.MaxSlice(allCheckpoints.Slice())) - log.Infof("Checkpoint count: %d (%d possible ledgers, ~%0.2f days)", - len(allCheckpoints), ledgerCount, - float64(ledgerCount)/(float64(60*60*24)/6.0) /* approx. ledgers per day */) - log.Infof("Index names: %s", strings.Join(allIndexNames.Slice(), ", ")) - log.Infof("Most active account: %s (%d checkpoints)", - mostActiveAccount, mostActiveAccountChk) - - return nil - }, - } - - view := &cobra.Command{ - Use: "view [accounts?]", - Long: "View the accounts in an index source or view the " + - "checkpoints specific account(s) are active in.", - Example: `view s3://indices -view s3:///indices GAXLQGKIUAIIUHAX4GJO3J7HFGLBCNF6ZCZSTLJE7EKO5IUHGLQLMXZO -view file:///tmp/indices --limit=0 GAXLQGKIUAIIUHAX4GJO3J7HFGLBCNF6ZCZSTLJE7EKO5IUHGLQLMXZO -view gcs://indices --limit=10 GAXLQGKIUAIIUHAX4GJO3J7HFGLBCNF6ZCZSTLJE7EKO5IUHGLQLMXZO,GBUUWQDVEEXBJCUF5UL24YGXKJIP5EMM7KFWIAR33KQRJR34GN6HEDPV,GBYETUYNBK2ZO5MSYBJKSLDEA2ZHIXLCFL3MMWU6RHFVAUBKEWQORYKS`, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) < 1 || len(args) > 2 { - return cmd.Usage() - } - - path := args[0] - log.Infof("Analyzing indices at %s", path) - - accounts := []string{} - if len(args) == 2 { - accounts = strings.Split(args[1], ",") - } - - limit, err := cmd.Flags().GetUint("limit") - if err != nil { - return cmd.Usage() - } - - if len(accounts) > 0 { - indexName, err := cmd.Flags().GetString("index-name") - if err != nil { - return cmd.Usage() - } - - for _, account := range accounts { - if !strkey.IsValidEd25519PublicKey(account) && - !strkey.IsValidMuxedAccountEd25519PublicKey(account) { - log.Errorf("Invalid account ID: '%s'", account) - continue - } - - getIndex(path, account, indexName, limit) - } - } else { - showAccounts(path, limit) - } - - return nil - }, - } - - purge := &cobra.Command{ - Use: "purge ", - Long: "Purges all indices for the given ledger range.", - Example: `purge s3://indices 10000 10005`, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 3 { - return cmd.Usage() - } - - path := args[0] - start, err := strconv.ParseUint(args[1], 10, 32) - if err != nil { - return cmd.Usage() - } - end, err := strconv.ParseUint(args[2], 10, 32) - if err != nil { - return cmd.Usage() - } - - r := historyarchive.Range{Low: uint32(start), High: uint32(end)} - log.Infof("Purging all indices from %s for ledger range: [%d, %d].", - path, r.Low, r.High) - - return purgeIndex(path, r) - }, - } - - view.Flags().Uint("limit", 10, "a maximum number of accounts or checkpoints to show") - view.Flags().String("index-name", "", "filter for a particular index") - cmd.AddCommand(stats, view, purge) - - if parent == nil { - return cmd - } - parent.AddCommand(cmd) - return parent -} - -func getIndex(path, account, indexName string, limit uint) map[uint32][]string { - freq := checkpointMgr.GetCheckpointFrequency() - - store, err := index.Connect(path) - if err != nil { - log.Fatalf("Failed to connect to index store at %s: %v", path, err) - return nil - } - - indices, err := store.Read(account) - if err != nil { - log.Fatalf("Failed to read indices for %s from index store at %s: %v", - account, path, err) - return nil - } - - // It's better to summarize activity and then group it by index rather than - // just show activity in each index, because there's likely a ton of overlap - // across indices. - activity := map[uint32][]string{} - indexNames := []string{} - - for name, idx := range indices { - log.Infof("Index found: '%s'", name) - if indexName != "" && name != indexName { - continue - } - - indexNames = append(indexNames, name) - - checkpoint, err := idx.NextActiveBit(0) - for err != io.EOF { - activity[checkpoint] = append(activity[checkpoint], name) - checkpoint, err = idx.NextActiveBit(checkpoint + 1) - - if limit > 0 && limit <= uint(len(activity)) { - break - } - } - } - - log.WithField("account", account).WithField("limit", limit). - Infof("Activity for account:") - - for checkpoint, names := range activity { - first := (checkpoint - 1) * freq - last := first + freq - - nameStr := strings.Join(names, ", ") - log.WithField("indices", nameStr). - Infof(" - checkpoint %d, ledgers [%d, %d)", checkpoint, first, last) - } - - log.Infof("Summary: %d active checkpoints, %d possible active ledgers", - len(activity), len(activity)*int(freq)) - log.Infof("Checkpoint range: [%d, %d]", - ordered.MinSlice(maps.Keys(activity)), - ordered.MaxSlice(maps.Keys(activity))) - log.Infof("All discovered indices: %s", strings.Join(indexNames, ", ")) - - return activity -} - -func showAccounts(path string, limit uint) []string { - store, err := index.Connect(path) - if err != nil { - log.Fatalf("Failed to connect to index store at %s: %v", path, err) - return nil - } - - accounts, err := store.ReadAccounts() - if err != nil { - log.Fatalf("Failed read accounts from index store at %s: %v", path, err) - return nil - } - - if limit == 0 { - limit = uint(len(accounts)) - } - - for i := uint(0); i < limit; i++ { - log.Info(accounts[i]) - } - - return accounts -} - -func purgeIndex(path string, r historyarchive.Range) error { - freq := historyarchive.DefaultCheckpointFrequency - store, err := index.Connect(path) - if err != nil { - log.Fatalf("Failed to connect to index store at %s: %v", path, err) - return err - } - - accounts, err := store.ReadAccounts() - if err != nil { - log.Fatalf("Failed read accounts: %v", err) - return err - } - - purged := 0 - for _, account := range accounts { - L := log.WithField("account", account) - - indices, err := store.Read(account) - if err != nil { - L.Errorf("Failed to read indices: %v", err) - continue - } - - for name, index := range indices { - var err error - active := uint32(0) - for err == nil { - if active*freq < r.Low { // too low, skip ahead - active, err = index.NextActiveBit(active + 1) - continue - } else if active*freq > r.High { // too high, we're done - break - } - - L.WithField("index", name). - Debugf("Purged checkpoint %d (ledgers %d through %d).", - active, active*freq, (active+1)*freq-1) - - purged++ - - index.SetInactive(active) - active, err = index.NextActiveBit(active) - } - - if err != nil && err != io.EOF { - L.WithField("index", name). - Errorf("Iterating over index failed: %v", err) - continue - } - - } - - store.AddParticipantToIndexesNoBackend(account, indices) - if err := store.Flush(); err != nil { - log.WithField("account", account). - Errorf("Flushing index failed: %v", err) - continue - } - } - - log.Infof("Purged %d values across %d accounts from all indices at %s.", - purged, len(accounts), path) - return nil -} diff --git a/exp/lighthorizon/tools/index_test.go b/exp/lighthorizon/tools/index_test.go deleted file mode 100644 index 6d42f88f30..0000000000 --- a/exp/lighthorizon/tools/index_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package tools - -import ( - "path/filepath" - "testing" - - "github.com/stellar/go/exp/lighthorizon/index" - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/keypair" - "github.com/stellar/go/support/log" - "github.com/stretchr/testify/require" -) - -const ( - freq = historyarchive.DefaultCheckpointFrequency -) - -func TestIndexPurge(t *testing.T) { - log.SetLevel(log.DebugLevel) - - tempFile := "file://" + filepath.Join(t.TempDir(), "index-store") - accounts := []string{keypair.MustRandom().Address()} - - idx, err := index.Connect(tempFile) - require.NoError(t, err) - - for _, chk := range []uint32{14, 15, 16, 17, 20, 25, 123} { - require.NoError(t, idx.AddParticipantsToIndexes(chk, "test", accounts)) - } - - idx.Flush() // saves to disk - - // Try purging the index - err = purgeIndex(tempFile, historyarchive.Range{Low: 15 * freq, High: 22 * freq}) - require.NoError(t, err) - - // Check to make sure it worked. - idx, err = index.Connect(tempFile) - require.NoError(t, err) - - // Ensure that the index is in the expected state. - indices, err := idx.Read(accounts[0]) - require.NoError(t, err) - require.Contains(t, indices, "test") - - index := indices["test"] - i, err := index.NextActiveBit(0) - require.NoError(t, err) - require.EqualValues(t, 14, i) - - i, err = index.NextActiveBit(15) - require.NoError(t, err) - require.EqualValues(t, 25, i) - - i, err = index.NextActiveBit(i + 1) - require.NoError(t, err) - require.EqualValues(t, 123, i) -} diff --git a/go.mod b/go.mod index b1edd86933..9e1caca7aa 100644 --- a/go.mod +++ b/go.mod @@ -61,7 +61,6 @@ require ( require ( github.com/cenkalti/backoff/v4 v4.2.1 github.com/fsouza/fake-gcs-server v1.49.0 - golang.org/x/sync v0.7.0 ) require ( @@ -104,6 +103,7 @@ require ( go.opentelemetry.io/otel/trace v1.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.13.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/tools v0.14.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect diff --git a/gxdr/xdr_generated.go b/gxdr/xdr_generated.go index 7265cb5a71..0b41834ce9 100644 --- a/gxdr/xdr_generated.go +++ b/gxdr/xdr_generated.go @@ -1,4 +1,4 @@ -// Code generated by goxdr -p gxdr -enum-comments -o gxdr/xdr_generated.go xdr/Stellar-SCP.x xdr/Stellar-ledger-entries.x xdr/Stellar-ledger.x xdr/Stellar-overlay.x xdr/Stellar-transaction.x xdr/Stellar-types.x xdr/Stellar-contract-env-meta.x xdr/Stellar-contract-meta.x xdr/Stellar-contract-spec.x xdr/Stellar-contract.x xdr/Stellar-internal.x xdr/Stellar-contract-config-setting.x xdr/Stellar-lighthorizon.x xdr/Stellar-exporter.x; DO NOT EDIT. +// Code generated by goxdr -p gxdr -enum-comments -o gxdr/xdr_generated.go xdr/Stellar-SCP.x xdr/Stellar-ledger-entries.x xdr/Stellar-ledger.x xdr/Stellar-overlay.x xdr/Stellar-transaction.x xdr/Stellar-types.x xdr/Stellar-contract-env-meta.x xdr/Stellar-contract-meta.x xdr/Stellar-contract-spec.x xdr/Stellar-contract.x xdr/Stellar-internal.x xdr/Stellar-contract-config-setting.x xdr/Stellar-exporter.x; DO NOT EDIT. package gxdr @@ -4622,37 +4622,6 @@ type ConfigSettingEntry struct { _u interface{} } -type BitmapIndex struct { - FirstBit Uint32 - LastBit Uint32 - Bitmap Value -} - -type TrieIndex struct { - // goxdr gives an error if we simply use "version" as an identifier - Version_ Uint32 - Root TrieNode -} - -type TrieNodeChild struct { - Key [1]byte - Node TrieNode -} - -type TrieNode struct { - Prefix Value - Value Value - Children []TrieNodeChild -} - -type SerializedLedgerCloseMeta struct { - // The union discriminant V selects among the following arms: - // 0: - // V0() *LedgerCloseMeta - V int32 - _u interface{} -} - // Batch of ledgers along with their transaction metadata type LedgerCloseMetaBatch struct { // starting ledger sequence number in the batch @@ -30202,211 +30171,6 @@ func (u *ConfigSettingEntry) XdrRecurse(x XDR, name string) { } func XDR_ConfigSettingEntry(v *ConfigSettingEntry) *ConfigSettingEntry { return v } -type XdrType_BitmapIndex = *BitmapIndex - -func (v *BitmapIndex) XdrPointer() interface{} { return v } -func (BitmapIndex) XdrTypeName() string { return "BitmapIndex" } -func (v BitmapIndex) XdrValue() interface{} { return v } -func (v *BitmapIndex) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -func (v *BitmapIndex) XdrRecurse(x XDR, name string) { - if name != "" { - name = x.Sprintf("%s.", name) - } - x.Marshal(x.Sprintf("%sfirstBit", name), XDR_Uint32(&v.FirstBit)) - x.Marshal(x.Sprintf("%slastBit", name), XDR_Uint32(&v.LastBit)) - x.Marshal(x.Sprintf("%sbitmap", name), XDR_Value(&v.Bitmap)) -} -func XDR_BitmapIndex(v *BitmapIndex) *BitmapIndex { return v } - -type XdrType_TrieIndex = *TrieIndex - -func (v *TrieIndex) XdrPointer() interface{} { return v } -func (TrieIndex) XdrTypeName() string { return "TrieIndex" } -func (v TrieIndex) XdrValue() interface{} { return v } -func (v *TrieIndex) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -func (v *TrieIndex) XdrRecurse(x XDR, name string) { - if name != "" { - name = x.Sprintf("%s.", name) - } - x.Marshal(x.Sprintf("%sversion_", name), XDR_Uint32(&v.Version_)) - x.Marshal(x.Sprintf("%sroot", name), XDR_TrieNode(&v.Root)) -} -func XDR_TrieIndex(v *TrieIndex) *TrieIndex { return v } - -type _XdrArray_1_opaque [1]byte - -func (v *_XdrArray_1_opaque) GetByteSlice() []byte { return v[:] } -func (v *_XdrArray_1_opaque) XdrTypeName() string { return "opaque[]" } -func (v *_XdrArray_1_opaque) XdrValue() interface{} { return v[:] } -func (v *_XdrArray_1_opaque) XdrPointer() interface{} { return (*[1]byte)(v) } -func (v *_XdrArray_1_opaque) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -func (v *_XdrArray_1_opaque) String() string { return fmt.Sprintf("%x", v[:]) } -func (v *_XdrArray_1_opaque) Scan(ss fmt.ScanState, c rune) error { - return XdrArrayOpaqueScan(v[:], ss, c) -} -func (_XdrArray_1_opaque) XdrArraySize() uint32 { - const bound uint32 = 1 // Force error if not const or doesn't fit - return bound -} - -type XdrType_TrieNodeChild = *TrieNodeChild - -func (v *TrieNodeChild) XdrPointer() interface{} { return v } -func (TrieNodeChild) XdrTypeName() string { return "TrieNodeChild" } -func (v TrieNodeChild) XdrValue() interface{} { return v } -func (v *TrieNodeChild) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -func (v *TrieNodeChild) XdrRecurse(x XDR, name string) { - if name != "" { - name = x.Sprintf("%s.", name) - } - x.Marshal(x.Sprintf("%skey", name), (*_XdrArray_1_opaque)(&v.Key)) - x.Marshal(x.Sprintf("%snode", name), XDR_TrieNode(&v.Node)) -} -func XDR_TrieNodeChild(v *TrieNodeChild) *TrieNodeChild { return v } - -type _XdrVec_unbounded_TrieNodeChild []TrieNodeChild - -func (_XdrVec_unbounded_TrieNodeChild) XdrBound() uint32 { - const bound uint32 = 4294967295 // Force error if not const or doesn't fit - return bound -} -func (_XdrVec_unbounded_TrieNodeChild) XdrCheckLen(length uint32) { - if length > uint32(4294967295) { - XdrPanic("_XdrVec_unbounded_TrieNodeChild length %d exceeds bound 4294967295", length) - } else if int(length) < 0 { - XdrPanic("_XdrVec_unbounded_TrieNodeChild length %d exceeds max int", length) - } -} -func (v _XdrVec_unbounded_TrieNodeChild) GetVecLen() uint32 { return uint32(len(v)) } -func (v *_XdrVec_unbounded_TrieNodeChild) SetVecLen(length uint32) { - v.XdrCheckLen(length) - if int(length) <= cap(*v) { - if int(length) != len(*v) { - *v = (*v)[:int(length)] - } - return - } - newcap := 2 * cap(*v) - if newcap < int(length) { // also catches overflow where 2*cap < 0 - newcap = int(length) - } else if bound := uint(4294967295); uint(newcap) > bound { - if int(bound) < 0 { - bound = ^uint(0) >> 1 - } - newcap = int(bound) - } - nv := make([]TrieNodeChild, int(length), newcap) - copy(nv, *v) - *v = nv -} -func (v *_XdrVec_unbounded_TrieNodeChild) XdrMarshalN(x XDR, name string, n uint32) { - v.XdrCheckLen(n) - for i := 0; i < int(n); i++ { - if i >= len(*v) { - v.SetVecLen(uint32(i + 1)) - } - XDR_TrieNodeChild(&(*v)[i]).XdrMarshal(x, x.Sprintf("%s[%d]", name, i)) - } - if int(n) < len(*v) { - *v = (*v)[:int(n)] - } -} -func (v *_XdrVec_unbounded_TrieNodeChild) XdrRecurse(x XDR, name string) { - size := XdrSize{Size: uint32(len(*v)), Bound: 4294967295} - x.Marshal(name, &size) - v.XdrMarshalN(x, name, size.Size) -} -func (_XdrVec_unbounded_TrieNodeChild) XdrTypeName() string { return "TrieNodeChild<>" } -func (v *_XdrVec_unbounded_TrieNodeChild) XdrPointer() interface{} { return (*[]TrieNodeChild)(v) } -func (v _XdrVec_unbounded_TrieNodeChild) XdrValue() interface{} { return ([]TrieNodeChild)(v) } -func (v *_XdrVec_unbounded_TrieNodeChild) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } - -type XdrType_TrieNode = *TrieNode - -func (v *TrieNode) XdrPointer() interface{} { return v } -func (TrieNode) XdrTypeName() string { return "TrieNode" } -func (v TrieNode) XdrValue() interface{} { return v } -func (v *TrieNode) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -func (v *TrieNode) XdrRecurse(x XDR, name string) { - if name != "" { - name = x.Sprintf("%s.", name) - } - x.Marshal(x.Sprintf("%sprefix", name), XDR_Value(&v.Prefix)) - x.Marshal(x.Sprintf("%svalue", name), XDR_Value(&v.Value)) - x.Marshal(x.Sprintf("%schildren", name), (*_XdrVec_unbounded_TrieNodeChild)(&v.Children)) -} -func XDR_TrieNode(v *TrieNode) *TrieNode { return v } - -var _XdrTags_SerializedLedgerCloseMeta = map[int32]bool{ - XdrToI32(0): true, -} - -func (_ SerializedLedgerCloseMeta) XdrValidTags() map[int32]bool { - return _XdrTags_SerializedLedgerCloseMeta -} -func (u *SerializedLedgerCloseMeta) V0() *LedgerCloseMeta { - switch u.V { - case 0: - if v, ok := u._u.(*LedgerCloseMeta); ok { - return v - } else { - var zero LedgerCloseMeta - u._u = &zero - return &zero - } - default: - XdrPanic("SerializedLedgerCloseMeta.V0 accessed when V == %v", u.V) - return nil - } -} -func (u SerializedLedgerCloseMeta) XdrValid() bool { - switch u.V { - case 0: - return true - } - return false -} -func (u *SerializedLedgerCloseMeta) XdrUnionTag() XdrNum32 { - return XDR_int32(&u.V) -} -func (u *SerializedLedgerCloseMeta) XdrUnionTagName() string { - return "V" -} -func (u *SerializedLedgerCloseMeta) XdrUnionBody() XdrType { - switch u.V { - case 0: - return XDR_LedgerCloseMeta(u.V0()) - } - return nil -} -func (u *SerializedLedgerCloseMeta) XdrUnionBodyName() string { - switch u.V { - case 0: - return "V0" - } - return "" -} - -type XdrType_SerializedLedgerCloseMeta = *SerializedLedgerCloseMeta - -func (v *SerializedLedgerCloseMeta) XdrPointer() interface{} { return v } -func (SerializedLedgerCloseMeta) XdrTypeName() string { return "SerializedLedgerCloseMeta" } -func (v SerializedLedgerCloseMeta) XdrValue() interface{} { return v } -func (v *SerializedLedgerCloseMeta) XdrMarshal(x XDR, name string) { x.Marshal(name, v) } -func (u *SerializedLedgerCloseMeta) XdrRecurse(x XDR, name string) { - if name != "" { - name = x.Sprintf("%s.", name) - } - XDR_int32(&u.V).XdrMarshal(x, x.Sprintf("%sv", name)) - switch u.V { - case 0: - x.Marshal(x.Sprintf("%sv0", name), XDR_LedgerCloseMeta(u.V0())) - return - } - XdrPanic("invalid V (%v) in SerializedLedgerCloseMeta", u.V) -} -func XDR_SerializedLedgerCloseMeta(v *SerializedLedgerCloseMeta) *SerializedLedgerCloseMeta { return v } - type _XdrVec_unbounded_LedgerCloseMeta []LedgerCloseMeta func (_XdrVec_unbounded_LedgerCloseMeta) XdrBound() uint32 { diff --git a/ingest/ledgerbackend/history_archive_backend.go b/ingest/ledgerbackend/history_archive_backend.go deleted file mode 100644 index 331f43032d..0000000000 --- a/ingest/ledgerbackend/history_archive_backend.go +++ /dev/null @@ -1,51 +0,0 @@ -package ledgerbackend - -import ( - "context" - "fmt" - - "github.com/stellar/go/metaarchive" - "github.com/stellar/go/xdr" -) - -type HistoryArchiveBackend struct { - metaArchive metaarchive.MetaArchive -} - -func NewHistoryArchiveBackend(metaArchive metaarchive.MetaArchive) *HistoryArchiveBackend { - return &HistoryArchiveBackend{ - metaArchive: metaArchive, - } -} - -func (b *HistoryArchiveBackend) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - return b.metaArchive.GetLatestLedgerSequence(ctx) -} - -func (b *HistoryArchiveBackend) PrepareRange(ctx context.Context, ledgerRange Range) error { - // Noop - return nil -} - -func (b *HistoryArchiveBackend) IsPrepared(ctx context.Context, ledgerRange Range) (bool, error) { - // Noop - return true, nil -} - -func (b *HistoryArchiveBackend) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, error) { - serializedLedger, err := b.metaArchive.GetLedger(ctx, sequence) - if err != nil { - return xdr.LedgerCloseMeta{}, err - } - - output, isV0 := serializedLedger.GetV0() - if !isV0 { - return xdr.LedgerCloseMeta{}, fmt.Errorf("unexpected serialized ledger version number (0x%x)", serializedLedger.V) - } - return output, nil -} - -func (b *HistoryArchiveBackend) Close() error { - // Noop - return nil -} diff --git a/metaarchive/main.go b/metaarchive/main.go deleted file mode 100644 index 7d06a46f9a..0000000000 --- a/metaarchive/main.go +++ /dev/null @@ -1,62 +0,0 @@ -package metaarchive - -import ( - "bytes" - "context" - "io" - "os" - "strconv" - - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/storage" - "github.com/stellar/go/xdr" -) - -type MetaArchive interface { - GetLatestLedgerSequence(ctx context.Context) (uint32, error) - GetLedger(ctx context.Context, sequence uint32) (xdr.SerializedLedgerCloseMeta, error) -} - -type metaArchive struct { - s storage.Storage -} - -func NewMetaArchive(b storage.Storage) MetaArchive { - return &metaArchive{s: b} -} - -func (m *metaArchive) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - r, err := m.s.GetFile("latest") - if os.IsNotExist(err) { - return 2, nil - } else if err != nil { - return 0, errors.Wrap(err, "could not open latest ledger bucket") - } - defer r.Close() - var buf bytes.Buffer - if _, err = io.Copy(&buf, r); err != nil { - return 0, errors.Wrap(err, "could not read latest ledger") - } - parsed, err := strconv.ParseUint(buf.String(), 10, 32) - if err != nil { - return 0, errors.Wrapf(err, "could not parse latest ledger: %q", buf.String()) - } - return uint32(parsed), nil -} - -func (m *metaArchive) GetLedger(ctx context.Context, sequence uint32) (xdr.SerializedLedgerCloseMeta, error) { - var ledger xdr.SerializedLedgerCloseMeta - r, err := m.s.GetFile("ledgers/" + strconv.FormatUint(uint64(sequence), 10)) - if err != nil { - return xdr.SerializedLedgerCloseMeta{}, err - } - defer r.Close() - var buf bytes.Buffer - if _, err = io.Copy(&buf, r); err != nil { - return xdr.SerializedLedgerCloseMeta{}, err - } - if err = ledger.UnmarshalBinary(buf.Bytes()); err != nil { - return xdr.SerializedLedgerCloseMeta{}, err - } - return ledger, nil -} diff --git a/xdr/Stellar-lighthorizon.x b/xdr/Stellar-lighthorizon.x deleted file mode 100644 index 8955871cd1..0000000000 --- a/xdr/Stellar-lighthorizon.x +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2022 Stellar Development Foundation and contributors. Licensed -// under the Apache License, Version 2.0. See the COPYING file at the root -// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 - -%#include "xdr/Stellar-ledger.h" -%#include "xdr/Stellar-types.h" - -namespace stellar -{ - -struct BitmapIndex { - uint32 firstBit; - uint32 lastBit; - Value bitmap; -}; - -struct TrieIndex { - uint32 version_; // goxdr gives an error if we simply use "version" as an identifier - TrieNode root; -}; - -struct TrieNodeChild { - opaque key[1]; - TrieNode node; -}; - -struct TrieNode { - Value prefix; - Value value; - TrieNodeChild children<>; -}; - -union SerializedLedgerCloseMeta switch (int v) -{ -case 0: - LedgerCloseMeta v0; -}; - -} diff --git a/xdr/xdr_generated.go b/xdr/xdr_generated.go index c47ab760c9..fb7fb7471e 100644 --- a/xdr/xdr_generated.go +++ b/xdr/xdr_generated.go @@ -13,7 +13,6 @@ // xdr/Stellar-internal.x // xdr/Stellar-ledger-entries.x // xdr/Stellar-ledger.x -// xdr/Stellar-lighthorizon.x // xdr/Stellar-overlay.x // xdr/Stellar-transaction.x // xdr/Stellar-types.x @@ -43,7 +42,6 @@ var XdrFilesSHA256 = map[string]string{ "xdr/Stellar-internal.x": "227835866c1b2122d1eaf28839ba85ea7289d1cb681dda4ca619c2da3d71fe00", "xdr/Stellar-ledger-entries.x": "77dc7062ae6d0812136333e12e35b2294d7c2896a536be9c811eb0ed2abbbccb", "xdr/Stellar-ledger.x": "46c1c55972750b97650ff00788a2be4764975b787ef51c8fa931c56e2028a3c4", - "xdr/Stellar-lighthorizon.x": "1aac09eaeda224154f653a0c95f02167be0c110fc295bb41b756a080eb8c06df", "xdr/Stellar-overlay.x": "8c73b7c3ad974e7fc4aa4fdf34f7ad50053406254efbd7406c96657cf41691d3", "xdr/Stellar-transaction.x": "0d2b35a331a540b48643925d0869857236eb2487c02d340ea32e365e784ea2b8", "xdr/Stellar-types.x": "6e3b13f0d3e360b09fa5e2b0e55d43f4d974a769df66afb34e8aecbb329d3f15", @@ -58962,482 +58960,6 @@ func (s ConfigSettingEntry) xdrType() {} var _ xdrType = (*ConfigSettingEntry)(nil) -// BitmapIndex is an XDR Struct defines as: -// -// struct BitmapIndex { -// uint32 firstBit; -// uint32 lastBit; -// Value bitmap; -// }; -type BitmapIndex struct { - FirstBit Uint32 - LastBit Uint32 - Bitmap Value -} - -// EncodeTo encodes this value using the Encoder. -func (s *BitmapIndex) EncodeTo(e *xdr.Encoder) error { - var err error - if err = s.FirstBit.EncodeTo(e); err != nil { - return err - } - if err = s.LastBit.EncodeTo(e); err != nil { - return err - } - if err = s.Bitmap.EncodeTo(e); err != nil { - return err - } - return nil -} - -var _ decoderFrom = (*BitmapIndex)(nil) - -// DecodeFrom decodes this value using the Decoder. -func (s *BitmapIndex) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { - if maxDepth == 0 { - return 0, fmt.Errorf("decoding BitmapIndex: %w", ErrMaxDecodingDepthReached) - } - maxDepth -= 1 - var err error - var n, nTmp int - nTmp, err = s.FirstBit.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding Uint32: %w", err) - } - nTmp, err = s.LastBit.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding Uint32: %w", err) - } - nTmp, err = s.Bitmap.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding Value: %w", err) - } - return n, nil -} - -// MarshalBinary implements encoding.BinaryMarshaler. -func (s BitmapIndex) MarshalBinary() ([]byte, error) { - b := bytes.Buffer{} - e := xdr.NewEncoder(&b) - err := s.EncodeTo(e) - return b.Bytes(), err -} - -// UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *BitmapIndex) UnmarshalBinary(inp []byte) error { - r := bytes.NewReader(inp) - o := xdr.DefaultDecodeOptions - o.MaxInputLen = len(inp) - d := xdr.NewDecoderWithOptions(r, o) - _, err := s.DecodeFrom(d, o.MaxDepth) - return err -} - -var ( - _ encoding.BinaryMarshaler = (*BitmapIndex)(nil) - _ encoding.BinaryUnmarshaler = (*BitmapIndex)(nil) -) - -// xdrType signals that this type represents XDR values defined by this package. -func (s BitmapIndex) xdrType() {} - -var _ xdrType = (*BitmapIndex)(nil) - -// TrieIndex is an XDR Struct defines as: -// -// struct TrieIndex { -// uint32 version_; // goxdr gives an error if we simply use "version" as an identifier -// TrieNode root; -// }; -type TrieIndex struct { - Version Uint32 - Root TrieNode -} - -// EncodeTo encodes this value using the Encoder. -func (s *TrieIndex) EncodeTo(e *xdr.Encoder) error { - var err error - if err = s.Version.EncodeTo(e); err != nil { - return err - } - if err = s.Root.EncodeTo(e); err != nil { - return err - } - return nil -} - -var _ decoderFrom = (*TrieIndex)(nil) - -// DecodeFrom decodes this value using the Decoder. -func (s *TrieIndex) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { - if maxDepth == 0 { - return 0, fmt.Errorf("decoding TrieIndex: %w", ErrMaxDecodingDepthReached) - } - maxDepth -= 1 - var err error - var n, nTmp int - nTmp, err = s.Version.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding Uint32: %w", err) - } - nTmp, err = s.Root.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding TrieNode: %w", err) - } - return n, nil -} - -// MarshalBinary implements encoding.BinaryMarshaler. -func (s TrieIndex) MarshalBinary() ([]byte, error) { - b := bytes.Buffer{} - e := xdr.NewEncoder(&b) - err := s.EncodeTo(e) - return b.Bytes(), err -} - -// UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *TrieIndex) UnmarshalBinary(inp []byte) error { - r := bytes.NewReader(inp) - o := xdr.DefaultDecodeOptions - o.MaxInputLen = len(inp) - d := xdr.NewDecoderWithOptions(r, o) - _, err := s.DecodeFrom(d, o.MaxDepth) - return err -} - -var ( - _ encoding.BinaryMarshaler = (*TrieIndex)(nil) - _ encoding.BinaryUnmarshaler = (*TrieIndex)(nil) -) - -// xdrType signals that this type represents XDR values defined by this package. -func (s TrieIndex) xdrType() {} - -var _ xdrType = (*TrieIndex)(nil) - -// TrieNodeChild is an XDR Struct defines as: -// -// struct TrieNodeChild { -// opaque key[1]; -// TrieNode node; -// }; -type TrieNodeChild struct { - Key [1]byte `xdrmaxsize:"1"` - Node TrieNode -} - -// EncodeTo encodes this value using the Encoder. -func (s *TrieNodeChild) EncodeTo(e *xdr.Encoder) error { - var err error - if _, err = e.EncodeFixedOpaque(s.Key[:]); err != nil { - return err - } - if err = s.Node.EncodeTo(e); err != nil { - return err - } - return nil -} - -var _ decoderFrom = (*TrieNodeChild)(nil) - -// DecodeFrom decodes this value using the Decoder. -func (s *TrieNodeChild) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { - if maxDepth == 0 { - return 0, fmt.Errorf("decoding TrieNodeChild: %w", ErrMaxDecodingDepthReached) - } - maxDepth -= 1 - var err error - var n, nTmp int - nTmp, err = d.DecodeFixedOpaqueInplace(s.Key[:]) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding Key: %w", err) - } - nTmp, err = s.Node.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding TrieNode: %w", err) - } - return n, nil -} - -// MarshalBinary implements encoding.BinaryMarshaler. -func (s TrieNodeChild) MarshalBinary() ([]byte, error) { - b := bytes.Buffer{} - e := xdr.NewEncoder(&b) - err := s.EncodeTo(e) - return b.Bytes(), err -} - -// UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *TrieNodeChild) UnmarshalBinary(inp []byte) error { - r := bytes.NewReader(inp) - o := xdr.DefaultDecodeOptions - o.MaxInputLen = len(inp) - d := xdr.NewDecoderWithOptions(r, o) - _, err := s.DecodeFrom(d, o.MaxDepth) - return err -} - -var ( - _ encoding.BinaryMarshaler = (*TrieNodeChild)(nil) - _ encoding.BinaryUnmarshaler = (*TrieNodeChild)(nil) -) - -// xdrType signals that this type represents XDR values defined by this package. -func (s TrieNodeChild) xdrType() {} - -var _ xdrType = (*TrieNodeChild)(nil) - -// TrieNode is an XDR Struct defines as: -// -// struct TrieNode { -// Value prefix; -// Value value; -// TrieNodeChild children<>; -// }; -type TrieNode struct { - Prefix Value - Value Value - Children []TrieNodeChild -} - -// EncodeTo encodes this value using the Encoder. -func (s *TrieNode) EncodeTo(e *xdr.Encoder) error { - var err error - if err = s.Prefix.EncodeTo(e); err != nil { - return err - } - if err = s.Value.EncodeTo(e); err != nil { - return err - } - if _, err = e.EncodeUint(uint32(len(s.Children))); err != nil { - return err - } - for i := 0; i < len(s.Children); i++ { - if err = s.Children[i].EncodeTo(e); err != nil { - return err - } - } - return nil -} - -var _ decoderFrom = (*TrieNode)(nil) - -// DecodeFrom decodes this value using the Decoder. -func (s *TrieNode) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { - if maxDepth == 0 { - return 0, fmt.Errorf("decoding TrieNode: %w", ErrMaxDecodingDepthReached) - } - maxDepth -= 1 - var err error - var n, nTmp int - nTmp, err = s.Prefix.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding Value: %w", err) - } - nTmp, err = s.Value.DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding Value: %w", err) - } - var l uint32 - l, nTmp, err = d.DecodeUint() - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding TrieNodeChild: %w", err) - } - s.Children = nil - if l > 0 { - if il, ok := d.InputLen(); ok && uint(il) < uint(l) { - return n, fmt.Errorf("decoding TrieNodeChild: length (%d) exceeds remaining input length (%d)", l, il) - } - s.Children = make([]TrieNodeChild, l) - for i := uint32(0); i < l; i++ { - nTmp, err = s.Children[i].DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding TrieNodeChild: %w", err) - } - } - } - return n, nil -} - -// MarshalBinary implements encoding.BinaryMarshaler. -func (s TrieNode) MarshalBinary() ([]byte, error) { - b := bytes.Buffer{} - e := xdr.NewEncoder(&b) - err := s.EncodeTo(e) - return b.Bytes(), err -} - -// UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *TrieNode) UnmarshalBinary(inp []byte) error { - r := bytes.NewReader(inp) - o := xdr.DefaultDecodeOptions - o.MaxInputLen = len(inp) - d := xdr.NewDecoderWithOptions(r, o) - _, err := s.DecodeFrom(d, o.MaxDepth) - return err -} - -var ( - _ encoding.BinaryMarshaler = (*TrieNode)(nil) - _ encoding.BinaryUnmarshaler = (*TrieNode)(nil) -) - -// xdrType signals that this type represents XDR values defined by this package. -func (s TrieNode) xdrType() {} - -var _ xdrType = (*TrieNode)(nil) - -// SerializedLedgerCloseMeta is an XDR Union defines as: -// -// union SerializedLedgerCloseMeta switch (int v) -// { -// case 0: -// LedgerCloseMeta v0; -// }; -type SerializedLedgerCloseMeta struct { - V int32 - V0 *LedgerCloseMeta -} - -// SwitchFieldName returns the field name in which this union's -// discriminant is stored -func (u SerializedLedgerCloseMeta) SwitchFieldName() string { - return "V" -} - -// ArmForSwitch returns which field name should be used for storing -// the value for an instance of SerializedLedgerCloseMeta -func (u SerializedLedgerCloseMeta) ArmForSwitch(sw int32) (string, bool) { - switch int32(sw) { - case 0: - return "V0", true - } - return "-", false -} - -// NewSerializedLedgerCloseMeta creates a new SerializedLedgerCloseMeta. -func NewSerializedLedgerCloseMeta(v int32, value interface{}) (result SerializedLedgerCloseMeta, err error) { - result.V = v - switch int32(v) { - case 0: - tv, ok := value.(LedgerCloseMeta) - if !ok { - err = errors.New("invalid value, must be LedgerCloseMeta") - return - } - result.V0 = &tv - } - return -} - -// MustV0 retrieves the V0 value from the union, -// panicing if the value is not set. -func (u SerializedLedgerCloseMeta) MustV0() LedgerCloseMeta { - val, ok := u.GetV0() - - if !ok { - panic("arm V0 is not set") - } - - return val -} - -// GetV0 retrieves the V0 value from the union, -// returning ok if the union's switch indicated the value is valid. -func (u SerializedLedgerCloseMeta) GetV0() (result LedgerCloseMeta, ok bool) { - armName, _ := u.ArmForSwitch(int32(u.V)) - - if armName == "V0" { - result = *u.V0 - ok = true - } - - return -} - -// EncodeTo encodes this value using the Encoder. -func (u SerializedLedgerCloseMeta) EncodeTo(e *xdr.Encoder) error { - var err error - if _, err = e.EncodeInt(int32(u.V)); err != nil { - return err - } - switch int32(u.V) { - case 0: - if err = (*u.V0).EncodeTo(e); err != nil { - return err - } - return nil - } - return fmt.Errorf("V (int32) switch value '%d' is not valid for union SerializedLedgerCloseMeta", u.V) -} - -var _ decoderFrom = (*SerializedLedgerCloseMeta)(nil) - -// DecodeFrom decodes this value using the Decoder. -func (u *SerializedLedgerCloseMeta) DecodeFrom(d *xdr.Decoder, maxDepth uint) (int, error) { - if maxDepth == 0 { - return 0, fmt.Errorf("decoding SerializedLedgerCloseMeta: %w", ErrMaxDecodingDepthReached) - } - maxDepth -= 1 - var err error - var n, nTmp int - u.V, nTmp, err = d.DecodeInt() - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding Int: %w", err) - } - switch int32(u.V) { - case 0: - u.V0 = new(LedgerCloseMeta) - nTmp, err = (*u.V0).DecodeFrom(d, maxDepth) - n += nTmp - if err != nil { - return n, fmt.Errorf("decoding LedgerCloseMeta: %w", err) - } - return n, nil - } - return n, fmt.Errorf("union SerializedLedgerCloseMeta has invalid V (int32) switch value '%d'", u.V) -} - -// MarshalBinary implements encoding.BinaryMarshaler. -func (s SerializedLedgerCloseMeta) MarshalBinary() ([]byte, error) { - b := bytes.Buffer{} - e := xdr.NewEncoder(&b) - err := s.EncodeTo(e) - return b.Bytes(), err -} - -// UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (s *SerializedLedgerCloseMeta) UnmarshalBinary(inp []byte) error { - r := bytes.NewReader(inp) - o := xdr.DefaultDecodeOptions - o.MaxInputLen = len(inp) - d := xdr.NewDecoderWithOptions(r, o) - _, err := s.DecodeFrom(d, o.MaxDepth) - return err -} - -var ( - _ encoding.BinaryMarshaler = (*SerializedLedgerCloseMeta)(nil) - _ encoding.BinaryUnmarshaler = (*SerializedLedgerCloseMeta)(nil) -) - -// xdrType signals that this type represents XDR values defined by this package. -func (s SerializedLedgerCloseMeta) xdrType() {} - -var _ xdrType = (*SerializedLedgerCloseMeta)(nil) - // LedgerCloseMetaBatch is an XDR Struct defines as: // // struct LedgerCloseMetaBatch From 8e716d8bb9ff71c449987d1df897f29b851af369 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Wed, 26 Jun 2024 13:00:07 -0700 Subject: [PATCH 196/234] ingest: fixed dead links and remove outdated information. (#5357) --- ingest/README.md | 12 ++++++------ ingest/doc.go | 3 +-- ingest/tutorial/example_common.go | 9 +++------ 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/ingest/README.md b/ingest/README.md index cf3d38f8da..277807f978 100644 --- a/ingest/README.md +++ b/ingest/README.md @@ -67,15 +67,15 @@ func main() { _(The `panicIf` function is defined in the [footnotes](#footnotes); it's used here for error-checking brevity.)_ -Notice that the mysterious `config` variable above isn't defined. This will be environment-specific and users should consult both the [Captive Core documentation](../../services/horizon/internal/docs/captive_core.md) and the [config docs](./ledgerbackend/captive_core_backend.go#L96-L125) directly for more details if they want to use this backend in production. For now, though, we'll have some hardcoded values for the SDF testnet: +Notice that the mysterious `config` variable above isn't defined. This will be environment-specific and refer to the code [here](./ledgerbackend/captive_core_backend.go) for the complete list of configuration parameters. For now, we'll use the [default](../network/main.go) values defined for the SDF testnet: ```go -networkPassphrase := "Test SDF Network ; September 2015" +archiveURLs := network.TestNetworkhistoryArchiveURLs +networkPassphrase := network.TestNetworkPassphrase captiveCoreToml, err := ledgerbackend.NewCaptiveCoreToml( ledgerbackend.CaptiveCoreTomlParams{ NetworkPassphrase: networkPassphrase, - HistoryArchiveURLs: []string{ - "https://history.stellar.org/prd/core-testnet/core_testnet_001", + HistoryArchiveURLs: archiveURLs, }, }) panicIf(err) @@ -258,7 +258,7 @@ As of this writing, the stats are as follows: - total operations: 33845 - succeeded / failed: 25387 / 8458 -The full, runnable example is available [here](./example_statistics.go). +The full, runnable example is available [here](./tutorial/example_statistics.go). # **Example**: Feature Popularity @@ -392,4 +392,4 @@ func panicIf(err error) { 2. Since the Stellar testnet undergoes periodic resets, the example outputs from various sections (especially regarding network statistics) will not always be accurate. - 3. It's worth noting that even though the [second example](example-tracking-feature-popularity) could *also* be done by using the `LedgerTransactionReader` and inspecting the individual operations, that'd be bit redundant as far as examples go. + 3. It's worth noting that even though the [second example](#example-feature-popularity) could *also* be done by using the `LedgerTransactionReader` and inspecting the individual operations, that'd be bit redundant as far as examples go. diff --git a/ingest/doc.go b/ingest/doc.go index d7fa6ebc95..e4360b9acc 100644 --- a/ingest/doc.go +++ b/ingest/doc.go @@ -8,8 +8,7 @@ possible features. This is why this package was created. # Ledger Backend Ledger backends are sources of information about Stellar network ledgers. This -can be, for example: a Stellar-Core database, (possibly-remote) Captive -Stellar-Core instances, or History Archives. Please consult the "ledgerbackend" +can be, for example: Captive Stellar-Core instances. Please consult the "ledgerbackend" package docs for more information about each backend. Warning: Ledger backends provide low-level xdr.LedgerCloseMeta that should not diff --git a/ingest/tutorial/example_common.go b/ingest/tutorial/example_common.go index 133ac02de6..aaeb14d447 100644 --- a/ingest/tutorial/example_common.go +++ b/ingest/tutorial/example_common.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/stellar/go/ingest/ledgerbackend" + "github.com/stellar/go/network" ) var ( @@ -11,12 +12,8 @@ var ( ) func captiveCoreConfig() ledgerbackend.CaptiveCoreConfig { - archiveURLs := []string{ - "https://history.stellar.org/prd/core-testnet/core_testnet_001", - "https://history.stellar.org/prd/core-testnet/core_testnet_002", - "https://history.stellar.org/prd/core-testnet/core_testnet_003", - } - networkPassphrase := "Test SDF Network ; September 2015" + archiveURLs := network.TestNetworkhistoryArchiveURLs + networkPassphrase := network.TestNetworkPassphrase captiveCoreToml, err := ledgerbackend.NewCaptiveCoreToml(ledgerbackend.CaptiveCoreTomlParams{ NetworkPassphrase: networkPassphrase, HistoryArchiveURLs: archiveURLs, From 153bbcfa8dda57ac38d3faa5d4eff0fdb09b9f46 Mon Sep 17 00:00:00 2001 From: tamirms Date: Thu, 27 Jun 2024 16:53:09 +0100 Subject: [PATCH 197/234] Fix ingestion duration metric so it includes time reaping lookup tables (#5361) --- services/horizon/internal/ingest/fsm.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/services/horizon/internal/ingest/fsm.go b/services/horizon/internal/ingest/fsm.go index 283cd3347a..c165faf371 100644 --- a/services/horizon/internal/ingest/fsm.go +++ b/services/horizon/internal/ingest/fsm.go @@ -537,9 +537,6 @@ func (r resumeState) run(s *system) (transition, error) { return retryResume(r), err } - duration = time.Since(startTime).Seconds() - s.Metrics().LedgerIngestionDuration.Observe(float64(duration)) - // Update stats metrics changeStatsMap := stats.changeStats.Map() r.addLedgerStatsMetricFromMap(s, "change", changeStatsMap) @@ -560,6 +557,13 @@ func (r resumeState) run(s *system) (transition, error) { // roll up and be reported here as part of resumeState transition addHistoryArchiveStatsMetrics(s, s.historyAdapter.GetStats()) + s.maybeVerifyState(ingestLedger, ledgerCloseMeta.BucketListHash()) + s.maybeReapHistory(ingestLedger) + s.maybeReapLookupTables(ingestLedger) + + duration = time.Since(startTime).Seconds() + s.Metrics().LedgerIngestionDuration.Observe(float64(duration)) + localLog := log.WithFields(logpkg.F{ "sequence": ingestLedger, "duration": duration, @@ -577,10 +581,6 @@ func (r resumeState) run(s *system) (transition, error) { localLog.Info("Processed ledger") - s.maybeVerifyState(ingestLedger, ledgerCloseMeta.BucketListHash()) - s.maybeReapHistory(ingestLedger) - s.maybeReapLookupTables(ingestLedger) - return resumeImmediately(ingestLedger), nil } From 6ebfa537f87aa23c41384ab22d61089863b385a5 Mon Sep 17 00:00:00 2001 From: urvisavla Date: Thu, 27 Jun 2024 09:33:18 -0700 Subject: [PATCH 198/234] exp/services/ledgerexporter: Guide to installing and running ledger exporter (#5355) --- exp/services/ledgerexporter/README.md | 167 ++++++++++-------- .../ledgerexporter/config.example.toml | 42 +++++ exp/services/ledgerexporter/config.toml | 14 -- 3 files changed, 140 insertions(+), 83 deletions(-) create mode 100644 exp/services/ledgerexporter/config.example.toml delete mode 100644 exp/services/ledgerexporter/config.toml diff --git a/exp/services/ledgerexporter/README.md b/exp/services/ledgerexporter/README.md index 57757508e1..008981b551 100644 --- a/exp/services/ledgerexporter/README.md +++ b/exp/services/ledgerexporter/README.md @@ -1,101 +1,130 @@ -# Ledger Exporter (Work in Progress) +## Ledger Exporter: Installation and Usage Guide -The Ledger Exporter is a tool designed to export ledger data from a Stellar network and upload it to a specified destination. It supports both bounded and unbounded modes, allowing users to export a specific range of ledgers or continuously export new ledgers as they arrive on the network. +This guide provides step-by-step instructions on installing and using the Ledger Exporter, a tool that exports Stellar network ledger data to a Google Cloud Storage (GCS) bucket for efficient analysis and storage. -Ledger Exporter currently uses captive-core as the ledger backend and GCS as the destination data store. +* [Prerequisites](#prerequisites) +* [Setup](#setup) + * [Set Up GCP Credentials](#set-up-gcp-credentials) + * [Create a GCS Bucket for Storage](#create-a-gcs-bucket-for-storage) +* [Running the Ledger Exporter](#running-the-ledger-exporter) + * [Pull the Docker Image](#1-pull-the-docker-image) + * [Configure the Exporter](#2-configure-the-exporter-configtoml) + * [Run the Exporter](#3-run-the-exporter) +* [Command Line Interface (CLI)](#command-line-interface-cli) + 1. [scan-and-fill: Fill Data Gaps](#1-scan-and-fill-fill-data-gaps) + 2. [append: Continuously Export New Data](#2-append-continuously-export-new-data) -# Exported Data Format -The tool allows for the export of multiple ledgers in a single exported file. The exported data is in XDR format and is compressed using zstd before being uploaded. +## Prerequisites -```go -type LedgerCloseMetaBatch struct { - StartSequence uint32 - EndSequence uint32 - LedgerCloseMetas []LedgerCloseMeta -} -``` +* **Google Cloud Platform (GCP) Account:** You will need a GCP account to create a GCS bucket for storing the exported data. +* **Docker:** Allows you to run the Ledger Exporter in a self-contained environment. The official Docker installation guide: [https://docs.docker.com/engine/install/](https://docs.docker.com/engine/install/) + +## Setup + +### Set Up GCP Credentials + +Create application default credentials for your Google Cloud Platform (GCP) project by following these steps: +1. Download the [SDK](https://cloud.google.com/sdk/docs/install). +2. Install and initialize the [gcloud CLI](https://cloud.google.com/sdk/docs/initializing). +3. Create [application authentication credentials](https://cloud.google.com/docs/authentication/provide-credentials-adc#google-idp) and store it in a secure location on your system, such as $HOME/.config/gcloud/application_default_credentials.json. + +For detailed instructions, refer to the [Providing Credentials for Application Default Credentials (ADC) guide.](https://cloud.google.com/docs/authentication/provide-credentials-adc) + +### Create a GCS Bucket for Storage -## Getting Started +1. Go to the GCP Console's Storage section ([https://console.cloud.google.com/storage](https://console.cloud.google.com/storage)) and create a new bucket. +2. Choose a descriptive name for the bucket, such as `stellar-ledger-data`. Refer to [Google Cloud Storage Bucket Naming Guideline](https://cloud.google.com/storage/docs/buckets#naming) for more information. +3. **Note down the bucket name** as you'll need it later in the configuration process. -### Installation (coming soon) -### Command Line Options +## Running the Ledger Exporter + +### 1. Pull the Docker Image + +Open a terminal window and download the Stellar Ledger Exporter Docker image using the following command: -#### Scan and Fill Mode: -Exports a specific range of ledgers, defined by --start and --end. Will only export to remote datastore if data is absent. ```bash -ledgerexporter scan-and-fill --start --end --config-file +docker pull stellar/ledger-exporter ``` -#### Append Mode: -Exports ledgers initially searching from --start, looking for the next absent ledger sequence number proceeding --start on the data store. If abscence is detected, the export range is narrowed to `--start `. -This feature requires ledgers to be present on the remote data store for some (possibly empty) prefix of the requested range and then absent for the (possibly empty) remainder. +### 2. Configure the Exporter (config.toml) +The Ledger Exporter relies on a configuration file (config.toml) to connect to your specific environment. This file defines details like: +- Your Google Cloud Storage (GCS) bucket where exported ledger data will be stored. +- Stellar network settings, such as the network you're using (testnet or pubnet). +- Datastore schema to control data organization. -In this mode, the --end ledger can be provided to stop the process once export has reached that ledger, or if absent or 0 it will result in continous exporting of new ledgers emitted from the network. +A sample configuration file [config.example.toml](config.example.toml) is provided. Copy and rename it to config.toml for customization. Edit the copied file (config.toml) to replace placeholders with your specific details. + +### 3. Run the Exporter + +The following command demonstrates how to run the Ledger Exporter: - It’s guaranteed that ledgers exported during `append` mode from `start` and up to the last logged ledger file `Uploaded {ledger file name}` were contiguous, meaning all ledgers within that range were exported to the data lake with no gaps or missing ledgers in between. ```bash -ledgerexporter append --start --config-file +docker run --platform linux/amd64 \ + -v "$HOME/.config/gcloud/application_default_credentials.json":/.config/gcp/credentials.json:ro \ + -e GOOGLE_APPLICATION_CREDENTIALS=/.config/gcp/credentials.json \ + -v ${PWD}/config.toml:/config.toml \ + stellar/ledger-exporter [options] ``` -### Configuration (toml): -The `stellar_core_config` supports two ways for configuring captive core: - - use prebuilt captive core config toml, archive urls, and passphrase based on `stellar_core_config.network = testnet|pubnet`. - - manually set the the captive core confg by supplying these core parameters which will override any defaults when `stellar_core_config.network` is present also: - `stellar_core_config.captive_core_toml_path` - `stellar_core_config.history_archive_urls` - `stellar_core_config.network_passphrase` +**Explanation:** -Ensure you have stellar-core installed and set `stellar_core_config.stellar_core_binary_path` to it's path on o/s. +* `--platform linux/amd64`: Specifies the platform architecture (adjust if needed for your system). +* `-v`: Mounts volumes to map your local GCP credentials and config.toml file to the container: + * `$HOME/.config/gcloud/application_default_credentials.json`: Your local GCP credentials file. + * `${PWD}/config.toml`: Your local configuration file. +* `-e GOOGLE_APPLICATION_CREDENTIALS=/.config/gcp/credentials.json`: Sets the environment variable for credentials within the container. +* `stellar/ledger-exporter`: The Docker image name. +* ``: The Stellar Ledger Exporter command: [append](#1-append-continuously-export-new-data), [scan-and-fill](#2-scan-and-fill-fill-data-gaps)) -Enable web service that will be bound to localhost post and publishes metrics by including `admin_port = {port}` +## Command Line Interface (CLI) -An example config, demonstrating preconfigured captive core settings and gcs data store config. -```toml -admin_port = 6061 +The Ledger Exporter offers two mode of operation for exporting ledger data: -[datastore_config] -type = "GCS" +### 1. append: Continuously Export New Data -[datastore_config.params] -destination_bucket_path = "your-bucket-name///" -[datastore_config.schema] -ledgers_per_file = 64 -files_per_partition = 10 +Exports ledgers initially searching from --start, looking for the next absent ledger sequence number proceeding --start on the data store. If abscence is detected, the export range is narrowed to `--start `. +This feature requires ledgers to be present on the remote data store for some (possibly empty) prefix of the requested range and then absent for the (possibly empty) remainder. -[stellar_core_config] - network = "testnet" - stellar_core_binary_path = "/my/path/to/stellar-core" - captive_core_toml_path = "my-captive-core.cfg" - history_archive_urls = ["http://testarchiveurl1", "http://testarchiveurl2"] - network_passphrase = "test" -``` +In this mode, the --end ledger can be provided to stop the process once export has reached that ledger, or if absent or 0 it will result in continous exporting of new ledgers emitted from the network. -### Exported Files +It’s guaranteed that ledgers exported during `append` mode from `start` and up to the last logged ledger file `Uploaded {ledger file name}` were contiguous, meaning all ledgers within that range were exported to the data lake with no gaps or missing ledgers in between. -#### File Organization: -- Ledgers are grouped into files, with the number of ledgers per file set by `ledgers_per_file`. -- Files are further organized into partitions, with the number of files per partition set by `files_per_partition`. -### Filename Structure: -- Filenames indicate the ledger range they contain, e.g., `0-63.xdr.zstd` holds ledgers 0 to 63. -- Partition directories group files, e.g., `/0-639/` holds files for ledgers 0 to 639. +**Usage:** -#### Example: -with `ledgers_per_file = 64` and `files_per_partition = 10`: -- Partition names: `/0-639`, `/640-1279`, ... -- Filenames: `/0-639/0-63.xdr.zstd`, `/0-639/64-127.xdr.zstd`, ... +```bash +docker run --platform linux/amd64 -d \ + -v "$HOME/.config/gcloud/application_default_credentials.json":/.config/gcp/credentials.json:ro \ + -e GOOGLE_APPLICATION_CREDENTIALS=/.config/gcp/credentials.json \ + -v ${PWD}/config.toml:/config.toml \ + stellar/ledger-exporter \ + append --start [--end ] [--config-file ] +``` + +Arguments: +- `--start ` (required): The starting ledger sequence number for the export process. +- `--end ` (optional): The ending ledger sequence number. If omitted or set to 0, the exporter will continuously export new ledgers as they appear on the network. +- `--config-file ` (optional): The path to your configuration file, containing details like GCS bucket information. If not provided, the exporter will look for config.toml in the directory where you run the command. + +### 2. scan-and-fill: Fill Data Gaps -#### Special Cases: +Scans the datastore (GCS bucket) for the specified ledger range and exports any missing ledgers to the datastore. This mode avoids unnecessary exports if the data is already present. The range is specified using the --start and --end options. -- If `ledgers_per_file` is set to 1, filenames will only contain the ledger number. -- If `files_per_partition` is set to 1, filenames will not contain the partition. +**Usage:** -#### Note: -- Avoid changing `ledgers_per_file` and `files_per_partition` after configuration for consistency. +```bash +docker run --platform linux/amd64 -d \ + -v "$HOME/.config/gcloud/application_default_credentials.json":/.config/gcp/credentials.json:ro \ + -e GOOGLE_APPLICATION_CREDENTIALS=/.config/gcp/credentials.json \ + -v ${PWD}/config.toml:/config.toml \ + stellar/ledger-exporter \ + scan-and-fill --start --end [--config-file ] +``` -#### Retrieving Data: -- To locate a specific ledger sequence, calculate the partition name and ledger file name using `files_per_partition` and `ledgers_per_file`. -- The `GetObjectKeyFromSequenceNumber` function automates this calculation. +Arguments: +- `--start ` (required): The starting ledger sequence number in the range to export. +- `--end ` (required): The ending ledger sequence number in the range. +- `--config-file ` (optional): The path to your configuration file, containing details like GCS bucket information. If not provided, the exporter will look for config.toml in the directory where you run the command. diff --git a/exp/services/ledgerexporter/config.example.toml b/exp/services/ledgerexporter/config.example.toml new file mode 100644 index 0000000000..db72169311 --- /dev/null +++ b/exp/services/ledgerexporter/config.example.toml @@ -0,0 +1,42 @@ + +# Sample TOML Configuration + +# Admin port configuration +# Specifies the port number for hosting the web service locally to publish metrics. +admin_port = 6061 + +# Datastore Configuration +[datastore_config] +# Specifies the type of datastore. Currently, only Google Cloud Storage (GCS) is supported. +type = "GCS" + +[datastore_config.params] +# The Google Cloud Storage bucket path for storing data, with optional subpaths for organization. +destination_bucket_path = "your-bucket-name///" + +[datastore_config.schema] +# Configuration for data organization +ledgers_per_file = 64 # Number of ledgers stored in each file. +files_per_partition = 10 # Number of files per partition/directory. + +# Stellar-core Configuration +[stellar_core_config] +# Use default captive-core config based on network +# Options are "testnet" for the test network or "pubnet" for the public network. +network = "testnet" + +# Alternatively, you can manually configure captive-core parameters (overrides defaults if 'network' is set). + +# Path to the captive-core configuration file. +#captive_core_config_path = "my-captive-core.cfg" + +# URLs for Stellar history archives, with multiple URLs allowed. +#history_archive_urls = ["http://testarchiveurl1", "http://testarchiveurl2"] + +# Network passphrase for the Stellar network. +#network_passphrase = "Test SDF Network ; September 2015" + +# Path to stellar-core binary +# Not required when running in a Docker container as it has the stellar-core installed and path is set. +# When running outside of Docker, it will look for stellar-core in the OS path if it exists. +#stellar_core_binary_path = "/my/path/to/stellar-core diff --git a/exp/services/ledgerexporter/config.toml b/exp/services/ledgerexporter/config.toml deleted file mode 100644 index c41d9376ac..0000000000 --- a/exp/services/ledgerexporter/config.toml +++ /dev/null @@ -1,14 +0,0 @@ -[datastore_config] -type = "GCS" - -[datastore_config.params] -destination_bucket_path = "exporter-test/ledgers/testnet" - -[datastore_config.schema] -ledgers_per_file = 1 -files_per_partition = 64000 - -[stellar_core_config] - network = "testnet" - stellar_core_binary_path = "/usr/local/bin/stellar-core" - From 6fbc6e0c96e2b550dd18322efce88524cb06f1bb Mon Sep 17 00:00:00 2001 From: urvisavla Date: Thu, 27 Jun 2024 15:56:36 -0700 Subject: [PATCH 199/234] exp/services/ledgerexporter: Add guide for contributors/developers (#5340) Co-authored-by: shawn Co-authored-by: George --- .../ledgerexporter/DEVELOPER_GUIDE.md | 65 ++++++++++++++++++ exp/services/ledgerexporter/architecture.png | Bin 0 -> 357065 bytes 2 files changed, 65 insertions(+) create mode 100644 exp/services/ledgerexporter/DEVELOPER_GUIDE.md create mode 100644 exp/services/ledgerexporter/architecture.png diff --git a/exp/services/ledgerexporter/DEVELOPER_GUIDE.md b/exp/services/ledgerexporter/DEVELOPER_GUIDE.md new file mode 100644 index 0000000000..ef81553e37 --- /dev/null +++ b/exp/services/ledgerexporter/DEVELOPER_GUIDE.md @@ -0,0 +1,65 @@ + +# Ledger Exporter Developer Guide +The ledger exporter is a tool to export Stellar network transaction data to cloud storage in a way that is easy to access. + +## Prerequisites +This document assumes that you have installed and can run the ledger exporter, and that you have familiarity with its CLI and configuration. If not, please refer to the [Installation Guide](./README.md). + +## Goal +The goal of the ledger exporter is to build an easy-to-use tool to export Stellar network ledger data to a configurable remote data store, such as cloud blob storage. + - Use cloud storage optimally + - Minimize network usage to export + - Make it easy and fast to search for a specific ledger or ledger range + +## Architecture +To achieve its goals, the ledger exporter uses the following architecture, which consists of the 3 main components: +- Captive-core to extract raw transaction metadata from the Stellar Network. +- Export manager to bundles and organizes the ledgers to get them ready for export. +- The cloud storage plugin writes to the cloud storage. This is specific to the type of cloud storage, GCS in this case. + + +![ledgerexporter-architecture](./architecture.png) + + +## Data Format +- Ledger exporter uses a compact and efficient data format called [XDR](https://developers.stellar.org/docs/learn/encyclopedia/data-format/xdr) (External Data Representation), which is a compact binary format. A Stellar Captive Core instance emits data in this format and the data structure is referred to as `LedgerCloseMeta`. The exporter bundles multiple `LedgerCloseMeta`'s into a single object using a custom XDR structure called `LedgerCloseMetaBatch` which is defined in [Stellar-exporter.x](https://github.com/stellar/go/blob/master/xdr/Stellar-exporter.x). + +- The metadata for the same batch is also stored alongside each exported object. Supported metadata is defined in [metadata.go](https://github.com/stellar/go/blob/master/support/datastore/metadata.go). + +- Objects are compressed before uploading using the [zstd](http://facebook.github.io/zstd/) (zstandard) compression algorithm to optimize network usage and storage needs. + +## Data Storage +- An example implementation of `DataStore` for GCS, Google Cloud Storage. This plugin is located in the [support](https://github.com/stellar/go/tree/master/support/datastore) package. +- The ledger exporter currently implements the interface only for Google Cloud Storage (GCS). The [GCS plugin](https://github.com/stellar/go/blob/master/support/datastore/gcs_datastore.go) uses GCS-specific behaviors like conditional puts, automatic retry, metadata, and CRC checksum. + +## Build, Run and Test using Docker +The Dockerfile contains all the necessary dependencies (e.g., Stellar-core) required to run the ledger exporter. + +- Build: To build the Docker container, use the provided [Makefile](./Makefile). Simply run make `make docker-build` to build a new container after making any changes. + +- Run: For instructions on running the Docker container, refer to the [Installation Guide](./README.md). + +- Test: To test the Docker container, refer to the [docker-test](./Makefile) command for an example of how to use the [GCS emulator](https://github.com/fsouza/fake-gcs-server) for local testing. + +## Adding support for a new storage type +Support for different data storage types are encapsulated as 'plugins', which are implementation of `DataStore` interface in a go package. To add a data storage plugin based on a new storage type (e.g. AWS S3), follow these steps: + +- A data storage plugin must implement the [DataStore](https://github.com/stellar/go/blob/master/support/datastore/datastore.go) interface. +- Add support for new datastore-specific features. Implement any datastore-specific custom logic. Different datastores have different ways of handling + - race conditions + - automatic retries + - metadata storage, etc. +- Add the new datastore to the factory function [NewDataStore](https://github.com/stellar/go/blob/master/support/datastore/datastore.go). +- Add a [config](./config.example.toml) section for the new storage type. This may include configurations like destination, authentication information etc. +- An emulator such as a GCS emulator [fake-gcs-server](https://github.com/fsouza/fake-gcs-server) can be used for testing without connecting to real cloud storage. + +### Design DOs and DONTs +- Multiple exporters should be able to run in parallel without the need for explicit locking or synchronization. +- Exporters when restarted do not have any memory of prior operation and rely on the already exported data as much as possible to decide where to resume. + +## Using exported data +The exported data in storage can be used in the ETL pipeline to gather analytics and reporting. To write a tool that consumes exported data you can use Stellar ingestion library's `ledgerbackend` package. This package includes a ledger backend called [BufferedStorageBackend](https://github.com/stellar/go/blob/master/ingest/ledgerbackend/buffered_storage_backend.go), +which imports data from the storage and validates it. For more details, refer to the ledgerbackend [documentation](https://github.com/stellar/go/tree/master/ingest/ledgerbackend). + +## Contributing +For information on how to contribute, please refer to our [Contribution Guidelines](https://github.com/stellar/go/blob/master/CONTRIBUTING.md). diff --git a/exp/services/ledgerexporter/architecture.png b/exp/services/ledgerexporter/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..85bd6d8b31d6224a6897681351c9b8f90f48d479 GIT binary patch literal 357065 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