Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ file. This project adheres to [Semantic Versioning](http://semver.org/).
### Fixed
- Fixed the history lookup-table reaper deleting rows (e.g. `history_accounts`) that live ingestion was concurrently inserting references to, which left dangling references and could make an account's history endpoints return 404 or silently omit records until a reingest ([#222](https://github.com/stellar/stellar-horizon/pull/222)).

### Changed
- Stopped requiring a `to_muxed_id`, and an exact entry count, in the data map of a V4 Stellar Asset Contract event. A transfer with no muxed destination may express that by omitting the field — as a CAP-0086 sparse map (protocol 28) would — or by binding the key to `Void`, and unrecognized keys are now ignored rather than rejected. This is forward-compat hardening: the Stellar Asset Contract emits a map only when a muxed destination exists, and always with both keys, so no event in ledger history today is affected and no reingestion is needed. It matters because every caller discards an event that fails to parse instead of surfacing the error, so a shape the parser rejects silently costs that event its `account_credited`/`account_debited` effects, its `balance_changes` operation details, and its participants ([#223](https://github.com/stellar/stellar-horizon/pull/223)).

## 28.0.0

**This release adds support for Protocol 28.**
Expand Down
17 changes: 9 additions & 8 deletions internal/ingest/contractevents/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,15 +321,17 @@ func parseSacEventFromTxMetaV4(event *xdr.ContractEvent, networkPassphrase strin
// MuxedAddressObject - which is always a uint64. ScvBytes and ScvString are NOT
// valid for SAC events (those are only used for classic transaction memo mappings
// per CAP-67, which are processed through a different code path).
//
// Only amount is required. A to_muxed_id holding None arrives either bound to
// Void or, for a CAP-86 sparse map, with no key at all, and both mean the
// transfer has no muxed destination. Unrecognized keys are ignored for the same
// reason: callers discard an event whose parse fails rather than surfacing an
// error, so rejecting one silently drops its effects and participants.
func parseSacEventMap(mapData xdr.ScMap) (xdr.Int128Parts, xdr.Memo, error) {
var foundAmount, foundMuxedId bool
var foundAmount bool
var amount xdr.Int128Parts
var memo xdr.Memo

if len(mapData) != 2 {
return amount, memo, fmt.Errorf("expected exactly 2 elements in map data, but found %d", len(mapData))
}

for _, entry := range mapData {
key, ok := entry.Key.GetSym()
if !ok {
Expand All @@ -345,7 +347,6 @@ func parseSacEventMap(mapData xdr.ScMap) (xdr.Int128Parts, xdr.Memo, error) {
foundAmount = true

case "to_muxed_id":
foundMuxedId = true
// SAC events only emit uint64 for to_muxed_id (muxed account ID).
// ScvBytes/ScvString are NOT valid here - those are only for classic
// transaction memo mappings which use a different code path.
Expand All @@ -354,6 +355,8 @@ func parseSacEventMap(mapData xdr.ScMap) (xdr.Int128Parts, xdr.Memo, error) {
if val, ok := entry.Val.GetU64(); ok {
memo = xdr.MemoID(uint64(val))
}
case xdr.ScValTypeScvVoid:
// No muxed destination, leaving memo as MemoNone.
default:
return amount, memo, fmt.Errorf("invalid to_muxed_id type in SAC event: expected ScvU64, got %s", entry.Val.Type)
}
Expand All @@ -362,8 +365,6 @@ func parseSacEventMap(mapData xdr.ScMap) (xdr.Int128Parts, xdr.Memo, error) {

if !foundAmount {
return amount, memo, errors.New("amount field not found in map")
} else if !foundMuxedId {
return amount, memo, errors.New("to_muxed_id field not found in map")
}

return amount, memo, nil
Expand Down
70 changes: 64 additions & 6 deletions internal/ingest/contractevents/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,55 @@ func TestStellarAssetContractEventParsing(t *testing.T) {
DestinationMemo: xdr.MemoID(12345),
},
},
{
// CAP-86 sparse maps omit a field holding None, so the map carries
// nothing but the amount.
name: "Valid V4 transfer with to_muxed_id omitted",
txMetaVersion: 4,
eventType: EventTypeTransfer,
topics: []xdr.ScVal{
makeSymbol("transfer"),
makeAddress(randomAccount),
makeAddress(zeroContract),
makeAsset(randomAsset),
},
data: makeV4MapData(big.NewInt(1000), xdr.Memo{}),
asset: randomAsset,
contractID: mustGetContractID(randomAsset),
expectedResult: &StellarAssetContractEvent{
Type: EventTypeTransfer,
Asset: randomAsset,
From: randomAccount,
To: zeroContract,
Amount: xdr.Int128Parts{Lo: 1000, Hi: 0},
},
},
{
// The encoding a contract built before CAP-86 produces for the same
// None field: the key is present, bound to Void.
name: "Valid V4 transfer with void to_muxed_id",
txMetaVersion: 4,
eventType: EventTypeTransfer,
topics: []xdr.ScVal{
makeSymbol("transfer"),
makeAddress(randomAccount),
makeAddress(zeroContract),
makeAsset(randomAsset),
},
data: makeV4MapDataWithMuxedID(
big.NewInt(1000),
&xdr.ScVal{Type: xdr.ScValTypeScvVoid},
),
asset: randomAsset,
contractID: mustGetContractID(randomAsset),
expectedResult: &StellarAssetContractEvent{
Type: EventTypeTransfer,
Asset: randomAsset,
From: randomAccount,
To: zeroContract,
Amount: xdr.Int128Parts{Lo: 1000, Hi: 0},
},
},
{
name: "V4 SAC event rejects ScvString for to_muxed_id",
txMetaVersion: 4,
Expand Down Expand Up @@ -362,7 +411,7 @@ func TestStellarAssetContractEventParsing(t *testing.T) {
expectedError: "invalid from address",
},
{
name: "V4 map data insufficient elements",
name: "V4 map data with no entries",
txMetaVersion: 4,
eventType: EventTypeTransfer,
topics: []xdr.ScVal{
Expand All @@ -380,7 +429,7 @@ func TestStellarAssetContractEventParsing(t *testing.T) {
}(),
asset: randomAsset,
contractID: mustGetContractID(randomAsset),
expectedError: "failed to parse V4 map data: expected exactly 2 elements in map data",
expectedError: "failed to parse V4 map data: amount field not found in map",
},
{
name: "V4 map data - missing amount",
Expand Down Expand Up @@ -413,7 +462,10 @@ func TestStellarAssetContractEventParsing(t *testing.T) {
expectedError: "amount field not found in map",
},
{
name: "V4 map data - missing muxed id",
// A key the parser does not recognize is ignored rather than
// rejected, so that a future field added to the event data does not
// take payment effects down with it.
name: "V4 map data with an unrecognized key",
txMetaVersion: 4,
eventType: EventTypeTransfer,
topics: []xdr.ScVal{
Expand All @@ -438,9 +490,15 @@ func TestStellarAssetContractEventParsing(t *testing.T) {
Map: &mapData,
}
}(),
asset: randomAsset,
contractID: mustGetContractID(randomAsset),
expectedError: "failed to parse V4 map data: to_muxed_id field not found in map",
asset: randomAsset,
contractID: mustGetContractID(randomAsset),
expectedResult: &StellarAssetContractEvent{
Type: EventTypeTransfer,
Asset: randomAsset,
From: randomAccount,
To: zeroContract,
Amount: xdr.Int128Parts{Lo: 1000, Hi: 0},
},
},
{
name: "V3 Invalid amount data type",
Expand Down
53 changes: 25 additions & 28 deletions internal/ingest/contractevents/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,54 +161,51 @@ func makeAsset(asset xdr.Asset) xdr.ScVal {
}
}

// makeV4MapData builds the V4 event data map for the given amount and memo,
// omitting to_muxed_id entirely for a MemoNone memo the way a CAP-86 sparse map
// does.
func makeV4MapData(amount *big.Int, memo xdr.Memo) xdr.ScVal {
mapEntries := xdr.ScMap{}

// Add amount entry
amountEntry := xdr.ScMapEntry{
Key: xdr.ScVal{
Type: xdr.ScValTypeScvSymbol,
Sym: &[]xdr.ScSymbol{"amount"}[0],
},
Val: makeBigAmount(amount),
}
mapEntries = append(mapEntries, amountEntry)

// Add to_muxed_id entry based on memo type
var muxedIdVal xdr.ScVal
var muxedIdVal *xdr.ScVal
switch memo.Type {
case xdr.MemoTypeMemoNone:
case xdr.MemoTypeMemoId:
id := memo.Id
val := *id
muxedIdVal = xdr.ScVal{
val := *memo.Id
muxedIdVal = &xdr.ScVal{
Type: xdr.ScValTypeScvU64,
U64: &val,
}
case xdr.MemoTypeMemoText:
str := memo.Text
val := xdr.ScString(*str)
muxedIdVal = xdr.ScVal{
val := xdr.ScString(*memo.Text)
muxedIdVal = &xdr.ScVal{
Type: xdr.ScValTypeScvString,
Str: &val,
}
case xdr.MemoTypeMemoHash:
bytes := xdr.ScBytes(memo.Hash[:])
muxedIdVal = xdr.ScVal{
muxedIdVal = &xdr.ScVal{
Type: xdr.ScValTypeScvBytes,
Bytes: &bytes,
}
default:
panic(fmt.Errorf("unsupported memo type: %v", memo.Type))
}

muxedIdEntry := xdr.ScMapEntry{
Key: xdr.ScVal{
Type: xdr.ScValTypeScvSymbol,
Sym: &[]xdr.ScSymbol{"to_muxed_id"}[0],
},
Val: muxedIdVal,
return makeV4MapDataWithMuxedID(amount, muxedIdVal)
}

// makeV4MapDataWithMuxedID builds the V4 event data map with to_muxed_id bound
// to the given value, or without the key at all when muxedID is nil.
func makeV4MapDataWithMuxedID(amount *big.Int, muxedID *xdr.ScVal) xdr.ScVal {
mapEntries := xdr.ScMap{{
Key: makeSymbol("amount"),
Val: makeBigAmount(amount),
}}
if muxedID != nil {
mapEntries = append(mapEntries, xdr.ScMapEntry{
Key: makeSymbol("to_muxed_id"),
Val: *muxedID,
})
}
mapEntries = append(mapEntries, muxedIdEntry)
mapPtr := &mapEntries

// Need to use double pointer for Map field
Expand Down
Loading