Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
fcd4aed
Add Protocol 30 (CAP-0084) support
sisuresh Sep 23, 2026
236b6eb
Repin go-stellar-sdk to go-stellar-sdk#6015 (73e446de)
sisuresh Sep 29, 2026
16d3e1d
Ingest stellar-core#5472 CAP-0084 LCM vectors
sisuresh Sep 29, 2026
5647d8d
CI: test protocols 29 and 30, drop 28
sisuresh Sep 29, 2026
de18137
CI: run the P30 leg on stellar-core 73f2bdd1's vnext packages
sisuresh Sep 29, 2026
1a58631
effects: record muxed contract destinations on contract_credited
sisuresh Oct 5, 2026
185126b
Merge origin/main into p30-cap-0084-validate
tamirms Oct 8, 2026
7cbd08e
Repin go-stellar-sdk to go-stellar-sdk#6015 (663a89a1)
tamirms Oct 8, 2026
216905d
ingest: give TestGetState_Read a HAS that matches its bucket
tamirms Oct 8, 2026
9da6d51
effects: test contract_muxed on contract_credited end to end
tamirms Oct 8, 2026
c420bfa
ingest: test ledgers with CAP-0088 millisecond close times
tamirms Oct 8, 2026
8d1b94a
CI: run the P30 leg on stellar-core master's 88d2431d7 vnext build
tamirms Oct 8, 2026
4042796
CHANGELOG: describe the Protocol 30 changes
tamirms Oct 8, 2026
fad3ef0
CI: run the P30 leg on stellar-core master after protocol 30 landed
tamirms Oct 9, 2026
bfb260f
Require the merged go-stellar-sdk Protocol 30 commit
tamirms Oct 14, 2026
a81a6eb
Run the P30 leg on core 3640, which includes CAP-0088
tamirms Oct 14, 2026
8025f43
Run the P30 leg on the stellar-core 30.0.0 release candidate
tamirms Oct 14, 2026
0f057e4
Trim comments and the CHANGELOG, drop the CAP-0088 tests
tamirms Oct 14, 2026
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
34 changes: 12 additions & 22 deletions .github/workflows/horizon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,12 @@ on:
pull_request:

jobs:
integration-p28-pkg:
name: Integration tests (P28)
integration-p29-pkg:
name: Integration tests (P29)
uses: ./.github/workflows/integration-tests.yml
with:
protocol_version: '28'
# The P28 leg runs the NEW core and rpc builds at protocol 28 — the point
# is verifying the updated binaries still serve the previous protocol
# (the real configuration during the upgrade window), not re-testing
# already-shipped versions against each other.
protocol_version: '29'
# Move to the new core and rpc builds at protocol 29 once they are released.
core_deb_version: '29.0.0-3589.4eb833373.jammy~buildtests'
core_docker_img: 'stellar/stellar-core:29.0.0-3589.4eb833373.jammy'
stellar_rpc_docker_img: 'stellar/stellar-rpc:29.0.0'
Expand All @@ -23,23 +20,16 @@ jobs:
# latest-go/latest-pg job.
full_matrix: ${{ github.event_name == 'push' }}

integration-p29-pkg:
name: Integration tests (P29)
integration-p30-pkg:
name: Integration tests (P30)
uses: ./.github/workflows/integration-tests.yml
with:
protocol_version: '29'
core_deb_version: '29.0.0-3589.4eb833373.jammy~buildtests'
core_docker_img: 'stellar/stellar-core:29.0.0-3589.4eb833373.jammy'
stellar_rpc_docker_img: 'stellar/stellar-rpc:29.0.0'
full_matrix: ${{ github.event_name == 'push' }}

# integration-p29-src:
# name: Integration tests (P29, core from source)
# uses: ./.github/workflows/integration-tests.yml
# with:
# protocol_version: '29'
# core_git_ref: 'master'
# stellar_rpc_docker_img: 'stellar/stellar-rpc:29.0.0'
protocol_version: '30'
# stellar-core 30.0.0 release candidate.
core_deb_version: '30.0.0-3642.0e49751aa.noble~buildtests'
core_docker_img: 'stellar/unsafe-stellar-core:30.0.0-3642.0e49751aa.noble'
Comment on lines +29 to +30
# No protocol 30 stellar-rpc image yet, so the RPC tests are skipped.
stellar_rpc_docker_img: ''

