Skip to content
Draft
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
30 changes: 23 additions & 7 deletions .github/workflows/horizon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
os: [ubuntu-24.04]
go: ["1.25", "1.26"]
pg: [14, 16]
protocol-version: [26, 27]
protocol-version: [27, 28]
runs-on: ${{ matrix.os }}
services:
postgres:
Expand All @@ -32,12 +32,23 @@ jobs:
env:
HORIZON_INTEGRATION_TESTS_ENABLED: true
HORIZON_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL: ${{ matrix.protocol-version }}
PROTOCOL_26_CORE_DOCKER_IMG: stellar/stellar-core:26.0.0-3089.8e43a2d3b.jammy
PROTOCOL_26_CORE_DEBIAN_PKG_VERSION: 26.0.0-3089.8e43a2d3b.jammy~buildtests
PROTOCOL_26_STELLAR_RPC_DOCKER_IMG: stellar/stellar-rpc:26.0.0
PROTOCOL_27_CORE_DOCKER_IMG: stellar/stellar-core:27.0.0-3288.7696c069d.jammy
PROTOCOL_27_CORE_DEBIAN_PKG_VERSION: 27.0.0-3288.7696c069d.jammy~buildtests
PROTOCOL_27_STELLAR_RPC_DOCKER_IMG: stellar/stellar-rpc:27.0.0-preview-190
# Protocol 28 (CAP-0084) runs against the vnext (next-protocol-enabled) core build.
# Pinned to build 3395 (4cdd2fc9e) — the -vnext image/deb matching CAP-0084
# core (stellar-core#5337, HEAD 4cdd2fc9e). Variant preserved from the p27 leg:
# plain -vnext docker + ~vnext~buildtests deb.
PROTOCOL_28_CORE_DOCKER_IMG: stellar/unsafe-stellar-core:27.1.1-3395.4cdd2fc9e.jammy-vnext
PROTOCOL_28_CORE_DEBIAN_PKG_VERSION: 27.1.1-3395.4cdd2fc9e.jammy~vnext~buildtests
# No Protocol 28 stellar-rpc image is published yet (tracked: stellar-rpc#837).
# Leave EMPTY on purpose: the P27 RPC image bundles a P27 captive-core that
# cannot ingest a protocol-28 network, so bringing it up makes every
# EnableStellarRPC test hang in waitForStellarRPC() ("DB is empty") until the
# 75m suite timeout. With this empty, NewTest() AMBER-skips RPC-dependent tests
# (sac/invokehostfunction/extend_footprint_ttl/txsub/txsub_async/transaction)
# on the P28 leg. Re-pin to a real P28 image once #789 publishes one.
PROTOCOL_28_STELLAR_RPC_DOCKER_IMG: ""
PGHOST: localhost
PGPORT: 5432
PGUSER: postgres
Expand Down Expand Up @@ -75,8 +86,13 @@ jobs:
- name: Pull and set Stellar RPC image
shell: bash
run: |
docker pull "$PROTOCOL_${{ matrix.protocol-version }}_STELLAR_RPC_DOCKER_IMG"
echo HORIZON_INTEGRATION_TESTS_STELLAR_RPC_DOCKER_IMG="$PROTOCOL_${{ matrix.protocol-version }}_STELLAR_RPC_DOCKER_IMG" >> $GITHUB_ENV
IMG="$PROTOCOL_${{ matrix.protocol-version }}_STELLAR_RPC_DOCKER_IMG"
if [ -n "$IMG" ]; then
docker pull "$IMG"
else
echo "::warning::No stellar-rpc image for protocol ${{ matrix.protocol-version }}; RPC-dependent tests will AMBER-skip (tracked: stellar-rpc#789)."
fi
echo "HORIZON_INTEGRATION_TESTS_STELLAR_RPC_DOCKER_IMG=$IMG" >> "$GITHUB_ENV"

