Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preprocess _muxed_id fields before unmarshalling from the DB #3722

Merged
merged 11 commits into from
Jun 25, 2021
6 changes: 3 additions & 3 deletions protocols/horizon/operations/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand All @@ -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"`
}

Expand Down
6 changes: 6 additions & 0 deletions services/horizon/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
33 changes: 29 additions & 4 deletions services/horizon/internal/db2/history/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"strings"
"text/template"

sq "github.com/Masterminds/squirrel"
Expand All @@ -24,13 +25,37 @@ 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we do the same thing for effects? I'll update an integration test to generate a trade.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, it is working because we are lucky:

  • account_muxed_id is done in PopulateBaseEffect so it's not unmarshaling directly to other struct.
  • seller_muxed_id is never populated during ingestion, even when seller is a muxed account. This is likely ingestion bug or we just can't extract this information during ingestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* `seller_muxed_id` is never populated during ingestion, even when seller is a muxed account. This is likely ingestion bug or we just can't extract this information during ingestion.

I think it is populated (see addAccountAndMuxedAccountDetails(sd, buyer, "seller") in tradeDetails()).

The reason I think it works is because we use an intermediate datastructure (history.TradeEffectDetails, which expects an uint64) before copying the fields to the final effects.Trade data structure (which does have the ,string json tag) in when unmarshalling the details from the database in NewEffect().

This BTW confirms that we don't need a ,string tag in services/horizon/internal/db2/history/main.go (which I wanted to check in #3722 (comment) ).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@2opremio you are correct. I missed it when checking my test results because I forgot there are always two trade effects for each trade.

if err != nil {
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) {
var dest map[string]interface{}
// 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") {
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
dest[k] = vNumber.String()
}
}
}
return json.Marshal(dest)
}

var feeStatsQueryTemplate = template.Must(template.New("trade_aggregations_query").Parse(`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package integration

import (
"math"
"testing"

"github.com/stellar/go/clients/horizonclient"
Expand All @@ -27,15 +28,16 @@ func TestMuxedAccountDetails(t *testing.T) {
source := xdr.MuxedAccount{
Type: xdr.CryptoKeyTypeKeyTypeMuxedEd25519,
Med25519: &xdr.MuxedAccountMed25519{
Id: 0xcafebabe,
Id: 0xcafebabecafebabe,
Ed25519: *masterAcID.Ed25519,
},
}

destination := xdr.MuxedAccount{
Type: xdr.CryptoKeyTypeKeyTypeMuxedEd25519,
Med25519: &xdr.MuxedAccountMed25519{
Id: 0xdeadbeef,
// Make sure we cover the full uint64 range
Id: math.MaxUint64,
Ed25519: *destinationAcID.Ed25519,
},
}
Expand Down
125 changes: 125 additions & 0 deletions services/horizon/internal/integration/muxed_operations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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",
},
// 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",
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(),
},
}
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.Payment{
SourceAccount: sponsoredMuxed.Address(),
Destination: master.Address(),
Amount: "3",
Asset: txnbuild.CreditAsset{"ABCD", master.Address()},
},
&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})
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")
}
4 changes: 2 additions & 2 deletions txnbuild/account_merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Comment on lines 63 to 71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the order here to be able to merge account into muxed destination. This should be probably a part of txnbuild patch release. @stellar/horizon-committers

Expand Down
2 changes: 1 addition & 1 deletion txnbuild/account_merge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down