verify-range:
name: Test (and push) verify-range image
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ on:
required: false
type: string
stellar_rpc_docker_img:
description: 'Full docker image reference for Stellar RPC (e.g. stellar/stellar-rpc:27.0.0)'
required: true
description: 'Full docker image reference for Stellar RPC (e.g. stellar/stellar-rpc:27.0.0). Empty skips the stellar-rpc tests.'
required: false
type: string
full_matrix:
description: 'Run the full go/postgres version matrix. Defaults to false: a single job on the latest go and postgres, which is what external callers (e.g. stellar-core CI) want.'
Expand Down Expand Up @@ -189,6 +189,7 @@ jobs:
echo "Using stellar core version $(stellar-core version)"

- name: Pull and set Stellar RPC image
if: env.STELLAR_RPC_DOCKER_IMG != ''
run: |
docker pull "$STELLAR_RPC_DOCKER_IMG"
echo HORIZON_INTEGRATION_TESTS_STELLAR_RPC_DOCKER_IMG="$STELLAR_RPC_DOCKER_IMG" >> $GITHUB_ENV
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ file. This project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Added
- Added Protocol 30 support and bumped `MaxSupportedProtocolVersion` to 30 ([#237](https://github.com/stellar/stellar-horizon/pull/237)). A Stellar Asset Contract `transfer` or `mint` to a muxed contract (`W...`) gives the `contract_credited` effect `contract_muxed` and `contract_muxed_id` fields.

### Fixed
- The asset ingestion filter now matches rules on the inner operations of fee-bump transactions. Earlier versions collected operations only from TxV0 and Tx envelopes, so a fee-bump transaction was not ingested even when its inner operations referenced a whitelisted asset.

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ require (
require (
github.com/creachadair/jrpc2 v1.2.0
github.com/fsouza/fake-gcs-server v1.49.2
github.com/stellar/go-stellar-sdk v0.7.2
github.com/stellar/go-stellar-sdk v0.7.4-0.20261014050248-990270d32ebe
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,8 @@ github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI=
github.com/spiffe/go-spiffe/v2 v2.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4=
github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
github.com/stellar/go-stellar-sdk v0.7.2 h1:LTO860ulPKRmsKgxGTAf8APL2nFtVvRKClhw2unGhyY=
github.com/stellar/go-stellar-sdk v0.7.2/go.mod h1:YTYl7/zFt7oIX5Y4WRgL+3d2upWQDLywlHunZ4fOxvc=
github.com/stellar/go-stellar-sdk v0.7.4-0.20261014050248-990270d32ebe h1:LeZAWquMEbuR/yF6n+JRD24zZyfm0apTFSwsHCcuJME=
github.com/stellar/go-stellar-sdk v0.7.4-0.20261014050248-990270d32ebe/go.mod h1:TE/oIrVngAujvcalNBggneqRLI3wSlUZb2pWYyqU3QA=
github.com/stellar/go-xdr v0.0.0-20260806060815-dc590f17552a h1:40YIhQusSioBKDW5Tr+YdjoXphcVeu7eXUS2ibMQBTs=
github.com/stellar/go-xdr v0.0.0-20260806060815-dc590f17552a/go.mod h1:If+U9Z1W5xU97VrOgJandQT+2dN7/iOpkCrxBJEyF80=
github.com/stellar/throttled v2.2.3-0.20190823235211-89d75816f59d+incompatible h1:jMXXAcz6xTarGDQ4VtVbtERogcmDQw4RaE85Cr9CgoQ=
Expand Down
6 changes: 3 additions & 3 deletions internal/ingest/contractevents/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,8 @@ func parseSacEventFromTxMetaV4(event *xdr.ContractEvent, networkPassphrase strin
}

// parseSacEventMap parses the ScMap data format used in V4 SAC events.
// For SAC events, to_muxed_id represents the muxed account ID from
// MuxedAddressObject - which is always a uint64. ScvBytes and ScvString are NOT
// For SAC events, to_muxed_id represents the muxed account or muxed contract
// ID from 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).
func parseSacEventMap(mapData xdr.ScMap) (xdr.Int128Parts, xdr.Memo, error) {
Expand Down Expand Up @@ -346,7 +346,7 @@ func parseSacEventMap(mapData xdr.ScMap) (xdr.Int128Parts, xdr.Memo, error) {

case "to_muxed_id":
foundMuxedId = true
// SAC events only emit uint64 for to_muxed_id (muxed account ID).
// SAC events only emit uint64 for to_muxed_id (muxed account or contract ID).
// ScvBytes/ScvString are NOT valid here - those are only for classic
// transaction memo mappings which use a different code path.
switch entry.Val.Type {
Expand Down
19 changes: 19 additions & 0 deletions internal/ingest/contractevents/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,25 @@ func TestStellarAssetContractEventParsing(t *testing.T) {
}
}

func TestParseAddressMuxedContract(t *testing.T) {
address := xdr.ScAddress{
Type: xdr.ScAddressTypeScAddressTypeMuxedContract,
MuxedContract: &xdr.MuxedContract{
Id: 12345,
ContractId: xdr.ContractId(zeroContractHash),
},
}
parsed, err := parseAddress(xdr.ScVal{Type: xdr.ScValTypeScvAddress, Address: &address})
require.NoError(t, err)

muxed, err := strkey.DecodeMuxedContract(parsed)
require.NoError(t, err)
assert.Equal(t, uint64(12345), muxed.ID())
contractID, err := muxed.ContractID()
require.NoError(t, err)
assert.Equal(t, zeroContract, contractID)
}

// Test helper functions
func someLedgerTransaction(version int32) ingest.LedgerTransaction {
return ingest.LedgerTransaction{
Expand Down
175 changes: 124 additions & 51 deletions internal/ingest/core_lcm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
"io"
"os"
"path/filepath"
"strconv"
"testing"

"github.com/stretchr/testify/require"

"github.com/stellar/go-stellar-sdk/protocols/horizon/effects"
"github.com/stellar/go-stellar-sdk/xdr"
"github.com/stellar/stellar-horizon/internal/db2"
"github.com/stellar/stellar-horizon/internal/db2/history"
"github.com/stellar/stellar-horizon/internal/db2/schema"
"github.com/stellar/stellar-horizon/internal/ingest/filters"
Expand Down Expand Up @@ -50,6 +53,61 @@
return ledgers
}

// ingestCoreLCMFile ingests every LedgerCloseMeta in path into its own test
// database, so parallel sub-tests don't conflict.
func ingestCoreLCMFile(t *testing.T, path string) *history.Q {
t.Helper()
testDB := dbtest.Postgres(t)
t.Cleanup(testDB.Close)

dbConn := testDB.Open()
t.Cleanup(func() { _ = dbConn.Close() })

_, err := schema.Migrate(dbConn.DB, schema.MigrateUp, 0)
require.NoError(t, err, "failed to run migrations")

historyQ := &history.Q{SessionInterface: &supportdb.Session{DB: dbConn}}

ledgers := readLedgerCloseMetasFromFile(t, path)
if len(ledgers) == 0 {
t.Skipf("no LedgerCloseMeta records in %s, skipping", path)
}
t.Logf("decoded %d LedgerCloseMeta(s)", len(ledgers))

ctx := context.Background()
runner := ProcessorRunner{
ctx: ctx,
config: Config{
NetworkPassphrase: coreTestNetworkPassphrase,
SkipProtocolVersionCheck: true,
},
historyQ: historyQ,
session: historyQ,
filters: filters.NewFilters(),
}

// Run the full pipeline (change + transaction processors) on
// each ledger sequentially, inside a DB transaction.
for i, lcm := range ledgers {
t.Logf("ingesting ledger %d through all processors", lcm.LedgerSequence())

require.NoError(t, historyQ.Begin(ctx),
"failed to begin transaction for ledger index %d", i)
defer historyQ.Rollback()

Check failure on line 96 in internal/ingest/core_lcm_test.go

View workflow job for this annotation

GitHub Actions / golangci

Error return value of `historyQ.Rollback` is not checked (errcheck)

_, err := runner.RunAllProcessorsOnLedger(lcm)
require.NoError(t, err,
"RunAllProcessorsOnLedger failed on ledger %d (index %d)",
lcm.LedgerSequence(), i)

require.NoError(t, historyQ.Commit(),
"failed to commit transaction for ledger index %d", i)
}

t.Logf("successfully ingested %d ledgers through all processors", len(ledgers))
return historyQ
}

// TestCoreLCMIngestion walks every child directory of coreTestLCMDir, reads
// every .xdr file in each directory, decodes framed LedgerCloseMeta records,
// and runs Horizon's ingestion processors against an isolated test database
Expand Down Expand Up @@ -86,60 +144,75 @@

t.Run(fileEntry.Name(), func(t *testing.T) {
t.Parallel()
ingestCoreLCMFile(t, filepath.Join(dirPath, fileEntry.Name()))
})
}
})
}
}

// Each parallel sub-test gets its own isolated database so
// there are no conflicts between concurrent DB transactions
// or migration resets.
testDB := dbtest.Postgres(t)
defer testDB.Close()

dbConn := testDB.Open()
defer dbConn.Close()

_, err := schema.Migrate(dbConn.DB, schema.MigrateUp, 0)
require.NoError(t, err, "failed to run migrations")

historyQ := &history.Q{SessionInterface: &supportdb.Session{DB: dbConn}}

path := filepath.Join(dirPath, fileEntry.Name())
ledgers := readLedgerCloseMetasFromFile(t, path)
if len(ledgers) == 0 {
t.Skipf("no LedgerCloseMeta records in %s, skipping", path)
}
t.Logf("decoded %d LedgerCloseMeta(s)", len(ledgers))

ctx := context.Background()
runner := ProcessorRunner{
ctx: ctx,
config: Config{
NetworkPassphrase: coreTestNetworkPassphrase,
SkipProtocolVersionCheck: true,
},
historyQ: historyQ,
session: historyQ,
filters: filters.NewFilters(),
}

// Run the full pipeline (change + transaction processors) on
// each ledger sequentially, inside a DB transaction.
for i, lcm := range ledgers {
t.Logf("ingesting ledger %d through all processors", lcm.LedgerSequence())

require.NoError(t, historyQ.Begin(ctx),
"failed to begin transaction for ledger index %d", i)
defer historyQ.Rollback()

_, err := runner.RunAllProcessorsOnLedger(lcm)
require.NoError(t, err,
"RunAllProcessorsOnLedger failed on ledger %d (index %d)",
lcm.LedgerSequence(), i)
// TestCoreLCMMuxedContractDestination checks that SAC transfers and mints to a
// muxed contract (CAP-0084) keep the muxed id in asset_balance_changes and on
// the contract_credited effect.
func TestCoreLCMMuxedContractDestination(t *testing.T) {
const (
sender = "GCE4HENKZ3ZIHQY4VEYCVX5ZE5LNDIN3FH4MHZCWFKXQZGQIOGAO77CN"
contract = "CAA3QKIP2SNVXUJTB4HKOGF55JTSSMQGED3FZYNHMNSXYV3DRRMAWA3Y"
)
type balanceChange struct {
ledger int32
changeType string
from string
muxedID string
muxedContract string
}
transfer := balanceChange{23, "transfer", sender, "987654321987654321",
"WAA3QKIP2SNVXUJTB4HKOGF55JTSSMQGED3FZYNHMNSXYV3DRRMAWDNU3JPX55ASWEDNS"}
mint := balanceChange{25, "mint", "", "111222333444555666",
"WAA3QKIP2SNVXUJTB4HKOGF55JTSSMQGED3FZYNHMNSXYV3DRRMAWAMLEQPXYDMDSLJLI"}
for file, expected := range map[string][]balanceChange{
"8fe0b7272f3a072f.xdr": {transfer, mint},
"938fb779c48ac3af.xdr": {transfer},
} {
t.Run(file, func(t *testing.T) {
historyQ := ingestCoreLCMFile(t, filepath.Join(coreTestLCMDir, "InvokeHostFunctionTests", file))
ctx := context.Background()
for _, want := range expected {
ops, _, err := historyQ.Operations().ForLedger(ctx, want.ledger).Fetch(ctx)
require.NoError(t, err)
require.Len(t, ops, 1)

var details struct {
AssetBalanceChanges []map[string]interface{} `json:"asset_balance_changes"`
}
require.NoError(t, ops[0].UnmarshalDetails(&details))
require.Len(t, details.AssetBalanceChanges, 1)
change := details.AssetBalanceChanges[0]
require.Equal(t, want.changeType, change["type"])
require.Equal(t, contract, change["to"])
require.Equal(t, want.muxedID, change["destination_muxed_id"])
Comment on lines +191 to +193
if want.from == "" {
require.NotContains(t, change, "from")
} else {
require.Equal(t, want.from, change["from"])
}

require.NoError(t, historyQ.Commit(),
"failed to commit transaction for ledger index %d", i)
ledgerEffects, err := historyQ.EffectsForLedger(ctx, want.ledger,
db2.PageQuery{Order: db2.OrderAscending, Limit: 10})
require.NoError(t, err)
var credits []history.Effect
for _, effect := range ledgerEffects {
if effect.Type == history.EffectContractCredited {
credits = append(credits, effect)
}

t.Logf("successfully ingested %d ledgers through all processors", len(ledgers))
})
}
require.Len(t, credits, 1)
// Decode the way the /effects endpoint does.
var credit effects.ContractCredited
require.NoError(t, credits[0].UnmarshalDetails(&credit))
require.Equal(t, contract, credit.Contract)
require.Equal(t, want.muxedContract, credit.ContractMuxed)
require.Equal(t, want.muxedID, strconv.FormatUint(credit.ContractMuxedID, 10))
}
})
}
Expand Down
Loading
Loading