- name: Install core
run: |
Expand All @@ -99,7 +115,7 @@ jobs:
- name: Calculate the source hash
id: calculate_source_hash
run: |
combined_hash=$(echo "horizon-hash-${{ hashFiles('./') }}-${{ hashFiles('./docker/**') }}-${{ hashFiles('./internal/**') }}-protocol-${{ matrix.protocol-version }}-${{ env.PROTOCOL_27_CORE_DOCKER_IMG }}-${{ env.PROTOCOL_27_STELLAR_RPC_DOCKER_IMG }}-${{ env.PROTOCOL_27_CORE_DEBIAN_PKG_VERSION }}-${{ env.PROTOCOL_26_CORE_DOCKER_IMG }}-${{ env.PREFIX }}" | sha256sum | cut -d ' ' -f 1)
combined_hash=$(echo "horizon-hash-${{ hashFiles('./') }}-${{ hashFiles('./docker/**') }}-${{ hashFiles('./internal/**') }}-protocol-${{ matrix.protocol-version }}-${{ env.PROTOCOL_28_CORE_DOCKER_IMG }}-${{ env.PROTOCOL_28_STELLAR_RPC_DOCKER_IMG }}-${{ env.PROTOCOL_28_CORE_DEBIAN_PKG_VERSION }}-${{ env.PROTOCOL_27_CORE_DOCKER_IMG }}-${{ env.PROTOCOL_27_STELLAR_RPC_DOCKER_IMG }}-${{ env.PROTOCOL_27_CORE_DEBIAN_PKG_VERSION }}-${{ env.PREFIX }}" | sha256sum | cut -d ' ' -f 1)
echo "COMBINED_SOURCE_HASH=$combined_hash" >> "$GITHUB_ENV"

- name: Restore Horizon binary and integration tests source hash to cache
Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/release-readiness.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Release readiness

# Validates that empty placeholder pins in `horizon.yml` get flipped to real
# values before a protocol-next branch is merged to a stable branch. Pairs
# with the AMBER-skip in `internal/test/integration/integration.go`: tests
# skip silently when their image env-var is empty (acceptable on
# protocol-next while waiting for the new-protocol image to publish), and
# this check makes sure that temporary state can't reach `main` undetected.

on:
pull_request:
push:
branches: [main, master, protocol-next]

jobs:
no-empty-rpc-image-pins:
name: No empty PROTOCOL_*_STELLAR_RPC_DOCKER_IMG values
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}

- name: Check horizon.yml for empty RPC image pins
env:
# Branches where empty pins are a hard failure. Any other target
# (typically protocol-next) gets a yellow-warning annotation but
# still passes the check.
STABLE_BRANCHES: 'main master'
run: |
set -e

# Match `PROTOCOL_<N>_STELLAR_RPC_DOCKER_IMG:` followed by either an
# empty quoted string or nothing-then-end-of-line. The leading
# whitespace anchor avoids matching the var name inside a comment.
empty=$(grep -nE '^[[:space:]]+PROTOCOL_[0-9]+_STELLAR_RPC_DOCKER_IMG:[[:space:]]*("[[:space:]]*"|$)' \
.github/workflows/horizon.yml || true)

if [ -z "$empty" ]; then
echo "All PROTOCOL_*_STELLAR_RPC_DOCKER_IMG values in horizon.yml are set."
exit 0
fi

# Resolve the effective target branch:
# - On a PR: github.base_ref is the destination branch ("main"/etc.)
# - On a push: github.ref_name is the branch being pushed
target="${{ github.base_ref || github.ref_name }}"

echo "Empty PROTOCOL_*_STELLAR_RPC_DOCKER_IMG entries found:"
echo "$empty"
echo

