Skip to content

Commit

Permalink
services/horizon: Fix operations participants logic to include Invoke…
Browse files Browse the repository at this point in the history
…HostFunction operation (#5574)
  • Loading branch information
urvisavla authored Jan 23, 2025
1 parent 07d197a commit eecf724
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 7 deletions.
3 changes: 3 additions & 0 deletions services/horizon/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ file. This project adheres to [Semantic Versioning](http://semver.org/).

- Update default pubnet captive core configuration to replace Whalestack with Creit Technologies in the quorum set ([5564](https://github.com/stellar/go/pull/5564)).

### Fixed
- Fix the account operations endpoint to include InvokeHostFunction operations. The fix ensures that all account operations will be listed going forward. However, it will not retroactively include these operations for previously ingested ledgers; reingesting the historical data is required to address that. ([5574](https://github.com/stellar/go/pull/5574)).

## 22.0.2

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion services/horizon/internal/ingest/processor_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ func (s *ProcessorRunner) buildTransactionProcessor(ledgersProcessor *processors
processors.NewOperationProcessor(s.historyQ.NewOperationBatchInsertBuilder(), s.config.NetworkPassphrase),
tradeProcessor,
processors.NewParticipantsProcessor(accountLoader,
s.historyQ.NewTransactionParticipantsBatchInsertBuilder(), s.historyQ.NewOperationParticipantBatchInsertBuilder()),
s.historyQ.NewTransactionParticipantsBatchInsertBuilder(), s.historyQ.NewOperationParticipantBatchInsertBuilder(), s.config.NetworkPassphrase),
processors.NewTransactionProcessor(s.historyQ.NewTransactionBatchInsertBuilder(), s.config.SkipTxmeta),
processors.NewClaimableBalancesTransactionProcessor(cbLoader,
s.historyQ.NewTransactionClaimableBalanceBatchInsertBuilder(), s.historyQ.NewOperationClaimableBalanceBatchInsertBuilder()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1047,7 +1047,31 @@ func (operation *transactionOperationWrapper) Participants() ([]xdr.AccountId, e
case xdr.OperationTypeLiquidityPoolWithdraw:
// the only direct participant is the source_account
case xdr.OperationTypeInvokeHostFunction:
// the only direct participant is the source_account
if changes, err := operation.transaction.GetOperationChanges(operation.index); err != nil {
return participants, err
} else {
for _, change := range changes {
var data xdr.LedgerEntryData
switch {
case change.Post != nil:
data = change.Post.Data
case change.Pre != nil:
data = change.Pre.Data
default:
log.Errorf("Change Type %s with no pre or post", change.Type.String())
continue
}
if ledgerKey, err := data.LedgerKey(); err == nil {
participants = append(participants, getLedgerKeyParticipants(ledgerKey)...)
}
}
}
if diagnosticEvents, err := operation.transaction.GetDiagnosticEvents(); err != nil {
return participants, err
} else {
participants = append(participants, getParticipantsFromSACEvents(filterEvents(diagnosticEvents), operation.network)...)
}

case xdr.OperationTypeExtendFootprintTtl:
// the only direct participant is the source_account
case xdr.OperationTypeRestoreFootprint:
Expand All @@ -1067,6 +1091,48 @@ func (operation *transactionOperationWrapper) Participants() ([]xdr.AccountId, e
return dedupeParticipants(participants), nil
}

func getParticipantsFromSACEvents(contractEvents []xdr.ContractEvent, network string) []xdr.AccountId {
var participants []xdr.AccountId

for _, contractEvent := range contractEvents {
if sacEvent, err := contractevents.NewStellarAssetContractEvent(&contractEvent, network); err == nil {
// 'to' and 'from' fields in the events can be either a Contract address or an Account address. We're
// only interested in account addresses and will skip Contract addresses.
switch sacEvent.GetType() {
case contractevents.EventTypeTransfer:
var transferEvt *contractevents.TransferEvent
transferEvt = sacEvent.(*contractevents.TransferEvent)
var from, to xdr.AccountId
if from, err = xdr.AddressToAccountId(transferEvt.From); err == nil {
participants = append(participants, from)
}
if to, err = xdr.AddressToAccountId(transferEvt.To); err == nil {
participants = append(participants, to)
}
case contractevents.EventTypeMint:
mintEvt := sacEvent.(*contractevents.MintEvent)
var to xdr.AccountId
if to, err = xdr.AddressToAccountId(mintEvt.To); err == nil {
participants = append(participants, to)
}
case contractevents.EventTypeClawback:
clawbackEvt := sacEvent.(*contractevents.ClawbackEvent)
var from xdr.AccountId
if from, err = xdr.AddressToAccountId(clawbackEvt.From); err == nil {
participants = append(participants, from)
}
case contractevents.EventTypeBurn:
burnEvt := sacEvent.(*contractevents.BurnEvent)
var from xdr.AccountId
if from, err = xdr.AddressToAccountId(burnEvt.From); err == nil {
participants = append(participants, from)
}
}
}
}
return participants
}

// dedupeParticipants remove any duplicate ids from `in`
func dedupeParticipants(in []xdr.AccountId) []xdr.AccountId {
if len(in) <= 1 {
Expand All @@ -1090,7 +1156,7 @@ func dedupeParticipants(in []xdr.AccountId) []xdr.AccountId {
}

// OperationsParticipants returns a map with all participants per operation
func operationsParticipants(transaction ingest.LedgerTransaction, sequence uint32) (map[int64][]xdr.AccountId, error) {
func operationsParticipants(transaction ingest.LedgerTransaction, sequence uint32, network string) (map[int64][]xdr.AccountId, error) {
participants := map[int64][]xdr.AccountId{}

for opi, op := range transaction.Envelope.Operations() {
Expand All @@ -1099,6 +1165,7 @@ func operationsParticipants(transaction ingest.LedgerTransaction, sequence uint3
transaction: transaction,
operation: op,
ledgerSequence: sequence,
network: network,
}

p, err := operation.Participants()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,21 @@ type ParticipantsProcessor struct {
accountLoader *history.AccountLoader
txBatch history.TransactionParticipantsBatchInsertBuilder
opBatch history.OperationParticipantBatchInsertBuilder
network string
}

func NewParticipantsProcessor(
accountLoader *history.AccountLoader,
txBatch history.TransactionParticipantsBatchInsertBuilder,
opBatch history.OperationParticipantBatchInsertBuilder,
network string,

) *ParticipantsProcessor {
return &ParticipantsProcessor{
accountLoader: accountLoader,
txBatch: txBatch,
opBatch: opBatch,
network: network,
}
}

Expand Down Expand Up @@ -129,7 +133,7 @@ func (p *ParticipantsProcessor) addOperationsParticipants(
sequence uint32,
transaction ingest.LedgerTransaction,
) error {
participants, err := operationsParticipants(transaction, sequence)
participants, err := operationsParticipants(transaction, sequence, p.network)
if err != nil {
return errors.Wrap(err, "could not determine operation participants")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ func (s *ParticipantsProcessorTestSuiteLedger) SetupTest() {
s.accountLoader,
s.mockBatchInsertBuilder,
s.mockOperationsBatchInsertBuilder,
networkPassphrase,
)

s.txs = []ingest.LedgerTransaction{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@
package processors

import (
"math/big"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"

"github.com/stellar/go/keypair"
"github.com/stellar/go/protocols/horizon/base"
"github.com/stellar/go/strkey"
"github.com/stellar/go/support/contractevents"

"github.com/stellar/go/ingest"
"github.com/stellar/go/services/horizon/internal/db2/history"
Expand Down Expand Up @@ -1104,7 +1108,7 @@ func TestOperationParticipants(t *testing.T) {
xdr.MustAddress("GDRW375MAYR46ODGF2WGANQC2RRZL7O246DYHHCGWTV2RE7IHE2QUQLD"),
xdr.MustAddress("GACAR2AEYEKITE2LKI5RMXF5MIVZ6Q7XILROGDT22O7JX4DSWFS7FDDP"),
}
participantsMap, err := operationsParticipants(transaction, sequence)
participantsMap, err := operationsParticipants(transaction, sequence, networkPassphrase)
tt.NoError(err)
tt.Len(participantsMap, 1)
for k, v := range participantsMap {
Expand Down Expand Up @@ -2285,3 +2289,110 @@ func TestDetailsCoversAllOperationTypes(t *testing.T) {
_, err := operation.Details()
assert.ErrorContains(t, err, "unknown operation type: ")
}

func TestTestInvokeHostFnOperationParticipants(t *testing.T) {
sourceAddress := "GAUJETIZVEP2NRYLUESJ3LS66NVCEGMON4UDCBCSBEVPIID773P2W6AY"
source := xdr.MustMuxedAddress(sourceAddress)

randomIssuer := keypair.MustRandom()
randomAsset := xdr.MustNewCreditAsset("TESTING", randomIssuer.Address())
passphrase := "passphrase"
randomAccount := keypair.MustRandom().Address()

burnEvtAcc := keypair.MustRandom().Address()
mintEvtAcc := keypair.MustRandom().Address()
clawbkEvtAcc := keypair.MustRandom().Address()
transferEvtFromAcc := keypair.MustRandom().Address()
transferEvtToAcc := keypair.MustRandom().Address()

transferContractEvent := contractevents.GenerateEvent(contractevents.EventTypeTransfer, transferEvtFromAcc, transferEvtToAcc, "", randomAsset, big.NewInt(10000000), passphrase)
mintContractEvent := contractevents.GenerateEvent(contractevents.EventTypeMint, "", mintEvtAcc, randomAccount, randomAsset, big.NewInt(10000000), passphrase)
burnContractEvent := contractevents.GenerateEvent(contractevents.EventTypeBurn, burnEvtAcc, "", randomAccount, randomAsset, big.NewInt(10000000), passphrase)
clawbackContractEvent := contractevents.GenerateEvent(contractevents.EventTypeClawback, clawbkEvtAcc, "", randomAccount, randomAsset, big.NewInt(10000000), passphrase)

tx1 := ingest.LedgerTransaction{
UnsafeMeta: xdr.TransactionMeta{
V: 3,
V3: &xdr.TransactionMetaV3{
SorobanMeta: &xdr.SorobanTransactionMeta{
Events: []xdr.ContractEvent{
transferContractEvent,
burnContractEvent,
mintContractEvent,
clawbackContractEvent,
},
},
},
},
}

wrapper1 := transactionOperationWrapper{
transaction: tx1,
operation: xdr.Operation{
SourceAccount: &source,
Body: xdr.OperationBody{
Type: xdr.OperationTypeInvokeHostFunction,
},
},
network: passphrase,
}

participants, err := wrapper1.Participants()
assert.NoError(t, err)
assert.ElementsMatch(t,
[]xdr.AccountId{
xdr.MustAddress(source.Address()),
xdr.MustAddress(mintEvtAcc),
xdr.MustAddress(burnEvtAcc),
xdr.MustAddress(clawbkEvtAcc),
xdr.MustAddress(transferEvtFromAcc),
xdr.MustAddress(transferEvtToAcc),
},
participants,
)

contractId := [32]byte{}
zeroContractStrKey, err := strkey.Encode(strkey.VersionByteContract, contractId[:])
assert.NoError(t, err)

transferContractEvent = contractevents.GenerateEvent(contractevents.EventTypeTransfer, zeroContractStrKey, zeroContractStrKey, "", randomAsset, big.NewInt(10000000), passphrase)
mintContractEvent = contractevents.GenerateEvent(contractevents.EventTypeMint, "", zeroContractStrKey, randomAccount, randomAsset, big.NewInt(10000000), passphrase)
burnContractEvent = contractevents.GenerateEvent(contractevents.EventTypeBurn, zeroContractStrKey, "", randomAccount, randomAsset, big.NewInt(10000000), passphrase)
clawbackContractEvent = contractevents.GenerateEvent(contractevents.EventTypeClawback, zeroContractStrKey, "", randomAccount, randomAsset, big.NewInt(10000000), passphrase)

tx2 := ingest.LedgerTransaction{
UnsafeMeta: xdr.TransactionMeta{
V: 3,
V3: &xdr.TransactionMetaV3{
SorobanMeta: &xdr.SorobanTransactionMeta{
Events: []xdr.ContractEvent{
transferContractEvent,
burnContractEvent,
mintContractEvent,
clawbackContractEvent,
},
},
},
},
}

wrapper2 := transactionOperationWrapper{
transaction: tx2,
operation: xdr.Operation{
SourceAccount: &source,
Body: xdr.OperationBody{
Type: xdr.OperationTypeInvokeHostFunction,
},
},
network: passphrase,
}

participants, err = wrapper2.Participants()
assert.NoError(t, err)
assert.ElementsMatch(t,
[]xdr.AccountId{
xdr.MustAddress(source.Address()),
},
participants,
)
}
27 changes: 25 additions & 2 deletions services/horizon/internal/integration/sac_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func TestContractMintToAccount(t *testing.T) {
itest.Master(),
mint(itest, issuer, asset, "20", accountAddressParam(recipient.GetAccountID())),
)

assertAccountInvokeHostFunctionOperation(itest, recipientKp.Address(), "", recipientKp.Address(), "20.0000000")
assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("20"))
assertAssetStats(itest, assetStats{
code: code,
Expand Down Expand Up @@ -92,6 +92,7 @@ func TestContractMintToAccount(t *testing.T) {
itest.Master(),
transfer(itest, issuer, asset, "30", accountAddressParam(otherRecipient.GetAccountID())),
)
assertAccountInvokeHostFunctionOperation(itest, otherRecipientKp.Address(), issuer, otherRecipientKp.Address(), "30.0000000")
assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("20"))
assertContainsBalance(itest, otherRecipientKp, issuer, code, amount.MustParse("30"))

Expand Down Expand Up @@ -552,7 +553,8 @@ func TestContractTransferBetweenAccounts(t *testing.T) {
recipientKp,
transfer(itest, recipientKp.Address(), asset, "30", accountAddressParam(otherRecipient.GetAccountID())),
)

assertAccountInvokeHostFunctionOperation(itest, recipientKp.Address(), recipientKp.Address(), otherRecipientKp.Address(), "30.0000000")
assertAccountInvokeHostFunctionOperation(itest, otherRecipientKp.Address(), recipientKp.Address(), otherRecipientKp.Address(), "30.0000000")
assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("970"))
assertContainsBalance(itest, otherRecipientKp, issuer, code, amount.MustParse("30"))

Expand Down Expand Up @@ -646,6 +648,7 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) {
recipientKp,
transfer(itest, recipientKp.Address(), asset, "30", contractAddressParam(recipientContractID)),
)
assertAccountInvokeHostFunctionOperation(itest, recipientKp.Address(), recipientKp.Address(), strkeyRecipientContractID, "30.0000000")
assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("970"))
assertContainsEffect(t, getTxEffects(itest, transferTx, asset),
effects.EffectAccountDebited, effects.EffectContractCredited)
Expand All @@ -668,6 +671,7 @@ func TestContractTransferBetweenAccountAndContract(t *testing.T) {
recipientKp,
transferFromContract(itest, recipientKp.Address(), asset, recipientContractID, recipientContractHash, "500", accountAddressParam(recipient.GetAccountID())),
)
assertAccountInvokeHostFunctionOperation(itest, recipientKp.Address(), strkeyRecipientContractID, recipientKp.Address(), "500.0000000")
assertContainsEffect(t, getTxEffects(itest, transferTx, asset),
effects.EffectContractDebited, effects.EffectAccountCredited)
assertContainsBalance(itest, recipientKp, issuer, code, amount.MustParse("1470"))
Expand Down Expand Up @@ -827,6 +831,7 @@ func TestContractBurnFromAccount(t *testing.T) {
recipientKp,
burn(itest, recipientKp.Address(), asset, "500"),
)
assertAccountInvokeHostFunctionOperation(itest, recipientKp.Address(), recipientKp.Address(), "", "500.0000000")

fx := getTxEffects(itest, burnTx, asset)
require.Len(t, fx, 1)
Expand Down Expand Up @@ -981,6 +986,7 @@ func TestContractClawbackFromAccount(t *testing.T) {
itest.Master(),
clawback(itest, issuer, asset, "1000", accountAddressParam(recipientKp.Address())),
)
assertAccountInvokeHostFunctionOperation(itest, recipientKp.Address(), recipientKp.Address(), "", "1000.0000000")

assertContainsEffect(t, getTxEffects(itest, clawTx, asset), effects.EffectAccountDebited)
assertContainsBalance(itest, recipientKp, issuer, code, 0)
Expand Down Expand Up @@ -1164,6 +1170,23 @@ func getTxEffects(itest *integration.Test, txHash string, asset xdr.Asset) []eff
return result
}

func assertAccountInvokeHostFunctionOperation(itest *integration.Test, account string, from string, to string, amount string) {
ops, err := itest.Client().Operations(horizonclient.OperationRequest{
ForAccount: account,
Limit: 1,
Order: "desc",
})

assert.NoError(itest.CurrentTest(), err)
result := ops.Embedded.Records[0]
assert.Equal(itest.CurrentTest(), result.GetType(), operations.TypeNames[xdr.OperationTypeInvokeHostFunction])
invokeHostFn := result.(operations.InvokeHostFunction)
assert.Equal(itest.CurrentTest(), invokeHostFn.Function, "HostFunctionTypeHostFunctionTypeInvokeContract")
assert.Equal(itest.CurrentTest(), to, invokeHostFn.AssetBalanceChanges[0].To)
assert.Equal(itest.CurrentTest(), from, invokeHostFn.AssetBalanceChanges[0].From)
assert.Equal(itest.CurrentTest(), amount, invokeHostFn.AssetBalanceChanges[0].Amount)
}

func assertEventPayments(itest *integration.Test, txHash string, asset xdr.Asset, from string, to string, evtType string, amount string) {
ops, err := itest.Client().Operations(horizonclient.OperationRequest{
ForTransaction: txHash,
Expand Down

0 comments on commit eecf724

Please sign in to comment.