From b0220a0d92ba925cfb22f976f7805f4c4a3b4992 Mon Sep 17 00:00:00 2001 From: tamirms Date: Wed, 20 Dec 2023 15:21:32 +0000 Subject: [PATCH 01/34] 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 02/34] 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 03/34] 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 04/34] 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 05/34] 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 06/34] 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 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 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 zcmZQzfB=`PjUlWi#r5;7d9BmDrDBq6R<3tVyZW)MHb>9IwD=lOmGE+j9*u;nPh-}$ z>@)oI@_UPLpULwh`vnDWcE=>`pO+IZ+H*&zEre;o&Z-Ut35I#oBERin$(7AsSiS1V z%xgV|R6#Z+Ejp12v5|ohL~o51*tEM=!|&<=EBB=jw@x_8AKo15E^{QHdS4UEC7W)b z5{Huk7gTj};w}asXgb;0a`o4)gf4xrr^lirUW!1t16zK zk2c=+;m9-@lWnC6ymyvNU_8pND6Cf>Gkcxi>J@kDKm9gZ$hsxzN!C7(s}rvp6-;TC zyvp%%{r%OSCHK~?xA?$u?av`LeQn!$lN(jaUOrCvTf-pQV#fPm+vepdAQv+qKkFaj z17bnIDUfIigO9fZh~DPuS~~shmj#pdY`tS*r?aKu;6I(2YOE(ef4Lmh${2NNI?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$0(@ydY@79KO!ndi=={!aS@^V8O+rsZsB@&p#%{ix#B$-wXA01N|Tn0k;N zAOOW5I1UNwXJB9hsSS2@2I*r+n8g3;ih6PPcDW4>HU+)jzVnOtqvK|Bu3R#AR)BWS zW`>Q6_UIi2D+LjZU^f8M!NN|H#5=90t%UFG*Xi!^vZ$UbX*p5DKAofM-wU}Lf}1Lv zj@G<+C-{CsQh8{^a)(Vtt(V<}UM^dl=66|5)7YO2XcpMNt>P_=>hC4uKh4nN`+Aq9 zYrkK1#w0_N$x4YSwX@jmr!(jtJAme2kUu3{1AN`X3cVt|44ezg4bnpm@`|fM(`{{m za?C&s4hw>2Gcd3N)iNAd(0EXz+I>NFT~cn*>shBj&Y_jt!Ql-u9~`d0IE*x7pYN+%H>K+zU-kAmPgj5Q2yqUW z-9PP2OX&2@50YPEZZ8(9i@H`3RQ!0azh^Hy@1d8IHnJ327`5x%{WmX#6=)XN%v0Z3 z!@5Pg6pN*A7K*?8&*<%SCuO(WvJ3YPY54DH^_mI{<_iqWT5Sx>rQtCD0cmsqb005M zBG@%uayE!f*n9@Y&|nvEg65E&=ddNKb&}ousMFsT)LcEj_ZElv{&`l)D_YC%8+o}y z%vKkG+5w_q_Oa$%SC#<@5M!TfhzCe1gW8{}e`V^WW?HNNY%-eC)MXXD{^#}6i?g=4 z_PtWrCVUC3hyleuMo5@2I2?c_&YrP7pQ(x*>%@K04iLd zVsJXeKkzA2c^u!dYEPvd_pW-T%nvwx-_Ih%YM;cE&-L%_m#oz~37*FJ+)&6w5zRn7=?tHTSM_quO0Y((yD z)ohzmwIyanc=D;V6-SQfiB_CplD$;n^sRD2W9R)gLD9J{*L;t051%;k{$<9+_TJ0S z#<6PVfWn2P#{Bf}NWuNi&y(F2Zm{~m*}bbEir1sW*D3F)@3-7bXP|b1YY`wD7AByw z5e7i%kr`O_n7{;xC|en%E|thY%PW|fFzs+lfC3nDKrvYQhp7jZV+=6+fpv8}RDuyG zW(H*wt}lQJr7jikps|}kZij`}U~?NK;e{S2$Pq_M9D>6Ht-S?{UyvNwEF#hutR5m< zZ^IG@qW%W8F<=0cuE~uj+Jy;aZbFFw;7Q42(KZlOb^mZC60WknIPzH=yFM`~ojuh-o7*{rZ5d zeBlO}iDVHH6KWfolc%i3TP&$N# zmnkf1P`kYfPFdK?7g(5p(;(gKJEa{oc)U}*)E zKfr*9b~FR~`lm~vZE1g?CggY^xBLa$4{RGC+M-DHAdzlzps|}kVFe2>c=+y+a7 zq{JaCyg>aSYQ!OW`3rW-)Q9CUubKA=Z82Y5rgUL`yddZ8kB@IL-C3tF_1qzL>p-v~ z28I*RGztn|5a2~hqsVMn_`}-^MD$Y`7N;%Hg!a9|fM#=oZGaL;jRYhnTm{TMFacP5 s6>LAQ{s?hynx9BxH-X#^3om$j9VBi;Nv|Zj2{e8}jX1=bUSSap0K!sY`v3p{ 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 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 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 zcmd5=3pkY78~+C55@K1g-Ik$TR>UAp(Nylz#fnf{%Ov+p<^+Ne|_ z(>AGCSwUnyfXXd=$Iq&6n-uL~^Ip2XG4(*zk z9I2_)ALzp%DKJ*`P`pI6)4FB%np}@G-d(b6X*?_?+X*RpH`Q!?Hn`FtPeLT;pA(M# zLzn0|`&lENVHbN8uNBDTef7Jl!9Ubp-p{uC2{m^d>L)lg-tFGH(2wk0vM&&Laye6F z!|@0aKzmN*o&xJsps=k8s+PWTwaz-)QsYPs`Au=pNH7m<{ySy*QTZ=Mj zadEUKkWp0p2a#p!QFdjuXRt+~b!o$@cE>Jrz3i5Z#QRIj^+?ssF z>sGu-PIhK+qm7J~JaYpi7a1@4_}bOqXaKQbMa@L_4#07!3iQ&Tb=FkS`efEdhL0%x zX%rCR`}~w`z7AC>EQ_*Fuw%@)xKOcH=A=ZbR7cS-->RP08_U7YFnF@Y za7l)Lv&%Av7&Sf3*s+%oZQ|Wd+pXcV7O>NPj8vwkHtuP5mkKuNc(}7Sk3>Ixd@y3K z?$?_t6BbpG8#El~B#SN)=dNoUOb|3vu3$ML{1X83^t{TJ{D76L)8`*sJJtEB-nMpr z_2i}?wATGpdF;jH0Z3{M6^=n1EQ;g|=)i>Mq4Ihs^L$xf<0cQ|deapuV~w+1hs?Kk zWcge6&0-vsS_u>u+}jBnKm^1toLfzkW9pZFKD{bVxIp@t!8@5bj-4q1TNnl&V#-l{ zQSI-V#Rn=&7N;4<5xuNmE>j>bSnBxT>Vr00NsUzk&2x_c49FZhjY1)>)-umE^Vtn- zH&15Xo3Sfe3N;aXl9Yc?Ogt!p%t2f!9F|W2{^1`l0{zIC^np4rO+&Ipgw~;jqj~qg zu1li(L3X?C>WvrrMB6`3!<40NOXioh=GouCxfVl#A8#`jcE=rsu1_y>tB}0mVR#QHmc9Dm|<+rpsJpXMZ?1HTuZ@uR4r$ z_SdF_5Qk)A9JQVo-Wtrfe)}61t7_0*MJ=~r;bu}%>I-*f=6si>tpP%?K_r$s1FHp-E-O`^5L)EbuQEat z6JJaEc^%2u(zmiwpCPLQ%h4d|Icx}0I|iU&KtStqh8=|ViTUsGr;y2d`nnteOb%)J zh@Zg5&9)oYnR1lDKR9uk+T5u*Jsa&)CV z^O^(X`z8t2R&BH!W@l?3m8xxEMOht(AaN!ns1Xeb27-FR=8MANHi9Sz>;e+76YoZt zneB7r4UEG81MCwve{ZrDJcuqxrxtof#T#chu}3mP8!B$Lh|1qerzoF`3U1f6G6ym+ zeG;Fhb{2@S#p6Fs1gQoZXq+D}&`pe>Q=Bi2J!EHj7MlLruXs)N0V4E?d^)b#f|!eQ zqdrfZU>;;S-rlxpqB@#Xp%7MEBc!N#_=J5x)7lEuCxMd>i0ZGLS;p?u8P1s7@R!ud zV}Ax}=8*I2y5^?@uT8(FMj=FXD*MPiJF@bb@Ak>tvUhu^7HU2PGGUD>-pevsmDLik zJ>r3wL{oWk=zT4}728=E<72A1B{Z5#eY;b_@ga#ijRDjrZ4xG!$MQ~qAK##3pne3= zJqYRIqyFcpC5xKD=D?a|4=Z80L-a8x@B@xThQJ?EuiPi#8xUs?1ZNmd0)Z}kHo)Zg z!z9xAnK1$ESiHV8Y#$RZu?JXY`@|G{R zo9q|NxUG?Thm&K1CR(H1H3)v5;EX|%)9_d>Vo*7VpLHtB&7lWf3wObKV4vm^u3&L2 zcNqA?6IQ#3e|X;7=-`xG@Q2C%`e(+(vv$7_Y#$RZu%CU39P+GP({J~aA4@x}V@R)@ zRgySbV|Di0(8Kh9L#zyr7O(G>lScE+YkkFP7pqZl#z%;cGYlkh>7FQhj`6|D^93mfLAs~PwV;8TrN(Tv}Gu;2<+dML#C?&I!- zm_E)4j>Sf&I|Fe1yPUB-gGx?`IiHvmJ~JksHTs2M`nHl6Qs4$Cq^zqfpvpy3|vjq3C99wYZji1>{6PuLv_ zdj7^^1UwI%Q{v2@{U>SI`JNB^Jm0U+;OArJ@m^SP9XOUd+yC;eaBJ-Y=l-65TnCQ* 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 zcmZQzfPho0-1yz}*R`%@`P9_-{&m{VfKPg+S2(Qw+nV_=?KmL|R3)tXF=SEh{a63J zUT;lIvrZAOyzjq1*^v2t6mQJyh=U5&alzkK9@^mDDJ|s4^?{3b^L($ISKr_6uia+% zIiyqTvaSlPL+Z_4wg*K@3H zznP`}T6fjIec#xYMlM>Dcb6e9sWRijkD9VM2PZ99RIX+ERwUHj^mNq>&ZKkVR~+75 zd4G1ZwPU-CW7q1-VKcX}C!M)dl@P!8*rEf|0-9!3E-_#bZJELQVB6;9DIga!A3yh> zT?NE~fKwpR6b2t}2M|rjtm!DZAa4 zUAT8h!+%e!*Hi`u?i~!wT5Sx>rI|qG;CKV+Lj%lEJ};DJ3gLbNW)e0Ztc=Obt{%hy zna>y+?BWdKaLCSc*pk&c$!>ns>2C{at{&fei$i?>JS*iDt>yQPyxc)*fPgA?x`udw z4O9D5^{-65)J$vjpG`(nn!2o_*Z;hJdU4hk*S=Q@+k`KH6;a8~NT&#}aTC)V-cR|r zqi#>+f!M%*XWEpGOVnRD$D*LqJT0v7hVyZ#os5w9U~qU+bT@emqk`Kty@H*lj@lm; zFL=hj64{aKemz2Y#zA&7=D@^d5AL*fmt>~$Yn9%TY0zKPI`wGm%OWm@Y~5K?*n#GO zk#valkYmO5Kai0aNARuOKC~+qB(*&PFEi+Y4bVCz@RR`AncdHz!=sA)PoZD zz`WcB72_x-Se%;GEU-TEtEZ*mle+o=+Z*X0=k7RKweW-h+=8Kh34K&k<% zo)O|s28Yj2h3|2=ub8>*0@IiNKZa6E^1AQZ3Sa8gu~~heqc@Xj*PbI+Hpp}ESxamz zzALgwplyd@T&(`?B$KUq+ zD9a<(D!PDy-^qc2TM=acWFSTf6CfW3!08yo5vaYGtz%*A(WKWX_kGoQk&83B@)<&k zLN43M<+g=Am;=-BYw!B4Mz=ZF2J)vlc7}_8dBeW*_eS0fy{9uoW6kAVyn*V(J<49d zlrw?d0xTc5^Mu_$BrMIeYL~^NSIXKMZo4mk5ug7q=iVy07dMx&u1{Q3?{v`r(>p1a zMDf_^4^vLX|N1=hx?`riw(urNw}VVjH#l(APuXDZd-K5F*wbfLak^Z)lvp10qhiHj z1M4mGt4}?2z42h8=nS^ltG7MooSLsE*ZkDiMmcxB#gB(&!Can8q(SZihhK`b(U+v` zyq}zt97^Z(c6~S1OP{4SEnL;Hc6)Y2;SOJL_=&bm2dYO7J7hM){nD0gg`!GJ+5%nE zR-1lWFL^dSa+VB}f|PZcInTZMe?XSWo=pW&AixMV7Z?_yZ`a*O`Mbk!Eo=KM? zcc0Cxs!^=|oSVMvO50;YDX;A7=h%9i7uO51Y+(-2v7R!k$kz9X?1q5Y70oB~W^n_} z0{fvo{!!UI3+8(~7H?~E?I-MSI_`N`a0$=*;Bcjcg;UqOhNgklK=o8j1E;elzj^4k z!}?j)UESc&4831s)9my}JA8y*K-S_MxN!uv>uset#!4|5?x)!4}PBMtZqs z+hp&~Q#|XyxaR)#{4*1GCI=;-jN6p5F+;of)9FXRhICT$JA_}xxz_pVba0v9zo zVQFBJ$N8dP%u(R@U-L;b3#Dyw@ale#dgYOP4;rdjSeRR=>mZx|A{wztw;8)9iF#)sxpe3-fG$cl#gznJ50X zPi;L=4G=KH5;97>L*&45z|0_;dlt%PAebkC3aujp1)%wI5=B*nCY(Mt3R{D&~1jYBbLi7`p zj}hTaux@~rZ%A=Nz?~@JOq}^oB#`5tfcaSS7Y*#8#2*;Zha`Z+g!`17yaF%xNc1~~ z{YU~xOfuYtl$MEclh(?5SiED{izI-=B-u?Ud6MWh43ad1a#;B^Kx~NwuUDCV>it^~{md`^0|Al|j6m)$sM)Z(0$%nJ?gKE0%lPJ4 zK>GomKy!GYW`QXrcOo(2DsZJWu>HVtISr~3CH)iUri=g@y9sOFA0%$0B)mXZ>sS$%hxB+!!~m@Y%fTA#-g`<@GGby&pI81&hA%O@_ob_YP*MR*5P=6Ahwqg2_5+)K8CX2K00=6I6cR2=C zi4x|-xoPb?8oLQ=8XY8Vqa?iO(RYEj$7s;EK=C^f=Fe&ku_C+gLiCsi3GF0U?f?MF 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 zcmZQzfPlh_OV$|({A{_VbJg5)sprQdR|7a4Z#aCBpEC8XM^|nwP?hk@mnf4Zi4 zdzygWr%0YV2M(-Rrha_Kyx)u>cE>JVU2gyW&+(Oa)=xaNX+cV%(B_nNvUlS>S?6t< zsGo3-YtyN1Ae)jFoth1?k%19JZ;ch$w7XWr@9F_7_oWZFPB_UQ-W=*Kb0na8UlYqE zn{J>Iho@^#uV<~+-uh0oeVgozqt@$OW1oDu{{F1ww4QXOdDUt6pBv0p z?;bOy_f~P|xj($J|Adt3x+`b;xrAHw)vB_(Zh4kXXNx8?y7>S5-}Z(}HeWZ_>S&iuYi^TYZ68mHTg0L>45BRyc^_=sygUWuV&>!L zqt;yju^`|SNHm4P$J+r!zdyU#+Ob{6v1|3^u$kM~lg`|!N{HWkY|(*f0Zp?iml#av zna%%jYqz}mLX)5C^iL%`+fvrP^3Rs?|K}`p=NH^eG7s<3FwyD#`H3Or-`~`rm1iy@H;sG!(cv8 zDLBrcw1j0@YPN5Vdq$2yrglYOagk$oa-~I}tu2~5hTDn;P1o+NXy3Hm+}`lD?x{+R z`}<}|vCQ5(U)bn#%L&Z1? ziVLiajZMr!8lYluI>kTmDN}hI-?3^>r5*RKdZx?|IDFsFBE)K+#FWqbntg!^nF2zC z0(@LSS|A|()V1V_S$w6vTYu&2O^)JlJ5^%wx;#U`!g!t5%*EF}gH$mkML|_FFhbqx z5c$D!vAo1C0Y3AEH;;T=JyE|h<$~`o9*IvT$zQ(w(NcTE*QohMY~`g%o7aA_oxXp< zmmu8-OB|+3v)dh=P$cw#4P-3HP=an|Y+C>fj;#z#-{XMF(Zc1_R-hQm-Z^jOHtqLX z@^Bu{OnsA-FRn+}zT7vR{%DgHA zy?ttiPbdSYQ@-NvjqGPCSI_Ht_i^j)!)t_}&u{r0y5!RiY5P#Uj!2opH|_;VAy!rf zjH*091HoZ=?ZQ0sWjrBie}njS&ao%P?4IjrrPVY2l~?MqZJb)dx!`=nt=IrmKN*No z!V<`b0gyOYPN4Q;wvL6dN0VNo-1k-IMJ~?h%4Y~E3b|}2m)jQhU=B=!%b`hyO>=54 z>DSs`+_{JO%KLylpYjf{dTRXl4T;G)`2wg;+@lOcfdLcPEx^20)YdkOgJt`UKUE$k ztP`BC6f9+TN|cxW#kcEou(C+-(X|^yqWc8ro4D5RT(fK^`+DDJeor~;IK-=$KlqZn zAddm+1_$kiAc3BL*A0iqOrJ@a_nB8aj zbu}`nC_~*2E`fn;SlGd6kTfU`n4#qqh)=j&1FDfd3rwDnvJI>UnLsuiD$WS13t(yp zmYYC@vS(A~kYGN@4={hw(jH3ufe|c70!U0)C_(ZDoCk^{5P*gwT6+LgCxPOST)$)3 zk0gM^1iJu4fb%V!htF-KyJ%8Cq3%oh4+Ka~ zWCU{mLFHh11#Tx1^*jUn`jZOKx_$=G99F1VU)xfj%+vFFwLt8W7 ztbQ89xz{#lXGGZAw;M#D;fO7b{({QE!h8XgPq@v+z#yhEleG(&D?#n9SfD;$m|hT# zlrWK)a1~^wQ6k!9AUCPe*iBf|=pb<$N*X26O`x_jHR2F!8hy^WVxLdl_L7;?B{%*& zzmadz0;|Ri1xbG{Dm><}&Xk2mQ5lTPDf2=Rdf-eyNOsl$fs9*SixWZkq=cx3?8546mC5pzx2k^ z*Y%5CrFW&TQ%#!Fr_cXWDezo-bBBoG16__UzP1zmH#3N~EaH8zZS(RJkc*j*pO4y* z0AfMFDUfIigO9fZh<<-|v$bQpjAPg8%V9IOu_v9mQHX(*l}iRW31@&NG|; z;nr?>^@S!s*Xf^1c($diedV7m<^RuF=*};=nPeW`qhX@c`|}e+%D=y@ys+9)$CU)-%>caos~L$l=3|DPM$QKAZ!R!lENS$cA9}fV-}DN zjyI4#5J*~d`UH>yiE$Pb7g!k^o0wUGBoOMr>Qnp!pE8xl@g1x7RN8Uxs%OglfW!Cw zEJCdINlf{?uh|!-Ftay8i$%0t+hio8YL1q$kGh^EVptV~Wn7+pY zm7|5rsa-%Zmc4V{%5B>3wdCPEo|*b4DPLTVuzk62I{ncmot%>G6HQW~`iKo9u)3GF zm&@M-*lLDz)+ICUu-CLbHu;&=(ly6t2s}A|e_6&2pqWgLA)bCgKn4sD6PApIx-jL8 z&^U20|F@Vc_L}Tu(a-1H&*jWg5jom>XyeD-j?8OJT_3P-R9mXnW0u7|J(k7Ee>KaU zT}EtsRw*>UJMnq0ZY<}zBix{H1c#-|p-F{Jb80T>*V?`+r7cx=YQ#V{(Edf&nAgTwwVTB5?44lYi!X{<4>|WqL2%pRjbF)wi~&>p6xJwTFr{?8Dtl zR)_Lft}zLfmpc&Za8v$Wb$-o+Ya4!PN-lb18UfEI<_}5}?^s{g*_OCg@^{v}U!VR* zB<7`O97vyNtF$Fs?>5*E+={D#woe9P6t_d11aTKkPN4Q;wvL6dN0VNo-1k-IMJ~?h z%4Y~E3b|}2m)jQhU=B==)9hP!J#{+4PrdSG(OkusHjVx-%=(4Gs)V3OR;nwynRjoKN7zPSx7-cK*dv zgHjojk8WxS5ngt^V>;8Lv}+|hd3ZTBWww|5w%?UeJubV`nYFukT6{`3Gtgly-m|vM znV}auGgEXazndI?PRwbQ^_=r(-JX2yZr!dID}r6Xnr(cEiLwG0H~ z1E|ni#W;fj5+{>j`d~DYB}hz|EF|5+c_6z%0BV2Iq9B+%7(w*|OdXMKVxh5{VEGOh zUW3kUl!O;3U!ee`h(lt+m4M40I1e6&kT5}Oi-6)66kZEq>Ivr0b$8UH88?Xn)o%bM z*$FUxFdB=4Q2b7W`FxkUPLX0hFnuE8o9J=_Ne|5JAR3GN8R9<9=v`69wo~BTI?mD% z>51t_t4akVxY^X^FAe%&mG=;;pNM>n2xo$I1FU>QiW>s%LDJ>J@CaslCuz1I?7fAq# zNwS+z@+8r17$kqdZ3Hr~IVAb}`YJWwFY$((7q&e<_WFI%4|$0-#&TSnGxKuheqFvF zsz2pF5Fi=B2;}~P%7Nk@rR*cv_GMrY)0oMs26Pdq{Tm0=&kHpROd+`wi3wMME3GXA zsQ{MCv!NU*e^5DCn1j;@NQ{VnD+Bxb z6MLY2*O@>qtWdMS6jH)OV!~B`>_Z07_8+9)!j(pebW;S4-2`$wEW8%MJTXYzhLT1} 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 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 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 zcmZQzfPiYoZYJYZ4ll1HlsbghhkjH}o%X?LZu;RPYzev7MR(5usuDiQB_?)ZUPo^7 zv6&ku&0J`B(bMX|j(zh~xGa>PUU_c*>8?S>lLbi!9z5H={{F7xmea2tUz;I+YpS;8 zh0}LDjb>j5*_5>C)Io@i42&Rph00lpJ#$Ohd^Kl9N3U68eR!3=QKp5cZIAx!t*3K8 z7z33!MBKXe=E{~XmRKwQ%a2rV=BOz>||zPZ?j;3hz?#c+4LCcpJn`3=D+g z3aE}1h?zj@4PQGl&)A@NW*UoX_A8ZdDV*HSN}WDRd7gPEPnu2;HUuh3VGs~I&A_0s z0LTW%8;}Nrq(!G+02v@L&Vu3sD`R7069b48OdXg`@eh2;R3687tlCp)$GxkbDf0sk z-}kc!vDzmw<@3H~U!WGIfY6`-A6JNG5Sf1JT5`oKzS7>Uzw-4aM{&5FDzSK7o*`gi zyiRN8;%lElYM8|M4uEJNV1&BW!Go2zIb-JTcB5AX_xu0umwR}ERp8+@{VE6l{WZ0} ze?9(u-ER9{b^pv$7iTIhE_ofOQph}A^unu9hA#D)^S-w50L=o2ODU@!Z{OS7%l1i} zG@YmL`^&%XX%1dYC*MbDXl|Xnaq|lXekTWD+E@;<8;D^5na#kU8eWjpGjoo+=X?>T z-;*C$uDJ8**6m}VZ|hwP`55hUK$>LFro#1t%>}0U{ukToKkOD!o^2YERli5vZj)Sh z(W+Bfc{|@en|Q$c@4~R}OXk}D{aP%%-HET?;&8~$bJ&vAI>~N+)ah>vYOWsNdy7MS z|2!+@6|Lp>jlA4JYJh+$cDjamfDKdoQ}wS*z0^!=^`A{fQ<}Q0qSybtetL1%7T3O4 z3fqJ)ffZ57&Pb;SuyGU99Ntg)x1(-PEILD%((>yJ#@P_kosGW?E z_+W4d+Ms!?!_UZnb=TV52YmN7&AYQV>7Sd&x}7!*n|JE`*?nnF(hK{PBgV;hpN55( z9%agNV*m8ZEn{ZOksZfAEI7;pG!LA9yQaMkF`qd3uJa1vgpdihJ%zcHmLxBlBbeiK z^}&)hpQ8*6>H-YHei;mmVf{coC~*%gr*A>UI6(Q@(8RzLNex)-OWVukZvt#J!#V4c z8F$!g+8&$y%xdYH<1+-FoWH*;;|9nbjv>C0K|lr!5R<1R3ZxpK>KP&KWN=Ur&5Yuk z68wFSyz)8b8p)ZNr9am-6t_Oyb>6|`Y=Com`nSvHew<&-CE3b&=m{75XWrE}=05M) zx`*>x)SBg{Rq*^!U;lf@dQm}kn-IR6s&_UEv}&?!Oe=qzy&+gP!d?GcIW$i%1KJM` zLzp|DG?+SOe1xg4d)A(~XzK=UGgsGwXQ$O?c?-9yYrMF-;4i~3kRs$f4K^2;r)Nkm z{AJYfsM106q4(##{fn6rg!UG3UOe@|D^v8{#N!e>RGu9Ow`SQf^Mvz4m1&VLB%hye z+GDz5hKSSm)$hEr`GLl=ZqHW8x|HiZ|M2mI_z5Q$Zkklh&#adrYU%tg%051dVLjM^ zpz<05P~sFS2J#m(wA=y<5eyRs263&GO>=;HK;eR6CXxUW6KWf3ID!NrWeKF*1hohD zKvgn=>RFgBBHd&`V>f}q3Km|2&25x~7joJL2RTy2Au(aGfsz+Mg0MIQhY4Dn4kQOk zU*v`t?c7G9n|SWX!qOM5+{Az!PDGb0NdADw5Ric_;_4;E=Xp40a=+n~;m+K&Z~dYA zpWPZO{!RWeLvDxrRgMIx{*?bffMf(CkP9xyfMT%p4{GOt0pWUuL0rZ+?EtiH=>uv4 zwW;6$$(=|{xHNiL!_peqeq7}Rk#35iv710{hlLkB{SOkip_CUSx(U=iphg^`mlxnj z*OzMgmHy$f+>8}}x!*WFPe^Lov6aO!NGzC3{Mz!fEp5=0fh~>xgUZ3e9G*sr=+iK; zuRjLs*USf+18O6{0aC(5V#1}#N~1)&i2>TzqLrJlrqMy-Hk33s!kTsm ziQ6a%FHql`8gYmrE~=>3E)HWug=~wn^+W z5F8r;_8C0NUVz#+@G=={517J|Fj2yfIP;%_`Xt1fk2USkz#dBcfe|c70!U1FNP_%? z3?StWw0%gZT~4CkG3-YYKw`q3L%iFN+Ox#C3ECG0#XE+*NCHSqlHG*TE+={n56K^J 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 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 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 zcmZQzfPhnbl+*S{$RAT_`V;rRbE3?9`f%%nllvfh;`>(d%Zhd49`OSVk2V?03*UBJ%wm{eS)1McbHOWt z*?I>a|4zHiUB54@?0e|axqBJ^)*O6&XsykuYtaEG>yA!&b_T+1HA1X7HE0JiMOzY?V^i!l`eXN{mDTPfk`nWqegDyi3XBF?;mmZ4fsxFc69> zpgJZfW+>2@*cu!Bf7t>_c4xLPrK;8!6l1!Y9|*-t&p7ke?`RpwUknUkCm9$N7XjJe zcmvX40F2LUC=({m3*<9~dP77xRA209kd#f>Dl8+=$XDj$AY^BuY-@Dix93Lh-aF>H zKvnx(UWvMcR1@J2#sF8aQie00LTNUl6LbG;jbd0`sQx<$-V(;d&%oHW0GK8}gZu?j4+Y3<1`qZfsV_5gGm;N&^w?wISQ~Tn z_60f7O&dg$7RyYRjCldlBYQR#t`}@BFt2$$U8uNbfp6Vb>npu-%!_aMolbLLx$*Sp zb~ooXD@m)1%56SwtCulU2A_~r-~S=;>5ux>=ZhvetO#+Jx6QgcgA-^VQ}K`Wuj{qb zHJ*RG#-^=sc*bVt_{_kL7fp{^zuvjNaNcjIr78b`0LX^9n-|Fa2bBYb12fQ%V0VJV zh)5ga9+~Awpy_lWSU*T36u>M2a*>!YSxC6Uc_6z%0BS!ljSE5L7(rzLlufLg5@_ru zP*}mjYp}VElJHst^cN%^paG5)aY#(4BsA~9c`&`u_(H08z)ChszMWBXulGYnS!-pyq%}d>|W_eV{Z7%76X&Rw3X^tdL?)Au~*`M-0{`+wj2f8YQ8|9=hyQKdMA!1q#2 zA~l>Sa4$oPo*J8Y`E1Rc)25m5q4s@-a5eF!oZ-M5eis zVC1mMSxeo4OnJT|<+m`&btj%F98V{|;k^7~9k=nXo8G!e6+G=DrJBN?-16^ip?ZW+ za7&bJ7OgxPxX?l&3yVE>M(0-1DpemXwC>SW*_WF9M%4fWBiSn{N2qcs2wL5Qx9c_7 zHyov<`2y{4_zR?4XI{5wt+SD~&~qJy_H}9>A6YiOE9|e-Bl(S7Q)R0j+%_V37}AsG zjl3UOS?VEYBe-^7QApwTkvFZn=f{tBHPu=#Y+3U26M<2KvR>_fvQ*Ts#g(Lw44K_} z8T&Y3~*b*-nU_mDvdbMdO+r87`T;^A2U&0q3nL4 z#FUhnC0G!gcto>0acc8QPVDt3xx0!_@MiNrz7#Jhm#02)epRDue|ga1-`aO1MA#O!43piyrvt|yZ1nhnXMd4QTwdC6M1ZQW zRYS`1Mm4gceVmL*FDJIQg31YkX3G`8EdUoAjE_qnveisZ=`th=x!NXmZW7{=IpdyB zShB-_+;b+zyas~g1R$Z!I0&^R7r>Fc!Tn$X`Sb@3{EQ2~2d}+aT}6X-4;m_#qI*Ah z_!nITd+tzo6iH7(w>G6;yWm`1hCtul?n2yO0LP|pYvEuG_n?V)W4rs_ti^!|mnL_u zOmXBgZVb>G3dRX&shwBMJl#G9!$k2v9iqWeRGtu~53jwFBjPnWCnf2xFdgf+O}r!^}wM%-c$|Fg0ztuqCu&?rTjioI37q$OgKrN z6dymF`_11?DpmADoN78-CD5f8(Qc=gp7_z}8H9?-1@q!Hz%$2#S%+LF)5UugweN)4 zk%Kioz0sWl#KksIoYK*ecD*9&7HGoU*(OI1!7UIJVi(N!=oKLctb^Snyc8=*Pmh-Kj`qv?BvDW_j!iYANJ%tdLmlcq(odu7@A0qoxnMDkeelc-~+ah zF(eeG9We+;Q_S}smqk^obex~sKiPIz?oz11RA7Dn{xcB20R~zm9S-m*uzzsPB=B?O zoj^#NuacIUNyN%C2*tSWtZCb8&?-pOE?}h zRdI+hVDnw#g;m#_(tCaxKYUyB37>L{PmSg=Y+@I#tvfH^MEH&l4jKnc*6VSP)whCl zqjVEf!9;SxWEDc>G_e0`2%)0^Cb4BABMMe3HSveV3fH-Hbtj=OllW6|BE@|r4Vw=r6B4?} z<7=-g1(kC6L}%Q#c3cSTA+>X0$h_=|MnFf?U3AN)`0&8n^rpr7f?Hkgmx73 zxLajJFTV7Y5euet~t8EUAP&&yruS5%??E-lvd z%vSaKYXL)y+M!H@K2?k$xMZKqyu$88IF>4pE27|q2fdZY-#P0|9ndOEYD-;{E7VLz zH3eI`)tA+*6A<1x6#eo_s1BsJlpF9{5O-={zq*Wt&Od~uoqkmduop^0jz3nWa25|_ zH$WS&wgLbP97o5f2Lf>BqU{@ua}~J%K(o#F1)aV= zj?y`>a`rmywNRgSzS|G_!FwA+nD{fPjMrb9gNT2y7U|NE)9QjHPM(vo-kYr=QJ z?Q7x%pV8+dho7z^NGd5(A-@L^eT_HQ1{HU#I$~+_C+WF=?fRN71y$p2Y_}gZ->mE> zeh+AK1Df^w4FI)CWMSGj+;Mt8ddoC1nN<2)>N%Hj$2jv){k!E`chcTXi#cnY+LXRA zry1wycf##!;st+8OcSQWF{up5m)4n2=P2BlzSklDPGAeOe$Supcx&1D3Jsta!HS&( z_hFV92u&M13-)_BJ$dG0f7%|z@_&Xo>_Le_V=J-6!;dn`wR){vYu{~toPW$pZMaNd z)~du_dl>%L1%#zKen`ba(5&B*0H{3>_8Qa-{5?5eb&U82b&SjfV0T(>SmSh%_ZxGX tv5vnJZeJ6x|D%rSd1a-J=UQje#?C?=*Iyrz`R{ei>OYS;)G@vG{sl%V5&r-H 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 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 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 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 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 zcmZQzfB?Hv{>J=ui{{&0yO2}dt$NFRd0&k9gOZT*cJkiax;l>oRS65o9eVOHBy7^g zL*n~?=;iqS=|968bA0lX$!g&emo%%s`35$9);wU=@Js!B#>sti+zVrEI>Mv!bKdEU`YkO5Z5cLe#cLfA-eXxgU&y zN*vzq*do5zL3*P5&&}@dn=%UQPprSp6wTWC_)_KZTSC)H-|tHOG|4_NwV5GKd6wMK zJ$p7>?rP!7`u+bCD}RMy6aQzInUj9a|xp*z3eW|Db$kA{g(@6S&RDgXYa2E{aR$)28* z*rMz(`&#kM4E}PLhu2e|ty1b*IQ30aiIGU)$;qmxjIU~icPV*1W{-Zn4dNyS210QK zRL2a&Od$1>7ZvRmjBlTPc-xd~%L6z+OKNgCy~+P+F!Ruvu0AQ*i42Tw3xHu@2-O49 z0s^4;1IHmj{R|9jAhjXhMj(9*j|!v3>esg3_Ic;WDen4ZLWxlBoL$A)zJInSNC%5J z+yg2R_b7V-RtqE;!EOMigODi0^h1$aB3j~U_68D7=86Yu0-Cqmy}Mr$FWbLHd;hET zuB+?7-OP0nNv~@)atT8u;sA`s(`AeeW){%Rcm#faq z`f_Riu?`P2mTtbQ!kPDN%{|y{Ie-R&!@}^jBlC<6if5*=sAj)X`If@T?X1-4qm<{F zck-m^1YuB|r!WYJon~OrWCgh!<`58_wCLP3s2FEKaepFyfvzdnSjW?+Q6)#3ewXT9h1a%@y(Y=bSj&Ysa1 z`21GJon^zda~)A`j$b2{W!r@vT{$Wb>s-0Wcx+Qw^wj&Ar}p?xwRyr-Z*Mmh_h z4S@j>@yx*V>q7&w9++MbjchnnoDozm!PF3Gz6=TGgZu#V2QBTP#2*;Jf+T>%goP3$ z&BJ++GLQilj%alg%6={z9IuN{o-zd*cIaszRKCLiEZjhKHy996KQKsLD&7H% z2T*wf(vKV#$Q%@Li0L@v6l^~*PUl0_!NL^GCDKhHGM7uDd%uS&5 z2M0)=MPkCGQPcuOk>dinZO`DA;Cot|<)*-Ug{V`$Ki(AgH#*N;6g54Ey{7s#qw_jg z*^u%d2!Q6m>|z9R|3PhoS5k#+0IV-VeZYi6u=B()GHA}1yuhKWlv=FuJ(VxBbbnXXZ zpb`fbOK!&_9bYFm{GWZ<%r!{tv2KQ}xJX#)>C65X>zfp_JGQKNscSKgJ!XVme#rt5}=H)3M7c(EfaPTP$ zhy?+sihy(qgO9fZh+Yy{!*e5&Rj|l$iQb`2p}Y*UXSDxvQcw8KR{P*c=#*L0d1muJ 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 zI*=cjKm~Ky3vhV%Q7nzw>;4c-jZsUu_`RfZ8O1Q6K;CFIh;8t7%R5}@m z!SM#9(E&&tEGJNVFN`D1$4h^6>agVYWFy%~Ow*b?_u9>9@yl0+9Ej+xC z(`UNNC;7fDQ7yZkCV8dt3x|i~PFCR4j+Rn<$~EJ-+~fNODGKKooR6`eS3K!v*zD6+ z&!@3N-Qe)vdQHpz@Xrd}YZ^-f-|{hpvg=7soxulm7&!dYl+8ZcOnPv#Qu|u|#FpH}jKO{N_5K2T z#XI+0YbuC;&cN8V0GNi1fa;lHo&nPE@FJ+6fq@OAKExYj1Ovmb^r_FSmT1P_e{1&R z!9;HVZOoQ-)*G28T-TelW?!ab9#9E#xG{p=01USm6FpVtP1<7VuU znF62Vx=*}lIPVfTF1^d_V zwIlP44T@)`v8ZOhQu&s`$?dGv>7$hAnRoJ}=>%azpt=+W0kP8z44Pnf1Nm@}wCLO$ zAP3}k&Vu3sD`R6515hRh0&v=a(JB6cPnpW&_>NV3D($#;)iY&&z~TFT79m#qB&K}c z*X#>a#}p766yW0u(h33Tr>-Se%;GEU-TEtEZ*mle+o=+Z*X0=k7RKweW-h+=8LVo? zv<9ehMyOjI{_v@WKYS;mQN^=Lcw%&s@V;-;?x)CrxI+`Y)FN$N)VnaiG{{dNU6 zy8bm)rrn+|ZkE-dTp4%d2&;v+MhH9~{n@>A`Zc+ChI2pWU2y$X|26X(=SknPP{qS@E3StI^BQL)dto|1%yJ`#XrQT;I?UB12c>hc-5qF5= zUe2s>xDcoWB^`j>089tB-(St%Xn*^Gs!^JwJVQ03{uYfTr?zk!8HH#~U#7%;{&lGC z*TP@Z?)cwcn>=sBF4x9|zBf4KV$!bjc=^261g8NO=lmEN(&R0R}*6i5ZwL%wYmV_@6=QQt=L;9#9y9^ux>r z8-c_@V!}*^q+K`<76(xKf%Vd6s2n4xyoaeH(oG>Wb`!|2u<#meZlff;K=}YAu8=rL zOjvB-j6-mkptUU^c?=SlV6%vbOJEsGbl!qF3YN!^-3v2!h|w6fzlrwAbA#v z371Au3lv3;3*=AbnPIX3J~c@XYe6|rNQTtY?mmG}oYrEYxdX9$CunDQS8 zfNYpuj6m)`s2nU`SVH*>MAT0V?Ca7pPvLMjWD-BjBi<`QIR}qI2K!4cE)^%1Vka7L`AF zx2`s7Q;Wk(dojLElF;}jrmi!As%0Re%?GRNK#e|7oS~#eBn}c2W;)Jt5^O&(jV^(z zL^ddjgs)9UAc~0zJk*O5#dEje*%`U2-bB(*b7S6urx%wFhQ#8 z$Z!)V4Z;CZ218=PrBT!ZMRC@3Cb|owWLU2~;h!`~pwbP%WH13pqW~-pLh(Bh=Fbv%5=Dyn49IP7qQ{t!$_r!zZR*J}7(Jz(X&^x@VCC;7vhL)~SL1XS;9V!33~ z4OHU5mcjje@BNh~SD&g*oBd_+VJ9!9?4Oc1FZO=2ogJ;3uFo&X@BZP}jk%l#K0E6+ z82>JxAfnZ`+h6pU_W{urlJ^+IRvy&-`um>u<+*&0JFlzO|2$yP8kb|lbEHS$(E11O z>yJ(|b-Zhkmu^=*mD$*J$t@AH6%qX}C1ek*XSRL7RXmG9wAGsT!M4rIQ$Q|eK7Qfg zGXoF{0!|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^lK+FVE?_>Hw`&iA<1(V9}wOet06b+f{nqyk(bVGQ?UH)^14^J>Kwk-gL0m#oF zJum={KN#mkWWuKU(50KhH_wyUIltlMtoDqi7Z<1BcoezE%f^D;5~NP{Y${ASBiLME z+#bEKf5W7+BKx~Df+n0WaOn6P(4Mh$!>3EfTTHy_7RU*?c0Lz8S){hcqi_E%`R8wM zEer@fX;ab0=4N&@R*8EG+z*DY9hqlrP&_k@MK$}C%C{6wZfB)VAEi9cyptzQCkPt? zElyz&5IfDlpvewoqqse3(YcRMG0uYG0xM%OAbzGKy%N;~dd z^-P%`aQMETMTpfti7B7=HTwd!Fa?AL1^Bo^G=s?WQ`eF!X7QEwZvB<7H#v&K?No`y z>+%c%3*&WKGZ$a`3{u1FZhZhm0|6t{tqwL?8a2n}-EfW<+~5=S-u#)NwV;l+^kFIO zkEt7XP104X_nakSyZrEjNh~wlTV`wwxg?&PMr+2Hqt&xw^ZqJQZN!oi0s6^bO>;+gYkYEJ60hkWjJJuAf zJtE?}We)eGFolIun`7(_u|%gWewn}M(7fjl12Db^qMXy2OSlAy>mH zbJ?F1K2PC%;K>a%i}~4K>1eI&SKBPR`QIM+60`0@+K(@sud5I6Jo#a_OLJ#D)Y6py zKmf8E%4Y;}|3TTHFk=R$bt@>Jfrxa)z`lN-1~hGf!h#j17evD>L1n^Kfa3_xgT(>V zeqdR71S-b}DuZC^h;!52BQ$ms$nCK38fLV0 zFUfy)^`y04{mq6aM;^6mulJe#ZIyGfHZ+dGH4TuBlzgBvh%h&Wi4ZKmfq9PSj;sZ= z3`b9M$d=%;4`vmJhLjVctzi3s{#yx^L$l*kE8wklCNO6Lk z58=W$7t}Mg6m2!it50(@@HCYF{oePYE?Y;fM|rQTGvkl@T~=;b$cSR3&^a)2@C=!G9n7 z&k?(49kY9*ps`R&UT#~W`CHrVYg*g)zIz$WtSZ^QLp|_DWc#i!>TG|iXCCfSyI4{g z+weK()jg0+NsG>&gV@Nx2%@hRW!_8*`0km@KKs!NQz5hW|6g35qsO*?^+qRo@jvM@ zKqU^x7gjCZrM}R*FlpBN-?<#$y-rRJ`NH#cm6(dNuJHW4thMb%vyyc6U!C7Q%FNmPMMFHeU*^u=`24Dd3S@q?{1Z@v{XK@--AK4)rR-Mw#~~^KrUuJe&OJY zHy{=SoGJp+DGWZ|4j_6-U=7cWNLIlj$0d4)Hihys%%0Ky%Sk=qJ6r98BcW4fP3M`- z|8Q%!y!t|upX>BbB|O_w*1q!3mh%7SEOh4=+)OeL@6j;P>HYbMA?4rS)S#FKF4@y_ z5?hoVW?w75nZaM~^6+}71HbGf@$y z2W&ntjwes|D*pfQ_5V|sw@K~a)8V@--R?2>>)c%;V4; z$0%Ft#lK`o$Wk9@!soe-YHrz`zF58{%yU_QH{u-wIa$i9y#a zL99>P^jSVV*I{HR4F4z1GiS@yGVKEajZ-~be0EMRzj0_&`pLtWQ}ot$@BqyM`}fp0 z*065TF2!Q$n}y;p|1)}f-AUQ)w(P>aLmK{jTD_(+FmUf+VAg75U@q+h*$;CF8Vw3R z5a5MS3``;1PryvV=7W_nnc3BY7~pUW4R&z`aX4h>Ic&*lon$vZ>h!k-HCK=Cy~QEE zf1Z`{iq`V`Mqch9H9$ZWJ6%IOz=o;)srpx@UTUVb`p+h#DNS8g(d&O+KfO3>i)-I2 zg>Ax@z>273XQWdE*tm&l4)3S@+flbC@<43hzcX!0$0h17oMTbYX`U8Vc*FTP)J{f7 zd@wkyHWQq`)H<`{u+%{xuGD8sU88OqcKtNhw{{^UOPW z(sY6_D1oLh2#B3#V9*5RNtC!xT6FFkRE)ErxWLNT*x1AXC;%0M(<%OePnpW&_>NV3 zD($#;)iY&&z~TFT79m#qB&K}c*X#>a$P^G76yW0u(gFeLr>-Se%;GEU-TEtEZ*mle z+o=+Z*X0=k7RKweW-h+=8KjCyjPC$MH3K8mtqyEIJFoZpbost9T=rI2F+9mVA}#)` zs@YAYe*fuwxkXhAT+F>Dgjh1CF62A*Q2mM4w+BAq;R`2br`^7`NaT?zybPGkD99u1 zvF*c!U0SuD{=Ap?k&|cZ%k|1&RYykag`Y>Ipk)9kd|>Gw_@Cn{|B$xsZMB_E*3nGJUV$@mKcGM3fjcRGUKdt3hV5 zCQbO*wm$1@@wcs7gkl`8F_J#JNf4 z3XR~$PF*rxs5b8iM{9B z4ohFOauWk`I1yd0Ao&9xLqG<$h}%0)woH=QEN5y zd(|GQKjl9VAQ{03*pSU)-UWpO`!H993Z(9i3yj+RbGJY z2d1@?P?ad@pGY?)(AZ5_%ZowcHcG+^)JC929HN&O;HW&m;=e=p{m@&7So~S~f~39g zPn7)Hopve2&yHb2d4i)9B)%DSplKAA7kQD=C@wZE?V3Z=3C(p4J%HmpUr!*XF9SoB-x1*mwc5dr{h2MA!>VJMg$gHV146k^mADW<3e*K%_Pb8E!(c8;OI& zB-u?2hH=@el$sE5bpEEH;vEKaYN!YN(0mc@hqix{rACvF2GC*h7gwFoFe10Er0?MpE)D z3s4tI-XYQN81^FxATddH8{7{>xCi8Z=+F$|aaEpW`&iPu7lX!RHvln)8!-exYLN0V 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 07/34] 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 08/34] 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 09/34] 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 10/34] 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 11/34] 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 12/34] #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 13/34] 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 14/34] #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 15/34] 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 16/34] 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 17/34] 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 18/34] 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 19/34] 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 20/34] 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 21/34] 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 22/34] 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 23/34] 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 24/34] 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 25/34] 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 26/34] #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 27/34] 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 28/34] 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 29/34] 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 30/34] 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 31/34] 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 32/34] 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 33/34] 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 34/34] 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