diff --git a/.github/workflows/horizon.yml b/.github/workflows/horizon.yml index 40c598e2..957bb34c 100644 --- a/.github/workflows/horizon.yml +++ b/.github/workflows/horizon.yml @@ -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: @@ -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 @@ -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: | @@ -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 diff --git a/.github/workflows/release-readiness.yml b/.github/workflows/release-readiness.yml new file mode 100644 index 00000000..be0d860a --- /dev/null +++ b/.github/workflows/release-readiness.yml @@ -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__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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 903e4e88..049aa0a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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** diff --git a/go.mod b/go.mod index 9a167888..07222c46 100644 --- a/go.mod +++ b/go.mod @@ -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 ( @@ -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 diff --git a/go.sum b/go.sum index c2d2600d..a28227b7 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= diff --git a/internal/ingest/contractevents/events.go b/internal/ingest/contractevents/events.go index a1b31940..15e6a1e2 100644 --- a/internal/ingest/contractevents/events.go +++ b/internal/ingest/contractevents/events.go @@ -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 @@ -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 { diff --git a/internal/ingest/contractevents/events_test.go b/internal/ingest/contractevents/events_test.go index e5118b98..fd6b44da 100644 --- a/internal/ingest/contractevents/events_test.go +++ b/internal/ingest/contractevents/events_test.go @@ -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 @@ -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, diff --git a/internal/ingest/main.go b/internal/ingest/main.go index aef8f51a..219c3641 100644 --- a/internal/ingest/main.go +++ b/internal/ingest/main.go @@ -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 diff --git a/internal/ingest/testdata/test-lcms/InvokeHostFunctionTests/221f1cad327fb891.xdr b/internal/ingest/testdata/test-lcms/InvokeHostFunctionTests/221f1cad327fb891.xdr index fffe8d03..0680d2ec 100644 Binary files a/internal/ingest/testdata/test-lcms/InvokeHostFunctionTests/221f1cad327fb891.xdr and b/internal/ingest/testdata/test-lcms/InvokeHostFunctionTests/221f1cad327fb891.xdr differ diff --git a/internal/ingest/testdata/test-lcms/InvokeHostFunctionTests/efd47464023e9e55.xdr b/internal/ingest/testdata/test-lcms/InvokeHostFunctionTests/efd47464023e9e55.xdr index 961bd702..047bf34f 100644 Binary files a/internal/ingest/testdata/test-lcms/InvokeHostFunctionTests/efd47464023e9e55.xdr and b/internal/ingest/testdata/test-lcms/InvokeHostFunctionTests/efd47464023e9e55.xdr differ diff --git a/internal/integration/testdata/load-test-fixtures-v28.xdr.zstd b/internal/integration/testdata/load-test-fixtures-v28.xdr.zstd new file mode 100644 index 00000000..321db899 Binary files /dev/null and b/internal/integration/testdata/load-test-fixtures-v28.xdr.zstd differ diff --git a/internal/integration/testdata/load-test-ledgers-v28.xdr.zstd b/internal/integration/testdata/load-test-ledgers-v28.xdr.zstd new file mode 100644 index 00000000..e6a50a3b Binary files /dev/null and b/internal/integration/testdata/load-test-ledgers-v28.xdr.zstd differ diff --git a/internal/integration/testdata/unlimited-config-v26.xdr b/internal/integration/testdata/unlimited-config-v28.xdr similarity index 100% rename from internal/integration/testdata/unlimited-config-v26.xdr rename to internal/integration/testdata/unlimited-config-v28.xdr diff --git a/internal/test/integration/integration.go b/internal/test/integration/integration.go index 7a8d3849..dec5bb28 100644 --- a/internal/test/integration/integration.go +++ b/internal/test/integration/integration.go @@ -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