From 2223361e6bca3c9d24a9fd13dfef609b3d668821 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Fri, 25 Jun 2021 12:09:48 +0200 Subject: [PATCH 01/11] Preprocess _muxed_id fields before unmarshalling from the DB --- protocols/horizon/operations/main.go | 4 +-- .../horizon/internal/db2/history/operation.go | 29 +++++++++++++++++-- .../ingest/processors/operations_processor.go | 4 +++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/protocols/horizon/operations/main.go b/protocols/horizon/operations/main.go index 10d20a559d..98cc49eeaf 100644 --- a/protocols/horizon/operations/main.go +++ b/protocols/horizon/operations/main.go @@ -99,10 +99,10 @@ type Payment struct { base.Asset From string `json:"from"` FromMuxed string `json:"from_muxed,omitempty"` - FromMuxedID uint64 `json:"from_muxed_id,omitempty"` + FromMuxedID uint64 `json:"from_muxed_id,omitempty,string"` To string `json:"to"` ToMuxed string `json:"to_muxed,omitempty"` - ToMuxedID uint64 `json:"to_muxed_id,omitempty"` + ToMuxedID uint64 `json:"to_muxed_id,omitempty,string"` Amount string `json:"amount"` } diff --git a/services/horizon/internal/db2/history/operation.go b/services/horizon/internal/db2/history/operation.go index a70eaf47d5..f7ad258f98 100644 --- a/services/horizon/internal/db2/history/operation.go +++ b/services/horizon/internal/db2/history/operation.go @@ -4,6 +4,8 @@ import ( "bytes" "context" "encoding/json" + "fmt" + "strings" "text/template" sq "github.com/Masterminds/squirrel" @@ -24,8 +26,11 @@ func (r *Operation) UnmarshalDetails(dest interface{}) error { if !r.DetailsString.Valid { return nil } - - err := json.Unmarshal([]byte(r.DetailsString.String), &dest) + preprocessedDetails, err := preprocessDetails(r.DetailsString.String) + if err != nil { + err = errors.Wrap(err, "error in unmarshal") + } + err = json.Unmarshal(preprocessedDetails, &dest) if err != nil { err = errors.Wrap(err, "error in unmarshal") } @@ -33,6 +38,26 @@ func (r *Operation) UnmarshalDetails(dest interface{}) error { return err } +func preprocessDetails(details string) ([]byte, error) { + var dest map[string]interface{} + err := json.Unmarshal([]byte(details), &dest) + if err != nil { + return nil, err + } + for k, v := range dest { + if strings.HasSuffix(k, "_muxed_id") { + // encoding/json uses a float64 representation for numbers if interface{} is used as the destination + if vFloat, ok := v.(float64); ok { + // transform it into a string so that _muxed_id unmarshalling works with `,string` tags + // see https://github.com/stellar/go/pull/3716#issuecomment-867057436 + vIntStringed := fmt.Sprintf("%d", uint64(vFloat)) + dest[k] = vIntStringed + } + } + } + return json.Marshal(dest) +} + var feeStatsQueryTemplate = template.Must(template.New("trade_aggregations_query").Parse(` {{define "operation_count"}}(CASE WHEN new_max_fee IS NULL THEN operation_count ELSE operation_count + 1 END){{end}} SELECT diff --git a/services/horizon/internal/ingest/processors/operations_processor.go b/services/horizon/internal/ingest/processors/operations_processor.go index 010f4b5b88..594572461b 100644 --- a/services/horizon/internal/ingest/processors/operations_processor.go +++ b/services/horizon/internal/ingest/processors/operations_processor.go @@ -216,6 +216,10 @@ func addAccountAndMuxedAccountDetails(result map[string]interface{}, a xdr.Muxed result[prefix] = accid.Address() if a.Type == xdr.CryptoKeyTypeKeyTypeMuxedEd25519 { result[prefix+"_muxed"] = a.Address() + // _muxed_id fields should had ideally been stored in the DB as a string instead of uint64 + // due to Javascript not being able to handle them, see https://github.com/stellar/go/issues/3714 + // However, we released this code in the wild before correcting it. Thus, what we do is + // work around it (by preprocessing it into a string) in Operation.UnmarshalDetails() result[prefix+"_muxed_id"] = uint64(a.Med25519.Id) } } From 2f337fa7ed072dbc3f853d995653f8f7a3024483 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Fri, 25 Jun 2021 12:28:54 +0200 Subject: [PATCH 02/11] Extend test case to make it fail --- .../internal/integration/muxed_account_details_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/horizon/internal/integration/muxed_account_details_test.go b/services/horizon/internal/integration/muxed_account_details_test.go index 98bc0c3503..d9f7933794 100644 --- a/services/horizon/internal/integration/muxed_account_details_test.go +++ b/services/horizon/internal/integration/muxed_account_details_test.go @@ -27,7 +27,7 @@ func TestMuxedAccountDetails(t *testing.T) { source := xdr.MuxedAccount{ Type: xdr.CryptoKeyTypeKeyTypeMuxedEd25519, Med25519: &xdr.MuxedAccountMed25519{ - Id: 0xcafebabe, + Id: 0xcafebabecafebabe, Ed25519: *masterAcID.Ed25519, }, } @@ -35,7 +35,7 @@ func TestMuxedAccountDetails(t *testing.T) { destination := xdr.MuxedAccount{ Type: xdr.CryptoKeyTypeKeyTypeMuxedEd25519, Med25519: &xdr.MuxedAccountMed25519{ - Id: 0xdeadbeef, + Id: 0xdeadbeefdeadbeef, Ed25519: *destinationAcID.Ed25519, }, } From 869f90caee78da9deb7daae2ae0f9824f4c200f5 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Fri, 25 Jun 2021 12:41:08 +0200 Subject: [PATCH 03/11] Change JSON decoder to use json.Number instead of float64 --- services/horizon/internal/db2/history/operation.go | 14 +++++++------- .../integration/muxed_account_details_test.go | 4 +++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/services/horizon/internal/db2/history/operation.go b/services/horizon/internal/db2/history/operation.go index f7ad258f98..e8bc682966 100644 --- a/services/horizon/internal/db2/history/operation.go +++ b/services/horizon/internal/db2/history/operation.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "fmt" "strings" "text/template" @@ -40,18 +39,19 @@ func (r *Operation) UnmarshalDetails(dest interface{}) error { func preprocessDetails(details string) ([]byte, error) { var dest map[string]interface{} - err := json.Unmarshal([]byte(details), &dest) - if err != nil { + // Create a decoder using Number instead of float64 when decoding + // (so that decoding covers the full uint64 range) + decoder := json.NewDecoder(strings.NewReader(details)) + decoder.UseNumber() + if err := decoder.Decode(&dest); err != nil { return nil, err } for k, v := range dest { if strings.HasSuffix(k, "_muxed_id") { - // encoding/json uses a float64 representation for numbers if interface{} is used as the destination - if vFloat, ok := v.(float64); ok { + if vNumber, ok := v.(json.Number); ok { // transform it into a string so that _muxed_id unmarshalling works with `,string` tags // see https://github.com/stellar/go/pull/3716#issuecomment-867057436 - vIntStringed := fmt.Sprintf("%d", uint64(vFloat)) - dest[k] = vIntStringed + dest[k] = vNumber.String() } } } diff --git a/services/horizon/internal/integration/muxed_account_details_test.go b/services/horizon/internal/integration/muxed_account_details_test.go index d9f7933794..3ab7029399 100644 --- a/services/horizon/internal/integration/muxed_account_details_test.go +++ b/services/horizon/internal/integration/muxed_account_details_test.go @@ -1,6 +1,7 @@ package integration import ( + "math" "testing" "github.com/stellar/go/clients/horizonclient" @@ -35,7 +36,8 @@ func TestMuxedAccountDetails(t *testing.T) { destination := xdr.MuxedAccount{ Type: xdr.CryptoKeyTypeKeyTypeMuxedEd25519, Med25519: &xdr.MuxedAccountMed25519{ - Id: 0xdeadbeefdeadbeef, + // Make sure we cover the full uint64 range + Id: math.MaxUint64, Ed25519: *destinationAcID.Ed25519, }, } From 3ee288ca4f9bfbe67ceb59845c704b32dac706f7 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Fri, 25 Jun 2021 12:46:10 +0200 Subject: [PATCH 04/11] Add missing string tag --- protocols/horizon/operations/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protocols/horizon/operations/main.go b/protocols/horizon/operations/main.go index 98cc49eeaf..6323d9ae50 100644 --- a/protocols/horizon/operations/main.go +++ b/protocols/horizon/operations/main.go @@ -88,7 +88,7 @@ type CreateAccount struct { StartingBalance string `json:"starting_balance"` Funder string `json:"funder"` FunderMuxed string `json:"funder_muxed,omitempty"` - FunderMuxedID uint64 `json:"funder_muxed_id,omitempty"` + FunderMuxedID uint64 `json:"funder_muxed_id,omitempty,string"` Account string `json:"account"` } From c4c2049519f8640457cdc807e14ac83b7081b761 Mon Sep 17 00:00:00 2001 From: Bartek Nowotarski Date: Fri, 25 Jun 2021 12:50:32 +0200 Subject: [PATCH 05/11] Add simple integration test --- .../integration/muxed_operations_test.go | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 services/horizon/internal/integration/muxed_operations_test.go diff --git a/services/horizon/internal/integration/muxed_operations_test.go b/services/horizon/internal/integration/muxed_operations_test.go new file mode 100644 index 0000000000..fca73b3991 --- /dev/null +++ b/services/horizon/internal/integration/muxed_operations_test.go @@ -0,0 +1,98 @@ +package integration + +import ( + "testing" + + "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/keypair" + "github.com/stellar/go/services/horizon/internal/test/integration" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" +) + +func TestMuxedOperations(t *testing.T) { + itest := integration.NewTest(t, integration.Config{ProtocolVersion: 17}) + + sponsored := keypair.MustRandom() + // Is there an easier way? + sponsoredMuxed := xdr.MustMuxedAddress(sponsored.Address()) + sponsoredMuxed.Type = xdr.CryptoKeyTypeKeyTypeMuxedEd25519 + sponsoredMuxed.Med25519 = &xdr.MuxedAccountMed25519{ + Ed25519: *sponsoredMuxed.Ed25519, + Id: 100, + } + + master := itest.Master() + masterMuxed := xdr.MustMuxedAddress(master.Address()) + masterMuxed.Type = xdr.CryptoKeyTypeKeyTypeMuxedEd25519 + masterMuxed.Med25519 = &xdr.MuxedAccountMed25519{ + Ed25519: *masterMuxed.Ed25519, + Id: 200, + } + + ops := []txnbuild.Operation{ + &txnbuild.BeginSponsoringFutureReserves{ + SponsoredID: sponsored.Address(), + }, + &txnbuild.CreateAccount{ + Destination: sponsored.Address(), + Amount: "100", + }, + &txnbuild.ChangeTrust{ + SourceAccount: sponsoredMuxed.Address(), + Line: txnbuild.CreditAsset{"ABCD", master.Address()}, + Limit: txnbuild.MaxTrustlineLimit, + }, + &txnbuild.ManageSellOffer{ + SourceAccount: sponsoredMuxed.Address(), + Selling: txnbuild.NativeAsset{}, + Buying: txnbuild.CreditAsset{"ABCD", master.Address()}, + Amount: "3", + Price: "1", + }, + &txnbuild.ManageData{ + SourceAccount: sponsoredMuxed.Address(), + Name: "test", + Value: []byte("test"), + }, + &txnbuild.Payment{ + SourceAccount: sponsoredMuxed.Address(), + Destination: master.Address(), + Amount: "1", + Asset: txnbuild.NativeAsset{}, + }, + &txnbuild.CreateClaimableBalance{ + SourceAccount: sponsoredMuxed.Address(), + Amount: "2", + Asset: txnbuild.NativeAsset{}, + Destinations: []txnbuild.Claimant{ + txnbuild.NewClaimant(keypair.MustRandom().Address(), nil), + }, + }, + &txnbuild.EndSponsoringFutureReserves{ + SourceAccount: sponsored.Address(), + }, + // This with: + // > Field: Destination, Error: invalid version byte + // > validation failed for *txnbuild.AccountMerge operation + // &txnbuild.AccountMerge{ + // SourceAccount: sponsoredMuxed.Address(), + // Destination: masterMuxed.Address(), + // }, + } + txResp, err := itest.SubmitMultiSigOperations(itest.MasterAccount(), []*keypair.Full{master, sponsored}, ops...) + assert.NoError(t, err) + assert.True(t, txResp.Successful) + + // Check if no 5xx after processing the tx above + // TODO expand it to test actual muxed fields + _, err = itest.Client().Operations(horizonclient.OperationRequest{Limit: 200}) + assert.NoError(t, err, "/operations failed") + + _, err = itest.Client().Payments(horizonclient.OperationRequest{Limit: 200}) + assert.NoError(t, err, "/payments failed") + + _, err = itest.Client().Effects(horizonclient.EffectRequest{Limit: 200}) + assert.NoError(t, err, "/effects failed") +} From dc997caaf3776dbb770c857dff48ad82859572f3 Mon Sep 17 00:00:00 2001 From: Bartek Nowotarski Date: Fri, 25 Jun 2021 13:07:04 +0200 Subject: [PATCH 06/11] Fix test --- .../integration/muxed_operations_test.go | 35 +++++++++++++++---- txnbuild/account_merge.go | 4 +-- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/services/horizon/internal/integration/muxed_operations_test.go b/services/horizon/internal/integration/muxed_operations_test.go index fca73b3991..0e0ff18fb7 100644 --- a/services/horizon/internal/integration/muxed_operations_test.go +++ b/services/horizon/internal/integration/muxed_operations_test.go @@ -73,18 +73,39 @@ func TestMuxedOperations(t *testing.T) { &txnbuild.EndSponsoringFutureReserves{ SourceAccount: sponsored.Address(), }, - // This with: - // > Field: Destination, Error: invalid version byte - // > validation failed for *txnbuild.AccountMerge operation - // &txnbuild.AccountMerge{ - // SourceAccount: sponsoredMuxed.Address(), - // Destination: masterMuxed.Address(), - // }, } txResp, err := itest.SubmitMultiSigOperations(itest.MasterAccount(), []*keypair.Full{master, sponsored}, ops...) assert.NoError(t, err) assert.True(t, txResp.Successful) + ops = []txnbuild.Operation{ + // Remove subentries to be able to merge account + &txnbuild.ManageSellOffer{ + SourceAccount: sponsoredMuxed.Address(), + Selling: txnbuild.NativeAsset{}, + Buying: txnbuild.CreditAsset{"ABCD", master.Address()}, + Amount: "0", + Price: "1", + OfferID: 1, + }, + &txnbuild.ChangeTrust{ + SourceAccount: sponsoredMuxed.Address(), + Line: txnbuild.CreditAsset{"ABCD", master.Address()}, + Limit: "0", + }, + &txnbuild.ManageData{ + SourceAccount: sponsoredMuxed.Address(), + Name: "test", + }, + &txnbuild.AccountMerge{ + SourceAccount: sponsoredMuxed.Address(), + Destination: masterMuxed.Address(), + }, + } + txResp, err = itest.SubmitMultiSigOperations(itest.MasterAccount(), []*keypair.Full{master, sponsored}, ops...) + assert.NoError(t, err) + assert.True(t, txResp.Successful) + // Check if no 5xx after processing the tx above // TODO expand it to test actual muxed fields _, err = itest.Client().Operations(horizonclient.OperationRequest{Limit: 200}) diff --git a/txnbuild/account_merge.go b/txnbuild/account_merge.go index ddee989b4c..4228b1bfda 100644 --- a/txnbuild/account_merge.go +++ b/txnbuild/account_merge.go @@ -63,9 +63,9 @@ func (am *AccountMerge) FromXDR(xdrOp xdr.Operation, withMuxedAccounts bool) err func (am *AccountMerge) Validate(withMuxedAccounts bool) error { var err error if withMuxedAccounts { - _, err = xdr.AddressToAccountId(am.Destination) - } else { _, err = xdr.AddressToMuxedAccount(am.Destination) + } else { + _, err = xdr.AddressToAccountId(am.Destination) } if err != nil { return NewValidationError("Destination", err.Error()) From 7b351b43b973efd6bde92e31fd77ec16fcb45587 Mon Sep 17 00:00:00 2001 From: Bartek Nowotarski Date: Fri, 25 Jun 2021 13:54:46 +0200 Subject: [PATCH 07/11] Add trade case --- .../integration/muxed_operations_test.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/services/horizon/internal/integration/muxed_operations_test.go b/services/horizon/internal/integration/muxed_operations_test.go index 0e0ff18fb7..243d323aba 100644 --- a/services/horizon/internal/integration/muxed_operations_test.go +++ b/services/horizon/internal/integration/muxed_operations_test.go @@ -51,6 +51,14 @@ func TestMuxedOperations(t *testing.T) { Amount: "3", Price: "1", }, + // This will generate a trade effect: + &txnbuild.ManageSellOffer{ + SourceAccount: masterMuxed.Address(), + Selling: txnbuild.CreditAsset{"ABCD", master.Address()}, + Buying: txnbuild.NativeAsset{}, + Amount: "3", + Price: "1", + }, &txnbuild.ManageData{ SourceAccount: sponsoredMuxed.Address(), Name: "test", @@ -80,13 +88,11 @@ func TestMuxedOperations(t *testing.T) { ops = []txnbuild.Operation{ // Remove subentries to be able to merge account - &txnbuild.ManageSellOffer{ + &txnbuild.Payment{ SourceAccount: sponsoredMuxed.Address(), - Selling: txnbuild.NativeAsset{}, - Buying: txnbuild.CreditAsset{"ABCD", master.Address()}, - Amount: "0", - Price: "1", - OfferID: 1, + Destination: master.Address(), + Amount: "3", + Asset: txnbuild.CreditAsset{"ABCD", master.Address()}, }, &txnbuild.ChangeTrust{ SourceAccount: sponsoredMuxed.Address(), From 352da561721ef489351a0c0bb5fe2cfd63f51fbd Mon Sep 17 00:00:00 2001 From: Bartek Nowotarski Date: Fri, 25 Jun 2021 13:55:38 +0200 Subject: [PATCH 08/11] Fix unused variable --- services/horizon/internal/db2/history/operation.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/horizon/internal/db2/history/operation.go b/services/horizon/internal/db2/history/operation.go index e8bc682966..f0de41e563 100644 --- a/services/horizon/internal/db2/history/operation.go +++ b/services/horizon/internal/db2/history/operation.go @@ -27,14 +27,14 @@ func (r *Operation) UnmarshalDetails(dest interface{}) error { } preprocessedDetails, err := preprocessDetails(r.DetailsString.String) if err != nil { - err = errors.Wrap(err, "error in unmarshal") + return errors.Wrap(err, "error in unmarshal") } err = json.Unmarshal(preprocessedDetails, &dest) if err != nil { - err = errors.Wrap(err, "error in unmarshal") + return errors.Wrap(err, "error in unmarshal") } - return err + return nil } func preprocessDetails(details string) ([]byte, error) { From 699e70bd738fab27348439ddce0c4528445a893a Mon Sep 17 00:00:00 2001 From: Bartek Nowotarski Date: Fri, 25 Jun 2021 13:59:44 +0200 Subject: [PATCH 09/11] Fix txnbuild test --- txnbuild/account_merge_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/txnbuild/account_merge_test.go b/txnbuild/account_merge_test.go index 7d1e21ff32..d221059bd5 100644 --- a/txnbuild/account_merge_test.go +++ b/txnbuild/account_merge_test.go @@ -23,7 +23,7 @@ func TestAccountMergeValidate(t *testing.T) { }, ) if assert.Error(t, err) { - expected := "invalid address" + expected := "minimum valid length is 5" assert.Contains(t, err.Error(), expected) } } From b49970df5b5ce239edf4dbebaed069916bc45a15 Mon Sep 17 00:00:00 2001 From: Bartek Nowotarski Date: Fri, 25 Jun 2021 14:02:39 +0200 Subject: [PATCH 10/11] CHANGELOG --- services/horizon/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index bc8f35c7cf..1219ceb2fc 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -10,6 +10,12 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). **Upgrading to this version from <= v2.1.1 will trigger a state rebuild. During this process (which can take up to 20 minutes), Horizon will not ingest new ledgers.** +* Fix a bug in the method unmarshaling payment operation details. ([#3722](https://github.com/stellar/go/pull/3722)) + +## v2.5.1 + +**Upgrading to this version from <= v2.1.1 will trigger a state rebuild. During this process (which can take up to 20 minutes), Horizon will not ingest new ledgers.** + * Fix for Stellar-Core 17.1.0 bug that can potentially corrupt Captive-Core storage dir. * All muxed ID fields are now represented as strings. This is to support JS that may not handle uint64 values in JSON responses properly. From c0145722f89af457ef22f96fdb81372c5a730d84 Mon Sep 17 00:00:00 2001 From: Bartek Nowotarski Date: Fri, 25 Jun 2021 14:58:15 +0200 Subject: [PATCH 11/11] fixes --- services/horizon/CHANGELOG.md | 2 +- .../internal/integration/muxed_operations_test.go | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/services/horizon/CHANGELOG.md b/services/horizon/CHANGELOG.md index 1219ceb2fc..e372c7dc42 100644 --- a/services/horizon/CHANGELOG.md +++ b/services/horizon/CHANGELOG.md @@ -6,7 +6,7 @@ file. This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased -## v2.5.1 +## v2.5.2 **Upgrading to this version from <= v2.1.1 will trigger a state rebuild. During this process (which can take up to 20 minutes), Horizon will not ingest new ledgers.** diff --git a/services/horizon/internal/integration/muxed_operations_test.go b/services/horizon/internal/integration/muxed_operations_test.go index 243d323aba..e153a547b1 100644 --- a/services/horizon/internal/integration/muxed_operations_test.go +++ b/services/horizon/internal/integration/muxed_operations_test.go @@ -5,6 +5,7 @@ import ( "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/keypair" + "github.com/stellar/go/protocols/horizon/effects" "github.com/stellar/go/services/horizon/internal/test/integration" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" @@ -120,6 +121,14 @@ func TestMuxedOperations(t *testing.T) { _, err = itest.Client().Payments(horizonclient.OperationRequest{Limit: 200}) assert.NoError(t, err, "/payments failed") - _, err = itest.Client().Effects(horizonclient.EffectRequest{Limit: 200}) + effectsPage, err := itest.Client().Effects(horizonclient.EffectRequest{Limit: 200}) assert.NoError(t, err, "/effects failed") + + for _, effect := range effectsPage.Embedded.Records { + if effect.GetType() == "trade" { + trade := effect.(effects.Trade) + oneSet := trade.AccountMuxedID != 0 || trade.SellerMuxedID != 0 + assert.True(t, oneSet, "at least one of account_muxed_id, seller_muxed_id must be set") + } + } }