diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index f62c2a965a..486b9b13d9 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -32,11 +32,11 @@ jobs: env: HORIZON_INTEGRATION_TESTS_ENABLED: true HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL: ${{ matrix.protocol-version }} - PROTOCOL_22_CORE_DEBIAN_PKG_VERSION: 23.0.0-2587.rc4.dcf366957.focal - PROTOCOL_22_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:23.0.0-2587.rc4.dcf366957.focal + PROTOCOL_22_CORE_DEBIAN_PKG_VERSION: 22.4.2-2603.136aea451.focal + PROTOCOL_22_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:22.4.2-2603.136aea451.focal PROTOCOL_22_STELLAR_RPC_DOCKER_IMG: stellar/stellar-rpc:23.0.0-rc2-126 - PROTOCOL_23_CORE_DEBIAN_PKG_VERSION: 23.0.0-2587.rc4.dcf366957.focal - PROTOCOL_23_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:23.0.0-2587.rc4.dcf366957.focal + PROTOCOL_23_CORE_DEBIAN_PKG_VERSION: 22.4.2-2603.136aea451.focal + PROTOCOL_23_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:22.4.2-2603.136aea451.focal PROTOCOL_23_STELLAR_RPC_DOCKER_IMG: stellar/stellar-rpc:23.0.0-rc2-126 PGHOST: localhost PGPORT: 5432 @@ -126,7 +126,7 @@ jobs: runs-on: ubuntu-22.04 env: GO_VERSION: 1.23.4 - STELLAR_CORE_VERSION: 23.0.0-2587.rc4.dcf366957.jammy + STELLAR_CORE_VERSION: 22.4.2-2603.136aea451.jammy steps: - uses: actions/checkout@v3 with: diff --git a/services/horizon/internal/integration/sac_test.go b/services/horizon/internal/integration/sac_test.go index 7323a49594..0d91c329fe 100644 --- a/services/horizon/internal/integration/sac_test.go +++ b/services/horizon/internal/integration/sac_test.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "math" "math/big" + "strconv" "strings" "testing" "time" @@ -812,6 +813,129 @@ func TestEvictionAndDeletion(t *testing.T) { }) } +func TestParallelSACTransfer(t *testing.T) { + if integration.GetCoreMaxSupportedProtocol() < 23 { + t.Skip("This test run does not support less than Protocol 23") + } + + itest := integration.NewTest(t, integration.Config{ + EnableStellarRPC: true, + HorizonIngestParameters: map[string]string{ + // disable state verification because we will insert + // a fake asset contract in the horizon db and we don't + // want state verification to detect this + "ingest-disable-state-verification": "true", + }, + QuickEviction: true, + }) + + issuer := itest.Master().Address() + code := "USD" + asset := xdr.MustNewCreditAsset(code, issuer) + + createSAC(itest, asset) + + accountKeys, accounts := itest.CreateAccounts(2, "100") + recipientKp, recipient := accountKeys[0], accounts[0] + senderKp, sender := accountKeys[1], accounts[1] + itest.MustEstablishTrustline(recipientKp, recipient, txnbuild.MustAssetFromXDR(asset)) + itest.MustEstablishTrustline(senderKp, sender, txnbuild.MustAssetFromXDR(asset)) + + itest.MustSubmitOperations( + itest.MasterAccount(), + itest.Master(), + &txnbuild.Payment{ + SourceAccount: issuer, + Destination: senderKp.Address(), + Asset: txnbuild.CreditAsset{ + Code: code, + Issuer: issuer, + }, + Amount: "2000", + }, + ) + + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 2, + balanceAccounts: 0, + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), + }) + + i := 0 + for { + // tx to remove evicted balance + transferOp := itest.PreflightHostFunctions(sender, + *transfer(itest, senderKp.Address(), asset, "3", accountAddressParam(recipient.GetAccountID())), + ) + mintOp := itest.PreflightHostFunctions(itest.MasterAccount(), + *transfer(itest, itest.Master().Address(), asset, "6", accountAddressParam(recipient.GetAccountID())), + ) + + mintTx, err := itest.CreateSignedTransactionFromOps(itest.MasterAccount(), []*keypair.Full{itest.Master()}, &mintOp) + require.NoError(t, err) + + account := itest.MustGetAccount(senderKp) + transferTx, err := itest.CreateSignedTransactionFromOps(&account, []*keypair.Full{senderKp}, &transferOp) + require.NoError(t, err) + + responses, err := itest.SubmitTransactions([]*txnbuild.Transaction{mintTx, transferTx}) + require.NoError(t, err) + require.True(t, responses[0].Successful) + require.True(t, responses[1].Successful) + + masterAccount := itest.MustGetAccount(itest.Master()) + recipientAccount := itest.MustGetAccount(recipientKp) + senderAccount := itest.MustGetAccount(senderKp) + + require.Equal(t, masterAccount.LastModifiedLedger, uint32(responses[0].Ledger)) + require.Equal(t, senderAccount.LastModifiedLedger, uint32(responses[1].Ledger)) + + var found bool + for _, balance := range senderAccount.Balances { + if balance.Issuer != issuer || balance.Code != code { + continue + } + found = true + require.Equal(t, balance.LastModifiedLedger, uint32(responses[1].Ledger)) + } + require.True(t, found) + + found = false + for _, balance := range recipientAccount.Balances { + if balance.Issuer != issuer || balance.Code != code { + continue + } + found = true + require.Equal(t, balance.LastModifiedLedger, max(uint32(responses[1].Ledger), uint32(responses[0].Ledger))) + } + require.True(t, found) + + if responses[0].Ledger == responses[1].Ledger { + break + } + if i >= 100 { + require.Fail(t, "could not produce ledger with both transfers") + } + i++ + time.Sleep(time.Second) + } + + expectedAmount := strconv.Itoa(90 * (i + 1)) + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 2, + balanceAccounts: amount.MustParse(expectedAmount), + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: stellarAssetContractID(itest, asset), + }) +} + func TestEvictionOfSACWithActiveBalance(t *testing.T) { if integration.GetCoreMaxSupportedProtocol() < 23 { t.Skip("This test run does not support less than Protocol 23") @@ -1052,6 +1176,85 @@ func TestEvictionOfSACAndRestoration(t *testing.T) { }) } +func TestEvictionOfSACAndAutoRestore(t *testing.T) { + if integration.GetCoreMaxSupportedProtocol() < 23 { + t.Skip("This test run does not support less than Protocol 23") + } + + itest := integration.NewTest(t, integration.Config{ + EnableStellarRPC: true, + HorizonIngestParameters: map[string]string{ + // disable state verification because we will insert + // a fake asset contract in the horizon db and we don't + // want state verification to detect this + "ingest-disable-state-verification": "true", + }, + QuickEviction: true, + }) + + issuer := itest.Master().Address() + code := "USD" + asset := xdr.MustNewCreditAsset(code, issuer) + + recipientKp, recipient := itest.CreateAccount("100") + itest.MustEstablishTrustline(recipientKp, recipient, txnbuild.MustAssetFromXDR(asset)) + + createSACWithTTL(itest, asset, 30) + contractID := stellarAssetContractID(itest, asset) + + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: 0, + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: contractID, + }) + + contractToEvictLedgerKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.LedgerKeyContractData{ + Contract: xdr.ScAddress{ + Type: xdr.ScAddressTypeScAddressTypeContract, + AccountId: nil, + ContractId: &contractID, + }, + Key: xdr.ScVal{ + Type: xdr.ScValTypeScvLedgerKeyContractInstance, + }, + Durability: xdr.ContractDataDurabilityPersistent, + }, + } + itest.WaitUntilLedgerEntryIsEvicted(contractToEvictLedgerKey, time.Minute) + + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("0"), + numContracts: 0, + balanceContracts: big.NewInt(0), + }) + + // auto restore SAC via transfer() + assertInvokeHostFnSucceeds( + itest, + itest.Master(), + transfer(itest, itest.Master().Address(), asset, "6", accountAddressParam(recipient.GetAccountID())), + ) + + assertAssetStats(itest, assetStats{ + code: code, + issuer: issuer, + numAccounts: 1, + balanceAccounts: amount.MustParse("6"), + numContracts: 0, + balanceContracts: big.NewInt(0), + contractID: contractID, + }) +} + func invokeStoreSet( itest *integration.Test, storeContractID xdr.ContractId, diff --git a/services/horizon/internal/test/integration/integration.go b/services/horizon/internal/test/integration/integration.go index 742c196f3c..a963db743d 100644 --- a/services/horizon/internal/test/integration/integration.go +++ b/services/horizon/internal/test/integration/integration.go @@ -36,6 +36,7 @@ import ( "github.com/stellar/go/clients/stellarcore" "github.com/stellar/go/keypair" proto "github.com/stellar/go/protocols/horizon" + coreproto "github.com/stellar/go/protocols/stellarcore" horizoncmd "github.com/stellar/go/services/horizon/cmd" horizon "github.com/stellar/go/services/horizon/internal" "github.com/stellar/go/services/horizon/internal/ingest" @@ -173,7 +174,7 @@ func NewTest(t *testing.T, config Config) *Test { } if config.QuickEviction { validatorParams.OverrideEvictionParamsForTesting = true - validatorParams.TestingStartingEvictionScanLevel = 2 + validatorParams.TestingStartingEvictionScanLevel = 1 validatorParams.TestingMaxEntriesToArchive = 100 // QuickEviction implies QuickExpiration config.QuickExpiration = true @@ -1450,6 +1451,53 @@ func (i *Test) AsyncSubmitTransaction( return i.Client().AsyncSubmitTransaction(tx) } +func (i *Test) SubmitTransactions(transactions []*txnbuild.Transaction) ([]proto.Transaction, error) { + var results []proto.Transaction + byHash := make(map[string]proto.Transaction) + for _, tx := range transactions { + response, err := i.Client().AsyncSubmitTransaction(tx) + if err != nil { + return nil, err + } + if response.TxStatus != coreproto.TXStatusPending { + return nil, fmt.Errorf("transaction status is %s", response.TxStatus) + } + } + require.Eventually(i.t, func() bool { + for _, tx := range transactions { + hash, err := tx.HashHex(i.passPhrase) + if err != nil { + continue + } + if _, ok := byHash[hash]; ok { + continue + } + response, err := i.Client().TransactionDetail(hash) + if err != nil { + continue + } + byHash[hash] = response + } + return len(byHash) == len(transactions) + }, time.Minute, time.Second) + + if len(byHash) != len(transactions) { + return nil, fmt.Errorf("expected %d responses, got %d", len(transactions), len(byHash)) + } + for _, tx := range transactions { + hash, err := tx.HashHex(i.passPhrase) + if err != nil { + return nil, err + } + response, ok := byHash[hash] + if !ok { + return nil, fmt.Errorf("transaction %s not found", hash) + } + results = append(results, response) + } + return results, nil +} + func (i *Test) MustSubmitMultiSigTransaction( signers []*keypair.Full, txParams txnbuild.TransactionParams, ) proto.Transaction {