if echo "$STABLE_BRANCHES" | grep -qw "$target"; then
echo "::error::Empty PROTOCOL_*_STELLAR_RPC_DOCKER_IMG values are not allowed when targeting '$target'."
echo "Every matrix leg must point at a real stellar-rpc image before merging to a stable branch."
echo "If a P{N} stellar-rpc image is not published yet, the protocol-next branch is not ready to merge."
exit 1
else
echo "::warning::Empty PROTOCOL_*_STELLAR_RPC_DOCKER_IMG entries on '$target' are expected during a protocol-next transition."
echo "Flip these to real image refs before opening a PR to a stable branch (main/master)."
exit 0
fi
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ file. This project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Added
- Added Protocol 28 ingestion support for CAP-0084 (Muxed Contract Addresses):
SAC transfer/mint events to a muxed-contract destination surface the base
contract in the `to` topic and the mux id via `to_muxed_id`, and bumped
`MaxSupportedProtocolVersion` to 28 ([#203](https://github.com/stellar/stellar-horizon/pull/203)).

## 27.0.0

**This release adds support for Protocol 27**
Expand Down
4 changes: 3 additions & 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.6.0
github.com/stellar/go-stellar-sdk v0.6.1-0.20260616165505-26fec7c0e891
)

require (
Expand Down Expand Up @@ -170,3 +170,5 @@ require (
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/stellar/go-stellar-sdk => github.com/sisuresh/go v0.0.0-20260701230520-eeca7aa4a3bc
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,8 @@ github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c h1:aqg5Vm5dwtvL+Yg
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c/go.mod h1:owqhoLW1qZoYLZzLnBw+QkPP9WZnjlSWihhxAJC1+/M=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sisuresh/go v0.0.0-20260701230520-eeca7aa4a3bc h1:WVa7+1/bWNhqwKTBIZ+yHdANMwmXDb4VOym4bn2YpME=
github.com/sisuresh/go v0.0.0-20260701230520-eeca7aa4a3bc/go.mod h1:IkcqcrE9UQi7n/1y+MxKB+7qzdjG1T2kGOD7Ss8dqjw=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
Expand All @@ -429,8 +431,6 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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/stellar/go-stellar-sdk v0.6.0 h1:NM2oqZJQup0QxnJMq6C8s4iIIhU6rHFX0rlsF3wh/Ho=
github.com/stellar/go-stellar-sdk v0.6.0/go.mod h1:IkcqcrE9UQi7n/1y+MxKB+7qzdjG1T2kGOD7Ss8dqjw=
github.com/stellar/go-xdr v0.0.0-20260529210834-0bf8f4956364 h1:gOKrfuWdZ92LFlv0TAwgZ7OsWKeBsOMDlGLyFgduI1w=
github.com/stellar/go-xdr v0.0.0-20260529210834-0bf8f4956364/go.mod h1:If+U9Z1W5xU97VrOgJandQT+2dN7/iOpkCrxBJEyF80=
github.com/stellar/throttled v2.2.3-0.20190823235211-89d75816f59d+incompatible h1:jMXXAcz6xTarGDQ4VtVbtERogcmDQw4RaE85Cr9CgoQ=
Expand Down
21 changes: 14 additions & 7 deletions internal/ingest/contractevents/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,15 @@ 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
// valid for SAC events (those are only used for classic transaction memo mappings
// per CAP-67, which are processed through a different code path).
// For SAC events, to_muxed_id is always a uint64. It carries either the muxed
// account ID from a MuxedAddressObject (CAP-67) or, under CAP-0084, the muxed
// contract ID from a MUXED_CONTRACT ScAddress. In both cases the SAC host
// de-muxes the destination before emitting the event: the `to` topic holds the
// base (account or contract) address and the mux id is surfaced separately as a
// uint64, so the two are byte-identical in shape and indistinguishable from the
// event alone. This holds for both transfer and mint. 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) {
var foundAmount, foundMuxedId bool
var amount xdr.Int128Parts
Expand All @@ -346,9 +351,11 @@ 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).
// ScvBytes/ScvString are NOT valid here - those are only for classic
// transaction memo mappings which use a different code path.
// SAC events only emit uint64 for to_muxed_id (muxed account ID under
// CAP-67, or muxed contract ID under CAP-0084 - both de-muxed by the
// host to a uint64). ScvBytes/ScvString are NOT valid here - those are
// only for classic transaction memo mappings which use a different
// code path.
switch entry.Val.Type {
case xdr.ScValTypeScvU64:
if val, ok := entry.Val.GetU64(); ok {
Expand Down
54 changes: 54 additions & 0 deletions internal/ingest/contractevents/events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ var (
randomAccount = keypair.MustRandom().Address()
zeroContractHash = xdr.Hash([32]byte{})
zeroContract = strkey.MustEncode(strkey.VersionByteContract, zeroContractHash[:])
// muxedContractHash stands in for the base contract a CAP-0084
// MUXED_CONTRACT address de-muxes to. The host emits this base contract in
// the `to` topic and surfaces the mux id separately as a uint64.
muxedContractHash = xdr.Hash([32]byte{0xCA, 0xFE, 0xBA, 0xBE})
muxedContract = strkey.MustEncode(strkey.VersionByteContract, muxedContractHash[:])
)

// Test fixture structure
Expand Down Expand Up @@ -242,6 +247,55 @@ func TestStellarAssetContractEventParsing(t *testing.T) {
DestinationMemo: xdr.MemoID(12345),
},
},
{
// CAP-0084: a transfer to a muxed contract destination. The host
// de-muxes the MUXED_CONTRACT address, emitting the base contract in
// the `to` topic and the mux id as a uint64 to_muxed_id - byte-identical
// in shape to the CAP-67 muxed-account case above, by design.
name: "V4 transfer to muxed contract (CAP-0084)",
txMetaVersion: 4,
eventType: EventTypeTransfer,
topics: []xdr.ScVal{
makeSymbol("transfer"),
makeAddress(randomAccount),
makeAddress(muxedContract),
makeAsset(randomAsset),
},
data: makeV4MapData(big.NewInt(1000), xdr.MemoID(67890)),
asset: randomAsset,
contractID: mustGetContractID(randomAsset),
expectedResult: &StellarAssetContractEvent{
Type: EventTypeTransfer,
Asset: randomAsset,
From: randomAccount,
To: muxedContract,
Amount: xdr.Int128Parts{Lo: 1000, Hi: 0},
DestinationMemo: xdr.MemoID(67890),
},
},
{
// CAP-0084: a mint to a muxed contract destination. The mint path
// de-muxes the destination identically to transfer, so the muxed
// contract id surfaces as a uint64 to_muxed_id as well.
name: "V4 mint to muxed contract (CAP-0084)",
txMetaVersion: 4,
eventType: EventTypeMint,
topics: []xdr.ScVal{
makeSymbol("mint"),
makeAddress(muxedContract), // to (no admin in V4)
makeAsset(randomAsset),
},
data: makeV4MapData(big.NewInt(2000), xdr.MemoID(67890)),
asset: randomAsset,
contractID: mustGetContractID(randomAsset),
expectedResult: &StellarAssetContractEvent{
Type: EventTypeMint,
Asset: randomAsset,
To: muxedContract,
Amount: xdr.Int128Parts{Lo: 2000, Hi: 0},
DestinationMemo: xdr.MemoID(67890),
},
},
{
name: "V4 SAC event rejects ScvString for to_muxed_id",
txMetaVersion: 4,
Expand Down
2 changes: 1 addition & 1 deletion internal/ingest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import (
const (
// MaxSupportedProtocolVersion defines the maximum supported version of
// the Stellar protocol.
MaxSupportedProtocolVersion uint32 = 27
MaxSupportedProtocolVersion uint32 = 28

// CurrentVersion reflects the latest version of the ingestion
// algorithm. This value is stored in KV store and is used to decide
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
6 changes: 6 additions & 0 deletions internal/test/integration/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ func NewTest(t *testing.T, config Config) *Test {
t.Skip("skipping integration test: HORIZON_INTEGRATION_TESTS_ENABLED not set")
}

if config.EnableStellarRPC && os.Getenv("HORIZON_INTEGRATION_TESTS_STELLAR_RPC_DOCKER_IMG") == "" {
t.Skip("AMBER (tracked: stellar-rpc#789): no stellar-rpc image configured for this protocol; " +
"skipping rather than falling back to stellar/stellar-rpc:latest (a P27 RPC). " +
"Affects sac/invokehostfunction/extend_footprint_ttl/txsub/txsub_async/transaction.")
}

if config.ProtocolVersion == 0 {
// Default to the maximum supported protocol version
config.ProtocolVersion = ingest.MaxSupportedProtocolVersion
Expand Down
Loading