From 7075610874e85cca0ad611d502de920323346edd Mon Sep 17 00:00:00 2001 From: cawthorne Date: Sat, 28 Mar 2026 13:15:45 +0000 Subject: [PATCH 01/35] cre: finish aptos local CRE read and write setup parity --- .github/workflows/cre-system-tests.yaml | 32 + core/capabilities/fakes/register.go | 20 + core/capabilities/fakes/register_test.go | 33 + core/capabilities/fakes/streams_trigger.go | 35 +- core/cmd/shell_local.go | 17 + core/config/toml/types.go | 37 + .../configs/capability_defaults.toml | 10 + .../configs/workflow-gateway-don-aptos.toml | 76 ++ .../environment/environment/environment.go | 10 +- .../cre/environment/mock/trigger_types.go | 5 +- core/scripts/go.mod | 12 +- core/scripts/go.sum | 6 +- core/services/chainlink/application.go | 16 + core/services/chainlink/config.go | 4 + core/services/chainlink/config_general.go | 4 + .../chainlink/config_imported_aptos_key.go | 21 + .../conversions/conversions.go | 18 + .../conversions/conversions_test.go | 32 + deployment/cre/jobs/aptos.go | 42 + deployment/cre/jobs/propose_job_spec.go | 24 +- deployment/cre/jobs/propose_job_spec_test.go | 133 +++ deployment/cre/jobs/types/job_spec.go | 5 +- .../cre/jobs/types/job_spec_template.go | 5 + .../cre/jobs/types/job_spec_template_test.go | 14 + deployment/cre/jobs/types/job_spec_test.go | 14 +- deployment/cre/ocr3/config.go | 3 +- .../cre/ocr3/v2/changeset/configure_ocr3.go | 23 +- .../operations/contracts/configure_ocr3.go | 2 + deployment/go.mod | 2 +- deployment/go.sum | 4 +- go.md | 17 + go.mod | 2 +- go.sum | 4 +- integration-tests/go.mod | 2 +- integration-tests/go.sum | 4 +- integration-tests/load/go.mod | 2 +- integration-tests/load/go.sum | 4 +- plugins/plugins.private.yaml | 6 +- plugins/plugins.public.yaml | 2 +- system-tests/lib/cre/contracts/keystone.go | 47 +- system-tests/lib/cre/contracts/ocr3.go | 2 + system-tests/lib/cre/don.go | 106 +- system-tests/lib/cre/don/config/config.go | 75 +- system-tests/lib/cre/don/secrets/secrets.go | 64 ++ .../lib/cre/don/secrets/secrets_test.go | 65 ++ system-tests/lib/cre/don_test.go | 82 ++ .../environment/blockchains/aptos/aptos.go | 357 +++++++ .../blockchains/aptos/aptos_test.go | 36 + .../cre/environment/blockchains/sets/sets.go | 2 + .../lib/cre/environment/config/config.go | 4 +- system-tests/lib/cre/environment/dons.go | 9 +- .../lib/cre/environment/environment.go | 27 +- system-tests/lib/cre/features/aptos/aptos.go | 949 ++++++++++++++++++ .../lib/cre/features/aptos/aptos_test.go | 234 +++++ .../cre/features/consensus/v2/consensus.go | 17 +- system-tests/lib/cre/features/evm/v2/evm.go | 6 + .../features/read_contract/read_contract.go | 97 +- .../read_contract/read_contract_test.go | 45 + system-tests/lib/cre/features/sets/sets.go | 2 + .../lib/cre/features/solana/v2/solana.go | 8 +- system-tests/lib/cre/flags/flags.go | 5 +- system-tests/lib/cre/flags/flags_test.go | 25 + system-tests/lib/cre/flags/provider.go | 3 + .../lib/cre/ocr_extra_signer_families.go | 44 + system-tests/lib/cre/types.go | 43 +- system-tests/lib/cre/types_nodekeys_test.go | 58 ++ system-tests/lib/crypto/aptos.go | 49 + system-tests/lib/go.mod | 6 +- system-tests/lib/go.sum | 4 +- system-tests/tests/go.mod | 12 +- system-tests/tests/go.sum | 4 +- .../cre/aptos/aptosread/config/config.go | 8 + .../tests/smoke/cre/aptos/aptosread/go.mod | 20 + .../tests/smoke/cre/aptos/aptosread/go.sum | 26 + .../tests/smoke/cre/aptos/aptosread/main.go | 113 +++ .../cre/aptos/aptoswrite/config/config.go | 18 + .../tests/smoke/cre/aptos/aptoswrite/go.mod | 20 + .../tests/smoke/cre/aptos/aptoswrite/go.sum | 26 + .../tests/smoke/cre/aptos/aptoswrite/main.go | 285 ++++++ .../aptoswriteroundtrip/config/config.go | 15 + .../cre/aptos/aptoswriteroundtrip/go.mod | 20 + .../cre/aptos/aptoswriteroundtrip/go.sum | 26 + .../cre/aptos/aptoswriteroundtrip/main.go | 224 +++++ .../tests/smoke/cre/cre_suite_test.go | 6 + .../smoke/cre/v2_aptos_capability_test.go | 755 ++++++++++++++ .../tests/test-helpers/before_suite.go | 33 +- system-tests/tests/test-helpers/t_helpers.go | 29 + 87 files changed, 4648 insertions(+), 165 deletions(-) create mode 100644 core/capabilities/fakes/register.go create mode 100644 core/capabilities/fakes/register_test.go create mode 100644 core/scripts/cre/environment/configs/workflow-gateway-don-aptos.toml create mode 100644 core/services/chainlink/config_imported_aptos_key.go create mode 100644 deployment/cre/jobs/aptos.go create mode 100644 system-tests/lib/cre/don/secrets/secrets_test.go create mode 100644 system-tests/lib/cre/don_test.go create mode 100644 system-tests/lib/cre/environment/blockchains/aptos/aptos.go create mode 100644 system-tests/lib/cre/environment/blockchains/aptos/aptos_test.go create mode 100644 system-tests/lib/cre/features/aptos/aptos.go create mode 100644 system-tests/lib/cre/features/aptos/aptos_test.go create mode 100644 system-tests/lib/cre/features/read_contract/read_contract_test.go create mode 100644 system-tests/lib/cre/flags/flags_test.go create mode 100644 system-tests/lib/cre/ocr_extra_signer_families.go create mode 100644 system-tests/lib/cre/types_nodekeys_test.go create mode 100644 system-tests/lib/crypto/aptos.go create mode 100644 system-tests/tests/smoke/cre/aptos/aptosread/config/config.go create mode 100644 system-tests/tests/smoke/cre/aptos/aptosread/go.mod create mode 100644 system-tests/tests/smoke/cre/aptos/aptosread/go.sum create mode 100644 system-tests/tests/smoke/cre/aptos/aptosread/main.go create mode 100644 system-tests/tests/smoke/cre/aptos/aptoswrite/config/config.go create mode 100644 system-tests/tests/smoke/cre/aptos/aptoswrite/go.mod create mode 100644 system-tests/tests/smoke/cre/aptos/aptoswrite/go.sum create mode 100644 system-tests/tests/smoke/cre/aptos/aptoswrite/main.go create mode 100644 system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/config/config.go create mode 100644 system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/go.mod create mode 100644 system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/go.sum create mode 100644 system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/main.go create mode 100644 system-tests/tests/smoke/cre/v2_aptos_capability_test.go diff --git a/.github/workflows/cre-system-tests.yaml b/.github/workflows/cre-system-tests.yaml index ba27f356917..d9d07a06c7e 100644 --- a/.github/workflows/cre-system-tests.yaml +++ b/.github/workflows/cre-system-tests.yaml @@ -75,6 +75,9 @@ jobs: # Add list of tests with certain topologies PER_TEST_TOPOLOGIES_JSON=${PER_TEST_TOPOLOGIES_JSON:-'{ + "Test_CRE_V2_Aptos_Suite": [ + {"topology":"workflow-gateway-aptos","configs":"configs/workflow-gateway-don-aptos.toml"} + ], "Test_CRE_V2_Solana_Suite": [ {"topology":"workflow","configs":"configs/workflow-don-solana.toml"} ], @@ -213,6 +216,35 @@ jobs: chmod +x bin/ctf echo "::endgroup::" + - name: Install Aptos CLI + if: ${{ matrix.tests.test_name == 'Test_CRE_V2_Aptos_Suite' }} + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + APTOS_CLI_TAG: "aptos-cli-v7.8.0" + run: | + echo "::startgroup::Install Aptos CLI" + bin_dir="$HOME/.local/bin" + mkdir -p "$bin_dir" + + gh release download "${APTOS_CLI_TAG}" \ + --pattern "aptos-cli-*-Ubuntu-24.04-x86_64.zip" \ + --clobber \ + --repo aptos-labs/aptos-core \ + -O aptos-cli.zip + + unzip -o aptos-cli.zip -d aptos-cli-extract >/dev/null + aptos_path="$(find aptos-cli-extract -type f -name aptos | head -n1)" + if [[ -z "$aptos_path" ]]; then + echo "failed to locate aptos binary in release archive" + exit 1 + fi + + install -m 0755 "$aptos_path" "$bin_dir/aptos" + echo "$bin_dir" >> "$GITHUB_PATH" + "$bin_dir/aptos" --version + echo "::endgroup::" + - name: Start local CRE${{ matrix.tests.cre_version }} shell: bash id: start-local-cre diff --git a/core/capabilities/fakes/register.go b/core/capabilities/fakes/register.go new file mode 100644 index 00000000000..522c0a7bc50 --- /dev/null +++ b/core/capabilities/fakes/register.go @@ -0,0 +1,20 @@ +package fakes + +import ( + "context" + "fmt" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types/core" +) + +const EnableFakeStreamsTriggerEnvVar = "CL_ENABLE_FAKE_STREAMS_TRIGGER" + +func RegisterFakeStreamsTrigger(ctx context.Context, lggr logger.Logger, registry core.CapabilitiesRegistry, nSigners int) (*fakeStreamsTrigger, error) { + trigger := NewFakeStreamsTrigger(lggr, nSigners) + if err := registry.Add(ctx, trigger); err != nil { + return nil, fmt.Errorf("add fake streams trigger: %w", err) + } + + return trigger, nil +} diff --git a/core/capabilities/fakes/register_test.go b/core/capabilities/fakes/register_test.go new file mode 100644 index 00000000000..b71429e0977 --- /dev/null +++ b/core/capabilities/fakes/register_test.go @@ -0,0 +1,33 @@ +package fakes + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + corecaps "github.com/smartcontractkit/chainlink/v2/core/capabilities" +) + +func TestRegisterFakeStreamsTrigger(t *testing.T) { + registry := corecaps.NewRegistry(logger.Test(t)) + + trigger, err := RegisterFakeStreamsTrigger(t.Context(), logger.Test(t), registry, 4) + require.NoError(t, err) + require.NotNil(t, trigger) + + capability, err := registry.Get(t.Context(), "streams-trigger@1.0.0") + require.NoError(t, err) + + info, err := capability.Info(t.Context()) + require.NoError(t, err) + require.Equal(t, "streams-trigger@1.0.0", info.ID) +} + +func TestNewFakeStreamsTrigger_UsesDeterministicSigners(t *testing.T) { + triggerA := NewFakeStreamsTrigger(logger.Test(t), 4) + triggerB := NewFakeStreamsTrigger(logger.Test(t), 4) + + require.Equal(t, triggerA.meta.Signers, triggerB.meta.Signers) + require.Equal(t, triggerA.meta.MinRequiredSignatures, triggerB.meta.MinRequiredSignatures) +} diff --git a/core/capabilities/fakes/streams_trigger.go b/core/capabilities/fakes/streams_trigger.go index ac2ae56346d..278a339cf25 100644 --- a/core/capabilities/fakes/streams_trigger.go +++ b/core/capabilities/fakes/streams_trigger.go @@ -2,6 +2,7 @@ package fakes import ( "context" + "crypto/ecdsa" "encoding/hex" "errors" "fmt" @@ -9,11 +10,12 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" "github.com/smartcontractkit/libocr/offchainreporting2/chains/evmutil" ocrTypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - "github.com/smartcontractkit/chainlink-common/keystore/corekeys" commonCap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers" @@ -23,7 +25,6 @@ import ( v3 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v3" "github.com/smartcontractkit/chainlink-evm/pkg/mercury/v3/reportcodec" - "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" "github.com/smartcontractkit/chainlink/v2/core/capabilities/streams" ) @@ -32,7 +33,7 @@ type fakeStreamsTrigger struct { eng *services.Engine lggr logger.Logger - signers []ocr2key.KeyBundle + signers []fakeStreamsTriggerSigner codec datastreams.ReportCodec meta datastreams.Metadata @@ -47,6 +48,10 @@ type regState struct { eventCh chan commonCap.TriggerResponse } +type fakeStreamsTriggerSigner struct { + privateKey *ecdsa.PrivateKey +} + var _ services.Service = (*fakeStreamsTrigger)(nil) var _ commonCap.TriggerCapability = (*fakeStreamsTrigger)(nil) @@ -108,10 +113,16 @@ func (st *fakeStreamsTrigger) UnregisterTrigger(ctx context.Context, request com } func NewFakeStreamsTrigger(lggr logger.Logger, nSigners int) *fakeStreamsTrigger { - signers := make([]ocr2key.KeyBundle, nSigners) + signers := make([]fakeStreamsTriggerSigner, nSigners) rawSigners := make([][]byte, nSigners) for i := range nSigners { - signers[i], _ = ocr2key.New(corekeys.EVM) + keyMaterial := make([]byte, 32) + keyMaterial[31] = byte(i + 1) + privateKey, err := crypto.ToECDSA(keyMaterial) + if err != nil { + panic(err) + } + signers[i] = fakeStreamsTriggerSigner{privateKey: privateKey} rawSigners[i] = signers[i].PublicKey() } @@ -225,6 +236,20 @@ func newReport(ctx context.Context, lggr logger.Logger, feedID [32]byte, price i return raw } +func (s fakeStreamsTriggerSigner) PublicKey() ocrTypes.OnchainPublicKey { + address := crypto.PubkeyToAddress(s.privateKey.PublicKey) + return common.CopyBytes(address[:]) +} + +func (s fakeStreamsTriggerSigner) Sign(reportCtx ocrTypes.ReportContext, report ocrTypes.Report) ([]byte, error) { + rawReportContext := evmutil.RawReportContext(reportCtx) + sigData := crypto.Keccak256(report) + sigData = append(sigData, rawReportContext[0][:]...) + sigData = append(sigData, rawReportContext[1][:]...) + sigData = append(sigData, rawReportContext[2][:]...) + return crypto.Sign(crypto.Keccak256(sigData), s.privateKey) +} + func rawReportContext(reportCtx ocrTypes.ReportContext) []byte { rc := evmutil.RawReportContext(reportCtx) flat := []byte{} diff --git a/core/cmd/shell_local.go b/core/cmd/shell_local.go index d9ccf99f9e2..6184384ed2a 100644 --- a/core/cmd/shell_local.go +++ b/core/cmd/shell_local.go @@ -30,6 +30,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/custmsg" "github.com/smartcontractkit/chainlink-common/pkg/logger/otelzap" "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" + coreconfig "github.com/smartcontractkit/chainlink/v2/core/config" "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" "github.com/smartcontractkit/chainlink-evm/pkg/assets" @@ -505,6 +506,10 @@ func (s *Shell) runNode(c *cli.Context) error { } } + type importedAptosKeyConfig interface { + ImportedAptosKey() coreconfig.ImportableKey + } + if s.Config.P2P().Enabled() { if s.Config.ImportedP2PKey().JSON() != "" { lggr.Debugf("Importing p2p key %s", s.Config.ImportedP2PKey().JSON()) @@ -552,6 +557,18 @@ func (s *Shell) runNode(c *cli.Context) error { } } if s.Config.AptosEnabled() { + if cfg, ok := s.Config.(importedAptosKeyConfig); ok { + if k := cfg.ImportedAptosKey(); k != nil && k.JSON() != "" { + lggr.Debug("Importing aptos key") + _, err2 := app.GetKeyStore().Aptos().Import(rootCtx, []byte(k.JSON()), k.Password()) + if errors.Is(err2, keystore.ErrKeyExists) { + lggr.Debugf("Aptos key already exists %s", k.JSON()) + } else if err2 != nil { + return s.errorOut(fmt.Errorf("error importing aptos key: %w", err2)) + } + } + } + err2 := app.GetKeyStore().Aptos().EnsureKey(rootCtx) if err2 != nil { return fmt.Errorf("failed to ensure aptos key: %w", err2) diff --git a/core/config/toml/types.go b/core/config/toml/types.go index 74984ec5db9..e47f7c16b83 100644 --- a/core/config/toml/types.go +++ b/core/config/toml/types.go @@ -146,6 +146,7 @@ type Secrets struct { Threshold ThresholdKeyShareSecrets `toml:",omitempty"` EVM EthKeys `toml:",omitempty"` // choose EVM as the TOML field name to align with relayer config convention Solana SolKeys `toml:",omitempty"` // choose Solana as the TOML field name to align with relayer config convention + Aptos AptosKey `toml:",omitempty"` P2PKey P2PKey `toml:",omitempty"` DKGRecipientKey DKGRecipientKey `toml:",omitempty"` @@ -164,6 +165,11 @@ type SolKey struct { Password *models.Secret } +type AptosKey struct { + JSON *models.Secret + Password *models.Secret +} + func (s *SolKeys) SetFrom(f *SolKeys) error { err := s.validateMerge(f) if err != nil { @@ -245,6 +251,37 @@ func (e *SolKey) ValidateConfig() (err error) { return err } +func (p *AptosKey) SetFrom(f *AptosKey) (err error) { + err = p.validateMerge(f) + if err != nil { + return err + } + if v := f.JSON; v != nil { + p.JSON = v + } + if v := f.Password; v != nil { + p.Password = v + } + return nil +} + +func (p *AptosKey) validateMerge(f *AptosKey) (err error) { + if p.JSON != nil && f.JSON != nil { + err = errors.Join(err, configutils.ErrOverride{Name: "JSON"}) + } + if p.Password != nil && f.Password != nil { + err = errors.Join(err, configutils.ErrOverride{Name: "Password"}) + } + return err +} + +func (p *AptosKey) ValidateConfig() (err error) { + if (p.JSON != nil) != (p.Password != nil) { + err = errors.Join(err, configutils.ErrInvalid{Name: "AptosKey", Value: p.JSON, Msg: "all fields must be nil or non-nil"}) + } + return err +} + type EthKeys struct { Keys []*EthKey } diff --git a/core/scripts/cre/environment/configs/capability_defaults.toml b/core/scripts/cre/environment/configs/capability_defaults.toml index fad176d9741..6e46cd8d894 100644 --- a/core/scripts/cre/environment/configs/capability_defaults.toml +++ b/core/scripts/cre/environment/configs/capability_defaults.toml @@ -130,6 +130,16 @@ # FromAddress = "0x0000000000000000000000000000000000000000" # ForwarderAddress = "0x0000000000000000000000000000000000000000" +# Aptos chain capability plugin (View + WriteReport). Runtime values are injected per chain. +[capability_configs.write-aptos] + binary_name = "aptos" + +[capability_configs.write-aptos.values] + # ChainID and forwarder address are injected at job proposal time. + RequestTimeout = "30s" + TransmissionSchedule = "allAtOnce" + DeltaStage = "1500ms" + [capability_configs.solana.values] TxAcceptanceState = 3 TxRetentonTimeout = "120s" diff --git a/core/scripts/cre/environment/configs/workflow-gateway-don-aptos.toml b/core/scripts/cre/environment/configs/workflow-gateway-don-aptos.toml new file mode 100644 index 00000000000..db91c3d170f --- /dev/null +++ b/core/scripts/cre/environment/configs/workflow-gateway-don-aptos.toml @@ -0,0 +1,76 @@ +# Same as workflow-gateway-don.toml but with Aptos chain and Aptos read capability plumbing. +# Anvil 1337: registry and gateway. Aptos: local devnet (chain_id 4). Run: env config path , then env start. + +[[blockchains]] + type = "anvil" + chain_id = "1337" + container_name = "anvil-1337" + docker_cmd_params = ["-b", "0.5", "--mixed-mining"] + +[[blockchains]] + type = "aptos" + chain_id = "4" + +[jd] + csa_encryption_key = "d1093c0060d50a3c89c189b2e485da5a3ce57f3dcb38ab7e2c0d5f0bb2314a44" + # change to your version + image = "job-distributor:0.22.1" + +#[s3provider] +# # use all defaults +# port = 9000 +# console_port = 9001 + +[infra] + # either "docker" or "kubernetes" + type = "docker" + +[[nodesets]] + nodes = 4 + name = "workflow" + don_types = ["workflow"] + override_mode = "all" + http_port_range_start = 10100 + + supported_evm_chains = [1337] + env_vars = { CL_CRE_SETTINGS_DEFAULT = '{"PerWorkflow":{"CapabilityCallTimeout":"5m0s","ChainAllowed":{"Default":"false","Values":{"1337":"true","4457093679053095497":"true"}},"ChainWrite":{"EVM":{"GasLimit":{"Default":"5000000","Values":{"1337":"10000000"}}}}}}' } + capabilities = ["cron", "consensus", "read-contract-4", "write-aptos-4"] + registry_based_launch_allowlist = ["cron-trigger@1.0.0"] + + [nodesets.db] + image = "postgres:12.0" + port = 13000 + + [[nodesets.node_specs]] + roles = ["plugin"] + [nodesets.node_specs.node] + docker_ctx = "../../../.." + docker_file = "core/chainlink.Dockerfile" + docker_build_args = { "CL_IS_PROD_BUILD" = "false" } + # image = "chainlink-tmp:latest" + user_config_overrides = "" + +[[nodesets]] + nodes = 1 + name = "bootstrap-gateway" + don_types = ["bootstrap", "gateway"] + override_mode = "each" + http_port_range_start = 10300 + + supported_evm_chains = [1337] + + [nodesets.db] + image = "postgres:12.0" + port = 13200 + + [[nodesets.node_specs]] + roles = ["bootstrap", "gateway"] + [nodesets.node_specs.node] + docker_ctx = "../../../.." + docker_file = "core/chainlink.Dockerfile" + docker_build_args = { "CL_IS_PROD_BUILD" = "false" } + # 5002 is the web API capabilities port for incoming requests + # 15002 is the vault port for incoming requests + custom_ports = ["5002:5002", "15002:15002"] + # image = "chainlink-tmp:latest" + user_config_overrides = "" diff --git a/core/scripts/cre/environment/environment/environment.go b/core/scripts/cre/environment/environment/environment.go index 58f11f11353..e39de360694 100644 --- a/core/scripts/cre/environment/environment/environment.go +++ b/core/scripts/cre/environment/environment/environment.go @@ -330,8 +330,16 @@ func startCmd() *cobra.Command { } features := feature_set.New() + extraAllowedPorts := append([]int(nil), extraAllowedGatewayPorts...) + if in.Fake != nil { + extraAllowedPorts = append(extraAllowedPorts, in.Fake.Port) + } + if in.FakeHTTP != nil { + extraAllowedPorts = append(extraAllowedPorts, in.FakeHTTP.Port) + } + gatewayWhitelistConfig := gateway.WhitelistConfig{ - ExtraAllowedPorts: append(extraAllowedGatewayPorts, in.Fake.Port, in.FakeHTTP.Port), + ExtraAllowedPorts: extraAllowedPorts, ExtraAllowedIPsCIDR: []string{"0.0.0.0/0"}, } output, startErr := StartCLIEnvironment(cmdContext, relativePathToRepoRoot, in, nil, features, nil, envDependencies, gatewayWhitelistConfig) diff --git a/core/scripts/cre/environment/mock/trigger_types.go b/core/scripts/cre/environment/mock/trigger_types.go index 5725b892a2f..9e747c76586 100644 --- a/core/scripts/cre/environment/mock/trigger_types.go +++ b/core/scripts/cre/environment/mock/trigger_types.go @@ -5,9 +5,10 @@ import ( "time" "github.com/google/uuid" - cron2 "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" "google.golang.org/protobuf/types/known/anypb" + crontypedapi "github.com/smartcontractkit/chainlink-common/pkg/capabilities/v2/triggers/cron" + pb2 "github.com/smartcontractkit/chainlink/system-tests/lib/cre/mock/pb" ) @@ -24,7 +25,7 @@ func getTriggerRequest(triggerType TriggerType) (*pb2.SendTriggerEventRequest, e switch triggerType { case TriggerTypeCron: // First create the payload - payload := &cron2.LegacyPayload{ //nolint:staticcheck // legacy + payload := &crontypedapi.LegacyPayload{ //nolint:staticcheck // legacy ScheduledExecutionTime: time.Now().Format(time.RFC3339Nano), } diff --git a/core/scripts/go.mod b/core/scripts/go.mod index bb9d7fac716..249792562db 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -13,10 +13,7 @@ replace github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examp // Using a separate `require` here to avoid surrounding line changes // creating potential merge conflicts. -require ( - github.com/smartcontractkit/chainlink/deployment v0.0.0-20251021194914-c0e3fec1a97c - github.com/smartcontractkit/chainlink/v2 v2.32.0 -) +require github.com/smartcontractkit/chainlink/v2 v2.32.0 require ( github.com/Masterminds/semver/v3 v3.4.0 @@ -58,8 +55,8 @@ require ( github.com/smartcontractkit/chainlink-testing-framework/lib v1.54.5 github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based v0.0.0-00010101000000-000000000000 - github.com/smartcontractkit/chainlink/system-tests/lib v0.0.0-20251020210257-0a6ec41648b4 - github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.0 + github.com/smartcontractkit/chainlink/deployment v0.0.0-20251021194914-c0e3fec1a97c + github.com/smartcontractkit/chainlink/system-tests/lib v0.0.0-00010101000000-000000000000 github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 @@ -483,7 +480,7 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20260129135848-c86808ba5cb9 // indirect github.com/smartcontractkit/ccip-owner-contracts v0.1.0 // indirect - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 // indirect + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 // indirect github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260323224438-d819cb3228e1 // indirect github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260317185256-d5f7db87ae70 // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 // indirect @@ -519,7 +516,6 @@ require ( github.com/smartcontractkit/chainlink-ton v0.0.0-20260326230916-bcfdbe85f221 // indirect github.com/smartcontractkit/chainlink-ton/deployment v0.0.0-20260326230916-bcfdbe85f221 // indirect github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260218133534-cbd44da2856b // indirect - github.com/smartcontractkit/cre-sdk-go v1.5.0 // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect github.com/smartcontractkit/mcms v0.38.2 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 97c3f8d91d7..6b7e62398f0 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1616,8 +1616,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= @@ -1724,8 +1724,6 @@ github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.202602181 github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20260218133534-cbd44da2856b/go.mod h1:ea1LESxlSSOgc2zZBqf1RTkXTMthHaspdqUHd7W4lF0= github.com/smartcontractkit/cre-sdk-go v1.5.0 h1:kepW3QDKARrOOHjXwWAZ9j5KLk6bxLzvi6OMrLsFwVo= github.com/smartcontractkit/cre-sdk-go v1.5.0/go.mod h1:yYrQFz1UH7hhRbPO0q4fgo1tfsJNd4yXnI3oCZE0RzM= -github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.0 h1:qBZ4y6qlTOynSpU1QAi2Fgr3tUZQ332b6hit9EVZqkk= -github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v1.3.0/go.mod h1:Rzhy75vD3FqQo/SV6lypnxIwjWac6IOWzI5BYj3tYMU= github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad h1:lgHxTHuzJIF3Vj6LSMOnjhqKgRqYW+0MV2SExtCYL1Q= github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index 4803a14faa1..581584d8087 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -7,6 +7,7 @@ import ( "fmt" "math/big" "net/http" + "os" "strconv" "sync" "time" @@ -56,6 +57,7 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/build" "github.com/smartcontractkit/chainlink/v2/core/capabilities" "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/fakes" "github.com/smartcontractkit/chainlink/v2/core/config" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/logger/audit" @@ -241,6 +243,20 @@ func NewApplication(ctx context.Context, opts ApplicationOpts) (Application, err // for tests only, in prod Registry should always be set at this point opts.CapabilitiesRegistry = capabilities.NewRegistry(globalLogger) } + if raw := os.Getenv(fakes.EnableFakeStreamsTriggerEnvVar); raw != "" { + enabled, parseErr := strconv.ParseBool(raw) + if parseErr != nil { + return nil, fmt.Errorf("failed to parse %s: %w", fakes.EnableFakeStreamsTriggerEnvVar, parseErr) + } + if enabled { + trigger, registerErr := fakes.RegisterFakeStreamsTrigger(ctx, globalLogger, opts.CapabilitiesRegistry, 4) + if registerErr != nil { + return nil, fmt.Errorf("failed to register fake streams trigger: %w", registerErr) + } + srvcs = append(srvcs, trigger) + globalLogger.Infow("enabled fake streams trigger", "envVar", fakes.EnableFakeStreamsTriggerEnvVar) + } + } if opts.DonTimeStore == nil { opts.DonTimeStore = dontime.NewStore(dontime.DefaultRequestTimeout) diff --git a/core/services/chainlink/config.go b/core/services/chainlink/config.go index 020f74a7f57..160beb95e30 100644 --- a/core/services/chainlink/config.go +++ b/core/services/chainlink/config.go @@ -417,6 +417,10 @@ func (s *Secrets) SetFrom(f *Secrets) (err error) { err = errors.Join(err, commonconfig.NamedMultiErrorList(err2, "Solana")) } + if err2 := s.Aptos.SetFrom(&f.Aptos); err2 != nil { + err = errors.Join(err, commonconfig.NamedMultiErrorList(err2, "Aptos")) + } + if err2 := s.DKGRecipientKey.SetFrom(&f.DKGRecipientKey); err2 != nil { err = errors.Join(err, commonconfig.NamedMultiErrorList(err2, "DKGRecipientKey")) } diff --git a/core/services/chainlink/config_general.go b/core/services/chainlink/config_general.go index 38a50faacfd..fc0f2a0d1ea 100644 --- a/core/services/chainlink/config_general.go +++ b/core/services/chainlink/config_general.go @@ -565,6 +565,10 @@ func (g *generalConfig) ImportedSolKeys() coreconfig.ImportableChainKeyLister { return &importedSolKeyConfigs{s: g.secrets.Solana} } +func (g *generalConfig) ImportedAptosKey() coreconfig.ImportableKey { + return &importedAptosKeyConfig{s: g.secrets.Aptos} +} + func (g *generalConfig) ImportedDKGRecipientKey() coreconfig.ImportableKey { return &importedDKGRecipientKeyConfig{s: g.secrets.DKGRecipientKey} } diff --git a/core/services/chainlink/config_imported_aptos_key.go b/core/services/chainlink/config_imported_aptos_key.go new file mode 100644 index 00000000000..cadf5bb1b43 --- /dev/null +++ b/core/services/chainlink/config_imported_aptos_key.go @@ -0,0 +1,21 @@ +package chainlink + +import "github.com/smartcontractkit/chainlink/v2/core/config/toml" + +type importedAptosKeyConfig struct { + s toml.AptosKey +} + +func (t *importedAptosKeyConfig) JSON() string { + if t.s.JSON == nil { + return "" + } + return string(*t.s.JSON) +} + +func (t *importedAptosKeyConfig) Password() string { + if t.s.Password == nil { + return "" + } + return string(*t.s.Password) +} diff --git a/core/services/standardcapabilities/conversions/conversions.go b/core/services/standardcapabilities/conversions/conversions.go index 3f2130a7b19..257d7c025fd 100644 --- a/core/services/standardcapabilities/conversions/conversions.go +++ b/core/services/standardcapabilities/conversions/conversions.go @@ -25,6 +25,22 @@ func GetCapabilityIDFromCommand(command string, config string) string { return "" } return "evm:ChainSelector:" + strconv.FormatUint(selector, 10) + "@1.0.0" + case "aptos": + var cfg struct { + ChainID string `json:"chainId"` + } + if err := json.Unmarshal([]byte(config), &cfg); err != nil { + return "" + } + chainID, err := strconv.ParseUint(cfg.ChainID, 10, 64) + if err != nil { + return "" + } + selector, ok := chainselectors.AptosChainIdToChainSelector()[chainID] + if !ok { + return "" + } + return "aptos:ChainSelector:" + strconv.FormatUint(selector, 10) + "@1.0.0" case "consensus": return "consensus@1.0.0-alpha" case "cron": @@ -44,6 +60,8 @@ func GetCommandFromCapabilityID(capabilityID string) string { switch { case strings.HasPrefix(capabilityID, "evm"): return "evm" + case strings.HasPrefix(capabilityID, "aptos:ChainSelector:"): + return "aptos" case strings.HasPrefix(capabilityID, "consensus"): return "consensus" case strings.HasPrefix(capabilityID, "cron-trigger"): diff --git a/core/services/standardcapabilities/conversions/conversions_test.go b/core/services/standardcapabilities/conversions/conversions_test.go index f5de925893a..b410cce9ba6 100644 --- a/core/services/standardcapabilities/conversions/conversions_test.go +++ b/core/services/standardcapabilities/conversions/conversions_test.go @@ -43,6 +43,24 @@ func Test_GetCapabilityIDFromCommand(t *testing.T) { config: `{"chainId": 1, "network": "mainnet", "otherField": "value"}`, expected: "evm:ChainSelector:5009297550715157269@1.0.0", }, + { + name: "aptos command with valid config - localnet", + command: "/usr/local/bin/aptos", + config: `{"chainId":"4","network":"aptos"}`, + expected: "aptos:ChainSelector:4457093679053095497@1.0.0", + }, + { + name: "aptos command with invalid chainId", + command: "/usr/local/bin/aptos", + config: `{"chainId":"not-a-number","network":"aptos"}`, + expected: "", + }, + { + name: "aptos command with unknown chainId", + command: "/usr/local/bin/aptos", + config: `{"chainId":"999999","network":"aptos"}`, + expected: "", + }, { name: "evm command with invalid JSON", command: "/usr/local/bin/evm", @@ -173,6 +191,16 @@ func Test_GetCommandFromCapabilityID(t *testing.T) { capabilityID: "evm:ChainSelector:5009297550715157269@2.0.0", expected: "evm", }, + { + name: "aptos localnet capability", + capabilityID: "aptos:ChainSelector:4457093679053095497@1.0.0", + expected: "aptos", + }, + { + name: "aptos capability - different version", + capabilityID: "aptos:ChainSelector:4457093679053095497@2.0.0", + expected: "aptos", + }, { name: "unknown capability", capabilityID: "unknown@1.0.0", @@ -207,4 +235,8 @@ func Test_roundTrip(t *testing.T) { // EVM round-trip: command base name is preserved evmCapID := GetCapabilityIDFromCommand("/usr/local/bin/evm", `{"chainId": 1}`) assert.Equal(t, "evm", GetCommandFromCapabilityID(evmCapID)) + + // Aptos round-trip: command base name is preserved + aptosCapID := GetCapabilityIDFromCommand("/usr/local/bin/aptos", `{"chainId":"4","network":"aptos"}`) + assert.Equal(t, "aptos", GetCommandFromCapabilityID(aptosCapID)) } diff --git a/deployment/cre/jobs/aptos.go b/deployment/cre/jobs/aptos.go new file mode 100644 index 00000000000..a9a06a4d01d --- /dev/null +++ b/deployment/cre/jobs/aptos.go @@ -0,0 +1,42 @@ +package jobs + +import ( + "errors" + "strings" + + "github.com/smartcontractkit/chainlink/deployment/cre/jobs/pkg" + job_types "github.com/smartcontractkit/chainlink/deployment/cre/jobs/types" + "github.com/smartcontractkit/chainlink/v2/core/services/ocrcommon" +) + +func verifyAptosJobSpecInputs(inputs job_types.JobSpecInput) error { + scj := &pkg.StandardCapabilityJob{} + if err := inputs.UnmarshalTo(scj); err != nil { + return errors.New("failed to unmarshal job spec input to StandardCapabilityJob: " + err.Error()) + } + + if strings.TrimSpace(scj.Command) == "" { + return errors.New("command is required and must be a string") + } + + if strings.TrimSpace(scj.Config) == "" { + return errors.New("config is required and must be a string") + } + + if scj.ChainSelectorEVM == 0 { + return errors.New("chainSelectorEVM is required") + } + + if scj.ChainSelectorAptos == 0 { + return errors.New("chainSelectorAptos is required") + } + + if len(scj.BootstrapPeers) == 0 { + return errors.New("bootstrapPeers is required") + } + if _, err := ocrcommon.ParseBootstrapPeers(scj.BootstrapPeers); err != nil { + return errors.New("bootstrapPeers is invalid: " + err.Error()) + } + + return nil +} diff --git a/deployment/cre/jobs/propose_job_spec.go b/deployment/cre/jobs/propose_job_spec.go index fce94a5fbf9..3a80678fdae 100644 --- a/deployment/cre/jobs/propose_job_spec.go +++ b/deployment/cre/jobs/propose_job_spec.go @@ -62,9 +62,13 @@ func (u ProposeJobSpec) VerifyPreconditions(_ cldf.Environment, config ProposeJo if err := verifyEVMJobSpecInputs(config.Inputs); err != nil { return fmt.Errorf("invalid inputs for EVM job spec: %w", err) } + case job_types.Aptos: + if err := verifyAptosJobSpecInputs(config.Inputs); err != nil { + return fmt.Errorf("invalid inputs for Aptos job spec: %w", err) + } case job_types.Solana: if err := verifySolanaJobSpecInputs(config.Inputs); err != nil { - return fmt.Errorf("invalid inputs for EVM job spec: %w", err) + return fmt.Errorf("invalid inputs for Solana job spec: %w", err) } case job_types.Cron, job_types.BootstrapOCR3, job_types.OCR3, job_types.Gateway, job_types.HTTPTrigger, job_types.HTTPAction, job_types.ConfidentialHTTP, job_types.BootstrapVault, job_types.Consensus, job_types.WebAPITrigger, job_types.WebAPITarget, job_types.CustomCompute, job_types.LogEventTrigger, job_types.ReadContract: case job_types.CRESettings: @@ -90,12 +94,12 @@ func (u ProposeJobSpec) Apply(e cldf.Environment, input ProposeJobSpecInput) (cl var report operations.Report[any, any] switch input.Template { // This will hold all standard capabilities jobs as we add support for them. - case job_types.EVM, job_types.Cron, job_types.HTTPTrigger, job_types.HTTPAction, job_types.ConfidentialHTTP, job_types.Consensus, job_types.WebAPITrigger, job_types.WebAPITarget, job_types.CustomCompute, job_types.LogEventTrigger, job_types.ReadContract, job_types.Solana: - // Only consensus generates an oracle factory, for now... - job, err := input.Inputs.ToStandardCapabilityJob(input.JobName, input.Template == job_types.Consensus) + case job_types.EVM, job_types.Aptos, job_types.Cron, job_types.HTTPTrigger, job_types.HTTPAction, job_types.ConfidentialHTTP, job_types.Consensus, job_types.WebAPITrigger, job_types.WebAPITarget, job_types.CustomCompute, job_types.LogEventTrigger, job_types.ReadContract, job_types.Solana: + job, err := input.Inputs.ToStandardCapabilityJob(input.JobName) if err != nil { return cldf.ChangesetOutput{}, fmt.Errorf("failed to convert inputs to standard capability job: %w", err) } + job.GenerateOracleFactory = requiresOracleFactory(input.Template) r, rErr := operations.ExecuteSequence( e.OperationsBundle, @@ -328,3 +332,15 @@ func (u ProposeJobSpec) Apply(e cldf.Environment, input ProposeJobSpecInput) (cl Reports: []operations.Report[any, any]{report}, }, nil } + +func requiresOracleFactory(template job_types.JobSpecTemplate) bool { + if template == job_types.Consensus { + return true + } + + if template == job_types.Aptos { + return true + } + + return false +} diff --git a/deployment/cre/jobs/propose_job_spec_test.go b/deployment/cre/jobs/propose_job_spec_test.go index 37b7a97e51a..6086e29273e 100644 --- a/deployment/cre/jobs/propose_job_spec_test.go +++ b/deployment/cre/jobs/propose_job_spec_test.go @@ -440,6 +440,74 @@ func TestProposeJobSpec_VerifyPreconditions_EVM(t *testing.T) { } } +func TestProposeJobSpec_VerifyPreconditions_Aptos(t *testing.T) { + j := jobs.ProposeJobSpec{} + var env cldf.Environment + + base := jobs.ProposeJobSpecInput{ + Environment: "test", + Domain: "cre", + DONName: "test-don", + JobName: "aptos-test", + DONFilters: []offchain.TargetDONFilter{ + {Key: offchain.FilterKeyDONName, Value: "d"}, + {Key: "environment", Value: "e"}, + {Key: "product", Value: offchain.ProductLabel}, + }, + Template: job_types.Aptos, + } + + validAptosInputs := func() job_types.JobSpecInput { + return job_types.JobSpecInput{ + "command": "/usr/local/bin/aptos", + "config": `{"chainId":"4","network":"aptos","creForwarderAddress":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + "chainSelectorEVM": "3379446385462418246", + "chainSelectorAptos": "4457093679053095497", + "bootstrapPeers": []string{ + "12D3KooWHfYFQ8hGttAYbMCevQVESEQhzJAqFZokMVtom8bNxwGq@127.0.0.1:5001", + }, + "useCapRegOCRConfig": true, + "capRegVersion": "2.0.0", + } + } + + t.Run("valid aptos spec passes", func(t *testing.T) { + in := base + in.Inputs = validAptosInputs() + require.NoError(t, j.VerifyPreconditions(env, in)) + }) + + type negCase struct { + name string + mutate func(job_types.JobSpecInput) + wantEnd string + } + + const prefix = "invalid inputs for Aptos job spec: " + + cases := []negCase{ + {"missing command", func(m job_types.JobSpecInput) { delete(m, "command") }, "command is required and must be a string"}, + {"missing config", func(m job_types.JobSpecInput) { delete(m, "config") }, "config is required and must be a string"}, + {"missing chainSelectorEVM", func(m job_types.JobSpecInput) { delete(m, "chainSelectorEVM") }, "chainSelectorEVM is required"}, + {"missing chainSelectorAptos", func(m job_types.JobSpecInput) { delete(m, "chainSelectorAptos") }, "chainSelectorAptos is required"}, + {"missing bootstrapPeers", func(m job_types.JobSpecInput) { delete(m, "bootstrapPeers") }, "bootstrapPeers is required"}, + {"invalid bootstrapPeers", func(m job_types.JobSpecInput) { m["bootstrapPeers"] = []string{"not-a-peer"} }, "bootstrapPeers is invalid"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + in := base + in.Inputs = validAptosInputs() + tc.mutate(in.Inputs) + + err := j.VerifyPreconditions(env, in) + require.Error(t, err) + assert.Contains(t, err.Error(), prefix) + assert.Contains(t, err.Error(), tc.wantEnd) + }) + } +} + func TestProposeJobSpec_Apply(t *testing.T) { testEnv := test.SetupEnvV2(t, false) env := testEnv.Env @@ -766,6 +834,71 @@ PerSenderBurst = 100 assert.Contains(t, req.Spec, `command = "/usr/bin/read-contract"`) assert.Contains(t, req.Spec, `config = """{"chainId":1337,"network":"evm"}"""`) assert.Contains(t, req.Spec, `externalJobID = "a-readcontract-job-id"`) + assert.NotContains(t, req.Spec, `[oracle_factory]`) + } + }) + + t.Run("successful aptos job distribution includes oracle factory", func(t *testing.T) { + chainSelector := testEnv.RegistrySelector + ds := datastore.NewMemoryDataStore() + + err := ds.Addresses().Add(datastore.AddressRef{ + ChainSelector: chainSelector, + Type: datastore.ContractType("CapabilitiesRegistry"), + Version: semver.MustParse("2.0.0"), + Address: "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B", + Qualifier: "", + }) + require.NoError(t, err) + + env.DataStore = ds.Seal() + + input := jobs.ProposeJobSpecInput{ + Environment: "test", + Domain: "cre", + JobName: "aptos-cap-job", + DONName: test.DONName, + Template: job_types.Aptos, + DONFilters: []offchain.TargetDONFilter{ + {Key: offchain.FilterKeyDONName, Value: test.DONName}, + {Key: "environment", Value: "test"}, + {Key: "product", Value: offchain.ProductLabel}, + }, + Inputs: job_types.JobSpecInput{ + "command": "/usr/bin/aptos", + "config": `{"chainId":"4","network":"aptos","creForwarderAddress":"0x1111111111111111111111111111111111111111111111111111111111111111"}`, + "chainSelectorEVM": strconv.FormatUint(chainSelector, 10), + "chainSelectorAptos": strconv.FormatUint( + testEnv.AptosSelector, + 10, + ), + "bootstrapPeers": []string{ + "12D3KooWHfYFQ8hGttAYbMCevQVESEQhzJAqFZokMVtom8bNxwGq@127.0.0.1:5001", + }, + "useCapRegOCRConfig": true, + "capRegVersion": "2.0.0", + }, + } + + out, err := jobs.ProposeJobSpec{}.Apply(*env, input) + require.NoError(t, err) + assert.Len(t, out.Reports, 1) + + reqs, err := testEnv.TestJD.ListProposedJobRequests() + require.NoError(t, err) + + filteredReqs := slices.DeleteFunc(reqs, func(s *job.ProposeJobRequest) bool { + return !strings.Contains(s.Spec, `name = "aptos-cap-job"`) + }) + assert.Len(t, filteredReqs, 4) + + for _, req := range filteredReqs { + assert.Contains(t, req.Spec, `name = "aptos-cap-job"`) + assert.Contains(t, req.Spec, `command = "/usr/bin/aptos"`) + assert.Contains(t, req.Spec, `[oracle_factory]`) + assert.Contains(t, req.Spec, `enabled = true`) + assert.Contains(t, req.Spec, `strategyName = "multi-chain"`) + assert.Contains(t, req.Spec, `aptos = "fake_orc_bundle_aptos"`) } }) diff --git a/deployment/cre/jobs/types/job_spec.go b/deployment/cre/jobs/types/job_spec.go index ed03ba6a336..94bf847197c 100644 --- a/deployment/cre/jobs/types/job_spec.go +++ b/deployment/cre/jobs/types/job_spec.go @@ -30,10 +30,9 @@ func (j JobSpecInput) UnmarshalFrom(source any) error { return yaml.Unmarshal(bytes, &j) } -func (j JobSpecInput) ToStandardCapabilityJob(jobName string, generateOracleFactory bool) (pkg.StandardCapabilityJob, error) { +func (j JobSpecInput) ToStandardCapabilityJob(jobName string) (pkg.StandardCapabilityJob, error) { out := pkg.StandardCapabilityJob{ - JobName: jobName, - GenerateOracleFactory: generateOracleFactory, + JobName: jobName, } err := j.UnmarshalTo(&out) if err != nil { diff --git a/deployment/cre/jobs/types/job_spec_template.go b/deployment/cre/jobs/types/job_spec_template.go index d9a7d25e2ec..6d5e6ede7be 100644 --- a/deployment/cre/jobs/types/job_spec_template.go +++ b/deployment/cre/jobs/types/job_spec_template.go @@ -19,6 +19,7 @@ const ( HTTPAction ConfidentialHTTP EVM + Aptos Solana Gateway BootstrapVault @@ -48,6 +49,8 @@ func (jt JobSpecTemplate) String() string { return "confidential-http" case EVM: return "evm" + case Aptos: + return "aptos" case Solana: return "solana" case Gateway: @@ -92,6 +95,8 @@ func parseJobSpecTemplate(s string) (JobSpecTemplate, error) { return ConfidentialHTTP, nil case "evm": return EVM, nil + case "aptos": + return Aptos, nil case "solana": return Solana, nil case "gateway": diff --git a/deployment/cre/jobs/types/job_spec_template_test.go b/deployment/cre/jobs/types/job_spec_template_test.go index 3d9c8219e4e..0bcc2f1d181 100644 --- a/deployment/cre/jobs/types/job_spec_template_test.go +++ b/deployment/cre/jobs/types/job_spec_template_test.go @@ -19,6 +19,13 @@ func TestJobSpecTemplate_UnmarshalJSON(t *testing.T) { require.Equal(t, job_types.Cron, in.Template) }) + t.Run("aptos string", func(t *testing.T) { + var in jobs.ProposeJobSpecInput + js := `{"environment":"e","domain":"d","don_name":"don","don_filters":[],"job_name":"j","template":"aptos","inputs":{}}` + require.NoError(t, json.Unmarshal([]byte(js), &in)) + require.Equal(t, job_types.Aptos, in.Template) + }) + t.Run("invalid string", func(t *testing.T) { var in jobs.ProposeJobSpecInput js := `{"environment":"e","domain":"d","don_name":"don","don_filters":[],"job_name":"j","template":"nope","inputs":{}}` @@ -42,6 +49,13 @@ func TestJobSpecTemplate_UnmarshalYAML(t *testing.T) { require.Equal(t, job_types.Cron, in.Template) }) + t.Run("aptos string", func(t *testing.T) { + var in jobs.ProposeJobSpecInput + yml := "environment: e\ndomain: d\ndon_name: don\ndon_filters: []\njob_name: j\ntemplate: aptos\ninputs: {}\n" + require.NoError(t, yaml.Unmarshal([]byte(yml), &in)) + require.Equal(t, job_types.Aptos, in.Template) + }) + t.Run("invalid string", func(t *testing.T) { var in jobs.ProposeJobSpecInput yml := "environment: e\ndomain: d\ndon_name: don\ndon_filters: []\njob_name: j\ntemplate: nope\ninputs: {}\n" diff --git a/deployment/cre/jobs/types/job_spec_test.go b/deployment/cre/jobs/types/job_spec_test.go index 1cacb68e02f..1ad9ae9a960 100644 --- a/deployment/cre/jobs/types/job_spec_test.go +++ b/deployment/cre/jobs/types/job_spec_test.go @@ -34,7 +34,7 @@ func TestJobSpecInput_ToStandardCapabilityJob(t *testing.T) { }, } - job, err := input.ToStandardCapabilityJob(jobName, false) + job, err := input.ToStandardCapabilityJob(jobName) require.NoError(t, err) assert.Equal(t, jobName, job.JobName) assert.Equal(t, "run", job.Command) @@ -56,7 +56,7 @@ func TestJobSpecInput_ToStandardCapabilityJob(t *testing.T) { "externalJobID": "123", "oracleFactory": pkg.OracleFactory{}, } - _, err := input.ToStandardCapabilityJob(jobName, false) + _, err := input.ToStandardCapabilityJob(jobName) require.Error(t, err) assert.Contains(t, err.Error(), "command is required") }) @@ -68,7 +68,7 @@ func TestJobSpecInput_ToStandardCapabilityJob(t *testing.T) { "externalJobID": "123", "oracleFactory": pkg.OracleFactory{}, } - _, err := input.ToStandardCapabilityJob(jobName, false) + _, err := input.ToStandardCapabilityJob(jobName) require.Error(t, err) assert.Contains(t, err.Error(), "command is required and must be a string") }) @@ -80,7 +80,7 @@ func TestJobSpecInput_ToStandardCapabilityJob(t *testing.T) { "externalJobID": "123", "oracleFactory": pkg.OracleFactory{}, } - _, err := input.ToStandardCapabilityJob(jobName, false) + _, err := input.ToStandardCapabilityJob(jobName) require.NoError(t, err) }) @@ -91,7 +91,7 @@ func TestJobSpecInput_ToStandardCapabilityJob(t *testing.T) { "externalJobID": "123", "oracleFactory": pkg.OracleFactory{}, } - _, err := input.ToStandardCapabilityJob(jobName, false) + _, err := input.ToStandardCapabilityJob(jobName) require.Error(t, err) assert.Contains(t, err.Error(), "cannot unmarshal !!map into string") }) @@ -103,7 +103,7 @@ func TestJobSpecInput_ToStandardCapabilityJob(t *testing.T) { "externalJobID": struct{}{}, "oracleFactory": pkg.OracleFactory{}, } - _, err := input.ToStandardCapabilityJob(jobName, false) + _, err := input.ToStandardCapabilityJob(jobName) require.Error(t, err) assert.Contains(t, err.Error(), "cannot unmarshal !!map into string") }) @@ -115,7 +115,7 @@ func TestJobSpecInput_ToStandardCapabilityJob(t *testing.T) { "externalJobID": "123", "oracleFactory": "not a factory", } - _, err := input.ToStandardCapabilityJob(jobName, false) + _, err := input.ToStandardCapabilityJob(jobName) require.Error(t, err) assert.Contains(t, err.Error(), "cannot unmarshal !!str") }) diff --git a/deployment/cre/ocr3/config.go b/deployment/cre/ocr3/config.go index 1182e3a5ea0..e96c73b00cd 100644 --- a/deployment/cre/ocr3/config.go +++ b/deployment/cre/ocr3/config.go @@ -299,6 +299,7 @@ type ConfigureOCR3Config struct { DryRun bool ReportingPluginConfigOverride []byte + ExtraSignerFamilies []string UseMCMS bool Strategy strategies.TransactionStrategy @@ -327,7 +328,7 @@ func ConfigureOCR3ContractFromJD(env *cldf.Environment, cfg ConfigureOCR3Config) return nil, err } - config, err := GenerateOCR3ConfigFromNodes(*cfg.OCR3Config, nodes, cfg.ChainSel, env.OCRSecrets, cfg.ReportingPluginConfigOverride, nil) + config, err := GenerateOCR3ConfigFromNodes(*cfg.OCR3Config, nodes, cfg.ChainSel, env.OCRSecrets, cfg.ReportingPluginConfigOverride, cfg.ExtraSignerFamilies) if err != nil { return nil, err } diff --git a/deployment/cre/ocr3/v2/changeset/configure_ocr3.go b/deployment/cre/ocr3/v2/changeset/configure_ocr3.go index 7ecebe544c0..6971dfeb7bb 100644 --- a/deployment/cre/ocr3/v2/changeset/configure_ocr3.go +++ b/deployment/cre/ocr3/v2/changeset/configure_ocr3.go @@ -25,9 +25,10 @@ type ConfigureOCR3Input struct { ContractChainSelector uint64 `json:"contractChainSelector" yaml:"contractChainSelector"` ContractQualifier string `json:"contractQualifier" yaml:"contractQualifier"` - DON contracts.DonNodeSet `json:"don" yaml:"don"` - OracleConfig *ocr3.OracleConfig `json:"oracleConfig" yaml:"oracleConfig"` - DryRun bool `json:"dryRun" yaml:"dryRun"` + DON contracts.DonNodeSet `json:"don" yaml:"don"` + OracleConfig *ocr3.OracleConfig `json:"oracleConfig" yaml:"oracleConfig"` + DryRun bool `json:"dryRun" yaml:"dryRun"` + ExtraSignerFamilies []string `json:"extraSignerFamilies,omitempty" yaml:"extraSignerFamilies,omitempty"` MCMSConfig *crecontracts.MCMSConfig `json:"mcmsConfig" yaml:"mcmsConfig"` } @@ -50,6 +51,9 @@ func (l ConfigureOCR3) VerifyPreconditions(_ cldf.Environment, input ConfigureOC if input.OracleConfig == nil { return errors.New("oracle config is required") } + if err := ocr3.ValidateExtraSignerFamilies(input.ExtraSignerFamilies); err != nil { + return fmt.Errorf("invalid extra signer families: %w", err) + } return nil } @@ -93,12 +97,13 @@ func (l ConfigureOCR3) Apply(e cldf.Environment, input ConfigureOCR3Input) (cldf Env: &e, Strategy: strategy, }, contracts.ConfigureOCR3Input{ - ContractAddress: &contractAddr, - ChainSelector: input.ContractChainSelector, - DON: input.DON, - Config: input.OracleConfig, - DryRun: input.DryRun, - MCMSConfig: input.MCMSConfig, + ContractAddress: &contractAddr, + ChainSelector: input.ContractChainSelector, + DON: input.DON, + Config: input.OracleConfig, + DryRun: input.DryRun, + ExtraSignerFamilies: input.ExtraSignerFamilies, + MCMSConfig: input.MCMSConfig, }) if err != nil { return cldf.ChangesetOutput{}, fmt.Errorf("failed to configure OCR3 contract: %w", err) diff --git a/deployment/cre/ocr3/v2/changeset/operations/contracts/configure_ocr3.go b/deployment/cre/ocr3/v2/changeset/operations/contracts/configure_ocr3.go index ad59fc4f6d0..fd1b115bc18 100644 --- a/deployment/cre/ocr3/v2/changeset/operations/contracts/configure_ocr3.go +++ b/deployment/cre/ocr3/v2/changeset/operations/contracts/configure_ocr3.go @@ -35,6 +35,7 @@ type ConfigureOCR3Input struct { DryRun bool ReportingPluginConfigOverride []byte + ExtraSignerFamilies []string MCMSConfig *contracts.MCMSConfig } @@ -72,6 +73,7 @@ var ConfigureOCR3 = operations.NewOperation[ConfigureOCR3Input, ConfigureOCR3OpO OCR3Config: input.Config, Contract: contract.Contract, DryRun: input.DryRun, + ExtraSignerFamilies: input.ExtraSignerFamilies, UseMCMS: input.UseMCMS(), Strategy: deps.Strategy, ReportingPluginConfigOverride: input.ReportingPluginConfigOverride, diff --git a/deployment/go.mod b/deployment/go.mod index 1c9d573e7f4..520fa258311 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -38,7 +38,7 @@ require ( github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20260129135848-c86808ba5cb9 github.com/smartcontractkit/ccip-owner-contracts v0.1.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260224214816-cb23ec38649f diff --git a/deployment/go.sum b/deployment/go.sum index 195a9933489..19cdd00d5c5 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -1369,8 +1369,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/go.md b/go.md index 430c3a96691..d63f2f246cf 100644 --- a/go.md +++ b/go.md @@ -478,6 +478,8 @@ flowchart LR chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/evm/logtrigger-negative chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/http chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/httpaction-negative + chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite + chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/evm/evmread chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/evm/logtrigger chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/evmread @@ -501,6 +503,15 @@ flowchart LR chainlink/system-tests/tests/regression/cre/httpaction-negative --> cre-sdk-go/capabilities/networking/http chainlink/system-tests/tests/regression/cre/httpaction-negative --> cre-sdk-go/capabilities/scheduler/cron click chainlink/system-tests/tests/regression/cre/httpaction-negative href "https://github.com/smartcontractkit/chainlink" + chainlink/system-tests/tests/smoke/cre/aptos/aptosread --> cre-sdk-go/capabilities/blockchain/aptos + chainlink/system-tests/tests/smoke/cre/aptos/aptosread --> cre-sdk-go/capabilities/scheduler/cron + click chainlink/system-tests/tests/smoke/cre/aptos/aptosread href "https://github.com/smartcontractkit/chainlink" + chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite --> cre-sdk-go/capabilities/blockchain/aptos + chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite --> cre-sdk-go/capabilities/scheduler/cron + click chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite href "https://github.com/smartcontractkit/chainlink" + chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip --> cre-sdk-go/capabilities/blockchain/aptos + chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip --> cre-sdk-go/capabilities/scheduler/cron + click chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip href "https://github.com/smartcontractkit/chainlink" chainlink/system-tests/tests/smoke/cre/evm/evmread --> cre-sdk-go/capabilities/blockchain/evm chainlink/system-tests/tests/smoke/cre/evm/evmread --> cre-sdk-go/capabilities/scheduler/cron click chainlink/system-tests/tests/smoke/cre/evm/evmread href "https://github.com/smartcontractkit/chainlink" @@ -531,6 +542,8 @@ flowchart LR click chainlink/v2 href "https://github.com/smartcontractkit/chainlink" cre-sdk-go --> chainlink-protos/cre/go click cre-sdk-go href "https://github.com/smartcontractkit/cre-sdk-go" + cre-sdk-go/capabilities/blockchain/aptos --> cre-sdk-go + click cre-sdk-go/capabilities/blockchain/aptos href "https://github.com/smartcontractkit/cre-sdk-go" cre-sdk-go/capabilities/blockchain/evm --> chainlink-common/pkg/workflows/sdk/v2/pb cre-sdk-go/capabilities/blockchain/evm --> cre-sdk-go click cre-sdk-go/capabilities/blockchain/evm href "https://github.com/smartcontractkit/cre-sdk-go" @@ -584,6 +597,9 @@ flowchart LR chainlink/system-tests/tests/regression/cre/evm/logtrigger-negative chainlink/system-tests/tests/regression/cre/http chainlink/system-tests/tests/regression/cre/httpaction-negative + chainlink/system-tests/tests/smoke/cre/aptos/aptosread + chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite + chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip chainlink/system-tests/tests/smoke/cre/evm/evmread chainlink/system-tests/tests/smoke/cre/evm/logtrigger chainlink/system-tests/tests/smoke/cre/evmread @@ -684,6 +700,7 @@ flowchart LR subgraph cre-sdk-go-repo[cre-sdk-go] cre-sdk-go + cre-sdk-go/capabilities/blockchain/aptos cre-sdk-go/capabilities/blockchain/evm cre-sdk-go/capabilities/blockchain/solana cre-sdk-go/capabilities/networking/http diff --git a/go.mod b/go.mod index c5f44b12223..206a56f7de0 100644 --- a/go.mod +++ b/go.mod @@ -79,7 +79,7 @@ require ( github.com/shirou/gopsutil/v3 v3.24.3 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 github.com/smartcontractkit/chainlink-automation v0.8.1 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260224214816-cb23ec38649f diff --git a/go.sum b/go.sum index e0ae18005fa..911c076d66f 100644 --- a/go.sum +++ b/go.sum @@ -1221,8 +1221,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 1d550a92e54..488921c4729 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -34,7 +34,7 @@ require ( github.com/segmentio/ksuid v1.0.4 github.com/slack-go/slack v0.15.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 6e126ef7ed2..c6bea586dcd 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1356,8 +1356,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index 3f8c8cc6b8e..ba751fec833 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -23,7 +23,7 @@ require ( github.com/gagliardetto/solana-go v1.13.0 github.com/rs/zerolog v1.34.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index eb564e5975f..4929e15cba7 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1570,8 +1570,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/plugins/plugins.private.yaml b/plugins/plugins.private.yaml index a58595a9b03..99533f2d56b 100644 --- a/plugins/plugins.private.yaml +++ b/plugins/plugins.private.yaml @@ -23,7 +23,7 @@ plugins: installPath: "." consensus: - moduleURI: "github.com/smartcontractkit/capabilities/consensus" - gitRef: "9382dd467b74fe964497d84ac134162eac150322" + gitRef: "4bc25dd3e53308c15caa4f34a60d39dee2a6400c" installPath: "." workflowevent: - enabled: false @@ -46,6 +46,10 @@ plugins: - moduleURI: "github.com/smartcontractkit/capabilities/chain_capabilities/solana" gitRef: "6d553b1b59f12d948f163012c822f7417d66d996" installPath: "." + aptos: + - moduleURI: "github.com/smartcontractkit/capabilities/chain_capabilities/aptos" + gitRef: "a410cc7b314110c6793e80aa6cb3dcff1c44b881" + installPath: "." mock: - moduleURI: "github.com/smartcontractkit/capabilities/mock" gitRef: "9382dd467b74fe964497d84ac134162eac150322" diff --git a/plugins/plugins.public.yaml b/plugins/plugins.public.yaml index 85ea380e43b..861179d56b2 100644 --- a/plugins/plugins.public.yaml +++ b/plugins/plugins.public.yaml @@ -10,7 +10,7 @@ defaults: plugins: aptos: - moduleURI: "github.com/smartcontractkit/chainlink-aptos" - gitRef: "v0.0.0-20260318173523-755cafb24200" + gitRef: "v0.0.0-20260324144720-484863604698" installPath: "./cmd/chainlink-aptos" sui: diff --git a/system-tests/lib/cre/contracts/keystone.go b/system-tests/lib/cre/contracts/keystone.go index 97fce670aad..a9cd9496c01 100644 --- a/system-tests/lib/cre/contracts/keystone.go +++ b/system-tests/lib/cre/contracts/keystone.go @@ -178,7 +178,7 @@ func (d *dons) embedOCR3Config(capConfig *capabilitiespb.CapabilityConfig, don d return nil } -func (d *dons) mustToV2ConfigureInput(chainSelector uint64, contractAddress string, capabilityToOCR3Config map[string]*ocr3.OracleConfig, extraSignerFamilies []string) cap_reg_v2_seq.ConfigureCapabilitiesRegistryInput { +func (d *dons) mustToV2ConfigureInput(chainSelector uint64, contractAddress string, capabilityToOCR3Config map[string]*ocr3.OracleConfig, capabilityToExtraSignerFamilies map[string][]string) cap_reg_v2_seq.ConfigureCapabilitiesRegistryInput { nops := make([]capabilities_registry_v2.CapabilitiesRegistryNodeOperatorParams, 0) nodes := make([]contracts.NodesInput, 0) capabilities := make([]contracts.RegisterableCapability, 0) @@ -213,16 +213,16 @@ func (d *dons) mustToV2ConfigureInput(chainSelector uint64, contractAddress stri } for i, nop := range don.Nops { nopName := nop.Name - if _, exists := nopMap[nopName]; !exists { - nopMap[nopName] = capabilities_registry_v2.CapabilitiesRegistryNodeOperatorParams{ - Admin: adminAddrs[i], - Name: nopName, - } + if _, exists := nopMap[nopName]; !exists { ns, err := deployment.NodeInfo(nop.Nodes, d.offChain) if err != nil { panic(err) } + nopMap[nopName] = capabilities_registry_v2.CapabilitiesRegistryNodeOperatorParams{ + Admin: adminAddrs[i], + Name: nopName, + } // Add nodes for this NOP for _, n := range ns { @@ -258,17 +258,26 @@ func (d *dons) mustToV2ConfigureInput(chainSelector uint64, contractAddress stri for _, cap := range don.Capabilities { capID := fmt.Sprintf("%s@%s", cap.Capability.LabelledName, cap.Capability.Version) configBytes := []byte("{}") - if cap.Config != nil { - if cap.UseCapRegOCRConfig { - ocrConfig := capabilityToOCR3Config[cap.Capability.LabelledName] - if ocrConfig == nil { - panic("no OCR3 config found for capability " + cap.Capability.LabelledName) - } - if err := d.embedOCR3Config(cap.Config, don, chainSelector, ocrConfig, extraSignerFamilies); err != nil { - panic(fmt.Sprintf("failed to embed OCR3 config for capability %s: %s", cap.Capability.LabelledName, err)) - } + + capConfig := cap.Config + shouldMarshalProtoConfig := capConfig != nil + if cap.UseCapRegOCRConfig { + if capConfig == nil { + capConfig = &capabilitiespb.CapabilityConfig{} } - if protoBytes, err := proto.Marshal(cap.Config); err == nil { + shouldMarshalProtoConfig = true + + ocrConfig := capabilityToOCR3Config[cap.Capability.LabelledName] + if ocrConfig == nil { + panic("no OCR3 config found for capability " + cap.Capability.LabelledName) + } + if err := d.embedOCR3Config(capConfig, don, chainSelector, ocrConfig, capabilityToExtraSignerFamilies[cap.Capability.LabelledName]); err != nil { + panic(fmt.Sprintf("failed to embed OCR3 config for capability %s: %s", cap.Capability.LabelledName, err)) + } + } + + if shouldMarshalProtoConfig { + if protoBytes, err := proto.Marshal(capConfig); err == nil { configBytes = protoBytes } } @@ -387,7 +396,7 @@ func toDons(input cre.ConfigureCapabilityRegistryInput) (*dons, error) { capabilities = append(capabilities, enabledCapabilities...) } - // add capabilities that were passed directly via the input (from the PostDONStartup of features) + // add capabilities that were passed directly via feature startup hooks if input.DONCapabilityWithConfigs != nil && input.DONCapabilityWithConfigs[donMetadata.ID] != nil { capabilities = append(capabilities, input.DONCapabilityWithConfigs[donMetadata.ID]...) } @@ -455,7 +464,7 @@ func ConfigureCapabilityRegistry(input cre.ConfigureCapabilityRegistryInput) (Ca if ocrConfig == nil { return nil, fmt.Errorf("no OCR3 config found for capability %s", cap.Capability.LabelledName) } - if err := dons.embedOCR3Config(don.Capabilities[i].Config, don, input.ChainSelector, ocrConfig, input.ExtraSignerFamilies); err != nil { + if err := dons.embedOCR3Config(don.Capabilities[i].Config, don, input.ChainSelector, ocrConfig, input.CapabilityToExtraSignerFamilies[cap.Capability.LabelledName]); err != nil { return nil, fmt.Errorf("failed to embed OCR3 config for capability %s: %w", cap.Capability.LabelledName, err) } } @@ -490,7 +499,7 @@ func ConfigureCapabilityRegistry(input cre.ConfigureCapabilityRegistryInput) (Ca } // Transform dons data to V2 sequence input format - v2Input := dons.mustToV2ConfigureInput(input.ChainSelector, input.CapabilitiesRegistryAddress.Hex(), input.CapabilityToOCR3Config, input.ExtraSignerFamilies) + v2Input := dons.mustToV2ConfigureInput(input.ChainSelector, input.CapabilitiesRegistryAddress.Hex(), input.CapabilityToOCR3Config, input.CapabilityToExtraSignerFamilies) _, seqErr := operations.ExecuteSequence( input.CldEnv.OperationsBundle, cap_reg_v2_seq.ConfigureCapabilitiesRegistry, diff --git a/system-tests/lib/cre/contracts/ocr3.go b/system-tests/lib/cre/contracts/ocr3.go index 0916a83e607..9bf7de89d6c 100644 --- a/system-tests/lib/cre/contracts/ocr3.go +++ b/system-tests/lib/cre/contracts/ocr3.go @@ -2,6 +2,7 @@ package contracts import ( "fmt" + "time" "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/common" @@ -71,6 +72,7 @@ func DefaultOCR3Config() *ocr3.OracleConfig { MaxOutcomeLengthBytes: 1000000, MaxReportLengthBytes: 1000000, MaxBatchSize: 1000, + RequestTimeout: 30 * time.Second, }, UniqueReports: true, } diff --git a/system-tests/lib/cre/don.go b/system-tests/lib/cre/don.go index 9f0c6c7073d..0449a9c6d2a 100644 --- a/system-tests/lib/cre/don.go +++ b/system-tests/lib/cre/don.go @@ -3,6 +3,7 @@ package cre import ( "context" "fmt" + "net/http" "net/url" "slices" "strconv" @@ -437,8 +438,6 @@ type JobDistributorDetails struct { type Addresses struct { AdminAddress string `toml:"admin_address" json:"admin_address"` // address used to pay for transactions, applicable only for worker nodes MultiAddress string `toml:"multi_address" json:"multi_address"` // multi address used by OCR2, applicable only for bootstrap nodes - - // maybe in the future add public addresses per chain to avoid the need to access node's keys every time? } type NodeClients struct { @@ -452,8 +451,11 @@ type JDChainConfigInput struct { } func createJDChainConfigs(ctx context.Context, n *Node, supportedChains []blockchains.Blockchain, jd *jd.JobDistributor) error { + // Dedupe by (chain ID, chain type) so we never create the same config twice (avoids unique constraint violation). + seen := make(map[string]struct{}) for _, chain := range supportedChains { var account string + var accountAddrPubKey string chainIDStr := strconv.FormatUint(chain.ChainID(), 10) switch strings.ToLower(chain.ChainFamily()) { @@ -490,15 +492,14 @@ func createJDChainConfigs(ctx context.Context, n *Node, supportedChains []blockc account = accounts[0] } case chainselectors.FamilyAptos: - // always fetch; currently Node doesn't have Aptos keys - accounts, err := n.Clients.GQLClient.FetchKeys(ctx, strings.ToUpper(chain.ChainFamily())) - if err != nil { - return fmt.Errorf("failed to fetch account address for node %s and chain %s: %w", n.Name, chain.ChainFamily(), err) - } - if len(accounts) == 0 { - return fmt.Errorf("failed to fetch account address for node %s and chain %s", n.Name, chain.ChainFamily()) + aptosAccount, aptosErr := aptosAccountForNode(ctx, n) + if aptosErr != nil { + return fmt.Errorf("failed to fetch aptos account address for node %s: %w", n.Name, aptosErr) } - account = accounts[0] + account = aptosAccount + // Deployment parsing prefers AccountAddressPublicKey for Aptos chain configs. + // Mirror transmitter into this field so OCRConfigForChainSelector always resolves it. + accountAddrPubKey = account default: return fmt.Errorf("unsupported chainType %v", chain.ChainFamily()) } @@ -507,12 +508,18 @@ func createJDChainConfigs(ctx context.Context, n *Node, supportedChains []blockc if chain.IsFamily(blockchain.FamilyTron) { chainType = strings.ToUpper(blockchain.FamilyEVM) } + dedupeKey := chainIDStr + "\x00" + chainType + if _, exists := seen[dedupeKey]; exists { + continue + } + seen[dedupeKey] = struct{}{} + ocr2BundleID, createErr := n.Clients.GQLClient.FetchOCR2KeyBundleID(ctx, chainType) if createErr != nil { return fmt.Errorf("failed to fetch OCR2 key bundle id for node %s: %w", n.Name, createErr) } if ocr2BundleID == "" { - return fmt.Errorf("no OCR2 key bundle id found for node %s", n.Name) + return fmt.Errorf("no OCR2 key bundle id found for node %s (chainType=%s)", n.Name, chainType) } if n.Keys.OCR2BundleIDs == nil { @@ -542,20 +549,24 @@ func createJDChainConfigs(ctx context.Context, n *Node, supportedChains []blockc // we need to create JD chain config for each chain, because later on changestes ask the node for that chain data // each node needs to have OCR2 enabled, because p2pIDs are used by some contracts to identify nodes (e.g. capability registry) _, createErr = n.Clients.GQLClient.CreateJobDistributorChainConfig(ctx, client.JobDistributorChainConfigInput{ - JobDistributorID: n.JobDistributorDetails.JDID, - ChainID: chainIDStr, - ChainType: chainType, - AccountAddr: account, - AdminAddr: n.Addresses.AdminAddress, - Ocr2Enabled: true, - Ocr2IsBootstrap: n.HasRole(RoleBootstrap), - Ocr2Multiaddr: n.Addresses.MultiAddress, - Ocr2P2PPeerID: n.Keys.P2PKey.PeerID.String(), - Ocr2KeyBundleID: ocr2BundleID, - Ocr2Plugins: `{}`, + JobDistributorID: n.JobDistributorDetails.JDID, + ChainID: chainIDStr, + ChainType: chainType, + AccountAddr: account, + AccountAddrPubKey: accountAddrPubKey, + AdminAddr: n.Addresses.AdminAddress, + Ocr2Enabled: true, + Ocr2IsBootstrap: n.HasRole(RoleBootstrap), + Ocr2Multiaddr: n.Addresses.MultiAddress, + Ocr2P2PPeerID: n.Keys.P2PKey.PeerID.String(), + Ocr2KeyBundleID: ocr2BundleID, + Ocr2Plugins: `{}`, }) - // TODO: add a check if the chain config failed because of a duplicate in that case, should we update or return success? if createErr != nil { + // Config may already exist (e.g. duplicate key from prior run or concurrent node registration); treat as success. + if strings.Contains(createErr.Error(), "duplicate key") || strings.Contains(createErr.Error(), "23505") { + return nil + } return createErr } @@ -571,6 +582,49 @@ func createJDChainConfigs(ctx context.Context, n *Node, supportedChains []blockc return nil } +func aptosAccountForNode(ctx context.Context, n *Node) (string, error) { + if n.Keys != nil && n.Keys.AptosAccount() != "" { + return n.Keys.AptosAccount(), nil + } + + var runtimeKeys struct { + Data []struct { + Attributes struct { + Account string `json:"account"` + PublicKey string `json:"publicKey"` + } `json:"attributes"` + } `json:"data"` + } + resp, err := n.Clients.RestClient.APIClient.R(). + SetContext(ctx). + SetResult(&runtimeKeys). + Get("/v2/keys/aptos") + if err != nil { + return "", fmt.Errorf("failed to read Aptos keys from node API: %w", err) + } + if resp.StatusCode() != http.StatusOK { + return "", fmt.Errorf("aptos keys endpoint returned status %d", resp.StatusCode()) + } + if len(runtimeKeys.Data) == 0 { + return "", fmt.Errorf("no Aptos keys found on node %s", n.Name) + } + + account, err := crypto.NormalizeAptosAccount(runtimeKeys.Data[0].Attributes.Account) + if err != nil { + return "", fmt.Errorf("invalid Aptos account returned by node API: %w", err) + } + + if n.Keys != nil { + if n.Keys.Aptos == nil { + n.Keys.Aptos = &crypto.AptosKey{} + } + n.Keys.Aptos.Account = account + n.Keys.Aptos.PublicKey = runtimeKeys.Data[0].Attributes.PublicKey + } + + return account, nil +} + // AcceptJob accepts the job proposal for the given job proposal spec func (n *Node) AcceptJob(ctx context.Context, spec string) error { // fetch JD to get the job proposals @@ -809,12 +863,16 @@ func HasFlag(values []string, capability string) bool { func findDonSupportedChains(donMetadata *DonMetadata, bcs []blockchains.Blockchain) ([]blockchains.Blockchain, error) { chains := make([]blockchains.Blockchain, 0) + chainCapabilityIDs := donMetadata.MustNodeSet().ChainCapabilityChainIDs() for _, bc := range bcs { hasEVMChainEnabled := slices.Contains(donMetadata.EVMChains(), bc.ChainID()) + hasChainCapabilityEnabled := slices.Contains(chainCapabilityIDs, bc.ChainID()) chainIsSolana := bc.IsFamily(chainselectors.FamilySolana) - if !hasEVMChainEnabled && (!chainIsSolana) { + // Include all Solana chains (legacy behavior), and include any chain that is + // explicitly referenced by chain-scoped capabilities (e.g. write-aptos-4). + if !hasEVMChainEnabled && !hasChainCapabilityEnabled && !chainIsSolana { continue } diff --git a/system-tests/lib/cre/don/config/config.go b/system-tests/lib/cre/don/config/config.go index 9e17161b6b3..53ade9ae1eb 100644 --- a/system-tests/lib/cre/don/config/config.go +++ b/system-tests/lib/cre/don/config/config.go @@ -35,6 +35,7 @@ import ( "github.com/smartcontractkit/chainlink/system-tests/lib/cre" crecontracts "github.com/smartcontractkit/chainlink/system-tests/lib/cre/contracts" creblockchains "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains" + aptoschain "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/aptos" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/solana" "github.com/smartcontractkit/chainlink/system-tests/lib/infra" ) @@ -378,6 +379,18 @@ func addBootstrapNodeConfig( appendSolanaChain(&existingConfig.Solana, commonInputs.solanaChain) } + for _, ac := range commonInputs.aptosChains { + existingConfig.Aptos = append(existingConfig.Aptos, corechainlink.RawConfig{ + "ChainID": ac.ChainID, + "Enabled": true, + "Workflow": map[string]any{"ForwarderAddress": ac.ForwarderAddress}, + "Nodes": []map[string]any{{"Name": "default", "URL": ac.NodeURL}}, + }) + } + + // Set external registry only (local EVM capability registry). We do not set [Capabilities.Local]; + // capabilities (e.g. cron) are registered on the on-chain capability registry via Features (e.g. Cron + // feature PreEnvStartup), same as workflow-don-solana.toml, workflow-gateway-don.toml, workflow-don-tron.toml. if existingConfig.Capabilities.ExternalRegistry.Address == nil { existingConfig.Capabilities.ExternalRegistry = coretoml.ExternalRegistry{ Address: ptr.Ptr(commonInputs.capabilityRegistry.address), @@ -434,8 +447,9 @@ func addWorkerNodeConfig( } // Preserve existing WorkflowRegistry config (e.g., AdditionalSourcesConfig from user_config_overrides) - // before resetting Capabilities struct + // and Local capabilities config before resetting Capabilities struct. existingWorkflowRegistry := existingConfig.Capabilities.WorkflowRegistry + existingLocalCapabilities := existingConfig.Capabilities.Local existingConfig.Capabilities = coretoml.Capabilities{ Peering: coretoml.P2P{ @@ -450,6 +464,7 @@ func addWorkerNodeConfig( SendToSharedPeer: ptr.Ptr(true), }, WorkflowRegistry: existingWorkflowRegistry, + Local: existingLocalCapabilities, } if len(donMetadata.RegistryBasedLaunchAllowlist) > 0 { @@ -466,6 +481,15 @@ func addWorkerNodeConfig( appendSolanaChain(&existingConfig.Solana, commonInputs.solanaChain) } + for _, ac := range commonInputs.aptosChains { + existingConfig.Aptos = append(existingConfig.Aptos, corechainlink.RawConfig{ + "ChainID": ac.ChainID, + "Enabled": true, + "Workflow": map[string]any{"ForwarderAddress": ac.ForwarderAddress}, + "Nodes": []map[string]any{{"Name": "default", "URL": ac.NodeURL}}, + }) + } + if existingConfig.Capabilities.ExternalRegistry.Address == nil { existingConfig.Capabilities.ExternalRegistry = coretoml.ExternalRegistry{ Address: ptr.Ptr(commonInputs.capabilityRegistry.address), @@ -519,7 +543,7 @@ func addWorkerNodeConfig( } gateways := []coretoml.ConnectorGateway{} - if topology != nil && len(topology.GatewayConnectors.Configurations) > 0 { + if topology != nil && topology.GatewayConnectors != nil && len(topology.GatewayConnectors.Configurations) > 0 { for _, gateway := range topology.GatewayConnectors.Configurations { gateways = append(gateways, coretoml.ConnectorGateway{ ID: ptr.Ptr(gateway.AuthGatewayID), @@ -623,6 +647,12 @@ type versionedAddress struct { version *semver.Version } +type aptosChain struct { + ChainID string + NodeURL string + ForwarderAddress string +} + type commonInputs struct { registryChainID uint64 registryChainSelector uint64 @@ -632,6 +662,7 @@ type commonInputs struct { evmChains []*evmChain solanaChain *solanaChain + aptosChains []*aptosChain provider infra.Provider } @@ -651,6 +682,11 @@ func gatherCommonInputs(input cre.GenerateConfigsInput) (*commonInputs, error) { capabilitiesRegistryAddress := crecontracts.MustGetAddressFromDataStore(input.Datastore, input.RegistryChainSelector, keystone_changeset.CapabilitiesRegistry.String(), input.ContractVersions[keystone_changeset.CapabilitiesRegistry.String()], "") workflowRegistryAddress := crecontracts.MustGetAddressFromDataStore(input.Datastore, input.RegistryChainSelector, keystone_changeset.WorkflowRegistry.String(), input.ContractVersions[keystone_changeset.WorkflowRegistry.String()], "") + aptosChains, aptosErr := findAptosChains(input) + if aptosErr != nil { + return nil, errors.Wrap(aptosErr, "failed to find Aptos chains in the environment configuration") + } + return &commonInputs{ registryChainID: registryChainID, registryChainSelector: input.RegistryChainSelector, @@ -660,6 +696,7 @@ func gatherCommonInputs(input cre.GenerateConfigsInput) (*commonInputs, error) { }, evmChains: evmChains, solanaChain: solanaChain, + aptosChains: aptosChains, capabilityRegistry: versionedAddress{ address: capabilitiesRegistryAddress, version: input.ContractVersions[keystone_changeset.CapabilitiesRegistry.String()], @@ -677,8 +714,8 @@ type evmChain struct { func findEVMChains(input cre.GenerateConfigsInput) []*evmChain { evmChains := make([]*evmChain, 0) - for chainSelector, bcOut := range input.Blockchains { - if bcOut.IsFamily(chain_selectors.FamilySolana) { + for _, bcOut := range input.Blockchains { + if bcOut.IsFamily(chain_selectors.FamilySolana) || bcOut.IsFamily(chain_selectors.FamilyAptos) { continue } @@ -688,7 +725,7 @@ func findEVMChains(input cre.GenerateConfigsInput) []*evmChain { } evmChains = append(evmChains, &evmChain{ - Name: fmt.Sprintf("node-%d", chainSelector), + Name: fmt.Sprintf("node-%d", bcOut.ChainSelector()), ChainID: bcOut.ChainID(), HTTPRPC: bcOut.CtfOutput().Nodes[0].InternalHTTPUrl, WSRPC: bcOut.CtfOutput().Nodes[0].InternalWSUrl, @@ -737,6 +774,34 @@ func findOneSolanaChain(input cre.GenerateConfigsInput) (*solanaChain, error) { return solChain, nil } +const aptosZeroForwarderHex = "0x0000000000000000000000000000000000000000000000000000000000000000" + +func findAptosChains(input cre.GenerateConfigsInput) ([]*aptosChain, error) { + capabilityChainIDs := input.DonMetadata.MustNodeSet().ChainCapabilityChainIDs() + out := make([]*aptosChain, 0) + for _, bcOut := range input.Blockchains { + if !bcOut.IsFamily(chain_selectors.FamilyAptos) { + continue + } + if len(capabilityChainIDs) > 0 && !slices.Contains(capabilityChainIDs, bcOut.ChainID()) { + continue + } + + aptosBC := bcOut.(*aptoschain.Blockchain) + nodeURL, err := aptosBC.InternalNodeURL() + if err != nil { + return nil, errors.Wrapf(err, "failed to get Aptos internal node URL for chain %d", bcOut.ChainID()) + } + + out = append(out, &aptosChain{ + ChainID: strconv.FormatUint(bcOut.ChainID(), 10), + NodeURL: nodeURL, + ForwarderAddress: aptosZeroForwarderHex, + }) + } + return out, nil +} + func buildTronEVMConfig(evmChain *evmChain) evmconfigtoml.EVMConfig { tronRPC := strings.Replace(evmChain.HTTPRPC, "jsonrpc", "wallet", 1) return evmconfigtoml.EVMConfig{ diff --git a/system-tests/lib/cre/don/secrets/secrets.go b/system-tests/lib/cre/don/secrets/secrets.go index bc98c8aadf8..905fb197054 100644 --- a/system-tests/lib/cre/don/secrets/secrets.go +++ b/system-tests/lib/cre/don/secrets/secrets.go @@ -3,6 +3,7 @@ package secrets import ( "encoding/hex" "encoding/json" + "strings" "github.com/ethereum/go-ethereum/common" "github.com/gagliardetto/solana-go" @@ -11,6 +12,7 @@ import ( "github.com/smartcontractkit/smdkg/dkgocr/dkgocrtypes" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/aptoskey" "github.com/smartcontractkit/chainlink-common/keystore/corekeys/p2pkey" "github.com/smartcontractkit/chainlink/system-tests/lib/crypto" ) @@ -18,6 +20,7 @@ import ( type nodeSecret struct { EthKeys nodeEthKeyWrapper `toml:"EVM"` SolKeys nodeSolKeyWrapper `toml:"Solana"` + AptosKey nodeAptosKey `toml:"Aptos"` P2PKey nodeP2PKey `toml:"P2PKey"` DKGRecipientKey nodeDKGRecipientKey `toml:"DKGRecipientKey"` @@ -42,6 +45,11 @@ type nodeP2PKey struct { Password string `toml:"Password"` } +type nodeAptosKey struct { + JSON string `toml:"JSON"` + Password string `toml:"Password"` +} + type nodeDKGRecipientKey struct { JSON string `toml:"JSON"` Password string `toml:"Password"` @@ -61,6 +69,7 @@ type NodeKeys struct { CSAKey *crypto.CSAKey EVM map[uint64]*crypto.EVMKey Solana map[string]*crypto.SolKey + Aptos *crypto.AptosKey P2PKey *crypto.P2PKey DKGKey *crypto.DKGRecipientKey OCR2BundleIDs map[ChainFamily]string @@ -73,6 +82,13 @@ func (n NodeKeys) PeerID() string { return n.P2PKey.PeerID.String() } +func (n NodeKeys) AptosAccount() string { + if n.Aptos == nil { + return "" + } + return n.Aptos.Account +} + func (n *NodeKeys) ToNodeSecretsTOML() (string, error) { ns := nodeSecret{} @@ -83,6 +99,13 @@ func (n *NodeKeys) ToNodeSecretsTOML() (string, error) { } } + if n.Aptos != nil { + ns.AptosKey = nodeAptosKey{ + JSON: string(n.Aptos.EncryptedJSON), + Password: n.Aptos.Password, + } + } + if n.DKGKey != nil { ns.DKGRecipientKey = nodeDKGRecipientKey{ JSON: string(n.DKGKey.EncryptedJSON), @@ -125,6 +148,7 @@ type secrets struct { EVM ethKeys `toml:",omitempty"` // choose EVM as the TOML field name to align with relayer config convention P2PKey p2PKey `toml:",omitempty"` Solana solKeys `toml:",omitempty"` + Aptos aptosKey `toml:",omitempty"` DKGRecipientKey dkgRecipientKey `toml:",omitempty"` } @@ -138,6 +162,11 @@ type dkgRecipientKey struct { Password *string } +type aptosKey struct { + JSON *string + Password *string +} + type ethKeys struct { Keys []*ethKey } @@ -260,6 +289,41 @@ func ImportNodeKeys(secretsToml string) (*NodeKeys, error) { PeerID: *p, } + if sSecrets.Aptos.JSON != nil { + aptosJSON := strings.TrimSpace(*sSecrets.Aptos.JSON) + if aptosJSON == "" { + sSecrets.Aptos.JSON = nil + sSecrets.Aptos.Password = nil + } + } + + if sSecrets.Aptos.JSON != nil { + if sSecrets.Aptos.Password == nil { + return nil, errors.New("aptos key password is nil") + } + aptosPassword := strings.TrimSpace(*sSecrets.Aptos.Password) + if aptosPassword == "" { + return nil, errors.New("aptos key password is empty") + } + + aptosKeyValue, err := aptoskey.FromEncryptedJSON([]byte(*sSecrets.Aptos.JSON), aptosPassword) + if err != nil { + return nil, errors.Wrap(err, "failed to decrypt aptos key from encrypted JSON") + } + + account, err := crypto.NormalizeAptosAccount(aptosKeyValue.Account()) + if err != nil { + return nil, errors.Wrap(err, "failed to normalize aptos account") + } + + keys.Aptos = &crypto.AptosKey{ + EncryptedJSON: []byte(*sSecrets.Aptos.JSON), + PublicKey: aptosKeyValue.PublicKeyStr(), + Account: account, + Password: aptosPassword, + } + } + if sSecrets.DKGRecipientKey.JSON != nil { keys.DKGKey = &crypto.DKGRecipientKey{ EncryptedJSON: []byte(*sSecrets.DKGRecipientKey.JSON), diff --git a/system-tests/lib/cre/don/secrets/secrets_test.go b/system-tests/lib/cre/don/secrets/secrets_test.go new file mode 100644 index 00000000000..7b2744d3e7c --- /dev/null +++ b/system-tests/lib/cre/don/secrets/secrets_test.go @@ -0,0 +1,65 @@ +package secrets + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/system-tests/lib/crypto" +) + +func TestNodeKeysAptosSecretsRoundTrip(t *testing.T) { + t.Parallel() + + aptosKey, err := crypto.NewAptosKey("dev-password") + require.NoError(t, err) + p2pKey, err := crypto.NewP2PKey("dev-password") + require.NoError(t, err) + dkgKey, err := crypto.NewDKGRecipientKey("dev-password") + require.NoError(t, err) + + keys := &NodeKeys{ + Aptos: aptosKey, + P2PKey: p2pKey, + DKGKey: dkgKey, + EVM: map[uint64]*crypto.EVMKey{}, + Solana: map[string]*crypto.SolKey{}, + } + + secretsTOML, err := keys.ToNodeSecretsTOML() + require.NoError(t, err) + + imported, err := ImportNodeKeys(secretsTOML) + require.NoError(t, err) + require.NotNil(t, imported.Aptos) + require.Equal(t, aptosKey.Account, imported.Aptos.Account) + require.Equal(t, aptosKey.PublicKey, imported.Aptos.PublicKey) + require.Equal(t, aptosKey.Password, imported.Aptos.Password) + require.Equal(t, p2pKey.PeerID, imported.P2PKey.PeerID) + require.Equal(t, dkgKey.PubKey, imported.DKGKey.PubKey) +} + +func TestImportNodeKeys_IgnoresEmptyAptosSecret(t *testing.T) { + t.Parallel() + + p2pKey, err := crypto.NewP2PKey("dev-password") + require.NoError(t, err) + dkgKey, err := crypto.NewDKGRecipientKey("dev-password") + require.NoError(t, err) + + keys := &NodeKeys{ + P2PKey: p2pKey, + DKGKey: dkgKey, + EVM: map[uint64]*crypto.EVMKey{}, + Solana: map[string]*crypto.SolKey{}, + } + + secretsTOML, err := keys.ToNodeSecretsTOML() + require.NoError(t, err) + + imported, err := ImportNodeKeys(secretsTOML) + require.NoError(t, err) + require.Nil(t, imported.Aptos) + require.Equal(t, p2pKey.PeerID, imported.P2PKey.PeerID) + require.Equal(t, dkgKey.PubKey, imported.DKGKey.PubKey) +} diff --git a/system-tests/lib/cre/don_test.go b/system-tests/lib/cre/don_test.go new file mode 100644 index 00000000000..7c70dab226d --- /dev/null +++ b/system-tests/lib/cre/don_test.go @@ -0,0 +1,82 @@ +package cre + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + + "github.com/go-resty/resty/v2" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/secrets" + crecrypto "github.com/smartcontractkit/chainlink/system-tests/lib/crypto" +) + +func TestAptosAccountForNode_UsesMetadataKeyWithoutCallingNodeAPI(t *testing.T) { + t.Parallel() + + var hits atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hits.Add(1) + http.NotFound(w, r) + })) + t.Cleanup(server.Close) + + expected, err := crecrypto.NormalizeAptosAccount("0x1") + require.NoError(t, err) + + node := &Node{ + Name: "node-1", + Keys: &secrets.NodeKeys{ + Aptos: &crecrypto.AptosKey{Account: expected}, + }, + Clients: NodeClients{ + RestClient: &clclient.ChainlinkClient{APIClient: resty.New().SetBaseURL(server.URL)}, + }, + } + + account, err := aptosAccountForNode(context.Background(), node) + require.NoError(t, err) + require.Equal(t, expected, account) + require.Zero(t, hits.Load(), "node API must not be called when metadata already has the Aptos key") +} + +func TestAptosAccountForNode_FallsBackToNodeAPIAndCachesKey(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v2/keys/aptos" { + t.Errorf("unexpected path: got %q want %q", r.URL.Path, "/v2/keys/aptos") + http.Error(w, fmt.Sprintf("unexpected path %q", r.URL.Path), http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + _, err := w.Write([]byte(`{"data":[{"attributes":{"account":"0x1","publicKey":"0xabc123"}}]}`)) + if err != nil { + t.Errorf("failed to write response: %v", err) + } + })) + t.Cleanup(server.Close) + + node := &Node{ + Name: "node-1", + Keys: &secrets.NodeKeys{}, + Clients: NodeClients{ + RestClient: &clclient.ChainlinkClient{APIClient: resty.New().SetBaseURL(server.URL)}, + }, + } + + account, err := aptosAccountForNode(context.Background(), node) + require.NoError(t, err) + + expected, err := crecrypto.NormalizeAptosAccount("0x1") + require.NoError(t, err) + require.Equal(t, expected, account) + require.NotNil(t, node.Keys.Aptos) + require.Equal(t, expected, node.Keys.Aptos.Account) + require.Equal(t, "0xabc123", node.Keys.Aptos.PublicKey) +} diff --git a/system-tests/lib/cre/environment/blockchains/aptos/aptos.go b/system-tests/lib/cre/environment/blockchains/aptos/aptos.go new file mode 100644 index 00000000000..461a86ea4b4 --- /dev/null +++ b/system-tests/lib/cre/environment/blockchains/aptos/aptos.go @@ -0,0 +1,357 @@ +package aptos + +import ( + "context" + "errors" + "fmt" + "net/url" + "strconv" + "strings" + "time" + + aptoslib "github.com/aptos-labs/aptos-go-sdk" + aptoscrypto "github.com/aptos-labs/aptos-go-sdk/crypto" + pkgerrors "github.com/pkg/errors" + "github.com/rs/zerolog" + + chainselectors "github.com/smartcontractkit/chain-selectors" + + cldf_chain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldf_aptos "github.com/smartcontractkit/chainlink-deployments-framework/chain/aptos" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains" + "github.com/smartcontractkit/chainlink/system-tests/lib/infra" +) + +type Deployer struct { + provider infra.Provider + testLogger zerolog.Logger +} + +func NewDeployer(testLogger zerolog.Logger, provider *infra.Provider) *Deployer { + return &Deployer{ + provider: *provider, + testLogger: testLogger, + } +} + +type Blockchain struct { + testLogger zerolog.Logger + chainSelector uint64 + chainID uint64 + ctfOutput *blockchain.Output +} + +func (a *Blockchain) ChainSelector() uint64 { + return a.chainSelector +} + +func (a *Blockchain) ChainID() uint64 { + return a.chainID +} + +func (a *Blockchain) CtfOutput() *blockchain.Output { + return a.ctfOutput +} + +func (a *Blockchain) NodeURL() (string, error) { + if a.ctfOutput == nil || len(a.ctfOutput.Nodes) == 0 { + return "", fmt.Errorf("no nodes found for Aptos chain %s-%d", a.ChainFamily(), a.chainID) + } + return NormalizeNodeURL(a.ctfOutput.Nodes[0].ExternalHTTPUrl) +} + +func (a *Blockchain) InternalNodeURL() (string, error) { + if a.ctfOutput == nil || len(a.ctfOutput.Nodes) == 0 { + return "", fmt.Errorf("no nodes found for Aptos chain %s-%d", a.ChainFamily(), a.chainID) + } + return NormalizeNodeURL(a.ctfOutput.Nodes[0].InternalHTTPUrl) +} + +func (a *Blockchain) NodeClient() (*aptoslib.NodeClient, error) { + nodeURL, err := a.NodeURL() + if err != nil { + return nil, err + } + chainID, err := aptosChainIDUint8(a.chainID) + if err != nil { + return nil, err + } + return aptoslib.NewNodeClient(nodeURL, chainID) +} + +func (a *Blockchain) LocalDeployerAccount() (*aptoslib.Account, error) { + var deployerPrivateKey aptoscrypto.Ed25519PrivateKey + if err := deployerPrivateKey.FromHex(blockchain.DefaultAptosPrivateKey); err != nil { + return nil, fmt.Errorf("failed to parse default Aptos deployer private key: %w", err) + } + deployerAccount, err := aptoslib.NewAccountFromSigner(&deployerPrivateKey) + if err != nil { + return nil, fmt.Errorf("failed to create default Aptos deployer signer: %w", err) + } + return deployerAccount, nil +} + +func (a *Blockchain) LocalDeploymentChain() (cldf_aptos.Chain, error) { + nodeURL, err := a.NodeURL() + if err != nil { + return cldf_aptos.Chain{}, err + } + client, err := a.NodeClient() + if err != nil { + return cldf_aptos.Chain{}, err + } + deployerAccount, err := a.LocalDeployerAccount() + if err != nil { + return cldf_aptos.Chain{}, err + } + return cldf_aptos.Chain{ + Selector: a.chainSelector, + Client: client, + DeployerSigner: deployerAccount, + URL: nodeURL, + Confirm: func(txHash string, opts ...any) error { + tx, err := client.WaitForTransaction(txHash, opts...) + if err != nil { + return err + } + if !tx.Success { + return fmt.Errorf("transaction failed: %s", tx.VmStatus) + } + return nil + }, + }, nil +} + +func (a *Blockchain) IsFamily(chainFamily string) bool { + return strings.EqualFold(a.ctfOutput.Family, chainFamily) +} + +func (a *Blockchain) ChainFamily() string { + return a.ctfOutput.Family +} + +func (a *Blockchain) Fund(ctx context.Context, address string, amount uint64) error { + client, err := a.NodeClient() + if err != nil { + return fmt.Errorf("cannot fund Aptos address %s: create node client: %w", address, err) + } + + var account aptoslib.AccountAddress + if parseErr := account.ParseStringRelaxed(address); parseErr != nil { + return fmt.Errorf("cannot fund Aptos address %q: parse error: %w", address, parseErr) + } + + faucetURL, err := a.faucetURL() + if err != nil { + return fmt.Errorf("failed to derive Aptos faucet URL for %s: %w", address, err) + } + faucetClient, err := aptoslib.NewFaucetClient(client, faucetURL) + if err != nil { + return fmt.Errorf("failed to create Aptos faucet client for %s: %w", address, err) + } + if err := faucetClient.Fund(account, amount); err != nil { + return fmt.Errorf("failed to fund Aptos address %s via host faucet: %w", address, err) + } + if err := waitForAptosAccountVisible(ctx, client, account, 15*time.Second); err != nil { + return fmt.Errorf("aptos funding request completed but account is still not visible: %w", err) + } + + a.testLogger.Info().Msgf("Funded Aptos account %s via host faucet (%d octas)", account.StringLong(), amount) + return nil +} + +// ToCldfChain returns the chainlink-deployments-framework aptos.Chain for this blockchain +// so that BlockChains.AptosChains() and saved state work like EVM/Solana. +func (a *Blockchain) ToCldfChain() (cldf_chain.BlockChain, error) { + nodeURL, err := a.NodeURL() + if err != nil { + return nil, fmt.Errorf("invalid Aptos ExternalHTTPUrl for chain %d: %w", a.chainID, err) + } + if nodeURL == "" { + return nil, fmt.Errorf("aptos node has no ExternalHTTPUrl for chain %d", a.chainID) + } + client, err := a.NodeClient() + if err != nil { + return nil, pkgerrors.Wrapf(err, "create Aptos RPC client for chain %d", a.chainID) + } + return cldf_aptos.Chain{ + Selector: a.chainSelector, + Client: client, + DeployerSigner: nil, // CRE read-only use; deployer not required for View calls + URL: nodeURL, + Confirm: func(txHash string, opts ...any) error { + tx, err := client.WaitForTransaction(txHash, opts...) + if err != nil { + return err + } + if !tx.Success { + return fmt.Errorf("transaction failed: %s", tx.VmStatus) + } + return nil + }, + }, nil +} + +func (a *Deployer) Deploy(ctx context.Context, input *blockchain.Input) (blockchains.Blockchain, error) { + var bcOut *blockchain.Output + var err error + + switch { + case a.provider.IsKubernetes(): + if err = blockchains.ValidateKubernetesBlockchainOutput(input); err != nil { + return nil, err + } + a.testLogger.Info().Msgf("Using configured Kubernetes blockchain URLs for %s (chain_id: %s)", input.Type, input.ChainID) + bcOut = input.Out + case input.Out != nil: + bcOut = input.Out + default: + bcOut, err = blockchain.NewWithContext(ctx, input) + if err != nil { + return nil, pkgerrors.Wrapf(err, "failed to deploy blockchain %s chainID: %s", input.Type, input.ChainID) + } + } + + // Framework Aptos output may have empty ChainID; use config input.ChainID (e.g. "4" for local devnet) + chainIDStr := bcOut.ChainID + if chainIDStr == "" { + chainIDStr = input.ChainID + } + if chainIDStr == "" { + return nil, pkgerrors.New("aptos chain id is empty (set chain_id in [[blockchains]] in TOML)") + } + chainID, err := strconv.ParseUint(chainIDStr, 10, 64) + if err != nil { + return nil, pkgerrors.Wrapf(err, "failed to parse chain id %s", chainIDStr) + } + + selector, err := aptosChainSelector(chainIDStr, chainID) + if err != nil { + return nil, err + } + + // Ensure ctfOutput has ChainID set for downstream (e.g. findAptosChains) + bcOut.ChainID = chainIDStr + + return &Blockchain{ + testLogger: a.testLogger, + chainSelector: selector, + chainID: chainID, + ctfOutput: bcOut, + }, nil +} + +// aptosChainSelector returns the chain selector for the given Aptos chain ID. +// Uses chain-selectors when available; falls back to known Aptos localnet selector for chain_id 4. +func aptosChainSelector(chainIDStr string, chainID uint64) (uint64, error) { + chainDetails, err := chainselectors.GetChainDetailsByChainIDAndFamily(chainIDStr, chainselectors.FamilyAptos) + if err == nil { + return chainDetails.ChainSelector, nil + } + // Fallback: Aptos local devnet (aptos node run-local-testnet) uses chain_id 4 and this selector + if chainID == 4 { + const aptosLocalnetSelector = 4457093679053095497 + return aptosLocalnetSelector, nil + } + return 0, pkgerrors.Wrapf(err, "failed to get chain selector for Aptos chain id %s", chainIDStr) +} + +func aptosNodeURLWithV1(rawURL string) (string, error) { + u, err := url.Parse(strings.TrimSpace(rawURL)) + if err != nil { + return "", err + } + if u.Scheme == "" || u.Host == "" { + return "", fmt.Errorf("invalid url %q", rawURL) + } + path := strings.TrimRight(u.Path, "/") + if path == "" || path != "/v1" { + u.Path = "/v1" + } + u.RawPath = "" + u.RawQuery = "" + u.Fragment = "" + return u.String(), nil +} + +func NormalizeNodeURL(rawURL string) (string, error) { + return aptosNodeURLWithV1(rawURL) +} + +func aptosFaucetURLFromNodeURL(nodeURL string) (string, error) { + u, err := url.Parse(nodeURL) + if err != nil { + return "", err + } + host := u.Hostname() + if host == "" { + return "", fmt.Errorf("empty host in node url %q", nodeURL) + } + u.Host = host + ":8081" + u.Path = "" + u.RawPath = "" + u.RawQuery = "" + u.Fragment = "" + return u.String(), nil +} + +func FaucetURLFromNodeURL(nodeURL string) (string, error) { + return aptosFaucetURLFromNodeURL(nodeURL) +} + +func (a *Blockchain) faucetURL() (string, error) { + if a.ctfOutput == nil || len(a.ctfOutput.Nodes) == 0 { + return "", errors.New("missing chain nodes output") + } + nodeURL, err := NormalizeNodeURL(a.ctfOutput.Nodes[0].ExternalHTTPUrl) + if err != nil { + return "", err + } + return FaucetURLFromNodeURL(nodeURL) +} + +func waitForAptosAccountVisible(ctx context.Context, client *aptoslib.NodeClient, account aptoslib.AccountAddress, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + var lastErr error + for time.Now().Before(deadline) { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + _, accountErr := client.Account(account) + if accountErr == nil { + return nil + } + lastErr = accountErr + time.Sleep(1 * time.Second) + } + if lastErr != nil { + return fmt.Errorf("account %s not visible after funding attempt: %w", account.StringLong(), lastErr) + } + return fmt.Errorf("account %s not visible after funding attempt", account.StringLong()) +} + +func aptosChainIDUint8(chainID uint64) (uint8, error) { + if chainID > uint64(^uint8(0)) { + return 0, fmt.Errorf("aptos chain id %d does not fit in uint8", chainID) + } + + return uint8(chainID), nil +} + +func ChainIDUint8(chainID uint64) (uint8, error) { + return aptosChainIDUint8(chainID) +} + +func WaitForTransactionSuccess(client *aptoslib.NodeClient, txHash, label string) error { + tx, err := client.WaitForTransaction(txHash) + if err != nil { + return fmt.Errorf("failed waiting for Aptos tx %s: %w", label, err) + } + if !tx.Success { + return fmt.Errorf("aptos tx failed: %s vm_status=%s", label, tx.VmStatus) + } + return nil +} diff --git a/system-tests/lib/cre/environment/blockchains/aptos/aptos_test.go b/system-tests/lib/cre/environment/blockchains/aptos/aptos_test.go new file mode 100644 index 00000000000..246e1904f6c --- /dev/null +++ b/system-tests/lib/cre/environment/blockchains/aptos/aptos_test.go @@ -0,0 +1,36 @@ +package aptos + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNormalizeNodeURL(t *testing.T) { + t.Run("adds v1 when path is empty", func(t *testing.T) { + got, err := NormalizeNodeURL("http://127.0.0.1:8080") + require.NoError(t, err) + require.Equal(t, "http://127.0.0.1:8080/v1", got) + }) + + t.Run("preserves v1 path", func(t *testing.T) { + got, err := NormalizeNodeURL("http://127.0.0.1:8080/v1") + require.NoError(t, err) + require.Equal(t, "http://127.0.0.1:8080/v1", got) + }) +} + +func TestFaucetURLFromNodeURL(t *testing.T) { + got, err := FaucetURLFromNodeURL("http://127.0.0.1:8080/v1") + require.NoError(t, err) + require.Equal(t, "http://127.0.0.1:8081", got) +} + +func TestChainIDUint8(t *testing.T) { + got, err := ChainIDUint8(4) + require.NoError(t, err) + require.Equal(t, uint8(4), got) + + _, err = ChainIDUint8(256) + require.Error(t, err) +} diff --git a/system-tests/lib/cre/environment/blockchains/sets/sets.go b/system-tests/lib/cre/environment/blockchains/sets/sets.go index c450f607300..ff73c1c9d87 100644 --- a/system-tests/lib/cre/environment/blockchains/sets/sets.go +++ b/system-tests/lib/cre/environment/blockchains/sets/sets.go @@ -5,6 +5,7 @@ import ( "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/aptos" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/evm" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/solana" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/tron" @@ -16,5 +17,6 @@ func NewDeployerSet(testLogger zerolog.Logger, provider *infra.Provider) map[blo blockchain.FamilyEVM: evm.NewDeployer(testLogger, provider), blockchain.FamilySolana: solana.NewDeployer(testLogger, provider), blockchain.FamilyTron: tron.NewDeployer(testLogger, provider), + blockchain.FamilyAptos: aptos.NewDeployer(testLogger, provider), } } diff --git a/system-tests/lib/cre/environment/config/config.go b/system-tests/lib/cre/environment/config/config.go index 1339c7891e2..b31df9e1b6b 100644 --- a/system-tests/lib/cre/environment/config/config.go +++ b/system-tests/lib/cre/environment/config/config.go @@ -61,8 +61,8 @@ type Config struct { NodeSets []*cre.NodeSet `toml:"nodesets" validate:"required"` JD *jd.Input `toml:"jd" validate:"required"` Infra *infra.Provider `toml:"infra" validate:"required"` - Fake *fake.Input `toml:"fake" validate:"required"` - FakeHTTP *fake.Input `toml:"fake_http" validate:"required"` + Fake *fake.Input `toml:"fake"` + FakeHTTP *fake.Input `toml:"fake_http"` S3ProviderInput *s3provider.Input `toml:"s3provider"` CapabilityConfigs map[string]cre.CapabilityConfig `toml:"capability_configs"` // capability flag -> capability config Addresses []string `toml:"addresses"` diff --git a/system-tests/lib/cre/environment/dons.go b/system-tests/lib/cre/environment/dons.go index d395cbc25b0..e1bdbb6856c 100644 --- a/system-tests/lib/cre/environment/dons.go +++ b/system-tests/lib/cre/environment/dons.go @@ -214,7 +214,7 @@ func FundNodes(ctx context.Context, testLogger zerolog.Logger, dons *cre.Dons, b } for _, node := range don.Nodes { - address, addrErr := nodeAddress(node, chainFamily, bc) + address, addrErr := nodeAddress(ctx, node, chainFamily, bc) if addrErr != nil { return pkgerrors.Wrapf(addrErr, "failed to get address for node %s on chain family %s and chain %d", node.Name, chainFamily, bc.ChainID()) } @@ -237,7 +237,7 @@ func FundNodes(ctx context.Context, testLogger zerolog.Logger, dons *cre.Dons, b return nil } -func nodeAddress(node *cre.Node, chainFamily string, bc blockchains.Blockchain) (string, error) { +func nodeAddress(ctx context.Context, node *cre.Node, chainFamily string, bc blockchains.Blockchain) (string, error) { switch chainFamily { case chainselectors.FamilyEVM, chainselectors.FamilyTron: evmKey, ok := node.Keys.EVM[bc.ChainID()] @@ -253,6 +253,11 @@ func nodeAddress(node *cre.Node, chainFamily string, bc blockchains.Blockchain) return "", nil // Skip nodes without Solana keys for this chain } return solKey.PublicAddress.String(), nil + case chainselectors.FamilyAptos: + if node.Keys != nil && node.Keys.AptosAccount() != "" { + return node.Keys.AptosAccount(), nil + } + return "", nil // Skip nodes without Aptos keys for this chain default: return "", fmt.Errorf("unsupported chain family %s", chainFamily) } diff --git a/system-tests/lib/cre/environment/environment.go b/system-tests/lib/cre/environment/environment.go index f3a1ccaa2a8..925a4e53faa 100644 --- a/system-tests/lib/cre/environment/environment.go +++ b/system-tests/lib/cre/environment/environment.go @@ -189,7 +189,7 @@ func SetupTestEnvironment( fmt.Print(libformat.PurpleText("%s", input.StageGen.Wrap("Applying Features before environment startup"))) var donsCapabilities = make(map[uint64][]keystone_changeset.DONCapabilityWithConfig) var capabilityToOCR3Config = make(map[string]*ocr3.OracleConfig) - extraSignerFamiliesSet := make(map[string]bool) + capabilityToExtraSignerFamilies := make(map[string][]string) for _, feature := range input.Features.List() { for _, donMetadata := range topology.DonsMetadataWithFlag(feature.Flag()) { testLogger.Info().Msgf("Executing PreEnvStartup for feature %s for don '%s'", feature.Flag(), donMetadata.Name) @@ -209,17 +209,13 @@ func SetupTestEnvironment( } donsCapabilities[donMetadata.ID] = append(donsCapabilities[donMetadata.ID], output.DONCapabilityWithConfig...) maps.Copy(capabilityToOCR3Config, output.CapabilityToOCR3Config) - for _, f := range output.ExtraSignerFamilies { - extraSignerFamiliesSet[f] = true + for capability, families := range output.CapabilityToExtraSignerFamilies { + capabilityToExtraSignerFamilies[capability] = append([]string(nil), families...) } } testLogger.Info().Msgf("PreEnvStartup for feature %s executed successfully", feature.Flag()) } } - extraSignerFamilies := make([]string, 0, len(extraSignerFamiliesSet)) - for f := range extraSignerFamiliesSet { - extraSignerFamilies = append(extraSignerFamilies, f) - } fmt.Print(libformat.PurpleText("%s", input.StageGen.WrapAndNext("Applied Features in %.2f seconds", input.StageGen.Elapsed().Seconds()))) @@ -283,6 +279,7 @@ func SetupTestEnvironment( } fmt.Print(libformat.PurpleText("%s", input.StageGen.WrapAndNext("DONs and Job Distributor started and linked in %.2f seconds", input.StageGen.Elapsed().Seconds()))) + fmt.Print(libformat.PurpleText("%s", input.StageGen.Wrap("Creating Jobs with Job Distributor"))) gJobErr := gateway.CreateJobs(ctx, creEnvironment, dons, topology.GatewayServiceConfigs, input.GatewayWhitelistConfig) @@ -321,6 +318,7 @@ func SetupTestEnvironment( chainselectors.FamilyEVM: 10000000000000000, // 0.01 ETH chainselectors.FamilySolana: 50_000_000_000, // 50 SOL chainselectors.FamilyTron: 100_000_000, // 100 TRX in SUN + chainselectors.FamilyAptos: 1_000_000_000_000, // 1,000 APT (octas) for local devnet sender accounts } fErr := FundNodes( @@ -337,7 +335,9 @@ func SetupTestEnvironment( fmt.Print(libformat.PurpleText("%s", input.StageGen.Wrap("Configuring Workflow and Capability Registry contracts"))) - // Configure Capabilities Registry first so we can resolve actual contract donIDs + // Configure Capabilities Registry first so we can resolve actual contract DON IDs + // before wiring the workflow registry. Some downstream changesets read DON info + // from CapReg state rather than the pre-contract topology shape. capRegInput := cre.ConfigureCapabilityRegistryInput{ ChainSelector: deployedBlockchains.RegistryChain().ChainSelector(), CldEnv: creEnvironment.CldfEnvironment, @@ -350,11 +350,11 @@ func SetupTestEnvironment( input.ContractVersions[keystone_changeset.CapabilitiesRegistry.String()], ""), ), - NodeSets: input.NodeSets, - WithV2Registries: input.WithV2Registries, - DONCapabilityWithConfigs: make(map[uint64][]keystone_changeset.DONCapabilityWithConfig), - CapabilityToOCR3Config: capabilityToOCR3Config, - ExtraSignerFamilies: extraSignerFamilies, + NodeSets: input.NodeSets, + WithV2Registries: input.WithV2Registries, + DONCapabilityWithConfigs: make(map[uint64][]keystone_changeset.DONCapabilityWithConfig), + CapabilityToOCR3Config: capabilityToOCR3Config, + CapabilityToExtraSignerFamilies: capabilityToExtraSignerFamilies, } for _, capability := range input.Capabilities { @@ -373,7 +373,6 @@ func SetupTestEnvironment( if err := crecontracts.ResolveAndApplyContractDonIDs(capReg, dons, topology, input.NodeSets, input.WithV2Registries); err != nil { return nil, pkgerrors.Wrap(err, "failed to resolve and apply contract donIDs") } - wfRegVersion := input.ContractVersions[keystone_changeset.WorkflowRegistry.String()] workflowRegistryConfigurationOutput, wfErr := workflow.ConfigureWorkflowRegistry( ctx, diff --git a/system-tests/lib/cre/features/aptos/aptos.go b/system-tests/lib/cre/features/aptos/aptos.go new file mode 100644 index 00000000000..fad29a05799 --- /dev/null +++ b/system-tests/lib/cre/features/aptos/aptos.go @@ -0,0 +1,949 @@ +package aptos + +import ( + "context" + "encoding/hex" + "encoding/json" + stderrors "errors" + "fmt" + "strconv" + "strings" + "time" + + "dario.cat/mergo" + "github.com/Masterminds/semver/v3" + aptossdk "github.com/aptos-labs/aptos-go-sdk" + "github.com/pelletier/go-toml/v2" + pkgerrors "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/sethvargo/go-retry" + "google.golang.org/protobuf/types/known/durationpb" + + chainselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink-aptos/bindings/bind" + aptosplatform "github.com/smartcontractkit/chainlink-aptos/bindings/platform" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + kcr "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/capabilities_registry_1_1_0" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + "github.com/smartcontractkit/chainlink/deployment/cre/jobs" + crejobops "github.com/smartcontractkit/chainlink/deployment/cre/jobs/operations" + jobtypes "github.com/smartcontractkit/chainlink/deployment/cre/jobs/types" + "github.com/smartcontractkit/chainlink/deployment/cre/ocr3" + creocr3changeset "github.com/smartcontractkit/chainlink/deployment/cre/ocr3/v2/changeset" + creocr3contracts "github.com/smartcontractkit/chainlink/deployment/cre/ocr3/v2/changeset/operations/contracts" + "github.com/smartcontractkit/chainlink/deployment/cre/pkg/offchain" + aptoschangeset "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/aptos" + keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre" + crecontracts "github.com/smartcontractkit/chainlink/system-tests/lib/cre/contracts" + crejobs "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs/standardcapability" + creblockchains "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains" + aptoschain "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/aptos" + corechainlink "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" +) + +const ( + flag = cre.WriteAptosCapability + forwarderContractType = "AptosForwarder" + forwarderConfigVersion = 1 + capabilityVersion = "1.0.0" + capabilityLabelPrefix = "aptos:ChainSelector:" + specConfigP2PMapKey = "p2pToTransmitterMap" + specConfigScheduleKey = "transmissionSchedule" + specConfigDeltaStageKey = "deltaStage" + legacyTransmittersKey = "aptosTransmitters" + requestTimeoutKey = "RequestTimeout" + deltaStageKey = "DeltaStage" + transmissionScheduleKey = "TransmissionSchedule" + forwarderQualifier = "" + ocr3ContractQualifier = "aptos_capability_ocr3" + zeroForwarderHex = "0x0000000000000000000000000000000000000000000000000000000000000000" + defaultWriteDeltaStage = 500*time.Millisecond + 1*time.Second + defaultRequestTimeout = 30 * time.Second +) + +var forwarderContractVersion = semver.MustParse("1.0.0") + +type Aptos struct{} + +type methodConfigSettings struct { + RequestTimeout time.Duration + DeltaStage time.Duration + TransmissionSchedule capabilitiespb.TransmissionSchedule +} + +func (a *Aptos) Flag() cre.CapabilityFlag { + return flag +} + +func CapabilityLabel(chainSelector uint64) string { + return capabilityLabelPrefix + strconv.FormatUint(chainSelector, 10) +} + +func (a *Aptos) PreEnvStartup( + ctx context.Context, + testLogger zerolog.Logger, + don *cre.DonMetadata, + _ *cre.Topology, + creEnv *cre.Environment, +) (*cre.PreEnvStartupOutput, error) { + enabledChainIDs, err := don.MustNodeSet().GetEnabledChainIDsForCapability(flag) + if err != nil { + return nil, fmt.Errorf("could not find enabled chainIDs for '%s' in don '%s': %w", flag, don.Name, err) + } + if len(enabledChainIDs) == 0 { + return nil, nil + } + + forwardersByChainID := make(map[uint64]string, len(enabledChainIDs)) + for _, chainID := range enabledChainIDs { + aptosChain, findErr := findAptosChainByChainID(creEnv.Blockchains, chainID) + if findErr != nil { + return nil, findErr + } + + forwarderAddress, ensureErr := ensureForwarder(ctx, testLogger, creEnv, aptosChain) + if ensureErr != nil { + return nil, ensureErr + } + forwardersByChainID[chainID] = forwarderAddress + } + + if patchErr := patchNodeTOML(don, forwardersByChainID); patchErr != nil { + return nil, patchErr + } + + workers, err := don.Workers() + if err != nil { + return nil, err + } + p2pToTransmitterMap, err := p2pToTransmitterMapForWorkers(workers) + if err != nil { + return nil, fmt.Errorf("failed to collect Aptos worker transmitters for DON %q from metadata: %w", don.Name, err) + } + + caps := make([]keystone_changeset.DONCapabilityWithConfig, 0, len(enabledChainIDs)) + capabilityToOCR3Config := make(map[string]*ocr3.OracleConfig, len(enabledChainIDs)) + capabilityLabels := make([]string, 0, len(enabledChainIDs)) + for _, chainID := range enabledChainIDs { + aptosChain, err := findAptosChainByChainID(creEnv.Blockchains, chainID) + if err != nil { + return nil, err + } + labelledName := CapabilityLabel(aptosChain.ChainSelector()) + capabilityConfig, err := cre.ResolveCapabilityConfig(don.MustNodeSet(), flag, cre.ChainCapabilityScope(chainID)) + if err != nil { + return nil, fmt.Errorf("could not resolve capability config for '%s' on chain %d: %w", flag, chainID, err) + } + capConfig, err := BuildCapabilityConfig(capabilityConfig.Values, p2pToTransmitterMap, don.HasOnlyLocalCapabilities()) + if err != nil { + return nil, fmt.Errorf("failed to build Aptos capability config for capability %s: %w", labelledName, err) + } + + caps = append(caps, keystone_changeset.DONCapabilityWithConfig{ + Capability: kcr.CapabilitiesRegistryCapability{ + LabelledName: labelledName, + Version: capabilityVersion, + CapabilityType: 1, + }, + Config: capConfig, + UseCapRegOCRConfig: false, + }) + capabilityLabels = append(capabilityLabels, labelledName) + capabilityToOCR3Config[labelledName] = crecontracts.DefaultChainCapabilityOCR3Config() + } + + return &cre.PreEnvStartupOutput{ + DONCapabilityWithConfig: caps, + CapabilityToOCR3Config: capabilityToOCR3Config, + CapabilityToExtraSignerFamilies: cre.CapabilityToExtraSignerFamilies( + cre.OCRExtraSignerFamilies(creEnv.Blockchains), + capabilityLabels..., + ), + }, nil +} + +func (a *Aptos) PostEnvStartup( + ctx context.Context, + testLogger zerolog.Logger, + don *cre.Don, + dons *cre.Dons, + creEnv *cre.Environment, +) error { + specs := make(map[string][]string) + + var nodeSet cre.NodeSetWithCapabilityConfigs + for _, ns := range dons.AsNodeSetWithChainCapabilities() { + if ns.GetName() == don.Name { + nodeSet = ns + break + } + } + if nodeSet == nil { + return fmt.Errorf("could not find node set for Don named '%s'", don.Name) + } + + enabledChainIDs, err := nodeSet.GetEnabledChainIDsForCapability(flag) + if err != nil { + return fmt.Errorf("could not find enabled chainIDs for '%s' in don '%s': %w", flag, don.Name, err) + } + if len(enabledChainIDs) == 0 { + return nil + } + + if configureErr := configureForwarders(ctx, testLogger, don, creEnv, enabledChainIDs); configureErr != nil { + return configureErr + } + + bootstrapNode, ok := dons.Bootstrap() + if !ok { + return pkgerrors.New("bootstrap node not found; required for Aptos OCR bootstrap peers") + } + bootstrapPeers := []string{ + fmt.Sprintf("%s@%s:%d", strings.TrimPrefix(bootstrapNode.Keys.PeerID(), "p2p_"), bootstrapNode.Host, cre.OCRPeeringPort), + } + + if _, _, deployErr := crecontracts.DeployOCR3Contract(testLogger, ocr3ContractQualifier, creEnv.RegistryChainSelector, creEnv.CldfEnvironment, creEnv.ContractVersions); deployErr != nil { + return fmt.Errorf("failed to deploy Aptos OCR3 contract: %w", deployErr) + } + + for _, chainID := range enabledChainIDs { + aptosChain, chainErr := findAptosChainByChainID(creEnv.Blockchains, chainID) + if chainErr != nil { + return chainErr + } + + capabilityConfig, resolveErr := cre.ResolveCapabilityConfig(nodeSet, flag, cre.ChainCapabilityScope(chainID)) + if resolveErr != nil { + return fmt.Errorf("could not resolve capability config for '%s' on chain %d: %w", flag, chainID, resolveErr) + } + command, cErr := standardcapability.GetCommand(capabilityConfig.BinaryName) + if cErr != nil { + return pkgerrors.Wrap(cErr, "failed to get command for Aptos capability") + } + + forwarderAddress := mustForwarderAddress(creEnv.CldfEnvironment.DataStore, aptosChain.ChainSelector()) + workerMetadata, metadataErr := don.Metadata().Workers() + if metadataErr != nil { + return fmt.Errorf("failed to collect Aptos worker metadata for DON %q: %w", don.Name, metadataErr) + } + p2pToTransmitterMap, mapErr := p2pToTransmitterMapForWorkers(workerMetadata) + if mapErr != nil { + return fmt.Errorf("failed to collect Aptos worker transmitters for DON %q: %w", don.Name, mapErr) + } + methodSettings, settingsErr := resolveMethodConfigSettings(capabilityConfig.Values) + if settingsErr != nil { + return fmt.Errorf("failed to resolve Aptos method config settings for chain %d: %w", chainID, settingsErr) + } + configStr, configErr := buildWorkerConfigJSON(chainID, forwarderAddress, methodSettings, p2pToTransmitterMap, true) + if configErr != nil { + return fmt.Errorf("failed to build Aptos worker config: %w", configErr) + } + + workerInput := jobs.ProposeJobSpecInput{ + Domain: offchain.ProductLabel, + Environment: cre.EnvironmentName, + DONName: don.Name, + JobName: "write-aptos-worker-" + strconv.FormatUint(chainID, 10), + ExtraLabels: map[string]string{cre.CapabilityLabelKey: flag}, + DONFilters: []offchain.TargetDONFilter{ + {Key: offchain.FilterKeyDONName, Value: don.Name}, + }, + Template: jobtypes.Aptos, + Inputs: jobtypes.JobSpecInput{ + "command": command, + "config": configStr, + "chainSelectorEVM": creEnv.RegistryChainSelector, + "chainSelectorAptos": aptosChain.ChainSelector(), + "bootstrapPeers": bootstrapPeers, + "useCapRegOCRConfig": false, + "contractQualifier": ocr3ContractQualifier, + }, + } + + proposer := jobs.ProposeJobSpec{} + if verifyErr := proposer.VerifyPreconditions(*creEnv.CldfEnvironment, workerInput); verifyErr != nil { + return fmt.Errorf("precondition verification failed for Aptos worker job: %w", verifyErr) + } + workerReport, applyErr := proposer.Apply(*creEnv.CldfEnvironment, workerInput) + if applyErr != nil { + return fmt.Errorf("failed to propose Aptos worker job spec: %w", applyErr) + } + + for _, report := range workerReport.Reports { + out, ok := report.Output.(crejobops.ProposeStandardCapabilityJobOutput) + if !ok { + return fmt.Errorf("unable to cast to ProposeStandardCapabilityJobOutput, actual type: %T", report.Output) + } + if mergeErr := mergo.Merge(&specs, out.Specs, mergo.WithAppendSlice); mergeErr != nil { + return fmt.Errorf("failed to merge Aptos worker job specs: %w", mergeErr) + } + } + } + + if len(specs) == 0 { + return nil + } + if approveErr := crejobs.Approve(ctx, creEnv.CldfEnvironment.Offchain, dons, specs); approveErr != nil { + return fmt.Errorf("failed to approve Aptos jobs: %w", approveErr) + } + + workers, err := don.Workers() + if err != nil { + return fmt.Errorf("failed to collect Aptos worker nodes for OCR3 config: %w", err) + } + workerNodeIDs := make([]string, 0, len(workers)) + for _, worker := range workers { + if worker.JobDistributorDetails == nil { + return fmt.Errorf("worker %q is missing job distributor details", worker.Name) + } + workerNodeIDs = append(workerNodeIDs, worker.JobDistributorDetails.NodeID) + } + + _, err = creocr3changeset.ConfigureOCR3{}.Apply(*creEnv.CldfEnvironment, creocr3changeset.ConfigureOCR3Input{ + ContractChainSelector: creEnv.RegistryChainSelector, + ContractQualifier: ocr3ContractQualifier, + DON: creocr3contracts.DonNodeSet{ + Name: don.Name, + NodeIDs: workerNodeIDs, + }, + OracleConfig: don.ResolveORC3Config(crecontracts.DefaultChainCapabilityOCR3Config()), + DryRun: false, + ExtraSignerFamilies: cre.OCRExtraSignerFamilies(creEnv.Blockchains), + }) + if err != nil { + return fmt.Errorf("failed to configure Aptos OCR3 contract: %w", err) + } + return nil +} + +func forwarderAddress(ds datastore.DataStore, chainSelector uint64) (string, bool) { + key := datastore.NewAddressRefKey( + chainSelector, + datastore.ContractType(forwarderContractType), + forwarderContractVersion, + forwarderQualifier, + ) + ref, err := ds.Addresses().Get(key) + if err != nil { + return "", false + } + return ref.Address, true +} + +func mustForwarderAddress(ds datastore.DataStore, chainSelector uint64) string { + addr, ok := forwarderAddress(ds, chainSelector) + if !ok { + panic(fmt.Sprintf("missing Aptos forwarder address for chain selector %d", chainSelector)) + } + return addr +} + +// BuildCapabilityConfig builds the Aptos capability config passed directly +// through the capability manager: method execution policy in MethodConfigs and +// Aptos-specific runtime inputs in SpecConfig. +func BuildCapabilityConfig(values map[string]any, p2pToTransmitterMap map[string]string, localOnly bool) (*capabilitiespb.CapabilityConfig, error) { + methodSettings, err := resolveMethodConfigSettings(values) + if err != nil { + return nil, err + } + + capConfig := &capabilitiespb.CapabilityConfig{ + MethodConfigs: methodConfigs(methodSettings), + LocalOnly: localOnly, + } + if err := setRuntimeSpecConfig(capConfig, methodSettings, p2pToTransmitterMap); err != nil { + return nil, err + } + return capConfig, nil +} + +func buildWorkerConfigJSON(chainID uint64, forwarderAddress string, settings methodConfigSettings, p2pToTransmitterMap map[string]string, isLocal bool) (string, error) { + cfg := map[string]any{ + "chainId": strconv.FormatUint(chainID, 10), + "network": "aptos", + "creForwarderAddress": forwarderAddress, + "isLocal": isLocal, + "deltaStage": settings.DeltaStage, + } + if len(p2pToTransmitterMap) > 0 { + cfg[specConfigP2PMapKey] = p2pToTransmitterMap + } + + raw, err := json.Marshal(cfg) + if err != nil { + return "", fmt.Errorf("failed to marshal Aptos worker config: %w", err) + } + return string(raw), nil +} + +func methodConfigs(settings methodConfigSettings) map[string]*capabilitiespb.CapabilityMethodConfig { + return map[string]*capabilitiespb.CapabilityMethodConfig{ + "View": { + RemoteConfig: &capabilitiespb.CapabilityMethodConfig_RemoteExecutableConfig{ + RemoteExecutableConfig: &capabilitiespb.RemoteExecutableConfig{ + TransmissionSchedule: capabilitiespb.TransmissionSchedule_AllAtOnce, + RequestTimeout: durationpb.New(settings.RequestTimeout), + ServerMaxParallelRequests: 10, + RequestHasherType: capabilitiespb.RequestHasherType_Simple, + }, + }, + }, + "WriteReport": { + RemoteConfig: &capabilitiespb.CapabilityMethodConfig_RemoteExecutableConfig{ + RemoteExecutableConfig: &capabilitiespb.RemoteExecutableConfig{ + TransmissionSchedule: settings.TransmissionSchedule, + DeltaStage: durationpb.New(settings.DeltaStage), + RequestTimeout: durationpb.New(settings.RequestTimeout), + ServerMaxParallelRequests: 10, + RequestHasherType: capabilitiespb.RequestHasherType_WriteReportExcludeSignatures, + }, + }, + }, + } +} + +func resolveMethodConfigSettings(values map[string]any) (methodConfigSettings, error) { + settings := methodConfigSettings{ + RequestTimeout: defaultRequestTimeout, + DeltaStage: defaultWriteDeltaStage, + TransmissionSchedule: capabilitiespb.TransmissionSchedule_AllAtOnce, + } + + if values == nil { + return settings, nil + } + + requestTimeout, ok, err := durationValue(values, requestTimeoutKey) + if err != nil { + return methodConfigSettings{}, err + } + if ok { + settings.RequestTimeout = requestTimeout + } + + deltaStage, ok, err := durationValue(values, deltaStageKey) + if err != nil { + return methodConfigSettings{}, err + } + if ok { + settings.DeltaStage = deltaStage + } + + transmissionSchedule, ok, err := transmissionScheduleValue(values, transmissionScheduleKey) + if err != nil { + return methodConfigSettings{}, err + } + if ok { + settings.TransmissionSchedule = transmissionSchedule + } + + return settings, nil +} + +func transmissionScheduleValue(values map[string]any, key string) (capabilitiespb.TransmissionSchedule, bool, error) { + raw, ok := values[key] + if !ok { + return 0, false, nil + } + + schedule, ok := raw.(string) + if !ok { + return 0, false, fmt.Errorf("%s must be a string, got %T", key, raw) + } + + switch strings.TrimSpace(schedule) { + case "allAtOnce": + return capabilitiespb.TransmissionSchedule_AllAtOnce, true, nil + case "oneAtATime": + return capabilitiespb.TransmissionSchedule_OneAtATime, true, nil + default: + return 0, false, fmt.Errorf("%s must be allAtOnce or oneAtATime, got %q", key, schedule) + } +} + +func durationValue(values map[string]any, key string) (time.Duration, bool, error) { + raw, ok := values[key] + if !ok { + return 0, false, nil + } + + switch v := raw.(type) { + case string: + parsed, err := time.ParseDuration(strings.TrimSpace(v)) + if err != nil { + return 0, false, fmt.Errorf("%s must be a valid duration string: %w", key, err) + } + return parsed, true, nil + case time.Duration: + return v, true, nil + default: + return 0, false, fmt.Errorf("%s must be a duration string, got %T", key, raw) + } +} + +func patchNodeTOML(don *cre.DonMetadata, forwardersByChainID map[uint64]string) error { + for nodeIndex := range don.MustNodeSet().NodeSpecs { + currentConfig := don.MustNodeSet().NodeSpecs[nodeIndex].Node.TestConfigOverrides + if strings.TrimSpace(currentConfig) == "" { + return fmt.Errorf("missing node config for node index %d in DON %q", nodeIndex, don.Name) + } + + var typedConfig corechainlink.Config + if err := toml.Unmarshal([]byte(currentConfig), &typedConfig); err != nil { + return fmt.Errorf("failed to unmarshal config for node index %d: %w", nodeIndex, err) + } + + for chainID, forwarderAddress := range forwardersByChainID { + if err := setForwarderAddress(&typedConfig, strconv.FormatUint(chainID, 10), forwarderAddress); err != nil { + return fmt.Errorf("failed to patch Aptos forwarder address for node index %d: %w", nodeIndex, err) + } + } + + stringifiedConfig, err := toml.Marshal(typedConfig) + if err != nil { + return fmt.Errorf("failed to marshal patched config for node index %d: %w", nodeIndex, err) + } + don.MustNodeSet().NodeSpecs[nodeIndex].Node.TestConfigOverrides = string(stringifiedConfig) + } + + return nil +} + +func setForwarderAddress(cfg *corechainlink.Config, chainID, forwarderAddress string) error { + for i := range cfg.Aptos { + raw := map[string]any(cfg.Aptos[i]) + if fmt.Sprint(raw["ChainID"]) != chainID { + continue + } + + workflow := make(map[string]any) + switch existing := raw["Workflow"].(type) { + case map[string]any: + for k, v := range existing { + workflow[k] = v + } + case corechainlink.RawConfig: + for k, v := range existing { + workflow[k] = v + } + case nil: + default: + return fmt.Errorf("unexpected Aptos workflow config type %T", existing) + } + workflow["ForwarderAddress"] = forwarderAddress + raw["Workflow"] = workflow + cfg.Aptos[i] = corechainlink.RawConfig(raw) + return nil + } + + return fmt.Errorf("Aptos chain %s not found in node config", chainID) +} + +// ensureForwarder makes sure a forwarder exists for the Aptos chain selector and +// returns its address. In local Docker environments it will deploy the forwarder +// once and cache the resulting address in the CRE datastore; in non-Docker +// environments it only reuses an address that has already been injected. +func ensureForwarder( + ctx context.Context, + testLogger zerolog.Logger, + creEnv *cre.Environment, + chain *aptoschain.Blockchain, +) (string, error) { + if addr, ok := forwarderAddress(creEnv.CldfEnvironment.DataStore, chain.ChainSelector()); ok { + return addr, nil + } + if !creEnv.Provider.IsDocker() { + return "", fmt.Errorf("missing Aptos forwarder address for chain selector %d", chain.ChainSelector()) + } + + nodeURL, err := chain.NodeURL() + if err != nil { + return "", fmt.Errorf("invalid Aptos node URL for chain selector %d: %w", chain.ChainSelector(), err) + } + client, err := chain.NodeClient() + if err != nil { + return "", fmt.Errorf("failed to create Aptos client for chain selector %d (%s): %w", chain.ChainSelector(), nodeURL, err) + } + deployerAccount, err := chain.LocalDeployerAccount() + if err != nil { + return "", fmt.Errorf("failed to create Aptos deployer signer: %w", err) + } + deploymentChain, err := chain.LocalDeploymentChain() + if err != nil { + return "", fmt.Errorf("failed to build Aptos deployment chain for chain selector %d: %w", chain.ChainSelector(), err) + } + + owner := deployerAccount.AccountAddress() + if _, accountErr := client.Account(owner); accountErr != nil { + if fundErr := chain.Fund(ctx, owner.StringLong(), 100_000_000); fundErr != nil { + testLogger.Warn(). + Uint64("chainSelector", chain.ChainSelector()). + Str("nodeURL", nodeURL). + Err(fundErr). + Msg("Aptos deployer account not confirmed visible yet; proceeding with deploy retries") + } + } + + var deployedAddress string + var pendingTxHash string + var lastDeployErr error + if retryErr := retry.Do(ctx, retry.WithMaxDuration(3*time.Minute, retry.NewFibonacci(500*time.Millisecond)), func(ctx context.Context) error { + deploymentResp, deployErr := aptoschangeset.DeployPlatform(deploymentChain, owner, nil) + if deployErr != nil { + lastDeployErr = deployErr + if fundErr := chain.Fund(ctx, owner.StringLong(), 1_000_000_000_000); fundErr != nil { + testLogger.Warn(). + Uint64("chainSelector", chain.ChainSelector()). + Err(fundErr). + Msg("failed to re-fund Aptos deployer account during deploy retry") + } + return retry.RetryableError(fmt.Errorf("deploy-to-object failed: %w", deployErr)) + } + if deploymentResp == nil { + lastDeployErr = pkgerrors.New("nil deployment response") + return retry.RetryableError(pkgerrors.New("DeployPlatform returned nil response")) + } + deployedAddress = deploymentResp.Address.StringLong() + pendingTxHash = deploymentResp.Tx + return nil + }); retryErr != nil { + if lastDeployErr != nil { + return "", fmt.Errorf("failed to deploy Aptos platform forwarder for chain selector %d after retries: %w", chain.ChainSelector(), stderrors.Join(lastDeployErr, retryErr)) + } + return "", fmt.Errorf("failed to deploy Aptos platform forwarder for chain selector %d after retries: %w", chain.ChainSelector(), retryErr) + } + + addr, err := normalizeForwarderAddress(deployedAddress) + if err != nil { + return "", fmt.Errorf("invalid Aptos forwarder address parsed from deployment output for chain selector %d: %w", chain.ChainSelector(), err) + } + + if err := addForwarderToDataStore(creEnv, chain.ChainSelector(), addr); err != nil { + return "", err + } + + testLogger.Info(). + Uint64("chainSelector", chain.ChainSelector()). + Str("nodeURL", nodeURL). + Str("txHash", pendingTxHash). + Str("forwarderAddress", addr). + Msg("Aptos platform forwarder deployed") + + return addr, nil +} + +// addForwarderToDataStore seals a new datastore snapshot with the Aptos +// forwarder address so later setup phases can reuse it without redeploying. +func addForwarderToDataStore(creEnv *cre.Environment, chainSelector uint64, address string) error { + memoryDatastore, err := crecontracts.NewDataStoreFromExisting(creEnv.CldfEnvironment.DataStore) + if err != nil { + return fmt.Errorf("failed to create memory datastore: %w", err) + } + + err = memoryDatastore.AddressRefStore.Add(datastore.AddressRef{ + Address: address, + ChainSelector: chainSelector, + Type: datastore.ContractType(forwarderContractType), + Version: forwarderContractVersion, + Qualifier: forwarderQualifier, + }) + if err != nil && !stderrors.Is(err, datastore.ErrAddressRefExists) { + return fmt.Errorf("failed to add Aptos forwarder address to datastore: %w", err) + } + + creEnv.CldfEnvironment.DataStore = memoryDatastore.Seal() + return nil +} + +// configureForwarders writes the final DON membership and signer set to each +// Aptos forwarder after the DON has started and contract DON IDs are known. +func configureForwarders( + ctx context.Context, + testLogger zerolog.Logger, + don *cre.Don, + creEnv *cre.Environment, + chainIDs []uint64, +) error { + workers, err := don.Workers() + if err != nil { + return fmt.Errorf("failed to get worker nodes for DON %q: %w", don.Name, err) + } + f := (len(workers) - 1) / 3 + if f <= 0 { + return fmt.Errorf("invalid Aptos DON %q fault tolerance F=%d (workers=%d)", don.Name, f, len(workers)) + } + if f > 255 { + return fmt.Errorf("aptos DON %q fault tolerance F=%d exceeds u8", don.Name, f) + } + + donIDUint32, err := aptosDonIDUint32(don.ID) + if err != nil { + return fmt.Errorf("invalid DON id for Aptos forwarder config: %w", err) + } + + oracles, err := donOraclePublicKeys(ctx, don) + if err != nil { + return err + } + + for _, chainID := range chainIDs { + aptosChain, err := findAptosChainByChainID(creEnv.Blockchains, chainID) + if err != nil { + return err + } + + nodeURL, err := aptosChain.NodeURL() + if err != nil { + return fmt.Errorf("invalid Aptos node URL for chain selector %d: %w", aptosChain.ChainSelector(), err) + } + client, err := aptosChain.NodeClient() + if err != nil { + return fmt.Errorf("failed to create Aptos client for chain selector %d (%s): %w", aptosChain.ChainSelector(), nodeURL, err) + } + deployerAccount, err := aptosChain.LocalDeployerAccount() + if err != nil { + return fmt.Errorf("failed to create Aptos deployer signer for forwarder config: %w", err) + } + deployerAddress := deployerAccount.AccountAddress() + + if _, accountErr := client.Account(deployerAddress); accountErr != nil { + if fundErr := aptosChain.Fund(ctx, deployerAddress.StringLong(), 100_000_000); fundErr != nil { + testLogger.Warn(). + Uint64("chainSelector", aptosChain.ChainSelector()). + Str("nodeURL", nodeURL). + Err(fundErr). + Msg("Aptos deployer account not confirmed visible yet; proceeding with forwarder set_config retries") + } + } + + forwarderHex := mustForwarderAddress(creEnv.CldfEnvironment.DataStore, aptosChain.ChainSelector()) + var forwarderAddr aptossdk.AccountAddress + if err := forwarderAddr.ParseStringRelaxed(forwarderHex); err != nil { + return fmt.Errorf("invalid Aptos forwarder address for chain selector %d: %w", aptosChain.ChainSelector(), err) + } + forwarderContract := aptosplatform.Bind(forwarderAddr, client).Forwarder() + + var pendingTxHash string + var lastSetConfigErr error + if err := retry.Do(ctx, retry.WithMaxDuration(2*time.Minute, retry.NewFibonacci(500*time.Millisecond)), func(ctx context.Context) error { + pendingTx, err := forwarderContract.SetConfig(&bind.TransactOpts{Signer: deployerAccount}, donIDUint32, forwarderConfigVersion, byte(f), oracles) + if err != nil { + lastSetConfigErr = err + if fundErr := aptosChain.Fund(ctx, deployerAddress.StringLong(), 1_000_000_000_000); fundErr != nil { + testLogger.Warn(). + Uint64("chainSelector", aptosChain.ChainSelector()). + Err(fundErr). + Msg("failed to fund Aptos deployer account during set_config retry") + } + return retry.RetryableError(fmt.Errorf("set_config transaction submit failed: %w", err)) + } + pendingTxHash = pendingTx.Hash + receipt, err := client.WaitForTransaction(pendingTxHash) + if err != nil { + lastSetConfigErr = err + return retry.RetryableError(fmt.Errorf("waiting for set_config transaction failed: %w", err)) + } + if !receipt.Success { + lastSetConfigErr = fmt.Errorf("vm status: %s", receipt.VmStatus) + return retry.RetryableError(fmt.Errorf("set_config transaction failed: %s", receipt.VmStatus)) + } + return nil + }); err != nil { + if lastSetConfigErr != nil { + return fmt.Errorf("failed to configure Aptos forwarder %s for DON %q on chain selector %d: %w", forwarderHex, don.Name, aptosChain.ChainSelector(), stderrors.Join(lastSetConfigErr, err)) + } + return fmt.Errorf("failed to configure Aptos forwarder %s for DON %q on chain selector %d: %w", forwarderHex, don.Name, aptosChain.ChainSelector(), err) + } + + testLogger.Info(). + Str("donName", don.Name). + Uint64("donID", don.ID). + Uint64("chainSelector", aptosChain.ChainSelector()). + Str("txHash", pendingTxHash). + Str("forwarderAddress", forwarderHex). + Msg("configured Aptos forwarder set_config") + } + + return nil +} + +func donOraclePublicKeys(ctx context.Context, don *cre.Don) ([][]byte, error) { + workers, err := don.Workers() + if err != nil { + return nil, fmt.Errorf("failed to list worker nodes for DON %q: %w", don.Name, err) + } + + oracles := make([][]byte, 0, len(workers)) + for _, worker := range workers { + ocr2ID := "" + if worker.Keys != nil && worker.Keys.OCR2BundleIDs != nil { + ocr2ID = worker.Keys.OCR2BundleIDs[chainselectors.FamilyAptos] + } + if ocr2ID == "" { + fetchedID, err := worker.Clients.GQLClient.FetchOCR2KeyBundleID(ctx, strings.ToUpper(chainselectors.FamilyAptos)) + if err != nil { + return nil, fmt.Errorf("missing Aptos OCR2 bundle id for worker %q in DON %q and fallback fetch failed: %w", worker.Name, don.Name, err) + } + if fetchedID == "" { + return nil, fmt.Errorf("missing Aptos OCR2 bundle id for worker %q in DON %q", worker.Name, don.Name) + } + ocr2ID = fetchedID + if worker.Keys != nil { + if worker.Keys.OCR2BundleIDs == nil { + worker.Keys.OCR2BundleIDs = make(map[string]string) + } + worker.Keys.OCR2BundleIDs[chainselectors.FamilyAptos] = ocr2ID + } + } + + exported, err := worker.ExportOCR2Keys(ocr2ID) + if err != nil { + return nil, fmt.Errorf("failed to export Aptos OCR2 key for worker %q (bundle %s): %w", worker.Name, ocr2ID, err) + } + pubkey, err := parseOCR2OnchainPublicKey(exported.OnchainPublicKey) + if err != nil { + return nil, fmt.Errorf("invalid Aptos OCR2 onchain public key for worker %q: %w", worker.Name, err) + } + oracles = append(oracles, pubkey) + } + + return oracles, nil +} + +func p2pToTransmitterMapForWorkers(workers []*cre.NodeMetadata) (map[string]string, error) { + if len(workers) == 0 { + return nil, pkgerrors.New("no DON worker nodes provided") + } + + p2pToTransmitterMap := make(map[string]string) + for _, worker := range workers { + if worker.Keys == nil || worker.Keys.P2PKey == nil { + return nil, fmt.Errorf("missing P2P key for worker index %d", worker.Index) + } + + account := worker.Keys.AptosAccount() + if account == "" { + return nil, fmt.Errorf("missing Aptos account for worker index %d", worker.Index) + } + + transmitter, err := normalizeTransmitter(account) + if err != nil { + return nil, fmt.Errorf("invalid Aptos transmitter for worker index %d: %w", worker.Index, err) + } + + peerKey := hex.EncodeToString(worker.Keys.P2PKey.PeerID[:]) + p2pToTransmitterMap[peerKey] = transmitter + } + + if len(p2pToTransmitterMap) == 0 { + return nil, pkgerrors.New("no Aptos transmitters found for DON workers") + } + + return p2pToTransmitterMap, nil +} + +func setRuntimeSpecConfig(capConfig *capabilitiespb.CapabilityConfig, settings methodConfigSettings, p2pToTransmitterMap map[string]string) error { + if capConfig == nil { + return pkgerrors.New("capability config is nil") + } + + specConfig, err := values.FromMapValueProto(capConfig.SpecConfig) + if err != nil { + return fmt.Errorf("failed to decode existing spec config: %w", err) + } + if specConfig == nil { + specConfig = values.EmptyMap() + } + + delete(specConfig.Underlying, legacyTransmittersKey) + + scheduleValue, err := values.Wrap(remoteTransmissionScheduleString(settings.TransmissionSchedule)) + if err != nil { + return fmt.Errorf("failed to wrap transmission schedule: %w", err) + } + specConfig.Underlying[specConfigScheduleKey] = scheduleValue + + deltaStageValue, err := values.Wrap(settings.DeltaStage) + if err != nil { + return fmt.Errorf("failed to wrap delta stage: %w", err) + } + specConfig.Underlying[specConfigDeltaStageKey] = deltaStageValue + + if len(p2pToTransmitterMap) > 0 { + mapValue, err := values.Wrap(p2pToTransmitterMap) + if err != nil { + return fmt.Errorf("failed to wrap p2p transmitter map: %w", err) + } + specConfig.Underlying[specConfigP2PMapKey] = mapValue + } + + capConfig.SpecConfig = values.ProtoMap(specConfig) + return nil +} + +func remoteTransmissionScheduleString(schedule capabilitiespb.TransmissionSchedule) string { + switch schedule { + case capabilitiespb.TransmissionSchedule_OneAtATime: + return "oneAtATime" + default: + return "allAtOnce" + } +} + +func normalizeTransmitter(raw string) (string, error) { + s := strings.TrimSpace(raw) + if s == "" { + return "", pkgerrors.New("empty Aptos transmitter") + } + + var addr aptossdk.AccountAddress + if err := addr.ParseStringRelaxed(s); err != nil { + return "", err + } + return addr.StringLong(), nil +} + +func normalizeForwarderAddress(raw string) (string, error) { + var addr aptossdk.AccountAddress + if err := addr.ParseStringRelaxed(strings.TrimSpace(raw)); err != nil { + return "", err + } + return addr.StringLong(), nil +} + +func findAptosChainByChainID(chains []creblockchains.Blockchain, chainID uint64) (*aptoschain.Blockchain, error) { + for _, bc := range chains { + if bc.IsFamily(chainselectors.FamilyAptos) && bc.ChainID() == chainID { + aptosBlockchain, ok := bc.(*aptoschain.Blockchain) + if !ok { + return nil, fmt.Errorf("Aptos blockchain for chain id %d has unexpected type %T", chainID, bc) + } + return aptosBlockchain, nil + } + } + return nil, fmt.Errorf("Aptos blockchain for chain id %d not found", chainID) +} + +func aptosDonIDUint32(donID uint64) (uint32, error) { + if donID > uint64(^uint32(0)) { + return 0, fmt.Errorf("don id %d exceeds u32", donID) + } + return uint32(donID), nil +} + +func parseOCR2OnchainPublicKey(hexValue string) ([]byte, error) { + trimmed := strings.TrimPrefix(strings.TrimSpace(hexValue), "ocr2on_aptos_") + decoded, err := hex.DecodeString(trimmed) + if err != nil { + return nil, err + } + return decoded, nil +} + +var ( + _ cre.Feature = (*Aptos)(nil) +) diff --git a/system-tests/lib/cre/features/aptos/aptos_test.go b/system-tests/lib/cre/features/aptos/aptos_test.go new file mode 100644 index 00000000000..0b00cb9eb08 --- /dev/null +++ b/system-tests/lib/cre/features/aptos/aptos_test.go @@ -0,0 +1,234 @@ +package aptos + +import ( + "encoding/hex" + "encoding/json" + "math/big" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/p2pkey" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/secrets" + "github.com/smartcontractkit/chainlink/system-tests/lib/crypto" +) + +func TestSetRuntimeSpecConfig_ReplacesLegacyKey(t *testing.T) { + specConfig := values.EmptyMap() + legacy, err := values.Wrap([]string{"0x1"}) + require.NoError(t, err) + specConfig.Underlying[legacyTransmittersKey] = legacy + + capConfig := &capabilitiespb.CapabilityConfig{ + SpecConfig: values.ProtoMap(specConfig), + } + + expectedMap := map[string]string{ + "peer-a": "0x000000000000000000000000000000000000000000000000000000000000000a", + } + require.NoError(t, setRuntimeSpecConfig(capConfig, methodConfigSettings{ + TransmissionSchedule: capabilitiespb.TransmissionSchedule_AllAtOnce, + DeltaStage: 1500 * time.Millisecond, + }, expectedMap)) + + decoded, err := values.FromMapValueProto(capConfig.SpecConfig) + require.NoError(t, err) + require.NotNil(t, decoded) + require.NotContains(t, decoded.Underlying, legacyTransmittersKey) + + rawSchedule, ok := decoded.Underlying[specConfigScheduleKey] + require.True(t, ok) + schedule, err := rawSchedule.Unwrap() + require.NoError(t, err) + require.Equal(t, "allAtOnce", schedule) + + rawDeltaStage, ok := decoded.Underlying[specConfigDeltaStageKey] + require.True(t, ok) + deltaStage, err := rawDeltaStage.Unwrap() + require.NoError(t, err) + require.EqualValues(t, 1500*time.Millisecond, deltaStage) + + rawMap, ok := decoded.Underlying[specConfigP2PMapKey] + require.True(t, ok) + unwrapped, err := rawMap.Unwrap() + require.NoError(t, err) + require.Equal(t, map[string]any{ + "peer-a": "0x000000000000000000000000000000000000000000000000000000000000000a", + }, unwrapped) +} + +func TestBuildCapabilityConfig_UsesMethodConfigsAndSpecConfig(t *testing.T) { + capConfig, err := BuildCapabilityConfig( + map[string]any{ + requestTimeoutKey: "45s", + deltaStageKey: "2500ms", + }, + map[string]string{ + "peer-a": "0x000000000000000000000000000000000000000000000000000000000000000a", + }, + false, + ) + require.NoError(t, err) + require.False(t, capConfig.LocalOnly) + require.Nil(t, capConfig.Ocr3Configs) + require.Contains(t, capConfig.MethodConfigs, "View") + require.Contains(t, capConfig.MethodConfigs, "WriteReport") + + writeCfg := capConfig.MethodConfigs["WriteReport"].GetRemoteExecutableConfig() + require.NotNil(t, writeCfg) + require.Equal(t, capabilitiespb.TransmissionSchedule_AllAtOnce, writeCfg.TransmissionSchedule) + require.Equal(t, 2500*time.Millisecond, writeCfg.DeltaStage.AsDuration()) + require.Equal(t, 45*time.Second, writeCfg.RequestTimeout.AsDuration()) + + specConfig, err := values.FromMapValueProto(capConfig.SpecConfig) + require.NoError(t, err) + require.NotNil(t, specConfig) + + rawSchedule, ok := specConfig.Underlying[specConfigScheduleKey] + require.True(t, ok) + schedule, err := rawSchedule.Unwrap() + require.NoError(t, err) + require.Equal(t, "allAtOnce", schedule) + + rawDeltaStage, ok := specConfig.Underlying[specConfigDeltaStageKey] + require.True(t, ok) + deltaStage, err := rawDeltaStage.Unwrap() + require.NoError(t, err) + require.EqualValues(t, 2500*time.Millisecond, deltaStage) + + rawMap, ok := specConfig.Underlying[specConfigP2PMapKey] + require.True(t, ok) + unwrapped, err := rawMap.Unwrap() + require.NoError(t, err) + require.Equal(t, map[string]any{ + "peer-a": "0x000000000000000000000000000000000000000000000000000000000000000a", + }, unwrapped) +} + +func TestBuildCapabilityConfig_WithoutP2PMap_StillSetsRuntimeSpecConfig(t *testing.T) { + capConfig, err := BuildCapabilityConfig(nil, nil, true) + require.NoError(t, err) + require.True(t, capConfig.LocalOnly) + require.Nil(t, capConfig.Ocr3Configs) + require.Contains(t, capConfig.MethodConfigs, "View") + require.Contains(t, capConfig.MethodConfigs, "WriteReport") + + specConfig, err := values.FromMapValueProto(capConfig.SpecConfig) + require.NoError(t, err) + require.NotNil(t, specConfig) + require.NotContains(t, specConfig.Underlying, specConfigP2PMapKey) + require.Contains(t, specConfig.Underlying, specConfigScheduleKey) + require.Contains(t, specConfig.Underlying, specConfigDeltaStageKey) +} + +func TestBuildWorkerConfigJSON_IncludesLocalRuntimeValues(t *testing.T) { + configStr, err := buildWorkerConfigJSON( + 4, + "0x000000000000000000000000000000000000000000000000000000000000000a", + methodConfigSettings{DeltaStage: 2500 * time.Millisecond}, + map[string]string{"peer-a": "0x1"}, + true, + ) + require.NoError(t, err) + + var got map[string]any + require.NoError(t, json.Unmarshal([]byte(configStr), &got)) + require.Equal(t, "4", got["chainId"]) + require.Equal(t, "aptos", got["network"]) + require.Equal(t, true, got["isLocal"]) + require.EqualValues(t, (2500 * time.Millisecond).Nanoseconds(), got["deltaStage"]) + require.Equal(t, map[string]any{"peer-a": "0x1"}, got[specConfigP2PMapKey]) +} + +func TestNormalizeTransmitter(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "short address is normalized", + input: "0xa", + want: "0x000000000000000000000000000000000000000000000000000000000000000a", + }, + { + name: "whitespace is trimmed", + input: " 0xB ", + want: "0x000000000000000000000000000000000000000000000000000000000000000b", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := normalizeTransmitter(tc.input) + require.NoError(t, err) + require.Equal(t, tc.want, got) + }) + } + + _, err := normalizeTransmitter("not-an-address") + require.Error(t, err) +} + +func TestP2PToTransmitterMapForWorkers(t *testing.T) { + key := p2pkey.MustNewV2XXXTestingOnly(big.NewInt(1)) + workers := []*cre.NodeMetadata{ + { + Keys: &secrets.NodeKeys{ + P2PKey: &crypto.P2PKey{ + PeerID: key.PeerID(), + }, + Aptos: &crypto.AptosKey{ + Account: "0xa", + }, + }, + }, + } + + got, err := p2pToTransmitterMapForWorkers(workers) + require.NoError(t, err) + + peerID := key.PeerID() + expectedPeerKey := hex.EncodeToString(peerID[:]) + require.Equal(t, map[string]string{ + expectedPeerKey: "0x000000000000000000000000000000000000000000000000000000000000000a", + }, got) +} + +func TestResolveMethodConfigSettings_Defaults(t *testing.T) { + settings, err := resolveMethodConfigSettings(nil) + require.NoError(t, err) + require.Equal(t, defaultRequestTimeout, settings.RequestTimeout) + require.Equal(t, defaultWriteDeltaStage, settings.DeltaStage) + require.Equal(t, capabilitiespb.TransmissionSchedule_AllAtOnce, settings.TransmissionSchedule) +} + +func TestResolveMethodConfigSettings_Overrides(t *testing.T) { + settings, err := resolveMethodConfigSettings(map[string]any{ + requestTimeoutKey: "45s", + deltaStageKey: "2500ms", + transmissionScheduleKey: "oneAtATime", + }) + require.NoError(t, err) + require.Equal(t, 45*time.Second, settings.RequestTimeout) + require.Equal(t, 2500*time.Millisecond, settings.DeltaStage) + require.Equal(t, capabilitiespb.TransmissionSchedule_OneAtATime, settings.TransmissionSchedule) +} + +func TestResolveMethodConfigSettings_InvalidDuration(t *testing.T) { + _, err := resolveMethodConfigSettings(map[string]any{ + requestTimeoutKey: "not-a-duration", + }) + require.Error(t, err) +} + +func TestResolveMethodConfigSettings_InvalidTransmissionSchedule(t *testing.T) { + _, err := resolveMethodConfigSettings(map[string]any{ + transmissionScheduleKey: "staggered", + }) + require.Error(t, err) +} diff --git a/system-tests/lib/cre/features/consensus/v2/consensus.go b/system-tests/lib/cre/features/consensus/v2/consensus.go index ee1ca7e5e6f..1936400a657 100644 --- a/system-tests/lib/cre/features/consensus/v2/consensus.go +++ b/system-tests/lib/cre/features/consensus/v2/consensus.go @@ -65,6 +65,10 @@ func (c *Consensus) PreEnvStartup( CapabilityToOCR3Config: map[string]*ocr3.OracleConfig{ consensusLabelledName: contracts.DefaultOCR3Config(), }, + CapabilityToExtraSignerFamilies: cre.CapabilityToExtraSignerFamilies( + cre.OCRExtraSignerFamilies(creEnv.Blockchains), + consensusLabelledName, + ), }, nil } @@ -221,8 +225,13 @@ func proposeNodeJob(creEnv *cre.Environment, don *cre.Don, command string, boots inputs["config"] = configStr } - // Add Solana chain selector if present + // Add non-EVM OCR selectors when present so consensus can select the correct + // offchain key bundle path for report generation. for _, blockchain := range creEnv.Blockchains { + if blockchain.IsFamily(chainselectors.FamilyAptos) { + inputs["chainSelectorAptos"] = blockchain.ChainSelector() + continue + } if blockchain.IsFamily(chainselectors.FamilySolana) { inputs["chainSelectorSolana"] = blockchain.ChainSelector() break @@ -249,6 +258,12 @@ func proposeNodeJob(creEnv *cre.Environment, don *cre.Don, command string, boots report, applyErr := proposer.Apply(*creEnv.CldfEnvironment, input) if applyErr != nil { + if strings.Contains(applyErr.Error(), "no aptos ocr2 config for node") { + return nil, fmt.Errorf( + "failed to propose Consensus v2 node job spec: %w; Aptos workflows require Aptos OCR2 key bundles on all workflow DON nodes", + applyErr, + ) + } return nil, fmt.Errorf("failed to propose Consensus v2 node job spec: %w", applyErr) } diff --git a/system-tests/lib/cre/features/evm/v2/evm.go b/system-tests/lib/cre/features/evm/v2/evm.go index bfd2602feff..4cb06e319fb 100644 --- a/system-tests/lib/cre/features/evm/v2/evm.go +++ b/system-tests/lib/cre/features/evm/v2/evm.go @@ -127,13 +127,19 @@ func (o *EVM) PreEnvStartup( } capabilityToOCR3Config := make(map[string]*ocr3.OracleConfig, len(capabilities)) + capabilityLabels := make([]string, 0, len(capabilities)) for _, cap := range capabilities { capabilityToOCR3Config[cap.Capability.LabelledName] = contracts.DefaultChainCapabilityOCR3Config() + capabilityLabels = append(capabilityLabels, cap.Capability.LabelledName) } return &cre.PreEnvStartupOutput{ DONCapabilityWithConfig: capabilities, CapabilityToOCR3Config: capabilityToOCR3Config, + CapabilityToExtraSignerFamilies: cre.CapabilityToExtraSignerFamilies( + cre.OCRExtraSignerFamilies(creEnv.Blockchains), + capabilityLabels..., + ), }, nil } diff --git a/system-tests/lib/cre/features/read_contract/read_contract.go b/system-tests/lib/cre/features/read_contract/read_contract.go index 05aed9fa941..03334817659 100644 --- a/system-tests/lib/cre/features/read_contract/read_contract.go +++ b/system-tests/lib/cre/features/read_contract/read_contract.go @@ -12,6 +12,7 @@ import ( capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" kcr "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/capabilities_registry_1_1_0" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" cre_jobs "github.com/smartcontractkit/chainlink/deployment/cre/jobs" cre_jobs_ops "github.com/smartcontractkit/chainlink/deployment/cre/jobs/operations" job_types "github.com/smartcontractkit/chainlink/deployment/cre/jobs/types" @@ -22,6 +23,8 @@ import ( credon "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs/standardcapability" + creblockchains "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains" + aptosfeature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/aptos" ) const flag = cre.ReadContractCapability @@ -46,15 +49,40 @@ func (o *ReadContract) PreEnvStartup( } for _, chainID := range enabledChainIDs { + bc, findErr := findBlockchainByChainID(creEnv, chainID) + if findErr != nil { + return nil, findErr + } + + labelledName, skip, labelErr := capabilityLabelForChain(don, creEnv, chainID) + if labelErr != nil { + return nil, labelErr + } + if skip { + continue + } + + capConfig := &capabilitiespb.CapabilityConfig{ + LocalOnly: don.HasOnlyLocalCapabilities(), + } + if bc.IsFamily(blockchain.FamilyAptos) { + capabilityConfig, resolveErr := cre.ResolveCapabilityConfig(don.MustNodeSet(), flag, cre.ChainCapabilityScope(chainID)) + if resolveErr != nil { + return nil, fmt.Errorf("could not resolve capability config for '%s' on chain %d: %w", flag, chainID, resolveErr) + } + capConfig, err = aptosfeature.BuildCapabilityConfig(capabilityConfig.Values, nil, don.HasOnlyLocalCapabilities()) + if err != nil { + return nil, fmt.Errorf("failed to build Aptos read capability config for chain %d: %w", chainID, err) + } + } + capabilities = append(capabilities, keystone_changeset.DONCapabilityWithConfig{ Capability: kcr.CapabilitiesRegistryCapability{ - LabelledName: fmt.Sprintf("read-contract-evm-%d", chainID), + LabelledName: labelledName, Version: "1.0.0", CapabilityType: 1, // ACTION }, - Config: &capabilitiespb.CapabilityConfig{ - LocalOnly: don.HasOnlyLocalCapabilities(), - }, + Config: capConfig, }) } @@ -63,6 +91,38 @@ func (o *ReadContract) PreEnvStartup( }, nil } +func capabilityLabelForChain(don *cre.DonMetadata, creEnv *cre.Environment, chainID uint64) (string, bool, error) { + for _, bc := range creEnv.Blockchains { + if bc.ChainID() != chainID { + continue + } + + switch { + case bc.IsFamily(blockchain.FamilyAptos): + return aptosCapabilityLabel(don, bc) + case bc.IsFamily(blockchain.FamilyEVM), bc.IsFamily(blockchain.FamilyTron): + return fmt.Sprintf("read-contract-evm-%d", chainID), false, nil + default: + return "", false, fmt.Errorf("read-contract is not supported for chain family %s on chainID %d", bc.ChainFamily(), chainID) + } + } + + return "", false, fmt.Errorf("could not find blockchain for read-contract chainID %d", chainID) +} + +func aptosCapabilityLabel(don *cre.DonMetadata, bc blockchainOutput) (string, bool, error) { + // The Aptos feature owns capability registration when Aptos write is enabled on the DON. + if don.HasFlag(cre.WriteAptosCapability) { + return "", true, nil + } + return aptosfeature.CapabilityLabel(bc.ChainSelector()), false, nil +} + +type blockchainOutput interface { + ChainSelector() uint64 + ChainFamily() string +} + const configTemplate = `{"chainId":{{printf "%d" .ChainID}},"network":"{{.NetworkFamily}}"}` func (o *ReadContract) PostEnvStartup( @@ -91,6 +151,17 @@ func (o *ReadContract) PostEnvStartup( } for _, chainID := range enabledChainIDs { + blockchainOutput, findErr := findBlockchainByChainID(creEnv, chainID) + if findErr != nil { + return findErr + } + // Aptos write owns the Aptos ReadContract worker jobs because it needs the + // Aptos-specific OCR/bootstrap inputs that the generic read-contract path + // does not supply. Skip the duplicate generic proposal on those DONs. + if blockchainOutput.IsFamily(blockchain.FamilyAptos) && don.HasFlag(cre.WriteAptosCapability) { + continue + } + capabilityConfig, resolveErr := cre.ResolveCapabilityConfig(nodeSet, flag, cre.ChainCapabilityScope(chainID)) if resolveErr != nil { return fmt.Errorf("could not resolve capability config for '%s' on chain %d: %w", flag, chainID, resolveErr) @@ -157,10 +228,22 @@ func (o *ReadContract) PostEnvStartup( } } - approveErr := jobs.Approve(ctx, creEnv.CldfEnvironment.Offchain, dons, specs) - if approveErr != nil { - return fmt.Errorf("failed to approve Read Contract jobs: %w", approveErr) + if len(specs) > 0 { + approveErr := jobs.Approve(ctx, creEnv.CldfEnvironment.Offchain, dons, specs) + if approveErr != nil { + return fmt.Errorf("failed to approve Read Contract jobs: %w", approveErr) + } } return nil } + +func findBlockchainByChainID(creEnv *cre.Environment, chainID uint64) (creblockchains.Blockchain, error) { + for _, bc := range creEnv.Blockchains { + if bc.ChainID() == chainID { + return bc, nil + } + } + + return nil, fmt.Errorf("could not find blockchain for read-contract chainID %d", chainID) +} diff --git a/system-tests/lib/cre/features/read_contract/read_contract_test.go b/system-tests/lib/cre/features/read_contract/read_contract_test.go new file mode 100644 index 00000000000..ece08b19057 --- /dev/null +++ b/system-tests/lib/cre/features/read_contract/read_contract_test.go @@ -0,0 +1,45 @@ +package readcontract + +import ( + "testing" + + "github.com/stretchr/testify/require" + + chainselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink/system-tests/lib/cre" + aptosfeature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/aptos" +) + +type blockchainOutputStub struct { + chainSelector uint64 + chainFamily string +} + +func (s blockchainOutputStub) ChainSelector() uint64 { + return s.chainSelector +} + +func (s blockchainOutputStub) ChainFamily() string { + return s.chainFamily +} + +func TestAptosCapabilityLabel(t *testing.T) { + bc := blockchainOutputStub{chainSelector: 1, chainFamily: chainselectors.FamilyAptos} + + t.Run("skips aptos when write aptos feature owns the don", func(t *testing.T) { + don := &cre.DonMetadata{Flags: []string{cre.ReadContractCapability, cre.WriteAptosCapability}} + label, skip, err := aptosCapabilityLabel(don, bc) + require.NoError(t, err) + require.Empty(t, label) + require.True(t, skip) + }) + + t.Run("uses aptos label for read-only dons", func(t *testing.T) { + don := &cre.DonMetadata{Flags: []string{cre.ReadContractCapability}} + label, skip, err := aptosCapabilityLabel(don, bc) + require.NoError(t, err) + require.Equal(t, aptosfeature.CapabilityLabel(1), label) + require.False(t, skip) + }) +} diff --git a/system-tests/lib/cre/features/sets/sets.go b/system-tests/lib/cre/features/sets/sets.go index f29a5ad9be8..73d4c5e373e 100644 --- a/system-tests/lib/cre/features/sets/sets.go +++ b/system-tests/lib/cre/features/sets/sets.go @@ -2,6 +2,7 @@ package sets import ( "github.com/smartcontractkit/chainlink/system-tests/lib/cre" + aptos_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/aptos" consensus_v1_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/consensus/v1" consensus_v2_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/consensus/v2" cron_feature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/cron" @@ -33,6 +34,7 @@ func New() cre.Features { &http_trigger_feature.HTTPTrigger{}, &log_event_trigger_feature.LogEventTrigger{}, &mock_feature.Mock{}, + &aptos_feature.Aptos{}, &read_contract_feature.ReadContract{}, &web_api_target_feature.WebAPITarget{}, &web_api_trigger_feature.WebAPITrigger{}, diff --git a/system-tests/lib/cre/features/solana/v2/solana.go b/system-tests/lib/cre/features/solana/v2/solana.go index 91b2519ec70..e333f061aab 100644 --- a/system-tests/lib/cre/features/solana/v2/solana.go +++ b/system-tests/lib/cre/features/solana/v2/solana.go @@ -103,10 +103,14 @@ func (s *Solana) PreEnvStartup( } // 4. Register Solana capability & its methods with Keystone capabilities := registerSolanaCapability(solChain.ChainSelector()) + capabilityToExtraSignerFamilies := make(map[string][]string, len(capabilities)) + for _, capability := range capabilities { + capabilityToExtraSignerFamilies[capability.Capability.LabelledName] = []string{chainselectors.FamilySolana} + } return &cre.PreEnvStartupOutput{ - DONCapabilityWithConfig: capabilities, - ExtraSignerFamilies: []string{chainselectors.FamilySolana}, + DONCapabilityWithConfig: capabilities, + CapabilityToExtraSignerFamilies: capabilityToExtraSignerFamilies, }, nil } diff --git a/system-tests/lib/cre/flags/flags.go b/system-tests/lib/cre/flags/flags.go index 9640af140f9..ab3d568bf1b 100644 --- a/system-tests/lib/cre/flags/flags.go +++ b/system-tests/lib/cre/flags/flags.go @@ -42,7 +42,10 @@ func HasFlagForAnyChain(values []string, capability string) bool { } func RequiresForwarderContract(values []string, chainID uint64) bool { - return HasFlagForChain(values, cre.EVMCapability, chainID) || HasFlagForChain(values, cre.WriteEVMCapability, chainID) || HasFlagForAnyChain(values, cre.SolanaCapability) + return HasFlagForChain(values, cre.EVMCapability, chainID) || + HasFlagForChain(values, cre.WriteEVMCapability, chainID) || + HasFlagForChain(values, cre.WriteAptosCapability, chainID) || + HasFlagForAnyChain(values, cre.SolanaCapability) } func DonMetadataWithFlag(donTopologies []*cre.DonMetadata, flag string) []*cre.DonMetadata { diff --git a/system-tests/lib/cre/flags/flags_test.go b/system-tests/lib/cre/flags/flags_test.go new file mode 100644 index 00000000000..32593fc67b4 --- /dev/null +++ b/system-tests/lib/cre/flags/flags_test.go @@ -0,0 +1,25 @@ +package flags + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/system-tests/lib/cre" +) + +func TestRequiresForwarderContract(t *testing.T) { + t.Run("returns true for aptos write capability", func(t *testing.T) { + require.True(t, RequiresForwarderContract([]string{cre.WriteAptosCapability + "-4"}, 4)) + }) + + t.Run("returns true for evm and solana write paths", func(t *testing.T) { + require.True(t, RequiresForwarderContract([]string{cre.EVMCapability + "-1337"}, 1337)) + require.True(t, RequiresForwarderContract([]string{cre.WriteEVMCapability + "-1337"}, 1337)) + require.True(t, RequiresForwarderContract([]string{cre.SolanaCapability + "-1"}, 1)) + }) + + t.Run("returns false for read-only aptos capability set", func(t *testing.T) { + require.False(t, RequiresForwarderContract([]string{cre.ReadContractCapability + "-4"}, 4)) + }) +} diff --git a/system-tests/lib/cre/flags/provider.go b/system-tests/lib/cre/flags/provider.go index 7e38245f207..151123f1fa2 100644 --- a/system-tests/lib/cre/flags/provider.go +++ b/system-tests/lib/cre/flags/provider.go @@ -25,6 +25,7 @@ func NewDefaultCapabilityFlagsProvider() *DefaultCapbilityFlagsProvider { cre.WriteEVMCapability, cre.ReadContractCapability, cre.LogEventTriggerCapability, + cre.WriteAptosCapability, }, } } @@ -58,6 +59,7 @@ func NewExtensibleCapabilityFlagsProvider(extraGlobalFlags []string) *Extensible cre.SolanaCapability, cre.ReadContractCapability, cre.LogEventTriggerCapability, + cre.WriteAptosCapability, }, } } @@ -89,6 +91,7 @@ func NewSwappableCapabilityFlagsProvider() *DefaultCapbilityFlagsProvider { cre.ReadContractCapability, cre.LogEventTriggerCapability, cre.SolanaCapability, + cre.WriteAptosCapability, }, } } diff --git a/system-tests/lib/cre/ocr_extra_signer_families.go b/system-tests/lib/cre/ocr_extra_signer_families.go new file mode 100644 index 00000000000..ba9e22255dd --- /dev/null +++ b/system-tests/lib/cre/ocr_extra_signer_families.go @@ -0,0 +1,44 @@ +package cre + +import ( + "slices" + + chainselectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains" +) + +// OCRExtraSignerFamilies returns the additional signer families that should be +// included in OCR3 config generation beyond the default EVM signer family. +func OCRExtraSignerFamilies(blockchains []blockchains.Blockchain) []string { + familiesSet := make(map[string]struct{}) + for _, blockchain := range blockchains { + switch { + case blockchain.IsFamily(chainselectors.FamilyAptos): + familiesSet[chainselectors.FamilyAptos] = struct{}{} + case blockchain.IsFamily(chainselectors.FamilySolana): + familiesSet[chainselectors.FamilySolana] = struct{}{} + } + } + + families := make([]string, 0, len(familiesSet)) + for family := range familiesSet { + families = append(families, family) + } + slices.Sort(families) + + return families +} + +func CapabilityToExtraSignerFamilies(families []string, labelledNames ...string) map[string][]string { + if len(families) == 0 || len(labelledNames) == 0 { + return nil + } + + capabilityToFamilies := make(map[string][]string, len(labelledNames)) + for _, labelledName := range labelledNames { + capabilityToFamilies[labelledName] = append([]string(nil), families...) + } + + return capabilityToFamilies +} diff --git a/system-tests/lib/cre/types.go b/system-tests/lib/cre/types.go index 84a9da7074d..efc0ca18aa4 100644 --- a/system-tests/lib/cre/types.go +++ b/system-tests/lib/cre/types.go @@ -70,6 +70,7 @@ const ( HTTPTriggerCapability CapabilityFlag = "http-trigger" HTTPActionCapability CapabilityFlag = "http-action" SolanaCapability CapabilityFlag = "solana" + WriteAptosCapability CapabilityFlag = "write-aptos" // Add more capabilities as needed ) @@ -229,9 +230,10 @@ type CapabilityConfig struct { } // mergeCapabilityConfigs copies entries from src to dst only for keys that -// do not already exist in dst. This is NOT a deep merge - if a key exists -// in dst, its entire CapabilityConfig is preserved without modification. -// Users who override a capability config must provide all required values. +// do not already exist in dst. This is NOT a deep merge - when a key exists +// in dst, only BinaryName may be backfilled from src and Values are preserved +// exactly as provided by the override. Users who override a capability config +// must still provide all required Values. func mergeCapabilityConfigs(dst, src CapabilityConfigs) { for srcKey, srcValue := range src { if dstValue, exists := dst[srcKey]; !exists { @@ -395,9 +397,10 @@ type ConfigureCapabilityRegistryInput struct { // keyed by LabelledName CapabilityToOCR3Config map[string]*ocr3.OracleConfig - // Non-EVM chain families whose signing keys should be included in OCR3 - // config signers (e.g. ["solana"]). EVM is always included. - ExtraSignerFamilies []string + // keyed by LabelledName. Non-EVM chain families whose signing keys should be + // included in OCR3 config signers for that capability (e.g. ["solana"]). + // EVM is always included. + CapabilityToExtraSignerFamilies map[string][]string } func (c *ConfigureCapabilityRegistryInput) Validate() error { @@ -559,11 +562,16 @@ type DonMetadata struct { func NewDonMetadata(c *NodeSet, id uint64, provider infra.Provider, capabilityConfigs map[CapabilityFlag]CapabilityConfig) (*DonMetadata, error) { cfgs := make([]NodeMetadataConfig, len(c.NodeSpecs)) + aptosChainIDs, err := c.GetEnabledChainIDsForCapability(WriteAptosCapability) + if err != nil { + return nil, fmt.Errorf("failed to resolve Aptos chain ids for node metadata: %w", err) + } for i, nodeSpec := range c.NodeSpecs { cfg := NodeMetadataConfig{ Keys: NodeKeyInput{ EVMChainIDs: c.EVMChains(), SolanaChainIDs: c.SupportedSolChains, + AptosChainIDs: aptosChainIDs, Password: "dev-password", ImportedSecrets: nodeSpec.Node.TestSecretsOverrides, }, @@ -1286,6 +1294,11 @@ func (c *NodeSet) chainCapabilityIDs() []uint64 { return out } +// ChainCapabilityChainIDs returns the set of chain IDs supported by this node set's chain-scoped capabilities (e.g. read-contract-4, write-aptos-4). +func (c *NodeSet) ChainCapabilityChainIDs() []uint64 { + return c.chainCapabilityIDs() +} + func (c *NodeSet) Flags() []string { var stringCaps []string @@ -1418,6 +1431,7 @@ func (c *NodeSet) MaxFaultyNodes() (uint32, error) { type NodeKeyInput struct { EVMChainIDs []uint64 SolanaChainIDs []string + AptosChainIDs []uint64 Password string ImportedSecrets string // raw JSON string of secrets to import (usually from a previous run) @@ -1434,6 +1448,9 @@ func NewNodeKeys(input NodeKeyInput) (*secrets.NodeKeys, error) { if err != nil { return nil, errors.Wrap(err, "failed to parse imported secrets") } + if len(input.AptosChainIDs) > 0 && importedKeys.Aptos == nil { + return nil, errors.New("imported secrets are missing an Aptos key; regenerate node secrets with Aptos support") + } return importedKeys, nil } @@ -1467,6 +1484,13 @@ func NewNodeKeys(input NodeKeyInput) (*secrets.NodeKeys, error) { } out.Solana[chainID] = k } + if len(input.AptosChainIDs) > 0 { + k, err := crypto.NewAptosKey(input.Password) + if err != nil { + return nil, fmt.Errorf("failed to generate Aptos key: %w", err) + } + out.Aptos = k + } return out, nil } @@ -1633,7 +1657,8 @@ type PreEnvStartupOutput struct { DONCapabilityWithConfig []keystone_changeset.DONCapabilityWithConfig // keyed by LabelledName CapabilityToOCR3Config map[string]*ocr3.OracleConfig - // Non-EVM chain families whose signing keys should be included in OCR3 - // config signers (e.g. ["solana"]). EVM is always included. - ExtraSignerFamilies []string + // keyed by LabelledName. Non-EVM chain families whose signing keys should be + // included in OCR3 config signers for that capability (e.g. ["solana"]). + // EVM is always included. + CapabilityToExtraSignerFamilies map[string][]string } diff --git a/system-tests/lib/cre/types_nodekeys_test.go b/system-tests/lib/cre/types_nodekeys_test.go new file mode 100644 index 00000000000..662a288fb04 --- /dev/null +++ b/system-tests/lib/cre/types_nodekeys_test.go @@ -0,0 +1,58 @@ +package cre + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/secrets" + "github.com/smartcontractkit/chainlink/system-tests/lib/crypto" +) + +func TestNewNodeKeys_IgnoresEmptyImportedAptosSecretWhenAptosDisabled(t *testing.T) { + t.Parallel() + + p2pKey, err := crypto.NewP2PKey("dev-password") + require.NoError(t, err) + dkgKey, err := crypto.NewDKGRecipientKey("dev-password") + require.NoError(t, err) + + baseSecrets, err := (&secrets.NodeKeys{ + P2PKey: p2pKey, + DKGKey: dkgKey, + EVM: map[uint64]*crypto.EVMKey{}, + Solana: map[string]*crypto.SolKey{}, + }).ToNodeSecretsTOML() + require.NoError(t, err) + + keys, err := NewNodeKeys(NodeKeyInput{ + ImportedSecrets: baseSecrets, + AptosChainIDs: nil, + }) + require.NoError(t, err) + require.Nil(t, keys.Aptos) + require.Equal(t, p2pKey.PeerID, keys.P2PKey.PeerID) +} + +func TestNewNodeKeys_RejectsMissingImportedAptosSecretWhenAptosEnabled(t *testing.T) { + t.Parallel() + + p2pKey, err := crypto.NewP2PKey("dev-password") + require.NoError(t, err) + dkgKey, err := crypto.NewDKGRecipientKey("dev-password") + require.NoError(t, err) + + baseSecrets, err := (&secrets.NodeKeys{ + P2PKey: p2pKey, + DKGKey: dkgKey, + EVM: map[uint64]*crypto.EVMKey{}, + Solana: map[string]*crypto.SolKey{}, + }).ToNodeSecretsTOML() + require.NoError(t, err) + + _, err = NewNodeKeys(NodeKeyInput{ + ImportedSecrets: baseSecrets, + AptosChainIDs: []uint64{4}, + }) + require.ErrorContains(t, err, "missing an Aptos key") +} diff --git a/system-tests/lib/crypto/aptos.go b/system-tests/lib/crypto/aptos.go new file mode 100644 index 00000000000..a3c76b42768 --- /dev/null +++ b/system-tests/lib/crypto/aptos.go @@ -0,0 +1,49 @@ +package crypto + +import ( + "fmt" + + aptossdk "github.com/aptos-labs/aptos-go-sdk" + + "github.com/smartcontractkit/chainlink-common/keystore" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/aptoskey" +) + +type AptosKey struct { + EncryptedJSON []byte + PublicKey string + Account string + Password string +} + +func NewAptosKey(password string) (*AptosKey, error) { + key, err := aptoskey.New() + if err != nil { + return nil, fmt.Errorf("failed to create aptos key: %w", err) + } + + enc, err := key.ToEncryptedJSON(password, keystore.DefaultScryptParams) + if err != nil { + return nil, fmt.Errorf("failed to encrypt aptos key: %w", err) + } + + account, err := NormalizeAptosAccount(key.Account()) + if err != nil { + return nil, fmt.Errorf("failed to normalize aptos account: %w", err) + } + + return &AptosKey{ + EncryptedJSON: enc, + PublicKey: key.PublicKeyStr(), + Account: account, + Password: password, + }, nil +} + +func NormalizeAptosAccount(raw string) (string, error) { + var addr aptossdk.AccountAddress + if err := addr.ParseStringRelaxed(raw); err != nil { + return "", err + } + return addr.StringLong(), nil +} diff --git a/system-tests/lib/go.mod b/system-tests/lib/go.mod index b122b79ca02..7db360c4f4f 100644 --- a/system-tests/lib/go.mod +++ b/system-tests/lib/go.mod @@ -16,12 +16,14 @@ require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/alitto/pond/v2 v2.5.0 github.com/andybalholm/brotli v1.2.0 + github.com/aptos-labs/aptos-go-sdk v1.12.1-0.20260318141106-21b6ef4ed363 github.com/cockroachdb/errors v1.11.3 github.com/cosmos/gogoproto v1.7.0 github.com/docker/docker v28.5.2+incompatible github.com/ethereum/go-ethereum v1.17.1 github.com/fbsobreira/gotron-sdk v0.0.0-20250403083053-2943ce8c759b github.com/gagliardetto/solana-go v1.13.0 + github.com/go-resty/resty/v2 v2.17.2 github.com/goccy/go-yaml v1.19.2 github.com/google/uuid v1.6.0 github.com/jmoiron/sqlx v1.4.0 @@ -31,6 +33,7 @@ require ( github.com/scylladb/go-reflectx v1.0.1 github.com/sethvargo/go-retry v0.3.0 github.com/smartcontractkit/chain-selectors v1.0.97 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-common v0.11.2-0.20260326163134-c8e0d77df421 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 @@ -94,7 +97,6 @@ require ( github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/apache/arrow-go/v18 v18.3.1 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/aptos-labs/aptos-go-sdk v1.12.1-0.20260318141106-21b6ef4ed363 // indirect github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c // indirect github.com/avast/retry-go v3.0.0+incompatible // indirect github.com/avast/retry-go/v4 v4.7.0 // indirect @@ -262,7 +264,6 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.30.1 // indirect - github.com/go-resty/resty/v2 v2.17.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-webauthn/webauthn v0.9.4 // indirect github.com/go-webauthn/x v0.1.5 // indirect @@ -449,7 +450,6 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20260129135848-c86808ba5cb9 // indirect github.com/smartcontractkit/ccip-owner-contracts v0.1.0 // indirect - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 // indirect github.com/smartcontractkit/chainlink-automation v0.8.1 // indirect github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 // indirect github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260323224438-d819cb3228e1 // indirect diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index 05aaa3292e5..167c2033006 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -1583,8 +1583,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod index d5766e73fad..a2d33fd5083 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -23,6 +23,12 @@ replace github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evm/e replace github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evm/logtrigger => ./smoke/cre/evm/logtrigger +replace github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptosread => ./smoke/cre/aptos/aptosread + +replace github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite => ./smoke/cre/aptos/aptoswrite + +replace github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip => ./smoke/cre/aptos/aptoswriteroundtrip + replace github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/httpaction => ./smoke/cre/httpaction replace github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/consensus => ./regression/cre/consensus @@ -82,6 +88,8 @@ require ( github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/evm/evmwrite-negative v0.0.0-20251008094352-f74459c46e8c github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/evm/logtrigger-negative v0.0.0-00010101000000-000000000000 github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/http v0.0.0-20251008094352-f74459c46e8c + github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite v0.0.0-00010101000000-000000000000 + github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip v0.0.0-00010101000000-000000000000 github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evm/evmread v0.0.0-20251008094352-f74459c46e8c github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evm/logtrigger v0.0.0-00010101000000-000000000000 github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evmread v0.0.0-20250917232237-c4ecf802c6f8 @@ -192,7 +200,7 @@ require ( github.com/alitto/pond/v2 v2.5.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect github.com/apache/arrow-go/v18 v18.4.0 // indirect - github.com/aptos-labs/aptos-go-sdk v1.12.1-0.20260318141106-21b6ef4ed363 // indirect + github.com/aptos-labs/aptos-go-sdk v1.12.1-0.20260318141106-21b6ef4ed363 github.com/armon/go-metrics v0.4.1 // indirect github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c // indirect github.com/avast/retry-go v3.0.0+incompatible // indirect @@ -585,7 +593,7 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20260129135848-c86808ba5cb9 // indirect github.com/smartcontractkit/ccip-owner-contracts v0.1.0 // indirect - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 // indirect + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 github.com/smartcontractkit/chainlink-automation v0.8.1 // indirect github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20251211140724-319861e514c4 diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index 87696ed1917..ccf60adbdbb 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -1767,8 +1767,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/system-tests/tests/smoke/cre/aptos/aptosread/config/config.go b/system-tests/tests/smoke/cre/aptos/aptosread/config/config.go new file mode 100644 index 00000000000..a6b16c256db --- /dev/null +++ b/system-tests/tests/smoke/cre/aptos/aptosread/config/config.go @@ -0,0 +1,8 @@ +package config + +// Config for the Aptos read consensus workflow (reads 0x1::coin::name() on local devnet). +type Config struct { + ChainSelector uint64 `yaml:"chainSelector"` + WorkflowName string `yaml:"workflowName"` + ExpectedCoinName string `yaml:"expectedCoinName"` // expected exact value in the View reply data (e.g. "Aptos Coin" for 0x1::coin::name()) +} diff --git a/system-tests/tests/smoke/cre/aptos/aptosread/go.mod b/system-tests/tests/smoke/cre/aptos/aptosread/go.mod new file mode 100644 index 00000000000..92eaeaa1e11 --- /dev/null +++ b/system-tests/tests/smoke/cre/aptos/aptosread/go.mod @@ -0,0 +1,20 @@ +module github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptosread + +go 1.25.5 + +require ( + github.com/smartcontractkit/cre-sdk-go v1.4.1-0.20260312154349-ecb4cb615f37 + github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/aptos v0.0.0-20260312154349-ecb4cb615f37 + github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.10.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260325181729-0cac87f98cd4 // indirect + github.com/stretchr/testify v1.11.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/system-tests/tests/smoke/cre/aptos/aptosread/go.sum b/system-tests/tests/smoke/cre/aptos/aptosread/go.sum new file mode 100644 index 00000000000..32c4532781c --- /dev/null +++ b/system-tests/tests/smoke/cre/aptos/aptosread/go.sum @@ -0,0 +1,26 @@ +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260325181729-0cac87f98cd4 h1:kqdSsgt2OzJnAk0io8GsA2lJE5hKlLM2EY4uy+R6H9Y= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260325181729-0cac87f98cd4/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/cre-sdk-go v1.4.1-0.20260312154349-ecb4cb615f37 h1:+awsWWPj1CWtvcDwU8QAkUvljo/YYpnKGDrZc2afYls= +github.com/smartcontractkit/cre-sdk-go v1.4.1-0.20260312154349-ecb4cb615f37/go.mod h1:yYrQFz1UH7hhRbPO0q4fgo1tfsJNd4yXnI3oCZE0RzM= +github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/aptos v0.0.0-20260312154349-ecb4cb615f37 h1:UNem52lhklNEp4VdPBYHN+p1wgG0vDYEKSvonQgV+3o= +github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/aptos v0.0.0-20260312154349-ecb4cb615f37/go.mod h1:Ht8wSAAJRJgaZig5kwE5ogRJFngEmfQqBO0e+0y0Zng= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.10.0 h1:g7UrVaNKVEmIhVkJTk4f8raCM8Kp/RTFnAT64wqNmTY= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.10.0/go.mod h1:PWyrIw16It4TSyq6mDXqmSR0jF2evZRKuBxu7pK1yDw= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/system-tests/tests/smoke/cre/aptos/aptosread/main.go b/system-tests/tests/smoke/cre/aptos/aptosread/main.go new file mode 100644 index 00000000000..9a3ad82fc1d --- /dev/null +++ b/system-tests/tests/smoke/cre/aptos/aptosread/main.go @@ -0,0 +1,113 @@ +//go:build wasip1 + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "log/slog" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/aptos" + "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" + sdk "github.com/smartcontractkit/cre-sdk-go/cre" + "github.com/smartcontractkit/cre-sdk-go/cre/wasm" + + "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptosread/config" +) + +var aptosCoinTypeTag = &aptos.TypeTag{ + Kind: aptos.TypeTagKind_TYPE_TAG_KIND_STRUCT, + Value: &aptos.TypeTag_Struct{ + Struct: &aptos.StructTag{ + Address: []byte{0x1}, + Module: "aptos_coin", + Name: "AptosCoin", + }, + }, +} + +func main() { + wasm.NewRunner(func(b []byte) (config.Config, error) { + cfg := config.Config{} + if err := yaml.Unmarshal(b, &cfg); err != nil { + return config.Config{}, fmt.Errorf("unmarshal config: %w", err) + } + return cfg, nil + }).Run(RunReadWorkflow) +} + +func RunReadWorkflow(cfg config.Config, logger *slog.Logger, secretsProvider sdk.SecretsProvider) (sdk.Workflow[config.Config], error) { + return sdk.Workflow[config.Config]{ + sdk.Handler( + cron.Trigger(&cron.Config{Schedule: "*/30 * * * * *"}), + onAptosReadTrigger, + ), + }, nil +} + +func onAptosReadTrigger(cfg config.Config, runtime sdk.Runtime, payload *cron.Payload) (_ any, err error) { + runtime.Logger().Info("onAptosReadTrigger called", "workflow", cfg.WorkflowName) + defer func() { + if r := recover(); r != nil { + runtime.Logger().Info("Aptos read failed: panic in onAptosReadTrigger", "workflow", cfg.WorkflowName, "panic", fmt.Sprintf("%v", r)) + err = fmt.Errorf("panic: %v", r) + } + }() + + client := aptos.Client{ChainSelector: cfg.ChainSelector} + reply, err := client.View(runtime, &aptos.ViewRequest{ + Payload: &aptos.ViewPayload{ + Module: &aptos.ModuleID{ + Address: []byte{0x1}, + Name: "coin", + }, + Function: "name", + ArgTypes: []*aptos.TypeTag{aptosCoinTypeTag}, + }, + }).Await() + if err != nil { + msg := fmt.Sprintf("Aptos read failed: View error: %v", err) + runtime.Logger().Info(msg, "workflow", cfg.WorkflowName, "chainSelector", cfg.ChainSelector) + return nil, fmt.Errorf("Aptos View(0x1::coin::name): %w", err) + } + if reply == nil { + runtime.Logger().Info("Aptos read failed: View reply is nil", "workflow", cfg.WorkflowName) + return nil, errors.New("View reply is nil") + } + if len(reply.Data) == 0 { + runtime.Logger().Info("Aptos read failed: View reply data is empty", "workflow", cfg.WorkflowName) + return nil, errors.New("View reply data is empty") + } + + onchainValue, parseErr := parseSingleStringViewReply(reply.Data) + if parseErr != nil { + msg := fmt.Sprintf("Aptos read failed: cannot parse view reply data %q: %v", string(reply.Data), parseErr) + runtime.Logger().Info(msg, "workflow", cfg.WorkflowName) + return nil, fmt.Errorf("invalid Aptos view reply payload: %w", parseErr) + } + + if onchainValue != cfg.ExpectedCoinName { + msg := fmt.Sprintf("Aptos read failed: onchain value %q does not match expected %q", onchainValue, cfg.ExpectedCoinName) + runtime.Logger().Info(msg, "workflow", cfg.WorkflowName) + return nil, fmt.Errorf("onchain value %q does not match expected %q", onchainValue, cfg.ExpectedCoinName) + } + + msg := "Aptos read consensus succeeded" + runtime.Logger().Info(msg, "onchain_value", strings.TrimSpace(onchainValue), "workflow", cfg.WorkflowName) + return nil, nil +} + +func parseSingleStringViewReply(data []byte) (string, error) { + var values []string + if err := json.Unmarshal(data, &values); err != nil { + return "", fmt.Errorf("decode json string array: %w", err) + } + if len(values) == 0 { + return "", errors.New("empty json array") + } + return values[0], nil +} diff --git a/system-tests/tests/smoke/cre/aptos/aptoswrite/config/config.go b/system-tests/tests/smoke/cre/aptos/aptoswrite/config/config.go new file mode 100644 index 00000000000..bffa8499395 --- /dev/null +++ b/system-tests/tests/smoke/cre/aptos/aptoswrite/config/config.go @@ -0,0 +1,18 @@ +package config + +// Config for Aptos write workflow (submits a report via the Aptos write capability). +type Config struct { + ChainSelector uint64 `yaml:"chainSelector"` + WorkflowName string `yaml:"workflowName"` + ReceiverHex string `yaml:"receiverHex"` + ReportMessage string `yaml:"reportMessage"` + // When true, the workflow expects WriteReport to return a non-success tx status and treats that as success. + ExpectFailure bool `yaml:"expectFailure"` + // Number of OCR signatures to include in the submitted report (forwarder expects f+1). + RequiredSignatures int `yaml:"requiredSignatures"` + // Optional hex-encoded payload to pass through OCR report generation. + // If empty, ReportMessage bytes are used. + ReportPayloadHex string `yaml:"reportPayloadHex"` + MaxGasAmount uint64 `yaml:"maxGasAmount"` + GasUnitPrice uint64 `yaml:"gasUnitPrice"` +} diff --git a/system-tests/tests/smoke/cre/aptos/aptoswrite/go.mod b/system-tests/tests/smoke/cre/aptos/aptoswrite/go.mod new file mode 100644 index 00000000000..588331939b0 --- /dev/null +++ b/system-tests/tests/smoke/cre/aptos/aptoswrite/go.mod @@ -0,0 +1,20 @@ +module github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite + +go 1.25.5 + +require ( + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260325181729-0cac87f98cd4 + github.com/smartcontractkit/cre-sdk-go v1.4.1-0.20260312154349-ecb4cb615f37 + github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/aptos v0.0.0-20260312154349-ecb4cb615f37 + github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.10.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/system-tests/tests/smoke/cre/aptos/aptoswrite/go.sum b/system-tests/tests/smoke/cre/aptos/aptoswrite/go.sum new file mode 100644 index 00000000000..32c4532781c --- /dev/null +++ b/system-tests/tests/smoke/cre/aptos/aptoswrite/go.sum @@ -0,0 +1,26 @@ +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260325181729-0cac87f98cd4 h1:kqdSsgt2OzJnAk0io8GsA2lJE5hKlLM2EY4uy+R6H9Y= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260325181729-0cac87f98cd4/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/cre-sdk-go v1.4.1-0.20260312154349-ecb4cb615f37 h1:+awsWWPj1CWtvcDwU8QAkUvljo/YYpnKGDrZc2afYls= +github.com/smartcontractkit/cre-sdk-go v1.4.1-0.20260312154349-ecb4cb615f37/go.mod h1:yYrQFz1UH7hhRbPO0q4fgo1tfsJNd4yXnI3oCZE0RzM= +github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/aptos v0.0.0-20260312154349-ecb4cb615f37 h1:UNem52lhklNEp4VdPBYHN+p1wgG0vDYEKSvonQgV+3o= +github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/aptos v0.0.0-20260312154349-ecb4cb615f37/go.mod h1:Ht8wSAAJRJgaZig5kwE5ogRJFngEmfQqBO0e+0y0Zng= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.10.0 h1:g7UrVaNKVEmIhVkJTk4f8raCM8Kp/RTFnAT64wqNmTY= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.10.0/go.mod h1:PWyrIw16It4TSyq6mDXqmSR0jF2evZRKuBxu7pK1yDw= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/system-tests/tests/smoke/cre/aptos/aptoswrite/main.go b/system-tests/tests/smoke/cre/aptos/aptoswrite/main.go new file mode 100644 index 00000000000..41275dcbb26 --- /dev/null +++ b/system-tests/tests/smoke/cre/aptos/aptoswrite/main.go @@ -0,0 +1,285 @@ +//go:build wasip1 + +package main + +import ( + "encoding/hex" + "fmt" + "log/slog" + "regexp" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/aptos" + "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" + sdk "github.com/smartcontractkit/cre-sdk-go/cre" + "github.com/smartcontractkit/cre-sdk-go/cre/wasm" + + sdkpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite/config" +) + +func main() { + wasm.NewRunner(func(b []byte) (config.Config, error) { + cfg := config.Config{} + if err := yaml.Unmarshal(b, &cfg); err != nil { + return config.Config{}, fmt.Errorf("unmarshal config: %w", err) + } + return cfg, nil + }).Run(RunAptosWriteWorkflow) +} + +func RunAptosWriteWorkflow(cfg config.Config, logger *slog.Logger, secretsProvider sdk.SecretsProvider) (sdk.Workflow[config.Config], error) { + return sdk.Workflow[config.Config]{ + sdk.Handler( + cron.Trigger(&cron.Config{Schedule: "*/30 * * * * *"}), + onAptosWriteTrigger, + ), + }, nil +} + +func onAptosWriteTrigger(cfg config.Config, runtime sdk.Runtime, payload *cron.Payload) (_ any, err error) { + runtime.Logger().Info("onAptosWriteTrigger called", "workflow", cfg.WorkflowName) + + receiver, err := decodeAptosAddressHex(cfg.ReceiverHex) + if err != nil { + msg := fmt.Sprintf("Aptos write failed: invalid receiver address: %v", err) + runtime.Logger().Info(msg, "workflow", cfg.WorkflowName) + return nil, err + } + + reportPayload, err := resolveReportPayload(cfg) + if err != nil { + failMsg := fmt.Sprintf("Aptos write failed: invalid report payload: %v", err) + runtime.Logger().Info(failMsg, "workflow", cfg.WorkflowName) + return nil, err + } + + report, err := runtime.GenerateReport(&sdkpb.ReportRequest{ + EncodedPayload: reportPayload, + // Select Aptos key bundle path in consensus report generation. + EncoderName: "aptos", + // Aptos forwarder verifies ed25519 signatures over blake2b_256(raw_report). + SigningAlgo: "ed25519", + HashingAlgo: "blake2b_256", + }).Await() + if err != nil { + failMsg := fmt.Sprintf("Aptos write failed: generate report error: %v", err) + runtime.Logger().Info(failMsg, "workflow", cfg.WorkflowName) + return nil, err + } + reportResp := report.X_GeneratedCodeOnly_Unwrap() + if len(reportResp.ReportContext) == 0 { + err := fmt.Errorf("missing report context from generated report") + runtime.Logger().Info("Aptos write failed: missing report context", "workflow", cfg.WorkflowName, "error", err.Error()) + return nil, err + } + if len(reportResp.ReportContext) != 96 { + err := fmt.Errorf("unexpected report context length: got=%d want=96", len(reportResp.ReportContext)) + runtime.Logger().Info("Aptos write failed: invalid report context length", "workflow", cfg.WorkflowName, "error", err.Error()) + return nil, err + } + if len(reportResp.RawReport) == 0 { + err := fmt.Errorf("missing raw report from generated report") + runtime.Logger().Info("Aptos write failed: missing raw report", "workflow", cfg.WorkflowName, "error", err.Error()) + return nil, err + } + // Preserve generated report bytes as-is; Aptos capability handles wire-format packing. + reportVersion := int(reportResp.RawReport[0]) + runtime.Logger().Info( + "Aptos write: generated report details", + "workflow", cfg.WorkflowName, + "reportContextLen", len(reportResp.ReportContext), + "rawReportLen", len(reportResp.RawReport), + "reportVersion", reportVersion, + ) + + runtime.Logger().Info( + "Aptos write: generated report", + "workflow", cfg.WorkflowName, + "sigCount", len(reportResp.Sigs), + ) + if len(reportResp.Sigs) > 0 { + runtime.Logger().Info( + "Aptos write: first signature details", + "workflow", cfg.WorkflowName, + "firstSigLen", len(reportResp.Sigs[0].Signature), + "firstSignerID", reportResp.Sigs[0].SignerId, + ) + } + requiredSignatures := cfg.RequiredSignatures + if requiredSignatures <= 0 { + requiredSignatures = len(reportResp.Sigs) + } + if len(reportResp.Sigs) > requiredSignatures { + reportResp.Sigs = reportResp.Sigs[:requiredSignatures] + runtime.Logger().Info( + "Aptos write: trimmed report signatures for forwarder", + "workflow", cfg.WorkflowName, + "requiredSignatures", requiredSignatures, + "sigCount", len(reportResp.Sigs), + ) + } + if len(reportResp.Sigs) < requiredSignatures { + err := fmt.Errorf("insufficient report signatures: have=%d need=%d", len(reportResp.Sigs), requiredSignatures) + runtime.Logger().Info("Aptos write failed: report has fewer signatures than required", "workflow", cfg.WorkflowName, "error", err.Error()) + return nil, err + } + + client := aptos.Client{ChainSelector: cfg.ChainSelector} + runtime.Logger().Info( + "Aptos write: using gas config", + "workflow", cfg.WorkflowName, + "chainSelector", cfg.ChainSelector, + "maxGasAmount", cfg.MaxGasAmount, + "gasUnitPrice", cfg.GasUnitPrice, + ) + reply, err := client.WriteReport(runtime, &aptos.WriteReportRequest{ + Receiver: receiver, + Report: reportResp, + GasConfig: &aptos.GasConfig{ + MaxGasAmount: cfg.MaxGasAmount, + GasUnitPrice: cfg.GasUnitPrice, + }, + }).Await() + if err != nil { + if cfg.ExpectFailure { + runtime.Logger().Info( + "Aptos write failed: expected failure path requires non-empty failed tx hash", + "workflow", cfg.WorkflowName, + "txStatus", "call_error", + "txHash", "", + "error", err.Error(), + ) + return nil, fmt.Errorf("expected failed tx hash in WriteReport reply, got error instead: %w", err) + } + failMsg := fmt.Sprintf("Aptos write failed: WriteReport error: %v", err) + runtime.Logger().Info(failMsg, "workflow", cfg.WorkflowName, "chainSelector", cfg.ChainSelector) + return nil, err + } + if reply == nil { + runtime.Logger().Info("Aptos write failed: WriteReport reply is nil", "workflow", cfg.WorkflowName) + return nil, fmt.Errorf("nil WriteReport reply") + } + if cfg.ExpectFailure { + if reply.TxStatus == aptos.TxStatus_TX_STATUS_SUCCESS { + errorMsg := "" + if reply.ErrorMessage != nil { + errorMsg = *reply.ErrorMessage + } + runtime.Logger().Info( + "Aptos write failed: expected non-success tx status", + "workflow", cfg.WorkflowName, + "txStatus", reply.TxStatus.String(), + "error", errorMsg, + ) + return nil, fmt.Errorf("expected non-success tx status, got %s", reply.TxStatus.String()) + } + txHashRaw := reply.GetTxHash() + if txHashRaw == "" { + runtime.Logger().Info( + "Aptos write failed: expected failed tx hash but got empty hash", + "workflow", cfg.WorkflowName, + ) + return nil, fmt.Errorf("expected failed tx hash in WriteReport reply") + } + + txHash, err := normalizeTxHash(txHashRaw) + if err != nil { + runtime.Logger().Info("Aptos write failed: invalid failed tx hash format", "workflow", cfg.WorkflowName, "error", err.Error()) + return nil, fmt.Errorf("invalid failed tx hash format: %w", err) + } + + errorMsg := "" + if reply.ErrorMessage != nil { + errorMsg = *reply.ErrorMessage + } + runtime.Logger().Info( + fmt.Sprintf("Aptos write failure observed as expected txHash=%s", txHash), + "workflow", cfg.WorkflowName, + "txStatus", reply.TxStatus.String(), + "txHash", txHash, + "error", errorMsg, + ) + return nil, nil + } + if reply.TxStatus != aptos.TxStatus_TX_STATUS_SUCCESS { + errorMsg := "" + if reply.ErrorMessage != nil { + errorMsg = *reply.ErrorMessage + } + failMsg := fmt.Sprintf("Aptos write failed: tx status=%s error=%s", reply.TxStatus.String(), errorMsg) + runtime.Logger().Info(failMsg, "workflow", cfg.WorkflowName) + return nil, fmt.Errorf("unexpected tx status: %s", reply.TxStatus.String()) + } + txHashRaw := reply.GetTxHash() + if txHashRaw == "" { + runtime.Logger().Info( + "Aptos write failed: expected successful tx hash but got empty hash", + "workflow", cfg.WorkflowName, + "txStatus", reply.TxStatus.String(), + ) + return nil, fmt.Errorf("expected non-empty tx hash in successful WriteReport reply") + } + + txHash, err := normalizeTxHash(txHashRaw) + if err != nil { + runtime.Logger().Info("Aptos write failed: invalid tx hash format", "workflow", cfg.WorkflowName, "error", err.Error()) + return nil, fmt.Errorf("invalid tx hash format: %w", err) + } + + runtime.Logger().Info("Aptos write capability succeeded", "workflow", cfg.WorkflowName, "txHash", txHash) + return nil, nil +} + +func resolveReportPayload(cfg config.Config) ([]byte, error) { + if strings.TrimSpace(cfg.ReportPayloadHex) != "" { + trimmed := strings.TrimPrefix(strings.TrimSpace(cfg.ReportPayloadHex), "0x") + if trimmed == "" { + return nil, fmt.Errorf("empty hex payload") + } + raw, err := hex.DecodeString(trimmed) + if err != nil { + return nil, fmt.Errorf("decode hex payload: %w", err) + } + return raw, nil + } + + msg := cfg.ReportMessage + if msg == "" { + msg = "Aptos write workflow executed successfully" + } + return []byte(msg), nil +} + +func decodeAptosAddressHex(in string) ([]byte, error) { + trimmed := strings.TrimPrefix(strings.TrimSpace(in), "0x") + if trimmed == "" { + return nil, fmt.Errorf("empty address") + } + if len(trimmed)%2 != 0 { + trimmed = "0" + trimmed + } + raw, err := hex.DecodeString(trimmed) + if err != nil { + return nil, fmt.Errorf("decode hex address: %w", err) + } + if len(raw) > 32 { + return nil, fmt.Errorf("address too long: %d bytes", len(raw)) + } + out := make([]byte, 32) + copy(out[32-len(raw):], raw) + return out, nil +} + +var aptosHashRe = regexp.MustCompile(`^[0-9a-fA-F]{64}$`) + +func normalizeTxHash(raw string) (string, error) { + s := strings.TrimSpace(raw) + s = strings.TrimPrefix(strings.ToLower(s), "0x") + if !aptosHashRe.MatchString(s) { + return "", fmt.Errorf("expected 32-byte tx hash, got %q", raw) + } + return "0x" + s, nil +} diff --git a/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/config/config.go b/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/config/config.go new file mode 100644 index 00000000000..0e1987859c7 --- /dev/null +++ b/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/config/config.go @@ -0,0 +1,15 @@ +package config + +// Config for Aptos write->read roundtrip workflow. +// The workflow writes a benchmark report, then reads back get_feeds and validates the value. +type Config struct { + ChainSelector uint64 `yaml:"chainSelector"` + WorkflowName string `yaml:"workflowName"` + ReceiverHex string `yaml:"receiverHex"` + RequiredSignatures int `yaml:"requiredSignatures"` + ReportPayloadHex string `yaml:"reportPayloadHex"` + MaxGasAmount uint64 `yaml:"maxGasAmount"` + GasUnitPrice uint64 `yaml:"gasUnitPrice"` + FeedIDHex string `yaml:"feedIDHex"` + ExpectedBenchmark uint64 `yaml:"expectedBenchmark"` +} diff --git a/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/go.mod b/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/go.mod new file mode 100644 index 00000000000..3acde03e5a6 --- /dev/null +++ b/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/go.mod @@ -0,0 +1,20 @@ +module github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip + +go 1.25.5 + +require ( + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260325181729-0cac87f98cd4 + github.com/smartcontractkit/cre-sdk-go v1.4.1-0.20260312154349-ecb4cb615f37 + github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/aptos v0.0.0-20260312154349-ecb4cb615f37 + github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.10.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/go.sum b/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/go.sum new file mode 100644 index 00000000000..32c4532781c --- /dev/null +++ b/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/go.sum @@ -0,0 +1,26 @@ +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260325181729-0cac87f98cd4 h1:kqdSsgt2OzJnAk0io8GsA2lJE5hKlLM2EY4uy+R6H9Y= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260325181729-0cac87f98cd4/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/cre-sdk-go v1.4.1-0.20260312154349-ecb4cb615f37 h1:+awsWWPj1CWtvcDwU8QAkUvljo/YYpnKGDrZc2afYls= +github.com/smartcontractkit/cre-sdk-go v1.4.1-0.20260312154349-ecb4cb615f37/go.mod h1:yYrQFz1UH7hhRbPO0q4fgo1tfsJNd4yXnI3oCZE0RzM= +github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/aptos v0.0.0-20260312154349-ecb4cb615f37 h1:UNem52lhklNEp4VdPBYHN+p1wgG0vDYEKSvonQgV+3o= +github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/aptos v0.0.0-20260312154349-ecb4cb615f37/go.mod h1:Ht8wSAAJRJgaZig5kwE5ogRJFngEmfQqBO0e+0y0Zng= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.10.0 h1:g7UrVaNKVEmIhVkJTk4f8raCM8Kp/RTFnAT64wqNmTY= +github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron v0.10.0/go.mod h1:PWyrIw16It4TSyq6mDXqmSR0jF2evZRKuBxu7pK1yDw= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/main.go b/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/main.go new file mode 100644 index 00000000000..5ca3cfa592b --- /dev/null +++ b/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/main.go @@ -0,0 +1,224 @@ +//go:build wasip1 + +package main + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "log/slog" + "strconv" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/aptos" + "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron" + sdk "github.com/smartcontractkit/cre-sdk-go/cre" + "github.com/smartcontractkit/cre-sdk-go/cre/wasm" + + sdkpb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/config" +) + +type feedEntry struct { + FeedID string `json:"feed_id"` + Feed struct { + Benchmark string `json:"benchmark"` + } `json:"feed"` +} + +func main() { + wasm.NewRunner(func(b []byte) (config.Config, error) { + cfg := config.Config{} + if err := yaml.Unmarshal(b, &cfg); err != nil { + return config.Config{}, fmt.Errorf("unmarshal config: %w", err) + } + return cfg, nil + }).Run(RunAptosWriteReadRoundtripWorkflow) +} + +func RunAptosWriteReadRoundtripWorkflow(cfg config.Config, logger *slog.Logger, secretsProvider sdk.SecretsProvider) (sdk.Workflow[config.Config], error) { + return sdk.Workflow[config.Config]{ + sdk.Handler( + cron.Trigger(&cron.Config{Schedule: "*/30 * * * * *"}), + onAptosWriteReadRoundtripTrigger, + ), + }, nil +} + +func onAptosWriteReadRoundtripTrigger(cfg config.Config, runtime sdk.Runtime, payload *cron.Payload) (_ any, err error) { + runtime.Logger().Info("onAptosWriteReadRoundtripTrigger called", "workflow", cfg.WorkflowName) + + receiverBytes, err := decodeAptosAddressHex(cfg.ReceiverHex) + if err != nil { + return nil, fmt.Errorf("invalid receiver address: %w", err) + } + + reportPayload, err := resolveReportPayload(cfg.ReportPayloadHex) + if err != nil { + return nil, fmt.Errorf("invalid report payload: %w", err) + } + + report, err := runtime.GenerateReport(&sdkpb.ReportRequest{ + EncodedPayload: reportPayload, + EncoderName: "aptos", + SigningAlgo: "ed25519", + HashingAlgo: "blake2b_256", + }).Await() + if err != nil { + return nil, fmt.Errorf("generate report error: %w", err) + } + reportResp := report.X_GeneratedCodeOnly_Unwrap() + if len(reportResp.ReportContext) != 96 { + return nil, fmt.Errorf("invalid report context length: got=%d want=96", len(reportResp.ReportContext)) + } + if len(reportResp.RawReport) == 0 { + return nil, fmt.Errorf("missing raw report") + } + + requiredSignatures := cfg.RequiredSignatures + if requiredSignatures <= 0 { + requiredSignatures = len(reportResp.Sigs) + } + if len(reportResp.Sigs) < requiredSignatures { + return nil, fmt.Errorf("insufficient report signatures: have=%d need=%d", len(reportResp.Sigs), requiredSignatures) + } + if len(reportResp.Sigs) > requiredSignatures { + reportResp.Sigs = reportResp.Sigs[:requiredSignatures] + } + + client := aptos.Client{ChainSelector: cfg.ChainSelector} + reply, err := client.WriteReport(runtime, &aptos.WriteReportRequest{ + Receiver: receiverBytes, + Report: reportResp, + GasConfig: &aptos.GasConfig{ + MaxGasAmount: cfg.MaxGasAmount, + GasUnitPrice: cfg.GasUnitPrice, + }, + }).Await() + if err != nil { + return nil, fmt.Errorf("WriteReport error: %w", err) + } + if reply == nil { + return nil, fmt.Errorf("nil WriteReport reply") + } + if reply.TxStatus != aptos.TxStatus_TX_STATUS_SUCCESS { + return nil, fmt.Errorf("unexpected tx status: %s", reply.TxStatus.String()) + } + + viewReply, err := client.View(runtime, &aptos.ViewRequest{ + Payload: &aptos.ViewPayload{ + Module: &aptos.ModuleID{ + Address: receiverBytes, + Name: "registry", + }, + Function: "get_feeds", + }, + }).Await() + if err != nil { + return nil, fmt.Errorf("Aptos View(%s::registry::get_feeds): %w", normalizeHex(cfg.ReceiverHex), err) + } + if viewReply == nil || len(viewReply.Data) == 0 { + return nil, fmt.Errorf("empty view reply for %s::registry::get_feeds", normalizeHex(cfg.ReceiverHex)) + } + + benchmark, found, parseErr := parseBenchmark(viewReply.Data, cfg.FeedIDHex) + if parseErr != nil { + return nil, fmt.Errorf("parse benchmark view reply: %w", parseErr) + } + if !found { + return nil, fmt.Errorf("feed %s not found in get_feeds reply", cfg.FeedIDHex) + } + if benchmark != cfg.ExpectedBenchmark { + return nil, fmt.Errorf("benchmark mismatch: got=%d want=%d", benchmark, cfg.ExpectedBenchmark) + } + + runtime.Logger().Info( + "Aptos write/read consensus succeeded", + "workflow", cfg.WorkflowName, + "benchmark", benchmark, + "feedID", normalizeHex(cfg.FeedIDHex), + ) + return nil, nil +} + +func resolveReportPayload(reportPayloadHex string) ([]byte, error) { + trimmed := strings.TrimPrefix(strings.TrimSpace(reportPayloadHex), "0x") + if trimmed == "" { + return nil, fmt.Errorf("empty hex payload") + } + raw, err := hex.DecodeString(trimmed) + if err != nil { + return nil, fmt.Errorf("decode hex payload: %w", err) + } + return raw, nil +} + +func decodeAptosAddressHex(in string) ([]byte, error) { + trimmed := strings.TrimPrefix(strings.TrimSpace(in), "0x") + if trimmed == "" { + return nil, fmt.Errorf("empty address") + } + if len(trimmed)%2 != 0 { + trimmed = "0" + trimmed + } + raw, err := hex.DecodeString(trimmed) + if err != nil { + return nil, fmt.Errorf("decode hex address: %w", err) + } + if len(raw) > 32 { + return nil, fmt.Errorf("address too long: %d bytes", len(raw)) + } + out := make([]byte, 32) + copy(out[32-len(raw):], raw) + return out, nil +} + +func parseBenchmark(data []byte, feedIDHex string) (uint64, bool, error) { + normalizedFeedID := normalizeHex(feedIDHex) + if normalizedFeedID == "" { + return 0, false, fmt.Errorf("empty feed id") + } + + var wrapped [][]feedEntry + if err := json.Unmarshal(data, &wrapped); err == nil && len(wrapped) > 0 { + for _, entry := range wrapped[0] { + if normalizeHex(entry.FeedID) != normalizedFeedID { + continue + } + v, convErr := strconv.ParseUint(strings.TrimSpace(entry.Feed.Benchmark), 10, 64) + if convErr != nil { + return 0, false, fmt.Errorf("parse benchmark %q: %w", entry.Feed.Benchmark, convErr) + } + return v, true, nil + } + return 0, false, nil + } + + var direct []feedEntry + if err := json.Unmarshal(data, &direct); err != nil { + return 0, false, fmt.Errorf("decode get_feeds payload: %w", err) + } + for _, entry := range direct { + if normalizeHex(entry.FeedID) != normalizedFeedID { + continue + } + v, convErr := strconv.ParseUint(strings.TrimSpace(entry.Feed.Benchmark), 10, 64) + if convErr != nil { + return 0, false, fmt.Errorf("parse benchmark %q: %w", entry.Feed.Benchmark, convErr) + } + return v, true, nil + } + return 0, false, nil +} + +func normalizeHex(in string) string { + s := strings.TrimSpace(strings.ToLower(in)) + s = strings.TrimPrefix(s, "0x") + s = strings.TrimLeft(s, "0") + if s == "" { + return "0x0" + } + return "0x" + s +} diff --git a/system-tests/tests/smoke/cre/cre_suite_test.go b/system-tests/tests/smoke/cre/cre_suite_test.go index 8afebf4a006..6a92d66427e 100644 --- a/system-tests/tests/smoke/cre/cre_suite_test.go +++ b/system-tests/tests/smoke/cre/cre_suite_test.go @@ -242,6 +242,12 @@ func Test_CRE_V2_Solana_Suite(t *testing.T) { }) } +func Test_CRE_V2_Aptos_Suite(t *testing.T) { + testEnv := t_helpers.SetupTestEnvironmentWithConfig(t, t_helpers.GetTestConfig(t, "/configs/workflow-gateway-don-aptos.toml")) + t.Run("[v2] Aptos", func(t *testing.T) { + ExecuteAptosTest(t, testEnv) + }) +} func Test_CRE_V2_HTTP_Action_Regression_Suite(t *testing.T) { testEnv := t_helpers.SetupTestEnvironmentWithConfig(t, t_helpers.GetDefaultTestConfig(t)) diff --git a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go new file mode 100644 index 00000000000..2a5a614de13 --- /dev/null +++ b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go @@ -0,0 +1,755 @@ +package cre + +import ( + "context" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "net/http" + "os" + "regexp" + "strings" + "testing" + "time" + + "github.com/Masterminds/semver/v3" + aptoslib "github.com/aptos-labs/aptos-go-sdk" + aptoscrypto "github.com/aptos-labs/aptos-go-sdk/crypto" + "github.com/ethereum/go-ethereum/common" + "github.com/rs/zerolog" + "github.com/stretchr/testify/require" + + aptosbind "github.com/smartcontractkit/chainlink-aptos/bindings/bind" + aptosdatafeeds "github.com/smartcontractkit/chainlink-aptos/bindings/data_feeds" + aptosplatformsecondary "github.com/smartcontractkit/chainlink-aptos/bindings/platform_secondary" + "github.com/smartcontractkit/chainlink-testing-framework/framework" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + + commonevents "github.com/smartcontractkit/chainlink-protos/workflows/go/common" + workflowevents "github.com/smartcontractkit/chainlink-protos/workflows/go/events" + + crelib "github.com/smartcontractkit/chainlink/system-tests/lib/cre" + crecontracts "github.com/smartcontractkit/chainlink/system-tests/lib/cre/contracts" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains" + blockchains_aptos "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/aptos" + blockchains_evm "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/evm" + crecrypto "github.com/smartcontractkit/chainlink/system-tests/lib/crypto" + aptoswrite_config "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite/config" + aptoswriteroundtrip_config "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/config" + t_helpers "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers" + "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers/configuration" +) + +const aptosLocalMaxGasAmount uint64 = 200_000 +const aptosWorkerFundingAmountOctas uint64 = 1_000_000_000_000 + +var aptosForwarderVersion = semver.MustParse("1.0.0") + +// ExecuteAptosTest runs the Aptos CRE suite with the read path only. The write +// scenarios stay available for local/manual execution until the write-report CI +// path is ready again. +func ExecuteAptosTest(t *testing.T, tenv *configuration.TestEnvironment) { + executeAptosScenarios(t, tenv, aptosDefaultScenarios()) +} + +type aptosScenario struct { + name string + run func( + t *testing.T, + tenv *configuration.TestEnvironment, + aptosChain blockchains.Blockchain, + userLogsCh <-chan *workflowevents.UserLogs, + baseMessageCh <-chan *commonevents.BaseMessage, + ) +} + +func aptosDefaultScenarios() []aptosScenario { + return []aptosScenario{ + {name: "Aptos Read", run: ExecuteAptosReadTest}, + } +} + +func executeAptosScenarios(t *testing.T, tenv *configuration.TestEnvironment, scenarios []aptosScenario) { + creEnv := tenv.CreEnvironment + require.NotEmpty(t, creEnv.Blockchains, "Aptos suite expects at least one blockchain in the environment") + + var aptosChain blockchains.Blockchain + for _, bc := range creEnv.Blockchains { + if bc.IsFamily(blockchain.FamilyAptos) { + aptosChain = bc + break + } + } + require.NotNil(t, aptosChain, "Aptos suite expects an Aptos chain in the environment (use config workflow-gateway-don-aptos.toml)") + + lggr := framework.L + userLogsCh := make(chan *workflowevents.UserLogs, 1000) + baseMessageCh := make(chan *commonevents.BaseMessage, 1000) + + writeDon := findWriteAptosDonForChain(t, tenv, aptosChain.ChainID()) + assertAptosWorkerRuntimeKeysMatchMetadata(t, writeDon) + + server := t_helpers.StartChipTestSink(t, t_helpers.GetPublishFn(lggr, userLogsCh, baseMessageCh)) + t.Cleanup(func() { + server.Shutdown(t.Context()) + close(userLogsCh) + close(baseMessageCh) + }) + + for _, scenario := range scenarios { + t.Run(scenario.name, func(t *testing.T) { + scenario.run(t, tenv, aptosChain, userLogsCh, baseMessageCh) + }) + } +} + +func assertAptosWorkerRuntimeKeysMatchMetadata(t *testing.T, writeDon *crelib.Don) { + t.Helper() + + workers, err := writeDon.Workers() + require.NoError(t, err, "failed to list Aptos write DON workers") + require.NotEmpty(t, workers, "Aptos write DON workers list is empty") + + for _, worker := range workers { + require.NotNil(t, worker.Keys, "worker %q is missing metadata keys", worker.Name) + require.NotNil(t, worker.Keys.Aptos, "worker %q is missing metadata Aptos key", worker.Name) + + expectedAccount, err := crecrypto.NormalizeAptosAccount(worker.Keys.Aptos.Account) + require.NoError(t, err, "worker %q has invalid metadata Aptos account", worker.Name) + expectedPublicKey := normalizeHexValue(worker.Keys.Aptos.PublicKey) + require.NotEmpty(t, expectedPublicKey, "worker %q is missing metadata Aptos public key", worker.Name) + + var runtimeKeys struct { + Data []struct { + Attributes struct { + Account string `json:"account"` + PublicKey string `json:"publicKey"` + } `json:"attributes"` + } `json:"data"` + } + resp, err := worker.Clients.RestClient.APIClient.R(). + SetResult(&runtimeKeys). + Get("/v2/keys/aptos") + require.NoError(t, err, "failed to read runtime Aptos keys for worker %q", worker.Name) + require.Equal(t, http.StatusOK, resp.StatusCode(), "worker %q Aptos keys endpoint returned unexpected status", worker.Name) + require.Len(t, runtimeKeys.Data, 1, "worker %q must expose exactly one Aptos runtime key", worker.Name) + + runtimeKey := runtimeKeys.Data[0].Attributes + actualAccount, err := crecrypto.NormalizeAptosAccount(runtimeKey.Account) + require.NoError(t, err, "worker %q exposed invalid runtime Aptos account", worker.Name) + require.Equal(t, expectedAccount, actualAccount, "worker %q runtime Aptos account does not match metadata-generated account", worker.Name) + require.Equal(t, expectedPublicKey, normalizeHexValue(runtimeKey.PublicKey), "worker %q runtime Aptos public key does not match metadata-generated key", worker.Name) + } +} + +// ExecuteAptosReadTest deploys a workflow that reads 0x1::coin::name() on Aptos local devnet +// in a consensus read step and asserts the expected value. +func ExecuteAptosReadTest( + t *testing.T, + tenv *configuration.TestEnvironment, + aptosChain blockchains.Blockchain, + userLogsCh <-chan *workflowevents.UserLogs, + baseMessageCh <-chan *commonevents.BaseMessage, +) { + lggr := framework.L + + // Fixed name so re-runs against the same DON overwrite the same workflow instead of accumulating multiple (e.g. aptos-read-workflow-4838 and aptos-read-workflow-5736). + const workflowName = "aptos-read-workflow" + workflowConfig := t_helpers.AptosReadWorkflowConfig{ + ChainSelector: aptosChain.ChainSelector(), + WorkflowName: workflowName, + ExpectedCoinName: "Aptos Coin", // 0x1::coin::name<0x1::aptos_coin::AptosCoin>() on local devnet + } + + const workflowFileLocation = "./aptos/aptosread/main.go" + t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &workflowConfig, workflowFileLocation) + + expectedLog := "Aptos read consensus succeeded" + t_helpers.WatchWorkflowLogs(t, lggr, userLogsCh, baseMessageCh, t_helpers.WorkflowEngineInitErrorLog, expectedLog, 4*time.Minute) + lggr.Info().Str("expected_log", expectedLog).Msg("Aptos read capability test passed") +} + +func ExecuteAptosWriteTest( + t *testing.T, + tenv *configuration.TestEnvironment, + aptosChain blockchains.Blockchain, + userLogsCh <-chan *workflowevents.UserLogs, + baseMessageCh <-chan *commonevents.BaseMessage, +) { + lggr := framework.L + scenario := prepareAptosWriteScenario(t, tenv, aptosChain) + + const workflowName = "aptos-write-workflow" + workflowConfig := aptoswrite_config.Config{ + ChainSelector: scenario.chainSelector, + WorkflowName: workflowName, + ReceiverHex: scenario.receiverHex, + RequiredSignatures: scenario.requiredSignatures, + ReportPayloadHex: scenario.reportPayloadHex, + // Keep within the current local Aptos transaction max-gas bound. + MaxGasAmount: aptosLocalMaxGasAmount, + GasUnitPrice: 100, + } + + const workflowFileLocation = "./aptos/aptoswrite/main.go" + ensureAptosWriteWorkersFunded(t, aptosChain, scenario.writeDon) + t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &workflowConfig, workflowFileLocation) + + txHash := waitForAptosWriteSuccessLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, 4*time.Minute) + assertAptosReceiverUpdatedOnChain(t, aptosChain, scenario.receiverHex, scenario.expectedBenchmarkValue) + assertAptosWriteTxOnChain(t, aptosChain, txHash, scenario.receiverHex) + lggr.Info(). + Str("tx_hash", txHash). + Str("receiver", scenario.receiverHex). + Msg("Aptos write capability test passed with onchain verification") +} + +func ExecuteAptosWriteReadRoundtripTest( + t *testing.T, + tenv *configuration.TestEnvironment, + aptosChain blockchains.Blockchain, + userLogsCh <-chan *workflowevents.UserLogs, + baseMessageCh <-chan *commonevents.BaseMessage, +) { + lggr := framework.L + scenario := prepareAptosRoundtripScenario(t, tenv, aptosChain) + + const workflowName = "aptos-write-read-roundtrip-workflow" + roundtripCfg := aptoswriteroundtrip_config.Config{ + ChainSelector: scenario.chainSelector, + WorkflowName: workflowName, + ReceiverHex: scenario.receiverHex, + RequiredSignatures: scenario.requiredSignatures, + ReportPayloadHex: scenario.reportPayloadHex, + MaxGasAmount: aptosLocalMaxGasAmount, + GasUnitPrice: 100, + FeedIDHex: scenario.feedIDHex, + ExpectedBenchmark: scenario.expectedBenchmarkValue, + } + + ensureAptosWriteWorkersFunded(t, aptosChain, scenario.writeDon) + t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &roundtripCfg, "./aptos/aptoswriteroundtrip/main.go") + t_helpers.WatchWorkflowLogs( + t, + lggr, + userLogsCh, + baseMessageCh, + t_helpers.WorkflowEngineInitErrorLog, + "Aptos write/read consensus succeeded", + 4*time.Minute, + ) + lggr.Info(). + Str("receiver", scenario.receiverHex). + Uint64("expected_benchmark", scenario.expectedBenchmarkValue). + Str("feed_id", scenario.feedIDHex). + Msg("Aptos write/read roundtrip capability test passed") +} + +func ExecuteAptosWriteExpectedFailureTest( + t *testing.T, + tenv *configuration.TestEnvironment, + aptosChain blockchains.Blockchain, + userLogsCh <-chan *workflowevents.UserLogs, + baseMessageCh <-chan *commonevents.BaseMessage, +) { + lggr := framework.L + scenario := prepareAptosWriteScenario(t, tenv, aptosChain) + + const workflowName = "aptos-write-expected-failure-workflow" + workflowConfig := aptoswrite_config.Config{ + ChainSelector: scenario.chainSelector, + WorkflowName: workflowName, + ReceiverHex: "0x0", // Intentionally invalid write receiver to force onchain failure path. + RequiredSignatures: scenario.requiredSignatures, + ReportPayloadHex: scenario.reportPayloadHex, + MaxGasAmount: aptosLocalMaxGasAmount, + GasUnitPrice: 100, + ExpectFailure: true, + } + + const workflowFileLocation = "./aptos/aptoswrite/main.go" + ensureAptosWriteWorkersFunded(t, aptosChain, scenario.writeDon) + t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &workflowConfig, workflowFileLocation) + + txHash := waitForAptosWriteExpectedFailureLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, 4*time.Minute) + assertAptosWriteFailureTxOnChain(t, aptosChain, txHash) + + lggr.Info(). + Str("tx_hash", txHash). + Msg("Aptos expected write-failure workflow test passed") +} + +type aptosWriteScenario struct { + chainSelector uint64 + receiverHex string + reportPayloadHex string + feedIDHex string + expectedBenchmarkValue uint64 + requiredSignatures int + writeDon *crelib.Don +} + +func prepareAptosWriteScenario(t *testing.T, tenv *configuration.TestEnvironment, aptosChain blockchains.Blockchain) aptosWriteScenario { + return prepareAptosWriteScenarioWithBenchmark(t, tenv, aptosChain, aptosBenchmarkFeedID(), 123456789) +} + +func prepareAptosRoundtripScenario(t *testing.T, tenv *configuration.TestEnvironment, aptosChain blockchains.Blockchain) aptosWriteScenario { + return prepareAptosWriteScenarioWithBenchmark(t, tenv, aptosChain, aptosRoundtripFeedID(), 987654321) +} + +func prepareAptosWriteScenarioWithBenchmark( + t *testing.T, + tenv *configuration.TestEnvironment, + aptosChain blockchains.Blockchain, + feedID []byte, + expectedBenchmark uint64, +) aptosWriteScenario { + t.Helper() + + forwarderHex := aptosForwarderAddress(tenv, aptosChain.ChainSelector()) + require.NotEmpty(t, forwarderHex, "Aptos write test requires forwarder address for chainSelector=%d", aptosChain.ChainSelector()) + require.False(t, isZeroAptosAddress(forwarderHex), "Aptos write test requires non-zero forwarder address for chainSelector=%d", aptosChain.ChainSelector()) + + writeDon := findWriteAptosDonForChain(t, tenv, aptosChain.ChainID()) + workers, workerErr := writeDon.Workers() + require.NoError(t, workerErr, "failed to list Aptos write DON workers") + f := (len(workers) - 1) / 3 + require.GreaterOrEqual(t, f, 1, "Aptos write DON requires f>=1") + + return aptosWriteScenario{ + chainSelector: aptosChain.ChainSelector(), + receiverHex: deployAptosDataFeedsReceiverForWrite(t, tenv, aptosChain, forwarderHex, feedID), + reportPayloadHex: hex.EncodeToString(buildAptosDataFeedsBenchmarkPayloadFor(feedID, expectedBenchmark)), + feedIDHex: hex.EncodeToString(feedID), + expectedBenchmarkValue: expectedBenchmark, + requiredSignatures: f + 1, + writeDon: writeDon, + } +} + +func findWriteAptosDonForChain(t *testing.T, tenv *configuration.TestEnvironment, chainID uint64) *crelib.Don { + t.Helper() + require.NotNil(t, tenv.Dons, "test environment DON metadata is required") + + for _, don := range tenv.Dons.List() { + if !don.HasFlag("write-aptos") { + continue + } + chainIDs, err := don.GetEnabledChainIDsForCapability("write-aptos") + require.NoError(t, err, "failed to read enabled chain ids for DON %q", don.Name) + for _, id := range chainIDs { + if id == chainID { + return don + } + } + } + + require.FailNowf(t, "missing Aptos write DON", "could not find write-aptos DON for chainID=%d", chainID) + return nil +} + +func isZeroAptosAddress(addr string) bool { + trimmed := strings.TrimPrefix(strings.ToLower(strings.TrimSpace(addr)), "0x") + if trimmed == "" { + return true + } + for _, ch := range trimmed { + if ch != '0' { + return false + } + } + return true +} + +func aptosForwarderAddress(tenv *configuration.TestEnvironment, chainSelector uint64) string { + return crecontracts.MustGetAddressFromDataStore( + tenv.CreEnvironment.CldfEnvironment.DataStore, + chainSelector, + "AptosForwarder", + aptosForwarderVersion, + "", + ) +} + +var aptosTxHashInLogRe = regexp.MustCompile(`txHash=([^\s"]+)`) + +func waitForAptosWriteSuccessLogAndTxHash( + t *testing.T, + lggr zerolog.Logger, + userLogsCh <-chan *workflowevents.UserLogs, + baseMessageCh <-chan *commonevents.BaseMessage, + timeout time.Duration, +) string { + t.Helper() + return waitForAptosLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, "Aptos write capability succeeded", timeout) +} + +func waitForAptosWriteExpectedFailureLogAndTxHash( + t *testing.T, + lggr zerolog.Logger, + userLogsCh <-chan *workflowevents.UserLogs, + baseMessageCh <-chan *commonevents.BaseMessage, + timeout time.Duration, +) string { + t.Helper() + return waitForAptosLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, "Aptos write failure observed as expected", timeout) +} + +func waitForAptosLogAndTxHash( + t *testing.T, + lggr zerolog.Logger, + userLogsCh <-chan *workflowevents.UserLogs, + baseMessageCh <-chan *commonevents.BaseMessage, + expectedLog string, + timeout time.Duration, +) string { + t.Helper() + + ctx, cancelFn := context.WithTimeoutCause(t.Context(), timeout, fmt.Errorf("failed to find Aptos workflow log with non-empty tx hash: %s", expectedLog)) + defer cancelFn() + + cancelCtx, cancelCauseFn := context.WithCancelCause(ctx) + defer cancelCauseFn(nil) + + go func() { + t_helpers.FailOnBaseMessage(cancelCtx, cancelCauseFn, t, lggr, baseMessageCh, t_helpers.WorkflowEngineInitErrorLog) + }() + + mismatchCount := 0 + for { + select { + case <-cancelCtx.Done(): + require.NoError(t, context.Cause(cancelCtx), "failed to observe Aptos log with non-empty tx hash: %s", expectedLog) + return "" + case logs := <-userLogsCh: + for _, line := range logs.LogLines { + if !strings.Contains(line.Message, expectedLog) { + mismatchCount++ + if mismatchCount%20 == 0 { + lggr.Warn(). + Str("expected_log", expectedLog). + Str("found_message", strings.TrimSpace(line.Message)). + Int("mismatch_count", mismatchCount). + Msg("[soft assertion] Received UserLogs messages, but none match expected log yet") + } + continue + } + + matches := aptosTxHashInLogRe.FindStringSubmatch(line.Message) + if len(matches) == 2 { + txHash := normalizeTxHash(matches[1]) + if txHash != "" { + return txHash + } + } + + lggr.Warn(). + Str("message", strings.TrimSpace(line.Message)). + Str("expected_log", expectedLog). + Msg("[soft assertion] Matched Aptos log without non-empty tx hash; waiting for another match") + } + } + } +} + +func assertAptosWriteFailureTxOnChain(t *testing.T, aptosChain blockchains.Blockchain, txHash string) { + t.Helper() + + bc, ok := aptosChain.(*blockchains_aptos.Blockchain) + require.True(t, ok, "expected aptos blockchain type") + + nodeURL := bc.CtfOutput().Nodes[0].ExternalHTTPUrl + require.NotEmpty(t, nodeURL, "Aptos node URL is required for onchain verification") + nodeURL, err := blockchains_aptos.NormalizeNodeURL(nodeURL) + require.NoError(t, err, "failed to normalize Aptos node URL for onchain verification") + + chainID := bc.ChainID() + require.LessOrEqual(t, chainID, uint64(255), "Aptos chain id must fit in uint8") + chainIDUint8, err := blockchains_aptos.ChainIDUint8(chainID) + require.NoError(t, err, "failed to convert Aptos chain id") + + client, err := aptoslib.NewNodeClient(nodeURL, chainIDUint8) + require.NoError(t, err, "failed to create Aptos client") + + tx, err := client.WaitForTransaction(txHash) + require.NoError(t, err, "failed waiting for Aptos tx by hash") + require.False(t, tx.Success, "Aptos tx must fail in expected-failure workflow; vm_status=%s", tx.VmStatus) +} + +func assertAptosWriteTxOnChain(t *testing.T, aptosChain blockchains.Blockchain, txHash string, expectedReceiver string) { + t.Helper() + + bc, ok := aptosChain.(*blockchains_aptos.Blockchain) + require.True(t, ok, "expected aptos blockchain type") + + nodeURL := bc.CtfOutput().Nodes[0].ExternalHTTPUrl + require.NotEmpty(t, nodeURL, "Aptos node URL is required for onchain verification") + nodeURL, err := blockchains_aptos.NormalizeNodeURL(nodeURL) + require.NoError(t, err, "failed to normalize Aptos node URL for onchain verification") + + chainID := bc.ChainID() + require.LessOrEqual(t, chainID, uint64(255), "Aptos chain id must fit in uint8") + chainIDUint8, err := blockchains_aptos.ChainIDUint8(chainID) + require.NoError(t, err, "failed to convert Aptos chain id") + + client, err := aptoslib.NewNodeClient(nodeURL, chainIDUint8) + require.NoError(t, err, "failed to create Aptos client") + + tx, err := client.WaitForTransaction(txHash) + require.NoError(t, err, "failed waiting for Aptos tx by hash") + require.True(t, tx.Success, "Aptos tx must be successful; vm_status=%s", tx.VmStatus) + + expectedReceiverNorm := normalizeTxHashLikeHex(expectedReceiver) + found := false + for _, evt := range tx.Events { + if !strings.HasSuffix(evt.Type, "::forwarder::ReportProcessed") { + continue + } + receiverVal, ok := evt.Data["receiver"].(string) + require.True(t, ok, "ReportProcessed event receiver field must be a string") + if normalizeTxHashLikeHex(receiverVal) != expectedReceiverNorm { + continue + } + _, hasExecutionID := evt.Data["workflow_execution_id"] + _, hasReportID := evt.Data["report_id"] + require.True(t, hasExecutionID, "ReportProcessed must include workflow_execution_id") + require.True(t, hasReportID, "ReportProcessed must include report_id") + found = true + break + } + require.True(t, found, "expected ReportProcessed event for receiver %s in tx %s", expectedReceiverNorm, txHash) +} + +func assertAptosReceiverUpdatedOnChain( + t *testing.T, + aptosChain blockchains.Blockchain, + receiverHex string, + expectedBenchmark uint64, +) { + t.Helper() + + aptosBC, ok := aptosChain.(*blockchains_aptos.Blockchain) + require.True(t, ok, "expected aptos blockchain type") + nodeURL := aptosBC.CtfOutput().Nodes[0].ExternalHTTPUrl + require.NotEmpty(t, nodeURL, "Aptos node URL is required for onchain verification") + nodeURL, err := blockchains_aptos.NormalizeNodeURL(nodeURL) + require.NoError(t, err, "failed to normalize Aptos node URL for onchain verification") + + chainID := aptosBC.ChainID() + require.LessOrEqual(t, chainID, uint64(255), "Aptos chain id must fit in uint8") + chainIDUint8, err := blockchains_aptos.ChainIDUint8(chainID) + require.NoError(t, err, "failed to convert Aptos chain id") + client, err := aptoslib.NewNodeClient(nodeURL, chainIDUint8) + require.NoError(t, err, "failed to create Aptos client") + + var receiverAddr aptoslib.AccountAddress + err = receiverAddr.ParseStringRelaxed(receiverHex) + require.NoError(t, err, "failed to parse Aptos receiver address") + + dataFeeds := aptosdatafeeds.Bind(receiverAddr, client) + feedID := aptosBenchmarkFeedID() + feedIDHex := hex.EncodeToString(feedID) + + require.Eventually(t, func() bool { + feeds, bErr := dataFeeds.Registry().GetFeeds(&aptosbind.CallOpts{}) + if bErr != nil || len(feeds) == 0 { + return false + } + for _, feed := range feeds { + if hex.EncodeToString(feed.FeedId) != feedIDHex { + continue + } + if feed.Feed.Benchmark == nil { + return false + } + return feed.Feed.Benchmark.Uint64() == expectedBenchmark + } + return false + }, 2*time.Minute, 3*time.Second, "expected benchmark value %d not observed onchain for receiver %s", expectedBenchmark, receiverHex) +} + +func normalizeTxHash(input string) string { + s := strings.TrimSpace(strings.ToLower(input)) + if s == "" { + return "" + } + if strings.HasPrefix(s, "0x") { + return s + } + return "0x" + s +} + +func normalizeTxHashLikeHex(input string) string { + s := strings.TrimSpace(strings.ToLower(input)) + s = strings.TrimPrefix(s, "0x") + s = strings.TrimLeft(s, "0") + if s == "" { + return "0x0" + } + return "0x" + s +} + +func normalizeHexValue(input string) string { + return strings.TrimPrefix(strings.ToLower(strings.TrimSpace(input)), "0x") +} + +func deployAptosDataFeedsReceiverForWrite( + t *testing.T, + tenv *configuration.TestEnvironment, + aptosChain blockchains.Blockchain, + primaryForwarderHex string, + feedID []byte, +) string { + t.Helper() + + aptosBC, ok := aptosChain.(*blockchains_aptos.Blockchain) + require.True(t, ok, "expected aptos blockchain type") + nodeURL := aptosBC.CtfOutput().Nodes[0].ExternalHTTPUrl + require.NotEmpty(t, nodeURL, "Aptos node URL is required for receiver deployment") + nodeURL, err := blockchains_aptos.NormalizeNodeURL(nodeURL) + require.NoError(t, err, "failed to normalize Aptos node URL for receiver deployment") + + chainID := aptosBC.ChainID() + require.LessOrEqual(t, chainID, uint64(255), "Aptos chain id must fit in uint8") + chainIDUint8, err := blockchains_aptos.ChainIDUint8(chainID) + require.NoError(t, err, "failed to convert Aptos chain id") + client, err := aptoslib.NewNodeClient(nodeURL, chainIDUint8) + require.NoError(t, err, "failed to create Aptos client") + + deployer, err := aptosDeployerAccount() + require.NoError(t, err, "failed to create Aptos deployer account") + deployerAddress := deployer.AccountAddress() + require.NoError(t, aptosBC.Fund(t.Context(), deployerAddress.StringLong(), aptosWorkerFundingAmountOctas), "failed to fund Aptos deployer account") + + var primaryForwarderAddr aptoslib.AccountAddress + err = primaryForwarderAddr.ParseStringRelaxed(primaryForwarderHex) + require.NoError(t, err, "failed to parse primary forwarder address") + + owner := deployerAddress + secondaryAddress, secondaryTx, _, err := aptosplatformsecondary.DeployToObject(deployer, client, owner) + require.NoError(t, err, "failed to deploy Aptos secondary platform package") + require.NoError(t, blockchains_aptos.WaitForTransactionSuccess(client, secondaryTx.Hash, "platform_secondary deployment")) + + dataFeedsAddress, dataFeedsTx, dataFeeds, err := aptosdatafeeds.DeployToObject( + deployer, + client, + owner, + primaryForwarderAddr, + owner, + secondaryAddress, + ) + require.NoError(t, err, "failed to deploy Aptos data feeds receiver package") + require.NoError(t, blockchains_aptos.WaitForTransactionSuccess(client, dataFeedsTx.Hash, "data_feeds deployment")) + + workflowOwner := workflowRegistryOwnerBytes(t, tenv) + tx, err := dataFeeds.Registry().SetWorkflowConfig( + &aptosbind.TransactOpts{Signer: deployer}, + [][]byte{workflowOwner}, + [][]byte{}, + ) + require.NoError(t, err, "failed to set data feeds workflow config") + require.NoError(t, blockchains_aptos.WaitForTransactionSuccess(client, tx.Hash, "data_feeds set_workflow_config")) + + // Configure the feed that the write workflow will update. + // Without this, registry::perform_update emits WriteSkippedFeedNotSet and benchmark remains unchanged. + tx, err = dataFeeds.Registry().SetFeeds( + &aptosbind.TransactOpts{Signer: deployer}, + [][]byte{feedID}, + []string{"CRE-BENCHMARK"}, + []byte{0x99}, + ) + require.NoError(t, err, "failed to set data feeds feed config") + require.NoError(t, blockchains_aptos.WaitForTransactionSuccess(client, tx.Hash, "data_feeds set_feeds")) + + return dataFeedsAddress.StringLong() +} + +func aptosDeployerAccount() (*aptoslib.Account, error) { + const defaultAptosDeployerKey = "d477c65f88ed9e6d4ec6e2014755c3cfa3e0c44e521d0111a02868c5f04c41d4" + keyHex := strings.TrimSpace(os.Getenv("CRE_APTOS_DEPLOYER_PRIVATE_KEY")) + if keyHex == "" { + keyHex = defaultAptosDeployerKey + } + if keyHex == "" { + return nil, errors.New("empty Aptos deployer key") + } + keyHex = strings.TrimPrefix(keyHex, "0x") + var privateKey aptoscrypto.Ed25519PrivateKey + if err := privateKey.FromHex(keyHex); err != nil { + return nil, fmt.Errorf("parse Aptos deployer private key: %w", err) + } + return aptoslib.NewAccountFromSigner(&privateKey) +} + +func ensureAptosWriteWorkersFunded(t *testing.T, aptosChain blockchains.Blockchain, writeDon *crelib.Don) { + t.Helper() + + aptosBC, ok := aptosChain.(*blockchains_aptos.Blockchain) + require.True(t, ok, "expected aptos blockchain type") + workers, workerErr := writeDon.Workers() + require.NoError(t, workerErr, "failed to list Aptos write DON workers for funding") + require.NotEmpty(t, workers, "Aptos write DON workers list is empty") + + for _, worker := range workers { + require.NotNil(t, worker.Keys, "worker %q is missing metadata keys", worker.Name) + require.NotNil(t, worker.Keys.Aptos, "worker %q is missing metadata Aptos key", worker.Name) + + var account aptoslib.AccountAddress + parseErr := account.ParseStringRelaxed(worker.Keys.Aptos.Account) + require.NoError(t, parseErr, "failed to parse Aptos worker account for worker %q", worker.Name) + + require.NoError(t, aptosBC.Fund(t.Context(), account.StringLong(), aptosWorkerFundingAmountOctas), "failed to fund Aptos worker account %s for worker %q", account.StringLong(), worker.Name) + } +} + +func workflowRegistryOwnerBytes(t *testing.T, tenv *configuration.TestEnvironment) []byte { + t.Helper() + registryChain, ok := tenv.CreEnvironment.Blockchains[0].(*blockchains_evm.Blockchain) + require.True(t, ok, "registry chain must be EVM") + rootOwner := registryChain.SethClient.MustGetRootKeyAddress() + return common.HexToAddress(rootOwner.Hex()).Bytes() +} + +func buildAptosDataFeedsBenchmarkPayloadFor(feedID []byte, benchmark uint64) []byte { + // ABI-like benchmark payload expected by data_feeds::registry::parse_raw_report + // [offset=32][count=1][feed_id(32)][report(64)] + const ( + offsetToArray = uint64(32) + reportCount = uint64(1) + timestamp = uint64(1700000000) + ) + + report := make([]byte, 64) + writeU256BE(report[0:32], timestamp) + writeU256BE(report[32:64], benchmark) + + out := make([]byte, 0, 160) + out = appendU256BE(out, offsetToArray) + out = appendU256BE(out, reportCount) + out = append(out, feedID...) + out = append(out, report...) + return out +} + +func aptosBenchmarkFeedID() []byte { + feedID := make([]byte, 32) + feedID[31] = 1 + return feedID +} + +func aptosRoundtripFeedID() []byte { + feedID := make([]byte, 32) + feedID[31] = 2 + return feedID +} + +func appendU256BE(dst []byte, v uint64) []byte { + buf := make([]byte, 32) + binary.BigEndian.PutUint64(buf[24:], v) + return append(dst, buf...) +} + +func writeU256BE(dst []byte, v uint64) { + binary.BigEndian.PutUint64(dst[24:], v) +} diff --git a/system-tests/tests/test-helpers/before_suite.go b/system-tests/tests/test-helpers/before_suite.go index 8cc18a6562d..eab0ee7e0d7 100644 --- a/system-tests/tests/test-helpers/before_suite.go +++ b/system-tests/tests/test-helpers/before_suite.go @@ -336,18 +336,13 @@ func setConfigurationIfMissing(configName string) error { func createEnvironmentIfNotExists(ctx context.Context, relativePathToRepoRoot, environmentDir string, flags ...string) error { if !envconfig.LocalCREStateFileExists(relativePathToRepoRoot) { - framework.L.Info().Str("CTF_CONFIGS", os.Getenv("CTF_CONFIGS")).Str("local CRE state file", envconfig.MustLocalCREStateFileAbsPath(relativePathToRepoRoot)).Msg("Local CRE state file does not exist, starting environment...") - - args := []string{"run", ".", "env", "start"} - args = append(args, flags...) - - cmd := exec.CommandContext(ctx, "go", args...) - cmd.Dir = environmentDir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmdErr := cmd.Run() - if cmdErr != nil { - return errors.Wrap(cmdErr, "failed to start environment") + framework.L.Info(). + Str("CTF_CONFIGS", os.Getenv("CTF_CONFIGS")). + Str("local CRE state file", envconfig.MustLocalCREStateFileAbsPath(relativePathToRepoRoot)). + Msg("Local CRE state file does not exist, starting environment...") + + if err := startEnvironment(ctx, environmentDir, flags...); err != nil { + return err } } @@ -397,3 +392,17 @@ func setCldfEVMDeployerKey(env *cldf.Environment, chainSelector uint64, deployer env.BlockChains = cldf_chain.NewBlockChainsFromSlice(chainCopies) return nil } + +func startEnvironment(ctx context.Context, environmentDir string, flags ...string) error { + args := []string{"run", ".", "env", "start"} + args = append(args, flags...) + + cmd := exec.CommandContext(ctx, "go", args...) + cmd.Dir = environmentDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return errors.Wrap(err, "failed to start environment") + } + return nil +} diff --git a/system-tests/tests/test-helpers/t_helpers.go b/system-tests/tests/test-helpers/t_helpers.go index d460e252846..830d071fb21 100644 --- a/system-tests/tests/test-helpers/t_helpers.go +++ b/system-tests/tests/test-helpers/t_helpers.go @@ -47,6 +47,8 @@ import ( evmread_negative_config "github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/evm/evmread-negative/config" evmwrite_negative_config "github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/evm/evmwrite-negative/config" logtrigger_negative_config "github.com/smartcontractkit/chainlink/system-tests/tests/regression/cre/evm/logtrigger-negative/config" + aptoswrite_config "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite/config" + aptoswriteroundtrip_config "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/config" evmread_config "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evm/evmread/config" logtrigger_config "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/evm/logtrigger/config" solwrite_config "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/solana/solwrite/config" @@ -288,6 +290,9 @@ type WorkflowConfig interface { None | portypes.WorkflowConfig | porV2types.WorkflowConfig | + AptosReadWorkflowConfig | + aptoswrite_config.Config | + aptoswriteroundtrip_config.Config | crontypes.WorkflowConfig | HTTPWorkflowConfig | consensus_negative_config.Config | @@ -312,6 +317,12 @@ type HTTPWorkflowConfig struct { URL string `json:"url"` } +type AptosReadWorkflowConfig struct { + ChainSelector uint64 `yaml:"chainSelector"` + WorkflowName string `yaml:"workflowName"` + ExpectedCoinName string `yaml:"expectedCoinName"` +} + // WorkflowRegistrationConfig holds configuration for workflow registration type WorkflowRegistrationConfig struct { WorkflowName string @@ -395,6 +406,24 @@ func workflowConfigFactory[T WorkflowConfig](t *testing.T, testLogger zerolog.Lo require.NoError(t, configErr, "failed to create PoR v2 workflow config file") testLogger.Info().Msg("PoR v2 workflow config file created.") + case *AptosReadWorkflowConfig: + workflowCfgFilePath, configErr := CreateWorkflowYamlConfigFile(workflowName, cfg, outputDir) + workflowConfigFilePath = workflowCfgFilePath + require.NoError(t, configErr, "failed to create aptos read workflow config file") + testLogger.Info().Msg("Aptos read workflow config file created.") + + case *aptoswrite_config.Config: + workflowCfgFilePath, configErr := CreateWorkflowYamlConfigFile(workflowName, cfg, outputDir) + workflowConfigFilePath = workflowCfgFilePath + require.NoError(t, configErr, "failed to create aptos write workflow config file") + testLogger.Info().Msg("Aptos write workflow config file created.") + + case *aptoswriteroundtrip_config.Config: + workflowCfgFilePath, configErr := CreateWorkflowYamlConfigFile(workflowName, cfg, outputDir) + workflowConfigFilePath = workflowCfgFilePath + require.NoError(t, configErr, "failed to create aptos write roundtrip workflow config file") + testLogger.Info().Msg("Aptos write roundtrip workflow config file created.") + case *HTTPWorkflowConfig: workflowCfgFilePath, configErr := createHTTPWorkflowConfigFile(workflowName, cfg, outputDir) workflowConfigFilePath = workflowCfgFilePath From d8bdcc0459ceeba02466a19b83b1da393ecd8d51 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Sat, 28 Mar 2026 13:50:55 +0000 Subject: [PATCH 02/35] test: use known aptos chain id in health fixture --- .../smoke/ccip/ccip_reader_test.go | 4 +-- .../scripts/health/multi-chain-loopp.txtar | 26 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/integration-tests/smoke/ccip/ccip_reader_test.go b/integration-tests/smoke/ccip/ccip_reader_test.go index 9b134627b8d..b652786589c 100644 --- a/integration-tests/smoke/ccip/ccip_reader_test.go +++ b/integration-tests/smoke/ccip/ccip_reader_test.go @@ -746,14 +746,14 @@ func TestCCIPReader_Nonces(t *testing.T) { Auth: auth, }) - // Add some nonces. + // Commit each simulated transaction so bind does not reuse a stale pending nonce. for chain, addrs := range nonces { for addr, nonce := range addrs { _, err := s.contract.SetInboundNonce(s.auth, uint64(chain), nonce, common.LeftPadBytes(addr.Bytes(), 32)) require.NoError(t, err) + s.sb.Commit() } } - s.sb.Commit() request := make(map[cciptypes.ChainSelector][]string) for chain, addresses := range nonces { diff --git a/testdata/scripts/health/multi-chain-loopp.txtar b/testdata/scripts/health/multi-chain-loopp.txtar index 04e0dc87c4f..561bcd58a82 100644 --- a/testdata/scripts/health/multi-chain-loopp.txtar +++ b/testdata/scripts/health/multi-chain-loopp.txtar @@ -43,7 +43,7 @@ fj293fbBnlQ!f9vNs HTTPPort = $PORT [[Aptos]] -ChainID = '42' +ChainID = '4' [[Aptos.Nodes]] Name = 'primary' @@ -103,10 +103,10 @@ URL = 'http://tron.org' SolidityURL = 'https://solidity.evm' -- out.txt -- -ok Aptos.42.RelayerService -ok Aptos.42.RelayerService.PluginRelayerClient -ok Aptos.42.RelayerService.PluginRelayerClient.PluginAptos -ok Aptos.42.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter +ok Aptos.4.RelayerService +ok Aptos.4.RelayerService.PluginRelayerClient +ok Aptos.4.RelayerService.PluginRelayerClient.PluginAptos +ok Aptos.4.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter ok BridgeStatusReporter ok CRE ok CRE.DispatcherWrapper @@ -195,36 +195,36 @@ ok WorkflowStore "data": [ { "type": "checks", - "id": "Aptos.42.RelayerService", + "id": "Aptos.4.RelayerService", "attributes": { - "name": "Aptos.42.RelayerService", + "name": "Aptos.4.RelayerService", "status": "passing", "output": "" } }, { "type": "checks", - "id": "Aptos.42.RelayerService.PluginRelayerClient", + "id": "Aptos.4.RelayerService.PluginRelayerClient", "attributes": { - "name": "Aptos.42.RelayerService.PluginRelayerClient", + "name": "Aptos.4.RelayerService.PluginRelayerClient", "status": "passing", "output": "" } }, { "type": "checks", - "id": "Aptos.42.RelayerService.PluginRelayerClient.PluginAptos", + "id": "Aptos.4.RelayerService.PluginRelayerClient.PluginAptos", "attributes": { - "name": "Aptos.42.RelayerService.PluginRelayerClient.PluginAptos", + "name": "Aptos.4.RelayerService.PluginRelayerClient.PluginAptos", "status": "passing", "output": "" } }, { "type": "checks", - "id": "Aptos.42.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter", + "id": "Aptos.4.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter", "attributes": { - "name": "Aptos.42.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter", + "name": "Aptos.4.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter", "status": "passing", "output": "" } From 3a7fa0c7f9d6c50b9d55e059ce2a50bd9bf14bf2 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Sun, 29 Mar 2026 20:03:17 +0100 Subject: [PATCH 03/35] chore: trim aptos branch pin carryover --- core/capabilities/fakes/register.go | 20 ------------- core/capabilities/fakes/register_test.go | 33 -------------------- core/capabilities/fakes/streams_trigger.go | 35 ++++------------------ core/scripts/go.mod | 2 +- core/scripts/go.sum | 2 ++ core/services/chainlink/application.go | 16 ---------- deployment/go.mod | 2 +- deployment/go.sum | 2 ++ go.mod | 2 +- go.sum | 2 ++ integration-tests/go.mod | 2 +- integration-tests/go.sum | 2 ++ integration-tests/load/go.mod | 2 +- integration-tests/load/go.sum | 2 ++ plugins/plugins.private.yaml | 2 +- plugins/plugins.public.yaml | 2 +- system-tests/lib/go.mod | 2 +- system-tests/lib/go.sum | 2 ++ system-tests/tests/go.mod | 2 +- system-tests/tests/go.sum | 2 ++ 20 files changed, 28 insertions(+), 108 deletions(-) delete mode 100644 core/capabilities/fakes/register.go delete mode 100644 core/capabilities/fakes/register_test.go diff --git a/core/capabilities/fakes/register.go b/core/capabilities/fakes/register.go deleted file mode 100644 index 522c0a7bc50..00000000000 --- a/core/capabilities/fakes/register.go +++ /dev/null @@ -1,20 +0,0 @@ -package fakes - -import ( - "context" - "fmt" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/types/core" -) - -const EnableFakeStreamsTriggerEnvVar = "CL_ENABLE_FAKE_STREAMS_TRIGGER" - -func RegisterFakeStreamsTrigger(ctx context.Context, lggr logger.Logger, registry core.CapabilitiesRegistry, nSigners int) (*fakeStreamsTrigger, error) { - trigger := NewFakeStreamsTrigger(lggr, nSigners) - if err := registry.Add(ctx, trigger); err != nil { - return nil, fmt.Errorf("add fake streams trigger: %w", err) - } - - return trigger, nil -} diff --git a/core/capabilities/fakes/register_test.go b/core/capabilities/fakes/register_test.go deleted file mode 100644 index b71429e0977..00000000000 --- a/core/capabilities/fakes/register_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package fakes - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-common/pkg/logger" - corecaps "github.com/smartcontractkit/chainlink/v2/core/capabilities" -) - -func TestRegisterFakeStreamsTrigger(t *testing.T) { - registry := corecaps.NewRegistry(logger.Test(t)) - - trigger, err := RegisterFakeStreamsTrigger(t.Context(), logger.Test(t), registry, 4) - require.NoError(t, err) - require.NotNil(t, trigger) - - capability, err := registry.Get(t.Context(), "streams-trigger@1.0.0") - require.NoError(t, err) - - info, err := capability.Info(t.Context()) - require.NoError(t, err) - require.Equal(t, "streams-trigger@1.0.0", info.ID) -} - -func TestNewFakeStreamsTrigger_UsesDeterministicSigners(t *testing.T) { - triggerA := NewFakeStreamsTrigger(logger.Test(t), 4) - triggerB := NewFakeStreamsTrigger(logger.Test(t), 4) - - require.Equal(t, triggerA.meta.Signers, triggerB.meta.Signers) - require.Equal(t, triggerA.meta.MinRequiredSignatures, triggerB.meta.MinRequiredSignatures) -} diff --git a/core/capabilities/fakes/streams_trigger.go b/core/capabilities/fakes/streams_trigger.go index 278a339cf25..ac2ae56346d 100644 --- a/core/capabilities/fakes/streams_trigger.go +++ b/core/capabilities/fakes/streams_trigger.go @@ -2,7 +2,6 @@ package fakes import ( "context" - "crypto/ecdsa" "encoding/hex" "errors" "fmt" @@ -10,12 +9,11 @@ import ( "sync" "time" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" "github.com/smartcontractkit/libocr/offchainreporting2/chains/evmutil" ocrTypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys" commonCap "github.com/smartcontractkit/chainlink-common/pkg/capabilities" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/datastreams" "github.com/smartcontractkit/chainlink-common/pkg/capabilities/triggers" @@ -25,6 +23,7 @@ import ( v3 "github.com/smartcontractkit/chainlink-common/pkg/types/mercury/v3" "github.com/smartcontractkit/chainlink-evm/pkg/mercury/v3/reportcodec" + "github.com/smartcontractkit/chainlink-common/keystore/corekeys/ocr2key" "github.com/smartcontractkit/chainlink/v2/core/capabilities/streams" ) @@ -33,7 +32,7 @@ type fakeStreamsTrigger struct { eng *services.Engine lggr logger.Logger - signers []fakeStreamsTriggerSigner + signers []ocr2key.KeyBundle codec datastreams.ReportCodec meta datastreams.Metadata @@ -48,10 +47,6 @@ type regState struct { eventCh chan commonCap.TriggerResponse } -type fakeStreamsTriggerSigner struct { - privateKey *ecdsa.PrivateKey -} - var _ services.Service = (*fakeStreamsTrigger)(nil) var _ commonCap.TriggerCapability = (*fakeStreamsTrigger)(nil) @@ -113,16 +108,10 @@ func (st *fakeStreamsTrigger) UnregisterTrigger(ctx context.Context, request com } func NewFakeStreamsTrigger(lggr logger.Logger, nSigners int) *fakeStreamsTrigger { - signers := make([]fakeStreamsTriggerSigner, nSigners) + signers := make([]ocr2key.KeyBundle, nSigners) rawSigners := make([][]byte, nSigners) for i := range nSigners { - keyMaterial := make([]byte, 32) - keyMaterial[31] = byte(i + 1) - privateKey, err := crypto.ToECDSA(keyMaterial) - if err != nil { - panic(err) - } - signers[i] = fakeStreamsTriggerSigner{privateKey: privateKey} + signers[i], _ = ocr2key.New(corekeys.EVM) rawSigners[i] = signers[i].PublicKey() } @@ -236,20 +225,6 @@ func newReport(ctx context.Context, lggr logger.Logger, feedID [32]byte, price i return raw } -func (s fakeStreamsTriggerSigner) PublicKey() ocrTypes.OnchainPublicKey { - address := crypto.PubkeyToAddress(s.privateKey.PublicKey) - return common.CopyBytes(address[:]) -} - -func (s fakeStreamsTriggerSigner) Sign(reportCtx ocrTypes.ReportContext, report ocrTypes.Report) ([]byte, error) { - rawReportContext := evmutil.RawReportContext(reportCtx) - sigData := crypto.Keccak256(report) - sigData = append(sigData, rawReportContext[0][:]...) - sigData = append(sigData, rawReportContext[1][:]...) - sigData = append(sigData, rawReportContext[2][:]...) - return crypto.Sign(crypto.Keccak256(sigData), s.privateKey) -} - func rawReportContext(reportCtx ocrTypes.ReportContext) []byte { rc := evmutil.RawReportContext(reportCtx) flat := []byte{} diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 249792562db..5fcbba33061 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -480,7 +480,7 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20260129135848-c86808ba5cb9 // indirect github.com/smartcontractkit/ccip-owner-contracts v0.1.0 // indirect - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 // indirect + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 // indirect github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260323224438-d819cb3228e1 // indirect github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260317185256-d5f7db87ae70 // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 6b7e62398f0..e3be3a839db 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1616,6 +1616,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index 581584d8087..4803a14faa1 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -7,7 +7,6 @@ import ( "fmt" "math/big" "net/http" - "os" "strconv" "sync" "time" @@ -57,7 +56,6 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/build" "github.com/smartcontractkit/chainlink/v2/core/capabilities" "github.com/smartcontractkit/chainlink/v2/core/capabilities/ccip" - "github.com/smartcontractkit/chainlink/v2/core/capabilities/fakes" "github.com/smartcontractkit/chainlink/v2/core/config" "github.com/smartcontractkit/chainlink/v2/core/logger" "github.com/smartcontractkit/chainlink/v2/core/logger/audit" @@ -243,20 +241,6 @@ func NewApplication(ctx context.Context, opts ApplicationOpts) (Application, err // for tests only, in prod Registry should always be set at this point opts.CapabilitiesRegistry = capabilities.NewRegistry(globalLogger) } - if raw := os.Getenv(fakes.EnableFakeStreamsTriggerEnvVar); raw != "" { - enabled, parseErr := strconv.ParseBool(raw) - if parseErr != nil { - return nil, fmt.Errorf("failed to parse %s: %w", fakes.EnableFakeStreamsTriggerEnvVar, parseErr) - } - if enabled { - trigger, registerErr := fakes.RegisterFakeStreamsTrigger(ctx, globalLogger, opts.CapabilitiesRegistry, 4) - if registerErr != nil { - return nil, fmt.Errorf("failed to register fake streams trigger: %w", registerErr) - } - srvcs = append(srvcs, trigger) - globalLogger.Infow("enabled fake streams trigger", "envVar", fakes.EnableFakeStreamsTriggerEnvVar) - } - } if opts.DonTimeStore == nil { opts.DonTimeStore = dontime.NewStore(dontime.DefaultRequestTimeout) diff --git a/deployment/go.mod b/deployment/go.mod index 520fa258311..1c9d573e7f4 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -38,7 +38,7 @@ require ( github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20260129135848-c86808ba5cb9 github.com/smartcontractkit/ccip-owner-contracts v0.1.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260224214816-cb23ec38649f diff --git a/deployment/go.sum b/deployment/go.sum index 19cdd00d5c5..d9d2f72056e 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -1369,6 +1369,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= diff --git a/go.mod b/go.mod index 206a56f7de0..c5f44b12223 100644 --- a/go.mod +++ b/go.mod @@ -79,7 +79,7 @@ require ( github.com/shirou/gopsutil/v3 v3.24.3 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 github.com/smartcontractkit/chainlink-automation v0.8.1 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260224214816-cb23ec38649f diff --git a/go.sum b/go.sum index 911c076d66f..bb92fb66787 100644 --- a/go.sum +++ b/go.sum @@ -1221,6 +1221,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 488921c4729..1d550a92e54 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -34,7 +34,7 @@ require ( github.com/segmentio/ksuid v1.0.4 github.com/slack-go/slack v0.15.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 diff --git a/integration-tests/go.sum b/integration-tests/go.sum index c6bea586dcd..4715630f8c3 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1356,6 +1356,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index ba751fec833..3f8c8cc6b8e 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -23,7 +23,7 @@ require ( github.com/gagliardetto/solana-go v1.13.0 github.com/rs/zerolog v1.34.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index 4929e15cba7..e39e0aa9ada 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1570,6 +1570,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= diff --git a/plugins/plugins.private.yaml b/plugins/plugins.private.yaml index 99533f2d56b..475b04053b3 100644 --- a/plugins/plugins.private.yaml +++ b/plugins/plugins.private.yaml @@ -48,7 +48,7 @@ plugins: installPath: "." aptos: - moduleURI: "github.com/smartcontractkit/capabilities/chain_capabilities/aptos" - gitRef: "a410cc7b314110c6793e80aa6cb3dcff1c44b881" + gitRef: "ebca0cad82543712816668e5c418e1df49ec0c88" installPath: "." mock: - moduleURI: "github.com/smartcontractkit/capabilities/mock" diff --git a/plugins/plugins.public.yaml b/plugins/plugins.public.yaml index 861179d56b2..85ea380e43b 100644 --- a/plugins/plugins.public.yaml +++ b/plugins/plugins.public.yaml @@ -10,7 +10,7 @@ defaults: plugins: aptos: - moduleURI: "github.com/smartcontractkit/chainlink-aptos" - gitRef: "v0.0.0-20260324144720-484863604698" + gitRef: "v0.0.0-20260318173523-755cafb24200" installPath: "./cmd/chainlink-aptos" sui: diff --git a/system-tests/lib/go.mod b/system-tests/lib/go.mod index 7db360c4f4f..1ce077d0a39 100644 --- a/system-tests/lib/go.mod +++ b/system-tests/lib/go.mod @@ -33,7 +33,7 @@ require ( github.com/scylladb/go-reflectx v1.0.1 github.com/sethvargo/go-retry v0.3.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-common v0.11.2-0.20260326163134-c8e0d77df421 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index 167c2033006..7f946c98cab 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -1583,6 +1583,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod index a2d33fd5083..92f239a4d09 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -593,7 +593,7 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20260129135848-c86808ba5cb9 // indirect github.com/smartcontractkit/ccip-owner-contracts v0.1.0 // indirect - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 github.com/smartcontractkit/chainlink-automation v0.8.1 // indirect github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20251211140724-319861e514c4 diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index ccf60adbdbb..4d1a0254632 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -1767,6 +1767,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= From f917c51dda9884d33466d14c4bcd8a23d10ad3d0 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Sun, 29 Mar 2026 20:46:04 +0100 Subject: [PATCH 04/35] go: drop stale aptos checksum entries --- core/scripts/go.sum | 2 -- deployment/go.sum | 2 -- go.sum | 2 -- integration-tests/go.sum | 2 -- integration-tests/load/go.sum | 2 -- system-tests/lib/go.sum | 2 -- system-tests/tests/go.sum | 2 -- 7 files changed, 14 deletions(-) diff --git a/core/scripts/go.sum b/core/scripts/go.sum index e3be3a839db..a56f952631a 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1618,8 +1618,6 @@ github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0ku github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/deployment/go.sum b/deployment/go.sum index d9d2f72056e..195a9933489 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -1371,8 +1371,6 @@ github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0ku github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/go.sum b/go.sum index bb92fb66787..e0ae18005fa 100644 --- a/go.sum +++ b/go.sum @@ -1223,8 +1223,6 @@ github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0ku github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 4715630f8c3..6e126ef7ed2 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1358,8 +1358,6 @@ github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0ku github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index e39e0aa9ada..eb564e5975f 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1572,8 +1572,6 @@ github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0ku github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index 7f946c98cab..05aaa3292e5 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -1585,8 +1585,6 @@ github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0ku github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index 4d1a0254632..87696ed1917 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -1769,8 +1769,6 @@ github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0ku github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= From ec1e8f044d948ae189f2252576eb559520fb7c37 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 07:30:38 +0100 Subject: [PATCH 05/35] cre: bump aptos ledger version before read smoke --- .../smoke/cre/v2_aptos_capability_test.go | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go index 2a5a614de13..e453e02193d 100644 --- a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go +++ b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go @@ -154,6 +154,8 @@ func ExecuteAptosReadTest( ) { lggr := framework.L + ensureAptosLedgerVersionPositive(t, aptosChain) + // Fixed name so re-runs against the same DON overwrite the same workflow instead of accumulating multiple (e.g. aptos-read-workflow-4838 and aptos-read-workflow-5736). const workflowName = "aptos-read-workflow" workflowConfig := t_helpers.AptosReadWorkflowConfig{ @@ -170,6 +172,39 @@ func ExecuteAptosReadTest( lggr.Info().Str("expected_log", expectedLog).Msg("Aptos read capability test passed") } +// The local Aptos devnet can legitimately remain at ledger version 0 until the +// first Aptos transaction lands, which makes the read capability reject the +// consensus height before the workflow has a chance to execute. +func ensureAptosLedgerVersionPositive(t *testing.T, aptosChain blockchains.Blockchain) { + t.Helper() + + aptosBC, ok := aptosChain.(*blockchains_aptos.Blockchain) + require.True(t, ok, "expected aptos blockchain type") + + client, err := aptosBC.NodeClient() + require.NoError(t, err, "failed to create Aptos node client") + + info, err := client.Info() + require.NoError(t, err, "failed to fetch Aptos node info") + if info.LedgerVersion() > 0 { + return + } + + deployer, err := aptosBC.LocalDeployerAccount() + require.NoError(t, err, "failed to create Aptos local deployer account") + deployerAddress := deployer.AccountAddress() + + require.NoError(t, + aptosBC.Fund(t.Context(), deployerAddress.StringLong(), 1), + "failed to bump Aptos ledger version via faucet funding", + ) + + require.Eventuallyf(t, func() bool { + nodeInfo, nodeErr := client.Info() + return nodeErr == nil && nodeInfo.LedgerVersion() > 0 + }, 30*time.Second, time.Second, "expected Aptos ledger version to become positive after funding") +} + func ExecuteAptosWriteTest( t *testing.T, tenv *configuration.TestEnvironment, From 727d145dc34b1334e40bfb39023118a5c8eff1fd Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 10:05:35 +0100 Subject: [PATCH 06/35] deps: restore aptos relayer ledger version pin --- core/scripts/go.mod | 2 +- core/scripts/go.sum | 4 ++-- deployment/go.mod | 2 +- deployment/go.sum | 4 ++-- go.mod | 2 +- go.sum | 4 ++-- integration-tests/go.mod | 2 +- integration-tests/go.sum | 4 ++-- integration-tests/load/go.mod | 2 +- integration-tests/load/go.sum | 4 ++-- plugins/plugins.public.yaml | 2 +- system-tests/lib/go.mod | 2 +- system-tests/lib/go.sum | 4 ++-- system-tests/tests/go.mod | 2 +- system-tests/tests/go.sum | 4 ++-- 15 files changed, 22 insertions(+), 22 deletions(-) diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 5fcbba33061..249792562db 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -480,7 +480,7 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20260129135848-c86808ba5cb9 // indirect github.com/smartcontractkit/ccip-owner-contracts v0.1.0 // indirect - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 // indirect + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 // indirect github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260323224438-d819cb3228e1 // indirect github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260317185256-d5f7db87ae70 // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index a56f952631a..6b7e62398f0 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1616,8 +1616,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/deployment/go.mod b/deployment/go.mod index 1c9d573e7f4..520fa258311 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -38,7 +38,7 @@ require ( github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20260129135848-c86808ba5cb9 github.com/smartcontractkit/ccip-owner-contracts v0.1.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/evm/deployment v0.0.0-20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260224214816-cb23ec38649f diff --git a/deployment/go.sum b/deployment/go.sum index 195a9933489..19cdd00d5c5 100644 --- a/deployment/go.sum +++ b/deployment/go.sum @@ -1369,8 +1369,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/go.mod b/go.mod index c5f44b12223..206a56f7de0 100644 --- a/go.mod +++ b/go.mod @@ -79,7 +79,7 @@ require ( github.com/shirou/gopsutil/v3 v3.24.3 github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 github.com/smartcontractkit/chainlink-automation v0.8.1 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260224214816-cb23ec38649f diff --git a/go.sum b/go.sum index e0ae18005fa..911c076d66f 100644 --- a/go.sum +++ b/go.sum @@ -1221,8 +1221,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 1d550a92e54..488921c4729 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -34,7 +34,7 @@ require ( github.com/segmentio/ksuid v1.0.4 github.com/slack-go/slack v0.15.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 6e126ef7ed2..c6bea586dcd 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1356,8 +1356,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/integration-tests/load/go.mod b/integration-tests/load/go.mod index 3f8c8cc6b8e..ba751fec833 100644 --- a/integration-tests/load/go.mod +++ b/integration-tests/load/go.mod @@ -23,7 +23,7 @@ require ( github.com/gagliardetto/solana-go v1.13.0 github.com/rs/zerolog v1.34.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260310183131-8d0f0e383288 diff --git a/integration-tests/load/go.sum b/integration-tests/load/go.sum index eb564e5975f..4929e15cba7 100644 --- a/integration-tests/load/go.sum +++ b/integration-tests/load/go.sum @@ -1570,8 +1570,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/plugins/plugins.public.yaml b/plugins/plugins.public.yaml index 85ea380e43b..861179d56b2 100644 --- a/plugins/plugins.public.yaml +++ b/plugins/plugins.public.yaml @@ -10,7 +10,7 @@ defaults: plugins: aptos: - moduleURI: "github.com/smartcontractkit/chainlink-aptos" - gitRef: "v0.0.0-20260318173523-755cafb24200" + gitRef: "v0.0.0-20260324144720-484863604698" installPath: "./cmd/chainlink-aptos" sui: diff --git a/system-tests/lib/go.mod b/system-tests/lib/go.mod index 1ce077d0a39..7db360c4f4f 100644 --- a/system-tests/lib/go.mod +++ b/system-tests/lib/go.mod @@ -33,7 +33,7 @@ require ( github.com/scylladb/go-reflectx v1.0.1 github.com/sethvargo/go-retry v0.3.0 github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260310183131-8d0f0e383288 github.com/smartcontractkit/chainlink-common v0.11.2-0.20260326163134-c8e0d77df421 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index 05aaa3292e5..167c2033006 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -1583,8 +1583,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod index 92f239a4d09..a2d33fd5083 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -593,7 +593,7 @@ require ( github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/smartcontractkit/ccip-contract-examples/chains/evm v0.0.0-20260129135848-c86808ba5cb9 // indirect github.com/smartcontractkit/ccip-owner-contracts v0.1.0 // indirect - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 github.com/smartcontractkit/chainlink-automation v0.8.1 // indirect github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.11-0.20251211140724-319861e514c4 diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index 87696ed1917..ccf60adbdbb 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -1767,8 +1767,8 @@ github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9L github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200 h1:hXteBGuRfdFA5Zj3f07la22ttq6NohB3g5d4vsHFJZ0= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260318173523-755cafb24200/go.mod h1:CLrLo4q6s25t9IGSMn4P1tRkrZFGjRiLOWskwPJEXrc= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698 h1:goYYvVcQ3UV/Fw5R0V1jLJht/VXUxk5F8o+RwgSX9jo= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260324144720-484863604698/go.mod h1:khONNr+qqHzpZBjyTIXJEkcptm3pIOvke03ftAQibRw= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260317185256-d5f7db87ae70 h1:bpzTG/8qwnbnIQPcilnM8lPd/Or4Q22cnakzawds2NQ= From 32864c5f93c7d6a2f79885c27efb5046735d7641 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 10:33:33 +0100 Subject: [PATCH 07/35] test: drop aptos health fixture tweak --- .../scripts/health/multi-chain-loopp.txtar | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/testdata/scripts/health/multi-chain-loopp.txtar b/testdata/scripts/health/multi-chain-loopp.txtar index 561bcd58a82..04e0dc87c4f 100644 --- a/testdata/scripts/health/multi-chain-loopp.txtar +++ b/testdata/scripts/health/multi-chain-loopp.txtar @@ -43,7 +43,7 @@ fj293fbBnlQ!f9vNs HTTPPort = $PORT [[Aptos]] -ChainID = '4' +ChainID = '42' [[Aptos.Nodes]] Name = 'primary' @@ -103,10 +103,10 @@ URL = 'http://tron.org' SolidityURL = 'https://solidity.evm' -- out.txt -- -ok Aptos.4.RelayerService -ok Aptos.4.RelayerService.PluginRelayerClient -ok Aptos.4.RelayerService.PluginRelayerClient.PluginAptos -ok Aptos.4.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter +ok Aptos.42.RelayerService +ok Aptos.42.RelayerService.PluginRelayerClient +ok Aptos.42.RelayerService.PluginRelayerClient.PluginAptos +ok Aptos.42.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter ok BridgeStatusReporter ok CRE ok CRE.DispatcherWrapper @@ -195,36 +195,36 @@ ok WorkflowStore "data": [ { "type": "checks", - "id": "Aptos.4.RelayerService", + "id": "Aptos.42.RelayerService", "attributes": { - "name": "Aptos.4.RelayerService", + "name": "Aptos.42.RelayerService", "status": "passing", "output": "" } }, { "type": "checks", - "id": "Aptos.4.RelayerService.PluginRelayerClient", + "id": "Aptos.42.RelayerService.PluginRelayerClient", "attributes": { - "name": "Aptos.4.RelayerService.PluginRelayerClient", + "name": "Aptos.42.RelayerService.PluginRelayerClient", "status": "passing", "output": "" } }, { "type": "checks", - "id": "Aptos.4.RelayerService.PluginRelayerClient.PluginAptos", + "id": "Aptos.42.RelayerService.PluginRelayerClient.PluginAptos", "attributes": { - "name": "Aptos.4.RelayerService.PluginRelayerClient.PluginAptos", + "name": "Aptos.42.RelayerService.PluginRelayerClient.PluginAptos", "status": "passing", "output": "" } }, { "type": "checks", - "id": "Aptos.4.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter", + "id": "Aptos.42.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter", "attributes": { - "name": "Aptos.4.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter", + "name": "Aptos.42.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter", "status": "passing", "output": "" } From 28c4b2d5a96fd0e70ba0545366e74ebf0313cd70 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 10:57:58 +0100 Subject: [PATCH 08/35] test: keep aptos health fixture on localnet chain id --- .../scripts/health/multi-chain-loopp.txtar | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/testdata/scripts/health/multi-chain-loopp.txtar b/testdata/scripts/health/multi-chain-loopp.txtar index 04e0dc87c4f..561bcd58a82 100644 --- a/testdata/scripts/health/multi-chain-loopp.txtar +++ b/testdata/scripts/health/multi-chain-loopp.txtar @@ -43,7 +43,7 @@ fj293fbBnlQ!f9vNs HTTPPort = $PORT [[Aptos]] -ChainID = '42' +ChainID = '4' [[Aptos.Nodes]] Name = 'primary' @@ -103,10 +103,10 @@ URL = 'http://tron.org' SolidityURL = 'https://solidity.evm' -- out.txt -- -ok Aptos.42.RelayerService -ok Aptos.42.RelayerService.PluginRelayerClient -ok Aptos.42.RelayerService.PluginRelayerClient.PluginAptos -ok Aptos.42.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter +ok Aptos.4.RelayerService +ok Aptos.4.RelayerService.PluginRelayerClient +ok Aptos.4.RelayerService.PluginRelayerClient.PluginAptos +ok Aptos.4.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter ok BridgeStatusReporter ok CRE ok CRE.DispatcherWrapper @@ -195,36 +195,36 @@ ok WorkflowStore "data": [ { "type": "checks", - "id": "Aptos.42.RelayerService", + "id": "Aptos.4.RelayerService", "attributes": { - "name": "Aptos.42.RelayerService", + "name": "Aptos.4.RelayerService", "status": "passing", "output": "" } }, { "type": "checks", - "id": "Aptos.42.RelayerService.PluginRelayerClient", + "id": "Aptos.4.RelayerService.PluginRelayerClient", "attributes": { - "name": "Aptos.42.RelayerService.PluginRelayerClient", + "name": "Aptos.4.RelayerService.PluginRelayerClient", "status": "passing", "output": "" } }, { "type": "checks", - "id": "Aptos.42.RelayerService.PluginRelayerClient.PluginAptos", + "id": "Aptos.4.RelayerService.PluginRelayerClient.PluginAptos", "attributes": { - "name": "Aptos.42.RelayerService.PluginRelayerClient.PluginAptos", + "name": "Aptos.4.RelayerService.PluginRelayerClient.PluginAptos", "status": "passing", "output": "" } }, { "type": "checks", - "id": "Aptos.42.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter", + "id": "Aptos.4.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter", "attributes": { - "name": "Aptos.42.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter", + "name": "Aptos.4.RelayerService.PluginRelayerClient.PluginAptos.PluginRelayerConfigEmitter", "status": "passing", "output": "" } From aebe98f87c2bd67a3ac70079c25b905e4f2bfb07 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 12:18:01 +0100 Subject: [PATCH 09/35] tests: align aptos local and ci coverage --- system-tests/lib/cre/features/aptos/aptos.go | 829 ++---------------- .../lib/cre/features/aptos/aptos_config.go | 206 +++++ .../lib/cre/features/aptos/aptos_forwarder.go | 355 ++++++++ .../lib/cre/features/aptos/aptos_helpers.go | 47 + .../lib/cre/features/aptos/aptos_test.go | 161 ++++ .../lib/cre/features/aptos/aptos_workers.go | 202 +++++ .../tests/smoke/cre/aptos/aptoswrite/main.go | 4 +- .../cre/aptos/aptoswriteroundtrip/main.go | 4 +- .../smoke/cre/v2_aptos_capability_test.go | 89 +- 9 files changed, 1117 insertions(+), 780 deletions(-) create mode 100644 system-tests/lib/cre/features/aptos/aptos_config.go create mode 100644 system-tests/lib/cre/features/aptos/aptos_forwarder.go create mode 100644 system-tests/lib/cre/features/aptos/aptos_helpers.go create mode 100644 system-tests/lib/cre/features/aptos/aptos_workers.go diff --git a/system-tests/lib/cre/features/aptos/aptos.go b/system-tests/lib/cre/features/aptos/aptos.go index fad29a05799..a305520cacb 100644 --- a/system-tests/lib/cre/features/aptos/aptos.go +++ b/system-tests/lib/cre/features/aptos/aptos.go @@ -2,47 +2,25 @@ package aptos import ( "context" - "encoding/hex" - "encoding/json" - stderrors "errors" "fmt" "strconv" "strings" "time" - "dario.cat/mergo" "github.com/Masterminds/semver/v3" - aptossdk "github.com/aptos-labs/aptos-go-sdk" - "github.com/pelletier/go-toml/v2" pkgerrors "github.com/pkg/errors" "github.com/rs/zerolog" - "github.com/sethvargo/go-retry" - "google.golang.org/protobuf/types/known/durationpb" - chainselectors "github.com/smartcontractkit/chain-selectors" - - "github.com/smartcontractkit/chainlink-aptos/bindings/bind" - aptosplatform "github.com/smartcontractkit/chainlink-aptos/bindings/platform" capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" kcr "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/capabilities_registry_1_1_0" - "github.com/smartcontractkit/chainlink-protos/cre/go/values" - "github.com/smartcontractkit/chainlink/deployment/cre/jobs" - crejobops "github.com/smartcontractkit/chainlink/deployment/cre/jobs/operations" - jobtypes "github.com/smartcontractkit/chainlink/deployment/cre/jobs/types" "github.com/smartcontractkit/chainlink/deployment/cre/ocr3" creocr3changeset "github.com/smartcontractkit/chainlink/deployment/cre/ocr3/v2/changeset" creocr3contracts "github.com/smartcontractkit/chainlink/deployment/cre/ocr3/v2/changeset/operations/contracts" - "github.com/smartcontractkit/chainlink/deployment/cre/pkg/offchain" - aptoschangeset "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/aptos" keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" "github.com/smartcontractkit/chainlink/system-tests/lib/cre" crecontracts "github.com/smartcontractkit/chainlink/system-tests/lib/cre/contracts" crejobs "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs" - "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs/standardcapability" creblockchains "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains" - aptoschain "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/aptos" - corechainlink "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" ) const ( @@ -98,22 +76,12 @@ func (a *Aptos) PreEnvStartup( return nil, nil } - forwardersByChainID := make(map[uint64]string, len(enabledChainIDs)) - for _, chainID := range enabledChainIDs { - aptosChain, findErr := findAptosChainByChainID(creEnv.Blockchains, chainID) - if findErr != nil { - return nil, findErr - } - - forwarderAddress, ensureErr := ensureForwarder(ctx, testLogger, creEnv, aptosChain) - if ensureErr != nil { - return nil, ensureErr - } - forwardersByChainID[chainID] = forwarderAddress + forwardersByChainID, err := ensureForwardersForChains(ctx, testLogger, creEnv, enabledChainIDs) + if err != nil { + return nil, err } - - if patchErr := patchNodeTOML(don, forwardersByChainID); patchErr != nil { - return nil, patchErr + if err := patchNodeTOML(don, forwardersByChainID); err != nil { + return nil, err } workers, err := don.Workers() @@ -125,35 +93,9 @@ func (a *Aptos) PreEnvStartup( return nil, fmt.Errorf("failed to collect Aptos worker transmitters for DON %q from metadata: %w", don.Name, err) } - caps := make([]keystone_changeset.DONCapabilityWithConfig, 0, len(enabledChainIDs)) - capabilityToOCR3Config := make(map[string]*ocr3.OracleConfig, len(enabledChainIDs)) - capabilityLabels := make([]string, 0, len(enabledChainIDs)) - for _, chainID := range enabledChainIDs { - aptosChain, err := findAptosChainByChainID(creEnv.Blockchains, chainID) - if err != nil { - return nil, err - } - labelledName := CapabilityLabel(aptosChain.ChainSelector()) - capabilityConfig, err := cre.ResolveCapabilityConfig(don.MustNodeSet(), flag, cre.ChainCapabilityScope(chainID)) - if err != nil { - return nil, fmt.Errorf("could not resolve capability config for '%s' on chain %d: %w", flag, chainID, err) - } - capConfig, err := BuildCapabilityConfig(capabilityConfig.Values, p2pToTransmitterMap, don.HasOnlyLocalCapabilities()) - if err != nil { - return nil, fmt.Errorf("failed to build Aptos capability config for capability %s: %w", labelledName, err) - } - - caps = append(caps, keystone_changeset.DONCapabilityWithConfig{ - Capability: kcr.CapabilitiesRegistryCapability{ - LabelledName: labelledName, - Version: capabilityVersion, - CapabilityType: 1, - }, - Config: capConfig, - UseCapRegOCRConfig: false, - }) - capabilityLabels = append(capabilityLabels, labelledName) - capabilityToOCR3Config[labelledName] = crecontracts.DefaultChainCapabilityOCR3Config() + caps, capabilityToOCR3Config, capabilityLabels, err := buildCapabilityRegistrations(don, creEnv.Blockchains, enabledChainIDs, p2pToTransmitterMap) + if err != nil { + return nil, err } return &cre.PreEnvStartupOutput{ @@ -173,17 +115,9 @@ func (a *Aptos) PostEnvStartup( dons *cre.Dons, creEnv *cre.Environment, ) error { - specs := make(map[string][]string) - - var nodeSet cre.NodeSetWithCapabilityConfigs - for _, ns := range dons.AsNodeSetWithChainCapabilities() { - if ns.GetName() == don.Name { - nodeSet = ns - break - } - } - if nodeSet == nil { - return fmt.Errorf("could not find node set for Don named '%s'", don.Name) + nodeSet, err := nodeSetForDON(dons, don.Name) + if err != nil { + return err } enabledChainIDs, err := nodeSet.GetEnabledChainIDsForCapability(flag) @@ -194,101 +128,22 @@ func (a *Aptos) PostEnvStartup( return nil } - if configureErr := configureForwarders(ctx, testLogger, don, creEnv, enabledChainIDs); configureErr != nil { - return configureErr - } - - bootstrapNode, ok := dons.Bootstrap() - if !ok { - return pkgerrors.New("bootstrap node not found; required for Aptos OCR bootstrap peers") - } - bootstrapPeers := []string{ - fmt.Sprintf("%s@%s:%d", strings.TrimPrefix(bootstrapNode.Keys.PeerID(), "p2p_"), bootstrapNode.Host, cre.OCRPeeringPort), + if err := configureForwarders(ctx, testLogger, don, creEnv, enabledChainIDs); err != nil { + return err } - - if _, _, deployErr := crecontracts.DeployOCR3Contract(testLogger, ocr3ContractQualifier, creEnv.RegistryChainSelector, creEnv.CldfEnvironment, creEnv.ContractVersions); deployErr != nil { - return fmt.Errorf("failed to deploy Aptos OCR3 contract: %w", deployErr) + if _, _, err := crecontracts.DeployOCR3Contract(testLogger, ocr3ContractQualifier, creEnv.RegistryChainSelector, creEnv.CldfEnvironment, creEnv.ContractVersions); err != nil { + return fmt.Errorf("failed to deploy Aptos OCR3 contract: %w", err) } - for _, chainID := range enabledChainIDs { - aptosChain, chainErr := findAptosChainByChainID(creEnv.Blockchains, chainID) - if chainErr != nil { - return chainErr - } - - capabilityConfig, resolveErr := cre.ResolveCapabilityConfig(nodeSet, flag, cre.ChainCapabilityScope(chainID)) - if resolveErr != nil { - return fmt.Errorf("could not resolve capability config for '%s' on chain %d: %w", flag, chainID, resolveErr) - } - command, cErr := standardcapability.GetCommand(capabilityConfig.BinaryName) - if cErr != nil { - return pkgerrors.Wrap(cErr, "failed to get command for Aptos capability") - } - - forwarderAddress := mustForwarderAddress(creEnv.CldfEnvironment.DataStore, aptosChain.ChainSelector()) - workerMetadata, metadataErr := don.Metadata().Workers() - if metadataErr != nil { - return fmt.Errorf("failed to collect Aptos worker metadata for DON %q: %w", don.Name, metadataErr) - } - p2pToTransmitterMap, mapErr := p2pToTransmitterMapForWorkers(workerMetadata) - if mapErr != nil { - return fmt.Errorf("failed to collect Aptos worker transmitters for DON %q: %w", don.Name, mapErr) - } - methodSettings, settingsErr := resolveMethodConfigSettings(capabilityConfig.Values) - if settingsErr != nil { - return fmt.Errorf("failed to resolve Aptos method config settings for chain %d: %w", chainID, settingsErr) - } - configStr, configErr := buildWorkerConfigJSON(chainID, forwarderAddress, methodSettings, p2pToTransmitterMap, true) - if configErr != nil { - return fmt.Errorf("failed to build Aptos worker config: %w", configErr) - } - - workerInput := jobs.ProposeJobSpecInput{ - Domain: offchain.ProductLabel, - Environment: cre.EnvironmentName, - DONName: don.Name, - JobName: "write-aptos-worker-" + strconv.FormatUint(chainID, 10), - ExtraLabels: map[string]string{cre.CapabilityLabelKey: flag}, - DONFilters: []offchain.TargetDONFilter{ - {Key: offchain.FilterKeyDONName, Value: don.Name}, - }, - Template: jobtypes.Aptos, - Inputs: jobtypes.JobSpecInput{ - "command": command, - "config": configStr, - "chainSelectorEVM": creEnv.RegistryChainSelector, - "chainSelectorAptos": aptosChain.ChainSelector(), - "bootstrapPeers": bootstrapPeers, - "useCapRegOCRConfig": false, - "contractQualifier": ocr3ContractQualifier, - }, - } - - proposer := jobs.ProposeJobSpec{} - if verifyErr := proposer.VerifyPreconditions(*creEnv.CldfEnvironment, workerInput); verifyErr != nil { - return fmt.Errorf("precondition verification failed for Aptos worker job: %w", verifyErr) - } - workerReport, applyErr := proposer.Apply(*creEnv.CldfEnvironment, workerInput) - if applyErr != nil { - return fmt.Errorf("failed to propose Aptos worker job spec: %w", applyErr) - } - - for _, report := range workerReport.Reports { - out, ok := report.Output.(crejobops.ProposeStandardCapabilityJobOutput) - if !ok { - return fmt.Errorf("unable to cast to ProposeStandardCapabilityJobOutput, actual type: %T", report.Output) - } - if mergeErr := mergo.Merge(&specs, out.Specs, mergo.WithAppendSlice); mergeErr != nil { - return fmt.Errorf("failed to merge Aptos worker job specs: %w", mergeErr) - } - } + specs, err := proposeAptosWorkerSpecs(ctx, don, dons, creEnv, nodeSet, enabledChainIDs) + if err != nil { + return err } - if len(specs) == 0 { return nil } - if approveErr := crejobs.Approve(ctx, creEnv.CldfEnvironment.Offchain, dons, specs); approveErr != nil { - return fmt.Errorf("failed to approve Aptos jobs: %w", approveErr) + if err := crejobs.Approve(ctx, creEnv.CldfEnvironment.Offchain, dons, specs); err != nil { + return fmt.Errorf("failed to approve Aptos jobs: %w", err) } workers, err := don.Workers() @@ -320,628 +175,66 @@ func (a *Aptos) PostEnvStartup( return nil } -func forwarderAddress(ds datastore.DataStore, chainSelector uint64) (string, bool) { - key := datastore.NewAddressRefKey( - chainSelector, - datastore.ContractType(forwarderContractType), - forwarderContractVersion, - forwarderQualifier, - ) - ref, err := ds.Addresses().Get(key) - if err != nil { - return "", false - } - return ref.Address, true -} - -func mustForwarderAddress(ds datastore.DataStore, chainSelector uint64) string { - addr, ok := forwarderAddress(ds, chainSelector) - if !ok { - panic(fmt.Sprintf("missing Aptos forwarder address for chain selector %d", chainSelector)) - } - return addr -} - -// BuildCapabilityConfig builds the Aptos capability config passed directly -// through the capability manager: method execution policy in MethodConfigs and -// Aptos-specific runtime inputs in SpecConfig. -func BuildCapabilityConfig(values map[string]any, p2pToTransmitterMap map[string]string, localOnly bool) (*capabilitiespb.CapabilityConfig, error) { - methodSettings, err := resolveMethodConfigSettings(values) - if err != nil { - return nil, err - } - - capConfig := &capabilitiespb.CapabilityConfig{ - MethodConfigs: methodConfigs(methodSettings), - LocalOnly: localOnly, - } - if err := setRuntimeSpecConfig(capConfig, methodSettings, p2pToTransmitterMap); err != nil { - return nil, err - } - return capConfig, nil -} - -func buildWorkerConfigJSON(chainID uint64, forwarderAddress string, settings methodConfigSettings, p2pToTransmitterMap map[string]string, isLocal bool) (string, error) { - cfg := map[string]any{ - "chainId": strconv.FormatUint(chainID, 10), - "network": "aptos", - "creForwarderAddress": forwarderAddress, - "isLocal": isLocal, - "deltaStage": settings.DeltaStage, - } - if len(p2pToTransmitterMap) > 0 { - cfg[specConfigP2PMapKey] = p2pToTransmitterMap - } - - raw, err := json.Marshal(cfg) - if err != nil { - return "", fmt.Errorf("failed to marshal Aptos worker config: %w", err) - } - return string(raw), nil -} - -func methodConfigs(settings methodConfigSettings) map[string]*capabilitiespb.CapabilityMethodConfig { - return map[string]*capabilitiespb.CapabilityMethodConfig{ - "View": { - RemoteConfig: &capabilitiespb.CapabilityMethodConfig_RemoteExecutableConfig{ - RemoteExecutableConfig: &capabilitiespb.RemoteExecutableConfig{ - TransmissionSchedule: capabilitiespb.TransmissionSchedule_AllAtOnce, - RequestTimeout: durationpb.New(settings.RequestTimeout), - ServerMaxParallelRequests: 10, - RequestHasherType: capabilitiespb.RequestHasherType_Simple, - }, - }, - }, - "WriteReport": { - RemoteConfig: &capabilitiespb.CapabilityMethodConfig_RemoteExecutableConfig{ - RemoteExecutableConfig: &capabilitiespb.RemoteExecutableConfig{ - TransmissionSchedule: settings.TransmissionSchedule, - DeltaStage: durationpb.New(settings.DeltaStage), - RequestTimeout: durationpb.New(settings.RequestTimeout), - ServerMaxParallelRequests: 10, - RequestHasherType: capabilitiespb.RequestHasherType_WriteReportExcludeSignatures, - }, - }, - }, - } -} - -func resolveMethodConfigSettings(values map[string]any) (methodConfigSettings, error) { - settings := methodConfigSettings{ - RequestTimeout: defaultRequestTimeout, - DeltaStage: defaultWriteDeltaStage, - TransmissionSchedule: capabilitiespb.TransmissionSchedule_AllAtOnce, - } - - if values == nil { - return settings, nil - } - - requestTimeout, ok, err := durationValue(values, requestTimeoutKey) - if err != nil { - return methodConfigSettings{}, err - } - if ok { - settings.RequestTimeout = requestTimeout - } - - deltaStage, ok, err := durationValue(values, deltaStageKey) - if err != nil { - return methodConfigSettings{}, err - } - if ok { - settings.DeltaStage = deltaStage - } - - transmissionSchedule, ok, err := transmissionScheduleValue(values, transmissionScheduleKey) - if err != nil { - return methodConfigSettings{}, err - } - if ok { - settings.TransmissionSchedule = transmissionSchedule - } - - return settings, nil -} - -func transmissionScheduleValue(values map[string]any, key string) (capabilitiespb.TransmissionSchedule, bool, error) { - raw, ok := values[key] - if !ok { - return 0, false, nil - } - - schedule, ok := raw.(string) - if !ok { - return 0, false, fmt.Errorf("%s must be a string, got %T", key, raw) - } - - switch strings.TrimSpace(schedule) { - case "allAtOnce": - return capabilitiespb.TransmissionSchedule_AllAtOnce, true, nil - case "oneAtATime": - return capabilitiespb.TransmissionSchedule_OneAtATime, true, nil - default: - return 0, false, fmt.Errorf("%s must be allAtOnce or oneAtATime, got %q", key, schedule) - } -} - -func durationValue(values map[string]any, key string) (time.Duration, bool, error) { - raw, ok := values[key] - if !ok { - return 0, false, nil - } - - switch v := raw.(type) { - case string: - parsed, err := time.ParseDuration(strings.TrimSpace(v)) - if err != nil { - return 0, false, fmt.Errorf("%s must be a valid duration string: %w", key, err) - } - return parsed, true, nil - case time.Duration: - return v, true, nil - default: - return 0, false, fmt.Errorf("%s must be a duration string, got %T", key, raw) - } -} - -func patchNodeTOML(don *cre.DonMetadata, forwardersByChainID map[uint64]string) error { - for nodeIndex := range don.MustNodeSet().NodeSpecs { - currentConfig := don.MustNodeSet().NodeSpecs[nodeIndex].Node.TestConfigOverrides - if strings.TrimSpace(currentConfig) == "" { - return fmt.Errorf("missing node config for node index %d in DON %q", nodeIndex, don.Name) - } - - var typedConfig corechainlink.Config - if err := toml.Unmarshal([]byte(currentConfig), &typedConfig); err != nil { - return fmt.Errorf("failed to unmarshal config for node index %d: %w", nodeIndex, err) - } - - for chainID, forwarderAddress := range forwardersByChainID { - if err := setForwarderAddress(&typedConfig, strconv.FormatUint(chainID, 10), forwarderAddress); err != nil { - return fmt.Errorf("failed to patch Aptos forwarder address for node index %d: %w", nodeIndex, err) - } - } - - stringifiedConfig, err := toml.Marshal(typedConfig) - if err != nil { - return fmt.Errorf("failed to marshal patched config for node index %d: %w", nodeIndex, err) - } - don.MustNodeSet().NodeSpecs[nodeIndex].Node.TestConfigOverrides = string(stringifiedConfig) - } - - return nil -} - -func setForwarderAddress(cfg *corechainlink.Config, chainID, forwarderAddress string) error { - for i := range cfg.Aptos { - raw := map[string]any(cfg.Aptos[i]) - if fmt.Sprint(raw["ChainID"]) != chainID { - continue - } - - workflow := make(map[string]any) - switch existing := raw["Workflow"].(type) { - case map[string]any: - for k, v := range existing { - workflow[k] = v - } - case corechainlink.RawConfig: - for k, v := range existing { - workflow[k] = v - } - case nil: - default: - return fmt.Errorf("unexpected Aptos workflow config type %T", existing) - } - workflow["ForwarderAddress"] = forwarderAddress - raw["Workflow"] = workflow - cfg.Aptos[i] = corechainlink.RawConfig(raw) - return nil - } - - return fmt.Errorf("Aptos chain %s not found in node config", chainID) -} - -// ensureForwarder makes sure a forwarder exists for the Aptos chain selector and -// returns its address. In local Docker environments it will deploy the forwarder -// once and cache the resulting address in the CRE datastore; in non-Docker -// environments it only reuses an address that has already been injected. -func ensureForwarder( - ctx context.Context, - testLogger zerolog.Logger, - creEnv *cre.Environment, - chain *aptoschain.Blockchain, -) (string, error) { - if addr, ok := forwarderAddress(creEnv.CldfEnvironment.DataStore, chain.ChainSelector()); ok { - return addr, nil - } - if !creEnv.Provider.IsDocker() { - return "", fmt.Errorf("missing Aptos forwarder address for chain selector %d", chain.ChainSelector()) - } - - nodeURL, err := chain.NodeURL() - if err != nil { - return "", fmt.Errorf("invalid Aptos node URL for chain selector %d: %w", chain.ChainSelector(), err) - } - client, err := chain.NodeClient() - if err != nil { - return "", fmt.Errorf("failed to create Aptos client for chain selector %d (%s): %w", chain.ChainSelector(), nodeURL, err) - } - deployerAccount, err := chain.LocalDeployerAccount() - if err != nil { - return "", fmt.Errorf("failed to create Aptos deployer signer: %w", err) - } - deploymentChain, err := chain.LocalDeploymentChain() - if err != nil { - return "", fmt.Errorf("failed to build Aptos deployment chain for chain selector %d: %w", chain.ChainSelector(), err) - } - - owner := deployerAccount.AccountAddress() - if _, accountErr := client.Account(owner); accountErr != nil { - if fundErr := chain.Fund(ctx, owner.StringLong(), 100_000_000); fundErr != nil { - testLogger.Warn(). - Uint64("chainSelector", chain.ChainSelector()). - Str("nodeURL", nodeURL). - Err(fundErr). - Msg("Aptos deployer account not confirmed visible yet; proceeding with deploy retries") - } - } - - var deployedAddress string - var pendingTxHash string - var lastDeployErr error - if retryErr := retry.Do(ctx, retry.WithMaxDuration(3*time.Minute, retry.NewFibonacci(500*time.Millisecond)), func(ctx context.Context) error { - deploymentResp, deployErr := aptoschangeset.DeployPlatform(deploymentChain, owner, nil) - if deployErr != nil { - lastDeployErr = deployErr - if fundErr := chain.Fund(ctx, owner.StringLong(), 1_000_000_000_000); fundErr != nil { - testLogger.Warn(). - Uint64("chainSelector", chain.ChainSelector()). - Err(fundErr). - Msg("failed to re-fund Aptos deployer account during deploy retry") - } - return retry.RetryableError(fmt.Errorf("deploy-to-object failed: %w", deployErr)) - } - if deploymentResp == nil { - lastDeployErr = pkgerrors.New("nil deployment response") - return retry.RetryableError(pkgerrors.New("DeployPlatform returned nil response")) - } - deployedAddress = deploymentResp.Address.StringLong() - pendingTxHash = deploymentResp.Tx - return nil - }); retryErr != nil { - if lastDeployErr != nil { - return "", fmt.Errorf("failed to deploy Aptos platform forwarder for chain selector %d after retries: %w", chain.ChainSelector(), stderrors.Join(lastDeployErr, retryErr)) - } - return "", fmt.Errorf("failed to deploy Aptos platform forwarder for chain selector %d after retries: %w", chain.ChainSelector(), retryErr) - } - - addr, err := normalizeForwarderAddress(deployedAddress) - if err != nil { - return "", fmt.Errorf("invalid Aptos forwarder address parsed from deployment output for chain selector %d: %w", chain.ChainSelector(), err) - } - - if err := addForwarderToDataStore(creEnv, chain.ChainSelector(), addr); err != nil { - return "", err - } - - testLogger.Info(). - Uint64("chainSelector", chain.ChainSelector()). - Str("nodeURL", nodeURL). - Str("txHash", pendingTxHash). - Str("forwarderAddress", addr). - Msg("Aptos platform forwarder deployed") - - return addr, nil -} - -// addForwarderToDataStore seals a new datastore snapshot with the Aptos -// forwarder address so later setup phases can reuse it without redeploying. -func addForwarderToDataStore(creEnv *cre.Environment, chainSelector uint64, address string) error { - memoryDatastore, err := crecontracts.NewDataStoreFromExisting(creEnv.CldfEnvironment.DataStore) - if err != nil { - return fmt.Errorf("failed to create memory datastore: %w", err) - } - - err = memoryDatastore.AddressRefStore.Add(datastore.AddressRef{ - Address: address, - ChainSelector: chainSelector, - Type: datastore.ContractType(forwarderContractType), - Version: forwarderContractVersion, - Qualifier: forwarderQualifier, - }) - if err != nil && !stderrors.Is(err, datastore.ErrAddressRefExists) { - return fmt.Errorf("failed to add Aptos forwarder address to datastore: %w", err) - } - - creEnv.CldfEnvironment.DataStore = memoryDatastore.Seal() - return nil -} - -// configureForwarders writes the final DON membership and signer set to each -// Aptos forwarder after the DON has started and contract DON IDs are known. -func configureForwarders( - ctx context.Context, - testLogger zerolog.Logger, - don *cre.Don, - creEnv *cre.Environment, - chainIDs []uint64, -) error { - workers, err := don.Workers() - if err != nil { - return fmt.Errorf("failed to get worker nodes for DON %q: %w", don.Name, err) - } - f := (len(workers) - 1) / 3 - if f <= 0 { - return fmt.Errorf("invalid Aptos DON %q fault tolerance F=%d (workers=%d)", don.Name, f, len(workers)) - } - if f > 255 { - return fmt.Errorf("aptos DON %q fault tolerance F=%d exceeds u8", don.Name, f) - } - - donIDUint32, err := aptosDonIDUint32(don.ID) - if err != nil { - return fmt.Errorf("invalid DON id for Aptos forwarder config: %w", err) - } - - oracles, err := donOraclePublicKeys(ctx, don) - if err != nil { - return err - } - - for _, chainID := range chainIDs { - aptosChain, err := findAptosChainByChainID(creEnv.Blockchains, chainID) - if err != nil { - return err - } - - nodeURL, err := aptosChain.NodeURL() - if err != nil { - return fmt.Errorf("invalid Aptos node URL for chain selector %d: %w", aptosChain.ChainSelector(), err) - } - client, err := aptosChain.NodeClient() - if err != nil { - return fmt.Errorf("failed to create Aptos client for chain selector %d (%s): %w", aptosChain.ChainSelector(), nodeURL, err) - } - deployerAccount, err := aptosChain.LocalDeployerAccount() - if err != nil { - return fmt.Errorf("failed to create Aptos deployer signer for forwarder config: %w", err) - } - deployerAddress := deployerAccount.AccountAddress() - - if _, accountErr := client.Account(deployerAddress); accountErr != nil { - if fundErr := aptosChain.Fund(ctx, deployerAddress.StringLong(), 100_000_000); fundErr != nil { - testLogger.Warn(). - Uint64("chainSelector", aptosChain.ChainSelector()). - Str("nodeURL", nodeURL). - Err(fundErr). - Msg("Aptos deployer account not confirmed visible yet; proceeding with forwarder set_config retries") - } - } - - forwarderHex := mustForwarderAddress(creEnv.CldfEnvironment.DataStore, aptosChain.ChainSelector()) - var forwarderAddr aptossdk.AccountAddress - if err := forwarderAddr.ParseStringRelaxed(forwarderHex); err != nil { - return fmt.Errorf("invalid Aptos forwarder address for chain selector %d: %w", aptosChain.ChainSelector(), err) - } - forwarderContract := aptosplatform.Bind(forwarderAddr, client).Forwarder() - - var pendingTxHash string - var lastSetConfigErr error - if err := retry.Do(ctx, retry.WithMaxDuration(2*time.Minute, retry.NewFibonacci(500*time.Millisecond)), func(ctx context.Context) error { - pendingTx, err := forwarderContract.SetConfig(&bind.TransactOpts{Signer: deployerAccount}, donIDUint32, forwarderConfigVersion, byte(f), oracles) - if err != nil { - lastSetConfigErr = err - if fundErr := aptosChain.Fund(ctx, deployerAddress.StringLong(), 1_000_000_000_000); fundErr != nil { - testLogger.Warn(). - Uint64("chainSelector", aptosChain.ChainSelector()). - Err(fundErr). - Msg("failed to fund Aptos deployer account during set_config retry") - } - return retry.RetryableError(fmt.Errorf("set_config transaction submit failed: %w", err)) - } - pendingTxHash = pendingTx.Hash - receipt, err := client.WaitForTransaction(pendingTxHash) - if err != nil { - lastSetConfigErr = err - return retry.RetryableError(fmt.Errorf("waiting for set_config transaction failed: %w", err)) - } - if !receipt.Success { - lastSetConfigErr = fmt.Errorf("vm status: %s", receipt.VmStatus) - return retry.RetryableError(fmt.Errorf("set_config transaction failed: %s", receipt.VmStatus)) - } - return nil - }); err != nil { - if lastSetConfigErr != nil { - return fmt.Errorf("failed to configure Aptos forwarder %s for DON %q on chain selector %d: %w", forwarderHex, don.Name, aptosChain.ChainSelector(), stderrors.Join(lastSetConfigErr, err)) - } - return fmt.Errorf("failed to configure Aptos forwarder %s for DON %q on chain selector %d: %w", forwarderHex, don.Name, aptosChain.ChainSelector(), err) - } - - testLogger.Info(). - Str("donName", don.Name). - Uint64("donID", don.ID). - Uint64("chainSelector", aptosChain.ChainSelector()). - Str("txHash", pendingTxHash). - Str("forwarderAddress", forwarderHex). - Msg("configured Aptos forwarder set_config") - } - - return nil -} - -func donOraclePublicKeys(ctx context.Context, don *cre.Don) ([][]byte, error) { - workers, err := don.Workers() - if err != nil { - return nil, fmt.Errorf("failed to list worker nodes for DON %q: %w", don.Name, err) - } - - oracles := make([][]byte, 0, len(workers)) - for _, worker := range workers { - ocr2ID := "" - if worker.Keys != nil && worker.Keys.OCR2BundleIDs != nil { - ocr2ID = worker.Keys.OCR2BundleIDs[chainselectors.FamilyAptos] - } - if ocr2ID == "" { - fetchedID, err := worker.Clients.GQLClient.FetchOCR2KeyBundleID(ctx, strings.ToUpper(chainselectors.FamilyAptos)) - if err != nil { - return nil, fmt.Errorf("missing Aptos OCR2 bundle id for worker %q in DON %q and fallback fetch failed: %w", worker.Name, don.Name, err) - } - if fetchedID == "" { - return nil, fmt.Errorf("missing Aptos OCR2 bundle id for worker %q in DON %q", worker.Name, don.Name) - } - ocr2ID = fetchedID - if worker.Keys != nil { - if worker.Keys.OCR2BundleIDs == nil { - worker.Keys.OCR2BundleIDs = make(map[string]string) - } - worker.Keys.OCR2BundleIDs[chainselectors.FamilyAptos] = ocr2ID - } - } +func buildCapabilityRegistrations( + don *cre.DonMetadata, + blockchains []creblockchains.Blockchain, + enabledChainIDs []uint64, + p2pToTransmitterMap map[string]string, +) ([]keystone_changeset.DONCapabilityWithConfig, map[string]*ocr3.OracleConfig, []string, error) { + caps := make([]keystone_changeset.DONCapabilityWithConfig, 0, len(enabledChainIDs)) + capabilityToOCR3Config := make(map[string]*ocr3.OracleConfig, len(enabledChainIDs)) + capabilityLabels := make([]string, 0, len(enabledChainIDs)) - exported, err := worker.ExportOCR2Keys(ocr2ID) - if err != nil { - return nil, fmt.Errorf("failed to export Aptos OCR2 key for worker %q (bundle %s): %w", worker.Name, ocr2ID, err) - } - pubkey, err := parseOCR2OnchainPublicKey(exported.OnchainPublicKey) + for _, chainID := range enabledChainIDs { + aptosChain, err := findAptosChainByChainID(blockchains, chainID) if err != nil { - return nil, fmt.Errorf("invalid Aptos OCR2 onchain public key for worker %q: %w", worker.Name, err) - } - oracles = append(oracles, pubkey) - } - - return oracles, nil -} - -func p2pToTransmitterMapForWorkers(workers []*cre.NodeMetadata) (map[string]string, error) { - if len(workers) == 0 { - return nil, pkgerrors.New("no DON worker nodes provided") - } - - p2pToTransmitterMap := make(map[string]string) - for _, worker := range workers { - if worker.Keys == nil || worker.Keys.P2PKey == nil { - return nil, fmt.Errorf("missing P2P key for worker index %d", worker.Index) - } - - account := worker.Keys.AptosAccount() - if account == "" { - return nil, fmt.Errorf("missing Aptos account for worker index %d", worker.Index) + return nil, nil, nil, err } - transmitter, err := normalizeTransmitter(account) + labelledName := CapabilityLabel(aptosChain.ChainSelector()) + capabilityConfig, err := cre.ResolveCapabilityConfig(don.MustNodeSet(), flag, cre.ChainCapabilityScope(chainID)) if err != nil { - return nil, fmt.Errorf("invalid Aptos transmitter for worker index %d: %w", worker.Index, err) + return nil, nil, nil, fmt.Errorf("could not resolve capability config for '%s' on chain %d: %w", flag, chainID, err) } - - peerKey := hex.EncodeToString(worker.Keys.P2PKey.PeerID[:]) - p2pToTransmitterMap[peerKey] = transmitter - } - - if len(p2pToTransmitterMap) == 0 { - return nil, pkgerrors.New("no Aptos transmitters found for DON workers") - } - - return p2pToTransmitterMap, nil -} - -func setRuntimeSpecConfig(capConfig *capabilitiespb.CapabilityConfig, settings methodConfigSettings, p2pToTransmitterMap map[string]string) error { - if capConfig == nil { - return pkgerrors.New("capability config is nil") - } - - specConfig, err := values.FromMapValueProto(capConfig.SpecConfig) - if err != nil { - return fmt.Errorf("failed to decode existing spec config: %w", err) - } - if specConfig == nil { - specConfig = values.EmptyMap() - } - - delete(specConfig.Underlying, legacyTransmittersKey) - - scheduleValue, err := values.Wrap(remoteTransmissionScheduleString(settings.TransmissionSchedule)) - if err != nil { - return fmt.Errorf("failed to wrap transmission schedule: %w", err) - } - specConfig.Underlying[specConfigScheduleKey] = scheduleValue - - deltaStageValue, err := values.Wrap(settings.DeltaStage) - if err != nil { - return fmt.Errorf("failed to wrap delta stage: %w", err) - } - specConfig.Underlying[specConfigDeltaStageKey] = deltaStageValue - - if len(p2pToTransmitterMap) > 0 { - mapValue, err := values.Wrap(p2pToTransmitterMap) + capConfig, err := BuildCapabilityConfig(capabilityConfig.Values, p2pToTransmitterMap, don.HasOnlyLocalCapabilities()) if err != nil { - return fmt.Errorf("failed to wrap p2p transmitter map: %w", err) + return nil, nil, nil, fmt.Errorf("failed to build Aptos capability config for capability %s: %w", labelledName, err) } - specConfig.Underlying[specConfigP2PMapKey] = mapValue - } - - capConfig.SpecConfig = values.ProtoMap(specConfig) - return nil -} - -func remoteTransmissionScheduleString(schedule capabilitiespb.TransmissionSchedule) string { - switch schedule { - case capabilitiespb.TransmissionSchedule_OneAtATime: - return "oneAtATime" - default: - return "allAtOnce" - } -} -func normalizeTransmitter(raw string) (string, error) { - s := strings.TrimSpace(raw) - if s == "" { - return "", pkgerrors.New("empty Aptos transmitter") - } - - var addr aptossdk.AccountAddress - if err := addr.ParseStringRelaxed(s); err != nil { - return "", err + caps = append(caps, keystone_changeset.DONCapabilityWithConfig{ + Capability: kcr.CapabilitiesRegistryCapability{ + LabelledName: labelledName, + Version: capabilityVersion, + CapabilityType: 1, + }, + Config: capConfig, + UseCapRegOCRConfig: false, + }) + capabilityLabels = append(capabilityLabels, labelledName) + capabilityToOCR3Config[labelledName] = crecontracts.DefaultChainCapabilityOCR3Config() } - return addr.StringLong(), nil -} -func normalizeForwarderAddress(raw string) (string, error) { - var addr aptossdk.AccountAddress - if err := addr.ParseStringRelaxed(strings.TrimSpace(raw)); err != nil { - return "", err - } - return addr.StringLong(), nil + return caps, capabilityToOCR3Config, capabilityLabels, nil } -func findAptosChainByChainID(chains []creblockchains.Blockchain, chainID uint64) (*aptoschain.Blockchain, error) { - for _, bc := range chains { - if bc.IsFamily(chainselectors.FamilyAptos) && bc.ChainID() == chainID { - aptosBlockchain, ok := bc.(*aptoschain.Blockchain) - if !ok { - return nil, fmt.Errorf("Aptos blockchain for chain id %d has unexpected type %T", chainID, bc) - } - return aptosBlockchain, nil +func nodeSetForDON(dons *cre.Dons, donName string) (cre.NodeSetWithCapabilityConfigs, error) { + for _, ns := range dons.AsNodeSetWithChainCapabilities() { + if ns.GetName() == donName { + return ns, nil } } - return nil, fmt.Errorf("Aptos blockchain for chain id %d not found", chainID) + return nil, fmt.Errorf("could not find node set for Don named '%s'", donName) } -func aptosDonIDUint32(donID uint64) (uint32, error) { - if donID > uint64(^uint32(0)) { - return 0, fmt.Errorf("don id %d exceeds u32", donID) +func bootstrapPeersForDons(dons *cre.Dons) ([]string, error) { + bootstrapNode, ok := dons.Bootstrap() + if !ok { + return nil, pkgerrors.New("bootstrap node not found; required for Aptos OCR bootstrap peers") } - return uint32(donID), nil -} -func parseOCR2OnchainPublicKey(hexValue string) ([]byte, error) { - trimmed := strings.TrimPrefix(strings.TrimSpace(hexValue), "ocr2on_aptos_") - decoded, err := hex.DecodeString(trimmed) - if err != nil { - return nil, err - } - return decoded, nil + return []string{ + fmt.Sprintf("%s@%s:%d", strings.TrimPrefix(bootstrapNode.Keys.PeerID(), "p2p_"), bootstrapNode.Host, cre.OCRPeeringPort), + }, nil } var ( diff --git a/system-tests/lib/cre/features/aptos/aptos_config.go b/system-tests/lib/cre/features/aptos/aptos_config.go new file mode 100644 index 00000000000..09e2bf4d9fd --- /dev/null +++ b/system-tests/lib/cre/features/aptos/aptos_config.go @@ -0,0 +1,206 @@ +package aptos + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + pkgerrors "github.com/pkg/errors" + "google.golang.org/protobuf/types/known/durationpb" + + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + "github.com/smartcontractkit/chainlink-protos/cre/go/values" +) + +// BuildCapabilityConfig builds the Aptos capability config passed directly +// through the capability manager: method execution policy in MethodConfigs and +// Aptos-specific runtime inputs in SpecConfig. +func BuildCapabilityConfig(values map[string]any, p2pToTransmitterMap map[string]string, localOnly bool) (*capabilitiespb.CapabilityConfig, error) { + methodSettings, err := resolveMethodConfigSettings(values) + if err != nil { + return nil, err + } + + capConfig := &capabilitiespb.CapabilityConfig{ + MethodConfigs: methodConfigs(methodSettings), + LocalOnly: localOnly, + } + if err := setRuntimeSpecConfig(capConfig, methodSettings, p2pToTransmitterMap); err != nil { + return nil, err + } + return capConfig, nil +} + +func buildWorkerConfigJSON(chainID uint64, forwarderAddress string, settings methodConfigSettings, p2pToTransmitterMap map[string]string, isLocal bool) (string, error) { + cfg := map[string]any{ + "chainId": strconv.FormatUint(chainID, 10), + "network": "aptos", + "creForwarderAddress": forwarderAddress, + "isLocal": isLocal, + "deltaStage": settings.DeltaStage, + } + if len(p2pToTransmitterMap) > 0 { + cfg[specConfigP2PMapKey] = p2pToTransmitterMap + } + + raw, err := json.Marshal(cfg) + if err != nil { + return "", fmt.Errorf("failed to marshal Aptos worker config: %w", err) + } + return string(raw), nil +} + +func methodConfigs(settings methodConfigSettings) map[string]*capabilitiespb.CapabilityMethodConfig { + return map[string]*capabilitiespb.CapabilityMethodConfig{ + "View": { + RemoteConfig: &capabilitiespb.CapabilityMethodConfig_RemoteExecutableConfig{ + RemoteExecutableConfig: &capabilitiespb.RemoteExecutableConfig{ + TransmissionSchedule: capabilitiespb.TransmissionSchedule_AllAtOnce, + RequestTimeout: durationpb.New(settings.RequestTimeout), + ServerMaxParallelRequests: 10, + RequestHasherType: capabilitiespb.RequestHasherType_Simple, + }, + }, + }, + "WriteReport": { + RemoteConfig: &capabilitiespb.CapabilityMethodConfig_RemoteExecutableConfig{ + RemoteExecutableConfig: &capabilitiespb.RemoteExecutableConfig{ + TransmissionSchedule: settings.TransmissionSchedule, + DeltaStage: durationpb.New(settings.DeltaStage), + RequestTimeout: durationpb.New(settings.RequestTimeout), + ServerMaxParallelRequests: 10, + RequestHasherType: capabilitiespb.RequestHasherType_WriteReportExcludeSignatures, + }, + }, + }, + } +} + +func resolveMethodConfigSettings(values map[string]any) (methodConfigSettings, error) { + settings := methodConfigSettings{ + RequestTimeout: defaultRequestTimeout, + DeltaStage: defaultWriteDeltaStage, + TransmissionSchedule: capabilitiespb.TransmissionSchedule_AllAtOnce, + } + + if values == nil { + return settings, nil + } + + requestTimeout, ok, err := durationValue(values, requestTimeoutKey) + if err != nil { + return methodConfigSettings{}, err + } + if ok { + settings.RequestTimeout = requestTimeout + } + + deltaStage, ok, err := durationValue(values, deltaStageKey) + if err != nil { + return methodConfigSettings{}, err + } + if ok { + settings.DeltaStage = deltaStage + } + + transmissionSchedule, ok, err := transmissionScheduleValue(values, transmissionScheduleKey) + if err != nil { + return methodConfigSettings{}, err + } + if ok { + settings.TransmissionSchedule = transmissionSchedule + } + + return settings, nil +} + +func transmissionScheduleValue(values map[string]any, key string) (capabilitiespb.TransmissionSchedule, bool, error) { + raw, ok := values[key] + if !ok { + return 0, false, nil + } + + schedule, ok := raw.(string) + if !ok { + return 0, false, fmt.Errorf("%s must be a string, got %T", key, raw) + } + + switch strings.TrimSpace(schedule) { + case "allAtOnce": + return capabilitiespb.TransmissionSchedule_AllAtOnce, true, nil + case "oneAtATime": + return capabilitiespb.TransmissionSchedule_OneAtATime, true, nil + default: + return 0, false, fmt.Errorf("%s must be allAtOnce or oneAtATime, got %q", key, schedule) + } +} + +func durationValue(values map[string]any, key string) (time.Duration, bool, error) { + raw, ok := values[key] + if !ok { + return 0, false, nil + } + + switch v := raw.(type) { + case string: + parsed, err := time.ParseDuration(strings.TrimSpace(v)) + if err != nil { + return 0, false, fmt.Errorf("%s must be a valid duration string: %w", key, err) + } + return parsed, true, nil + case time.Duration: + return v, true, nil + default: + return 0, false, fmt.Errorf("%s must be a duration string, got %T", key, raw) + } +} + +func setRuntimeSpecConfig(capConfig *capabilitiespb.CapabilityConfig, settings methodConfigSettings, p2pToTransmitterMap map[string]string) error { + if capConfig == nil { + return pkgerrors.New("capability config is nil") + } + + specConfig, err := values.FromMapValueProto(capConfig.SpecConfig) + if err != nil { + return fmt.Errorf("failed to decode existing spec config: %w", err) + } + if specConfig == nil { + specConfig = values.EmptyMap() + } + + delete(specConfig.Underlying, legacyTransmittersKey) + + scheduleValue, err := values.Wrap(remoteTransmissionScheduleString(settings.TransmissionSchedule)) + if err != nil { + return fmt.Errorf("failed to wrap transmission schedule: %w", err) + } + specConfig.Underlying[specConfigScheduleKey] = scheduleValue + + deltaStageValue, err := values.Wrap(settings.DeltaStage) + if err != nil { + return fmt.Errorf("failed to wrap delta stage: %w", err) + } + specConfig.Underlying[specConfigDeltaStageKey] = deltaStageValue + + if len(p2pToTransmitterMap) > 0 { + mapValue, err := values.Wrap(p2pToTransmitterMap) + if err != nil { + return fmt.Errorf("failed to wrap p2p transmitter map: %w", err) + } + specConfig.Underlying[specConfigP2PMapKey] = mapValue + } + + capConfig.SpecConfig = values.ProtoMap(specConfig) + return nil +} + +func remoteTransmissionScheduleString(schedule capabilitiespb.TransmissionSchedule) string { + switch schedule { + case capabilitiespb.TransmissionSchedule_OneAtATime: + return "oneAtATime" + default: + return "allAtOnce" + } +} diff --git a/system-tests/lib/cre/features/aptos/aptos_forwarder.go b/system-tests/lib/cre/features/aptos/aptos_forwarder.go new file mode 100644 index 00000000000..0b0556867b3 --- /dev/null +++ b/system-tests/lib/cre/features/aptos/aptos_forwarder.go @@ -0,0 +1,355 @@ +package aptos + +import ( + "context" + stderrors "errors" + "fmt" + "strconv" + "strings" + "time" + + aptossdk "github.com/aptos-labs/aptos-go-sdk" + "github.com/pelletier/go-toml/v2" + pkgerrors "github.com/pkg/errors" + "github.com/rs/zerolog" + "github.com/sethvargo/go-retry" + + "github.com/smartcontractkit/chainlink-aptos/bindings/bind" + aptosplatform "github.com/smartcontractkit/chainlink-aptos/bindings/platform" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + aptoschangeset "github.com/smartcontractkit/chainlink/deployment/data-feeds/changeset/aptos" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre" + crecontracts "github.com/smartcontractkit/chainlink/system-tests/lib/cre/contracts" + aptoschain "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/aptos" + corechainlink "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" +) + +func forwarderAddress(ds datastore.DataStore, chainSelector uint64) (string, bool) { + key := datastore.NewAddressRefKey( + chainSelector, + datastore.ContractType(forwarderContractType), + forwarderContractVersion, + forwarderQualifier, + ) + ref, err := ds.Addresses().Get(key) + if err != nil { + return "", false + } + return ref.Address, true +} + +func mustForwarderAddress(ds datastore.DataStore, chainSelector uint64) string { + addr, ok := forwarderAddress(ds, chainSelector) + if !ok { + panic(fmt.Sprintf("missing Aptos forwarder address for chain selector %d", chainSelector)) + } + return addr +} + +func ensureForwardersForChains( + ctx context.Context, + testLogger zerolog.Logger, + creEnv *cre.Environment, + chainIDs []uint64, +) (map[uint64]string, error) { + forwardersByChainID := make(map[uint64]string, len(chainIDs)) + for _, chainID := range chainIDs { + aptosChain, err := findAptosChainByChainID(creEnv.Blockchains, chainID) + if err != nil { + return nil, err + } + + forwarderAddress, err := ensureForwarder(ctx, testLogger, creEnv, aptosChain) + if err != nil { + return nil, err + } + forwardersByChainID[chainID] = forwarderAddress + } + return forwardersByChainID, nil +} + +func patchNodeTOML(don *cre.DonMetadata, forwardersByChainID map[uint64]string) error { + for nodeIndex := range don.MustNodeSet().NodeSpecs { + currentConfig := don.MustNodeSet().NodeSpecs[nodeIndex].Node.TestConfigOverrides + if strings.TrimSpace(currentConfig) == "" { + return fmt.Errorf("missing node config for node index %d in DON %q", nodeIndex, don.Name) + } + + var typedConfig corechainlink.Config + if err := toml.Unmarshal([]byte(currentConfig), &typedConfig); err != nil { + return fmt.Errorf("failed to unmarshal config for node index %d: %w", nodeIndex, err) + } + + for chainID, forwarderAddress := range forwardersByChainID { + if err := setForwarderAddress(&typedConfig, strconv.FormatUint(chainID, 10), forwarderAddress); err != nil { + return fmt.Errorf("failed to patch Aptos forwarder address for node index %d: %w", nodeIndex, err) + } + } + + stringifiedConfig, err := toml.Marshal(typedConfig) + if err != nil { + return fmt.Errorf("failed to marshal patched config for node index %d: %w", nodeIndex, err) + } + don.MustNodeSet().NodeSpecs[nodeIndex].Node.TestConfigOverrides = string(stringifiedConfig) + } + + return nil +} + +func setForwarderAddress(cfg *corechainlink.Config, chainID, forwarderAddress string) error { + for i := range cfg.Aptos { + raw := map[string]any(cfg.Aptos[i]) + if fmt.Sprint(raw["ChainID"]) != chainID { + continue + } + + workflow := make(map[string]any) + switch existing := raw["Workflow"].(type) { + case map[string]any: + for k, v := range existing { + workflow[k] = v + } + case corechainlink.RawConfig: + for k, v := range existing { + workflow[k] = v + } + case nil: + default: + return fmt.Errorf("unexpected Aptos workflow config type %T", existing) + } + workflow["ForwarderAddress"] = forwarderAddress + raw["Workflow"] = workflow + cfg.Aptos[i] = corechainlink.RawConfig(raw) + return nil + } + + return fmt.Errorf("Aptos chain %s not found in node config", chainID) +} + +// ensureForwarder makes sure a forwarder exists for the Aptos chain selector and +// returns its address. In local Docker environments it will deploy the forwarder +// once and cache the resulting address in the CRE datastore; in non-Docker +// environments it only reuses an address that has already been injected. +func ensureForwarder( + ctx context.Context, + testLogger zerolog.Logger, + creEnv *cre.Environment, + chain *aptoschain.Blockchain, +) (string, error) { + if addr, ok := forwarderAddress(creEnv.CldfEnvironment.DataStore, chain.ChainSelector()); ok { + return addr, nil + } + if !creEnv.Provider.IsDocker() { + return "", fmt.Errorf("missing Aptos forwarder address for chain selector %d", chain.ChainSelector()) + } + + nodeURL, err := chain.NodeURL() + if err != nil { + return "", fmt.Errorf("invalid Aptos node URL for chain selector %d: %w", chain.ChainSelector(), err) + } + client, err := chain.NodeClient() + if err != nil { + return "", fmt.Errorf("failed to create Aptos client for chain selector %d (%s): %w", chain.ChainSelector(), nodeURL, err) + } + deployerAccount, err := chain.LocalDeployerAccount() + if err != nil { + return "", fmt.Errorf("failed to create Aptos deployer signer: %w", err) + } + deploymentChain, err := chain.LocalDeploymentChain() + if err != nil { + return "", fmt.Errorf("failed to build Aptos deployment chain for chain selector %d: %w", chain.ChainSelector(), err) + } + + owner := deployerAccount.AccountAddress() + if _, accountErr := client.Account(owner); accountErr != nil { + if fundErr := chain.Fund(ctx, owner.StringLong(), 100_000_000); fundErr != nil { + testLogger.Warn(). + Uint64("chainSelector", chain.ChainSelector()). + Str("nodeURL", nodeURL). + Err(fundErr). + Msg("Aptos deployer account not confirmed visible yet; proceeding with deploy retries") + } + } + + var deployedAddress string + var pendingTxHash string + var lastDeployErr error + if retryErr := retry.Do(ctx, retry.WithMaxDuration(3*time.Minute, retry.NewFibonacci(500*time.Millisecond)), func(ctx context.Context) error { + deploymentResp, deployErr := aptoschangeset.DeployPlatform(deploymentChain, owner, nil) + if deployErr != nil { + lastDeployErr = deployErr + if fundErr := chain.Fund(ctx, owner.StringLong(), 1_000_000_000_000); fundErr != nil { + testLogger.Warn(). + Uint64("chainSelector", chain.ChainSelector()). + Err(fundErr). + Msg("failed to re-fund Aptos deployer account during deploy retry") + } + return retry.RetryableError(fmt.Errorf("deploy-to-object failed: %w", deployErr)) + } + if deploymentResp == nil { + lastDeployErr = pkgerrors.New("nil deployment response") + return retry.RetryableError(pkgerrors.New("DeployPlatform returned nil response")) + } + deployedAddress = deploymentResp.Address.StringLong() + pendingTxHash = deploymentResp.Tx + return nil + }); retryErr != nil { + if lastDeployErr != nil { + return "", fmt.Errorf("failed to deploy Aptos platform forwarder for chain selector %d after retries: %w", chain.ChainSelector(), stderrors.Join(lastDeployErr, retryErr)) + } + return "", fmt.Errorf("failed to deploy Aptos platform forwarder for chain selector %d after retries: %w", chain.ChainSelector(), retryErr) + } + + addr, err := normalizeForwarderAddress(deployedAddress) + if err != nil { + return "", fmt.Errorf("invalid Aptos forwarder address parsed from deployment output for chain selector %d: %w", chain.ChainSelector(), err) + } + + if err := addForwarderToDataStore(creEnv, chain.ChainSelector(), addr); err != nil { + return "", err + } + + testLogger.Info(). + Uint64("chainSelector", chain.ChainSelector()). + Str("nodeURL", nodeURL). + Str("txHash", pendingTxHash). + Str("forwarderAddress", addr). + Msg("Aptos platform forwarder deployed") + + return addr, nil +} + +// addForwarderToDataStore seals a new datastore snapshot with the Aptos +// forwarder address so later setup phases can reuse it without redeploying. +func addForwarderToDataStore(creEnv *cre.Environment, chainSelector uint64, address string) error { + memoryDatastore, err := crecontracts.NewDataStoreFromExisting(creEnv.CldfEnvironment.DataStore) + if err != nil { + return fmt.Errorf("failed to create memory datastore: %w", err) + } + + err = memoryDatastore.AddressRefStore.Add(datastore.AddressRef{ + Address: address, + ChainSelector: chainSelector, + Type: datastore.ContractType(forwarderContractType), + Version: forwarderContractVersion, + Qualifier: forwarderQualifier, + }) + if err != nil && !stderrors.Is(err, datastore.ErrAddressRefExists) { + return fmt.Errorf("failed to add Aptos forwarder address to datastore: %w", err) + } + + creEnv.CldfEnvironment.DataStore = memoryDatastore.Seal() + return nil +} + +// configureForwarders writes the final DON membership and signer set to each +// Aptos forwarder after the DON has started and contract DON IDs are known. +func configureForwarders( + ctx context.Context, + testLogger zerolog.Logger, + don *cre.Don, + creEnv *cre.Environment, + chainIDs []uint64, +) error { + workers, err := don.Workers() + if err != nil { + return fmt.Errorf("failed to get worker nodes for DON %q: %w", don.Name, err) + } + f := (len(workers) - 1) / 3 + if f <= 0 { + return fmt.Errorf("invalid Aptos DON %q fault tolerance F=%d (workers=%d)", don.Name, f, len(workers)) + } + if f > 255 { + return fmt.Errorf("aptos DON %q fault tolerance F=%d exceeds u8", don.Name, f) + } + + donIDUint32, err := aptosDonIDUint32(don.ID) + if err != nil { + return fmt.Errorf("invalid DON id for Aptos forwarder config: %w", err) + } + + oracles, err := donOraclePublicKeys(ctx, don) + if err != nil { + return err + } + + for _, chainID := range chainIDs { + aptosChain, err := findAptosChainByChainID(creEnv.Blockchains, chainID) + if err != nil { + return err + } + + nodeURL, err := aptosChain.NodeURL() + if err != nil { + return fmt.Errorf("invalid Aptos node URL for chain selector %d: %w", aptosChain.ChainSelector(), err) + } + client, err := aptosChain.NodeClient() + if err != nil { + return fmt.Errorf("failed to create Aptos client for chain selector %d (%s): %w", aptosChain.ChainSelector(), nodeURL, err) + } + deployerAccount, err := aptosChain.LocalDeployerAccount() + if err != nil { + return fmt.Errorf("failed to create Aptos deployer signer for forwarder config: %w", err) + } + deployerAddress := deployerAccount.AccountAddress() + + if _, accountErr := client.Account(deployerAddress); accountErr != nil { + if fundErr := aptosChain.Fund(ctx, deployerAddress.StringLong(), 100_000_000); fundErr != nil { + testLogger.Warn(). + Uint64("chainSelector", aptosChain.ChainSelector()). + Str("nodeURL", nodeURL). + Err(fundErr). + Msg("Aptos deployer account not confirmed visible yet; proceeding with forwarder set_config retries") + } + } + + forwarderHex := mustForwarderAddress(creEnv.CldfEnvironment.DataStore, aptosChain.ChainSelector()) + var forwarderAddr aptossdk.AccountAddress + if err := forwarderAddr.ParseStringRelaxed(forwarderHex); err != nil { + return fmt.Errorf("invalid Aptos forwarder address for chain selector %d: %w", aptosChain.ChainSelector(), err) + } + forwarderContract := aptosplatform.Bind(forwarderAddr, client).Forwarder() + + var pendingTxHash string + var lastSetConfigErr error + if err := retry.Do(ctx, retry.WithMaxDuration(2*time.Minute, retry.NewFibonacci(500*time.Millisecond)), func(ctx context.Context) error { + pendingTx, err := forwarderContract.SetConfig(&bind.TransactOpts{Signer: deployerAccount}, donIDUint32, forwarderConfigVersion, byte(f), oracles) + if err != nil { + lastSetConfigErr = err + if fundErr := aptosChain.Fund(ctx, deployerAddress.StringLong(), 1_000_000_000_000); fundErr != nil { + testLogger.Warn(). + Uint64("chainSelector", aptosChain.ChainSelector()). + Err(fundErr). + Msg("failed to fund Aptos deployer account during set_config retry") + } + return retry.RetryableError(fmt.Errorf("set_config transaction submit failed: %w", err)) + } + pendingTxHash = pendingTx.Hash + receipt, err := client.WaitForTransaction(pendingTxHash) + if err != nil { + lastSetConfigErr = err + return retry.RetryableError(fmt.Errorf("waiting for set_config transaction failed: %w", err)) + } + if !receipt.Success { + lastSetConfigErr = fmt.Errorf("vm status: %s", receipt.VmStatus) + return retry.RetryableError(fmt.Errorf("set_config transaction failed: %s", receipt.VmStatus)) + } + return nil + }); err != nil { + if lastSetConfigErr != nil { + return fmt.Errorf("failed to configure Aptos forwarder %s for DON %q on chain selector %d: %w", forwarderHex, don.Name, aptosChain.ChainSelector(), stderrors.Join(lastSetConfigErr, err)) + } + return fmt.Errorf("failed to configure Aptos forwarder %s for DON %q on chain selector %d: %w", forwarderHex, don.Name, aptosChain.ChainSelector(), err) + } + + testLogger.Info(). + Str("donName", don.Name). + Uint64("donID", don.ID). + Uint64("chainSelector", aptosChain.ChainSelector()). + Str("txHash", pendingTxHash). + Str("forwarderAddress", forwarderHex). + Msg("configured Aptos forwarder set_config") + } + + return nil +} diff --git a/system-tests/lib/cre/features/aptos/aptos_helpers.go b/system-tests/lib/cre/features/aptos/aptos_helpers.go new file mode 100644 index 00000000000..d88b0b2be40 --- /dev/null +++ b/system-tests/lib/cre/features/aptos/aptos_helpers.go @@ -0,0 +1,47 @@ +package aptos + +import ( + "fmt" + "strings" + + aptossdk "github.com/aptos-labs/aptos-go-sdk" + pkgerrors "github.com/pkg/errors" + + chainselectors "github.com/smartcontractkit/chain-selectors" + creblockchains "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains" + aptoschain "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/aptos" +) + +func normalizeTransmitter(raw string) (string, error) { + s := strings.TrimSpace(raw) + if s == "" { + return "", pkgerrors.New("empty Aptos transmitter") + } + + var addr aptossdk.AccountAddress + if err := addr.ParseStringRelaxed(s); err != nil { + return "", err + } + return addr.StringLong(), nil +} + +func normalizeForwarderAddress(raw string) (string, error) { + var addr aptossdk.AccountAddress + if err := addr.ParseStringRelaxed(strings.TrimSpace(raw)); err != nil { + return "", err + } + return addr.StringLong(), nil +} + +func findAptosChainByChainID(chains []creblockchains.Blockchain, chainID uint64) (*aptoschain.Blockchain, error) { + for _, bc := range chains { + if bc.IsFamily(chainselectors.FamilyAptos) && bc.ChainID() == chainID { + aptosBlockchain, ok := bc.(*aptoschain.Blockchain) + if !ok { + return nil, fmt.Errorf("Aptos blockchain for chain id %d has unexpected type %T", chainID, bc) + } + return aptosBlockchain, nil + } + } + return nil, fmt.Errorf("Aptos blockchain for chain id %d not found", chainID) +} diff --git a/system-tests/lib/cre/features/aptos/aptos_test.go b/system-tests/lib/cre/features/aptos/aptos_test.go index 0b00cb9eb08..849fe1fb383 100644 --- a/system-tests/lib/cre/features/aptos/aptos_test.go +++ b/system-tests/lib/cre/features/aptos/aptos_test.go @@ -1,20 +1,32 @@ package aptos import ( + "context" "encoding/hex" "encoding/json" "math/big" + "reflect" "testing" "time" + "unsafe" + "github.com/pelletier/go-toml/v2" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/keystore/corekeys/p2pkey" capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" + cldfchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" "github.com/smartcontractkit/chainlink-protos/cre/go/values" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/clnode" + ns "github.com/smartcontractkit/chainlink-testing-framework/framework/components/simple_node_set" "github.com/smartcontractkit/chainlink/system-tests/lib/cre" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/secrets" + creblockchains "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains" + aptoschain "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/aptos" "github.com/smartcontractkit/chainlink/system-tests/lib/crypto" + "github.com/smartcontractkit/chainlink/system-tests/lib/infra" + corechainlink "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" ) func TestSetRuntimeSpecConfig_ReplacesLegacyKey(t *testing.T) { @@ -232,3 +244,152 @@ func TestResolveMethodConfigSettings_InvalidTransmissionSchedule(t *testing.T) { }) require.Error(t, err) } + +func TestSetForwarderAddress_UpdatesMatchingAptosChain(t *testing.T) { + cfg := corechainlink.Config{ + Aptos: corechainlink.RawConfigs{ + corechainlink.RawConfig{ + "ChainID": "4", + "Workflow": corechainlink.RawConfig{ + "ForwarderAddress": "0xold", + "Keep": "yes", + }, + }, + corechainlink.RawConfig{ + "ChainID": "8", + }, + }, + } + + err := setForwarderAddress(&cfg, "4", "0xnew") + require.NoError(t, err) + + workflow := workflowMap(t, cfg.Aptos[0]["Workflow"]) + require.Equal(t, "0xnew", workflow["ForwarderAddress"]) + require.Equal(t, "yes", workflow["Keep"]) + require.Nil(t, cfg.Aptos[1]["Workflow"]) +} + +func TestPatchNodeTOML_PatchesAllMatchingAptosChainsAndPreservesWorkflowFields(t *testing.T) { + baseConfig := corechainlink.Config{ + Aptos: corechainlink.RawConfigs{ + corechainlink.RawConfig{ + "ChainID": "4", + "Workflow": corechainlink.RawConfig{ + "ForwarderAddress": "0xold4", + "Keep": "value4", + }, + }, + corechainlink.RawConfig{ + "ChainID": "8", + "Workflow": corechainlink.RawConfig{ + "ForwarderAddress": "0xold8", + "Keep": "value8", + }, + }, + }, + } + rawConfig, err := toml.Marshal(baseConfig) + require.NoError(t, err) + + don := testDonMetadata(t, string(rawConfig), string(rawConfig)) + err = patchNodeTOML(don, map[uint64]string{ + 4: "0x0000000000000000000000000000000000000000000000000000000000000004", + 8: "0x0000000000000000000000000000000000000000000000000000000000000008", + }) + require.NoError(t, err) + + for _, spec := range don.MustNodeSet().NodeSpecs { + var patched corechainlink.Config + require.NoError(t, toml.Unmarshal([]byte(spec.Node.TestConfigOverrides), &patched)) + + workflow4 := workflowMap(t, patched.Aptos[0]["Workflow"]) + require.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000004", workflow4["ForwarderAddress"]) + require.Equal(t, "value4", workflow4["Keep"]) + + workflow8 := workflowMap(t, patched.Aptos[1]["Workflow"]) + require.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000000008", workflow8["ForwarderAddress"]) + require.Equal(t, "value8", workflow8["Keep"]) + } +} + +func TestFindAptosChainByChainID_ReturnsTypedBlockchain(t *testing.T) { + aptosBC := testAptosBlockchain(4, 4457093679053095497) + + got, err := findAptosChainByChainID([]creblockchains.Blockchain{aptosBC}, 4) + require.NoError(t, err) + require.Same(t, aptosBC, got) +} + +func TestFindAptosChainByChainID_ErrorsOnTypeMismatch(t *testing.T) { + chains := []creblockchains.Blockchain{fakeChain{family: "aptos", chainID: 4}} + + _, err := findAptosChainByChainID(chains, 4) + require.Error(t, err) + require.ErrorContains(t, err, "unexpected type") +} + +func testDonMetadata(t *testing.T, nodeConfigs ...string) *cre.DonMetadata { + t.Helper() + + nodeSpecs := make([]*cre.NodeSpecWithRole, len(nodeConfigs)) + for i, cfg := range nodeConfigs { + nodeSpecs[i] = &cre.NodeSpecWithRole{ + Input: &clnode.Input{ + Node: &clnode.NodeInput{TestConfigOverrides: cfg}, + }, + Roles: []cre.NodeType{cre.WorkerNode}, + } + } + + nodeSet := &cre.NodeSet{ + Input: &ns.Input{Name: "aptos-don"}, + NodeSpecs: nodeSpecs, + } + + don, err := cre.NewDonMetadata(nodeSet, 1, infra.Provider{Type: infra.Docker}, nil) + require.NoError(t, err) + return don +} + +func testAptosBlockchain(chainID, chainSelector uint64) *aptoschain.Blockchain { + bc := &aptoschain.Blockchain{} + setUnexportedField(bc, "chainID", chainID) + setUnexportedField(bc, "chainSelector", chainSelector) + setUnexportedField(bc, "ctfOutput", &blockchain.Output{Family: "aptos", ChainID: "4"}) + return bc +} + +func setUnexportedField(target any, fieldName string, value any) { + field := reflect.ValueOf(target).Elem().FieldByName(fieldName) + reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Set(reflect.ValueOf(value)) +} + +func workflowMap(t *testing.T, raw any) map[string]any { + t.Helper() + + switch v := raw.(type) { + case corechainlink.RawConfig: + return map[string]any(v) + case map[string]any: + return v + default: + t.Fatalf("unexpected workflow type %T", raw) + return nil + } +} + +type fakeChain struct { + family string + chainID uint64 +} + +func (f fakeChain) ChainSelector() uint64 { return 0 } +func (f fakeChain) ChainID() uint64 { return f.chainID } +func (f fakeChain) ChainFamily() string { return f.family } +func (f fakeChain) IsFamily(chainFamily string) bool { + return f.family == chainFamily +} +func (f fakeChain) Fund(context.Context, string, uint64) error { return nil } +func (f fakeChain) CtfOutput() *blockchain.Output { return nil } +func (f fakeChain) ToCldfChain() (cldfchain.BlockChain, error) { return nil, nil } diff --git a/system-tests/lib/cre/features/aptos/aptos_workers.go b/system-tests/lib/cre/features/aptos/aptos_workers.go new file mode 100644 index 00000000000..00d6077f6e7 --- /dev/null +++ b/system-tests/lib/cre/features/aptos/aptos_workers.go @@ -0,0 +1,202 @@ +package aptos + +import ( + "context" + "encoding/hex" + "fmt" + "strconv" + "strings" + + "dario.cat/mergo" + pkgerrors "github.com/pkg/errors" + + chainselectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink/deployment/cre/jobs" + crejobops "github.com/smartcontractkit/chainlink/deployment/cre/jobs/operations" + jobtypes "github.com/smartcontractkit/chainlink/deployment/cre/jobs/types" + "github.com/smartcontractkit/chainlink/deployment/cre/pkg/offchain" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs/standardcapability" +) + +func proposeAptosWorkerSpecs( + ctx context.Context, + don *cre.Don, + dons *cre.Dons, + creEnv *cre.Environment, + nodeSet cre.NodeSetWithCapabilityConfigs, + enabledChainIDs []uint64, +) (map[string][]string, error) { + specs := make(map[string][]string) + bootstrapPeers, err := bootstrapPeersForDons(dons) + if err != nil { + return nil, err + } + + for _, chainID := range enabledChainIDs { + aptosChain, err := findAptosChainByChainID(creEnv.Blockchains, chainID) + if err != nil { + return nil, err + } + + capabilityConfig, err := cre.ResolveCapabilityConfig(nodeSet, flag, cre.ChainCapabilityScope(chainID)) + if err != nil { + return nil, fmt.Errorf("could not resolve capability config for '%s' on chain %d: %w", flag, chainID, err) + } + command, err := standardcapability.GetCommand(capabilityConfig.BinaryName) + if err != nil { + return nil, pkgerrors.Wrap(err, "failed to get command for Aptos capability") + } + + forwarderAddress := mustForwarderAddress(creEnv.CldfEnvironment.DataStore, aptosChain.ChainSelector()) + workerMetadata, err := don.Metadata().Workers() + if err != nil { + return nil, fmt.Errorf("failed to collect Aptos worker metadata for DON %q: %w", don.Name, err) + } + p2pToTransmitterMap, err := p2pToTransmitterMapForWorkers(workerMetadata) + if err != nil { + return nil, fmt.Errorf("failed to collect Aptos worker transmitters for DON %q: %w", don.Name, err) + } + methodSettings, err := resolveMethodConfigSettings(capabilityConfig.Values) + if err != nil { + return nil, fmt.Errorf("failed to resolve Aptos method config settings for chain %d: %w", chainID, err) + } + configStr, err := buildWorkerConfigJSON(chainID, forwarderAddress, methodSettings, p2pToTransmitterMap, true) + if err != nil { + return nil, fmt.Errorf("failed to build Aptos worker config: %w", err) + } + + workerInput := jobs.ProposeJobSpecInput{ + Domain: offchain.ProductLabel, + Environment: cre.EnvironmentName, + DONName: don.Name, + JobName: "write-aptos-worker-" + strconv.FormatUint(chainID, 10), + ExtraLabels: map[string]string{cre.CapabilityLabelKey: flag}, + DONFilters: []offchain.TargetDONFilter{ + {Key: offchain.FilterKeyDONName, Value: don.Name}, + }, + Template: jobtypes.Aptos, + Inputs: jobtypes.JobSpecInput{ + "command": command, + "config": configStr, + "chainSelectorEVM": creEnv.RegistryChainSelector, + "chainSelectorAptos": aptosChain.ChainSelector(), + "bootstrapPeers": bootstrapPeers, + "useCapRegOCRConfig": false, + "contractQualifier": ocr3ContractQualifier, + }, + } + + proposer := jobs.ProposeJobSpec{} + if err := proposer.VerifyPreconditions(*creEnv.CldfEnvironment, workerInput); err != nil { + return nil, fmt.Errorf("precondition verification failed for Aptos worker job: %w", err) + } + workerReport, err := proposer.Apply(*creEnv.CldfEnvironment, workerInput) + if err != nil { + return nil, fmt.Errorf("failed to propose Aptos worker job spec: %w", err) + } + + for _, report := range workerReport.Reports { + out, ok := report.Output.(crejobops.ProposeStandardCapabilityJobOutput) + if !ok { + return nil, fmt.Errorf("unable to cast to ProposeStandardCapabilityJobOutput, actual type: %T", report.Output) + } + if err := mergo.Merge(&specs, out.Specs, mergo.WithAppendSlice); err != nil { + return nil, fmt.Errorf("failed to merge Aptos worker job specs: %w", err) + } + } + } + + return specs, nil +} + +func donOraclePublicKeys(ctx context.Context, don *cre.Don) ([][]byte, error) { + workers, err := don.Workers() + if err != nil { + return nil, fmt.Errorf("failed to list worker nodes for DON %q: %w", don.Name, err) + } + + oracles := make([][]byte, 0, len(workers)) + for _, worker := range workers { + ocr2ID := "" + if worker.Keys != nil && worker.Keys.OCR2BundleIDs != nil { + ocr2ID = worker.Keys.OCR2BundleIDs[chainselectors.FamilyAptos] + } + if ocr2ID == "" { + fetchedID, err := worker.Clients.GQLClient.FetchOCR2KeyBundleID(ctx, strings.ToUpper(chainselectors.FamilyAptos)) + if err != nil { + return nil, fmt.Errorf("missing Aptos OCR2 bundle id for worker %q in DON %q and fallback fetch failed: %w", worker.Name, don.Name, err) + } + if fetchedID == "" { + return nil, fmt.Errorf("missing Aptos OCR2 bundle id for worker %q in DON %q", worker.Name, don.Name) + } + ocr2ID = fetchedID + if worker.Keys != nil { + if worker.Keys.OCR2BundleIDs == nil { + worker.Keys.OCR2BundleIDs = make(map[string]string) + } + worker.Keys.OCR2BundleIDs[chainselectors.FamilyAptos] = ocr2ID + } + } + + exported, err := worker.ExportOCR2Keys(ocr2ID) + if err != nil { + return nil, fmt.Errorf("failed to export Aptos OCR2 key for worker %q (bundle %s): %w", worker.Name, ocr2ID, err) + } + pubkey, err := parseOCR2OnchainPublicKey(exported.OnchainPublicKey) + if err != nil { + return nil, fmt.Errorf("invalid Aptos OCR2 onchain public key for worker %q: %w", worker.Name, err) + } + oracles = append(oracles, pubkey) + } + + return oracles, nil +} + +func p2pToTransmitterMapForWorkers(workers []*cre.NodeMetadata) (map[string]string, error) { + if len(workers) == 0 { + return nil, pkgerrors.New("no DON worker nodes provided") + } + + p2pToTransmitterMap := make(map[string]string) + for _, worker := range workers { + if worker.Keys == nil || worker.Keys.P2PKey == nil { + return nil, fmt.Errorf("missing P2P key for worker index %d", worker.Index) + } + + account := worker.Keys.AptosAccount() + if account == "" { + return nil, fmt.Errorf("missing Aptos account for worker index %d", worker.Index) + } + + transmitter, err := normalizeTransmitter(account) + if err != nil { + return nil, fmt.Errorf("invalid Aptos transmitter for worker index %d: %w", worker.Index, err) + } + + peerKey := hex.EncodeToString(worker.Keys.P2PKey.PeerID[:]) + p2pToTransmitterMap[peerKey] = transmitter + } + + if len(p2pToTransmitterMap) == 0 { + return nil, pkgerrors.New("no Aptos transmitters found for DON workers") + } + + return p2pToTransmitterMap, nil +} + +func aptosDonIDUint32(donID uint64) (uint32, error) { + if donID > uint64(^uint32(0)) { + return 0, fmt.Errorf("don id %d exceeds u32", donID) + } + return uint32(donID), nil +} + +func parseOCR2OnchainPublicKey(hexValue string) ([]byte, error) { + trimmed := strings.TrimPrefix(strings.TrimSpace(hexValue), "ocr2on_aptos_") + decoded, err := hex.DecodeString(trimmed) + if err != nil { + return nil, err + } + return decoded, nil +} diff --git a/system-tests/tests/smoke/cre/aptos/aptoswrite/main.go b/system-tests/tests/smoke/cre/aptos/aptoswrite/main.go index 41275dcbb26..d62d485a79c 100644 --- a/system-tests/tests/smoke/cre/aptos/aptoswrite/main.go +++ b/system-tests/tests/smoke/cre/aptos/aptoswrite/main.go @@ -135,9 +135,9 @@ func onAptosWriteTrigger(cfg config.Config, runtime sdk.Runtime, payload *cron.P "maxGasAmount", cfg.MaxGasAmount, "gasUnitPrice", cfg.GasUnitPrice, ) - reply, err := client.WriteReport(runtime, &aptos.WriteReportRequest{ + reply, err := client.WriteReport(runtime, &aptos.WriteCreReportRequest{ Receiver: receiver, - Report: reportResp, + Report: report, GasConfig: &aptos.GasConfig{ MaxGasAmount: cfg.MaxGasAmount, GasUnitPrice: cfg.GasUnitPrice, diff --git a/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/main.go b/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/main.go index 5ca3cfa592b..af42817ee6f 100644 --- a/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/main.go +++ b/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip/main.go @@ -89,9 +89,9 @@ func onAptosWriteReadRoundtripTrigger(cfg config.Config, runtime sdk.Runtime, pa } client := aptos.Client{ChainSelector: cfg.ChainSelector} - reply, err := client.WriteReport(runtime, &aptos.WriteReportRequest{ + reply, err := client.WriteReport(runtime, &aptos.WriteCreReportRequest{ Receiver: receiverBytes, - Report: reportResp, + Report: report, GasConfig: &aptos.GasConfig{ MaxGasAmount: cfg.MaxGasAmount, GasUnitPrice: cfg.GasUnitPrice, diff --git a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go index e453e02193d..c569d92642c 100644 --- a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go +++ b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go @@ -43,14 +43,16 @@ import ( const aptosLocalMaxGasAmount uint64 = 200_000 const aptosWorkerFundingAmountOctas uint64 = 1_000_000_000_000 +const aptosScenarioOverrideEnv = "CRE_APTOS_SCENARIOS" var aptosForwarderVersion = semver.MustParse("1.0.0") -// ExecuteAptosTest runs the Aptos CRE suite with the read path only. The write -// scenarios stay available for local/manual execution until the write-report CI -// path is ready again. +// ExecuteAptosTest runs the Aptos CRE suite with the default CI coverage in a +// single scenario: successful write/read roundtrip and expected write failure. +// The standalone read and plain write scenarios stay available for +// local/manual execution. func ExecuteAptosTest(t *testing.T, tenv *configuration.TestEnvironment) { - executeAptosScenarios(t, tenv, aptosDefaultScenarios()) + executeAptosScenarios(t, tenv, resolveAptosScenarios(t)) } type aptosScenario struct { @@ -66,10 +68,77 @@ type aptosScenario struct { func aptosDefaultScenarios() []aptosScenario { return []aptosScenario{ - {name: "Aptos Read", run: ExecuteAptosReadTest}, + {name: "Aptos CI Coverage", run: ExecuteAptosCICoverageTest}, } } +func resolveAptosScenarios(t *testing.T) []aptosScenario { + t.Helper() + + raw := strings.TrimSpace(os.Getenv(aptosScenarioOverrideEnv)) + if raw == "" { + return aptosDefaultScenarios() + } + + available := map[string]aptosScenario{ + "ci": { + name: "Aptos CI Coverage", + run: ExecuteAptosCICoverageTest, + }, + "read": { + name: "Aptos Read", + run: ExecuteAptosReadTest, + }, + "write": { + name: "Aptos Write", + run: ExecuteAptosWriteTest, + }, + "roundtrip": { + name: "Aptos Write Read Roundtrip", + run: ExecuteAptosWriteReadRoundtripTest, + }, + "write-expected-failure": { + name: "Aptos Write Expected Failure", + run: ExecuteAptosWriteExpectedFailureTest, + }, + } + + parts := strings.Split(raw, ",") + scenarios := make([]aptosScenario, 0, len(parts)) + seen := make(map[string]struct{}, len(parts)) + for _, part := range parts { + key := strings.ToLower(strings.TrimSpace(part)) + if key == "" { + continue + } + scenario, ok := available[key] + require.Truef(t, ok, "unknown Aptos scenario %q in %s", key, aptosScenarioOverrideEnv) + if _, duplicate := seen[scenario.name]; duplicate { + continue + } + seen[scenario.name] = struct{}{} + scenarios = append(scenarios, scenario) + } + + require.NotEmptyf(t, scenarios, "%s was set but did not resolve to any Aptos scenarios", aptosScenarioOverrideEnv) + t.Logf("running Aptos scenarios from %s=%q", aptosScenarioOverrideEnv, raw) + + return scenarios +} + +func ExecuteAptosCICoverageTest( + t *testing.T, + tenv *configuration.TestEnvironment, + aptosChain blockchains.Blockchain, + userLogsCh <-chan *workflowevents.UserLogs, + baseMessageCh <-chan *commonevents.BaseMessage, +) { + t.Helper() + + ExecuteAptosWriteReadRoundtripTest(t, tenv, aptosChain, userLogsCh, baseMessageCh) + ExecuteAptosWriteExpectedFailureTest(t, tenv, aptosChain, userLogsCh, baseMessageCh) +} + func executeAptosScenarios(t *testing.T, tenv *configuration.TestEnvironment, scenarios []aptosScenario) { creEnv := tenv.CreEnvironment require.NotEmpty(t, creEnv.Blockchains, "Aptos suite expects at least one blockchain in the environment") @@ -215,7 +284,7 @@ func ExecuteAptosWriteTest( lggr := framework.L scenario := prepareAptosWriteScenario(t, tenv, aptosChain) - const workflowName = "aptos-write-workflow" + workflowName := uniqueAptosWorkflowName("aptos-write-workflow") workflowConfig := aptoswrite_config.Config{ ChainSelector: scenario.chainSelector, WorkflowName: workflowName, @@ -250,7 +319,7 @@ func ExecuteAptosWriteReadRoundtripTest( lggr := framework.L scenario := prepareAptosRoundtripScenario(t, tenv, aptosChain) - const workflowName = "aptos-write-read-roundtrip-workflow" + workflowName := uniqueAptosWorkflowName("aptos-write-read-roundtrip-workflow") roundtripCfg := aptoswriteroundtrip_config.Config{ ChainSelector: scenario.chainSelector, WorkflowName: workflowName, @@ -291,7 +360,7 @@ func ExecuteAptosWriteExpectedFailureTest( lggr := framework.L scenario := prepareAptosWriteScenario(t, tenv, aptosChain) - const workflowName = "aptos-write-expected-failure-workflow" + workflowName := uniqueAptosWorkflowName("aptos-write-expected-failure-workflow") workflowConfig := aptoswrite_config.Config{ ChainSelector: scenario.chainSelector, WorkflowName: workflowName, @@ -363,6 +432,10 @@ func prepareAptosWriteScenarioWithBenchmark( } } +func uniqueAptosWorkflowName(base string) string { + return fmt.Sprintf("%s-%d", base, time.Now().UnixNano()%1_000_000) +} + func findWriteAptosDonForChain(t *testing.T, tenv *configuration.TestEnvironment, chainID uint64) *crelib.Don { t.Helper() require.NotNil(t, tenv.Dons, "test environment DON metadata is required") From 7a7f416526b307090a8484bcb2459b56aa9c649c Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 12:26:59 +0100 Subject: [PATCH 10/35] lint: fix aptos refactor formatting and shadowing --- system-tests/lib/cre/features/aptos/aptos.go | 12 ++++++++---- system-tests/lib/cre/features/aptos/aptos_helpers.go | 2 +- system-tests/lib/cre/features/aptos/aptos_workers.go | 5 +++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/system-tests/lib/cre/features/aptos/aptos.go b/system-tests/lib/cre/features/aptos/aptos.go index a305520cacb..8af687fcc5e 100644 --- a/system-tests/lib/cre/features/aptos/aptos.go +++ b/system-tests/lib/cre/features/aptos/aptos.go @@ -80,7 +80,8 @@ func (a *Aptos) PreEnvStartup( if err != nil { return nil, err } - if err := patchNodeTOML(don, forwardersByChainID); err != nil { + err = patchNodeTOML(don, forwardersByChainID) + if err != nil { return nil, err } @@ -128,10 +129,12 @@ func (a *Aptos) PostEnvStartup( return nil } - if err := configureForwarders(ctx, testLogger, don, creEnv, enabledChainIDs); err != nil { + err = configureForwarders(ctx, testLogger, don, creEnv, enabledChainIDs) + if err != nil { return err } - if _, _, err := crecontracts.DeployOCR3Contract(testLogger, ocr3ContractQualifier, creEnv.RegistryChainSelector, creEnv.CldfEnvironment, creEnv.ContractVersions); err != nil { + _, _, err = crecontracts.DeployOCR3Contract(testLogger, ocr3ContractQualifier, creEnv.RegistryChainSelector, creEnv.CldfEnvironment, creEnv.ContractVersions) + if err != nil { return fmt.Errorf("failed to deploy Aptos OCR3 contract: %w", err) } @@ -142,7 +145,8 @@ func (a *Aptos) PostEnvStartup( if len(specs) == 0 { return nil } - if err := crejobs.Approve(ctx, creEnv.CldfEnvironment.Offchain, dons, specs); err != nil { + err = crejobs.Approve(ctx, creEnv.CldfEnvironment.Offchain, dons, specs) + if err != nil { return fmt.Errorf("failed to approve Aptos jobs: %w", err) } diff --git a/system-tests/lib/cre/features/aptos/aptos_helpers.go b/system-tests/lib/cre/features/aptos/aptos_helpers.go index d88b0b2be40..1621d46d4bb 100644 --- a/system-tests/lib/cre/features/aptos/aptos_helpers.go +++ b/system-tests/lib/cre/features/aptos/aptos_helpers.go @@ -6,8 +6,8 @@ import ( aptossdk "github.com/aptos-labs/aptos-go-sdk" pkgerrors "github.com/pkg/errors" - chainselectors "github.com/smartcontractkit/chain-selectors" + creblockchains "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains" aptoschain "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/aptos" ) diff --git a/system-tests/lib/cre/features/aptos/aptos_workers.go b/system-tests/lib/cre/features/aptos/aptos_workers.go index 00d6077f6e7..7388c822fd9 100644 --- a/system-tests/lib/cre/features/aptos/aptos_workers.go +++ b/system-tests/lib/cre/features/aptos/aptos_workers.go @@ -9,8 +9,8 @@ import ( "dario.cat/mergo" pkgerrors "github.com/pkg/errors" - chainselectors "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/chainlink/deployment/cre/jobs" crejobops "github.com/smartcontractkit/chainlink/deployment/cre/jobs/operations" jobtypes "github.com/smartcontractkit/chainlink/deployment/cre/jobs/types" @@ -88,7 +88,8 @@ func proposeAptosWorkerSpecs( } proposer := jobs.ProposeJobSpec{} - if err := proposer.VerifyPreconditions(*creEnv.CldfEnvironment, workerInput); err != nil { + err = proposer.VerifyPreconditions(*creEnv.CldfEnvironment, workerInput) + if err != nil { return nil, fmt.Errorf("precondition verification failed for Aptos worker job: %w", err) } workerReport, err := proposer.Apply(*creEnv.CldfEnvironment, workerInput) From d76523b426716c7a62de2e71ce5d541fb6a038a6 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 12:59:48 +0100 Subject: [PATCH 11/35] chore: trim aptos pr collateral changes --- go.md | 17 ---------- .../tests/test-helpers/before_suite.go | 33 +++++++------------ 2 files changed, 12 insertions(+), 38 deletions(-) diff --git a/go.md b/go.md index d63f2f246cf..430c3a96691 100644 --- a/go.md +++ b/go.md @@ -478,8 +478,6 @@ flowchart LR chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/evm/logtrigger-negative chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/http chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/httpaction-negative - chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite - chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/evm/evmread chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/evm/logtrigger chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/evmread @@ -503,15 +501,6 @@ flowchart LR chainlink/system-tests/tests/regression/cre/httpaction-negative --> cre-sdk-go/capabilities/networking/http chainlink/system-tests/tests/regression/cre/httpaction-negative --> cre-sdk-go/capabilities/scheduler/cron click chainlink/system-tests/tests/regression/cre/httpaction-negative href "https://github.com/smartcontractkit/chainlink" - chainlink/system-tests/tests/smoke/cre/aptos/aptosread --> cre-sdk-go/capabilities/blockchain/aptos - chainlink/system-tests/tests/smoke/cre/aptos/aptosread --> cre-sdk-go/capabilities/scheduler/cron - click chainlink/system-tests/tests/smoke/cre/aptos/aptosread href "https://github.com/smartcontractkit/chainlink" - chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite --> cre-sdk-go/capabilities/blockchain/aptos - chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite --> cre-sdk-go/capabilities/scheduler/cron - click chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite href "https://github.com/smartcontractkit/chainlink" - chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip --> cre-sdk-go/capabilities/blockchain/aptos - chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip --> cre-sdk-go/capabilities/scheduler/cron - click chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip href "https://github.com/smartcontractkit/chainlink" chainlink/system-tests/tests/smoke/cre/evm/evmread --> cre-sdk-go/capabilities/blockchain/evm chainlink/system-tests/tests/smoke/cre/evm/evmread --> cre-sdk-go/capabilities/scheduler/cron click chainlink/system-tests/tests/smoke/cre/evm/evmread href "https://github.com/smartcontractkit/chainlink" @@ -542,8 +531,6 @@ flowchart LR click chainlink/v2 href "https://github.com/smartcontractkit/chainlink" cre-sdk-go --> chainlink-protos/cre/go click cre-sdk-go href "https://github.com/smartcontractkit/cre-sdk-go" - cre-sdk-go/capabilities/blockchain/aptos --> cre-sdk-go - click cre-sdk-go/capabilities/blockchain/aptos href "https://github.com/smartcontractkit/cre-sdk-go" cre-sdk-go/capabilities/blockchain/evm --> chainlink-common/pkg/workflows/sdk/v2/pb cre-sdk-go/capabilities/blockchain/evm --> cre-sdk-go click cre-sdk-go/capabilities/blockchain/evm href "https://github.com/smartcontractkit/cre-sdk-go" @@ -597,9 +584,6 @@ flowchart LR chainlink/system-tests/tests/regression/cre/evm/logtrigger-negative chainlink/system-tests/tests/regression/cre/http chainlink/system-tests/tests/regression/cre/httpaction-negative - chainlink/system-tests/tests/smoke/cre/aptos/aptosread - chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite - chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip chainlink/system-tests/tests/smoke/cre/evm/evmread chainlink/system-tests/tests/smoke/cre/evm/logtrigger chainlink/system-tests/tests/smoke/cre/evmread @@ -700,7 +684,6 @@ flowchart LR subgraph cre-sdk-go-repo[cre-sdk-go] cre-sdk-go - cre-sdk-go/capabilities/blockchain/aptos cre-sdk-go/capabilities/blockchain/evm cre-sdk-go/capabilities/blockchain/solana cre-sdk-go/capabilities/networking/http diff --git a/system-tests/tests/test-helpers/before_suite.go b/system-tests/tests/test-helpers/before_suite.go index eab0ee7e0d7..8cc18a6562d 100644 --- a/system-tests/tests/test-helpers/before_suite.go +++ b/system-tests/tests/test-helpers/before_suite.go @@ -336,13 +336,18 @@ func setConfigurationIfMissing(configName string) error { func createEnvironmentIfNotExists(ctx context.Context, relativePathToRepoRoot, environmentDir string, flags ...string) error { if !envconfig.LocalCREStateFileExists(relativePathToRepoRoot) { - framework.L.Info(). - Str("CTF_CONFIGS", os.Getenv("CTF_CONFIGS")). - Str("local CRE state file", envconfig.MustLocalCREStateFileAbsPath(relativePathToRepoRoot)). - Msg("Local CRE state file does not exist, starting environment...") - - if err := startEnvironment(ctx, environmentDir, flags...); err != nil { - return err + framework.L.Info().Str("CTF_CONFIGS", os.Getenv("CTF_CONFIGS")).Str("local CRE state file", envconfig.MustLocalCREStateFileAbsPath(relativePathToRepoRoot)).Msg("Local CRE state file does not exist, starting environment...") + + args := []string{"run", ".", "env", "start"} + args = append(args, flags...) + + cmd := exec.CommandContext(ctx, "go", args...) + cmd.Dir = environmentDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmdErr := cmd.Run() + if cmdErr != nil { + return errors.Wrap(cmdErr, "failed to start environment") } } @@ -392,17 +397,3 @@ func setCldfEVMDeployerKey(env *cldf.Environment, chainSelector uint64, deployer env.BlockChains = cldf_chain.NewBlockChainsFromSlice(chainCopies) return nil } - -func startEnvironment(ctx context.Context, environmentDir string, flags ...string) error { - args := []string{"run", ".", "env", "start"} - args = append(args, flags...) - - cmd := exec.CommandContext(ctx, "go", args...) - cmd.Dir = environmentDir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return errors.Wrap(err, "failed to start environment") - } - return nil -} From 2d2b730399cf8f9fb3334a0abba3a575b5796f4b Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 13:08:47 +0100 Subject: [PATCH 12/35] docs: restore generated aptos dependency graph --- go.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/go.md b/go.md index 430c3a96691..d63f2f246cf 100644 --- a/go.md +++ b/go.md @@ -478,6 +478,8 @@ flowchart LR chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/evm/logtrigger-negative chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/http chainlink/system-tests/tests --> chainlink/system-tests/tests/regression/cre/httpaction-negative + chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite + chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/evm/evmread chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/evm/logtrigger chainlink/system-tests/tests --> chainlink/system-tests/tests/smoke/cre/evmread @@ -501,6 +503,15 @@ flowchart LR chainlink/system-tests/tests/regression/cre/httpaction-negative --> cre-sdk-go/capabilities/networking/http chainlink/system-tests/tests/regression/cre/httpaction-negative --> cre-sdk-go/capabilities/scheduler/cron click chainlink/system-tests/tests/regression/cre/httpaction-negative href "https://github.com/smartcontractkit/chainlink" + chainlink/system-tests/tests/smoke/cre/aptos/aptosread --> cre-sdk-go/capabilities/blockchain/aptos + chainlink/system-tests/tests/smoke/cre/aptos/aptosread --> cre-sdk-go/capabilities/scheduler/cron + click chainlink/system-tests/tests/smoke/cre/aptos/aptosread href "https://github.com/smartcontractkit/chainlink" + chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite --> cre-sdk-go/capabilities/blockchain/aptos + chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite --> cre-sdk-go/capabilities/scheduler/cron + click chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite href "https://github.com/smartcontractkit/chainlink" + chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip --> cre-sdk-go/capabilities/blockchain/aptos + chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip --> cre-sdk-go/capabilities/scheduler/cron + click chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip href "https://github.com/smartcontractkit/chainlink" chainlink/system-tests/tests/smoke/cre/evm/evmread --> cre-sdk-go/capabilities/blockchain/evm chainlink/system-tests/tests/smoke/cre/evm/evmread --> cre-sdk-go/capabilities/scheduler/cron click chainlink/system-tests/tests/smoke/cre/evm/evmread href "https://github.com/smartcontractkit/chainlink" @@ -531,6 +542,8 @@ flowchart LR click chainlink/v2 href "https://github.com/smartcontractkit/chainlink" cre-sdk-go --> chainlink-protos/cre/go click cre-sdk-go href "https://github.com/smartcontractkit/cre-sdk-go" + cre-sdk-go/capabilities/blockchain/aptos --> cre-sdk-go + click cre-sdk-go/capabilities/blockchain/aptos href "https://github.com/smartcontractkit/cre-sdk-go" cre-sdk-go/capabilities/blockchain/evm --> chainlink-common/pkg/workflows/sdk/v2/pb cre-sdk-go/capabilities/blockchain/evm --> cre-sdk-go click cre-sdk-go/capabilities/blockchain/evm href "https://github.com/smartcontractkit/cre-sdk-go" @@ -584,6 +597,9 @@ flowchart LR chainlink/system-tests/tests/regression/cre/evm/logtrigger-negative chainlink/system-tests/tests/regression/cre/http chainlink/system-tests/tests/regression/cre/httpaction-negative + chainlink/system-tests/tests/smoke/cre/aptos/aptosread + chainlink/system-tests/tests/smoke/cre/aptos/aptoswrite + chainlink/system-tests/tests/smoke/cre/aptos/aptoswriteroundtrip chainlink/system-tests/tests/smoke/cre/evm/evmread chainlink/system-tests/tests/smoke/cre/evm/logtrigger chainlink/system-tests/tests/smoke/cre/evmread @@ -684,6 +700,7 @@ flowchart LR subgraph cre-sdk-go-repo[cre-sdk-go] cre-sdk-go + cre-sdk-go/capabilities/blockchain/aptos cre-sdk-go/capabilities/blockchain/evm cre-sdk-go/capabilities/blockchain/solana cre-sdk-go/capabilities/networking/http From 7e52ae25b7615bb2a5e46c2878908c1f56f464bb Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 13:12:41 +0100 Subject: [PATCH 13/35] test: drop unrelated ccip nonce tweak --- integration-tests/smoke/ccip/ccip_reader_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration-tests/smoke/ccip/ccip_reader_test.go b/integration-tests/smoke/ccip/ccip_reader_test.go index b652786589c..9b134627b8d 100644 --- a/integration-tests/smoke/ccip/ccip_reader_test.go +++ b/integration-tests/smoke/ccip/ccip_reader_test.go @@ -746,14 +746,14 @@ func TestCCIPReader_Nonces(t *testing.T) { Auth: auth, }) - // Commit each simulated transaction so bind does not reuse a stale pending nonce. + // Add some nonces. for chain, addrs := range nonces { for addr, nonce := range addrs { _, err := s.contract.SetInboundNonce(s.auth, uint64(chain), nonce, common.LeftPadBytes(addr.Bytes(), 32)) require.NoError(t, err) - s.sb.Commit() } } + s.sb.Commit() request := make(map[cciptypes.ChainSelector][]string) for chain, addresses := range nonces { From 2804116f2e0679ba76ec67dde9cfb5ad63c46f29 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 15:25:13 +0100 Subject: [PATCH 14/35] aptos: pin consensus to main and keep CI read-only --- plugins/plugins.private.yaml | 2 +- .../smoke/cre/v2_aptos_capability_test.go | 26 +++++-------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/plugins/plugins.private.yaml b/plugins/plugins.private.yaml index 475b04053b3..fe49828b6c0 100644 --- a/plugins/plugins.private.yaml +++ b/plugins/plugins.private.yaml @@ -23,7 +23,7 @@ plugins: installPath: "." consensus: - moduleURI: "github.com/smartcontractkit/capabilities/consensus" - gitRef: "4bc25dd3e53308c15caa4f34a60d39dee2a6400c" + gitRef: "edadc5b6025c4927e79d8fa98093c230bf5aaba6" installPath: "." workflowevent: - enabled: false diff --git a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go index c569d92642c..5e4896dea2f 100644 --- a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go +++ b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go @@ -47,10 +47,9 @@ const aptosScenarioOverrideEnv = "CRE_APTOS_SCENARIOS" var aptosForwarderVersion = semver.MustParse("1.0.0") -// ExecuteAptosTest runs the Aptos CRE suite with the default CI coverage in a -// single scenario: successful write/read roundtrip and expected write failure. -// The standalone read and plain write scenarios stay available for -// local/manual execution. +// ExecuteAptosTest runs the Aptos CRE suite with plain Aptos read coverage by +// default. Write-oriented scenarios stay available for local/manual execution +// via CRE_APTOS_SCENARIOS. func ExecuteAptosTest(t *testing.T, tenv *configuration.TestEnvironment) { executeAptosScenarios(t, tenv, resolveAptosScenarios(t)) } @@ -68,7 +67,7 @@ type aptosScenario struct { func aptosDefaultScenarios() []aptosScenario { return []aptosScenario{ - {name: "Aptos CI Coverage", run: ExecuteAptosCICoverageTest}, + {name: "Aptos Read", run: ExecuteAptosReadTest}, } } @@ -82,8 +81,8 @@ func resolveAptosScenarios(t *testing.T) []aptosScenario { available := map[string]aptosScenario{ "ci": { - name: "Aptos CI Coverage", - run: ExecuteAptosCICoverageTest, + name: "Aptos Read", + run: ExecuteAptosReadTest, }, "read": { name: "Aptos Read", @@ -126,19 +125,6 @@ func resolveAptosScenarios(t *testing.T) []aptosScenario { return scenarios } -func ExecuteAptosCICoverageTest( - t *testing.T, - tenv *configuration.TestEnvironment, - aptosChain blockchains.Blockchain, - userLogsCh <-chan *workflowevents.UserLogs, - baseMessageCh <-chan *commonevents.BaseMessage, -) { - t.Helper() - - ExecuteAptosWriteReadRoundtripTest(t, tenv, aptosChain, userLogsCh, baseMessageCh) - ExecuteAptosWriteExpectedFailureTest(t, tenv, aptosChain, userLogsCh, baseMessageCh) -} - func executeAptosScenarios(t *testing.T, tenv *configuration.TestEnvironment, scenarios []aptosScenario) { creEnv := tenv.CreEnvironment require.NotEmpty(t, creEnv.Blockchains, "Aptos suite expects at least one blockchain in the environment") From f440911bcb68312f104dbbb9f4a74285a156daa3 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 16:32:02 +0100 Subject: [PATCH 15/35] docs: clarify aptos metadata fallback --- system-tests/lib/cre/don.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/system-tests/lib/cre/don.go b/system-tests/lib/cre/don.go index 0449a9c6d2a..6fa7684458c 100644 --- a/system-tests/lib/cre/don.go +++ b/system-tests/lib/cre/don.go @@ -587,6 +587,9 @@ func aptosAccountForNode(ctx context.Context, n *Node) (string, error) { return n.Keys.AptosAccount(), nil } + // Prefer Aptos account/public key from node metadata when available. Falling + // back to the node API here is only to backfill older metadata shapes, and we + // cache the result back into n.Keys.Aptos below so later callers can reuse it. var runtimeKeys struct { Data []struct { Attributes struct { From d8a4e154f29077db6cecdfdbe61edd8dcc50cab5 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 16:56:54 +0100 Subject: [PATCH 16/35] tests: tighten aptos workflow names and helpers --- .../environment/blockchains/aptos/aptos.go | 16 ++++++++++------ .../lib/cre/features/aptos/aptos_test.go | 19 +++++++------------ .../smoke/cre/v2_aptos_capability_test.go | 4 +++- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/system-tests/lib/cre/environment/blockchains/aptos/aptos.go b/system-tests/lib/cre/environment/blockchains/aptos/aptos.go index 461a86ea4b4..f267270ea82 100644 --- a/system-tests/lib/cre/environment/blockchains/aptos/aptos.go +++ b/system-tests/lib/cre/environment/blockchains/aptos/aptos.go @@ -42,6 +42,15 @@ type Blockchain struct { ctfOutput *blockchain.Output } +func NewBlockchain(testLogger zerolog.Logger, chainID, chainSelector uint64, ctfOutput *blockchain.Output) *Blockchain { + return &Blockchain{ + testLogger: testLogger, + chainSelector: chainSelector, + chainID: chainID, + ctfOutput: ctfOutput, + } +} + func (a *Blockchain) ChainSelector() uint64 { return a.chainSelector } @@ -234,12 +243,7 @@ func (a *Deployer) Deploy(ctx context.Context, input *blockchain.Input) (blockch // Ensure ctfOutput has ChainID set for downstream (e.g. findAptosChains) bcOut.ChainID = chainIDStr - return &Blockchain{ - testLogger: a.testLogger, - chainSelector: selector, - chainID: chainID, - ctfOutput: bcOut, - }, nil + return NewBlockchain(a.testLogger, chainID, selector, bcOut), nil } // aptosChainSelector returns the chain selector for the given Aptos chain ID. diff --git a/system-tests/lib/cre/features/aptos/aptos_test.go b/system-tests/lib/cre/features/aptos/aptos_test.go index 849fe1fb383..101933948ef 100644 --- a/system-tests/lib/cre/features/aptos/aptos_test.go +++ b/system-tests/lib/cre/features/aptos/aptos_test.go @@ -5,12 +5,11 @@ import ( "encoding/hex" "encoding/json" "math/big" - "reflect" "testing" "time" - "unsafe" "github.com/pelletier/go-toml/v2" + "github.com/rs/zerolog" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/keystore/corekeys/p2pkey" @@ -353,16 +352,12 @@ func testDonMetadata(t *testing.T, nodeConfigs ...string) *cre.DonMetadata { } func testAptosBlockchain(chainID, chainSelector uint64) *aptoschain.Blockchain { - bc := &aptoschain.Blockchain{} - setUnexportedField(bc, "chainID", chainID) - setUnexportedField(bc, "chainSelector", chainSelector) - setUnexportedField(bc, "ctfOutput", &blockchain.Output{Family: "aptos", ChainID: "4"}) - return bc -} - -func setUnexportedField(target any, fieldName string, value any) { - field := reflect.ValueOf(target).Elem().FieldByName(fieldName) - reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Set(reflect.ValueOf(value)) + return aptoschain.NewBlockchain( + zerolog.Nop(), + chainID, + chainSelector, + &blockchain.Output{Family: "aptos", ChainID: "4"}, + ) } func workflowMap(t *testing.T, raw any) map[string]any { diff --git a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go index 5e4896dea2f..5f05d140798 100644 --- a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go +++ b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go @@ -10,6 +10,7 @@ import ( "os" "regexp" "strings" + "sync/atomic" "testing" "time" @@ -46,6 +47,7 @@ const aptosWorkerFundingAmountOctas uint64 = 1_000_000_000_000 const aptosScenarioOverrideEnv = "CRE_APTOS_SCENARIOS" var aptosForwarderVersion = semver.MustParse("1.0.0") +var aptosWorkflowNameSeq uint64 // ExecuteAptosTest runs the Aptos CRE suite with plain Aptos read coverage by // default. Write-oriented scenarios stay available for local/manual execution @@ -419,7 +421,7 @@ func prepareAptosWriteScenarioWithBenchmark( } func uniqueAptosWorkflowName(base string) string { - return fmt.Sprintf("%s-%d", base, time.Now().UnixNano()%1_000_000) + return fmt.Sprintf("%s-%d-%d", base, time.Now().UnixNano(), atomic.AddUint64(&aptosWorkflowNameSeq, 1)) } func findWriteAptosDonForChain(t *testing.T, tenv *configuration.TestEnvironment, chainID uint64) *crelib.Don { From 00c29693822e9dd1a4844607961faa41b1a6062a Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 17:18:05 +0100 Subject: [PATCH 17/35] chore: address aptos review comments --- .changeset/aptos-local-cre-support.md | 7 +++++ core/scripts/go.mod | 6 +++-- system-tests/lib/cre/don.go | 4 +-- system-tests/lib/cre/don/config/config.go | 26 +++++++------------ system-tests/lib/cre/don/secrets/secrets.go | 7 ----- system-tests/lib/cre/environment/dons.go | 4 +-- .../lib/cre/features/aptos/aptos_workers.go | 5 +++- 7 files changed, 29 insertions(+), 30 deletions(-) create mode 100644 .changeset/aptos-local-cre-support.md diff --git a/.changeset/aptos-local-cre-support.md b/.changeset/aptos-local-cre-support.md new file mode 100644 index 00000000000..a2f9c282318 --- /dev/null +++ b/.changeset/aptos-local-cre-support.md @@ -0,0 +1,7 @@ +--- +"chainlink": patch +--- + +#internal + +Add Aptos local CRE support and read CI coverage. diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 249792562db..da806ff0308 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -13,7 +13,10 @@ replace github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examp // Using a separate `require` here to avoid surrounding line changes // creating potential merge conflicts. -require github.com/smartcontractkit/chainlink/v2 v2.32.0 +require ( + github.com/smartcontractkit/chainlink/deployment v0.0.0-20251021194914-c0e3fec1a97c + github.com/smartcontractkit/chainlink/v2 v2.32.0 +) require ( github.com/Masterminds/semver/v3 v3.4.0 @@ -55,7 +58,6 @@ require ( github.com/smartcontractkit/chainlink-testing-framework/lib v1.54.5 github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 github.com/smartcontractkit/chainlink/core/scripts/cre/environment/examples/workflows/v2/proof-of-reserve/cron-based v0.0.0-00010101000000-000000000000 - github.com/smartcontractkit/chainlink/deployment v0.0.0-20251021194914-c0e3fec1a97c github.com/smartcontractkit/chainlink/system-tests/lib v0.0.0-00010101000000-000000000000 github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e github.com/spf13/cobra v1.10.2 diff --git a/system-tests/lib/cre/don.go b/system-tests/lib/cre/don.go index 6fa7684458c..15a07382ed6 100644 --- a/system-tests/lib/cre/don.go +++ b/system-tests/lib/cre/don.go @@ -583,8 +583,8 @@ func createJDChainConfigs(ctx context.Context, n *Node, supportedChains []blockc } func aptosAccountForNode(ctx context.Context, n *Node) (string, error) { - if n.Keys != nil && n.Keys.AptosAccount() != "" { - return n.Keys.AptosAccount(), nil + if n.Keys != nil && n.Keys.Aptos != nil && n.Keys.Aptos.Account != "" { + return n.Keys.Aptos.Account, nil } // Prefer Aptos account/public key from node metadata when available. Falling diff --git a/system-tests/lib/cre/don/config/config.go b/system-tests/lib/cre/don/config/config.go index 53ade9ae1eb..423e7a57433 100644 --- a/system-tests/lib/cre/don/config/config.go +++ b/system-tests/lib/cre/don/config/config.go @@ -381,10 +381,9 @@ func addBootstrapNodeConfig( for _, ac := range commonInputs.aptosChains { existingConfig.Aptos = append(existingConfig.Aptos, corechainlink.RawConfig{ - "ChainID": ac.ChainID, - "Enabled": true, - "Workflow": map[string]any{"ForwarderAddress": ac.ForwarderAddress}, - "Nodes": []map[string]any{{"Name": "default", "URL": ac.NodeURL}}, + "ChainID": ac.ChainID, + "Enabled": true, + "Nodes": []map[string]any{{"Name": "default", "URL": ac.NodeURL}}, }) } @@ -483,10 +482,9 @@ func addWorkerNodeConfig( for _, ac := range commonInputs.aptosChains { existingConfig.Aptos = append(existingConfig.Aptos, corechainlink.RawConfig{ - "ChainID": ac.ChainID, - "Enabled": true, - "Workflow": map[string]any{"ForwarderAddress": ac.ForwarderAddress}, - "Nodes": []map[string]any{{"Name": "default", "URL": ac.NodeURL}}, + "ChainID": ac.ChainID, + "Enabled": true, + "Nodes": []map[string]any{{"Name": "default", "URL": ac.NodeURL}}, }) } @@ -648,9 +646,8 @@ type versionedAddress struct { } type aptosChain struct { - ChainID string - NodeURL string - ForwarderAddress string + ChainID string + NodeURL string } type commonInputs struct { @@ -774,8 +771,6 @@ func findOneSolanaChain(input cre.GenerateConfigsInput) (*solanaChain, error) { return solChain, nil } -const aptosZeroForwarderHex = "0x0000000000000000000000000000000000000000000000000000000000000000" - func findAptosChains(input cre.GenerateConfigsInput) ([]*aptosChain, error) { capabilityChainIDs := input.DonMetadata.MustNodeSet().ChainCapabilityChainIDs() out := make([]*aptosChain, 0) @@ -794,9 +789,8 @@ func findAptosChains(input cre.GenerateConfigsInput) ([]*aptosChain, error) { } out = append(out, &aptosChain{ - ChainID: strconv.FormatUint(bcOut.ChainID(), 10), - NodeURL: nodeURL, - ForwarderAddress: aptosZeroForwarderHex, + ChainID: strconv.FormatUint(bcOut.ChainID(), 10), + NodeURL: nodeURL, }) } return out, nil diff --git a/system-tests/lib/cre/don/secrets/secrets.go b/system-tests/lib/cre/don/secrets/secrets.go index 905fb197054..451e0952857 100644 --- a/system-tests/lib/cre/don/secrets/secrets.go +++ b/system-tests/lib/cre/don/secrets/secrets.go @@ -82,13 +82,6 @@ func (n NodeKeys) PeerID() string { return n.P2PKey.PeerID.String() } -func (n NodeKeys) AptosAccount() string { - if n.Aptos == nil { - return "" - } - return n.Aptos.Account -} - func (n *NodeKeys) ToNodeSecretsTOML() (string, error) { ns := nodeSecret{} diff --git a/system-tests/lib/cre/environment/dons.go b/system-tests/lib/cre/environment/dons.go index e1bdbb6856c..6758c0f5c2f 100644 --- a/system-tests/lib/cre/environment/dons.go +++ b/system-tests/lib/cre/environment/dons.go @@ -254,8 +254,8 @@ func nodeAddress(ctx context.Context, node *cre.Node, chainFamily string, bc blo } return solKey.PublicAddress.String(), nil case chainselectors.FamilyAptos: - if node.Keys != nil && node.Keys.AptosAccount() != "" { - return node.Keys.AptosAccount(), nil + if node.Keys != nil && node.Keys.Aptos != nil && node.Keys.Aptos.Account != "" { + return node.Keys.Aptos.Account, nil } return "", nil // Skip nodes without Aptos keys for this chain default: diff --git a/system-tests/lib/cre/features/aptos/aptos_workers.go b/system-tests/lib/cre/features/aptos/aptos_workers.go index 7388c822fd9..a885c288a43 100644 --- a/system-tests/lib/cre/features/aptos/aptos_workers.go +++ b/system-tests/lib/cre/features/aptos/aptos_workers.go @@ -165,7 +165,10 @@ func p2pToTransmitterMapForWorkers(workers []*cre.NodeMetadata) (map[string]stri return nil, fmt.Errorf("missing P2P key for worker index %d", worker.Index) } - account := worker.Keys.AptosAccount() + account := "" + if worker.Keys.Aptos != nil { + account = worker.Keys.Aptos.Account + } if account == "" { return nil, fmt.Errorf("missing Aptos account for worker index %d", worker.Index) } From 190626bcc3f7a9148ea496892d0b0f44960bca3a Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 17:49:33 +0100 Subject: [PATCH 18/35] refactor: use framework aptos account helper --- core/scripts/go.mod | 2 +- core/scripts/go.sum | 2 ++ system-tests/lib/cre/don.go | 29 +++++++---------------------- system-tests/lib/cre/don_test.go | 11 ++++++++--- system-tests/lib/go.mod | 2 +- system-tests/lib/go.sum | 2 ++ system-tests/tests/go.mod | 2 +- system-tests/tests/go.sum | 2 ++ 8 files changed, 24 insertions(+), 28 deletions(-) diff --git a/core/scripts/go.mod b/core/scripts/go.mod index da806ff0308..deeb94a48e3 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -53,7 +53,7 @@ require ( github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260326111235-8c09d1a4491f github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 - github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8 + github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.20 github.com/smartcontractkit/chainlink-testing-framework/lib v1.54.5 github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 6b7e62398f0..9bf108ff385 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1704,6 +1704,8 @@ github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e4 github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e48eb0cb0/go.mod h1:IfeW6t5Yc5293H5ixuooAft+wYBMSFQWKjbBTwYiKr4= github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8 h1:n6HFv6izmbfai90FibRVy1cC/+sfeECKTtty8JuKQtU= github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f h1:NSvEYsxvGxN0FfyL8uNYdePXHEvnSYLa1bdmLTHVDeU= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.20 h1:8D2DUnn7mLUZOLhPDGGFKKvBrgU6LQd00tq2VOprvfI= github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.20/go.mod h1:98jNYBOPuKWJw9a8x0LgQuudp5enrHhQQP5Hq0YwRB8= github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake v0.10.0 h1:PWAMYu0WaAMBfbpxCpFJGRIDHmcgmYin6a+UQC0OdtY= diff --git a/system-tests/lib/cre/don.go b/system-tests/lib/cre/don.go index 15a07382ed6..f4cdcee9229 100644 --- a/system-tests/lib/cre/don.go +++ b/system-tests/lib/cre/don.go @@ -3,7 +3,6 @@ package cre import ( "context" "fmt" - "net/http" "net/url" "slices" "strconv" @@ -587,32 +586,19 @@ func aptosAccountForNode(ctx context.Context, n *Node) (string, error) { return n.Keys.Aptos.Account, nil } - // Prefer Aptos account/public key from node metadata when available. Falling - // back to the node API here is only to backfill older metadata shapes, and we - // cache the result back into n.Keys.Aptos below so later callers can reuse it. - var runtimeKeys struct { - Data []struct { - Attributes struct { - Account string `json:"account"` - PublicKey string `json:"publicKey"` - } `json:"attributes"` - } `json:"data"` - } - resp, err := n.Clients.RestClient.APIClient.R(). - SetContext(ctx). - SetResult(&runtimeKeys). - Get("/v2/keys/aptos") + // Prefer Aptos account from node metadata when available. Falling back to the + // framework helper here is only to backfill older metadata shapes, and we + // cache the normalized account back into n.Keys.Aptos below so later callers + // can reuse it. + runtimeAccounts, err := n.Clients.RestClient.MustReadAptosAccounts() if err != nil { return "", fmt.Errorf("failed to read Aptos keys from node API: %w", err) } - if resp.StatusCode() != http.StatusOK { - return "", fmt.Errorf("aptos keys endpoint returned status %d", resp.StatusCode()) - } - if len(runtimeKeys.Data) == 0 { + if len(runtimeAccounts) == 0 { return "", fmt.Errorf("no Aptos keys found on node %s", n.Name) } - account, err := crypto.NormalizeAptosAccount(runtimeKeys.Data[0].Attributes.Account) + account, err := crypto.NormalizeAptosAccount(runtimeAccounts[0]) if err != nil { return "", fmt.Errorf("invalid Aptos account returned by node API: %w", err) } @@ -622,7 +608,6 @@ func aptosAccountForNode(ctx context.Context, n *Node) (string, error) { n.Keys.Aptos = &crypto.AptosKey{} } n.Keys.Aptos.Account = account - n.Keys.Aptos.PublicKey = runtimeKeys.Data[0].Attributes.PublicKey } return account, nil diff --git a/system-tests/lib/cre/don_test.go b/system-tests/lib/cre/don_test.go index 7c70dab226d..9feda3cdce4 100644 --- a/system-tests/lib/cre/don_test.go +++ b/system-tests/lib/cre/don_test.go @@ -35,7 +35,10 @@ func TestAptosAccountForNode_UsesMetadataKeyWithoutCallingNodeAPI(t *testing.T) Aptos: &crecrypto.AptosKey{Account: expected}, }, Clients: NodeClients{ - RestClient: &clclient.ChainlinkClient{APIClient: resty.New().SetBaseURL(server.URL)}, + RestClient: &clclient.ChainlinkClient{ + APIClient: resty.New().SetBaseURL(server.URL), + Config: &clclient.Config{URL: server.URL}, + }, }, } @@ -66,7 +69,10 @@ func TestAptosAccountForNode_FallsBackToNodeAPIAndCachesKey(t *testing.T) { Name: "node-1", Keys: &secrets.NodeKeys{}, Clients: NodeClients{ - RestClient: &clclient.ChainlinkClient{APIClient: resty.New().SetBaseURL(server.URL)}, + RestClient: &clclient.ChainlinkClient{ + APIClient: resty.New().SetBaseURL(server.URL), + Config: &clclient.Config{URL: server.URL}, + }, }, } @@ -78,5 +84,4 @@ func TestAptosAccountForNode_FallsBackToNodeAPIAndCachesKey(t *testing.T) { require.Equal(t, expected, account) require.NotNil(t, node.Keys.Aptos) require.Equal(t, expected, node.Keys.Aptos.Account) - require.Equal(t, "0xabc123", node.Keys.Aptos.PublicKey) } diff --git a/system-tests/lib/go.mod b/system-tests/lib/go.mod index 7db360c4f4f..b88947d1947 100644 --- a/system-tests/lib/go.mod +++ b/system-tests/lib/go.mod @@ -44,7 +44,7 @@ require ( github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260325152920-167c7a34804c - github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8 + github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.15 github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake v0.10.0 github.com/smartcontractkit/chainlink-testing-framework/lib v1.54.5 diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index 167c2033006..0802d11fcd2 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -1671,6 +1671,8 @@ github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e4 github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e48eb0cb0/go.mod h1:IfeW6t5Yc5293H5ixuooAft+wYBMSFQWKjbBTwYiKr4= github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8 h1:n6HFv6izmbfai90FibRVy1cC/+sfeECKTtty8JuKQtU= github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f h1:NSvEYsxvGxN0FfyL8uNYdePXHEvnSYLa1bdmLTHVDeU= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.15 h1:usf6YCNmSO8R1/rU28wUfIdp7zXlqGGOAttXW5mgkXU= github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.15/go.mod h1:YqrpawYGRkT/jcvXcmaZeZPOtu0erIenrHl5Mb8+U/c= github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake v0.10.0 h1:PWAMYu0WaAMBfbpxCpFJGRIDHmcgmYin6a+UQC0OdtY= diff --git a/system-tests/tests/go.mod b/system-tests/tests/go.mod index a2d33fd5083..e2916d3ff4b 100644 --- a/system-tests/tests/go.mod +++ b/system-tests/tests/go.mod @@ -72,7 +72,7 @@ require ( github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260128151123-605e9540b706 github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f - github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8 + github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake v0.10.0 github.com/smartcontractkit/chainlink-testing-framework/havoc v1.50.7 github.com/smartcontractkit/chainlink-testing-framework/lib v1.54.5 diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index ccf60adbdbb..0803a101efa 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -1855,6 +1855,8 @@ github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e4 github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e48eb0cb0/go.mod h1:IfeW6t5Yc5293H5ixuooAft+wYBMSFQWKjbBTwYiKr4= github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8 h1:n6HFv6izmbfai90FibRVy1cC/+sfeECKTtty8JuKQtU= github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f h1:NSvEYsxvGxN0FfyL8uNYdePXHEvnSYLa1bdmLTHVDeU= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.18 h1:1ng+p/+85zcVLHB050PiWUAjOcxyd4KjwkUlJy34rgE= github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.18/go.mod h1:2+OrSz56pdgtY0Oc20nCS9LH/bEksFDBQjoR82De5PI= github.com/smartcontractkit/chainlink-testing-framework/framework/components/fake v0.10.0 h1:PWAMYu0WaAMBfbpxCpFJGRIDHmcgmYin6a+UQC0OdtY= From c94d6c4048fbf153f64742e835e1158b49413716 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 18:47:29 +0100 Subject: [PATCH 19/35] chore: tidy framework aptos helper sums --- core/scripts/go.sum | 2 -- system-tests/lib/go.sum | 2 -- system-tests/tests/go.sum | 2 -- 3 files changed, 6 deletions(-) diff --git a/core/scripts/go.sum b/core/scripts/go.sum index 9bf108ff385..84b639d943a 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1702,8 +1702,6 @@ github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 h1: github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0/go.mod h1:U3XStbEnbx/+L22n1/8aOIdgcGVxtsZB7p59xJGngAs= github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e48eb0cb0 h1:5NdsaclAfx+p8lZUZ3WIqMW3M9Cze1ZVPENOQhha1pk= github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e48eb0cb0/go.mod h1:IfeW6t5Yc5293H5ixuooAft+wYBMSFQWKjbBTwYiKr4= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8 h1:n6HFv6izmbfai90FibRVy1cC/+sfeECKTtty8JuKQtU= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f h1:NSvEYsxvGxN0FfyL8uNYdePXHEvnSYLa1bdmLTHVDeU= github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.20 h1:8D2DUnn7mLUZOLhPDGGFKKvBrgU6LQd00tq2VOprvfI= diff --git a/system-tests/lib/go.sum b/system-tests/lib/go.sum index 0802d11fcd2..4e12a16cc76 100644 --- a/system-tests/lib/go.sum +++ b/system-tests/lib/go.sum @@ -1669,8 +1669,6 @@ github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 h1: github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0/go.mod h1:U3XStbEnbx/+L22n1/8aOIdgcGVxtsZB7p59xJGngAs= github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e48eb0cb0 h1:5NdsaclAfx+p8lZUZ3WIqMW3M9Cze1ZVPENOQhha1pk= github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e48eb0cb0/go.mod h1:IfeW6t5Yc5293H5ixuooAft+wYBMSFQWKjbBTwYiKr4= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8 h1:n6HFv6izmbfai90FibRVy1cC/+sfeECKTtty8JuKQtU= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f h1:NSvEYsxvGxN0FfyL8uNYdePXHEvnSYLa1bdmLTHVDeU= github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.15 h1:usf6YCNmSO8R1/rU28wUfIdp7zXlqGGOAttXW5mgkXU= diff --git a/system-tests/tests/go.sum b/system-tests/tests/go.sum index 0803a101efa..113fb0342b9 100644 --- a/system-tests/tests/go.sum +++ b/system-tests/tests/go.sum @@ -1853,8 +1853,6 @@ github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 h1: github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0/go.mod h1:U3XStbEnbx/+L22n1/8aOIdgcGVxtsZB7p59xJGngAs= github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e48eb0cb0 h1:5NdsaclAfx+p8lZUZ3WIqMW3M9Cze1ZVPENOQhha1pk= github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-20260304150206-c64e48eb0cb0/go.mod h1:IfeW6t5Yc5293H5ixuooAft+wYBMSFQWKjbBTwYiKr4= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8 h1:n6HFv6izmbfai90FibRVy1cC/+sfeECKTtty8JuKQtU= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.8/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f h1:NSvEYsxvGxN0FfyL8uNYdePXHEvnSYLa1bdmLTHVDeU= github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.9-0.20260330164022-15e89dd1431f/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= github.com/smartcontractkit/chainlink-testing-framework/framework/components/dockercompose v0.1.18 h1:1ng+p/+85zcVLHB050PiWUAjOcxyd4KjwkUlJy34rgE= From 2f813dd25b8ec75f58857508b160a064212432ed Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 19:23:59 +0100 Subject: [PATCH 20/35] test: simplify aptos scenario constants --- .../smoke/cre/v2_aptos_capability_test.go | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go index 5f05d140798..a4d2dcd6ef8 100644 --- a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go +++ b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go @@ -42,9 +42,21 @@ import ( "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers/configuration" ) -const aptosLocalMaxGasAmount uint64 = 200_000 -const aptosWorkerFundingAmountOctas uint64 = 1_000_000_000_000 -const aptosScenarioOverrideEnv = "CRE_APTOS_SCENARIOS" +const ( + aptosLocalMaxGasAmount uint64 = 200_000 + aptosLocalGasUnitPrice uint64 = 100 + aptosWorkerFundingAmountOctas uint64 = 1_000_000_000_000 + + aptosWorkflowTimeout = 4 * time.Minute + aptosOnchainBenchmarkTimeout = 2 * time.Minute + aptosOnchainBenchmarkPollInterval = 3 * time.Second + + aptosTestFeedIDSuffix = byte(1) + aptosWriteBenchmarkValue = uint64(123456789) + aptosRoundtripBenchmarkValue = uint64(987654321) + + aptosScenarioOverrideEnv = "CRE_APTOS_SCENARIOS" +) var aptosForwarderVersion = semver.MustParse("1.0.0") var aptosWorkflowNameSeq uint64 @@ -225,7 +237,7 @@ func ExecuteAptosReadTest( t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &workflowConfig, workflowFileLocation) expectedLog := "Aptos read consensus succeeded" - t_helpers.WatchWorkflowLogs(t, lggr, userLogsCh, baseMessageCh, t_helpers.WorkflowEngineInitErrorLog, expectedLog, 4*time.Minute) + t_helpers.WatchWorkflowLogs(t, lggr, userLogsCh, baseMessageCh, t_helpers.WorkflowEngineInitErrorLog, expectedLog, aptosWorkflowTimeout) lggr.Info().Str("expected_log", expectedLog).Msg("Aptos read capability test passed") } @@ -281,14 +293,14 @@ func ExecuteAptosWriteTest( ReportPayloadHex: scenario.reportPayloadHex, // Keep within the current local Aptos transaction max-gas bound. MaxGasAmount: aptosLocalMaxGasAmount, - GasUnitPrice: 100, + GasUnitPrice: aptosLocalGasUnitPrice, } const workflowFileLocation = "./aptos/aptoswrite/main.go" ensureAptosWriteWorkersFunded(t, aptosChain, scenario.writeDon) t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &workflowConfig, workflowFileLocation) - txHash := waitForAptosWriteSuccessLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, 4*time.Minute) + txHash := waitForAptosWriteSuccessLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, aptosWorkflowTimeout) assertAptosReceiverUpdatedOnChain(t, aptosChain, scenario.receiverHex, scenario.expectedBenchmarkValue) assertAptosWriteTxOnChain(t, aptosChain, txHash, scenario.receiverHex) lggr.Info(). @@ -315,7 +327,7 @@ func ExecuteAptosWriteReadRoundtripTest( RequiredSignatures: scenario.requiredSignatures, ReportPayloadHex: scenario.reportPayloadHex, MaxGasAmount: aptosLocalMaxGasAmount, - GasUnitPrice: 100, + GasUnitPrice: aptosLocalGasUnitPrice, FeedIDHex: scenario.feedIDHex, ExpectedBenchmark: scenario.expectedBenchmarkValue, } @@ -329,7 +341,7 @@ func ExecuteAptosWriteReadRoundtripTest( baseMessageCh, t_helpers.WorkflowEngineInitErrorLog, "Aptos write/read consensus succeeded", - 4*time.Minute, + aptosWorkflowTimeout, ) lggr.Info(). Str("receiver", scenario.receiverHex). @@ -356,7 +368,7 @@ func ExecuteAptosWriteExpectedFailureTest( RequiredSignatures: scenario.requiredSignatures, ReportPayloadHex: scenario.reportPayloadHex, MaxGasAmount: aptosLocalMaxGasAmount, - GasUnitPrice: 100, + GasUnitPrice: aptosLocalGasUnitPrice, ExpectFailure: true, } @@ -364,7 +376,7 @@ func ExecuteAptosWriteExpectedFailureTest( ensureAptosWriteWorkersFunded(t, aptosChain, scenario.writeDon) t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &workflowConfig, workflowFileLocation) - txHash := waitForAptosWriteExpectedFailureLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, 4*time.Minute) + txHash := waitForAptosWriteExpectedFailureLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, aptosWorkflowTimeout) assertAptosWriteFailureTxOnChain(t, aptosChain, txHash) lggr.Info(). @@ -383,11 +395,23 @@ type aptosWriteScenario struct { } func prepareAptosWriteScenario(t *testing.T, tenv *configuration.TestEnvironment, aptosChain blockchains.Blockchain) aptosWriteScenario { - return prepareAptosWriteScenarioWithBenchmark(t, tenv, aptosChain, aptosBenchmarkFeedID(), 123456789) + return prepareAptosWriteScenarioWithBenchmark( + t, + tenv, + aptosChain, + aptosTestFeedID(), + aptosWriteBenchmarkValue, + ) } func prepareAptosRoundtripScenario(t *testing.T, tenv *configuration.TestEnvironment, aptosChain blockchains.Blockchain) aptosWriteScenario { - return prepareAptosWriteScenarioWithBenchmark(t, tenv, aptosChain, aptosRoundtripFeedID(), 987654321) + return prepareAptosWriteScenarioWithBenchmark( + t, + tenv, + aptosChain, + aptosTestFeedID(), + aptosRoundtripBenchmarkValue, + ) } func prepareAptosWriteScenarioWithBenchmark( @@ -644,7 +668,7 @@ func assertAptosReceiverUpdatedOnChain( require.NoError(t, err, "failed to parse Aptos receiver address") dataFeeds := aptosdatafeeds.Bind(receiverAddr, client) - feedID := aptosBenchmarkFeedID() + feedID := aptosTestFeedID() feedIDHex := hex.EncodeToString(feedID) require.Eventually(t, func() bool { @@ -662,7 +686,7 @@ func assertAptosReceiverUpdatedOnChain( return feed.Feed.Benchmark.Uint64() == expectedBenchmark } return false - }, 2*time.Minute, 3*time.Second, "expected benchmark value %d not observed onchain for receiver %s", expectedBenchmark, receiverHex) + }, aptosOnchainBenchmarkTimeout, aptosOnchainBenchmarkPollInterval, "expected benchmark value %d not observed onchain for receiver %s", expectedBenchmark, receiverHex) } func normalizeTxHash(input string) string { @@ -828,15 +852,9 @@ func buildAptosDataFeedsBenchmarkPayloadFor(feedID []byte, benchmark uint64) []b return out } -func aptosBenchmarkFeedID() []byte { - feedID := make([]byte, 32) - feedID[31] = 1 - return feedID -} - -func aptosRoundtripFeedID() []byte { +func aptosTestFeedID() []byte { feedID := make([]byte, 32) - feedID[31] = 2 + feedID[len(feedID)-1] = aptosTestFeedIDSuffix return feedID } From b15e7eac3ca4c2e6ae8fc38ad618d172264b534a Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 19:28:53 +0100 Subject: [PATCH 21/35] fix: scope OCR signer families per capability --- system-tests/lib/cre/don/config/config.go | 5 +++- system-tests/lib/cre/features/aptos/aptos.go | 5 ++-- system-tests/lib/cre/features/evm/v2/evm.go | 6 ---- .../lib/cre/ocr_extra_signer_families.go | 9 ++++++ .../lib/cre/ocr_extra_signer_families_test.go | 28 +++++++++++++++++++ 5 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 system-tests/lib/cre/ocr_extra_signer_families_test.go diff --git a/system-tests/lib/cre/don/config/config.go b/system-tests/lib/cre/don/config/config.go index 423e7a57433..4eb10fd7462 100644 --- a/system-tests/lib/cre/don/config/config.go +++ b/system-tests/lib/cre/don/config/config.go @@ -782,7 +782,10 @@ func findAptosChains(input cre.GenerateConfigsInput) ([]*aptosChain, error) { continue } - aptosBC := bcOut.(*aptoschain.Blockchain) + aptosBC, ok := bcOut.(*aptoschain.Blockchain) + if !ok { + return nil, fmt.Errorf("expected Aptos blockchain implementation for chain %d, got %T", bcOut.ChainID(), bcOut) + } nodeURL, err := aptosBC.InternalNodeURL() if err != nil { return nil, errors.Wrapf(err, "failed to get Aptos internal node URL for chain %d", bcOut.ChainID()) diff --git a/system-tests/lib/cre/features/aptos/aptos.go b/system-tests/lib/cre/features/aptos/aptos.go index 8af687fcc5e..cfd7530aa20 100644 --- a/system-tests/lib/cre/features/aptos/aptos.go +++ b/system-tests/lib/cre/features/aptos/aptos.go @@ -11,6 +11,7 @@ import ( pkgerrors "github.com/pkg/errors" "github.com/rs/zerolog" + chainselectors "github.com/smartcontractkit/chain-selectors" capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" kcr "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/capabilities_registry_1_1_0" "github.com/smartcontractkit/chainlink/deployment/cre/ocr3" @@ -103,7 +104,7 @@ func (a *Aptos) PreEnvStartup( DONCapabilityWithConfig: caps, CapabilityToOCR3Config: capabilityToOCR3Config, CapabilityToExtraSignerFamilies: cre.CapabilityToExtraSignerFamilies( - cre.OCRExtraSignerFamilies(creEnv.Blockchains), + cre.OCRExtraSignerFamiliesForFamily(chainselectors.FamilyAptos), capabilityLabels..., ), }, nil @@ -171,7 +172,7 @@ func (a *Aptos) PostEnvStartup( }, OracleConfig: don.ResolveORC3Config(crecontracts.DefaultChainCapabilityOCR3Config()), DryRun: false, - ExtraSignerFamilies: cre.OCRExtraSignerFamilies(creEnv.Blockchains), + ExtraSignerFamilies: cre.OCRExtraSignerFamiliesForFamily(chainselectors.FamilyAptos), }) if err != nil { return fmt.Errorf("failed to configure Aptos OCR3 contract: %w", err) diff --git a/system-tests/lib/cre/features/evm/v2/evm.go b/system-tests/lib/cre/features/evm/v2/evm.go index 8e7cf351a37..4604fe78b41 100644 --- a/system-tests/lib/cre/features/evm/v2/evm.go +++ b/system-tests/lib/cre/features/evm/v2/evm.go @@ -130,19 +130,13 @@ func (o *EVM) PreEnvStartup( } capabilityToOCR3Config := make(map[string]*ocr3.OracleConfig, len(capabilities)) - capabilityLabels := make([]string, 0, len(capabilities)) for _, cap := range capabilities { capabilityToOCR3Config[cap.Capability.LabelledName] = contracts.DefaultChainCapabilityOCR3Config() - capabilityLabels = append(capabilityLabels, cap.Capability.LabelledName) } return &cre.PreEnvStartupOutput{ DONCapabilityWithConfig: capabilities, CapabilityToOCR3Config: capabilityToOCR3Config, - CapabilityToExtraSignerFamilies: cre.CapabilityToExtraSignerFamilies( - cre.OCRExtraSignerFamilies(creEnv.Blockchains), - capabilityLabels..., - ), }, nil } diff --git a/system-tests/lib/cre/ocr_extra_signer_families.go b/system-tests/lib/cre/ocr_extra_signer_families.go index ba9e22255dd..97dcd62dae9 100644 --- a/system-tests/lib/cre/ocr_extra_signer_families.go +++ b/system-tests/lib/cre/ocr_extra_signer_families.go @@ -30,6 +30,15 @@ func OCRExtraSignerFamilies(blockchains []blockchains.Blockchain) []string { return families } +func OCRExtraSignerFamiliesForFamily(family string) []string { + switch family { + case chainselectors.FamilyAptos, chainselectors.FamilySolana: + return []string{family} + default: + return nil + } +} + func CapabilityToExtraSignerFamilies(families []string, labelledNames ...string) map[string][]string { if len(families) == 0 || len(labelledNames) == 0 { return nil diff --git a/system-tests/lib/cre/ocr_extra_signer_families_test.go b/system-tests/lib/cre/ocr_extra_signer_families_test.go new file mode 100644 index 00000000000..0918103fcb3 --- /dev/null +++ b/system-tests/lib/cre/ocr_extra_signer_families_test.go @@ -0,0 +1,28 @@ +package cre + +import ( + "testing" + + "github.com/stretchr/testify/require" + + chainselectors "github.com/smartcontractkit/chain-selectors" +) + +func TestOCRExtraSignerFamiliesForFamily(t *testing.T) { + require.Equal(t, []string{chainselectors.FamilyAptos}, OCRExtraSignerFamiliesForFamily(chainselectors.FamilyAptos)) + require.Equal(t, []string{chainselectors.FamilySolana}, OCRExtraSignerFamiliesForFamily(chainselectors.FamilySolana)) + require.Nil(t, OCRExtraSignerFamiliesForFamily(chainselectors.FamilyEVM)) +} + +func TestCapabilityToExtraSignerFamiliesCopiesInput(t *testing.T) { + families := []string{chainselectors.FamilyAptos} + got := CapabilityToExtraSignerFamilies(families, "cap-a", "cap-b") + require.Equal(t, map[string][]string{ + "cap-a": {chainselectors.FamilyAptos}, + "cap-b": {chainselectors.FamilyAptos}, + }, got) + + families[0] = chainselectors.FamilySolana + require.Equal(t, []string{chainselectors.FamilyAptos}, got["cap-a"]) + require.Equal(t, []string{chainselectors.FamilyAptos}, got["cap-b"]) +} From 9aaf1b5ef00870bb4fef15b698a44158a85cc2ed Mon Sep 17 00:00:00 2001 From: cawthorne Date: Mon, 30 Mar 2026 19:46:46 +0100 Subject: [PATCH 22/35] style: fix aptos import grouping --- system-tests/lib/cre/features/aptos/aptos.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/system-tests/lib/cre/features/aptos/aptos.go b/system-tests/lib/cre/features/aptos/aptos.go index cfd7530aa20..71f9da99b2c 100644 --- a/system-tests/lib/cre/features/aptos/aptos.go +++ b/system-tests/lib/cre/features/aptos/aptos.go @@ -12,8 +12,10 @@ import ( "github.com/rs/zerolog" chainselectors "github.com/smartcontractkit/chain-selectors" + capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" kcr "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/capabilities_registry_1_1_0" + "github.com/smartcontractkit/chainlink/deployment/cre/ocr3" creocr3changeset "github.com/smartcontractkit/chainlink/deployment/cre/ocr3/v2/changeset" creocr3contracts "github.com/smartcontractkit/chainlink/deployment/cre/ocr3/v2/changeset/operations/contracts" From bad22084e432582ca265df849c644bbb37fd3f43 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Tue, 31 Mar 2026 09:29:57 +0100 Subject: [PATCH 23/35] test: migrate aptos write to capreg ocr config --- .changeset/aptos-capreg-write.md | 7 ++ .../cre/ocr3/v2/changeset/configure_ocr3.go | 23 +++--- .../operations/contracts/configure_ocr3.go | 2 - plugins/plugins.private.yaml | 2 +- system-tests/lib/cre/features/aptos/aptos.go | 38 +-------- .../lib/cre/features/aptos/aptos_test.go | 81 ++++++++++++++++++- .../lib/cre/features/aptos/aptos_workers.go | 59 +++++++++----- 7 files changed, 136 insertions(+), 76 deletions(-) create mode 100644 .changeset/aptos-capreg-write.md diff --git a/.changeset/aptos-capreg-write.md b/.changeset/aptos-capreg-write.md new file mode 100644 index 00000000000..580dd273fd8 --- /dev/null +++ b/.changeset/aptos-capreg-write.md @@ -0,0 +1,7 @@ +--- +"chainlink": patch +--- + +#internal + +Migrate Aptos local CRE write setup to use Capabilities Registry OCR config. diff --git a/deployment/cre/ocr3/v2/changeset/configure_ocr3.go b/deployment/cre/ocr3/v2/changeset/configure_ocr3.go index 6971dfeb7bb..7ecebe544c0 100644 --- a/deployment/cre/ocr3/v2/changeset/configure_ocr3.go +++ b/deployment/cre/ocr3/v2/changeset/configure_ocr3.go @@ -25,10 +25,9 @@ type ConfigureOCR3Input struct { ContractChainSelector uint64 `json:"contractChainSelector" yaml:"contractChainSelector"` ContractQualifier string `json:"contractQualifier" yaml:"contractQualifier"` - DON contracts.DonNodeSet `json:"don" yaml:"don"` - OracleConfig *ocr3.OracleConfig `json:"oracleConfig" yaml:"oracleConfig"` - DryRun bool `json:"dryRun" yaml:"dryRun"` - ExtraSignerFamilies []string `json:"extraSignerFamilies,omitempty" yaml:"extraSignerFamilies,omitempty"` + DON contracts.DonNodeSet `json:"don" yaml:"don"` + OracleConfig *ocr3.OracleConfig `json:"oracleConfig" yaml:"oracleConfig"` + DryRun bool `json:"dryRun" yaml:"dryRun"` MCMSConfig *crecontracts.MCMSConfig `json:"mcmsConfig" yaml:"mcmsConfig"` } @@ -51,9 +50,6 @@ func (l ConfigureOCR3) VerifyPreconditions(_ cldf.Environment, input ConfigureOC if input.OracleConfig == nil { return errors.New("oracle config is required") } - if err := ocr3.ValidateExtraSignerFamilies(input.ExtraSignerFamilies); err != nil { - return fmt.Errorf("invalid extra signer families: %w", err) - } return nil } @@ -97,13 +93,12 @@ func (l ConfigureOCR3) Apply(e cldf.Environment, input ConfigureOCR3Input) (cldf Env: &e, Strategy: strategy, }, contracts.ConfigureOCR3Input{ - ContractAddress: &contractAddr, - ChainSelector: input.ContractChainSelector, - DON: input.DON, - Config: input.OracleConfig, - DryRun: input.DryRun, - ExtraSignerFamilies: input.ExtraSignerFamilies, - MCMSConfig: input.MCMSConfig, + ContractAddress: &contractAddr, + ChainSelector: input.ContractChainSelector, + DON: input.DON, + Config: input.OracleConfig, + DryRun: input.DryRun, + MCMSConfig: input.MCMSConfig, }) if err != nil { return cldf.ChangesetOutput{}, fmt.Errorf("failed to configure OCR3 contract: %w", err) diff --git a/deployment/cre/ocr3/v2/changeset/operations/contracts/configure_ocr3.go b/deployment/cre/ocr3/v2/changeset/operations/contracts/configure_ocr3.go index fd1b115bc18..ad59fc4f6d0 100644 --- a/deployment/cre/ocr3/v2/changeset/operations/contracts/configure_ocr3.go +++ b/deployment/cre/ocr3/v2/changeset/operations/contracts/configure_ocr3.go @@ -35,7 +35,6 @@ type ConfigureOCR3Input struct { DryRun bool ReportingPluginConfigOverride []byte - ExtraSignerFamilies []string MCMSConfig *contracts.MCMSConfig } @@ -73,7 +72,6 @@ var ConfigureOCR3 = operations.NewOperation[ConfigureOCR3Input, ConfigureOCR3OpO OCR3Config: input.Config, Contract: contract.Contract, DryRun: input.DryRun, - ExtraSignerFamilies: input.ExtraSignerFamilies, UseMCMS: input.UseMCMS(), Strategy: deps.Strategy, ReportingPluginConfigOverride: input.ReportingPluginConfigOverride, diff --git a/plugins/plugins.private.yaml b/plugins/plugins.private.yaml index fe49828b6c0..4d9719df253 100644 --- a/plugins/plugins.private.yaml +++ b/plugins/plugins.private.yaml @@ -23,7 +23,7 @@ plugins: installPath: "." consensus: - moduleURI: "github.com/smartcontractkit/capabilities/consensus" - gitRef: "edadc5b6025c4927e79d8fa98093c230bf5aaba6" + gitRef: "06c5ed299fe6a58ec53923f9b0fe9f696c386d4b" installPath: "." workflowevent: - enabled: false diff --git a/system-tests/lib/cre/features/aptos/aptos.go b/system-tests/lib/cre/features/aptos/aptos.go index 71f9da99b2c..523cec41ee3 100644 --- a/system-tests/lib/cre/features/aptos/aptos.go +++ b/system-tests/lib/cre/features/aptos/aptos.go @@ -10,15 +10,11 @@ import ( "github.com/Masterminds/semver/v3" pkgerrors "github.com/pkg/errors" "github.com/rs/zerolog" - chainselectors "github.com/smartcontractkit/chain-selectors" capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" kcr "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/capabilities_registry_1_1_0" - "github.com/smartcontractkit/chainlink/deployment/cre/ocr3" - creocr3changeset "github.com/smartcontractkit/chainlink/deployment/cre/ocr3/v2/changeset" - creocr3contracts "github.com/smartcontractkit/chainlink/deployment/cre/ocr3/v2/changeset/operations/contracts" keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" "github.com/smartcontractkit/chainlink/system-tests/lib/cre" crecontracts "github.com/smartcontractkit/chainlink/system-tests/lib/cre/contracts" @@ -40,7 +36,6 @@ const ( deltaStageKey = "DeltaStage" transmissionScheduleKey = "TransmissionSchedule" forwarderQualifier = "" - ocr3ContractQualifier = "aptos_capability_ocr3" zeroForwarderHex = "0x0000000000000000000000000000000000000000000000000000000000000000" defaultWriteDeltaStage = 500*time.Millisecond + 1*time.Second defaultRequestTimeout = 30 * time.Second @@ -136,10 +131,6 @@ func (a *Aptos) PostEnvStartup( if err != nil { return err } - _, _, err = crecontracts.DeployOCR3Contract(testLogger, ocr3ContractQualifier, creEnv.RegistryChainSelector, creEnv.CldfEnvironment, creEnv.ContractVersions) - if err != nil { - return fmt.Errorf("failed to deploy Aptos OCR3 contract: %w", err) - } specs, err := proposeAptosWorkerSpecs(ctx, don, dons, creEnv, nodeSet, enabledChainIDs) if err != nil { @@ -152,33 +143,6 @@ func (a *Aptos) PostEnvStartup( if err != nil { return fmt.Errorf("failed to approve Aptos jobs: %w", err) } - - workers, err := don.Workers() - if err != nil { - return fmt.Errorf("failed to collect Aptos worker nodes for OCR3 config: %w", err) - } - workerNodeIDs := make([]string, 0, len(workers)) - for _, worker := range workers { - if worker.JobDistributorDetails == nil { - return fmt.Errorf("worker %q is missing job distributor details", worker.Name) - } - workerNodeIDs = append(workerNodeIDs, worker.JobDistributorDetails.NodeID) - } - - _, err = creocr3changeset.ConfigureOCR3{}.Apply(*creEnv.CldfEnvironment, creocr3changeset.ConfigureOCR3Input{ - ContractChainSelector: creEnv.RegistryChainSelector, - ContractQualifier: ocr3ContractQualifier, - DON: creocr3contracts.DonNodeSet{ - Name: don.Name, - NodeIDs: workerNodeIDs, - }, - OracleConfig: don.ResolveORC3Config(crecontracts.DefaultChainCapabilityOCR3Config()), - DryRun: false, - ExtraSignerFamilies: cre.OCRExtraSignerFamiliesForFamily(chainselectors.FamilyAptos), - }) - if err != nil { - return fmt.Errorf("failed to configure Aptos OCR3 contract: %w", err) - } return nil } @@ -215,7 +179,7 @@ func buildCapabilityRegistrations( CapabilityType: 1, }, Config: capConfig, - UseCapRegOCRConfig: false, + UseCapRegOCRConfig: true, }) capabilityLabels = append(capabilityLabels, labelledName) capabilityToOCR3Config[labelledName] = crecontracts.DefaultChainCapabilityOCR3Config() diff --git a/system-tests/lib/cre/features/aptos/aptos_test.go b/system-tests/lib/cre/features/aptos/aptos_test.go index 101933948ef..9136fefbd1a 100644 --- a/system-tests/lib/cre/features/aptos/aptos_test.go +++ b/system-tests/lib/cre/features/aptos/aptos_test.go @@ -12,6 +12,7 @@ import ( "github.com/rs/zerolog" "github.com/stretchr/testify/require" + "github.com/Masterminds/semver/v3" "github.com/smartcontractkit/chainlink-common/keystore/corekeys/p2pkey" capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" cldfchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" @@ -19,6 +20,7 @@ import ( "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/clnode" ns "github.com/smartcontractkit/chainlink-testing-framework/framework/components/simple_node_set" + keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" "github.com/smartcontractkit/chainlink/system-tests/lib/cre" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/secrets" creblockchains "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains" @@ -328,7 +330,79 @@ func TestFindAptosChainByChainID_ErrorsOnTypeMismatch(t *testing.T) { require.ErrorContains(t, err, "unexpected type") } +func TestBuildCapabilityRegistrations_UsesCapRegOCRConfig(t *testing.T) { + don := testDonMetadataWithCapabilities(t, []string{"[Aptos]\n"}, []string{cre.WriteAptosCapability + "-4"}, cre.CapabilityConfigs{ + cre.WriteAptosCapability: { + BinaryName: "write-aptos", + Values: map[string]any{ + requestTimeoutKey: "45s", + deltaStageKey: "2500ms", + transmissionScheduleKey: "allAtOnce", + }, + }, + }) + + caps, capabilityToOCR3Config, capabilityLabels, err := buildCapabilityRegistrations( + don, + []creblockchains.Blockchain{testAptosBlockchain(4, 4457093679053095497)}, + []uint64{4}, + map[string]string{"peer-a": "0x1"}, + ) + require.NoError(t, err) + require.Len(t, caps, 1) + require.Len(t, capabilityLabels, 1) + require.True(t, caps[0].UseCapRegOCRConfig) + require.Equal(t, CapabilityLabel(4457093679053095497), caps[0].Capability.LabelledName) + require.Contains(t, capabilityToOCR3Config, capabilityLabels[0]) + require.NotNil(t, capabilityToOCR3Config[capabilityLabels[0]]) +} + +func TestNewAptosWorkerJobInput_UsesCapRegVersion(t *testing.T) { + creEnv := &cre.Environment{ + RegistryChainSelector: 111, + ContractVersions: map[cre.ContractType]*semver.Version{ + keystone_changeset.CapabilitiesRegistry.String(): semver.MustParse("2.0.0"), + }, + } + + input, err := newAptosWorkerJobInput( + creEnv, + "workflow-don", + "/usr/local/bin/aptos", + `{"chainId":"4"}`, + []string{"peer@127.0.0.1:6690"}, + 222, + 4, + ) + require.NoError(t, err) + require.Equal(t, "write-aptos-worker-4", input.JobName) + require.Equal(t, true, input.Inputs["useCapRegOCRConfig"]) + require.Equal(t, "2.0.0", input.Inputs["capRegVersion"]) + require.Equal(t, uint64(111), input.Inputs["chainSelectorEVM"]) + require.Equal(t, uint64(222), input.Inputs["chainSelectorAptos"]) + _, hasQualifier := input.Inputs["contractQualifier"] + require.False(t, hasQualifier) +} + +func TestNewAptosWorkerJobInput_ErrorsWithoutCapRegVersion(t *testing.T) { + _, err := newAptosWorkerJobInput( + &cre.Environment{}, + "workflow-don", + "/usr/local/bin/aptos", + `{"chainId":"4"}`, + nil, + 222, + 4, + ) + require.Error(t, err) + require.ErrorContains(t, err, "CapabilitiesRegistry version not found") +} + func testDonMetadata(t *testing.T, nodeConfigs ...string) *cre.DonMetadata { + return testDonMetadataWithCapabilities(t, nodeConfigs, nil, nil) +} + +func testDonMetadataWithCapabilities(t *testing.T, nodeConfigs []string, capabilities []string, capabilityConfigs cre.CapabilityConfigs) *cre.DonMetadata { t.Helper() nodeSpecs := make([]*cre.NodeSpecWithRole, len(nodeConfigs)) @@ -342,11 +416,12 @@ func testDonMetadata(t *testing.T, nodeConfigs ...string) *cre.DonMetadata { } nodeSet := &cre.NodeSet{ - Input: &ns.Input{Name: "aptos-don"}, - NodeSpecs: nodeSpecs, + Input: &ns.Input{Name: "aptos-don"}, + NodeSpecs: nodeSpecs, + Capabilities: capabilities, } - don, err := cre.NewDonMetadata(nodeSet, 1, infra.Provider{Type: infra.Docker}, nil) + don, err := cre.NewDonMetadata(nodeSet, 1, infra.Provider{Type: infra.Docker}, capabilityConfigs) require.NoError(t, err) return don } diff --git a/system-tests/lib/cre/features/aptos/aptos_workers.go b/system-tests/lib/cre/features/aptos/aptos_workers.go index a885c288a43..f97a9871df6 100644 --- a/system-tests/lib/cre/features/aptos/aptos_workers.go +++ b/system-tests/lib/cre/features/aptos/aptos_workers.go @@ -15,6 +15,7 @@ import ( crejobops "github.com/smartcontractkit/chainlink/deployment/cre/jobs/operations" jobtypes "github.com/smartcontractkit/chainlink/deployment/cre/jobs/types" "github.com/smartcontractkit/chainlink/deployment/cre/pkg/offchain" + keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" "github.com/smartcontractkit/chainlink/system-tests/lib/cre" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs/standardcapability" ) @@ -66,25 +67,9 @@ func proposeAptosWorkerSpecs( return nil, fmt.Errorf("failed to build Aptos worker config: %w", err) } - workerInput := jobs.ProposeJobSpecInput{ - Domain: offchain.ProductLabel, - Environment: cre.EnvironmentName, - DONName: don.Name, - JobName: "write-aptos-worker-" + strconv.FormatUint(chainID, 10), - ExtraLabels: map[string]string{cre.CapabilityLabelKey: flag}, - DONFilters: []offchain.TargetDONFilter{ - {Key: offchain.FilterKeyDONName, Value: don.Name}, - }, - Template: jobtypes.Aptos, - Inputs: jobtypes.JobSpecInput{ - "command": command, - "config": configStr, - "chainSelectorEVM": creEnv.RegistryChainSelector, - "chainSelectorAptos": aptosChain.ChainSelector(), - "bootstrapPeers": bootstrapPeers, - "useCapRegOCRConfig": false, - "contractQualifier": ocr3ContractQualifier, - }, + workerInput, err := newAptosWorkerJobInput(creEnv, don.Name, command, configStr, bootstrapPeers, aptosChain.ChainSelector(), chainID) + if err != nil { + return nil, err } proposer := jobs.ProposeJobSpec{} @@ -111,6 +96,42 @@ func proposeAptosWorkerSpecs( return specs, nil } +func newAptosWorkerJobInput( + creEnv *cre.Environment, + donName string, + command string, + configStr string, + bootstrapPeers []string, + aptosChainSelector uint64, + chainID uint64, +) (jobs.ProposeJobSpecInput, error) { + capRegVersion, ok := creEnv.ContractVersions[keystone_changeset.CapabilitiesRegistry.String()] + if !ok { + return jobs.ProposeJobSpecInput{}, fmt.Errorf("CapabilitiesRegistry version not found in contract versions") + } + + return jobs.ProposeJobSpecInput{ + Domain: offchain.ProductLabel, + Environment: cre.EnvironmentName, + DONName: donName, + JobName: "write-aptos-worker-" + strconv.FormatUint(chainID, 10), + ExtraLabels: map[string]string{cre.CapabilityLabelKey: flag}, + DONFilters: []offchain.TargetDONFilter{ + {Key: offchain.FilterKeyDONName, Value: donName}, + }, + Template: jobtypes.Aptos, + Inputs: jobtypes.JobSpecInput{ + "command": command, + "config": configStr, + "chainSelectorEVM": creEnv.RegistryChainSelector, + "chainSelectorAptos": aptosChainSelector, + "bootstrapPeers": bootstrapPeers, + "useCapRegOCRConfig": true, + "capRegVersion": capRegVersion.String(), + }, + }, nil +} + func donOraclePublicKeys(ctx context.Context, don *cre.Don) ([][]byte, error) { workers, err := don.Workers() if err != nil { From 2d0c4aa39beafc3f18b731545a1e3b38c8bfdfc3 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Tue, 31 Mar 2026 09:41:32 +0100 Subject: [PATCH 24/35] fix: address aptos lint issues --- system-tests/lib/cre/features/aptos/aptos_test.go | 2 +- system-tests/lib/cre/features/aptos/aptos_workers.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/system-tests/lib/cre/features/aptos/aptos_test.go b/system-tests/lib/cre/features/aptos/aptos_test.go index 9136fefbd1a..0aa8e361e4d 100644 --- a/system-tests/lib/cre/features/aptos/aptos_test.go +++ b/system-tests/lib/cre/features/aptos/aptos_test.go @@ -8,11 +8,11 @@ import ( "testing" "time" + "github.com/Masterminds/semver/v3" "github.com/pelletier/go-toml/v2" "github.com/rs/zerolog" "github.com/stretchr/testify/require" - "github.com/Masterminds/semver/v3" "github.com/smartcontractkit/chainlink-common/keystore/corekeys/p2pkey" capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" cldfchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" diff --git a/system-tests/lib/cre/features/aptos/aptos_workers.go b/system-tests/lib/cre/features/aptos/aptos_workers.go index f97a9871df6..120e51c6b58 100644 --- a/system-tests/lib/cre/features/aptos/aptos_workers.go +++ b/system-tests/lib/cre/features/aptos/aptos_workers.go @@ -3,6 +3,7 @@ package aptos import ( "context" "encoding/hex" + "errors" "fmt" "strconv" "strings" @@ -107,7 +108,7 @@ func newAptosWorkerJobInput( ) (jobs.ProposeJobSpecInput, error) { capRegVersion, ok := creEnv.ContractVersions[keystone_changeset.CapabilitiesRegistry.String()] if !ok { - return jobs.ProposeJobSpecInput{}, fmt.Errorf("CapabilitiesRegistry version not found in contract versions") + return jobs.ProposeJobSpecInput{}, errors.New("CapabilitiesRegistry version not found in contract versions") } return jobs.ProposeJobSpecInput{ From 680c73edfe9be99f00b8d15c1db700ba79e7190b Mon Sep 17 00:00:00 2001 From: cawthorne Date: Tue, 31 Mar 2026 10:02:24 +0100 Subject: [PATCH 25/35] refactor: remove unused aptos forwarder placeholder --- system-tests/lib/cre/features/aptos/aptos.go | 1 - 1 file changed, 1 deletion(-) diff --git a/system-tests/lib/cre/features/aptos/aptos.go b/system-tests/lib/cre/features/aptos/aptos.go index 523cec41ee3..932f1aa2f47 100644 --- a/system-tests/lib/cre/features/aptos/aptos.go +++ b/system-tests/lib/cre/features/aptos/aptos.go @@ -36,7 +36,6 @@ const ( deltaStageKey = "DeltaStage" transmissionScheduleKey = "TransmissionSchedule" forwarderQualifier = "" - zeroForwarderHex = "0x0000000000000000000000000000000000000000000000000000000000000000" defaultWriteDeltaStage = 500*time.Millisecond + 1*time.Second defaultRequestTimeout = 30 * time.Second ) From 6fbda290a16807a1e189d6fcb9d9e4068ad30b63 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Tue, 31 Mar 2026 10:10:54 +0100 Subject: [PATCH 26/35] test: run aptos ci on roundtrip and expected failure --- .../tests/smoke/cre/v2_aptos_capability_test.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go index a4d2dcd6ef8..34ebf5b6b8c 100644 --- a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go +++ b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go @@ -61,9 +61,9 @@ const ( var aptosForwarderVersion = semver.MustParse("1.0.0") var aptosWorkflowNameSeq uint64 -// ExecuteAptosTest runs the Aptos CRE suite with plain Aptos read coverage by -// default. Write-oriented scenarios stay available for local/manual execution -// via CRE_APTOS_SCENARIOS. +// ExecuteAptosTest runs the Aptos CRE suite with the current CI scenario set by +// default. Individual scenarios still remain available for local/manual +// execution via CRE_APTOS_SCENARIOS. func ExecuteAptosTest(t *testing.T, tenv *configuration.TestEnvironment) { executeAptosScenarios(t, tenv, resolveAptosScenarios(t)) } @@ -81,7 +81,8 @@ type aptosScenario struct { func aptosDefaultScenarios() []aptosScenario { return []aptosScenario{ - {name: "Aptos Read", run: ExecuteAptosReadTest}, + {name: "Aptos Write Read Roundtrip", run: ExecuteAptosWriteReadRoundtripTest}, + {name: "Aptos Write Expected Failure", run: ExecuteAptosWriteExpectedFailureTest}, } } @@ -92,12 +93,12 @@ func resolveAptosScenarios(t *testing.T) []aptosScenario { if raw == "" { return aptosDefaultScenarios() } + if strings.EqualFold(raw, "ci") { + t.Logf("running Aptos scenarios from %s=%q", aptosScenarioOverrideEnv, raw) + return aptosDefaultScenarios() + } available := map[string]aptosScenario{ - "ci": { - name: "Aptos Read", - run: ExecuteAptosReadTest, - }, "read": { name: "Aptos Read", run: ExecuteAptosReadTest, From fdea90f7ce0f95b68483ff176c563365e81f8879 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Tue, 31 Mar 2026 11:34:11 +0100 Subject: [PATCH 27/35] docs: clarify aptos capability config defaults --- .../scripts/cre/environment/configs/capability_defaults.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/scripts/cre/environment/configs/capability_defaults.toml b/core/scripts/cre/environment/configs/capability_defaults.toml index 6e46cd8d894..562857c5b7e 100644 --- a/core/scripts/cre/environment/configs/capability_defaults.toml +++ b/core/scripts/cre/environment/configs/capability_defaults.toml @@ -135,9 +135,10 @@ binary_name = "aptos" [capability_configs.write-aptos.values] - # ChainID and forwarder address are injected at job proposal time. + # These values build the Aptos capability config registered in CapReg. + # ChainID and forwarder address are injected separately at job proposal time. + # They are not emitted as top-level worker job-spec fields. RequestTimeout = "30s" - TransmissionSchedule = "allAtOnce" DeltaStage = "1500ms" [capability_configs.solana.values] From fc68c50f5448620432496cf2e11af447276d294d Mon Sep 17 00:00:00 2001 From: cawthorne Date: Tue, 31 Mar 2026 12:34:55 +0100 Subject: [PATCH 28/35] docs: consolidate aptos changeset notes --- .changeset/aptos-capreg-write.md | 7 ------- .changeset/aptos-local-cre-support.md | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 .changeset/aptos-capreg-write.md diff --git a/.changeset/aptos-capreg-write.md b/.changeset/aptos-capreg-write.md deleted file mode 100644 index 580dd273fd8..00000000000 --- a/.changeset/aptos-capreg-write.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"chainlink": patch ---- - -#internal - -Migrate Aptos local CRE write setup to use Capabilities Registry OCR config. diff --git a/.changeset/aptos-local-cre-support.md b/.changeset/aptos-local-cre-support.md index a2f9c282318..10b1c924824 100644 --- a/.changeset/aptos-local-cre-support.md +++ b/.changeset/aptos-local-cre-support.md @@ -4,4 +4,4 @@ #internal -Add Aptos local CRE support and read CI coverage. +Add Aptos local CRE read/write support, including Capabilities Registry OCR config for Aptos write and CI coverage for the Aptos write roundtrip and expected-failure scenarios. From 8c79697685c2e3907ffb30ead7ce339bd818e65e Mon Sep 17 00:00:00 2001 From: cawthorne Date: Tue, 31 Mar 2026 20:45:46 +0100 Subject: [PATCH 29/35] refactor: align aptos capability naming --- core/cmd/shell_local.go | 23 ++++----- .../configs/capability_defaults.toml | 4 +- .../configs/workflow-gateway-don-aptos.toml | 7 ++- .../chainlink/config_imported_aptos_key.go | 2 + core/services/chainlink/types.go | 1 + .../conversions/conversions.go | 5 +- system-tests/lib/cre/don.go | 6 +-- system-tests/lib/cre/don_test.go | 4 +- system-tests/lib/cre/environment/dons.go | 4 +- system-tests/lib/cre/features/aptos/aptos.go | 2 +- .../lib/cre/features/aptos/aptos_test.go | 8 +-- .../lib/cre/features/aptos/aptos_workers.go | 2 +- .../features/read_contract/read_contract.go | 49 +++---------------- .../read_contract/read_contract_test.go | 45 ----------------- system-tests/lib/cre/flags/flags.go | 2 +- system-tests/lib/cre/flags/flags_test.go | 2 +- system-tests/lib/cre/flags/provider.go | 6 +-- system-tests/lib/cre/types.go | 6 +-- .../smoke/cre/v2_aptos_capability_test.go | 12 ++--- 19 files changed, 55 insertions(+), 135 deletions(-) delete mode 100644 system-tests/lib/cre/features/read_contract/read_contract_test.go diff --git a/core/cmd/shell_local.go b/core/cmd/shell_local.go index 6184384ed2a..3893f53cf5c 100644 --- a/core/cmd/shell_local.go +++ b/core/cmd/shell_local.go @@ -30,7 +30,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/custmsg" "github.com/smartcontractkit/chainlink-common/pkg/logger/otelzap" "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" - coreconfig "github.com/smartcontractkit/chainlink/v2/core/config" "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" "github.com/smartcontractkit/chainlink-evm/pkg/assets" @@ -506,10 +505,6 @@ func (s *Shell) runNode(c *cli.Context) error { } } - type importedAptosKeyConfig interface { - ImportedAptosKey() coreconfig.ImportableKey - } - if s.Config.P2P().Enabled() { if s.Config.ImportedP2PKey().JSON() != "" { lggr.Debugf("Importing p2p key %s", s.Config.ImportedP2PKey().JSON()) @@ -557,15 +552,15 @@ func (s *Shell) runNode(c *cli.Context) error { } } if s.Config.AptosEnabled() { - if cfg, ok := s.Config.(importedAptosKeyConfig); ok { - if k := cfg.ImportedAptosKey(); k != nil && k.JSON() != "" { - lggr.Debug("Importing aptos key") - _, err2 := app.GetKeyStore().Aptos().Import(rootCtx, []byte(k.JSON()), k.Password()) - if errors.Is(err2, keystore.ErrKeyExists) { - lggr.Debugf("Aptos key already exists %s", k.JSON()) - } else if err2 != nil { - return s.errorOut(fmt.Errorf("error importing aptos key: %w", err2)) - } + if k := s.Config.ImportedAptosKey(); k != nil && k.JSON() != "" { + lggr.Debug("Importing aptos key") + _, err2 := app.GetKeyStore().Aptos().Import(rootCtx, []byte(k.JSON()), k.Password()) + if errors.Is(err2, keystore.ErrKeyExists) { + lggr.Debug("Aptos key already exists") + } else if err2 != nil { + return s.errorOut(fmt.Errorf("error importing aptos key: %w", err2)) + } else { + lggr.Debug("Imported aptos key") } } diff --git a/core/scripts/cre/environment/configs/capability_defaults.toml b/core/scripts/cre/environment/configs/capability_defaults.toml index 562857c5b7e..65fb75d3247 100644 --- a/core/scripts/cre/environment/configs/capability_defaults.toml +++ b/core/scripts/cre/environment/configs/capability_defaults.toml @@ -131,10 +131,10 @@ # ForwarderAddress = "0x0000000000000000000000000000000000000000" # Aptos chain capability plugin (View + WriteReport). Runtime values are injected per chain. -[capability_configs.write-aptos] +[capability_configs.aptos] binary_name = "aptos" -[capability_configs.write-aptos.values] +[capability_configs.aptos.values] # These values build the Aptos capability config registered in CapReg. # ChainID and forwarder address are injected separately at job proposal time. # They are not emitted as top-level worker job-spec fields. diff --git a/core/scripts/cre/environment/configs/workflow-gateway-don-aptos.toml b/core/scripts/cre/environment/configs/workflow-gateway-don-aptos.toml index db91c3d170f..f3c017c6d6c 100644 --- a/core/scripts/cre/environment/configs/workflow-gateway-don-aptos.toml +++ b/core/scripts/cre/environment/configs/workflow-gateway-don-aptos.toml @@ -1,4 +1,4 @@ -# Same as workflow-gateway-don.toml but with Aptos chain and Aptos read capability plumbing. +# Same as workflow-gateway-don.toml but with Aptos chain and a single Aptos capability. # Anvil 1337: registry and gateway. Aptos: local devnet (chain_id 4). Run: env config path , then env start. [[blockchains]] @@ -32,9 +32,12 @@ override_mode = "all" http_port_range_start = 10100 + # Keep the registry chain (1337) in node TOML even though this DON's chain-scoped + # capability is Aptos-only; the workflow stack still needs the EVM registry chain. supported_evm_chains = [1337] + # Workflow runtime limits still need both the EVM registry chain and the Aptos chain selector. env_vars = { CL_CRE_SETTINGS_DEFAULT = '{"PerWorkflow":{"CapabilityCallTimeout":"5m0s","ChainAllowed":{"Default":"false","Values":{"1337":"true","4457093679053095497":"true"}},"ChainWrite":{"EVM":{"GasLimit":{"Default":"5000000","Values":{"1337":"10000000"}}}}}}' } - capabilities = ["cron", "consensus", "read-contract-4", "write-aptos-4"] + capabilities = ["cron", "consensus", "aptos-4"] registry_based_launch_allowlist = ["cron-trigger@1.0.0"] [nodesets.db] diff --git a/core/services/chainlink/config_imported_aptos_key.go b/core/services/chainlink/config_imported_aptos_key.go index cadf5bb1b43..a540f1e24aa 100644 --- a/core/services/chainlink/config_imported_aptos_key.go +++ b/core/services/chainlink/config_imported_aptos_key.go @@ -2,6 +2,8 @@ package chainlink import "github.com/smartcontractkit/chainlink/v2/core/config/toml" +// Aptos key import config is node-scoped rather than chain-scoped, so unlike +// EVM/Solana imported keys it intentionally only implements ImportableKey. type importedAptosKeyConfig struct { s toml.AptosKey } diff --git a/core/services/chainlink/types.go b/core/services/chainlink/types.go index 8c2d2f7ae55..b616e78a78b 100644 --- a/core/services/chainlink/types.go +++ b/core/services/chainlink/types.go @@ -29,5 +29,6 @@ type ImportedSecretConfig interface { ImportedP2PKey() coreconfig.ImportableKey ImportedEthKeys() coreconfig.ImportableChainKeyLister ImportedSolKeys() coreconfig.ImportableChainKeyLister + ImportedAptosKey() coreconfig.ImportableKey ImportedDKGRecipientKey() coreconfig.ImportableKey } diff --git a/core/services/standardcapabilities/conversions/conversions.go b/core/services/standardcapabilities/conversions/conversions.go index 257d7c025fd..b9a233243af 100644 --- a/core/services/standardcapabilities/conversions/conversions.go +++ b/core/services/standardcapabilities/conversions/conversions.go @@ -9,8 +9,9 @@ import ( chainselectors "github.com/smartcontractkit/chain-selectors" ) -// WARNING: Hacky and brittle - used only during migration to map job specs to capability IDs -// before executing the LOOPP. When std cap job specs are deprecated, capability IDs will be known upfront. +// WARNING: Hacky and brittle - used during the current std-capability transition to map +// job commands back to capability IDs. The standard-capability delegate still needs this +// for registry-based launch allowlisting and OCR config wiring, including remote caps like Aptos. func GetCapabilityIDFromCommand(command string, config string) string { switch filepath.Base(command) { case "evm": diff --git a/system-tests/lib/cre/don.go b/system-tests/lib/cre/don.go index 63ff39b69e7..b029537b114 100644 --- a/system-tests/lib/cre/don.go +++ b/system-tests/lib/cre/don.go @@ -500,7 +500,7 @@ func createJDChainConfigs(ctx context.Context, n *Node, supportedChains []blockc account = accounts[0] } case chainselectors.FamilyAptos: - aptosAccount, aptosErr := aptosAccountForNode(ctx, n) + aptosAccount, aptosErr := aptosAccountForNode(n) if aptosErr != nil { return fmt.Errorf("failed to fetch aptos account address for node %s: %w", n.Name, aptosErr) } @@ -612,7 +612,7 @@ func listNodeChainConfigIDs(ctx context.Context, jd nodeChainConfigLister, nodeI return chainIDs, nil } -func aptosAccountForNode(ctx context.Context, n *Node) (string, error) { +func aptosAccountForNode(n *Node) (string, error) { if n.Keys != nil && n.Keys.Aptos != nil && n.Keys.Aptos.Account != "" { return n.Keys.Aptos.Account, nil } @@ -915,7 +915,7 @@ func findDonSupportedChains(donMetadata *DonMetadata, bcs []blockchains.Blockcha chainIsSolana := bc.IsFamily(chainselectors.FamilySolana) // Include all Solana chains (legacy behavior), and include any chain that is - // explicitly referenced by chain-scoped capabilities (e.g. write-aptos-4). + // explicitly referenced by chain-scoped capabilities (e.g. aptos-4). if !hasEVMChainEnabled && !hasChainCapabilityEnabled && !chainIsSolana { continue } diff --git a/system-tests/lib/cre/don_test.go b/system-tests/lib/cre/don_test.go index 5ada446dee0..44aa82de3b5 100644 --- a/system-tests/lib/cre/don_test.go +++ b/system-tests/lib/cre/don_test.go @@ -184,7 +184,7 @@ func TestAptosAccountForNode_UsesMetadataKeyWithoutCallingNodeAPI(t *testing.T) }, } - account, err := aptosAccountForNode(context.Background(), node) + account, err := aptosAccountForNode(node) require.NoError(t, err) require.Equal(t, expected, account) require.Zero(t, hits.Load(), "node API must not be called when metadata already has the Aptos key") @@ -218,7 +218,7 @@ func TestAptosAccountForNode_FallsBackToNodeAPIAndCachesKey(t *testing.T) { }, } - account, err := aptosAccountForNode(context.Background(), node) + account, err := aptosAccountForNode(node) require.NoError(t, err) expected, err := crecrypto.NormalizeAptosAccount("0x1") diff --git a/system-tests/lib/cre/environment/dons.go b/system-tests/lib/cre/environment/dons.go index 6758c0f5c2f..0c46b8dec16 100644 --- a/system-tests/lib/cre/environment/dons.go +++ b/system-tests/lib/cre/environment/dons.go @@ -214,7 +214,7 @@ func FundNodes(ctx context.Context, testLogger zerolog.Logger, dons *cre.Dons, b } for _, node := range don.Nodes { - address, addrErr := nodeAddress(ctx, node, chainFamily, bc) + address, addrErr := nodeAddress(node, chainFamily, bc) if addrErr != nil { return pkgerrors.Wrapf(addrErr, "failed to get address for node %s on chain family %s and chain %d", node.Name, chainFamily, bc.ChainID()) } @@ -237,7 +237,7 @@ func FundNodes(ctx context.Context, testLogger zerolog.Logger, dons *cre.Dons, b return nil } -func nodeAddress(ctx context.Context, node *cre.Node, chainFamily string, bc blockchains.Blockchain) (string, error) { +func nodeAddress(node *cre.Node, chainFamily string, bc blockchains.Blockchain) (string, error) { switch chainFamily { case chainselectors.FamilyEVM, chainselectors.FamilyTron: evmKey, ok := node.Keys.EVM[bc.ChainID()] diff --git a/system-tests/lib/cre/features/aptos/aptos.go b/system-tests/lib/cre/features/aptos/aptos.go index 932f1aa2f47..d4106b086bf 100644 --- a/system-tests/lib/cre/features/aptos/aptos.go +++ b/system-tests/lib/cre/features/aptos/aptos.go @@ -23,7 +23,7 @@ import ( ) const ( - flag = cre.WriteAptosCapability + flag = cre.AptosCapability forwarderContractType = "AptosForwarder" forwarderConfigVersion = 1 capabilityVersion = "1.0.0" diff --git a/system-tests/lib/cre/features/aptos/aptos_test.go b/system-tests/lib/cre/features/aptos/aptos_test.go index 0aa8e361e4d..27052a45d03 100644 --- a/system-tests/lib/cre/features/aptos/aptos_test.go +++ b/system-tests/lib/cre/features/aptos/aptos_test.go @@ -331,9 +331,9 @@ func TestFindAptosChainByChainID_ErrorsOnTypeMismatch(t *testing.T) { } func TestBuildCapabilityRegistrations_UsesCapRegOCRConfig(t *testing.T) { - don := testDonMetadataWithCapabilities(t, []string{"[Aptos]\n"}, []string{cre.WriteAptosCapability + "-4"}, cre.CapabilityConfigs{ - cre.WriteAptosCapability: { - BinaryName: "write-aptos", + don := testDonMetadataWithCapabilities(t, []string{"[Aptos]\n"}, []string{cre.AptosCapability + "-4"}, cre.CapabilityConfigs{ + cre.AptosCapability: { + BinaryName: "aptos", Values: map[string]any{ requestTimeoutKey: "45s", deltaStageKey: "2500ms", @@ -375,7 +375,7 @@ func TestNewAptosWorkerJobInput_UsesCapRegVersion(t *testing.T) { 4, ) require.NoError(t, err) - require.Equal(t, "write-aptos-worker-4", input.JobName) + require.Equal(t, "aptos-worker-4", input.JobName) require.Equal(t, true, input.Inputs["useCapRegOCRConfig"]) require.Equal(t, "2.0.0", input.Inputs["capRegVersion"]) require.Equal(t, uint64(111), input.Inputs["chainSelectorEVM"]) diff --git a/system-tests/lib/cre/features/aptos/aptos_workers.go b/system-tests/lib/cre/features/aptos/aptos_workers.go index 120e51c6b58..9c2674f667a 100644 --- a/system-tests/lib/cre/features/aptos/aptos_workers.go +++ b/system-tests/lib/cre/features/aptos/aptos_workers.go @@ -115,7 +115,7 @@ func newAptosWorkerJobInput( Domain: offchain.ProductLabel, Environment: cre.EnvironmentName, DONName: donName, - JobName: "write-aptos-worker-" + strconv.FormatUint(chainID, 10), + JobName: "aptos-worker-" + strconv.FormatUint(chainID, 10), ExtraLabels: map[string]string{cre.CapabilityLabelKey: flag}, DONFilters: []offchain.TargetDONFilter{ {Key: offchain.FilterKeyDONName, Value: donName}, diff --git a/system-tests/lib/cre/features/read_contract/read_contract.go b/system-tests/lib/cre/features/read_contract/read_contract.go index 635c948ddd4..6f3f518a1d9 100644 --- a/system-tests/lib/cre/features/read_contract/read_contract.go +++ b/system-tests/lib/cre/features/read_contract/read_contract.go @@ -26,7 +26,6 @@ import ( "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs/standardcapability" creblockchains "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains" - aptosfeature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/aptos" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/jobhelpers" ) @@ -52,32 +51,14 @@ func (o *ReadContract) PreEnvStartup( } for _, chainID := range enabledChainIDs { - bc, findErr := findBlockchainByChainID(creEnv, chainID) - if findErr != nil { - return nil, findErr - } - - labelledName, skip, labelErr := capabilityLabelForChain(don, creEnv, chainID) + labelledName, labelErr := capabilityLabelForChain(creEnv, chainID) if labelErr != nil { return nil, labelErr } - if skip { - continue - } capConfig := &capabilitiespb.CapabilityConfig{ LocalOnly: don.HasOnlyLocalCapabilities(), } - if bc.IsFamily(blockchain.FamilyAptos) { - capabilityConfig, resolveErr := cre.ResolveCapabilityConfig(don.MustNodeSet(), flag, cre.ChainCapabilityScope(chainID)) - if resolveErr != nil { - return nil, fmt.Errorf("could not resolve capability config for '%s' on chain %d: %w", flag, chainID, resolveErr) - } - capConfig, err = aptosfeature.BuildCapabilityConfig(capabilityConfig.Values, nil, don.HasOnlyLocalCapabilities()) - if err != nil { - return nil, fmt.Errorf("failed to build Aptos read capability config for chain %d: %w", chainID, err) - } - } capabilities = append(capabilities, keystone_changeset.DONCapabilityWithConfig{ Capability: kcr.CapabilitiesRegistryCapability{ @@ -94,35 +75,21 @@ func (o *ReadContract) PreEnvStartup( }, nil } -func capabilityLabelForChain(don *cre.DonMetadata, creEnv *cre.Environment, chainID uint64) (string, bool, error) { +func capabilityLabelForChain(creEnv *cre.Environment, chainID uint64) (string, error) { for _, bc := range creEnv.Blockchains { if bc.ChainID() != chainID { continue } switch { - case bc.IsFamily(blockchain.FamilyAptos): - return aptosCapabilityLabel(don, bc) case bc.IsFamily(blockchain.FamilyEVM), bc.IsFamily(blockchain.FamilyTron): - return fmt.Sprintf("read-contract-evm-%d", chainID), false, nil + return fmt.Sprintf("read-contract-evm-%d", chainID), nil default: - return "", false, fmt.Errorf("read-contract is not supported for chain family %s on chainID %d", bc.ChainFamily(), chainID) + return "", fmt.Errorf("read-contract is not supported for chain family %s on chainID %d", bc.ChainFamily(), chainID) } } - return "", false, fmt.Errorf("could not find blockchain for read-contract chainID %d", chainID) -} - -func aptosCapabilityLabel(don *cre.DonMetadata, bc blockchainOutput) (string, bool, error) { - if don.HasFlag(cre.WriteAptosCapability) { - return "", true, nil - } - return aptosfeature.CapabilityLabel(bc.ChainSelector()), false, nil -} - -type blockchainOutput interface { - ChainSelector() uint64 - ChainFamily() string + return "", fmt.Errorf("could not find blockchain for read-contract chainID %d", chainID) } const configTemplate = `{"chainId":{{printf "%d" .ChainID}},"network":"{{.NetworkFamily}}"}` @@ -156,13 +123,9 @@ func (o *ReadContract) PostEnvStartup( for i, chainID := range enabledChainIDs { group.Go(func() error { - blockchainOutput, findErr := findBlockchainByChainID(creEnv, chainID) - if findErr != nil { + if _, findErr := findBlockchainByChainID(creEnv, chainID); findErr != nil { return findErr } - if blockchainOutput.IsFamily(blockchain.FamilyAptos) && don.HasFlag(cre.WriteAptosCapability) { - return nil - } capabilityConfig, resolveErr := cre.ResolveCapabilityConfig(nodeSet, flag, cre.ChainCapabilityScope(chainID)) if resolveErr != nil { diff --git a/system-tests/lib/cre/features/read_contract/read_contract_test.go b/system-tests/lib/cre/features/read_contract/read_contract_test.go deleted file mode 100644 index ece08b19057..00000000000 --- a/system-tests/lib/cre/features/read_contract/read_contract_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package readcontract - -import ( - "testing" - - "github.com/stretchr/testify/require" - - chainselectors "github.com/smartcontractkit/chain-selectors" - - "github.com/smartcontractkit/chainlink/system-tests/lib/cre" - aptosfeature "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/aptos" -) - -type blockchainOutputStub struct { - chainSelector uint64 - chainFamily string -} - -func (s blockchainOutputStub) ChainSelector() uint64 { - return s.chainSelector -} - -func (s blockchainOutputStub) ChainFamily() string { - return s.chainFamily -} - -func TestAptosCapabilityLabel(t *testing.T) { - bc := blockchainOutputStub{chainSelector: 1, chainFamily: chainselectors.FamilyAptos} - - t.Run("skips aptos when write aptos feature owns the don", func(t *testing.T) { - don := &cre.DonMetadata{Flags: []string{cre.ReadContractCapability, cre.WriteAptosCapability}} - label, skip, err := aptosCapabilityLabel(don, bc) - require.NoError(t, err) - require.Empty(t, label) - require.True(t, skip) - }) - - t.Run("uses aptos label for read-only dons", func(t *testing.T) { - don := &cre.DonMetadata{Flags: []string{cre.ReadContractCapability}} - label, skip, err := aptosCapabilityLabel(don, bc) - require.NoError(t, err) - require.Equal(t, aptosfeature.CapabilityLabel(1), label) - require.False(t, skip) - }) -} diff --git a/system-tests/lib/cre/flags/flags.go b/system-tests/lib/cre/flags/flags.go index ab3d568bf1b..b5428d568a9 100644 --- a/system-tests/lib/cre/flags/flags.go +++ b/system-tests/lib/cre/flags/flags.go @@ -44,7 +44,7 @@ func HasFlagForAnyChain(values []string, capability string) bool { func RequiresForwarderContract(values []string, chainID uint64) bool { return HasFlagForChain(values, cre.EVMCapability, chainID) || HasFlagForChain(values, cre.WriteEVMCapability, chainID) || - HasFlagForChain(values, cre.WriteAptosCapability, chainID) || + HasFlagForChain(values, cre.AptosCapability, chainID) || HasFlagForAnyChain(values, cre.SolanaCapability) } diff --git a/system-tests/lib/cre/flags/flags_test.go b/system-tests/lib/cre/flags/flags_test.go index 32593fc67b4..230287440ec 100644 --- a/system-tests/lib/cre/flags/flags_test.go +++ b/system-tests/lib/cre/flags/flags_test.go @@ -10,7 +10,7 @@ import ( func TestRequiresForwarderContract(t *testing.T) { t.Run("returns true for aptos write capability", func(t *testing.T) { - require.True(t, RequiresForwarderContract([]string{cre.WriteAptosCapability + "-4"}, 4)) + require.True(t, RequiresForwarderContract([]string{cre.AptosCapability + "-4"}, 4)) }) t.Run("returns true for evm and solana write paths", func(t *testing.T) { diff --git a/system-tests/lib/cre/flags/provider.go b/system-tests/lib/cre/flags/provider.go index 151123f1fa2..aa450b02085 100644 --- a/system-tests/lib/cre/flags/provider.go +++ b/system-tests/lib/cre/flags/provider.go @@ -25,7 +25,7 @@ func NewDefaultCapabilityFlagsProvider() *DefaultCapbilityFlagsProvider { cre.WriteEVMCapability, cre.ReadContractCapability, cre.LogEventTriggerCapability, - cre.WriteAptosCapability, + cre.AptosCapability, }, } } @@ -59,7 +59,7 @@ func NewExtensibleCapabilityFlagsProvider(extraGlobalFlags []string) *Extensible cre.SolanaCapability, cre.ReadContractCapability, cre.LogEventTriggerCapability, - cre.WriteAptosCapability, + cre.AptosCapability, }, } } @@ -91,7 +91,7 @@ func NewSwappableCapabilityFlagsProvider() *DefaultCapbilityFlagsProvider { cre.ReadContractCapability, cre.LogEventTriggerCapability, cre.SolanaCapability, - cre.WriteAptosCapability, + cre.AptosCapability, }, } } diff --git a/system-tests/lib/cre/types.go b/system-tests/lib/cre/types.go index cac5f0457e1..b896cbe2296 100644 --- a/system-tests/lib/cre/types.go +++ b/system-tests/lib/cre/types.go @@ -73,7 +73,7 @@ const ( HTTPTriggerCapability CapabilityFlag = "http-trigger" HTTPActionCapability CapabilityFlag = "http-action" SolanaCapability CapabilityFlag = "solana" - WriteAptosCapability CapabilityFlag = "write-aptos" + AptosCapability CapabilityFlag = "aptos" // Add more capabilities as needed ) @@ -566,7 +566,7 @@ type DonMetadata struct { func NewDonMetadata(c *NodeSet, id uint64, provider infra.Provider, capabilityConfigs map[CapabilityFlag]CapabilityConfig) (*DonMetadata, error) { cfgs := make([]NodeMetadataConfig, len(c.NodeSpecs)) - aptosChainIDs, err := c.GetEnabledChainIDsForCapability(WriteAptosCapability) + aptosChainIDs, err := c.GetEnabledChainIDsForCapability(AptosCapability) if err != nil { return nil, fmt.Errorf("failed to resolve Aptos chain ids for node metadata: %w", err) } @@ -1317,7 +1317,7 @@ func (c *NodeSet) chainCapabilityIDs() []uint64 { return out } -// ChainCapabilityChainIDs returns the set of chain IDs supported by this node set's chain-scoped capabilities (e.g. read-contract-4, write-aptos-4). +// ChainCapabilityChainIDs returns the set of chain IDs supported by this node set's chain-scoped capabilities (e.g. read-contract-4, aptos-4). func (c *NodeSet) ChainCapabilityChainIDs() []uint64 { return c.chainCapabilityIDs() } diff --git a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go index 34ebf5b6b8c..eb20937444a 100644 --- a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go +++ b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go @@ -157,7 +157,7 @@ func executeAptosScenarios(t *testing.T, tenv *configuration.TestEnvironment, sc userLogsCh := make(chan *workflowevents.UserLogs, 1000) baseMessageCh := make(chan *commonevents.BaseMessage, 1000) - writeDon := findWriteAptosDonForChain(t, tenv, aptosChain.ChainID()) + writeDon := findAptosDonForChain(t, tenv, aptosChain.ChainID()) assertAptosWorkerRuntimeKeysMatchMetadata(t, writeDon) server := t_helpers.StartChipTestSink(t, t_helpers.GetPublishFn(lggr, userLogsCh, baseMessageCh)) @@ -428,7 +428,7 @@ func prepareAptosWriteScenarioWithBenchmark( require.NotEmpty(t, forwarderHex, "Aptos write test requires forwarder address for chainSelector=%d", aptosChain.ChainSelector()) require.False(t, isZeroAptosAddress(forwarderHex), "Aptos write test requires non-zero forwarder address for chainSelector=%d", aptosChain.ChainSelector()) - writeDon := findWriteAptosDonForChain(t, tenv, aptosChain.ChainID()) + writeDon := findAptosDonForChain(t, tenv, aptosChain.ChainID()) workers, workerErr := writeDon.Workers() require.NoError(t, workerErr, "failed to list Aptos write DON workers") f := (len(workers) - 1) / 3 @@ -449,15 +449,15 @@ func uniqueAptosWorkflowName(base string) string { return fmt.Sprintf("%s-%d-%d", base, time.Now().UnixNano(), atomic.AddUint64(&aptosWorkflowNameSeq, 1)) } -func findWriteAptosDonForChain(t *testing.T, tenv *configuration.TestEnvironment, chainID uint64) *crelib.Don { +func findAptosDonForChain(t *testing.T, tenv *configuration.TestEnvironment, chainID uint64) *crelib.Don { t.Helper() require.NotNil(t, tenv.Dons, "test environment DON metadata is required") for _, don := range tenv.Dons.List() { - if !don.HasFlag("write-aptos") { + if !don.HasFlag("aptos") { continue } - chainIDs, err := don.GetEnabledChainIDsForCapability("write-aptos") + chainIDs, err := don.GetEnabledChainIDsForCapability("aptos") require.NoError(t, err, "failed to read enabled chain ids for DON %q", don.Name) for _, id := range chainIDs { if id == chainID { @@ -466,7 +466,7 @@ func findWriteAptosDonForChain(t *testing.T, tenv *configuration.TestEnvironment } } - require.FailNowf(t, "missing Aptos write DON", "could not find write-aptos DON for chainID=%d", chainID) + require.FailNowf(t, "missing Aptos DON", "could not find aptos DON for chainID=%d", chainID) return nil } From f9881082de0f55c0aa5ac1701aa3e12c1ee72d5f Mon Sep 17 00:00:00 2001 From: cawthorne Date: Tue, 31 Mar 2026 20:44:15 +0100 Subject: [PATCH 30/35] feat: make imported aptos keys chain-aware --- core/cmd/shell_local.go | 13 ++-- core/config/toml/types.go | 57 ++++++++++++++++- core/services/chainlink/config_general.go | 4 +- .../chainlink/config_imported_aptos_key.go | 37 ++++++++++- .../config_imported_aptos_key_test.go | 61 ++++++++++++++++++ .../chainlink/mocks/general_config.go | 47 ++++++++++++++ core/services/chainlink/types.go | 2 +- system-tests/lib/cre/don/secrets/secrets.go | 64 ++++++++++++++----- .../lib/cre/don/secrets/secrets_test.go | 14 ++-- system-tests/lib/cre/types.go | 1 + system-tests/lib/cre/types_nodekeys_test.go | 12 ++++ 11 files changed, 279 insertions(+), 33 deletions(-) create mode 100644 core/services/chainlink/config_imported_aptos_key_test.go diff --git a/core/cmd/shell_local.go b/core/cmd/shell_local.go index 3893f53cf5c..9de5e16a2be 100644 --- a/core/cmd/shell_local.go +++ b/core/cmd/shell_local.go @@ -552,16 +552,17 @@ func (s *Shell) runNode(c *cli.Context) error { } } if s.Config.AptosEnabled() { - if k := s.Config.ImportedAptosKey(); k != nil && k.JSON() != "" { + for _, k := range s.Config.ImportedAptosKeys().List() { lggr.Debug("Importing aptos key") _, err2 := app.GetKeyStore().Aptos().Import(rootCtx, []byte(k.JSON()), k.Password()) - if errors.Is(err2, keystore.ErrKeyExists) { - lggr.Debug("Aptos key already exists") - } else if err2 != nil { + if err2 != nil { + if errors.Is(err2, keystore.ErrKeyExists) { + lggr.Debugf("Aptos key %s already exists for chain %v", k.JSON(), k.ChainDetails()) + continue + } return s.errorOut(fmt.Errorf("error importing aptos key: %w", err2)) - } else { - lggr.Debug("Imported aptos key") } + lggr.Debugf("Imported aptos key %s for chain %v", k.JSON(), k.ChainDetails()) } err2 := app.GetKeyStore().Aptos().EnsureKey(rootCtx) diff --git a/core/config/toml/types.go b/core/config/toml/types.go index e47f7c16b83..0865babfc96 100644 --- a/core/config/toml/types.go +++ b/core/config/toml/types.go @@ -146,7 +146,7 @@ type Secrets struct { Threshold ThresholdKeyShareSecrets `toml:",omitempty"` EVM EthKeys `toml:",omitempty"` // choose EVM as the TOML field name to align with relayer config convention Solana SolKeys `toml:",omitempty"` // choose Solana as the TOML field name to align with relayer config convention - Aptos AptosKey `toml:",omitempty"` + Aptos AptosKeys `toml:",omitempty"` // choose Aptos as the TOML field name to align with relayer config convention P2PKey P2PKey `toml:",omitempty"` DKGRecipientKey DKGRecipientKey `toml:",omitempty"` @@ -165,8 +165,13 @@ type SolKey struct { Password *models.Secret } +type AptosKeys struct { + Keys []*AptosKey +} + type AptosKey struct { JSON *models.Secret + ID *uint64 Password *models.Secret } @@ -251,6 +256,43 @@ func (e *SolKey) ValidateConfig() (err error) { return err } +func (a *AptosKeys) SetFrom(f *AptosKeys) error { + err := a.validateMerge(f) + if err != nil { + return err + } + if f == nil || len(f.Keys) == 0 { + return nil + } + a.Keys = make([]*AptosKey, len(f.Keys)) + copy(a.Keys, f.Keys) + return nil +} + +func (a *AptosKeys) validateMerge(f *AptosKeys) (err error) { + have := make(map[uint64]struct{}) + if a != nil && f != nil { + for _, aptosKey := range a.Keys { + have[*aptosKey.ID] = struct{}{} + } + for _, aptosKey := range f.Keys { + if _, ok := have[*aptosKey.ID]; ok { + err = errors.Join(err, configutils.ErrOverride{Name: fmt.Sprintf("AptosKeys: %d", *aptosKey.ID)}) + } + } + } + return err +} + +func (a *AptosKeys) ValidateConfig() (err error) { + for i, aptosKey := range a.Keys { + if err2 := aptosKey.ValidateConfig(); err2 != nil { + err = errors.Join(err, configutils.ErrInvalid{Name: fmt.Sprintf("AptosKeys[%d]", i), Value: aptosKey, Msg: "invalid AptosKey"}) + } + } + return err +} + func (p *AptosKey) SetFrom(f *AptosKey) (err error) { err = p.validateMerge(f) if err != nil { @@ -259,6 +301,9 @@ func (p *AptosKey) SetFrom(f *AptosKey) (err error) { if v := f.JSON; v != nil { p.JSON = v } + if v := f.ID; v != nil { + p.ID = v + } if v := f.Password; v != nil { p.Password = v } @@ -269,6 +314,9 @@ func (p *AptosKey) validateMerge(f *AptosKey) (err error) { if p.JSON != nil && f.JSON != nil { err = errors.Join(err, configutils.ErrOverride{Name: "JSON"}) } + if p.ID != nil && f.ID != nil { + err = errors.Join(err, configutils.ErrOverride{Name: "ID"}) + } if p.Password != nil && f.Password != nil { err = errors.Join(err, configutils.ErrOverride{Name: "Password"}) } @@ -276,9 +324,14 @@ func (p *AptosKey) validateMerge(f *AptosKey) (err error) { } func (p *AptosKey) ValidateConfig() (err error) { - if (p.JSON != nil) != (p.Password != nil) { + if (p.JSON != nil) != (p.Password != nil) || (p.Password != nil) != (p.ID != nil) { err = errors.Join(err, configutils.ErrInvalid{Name: "AptosKey", Value: p.JSON, Msg: "all fields must be nil or non-nil"}) } + if p.ID != nil { + if _, ok := chain_selectors.AptosChainIdToChainSelector()[*p.ID]; !ok { + err = errors.Join(err, configutils.ErrInvalid{Name: "ID", Value: p.ID, Msg: "invalid chain id"}) + } + } return err } diff --git a/core/services/chainlink/config_general.go b/core/services/chainlink/config_general.go index fc0f2a0d1ea..cf1dca6593c 100644 --- a/core/services/chainlink/config_general.go +++ b/core/services/chainlink/config_general.go @@ -565,8 +565,8 @@ func (g *generalConfig) ImportedSolKeys() coreconfig.ImportableChainKeyLister { return &importedSolKeyConfigs{s: g.secrets.Solana} } -func (g *generalConfig) ImportedAptosKey() coreconfig.ImportableKey { - return &importedAptosKeyConfig{s: g.secrets.Aptos} +func (g *generalConfig) ImportedAptosKeys() coreconfig.ImportableChainKeyLister { + return &importedAptosKeyConfigs{s: g.secrets.Aptos} } func (g *generalConfig) ImportedDKGRecipientKey() coreconfig.ImportableKey { diff --git a/core/services/chainlink/config_imported_aptos_key.go b/core/services/chainlink/config_imported_aptos_key.go index a540f1e24aa..7b612c4a619 100644 --- a/core/services/chainlink/config_imported_aptos_key.go +++ b/core/services/chainlink/config_imported_aptos_key.go @@ -1,6 +1,13 @@ package chainlink -import "github.com/smartcontractkit/chainlink/v2/core/config/toml" +import ( + "strconv" + + chain_selectors "github.com/smartcontractkit/chain-selectors" + + "github.com/smartcontractkit/chainlink/v2/core/config" + "github.com/smartcontractkit/chainlink/v2/core/config/toml" +) // Aptos key import config is node-scoped rather than chain-scoped, so unlike // EVM/Solana imported keys it intentionally only implements ImportableKey. @@ -15,9 +22,37 @@ func (t *importedAptosKeyConfig) JSON() string { return string(*t.s.JSON) } +func (t *importedAptosKeyConfig) ChainDetails() chain_selectors.ChainDetails { + if t.s.ID == nil { + return chain_selectors.ChainDetails{} + } + details, err := chain_selectors.GetChainDetailsByChainIDAndFamily(strconv.FormatUint(*t.s.ID, 10), chain_selectors.FamilyAptos) + if err != nil { + return chain_selectors.ChainDetails{} + } + return details +} + func (t *importedAptosKeyConfig) Password() string { if t.s.Password == nil { return "" } return string(*t.s.Password) } + +type importedAptosKeyConfigs struct { + s toml.AptosKeys +} + +func (t *importedAptosKeyConfigs) List() []config.ImportableChainKey { + res := make([]config.ImportableChainKey, len(t.s.Keys)) + + if len(t.s.Keys) == 0 { + return res + } + + for i, v := range t.s.Keys { + res[i] = &importedAptosKeyConfig{s: *v} + } + return res +} diff --git a/core/services/chainlink/config_imported_aptos_key_test.go b/core/services/chainlink/config_imported_aptos_key_test.go new file mode 100644 index 00000000000..3ab1b01f753 --- /dev/null +++ b/core/services/chainlink/config_imported_aptos_key_test.go @@ -0,0 +1,61 @@ +package chainlink + +import ( + "strings" + "testing" + + chain_selectors "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/require" + + commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" +) + +func TestImportedAptosKeys_List(t *testing.T) { + t.Parallel() + + secrets, err := parseSecrets(` +[Aptos] +[[Aptos.Keys]] +JSON = '{"id":"aptos-key-1"}' +ID = 4 +Password = 'pw-1' + +[[Aptos.Keys]] +JSON = '{"id":"aptos-key-2"}' +ID = 1 +Password = 'pw-2' +`) + require.NoError(t, err) + + cfg := &generalConfig{secrets: secrets} + keys := cfg.ImportedAptosKeys().List() + require.Len(t, keys, 2) + + expected4, err := chain_selectors.GetChainDetailsByChainIDAndFamily("4", chain_selectors.FamilyAptos) + require.NoError(t, err) + expected1, err := chain_selectors.GetChainDetailsByChainIDAndFamily("1", chain_selectors.FamilyAptos) + require.NoError(t, err) + + require.Equal(t, `{"id":"aptos-key-1"}`, keys[0].JSON()) + require.Equal(t, "pw-1", keys[0].Password()) + require.Equal(t, expected4, keys[0].ChainDetails()) + + require.Equal(t, `{"id":"aptos-key-2"}`, keys[1].JSON()) + require.Equal(t, "pw-2", keys[1].Password()) + require.Equal(t, expected1, keys[1].ChainDetails()) +} + +func TestImportedAptosKeys_ValidateRejectsUnknownChainID(t *testing.T) { + t.Parallel() + + var secrets Secrets + err := commonconfig.DecodeTOML(strings.NewReader(` +[Aptos] +[[Aptos.Keys]] +JSON = '{"id":"aptos-key-1"}' +ID = 999999 +Password = 'pw-1' +`), &secrets) + require.NoError(t, err) + require.ErrorContains(t, secrets.Aptos.ValidateConfig(), "invalid AptosKey") +} diff --git a/core/services/chainlink/mocks/general_config.go b/core/services/chainlink/mocks/general_config.go index 758a60b27bc..02d110263ea 100644 --- a/core/services/chainlink/mocks/general_config.go +++ b/core/services/chainlink/mocks/general_config.go @@ -974,6 +974,53 @@ func (_c *GeneralConfig_ImportedEthKeys_Call) RunAndReturn(run func() config.Imp return _c } +// ImportedAptosKeys provides a mock function with no fields +func (_m *GeneralConfig) ImportedAptosKeys() config.ImportableChainKeyLister { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ImportedAptosKeys") + } + + var r0 config.ImportableChainKeyLister + if rf, ok := ret.Get(0).(func() config.ImportableChainKeyLister); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(config.ImportableChainKeyLister) + } + } + + return r0 +} + +// GeneralConfig_ImportedAptosKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ImportedAptosKeys' +type GeneralConfig_ImportedAptosKeys_Call struct { + *mock.Call +} + +// ImportedAptosKeys is a helper method to define mock.On call +func (_e *GeneralConfig_Expecter) ImportedAptosKeys() *GeneralConfig_ImportedAptosKeys_Call { + return &GeneralConfig_ImportedAptosKeys_Call{Call: _e.mock.On("ImportedAptosKeys")} +} + +func (_c *GeneralConfig_ImportedAptosKeys_Call) Run(run func()) *GeneralConfig_ImportedAptosKeys_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *GeneralConfig_ImportedAptosKeys_Call) Return(_a0 config.ImportableChainKeyLister) *GeneralConfig_ImportedAptosKeys_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *GeneralConfig_ImportedAptosKeys_Call) RunAndReturn(run func() config.ImportableChainKeyLister) *GeneralConfig_ImportedAptosKeys_Call { + _c.Call.Return(run) + return _c +} + // ImportedP2PKey provides a mock function with no fields func (_m *GeneralConfig) ImportedP2PKey() config.ImportableKey { ret := _m.Called() diff --git a/core/services/chainlink/types.go b/core/services/chainlink/types.go index b616e78a78b..037ee88019f 100644 --- a/core/services/chainlink/types.go +++ b/core/services/chainlink/types.go @@ -29,6 +29,6 @@ type ImportedSecretConfig interface { ImportedP2PKey() coreconfig.ImportableKey ImportedEthKeys() coreconfig.ImportableChainKeyLister ImportedSolKeys() coreconfig.ImportableChainKeyLister - ImportedAptosKey() coreconfig.ImportableKey + ImportedAptosKeys() coreconfig.ImportableChainKeyLister ImportedDKGRecipientKey() coreconfig.ImportableKey } diff --git a/system-tests/lib/cre/don/secrets/secrets.go b/system-tests/lib/cre/don/secrets/secrets.go index 451e0952857..e974c349234 100644 --- a/system-tests/lib/cre/don/secrets/secrets.go +++ b/system-tests/lib/cre/don/secrets/secrets.go @@ -20,7 +20,7 @@ import ( type nodeSecret struct { EthKeys nodeEthKeyWrapper `toml:"EVM"` SolKeys nodeSolKeyWrapper `toml:"Solana"` - AptosKey nodeAptosKey `toml:"Aptos"` + AptosKeys nodeAptosKeyWrapper `toml:"Aptos"` P2PKey nodeP2PKey `toml:"P2PKey"` DKGRecipientKey nodeDKGRecipientKey `toml:"DKGRecipientKey"` @@ -48,6 +48,7 @@ type nodeP2PKey struct { type nodeAptosKey struct { JSON string `toml:"JSON"` Password string `toml:"Password"` + ChainID uint64 `toml:"ID"` } type nodeDKGRecipientKey struct { @@ -63,6 +64,10 @@ type nodeSolKeyWrapper struct { SolKeys []nodeSolKey `toml:"Keys"` } +type nodeAptosKeyWrapper struct { + AptosKeys []nodeAptosKey `toml:"Keys"` +} + type ChainFamily = string type NodeKeys struct { @@ -70,6 +75,7 @@ type NodeKeys struct { EVM map[uint64]*crypto.EVMKey Solana map[string]*crypto.SolKey Aptos *crypto.AptosKey + AptosChainIDs []uint64 P2PKey *crypto.P2PKey DKGKey *crypto.DKGRecipientKey OCR2BundleIDs map[ChainFamily]string @@ -93,9 +99,16 @@ func (n *NodeKeys) ToNodeSecretsTOML() (string, error) { } if n.Aptos != nil { - ns.AptosKey = nodeAptosKey{ - JSON: string(n.Aptos.EncryptedJSON), - Password: n.Aptos.Password, + if len(n.AptosChainIDs) == 0 { + return "", errors.New("aptos key is present but AptosChainIDs is empty") + } + ns.AptosKeys = nodeAptosKeyWrapper{} + for _, chainID := range n.AptosChainIDs { + ns.AptosKeys.AptosKeys = append(ns.AptosKeys.AptosKeys, nodeAptosKey{ + JSON: string(n.Aptos.EncryptedJSON), + Password: n.Aptos.Password, + ChainID: chainID, + }) } } @@ -141,7 +154,7 @@ type secrets struct { EVM ethKeys `toml:",omitempty"` // choose EVM as the TOML field name to align with relayer config convention P2PKey p2PKey `toml:",omitempty"` Solana solKeys `toml:",omitempty"` - Aptos aptosKey `toml:",omitempty"` + Aptos aptosKeys `toml:",omitempty"` DKGRecipientKey dkgRecipientKey `toml:",omitempty"` } @@ -155,8 +168,13 @@ type dkgRecipientKey struct { Password *string } +type aptosKeys struct { + Keys []*aptosKey +} + type aptosKey struct { JSON *string + ID *uint64 Password *string } @@ -282,24 +300,34 @@ func ImportNodeKeys(secretsToml string) (*NodeKeys, error) { PeerID: *p, } - if sSecrets.Aptos.JSON != nil { - aptosJSON := strings.TrimSpace(*sSecrets.Aptos.JSON) - if aptosJSON == "" { - sSecrets.Aptos.JSON = nil - sSecrets.Aptos.Password = nil + for i := range sSecrets.Aptos.Keys { + if sSecrets.Aptos.Keys[i].JSON != nil { + aptosJSON := strings.TrimSpace(*sSecrets.Aptos.Keys[i].JSON) + if aptosJSON == "" { + sSecrets.Aptos.Keys[i].JSON = nil + sSecrets.Aptos.Keys[i].Password = nil + sSecrets.Aptos.Keys[i].ID = nil + } } } - if sSecrets.Aptos.JSON != nil { - if sSecrets.Aptos.Password == nil { + var importedAptosAccount string + for _, importedKey := range sSecrets.Aptos.Keys { + if importedKey.JSON == nil { + continue + } + if importedKey.Password == nil { return nil, errors.New("aptos key password is nil") } - aptosPassword := strings.TrimSpace(*sSecrets.Aptos.Password) + if importedKey.ID == nil { + return nil, errors.New("aptos key chain id is nil") + } + aptosPassword := strings.TrimSpace(*importedKey.Password) if aptosPassword == "" { return nil, errors.New("aptos key password is empty") } - aptosKeyValue, err := aptoskey.FromEncryptedJSON([]byte(*sSecrets.Aptos.JSON), aptosPassword) + aptosKeyValue, err := aptoskey.FromEncryptedJSON([]byte(*importedKey.JSON), aptosPassword) if err != nil { return nil, errors.Wrap(err, "failed to decrypt aptos key from encrypted JSON") } @@ -308,9 +336,13 @@ func ImportNodeKeys(secretsToml string) (*NodeKeys, error) { if err != nil { return nil, errors.Wrap(err, "failed to normalize aptos account") } - + if importedAptosAccount != "" && importedAptosAccount != account { + return nil, errors.New("multiple distinct imported Aptos keys are not supported in CRE node metadata") + } + importedAptosAccount = account + keys.AptosChainIDs = append(keys.AptosChainIDs, *importedKey.ID) keys.Aptos = &crypto.AptosKey{ - EncryptedJSON: []byte(*sSecrets.Aptos.JSON), + EncryptedJSON: []byte(*importedKey.JSON), PublicKey: aptosKeyValue.PublicKeyStr(), Account: account, Password: aptosPassword, diff --git a/system-tests/lib/cre/don/secrets/secrets_test.go b/system-tests/lib/cre/don/secrets/secrets_test.go index 7b2744d3e7c..755d010af43 100644 --- a/system-tests/lib/cre/don/secrets/secrets_test.go +++ b/system-tests/lib/cre/don/secrets/secrets_test.go @@ -1,6 +1,7 @@ package secrets import ( + "strings" "testing" "github.com/stretchr/testify/require" @@ -19,19 +20,22 @@ func TestNodeKeysAptosSecretsRoundTrip(t *testing.T) { require.NoError(t, err) keys := &NodeKeys{ - Aptos: aptosKey, - P2PKey: p2pKey, - DKGKey: dkgKey, - EVM: map[uint64]*crypto.EVMKey{}, - Solana: map[string]*crypto.SolKey{}, + Aptos: aptosKey, + AptosChainIDs: []uint64{4, 5}, + P2PKey: p2pKey, + DKGKey: dkgKey, + EVM: map[uint64]*crypto.EVMKey{}, + Solana: map[string]*crypto.SolKey{}, } secretsTOML, err := keys.ToNodeSecretsTOML() require.NoError(t, err) + require.Equal(t, 2, strings.Count(secretsTOML, "[[Aptos.Keys]]")) imported, err := ImportNodeKeys(secretsTOML) require.NoError(t, err) require.NotNil(t, imported.Aptos) + require.ElementsMatch(t, []uint64{4, 5}, imported.AptosChainIDs) require.Equal(t, aptosKey.Account, imported.Aptos.Account) require.Equal(t, aptosKey.PublicKey, imported.Aptos.PublicKey) require.Equal(t, aptosKey.Password, imported.Aptos.Password) diff --git a/system-tests/lib/cre/types.go b/system-tests/lib/cre/types.go index b896cbe2296..53dba9f03d3 100644 --- a/system-tests/lib/cre/types.go +++ b/system-tests/lib/cre/types.go @@ -1514,6 +1514,7 @@ func NewNodeKeys(input NodeKeyInput) (*secrets.NodeKeys, error) { return nil, fmt.Errorf("failed to generate Aptos key: %w", err) } out.Aptos = k + out.AptosChainIDs = append([]uint64(nil), input.AptosChainIDs...) } framework.L.Debug(). diff --git a/system-tests/lib/cre/types_nodekeys_test.go b/system-tests/lib/cre/types_nodekeys_test.go index 662a288fb04..08af4585b48 100644 --- a/system-tests/lib/cre/types_nodekeys_test.go +++ b/system-tests/lib/cre/types_nodekeys_test.go @@ -56,3 +56,15 @@ func TestNewNodeKeys_RejectsMissingImportedAptosSecretWhenAptosEnabled(t *testin }) require.ErrorContains(t, err, "missing an Aptos key") } + +func TestNewNodeKeys_PreservesAptosChainIDs(t *testing.T) { + t.Parallel() + + keys, err := NewNodeKeys(NodeKeyInput{ + AptosChainIDs: []uint64{4, 5}, + Password: "dev-password", + }) + require.NoError(t, err) + require.NotNil(t, keys.Aptos) + require.ElementsMatch(t, []uint64{4, 5}, keys.AptosChainIDs) +} From 8f941c79b0c2adb18c18e944cf2098d527352c6f Mon Sep 17 00:00:00 2001 From: cawthorne Date: Tue, 31 Mar 2026 21:16:15 +0100 Subject: [PATCH 31/35] docs: clarify imported aptos key adapter --- .../cre/environment/configs/workflow-gateway-don-aptos.toml | 2 +- core/services/chainlink/config_imported_aptos_key.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/scripts/cre/environment/configs/workflow-gateway-don-aptos.toml b/core/scripts/cre/environment/configs/workflow-gateway-don-aptos.toml index f3c017c6d6c..2748039bff0 100644 --- a/core/scripts/cre/environment/configs/workflow-gateway-don-aptos.toml +++ b/core/scripts/cre/environment/configs/workflow-gateway-don-aptos.toml @@ -36,7 +36,7 @@ # capability is Aptos-only; the workflow stack still needs the EVM registry chain. supported_evm_chains = [1337] # Workflow runtime limits still need both the EVM registry chain and the Aptos chain selector. - env_vars = { CL_CRE_SETTINGS_DEFAULT = '{"PerWorkflow":{"CapabilityCallTimeout":"5m0s","ChainAllowed":{"Default":"false","Values":{"1337":"true","4457093679053095497":"true"}},"ChainWrite":{"EVM":{"GasLimit":{"Default":"5000000","Values":{"1337":"10000000"}}}}}}' } + env_vars = { CL_CRE_SETTINGS_DEFAULT = '{"PerWorkflow":{"CapabilityCallTimeout":"5m0s","ChainAllowed":{"Default":"false","Values":{"1337":"true","4457093679053095497":"true"}}}}' } capabilities = ["cron", "consensus", "aptos-4"] registry_based_launch_allowlist = ["cron-trigger@1.0.0"] diff --git a/core/services/chainlink/config_imported_aptos_key.go b/core/services/chainlink/config_imported_aptos_key.go index 7b612c4a619..533a5694a4e 100644 --- a/core/services/chainlink/config_imported_aptos_key.go +++ b/core/services/chainlink/config_imported_aptos_key.go @@ -9,8 +9,8 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/config/toml" ) -// Aptos key import config is node-scoped rather than chain-scoped, so unlike -// EVM/Solana imported keys it intentionally only implements ImportableKey. +// importedAptosKeyConfig adapts a single chain-aware Aptos imported key entry +// to the shared ImportableChainKey interface used during keystore startup. type importedAptosKeyConfig struct { s toml.AptosKey } From a7259f17723890e65972998b89124c0a285131e6 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Tue, 31 Mar 2026 21:55:47 +0100 Subject: [PATCH 32/35] test: regenerate aptos config mocks --- .../config_imported_aptos_key_test.go | 17 +++- .../chainlink/mocks/general_config.go | 78 +++++++++---------- 2 files changed, 52 insertions(+), 43 deletions(-) diff --git a/core/services/chainlink/config_imported_aptos_key_test.go b/core/services/chainlink/config_imported_aptos_key_test.go index 3ab1b01f753..2751315ed4c 100644 --- a/core/services/chainlink/config_imported_aptos_key_test.go +++ b/core/services/chainlink/config_imported_aptos_key_test.go @@ -13,7 +13,7 @@ import ( func TestImportedAptosKeys_List(t *testing.T) { t.Parallel() - secrets, err := parseSecrets(` + secrets, err := parseImportedAptosSecrets(` [Aptos] [[Aptos.Keys]] JSON = '{"id":"aptos-key-1"}' @@ -36,11 +36,11 @@ Password = 'pw-2' expected1, err := chain_selectors.GetChainDetailsByChainIDAndFamily("1", chain_selectors.FamilyAptos) require.NoError(t, err) - require.Equal(t, `{"id":"aptos-key-1"}`, keys[0].JSON()) + require.JSONEq(t, `{"id":"aptos-key-1"}`, keys[0].JSON()) require.Equal(t, "pw-1", keys[0].Password()) require.Equal(t, expected4, keys[0].ChainDetails()) - require.Equal(t, `{"id":"aptos-key-2"}`, keys[1].JSON()) + require.JSONEq(t, `{"id":"aptos-key-2"}`, keys[1].JSON()) require.Equal(t, "pw-2", keys[1].Password()) require.Equal(t, expected1, keys[1].ChainDetails()) } @@ -55,7 +55,16 @@ func TestImportedAptosKeys_ValidateRejectsUnknownChainID(t *testing.T) { JSON = '{"id":"aptos-key-1"}' ID = 999999 Password = 'pw-1' -`), &secrets) + `), &secrets) require.NoError(t, err) require.ErrorContains(t, secrets.Aptos.ValidateConfig(), "invalid AptosKey") } + +func parseImportedAptosSecrets(secretsTOML string) (*Secrets, error) { + var secrets Secrets + if err := commonconfig.DecodeTOML(strings.NewReader(secretsTOML), &secrets); err != nil { + return nil, err + } + + return &secrets, nil +} diff --git a/core/services/chainlink/mocks/general_config.go b/core/services/chainlink/mocks/general_config.go index 02d110263ea..f39817c63c0 100644 --- a/core/services/chainlink/mocks/general_config.go +++ b/core/services/chainlink/mocks/general_config.go @@ -880,106 +880,106 @@ func (_c *GeneralConfig_FluxMonitor_Call) RunAndReturn(run func() config.FluxMon return _c } -// ImportedDKGRecipientKey provides a mock function with no fields -func (_m *GeneralConfig) ImportedDKGRecipientKey() config.ImportableKey { +// ImportedAptosKeys provides a mock function with no fields +func (_m *GeneralConfig) ImportedAptosKeys() config.ImportableChainKeyLister { ret := _m.Called() if len(ret) == 0 { - panic("no return value specified for ImportedDKGRecipientKey") + panic("no return value specified for ImportedAptosKeys") } - var r0 config.ImportableKey - if rf, ok := ret.Get(0).(func() config.ImportableKey); ok { + var r0 config.ImportableChainKeyLister + if rf, ok := ret.Get(0).(func() config.ImportableChainKeyLister); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(config.ImportableKey) + r0 = ret.Get(0).(config.ImportableChainKeyLister) } } return r0 } -// GeneralConfig_ImportedDKGRecipientKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ImportedDKGRecipientKey' -type GeneralConfig_ImportedDKGRecipientKey_Call struct { +// GeneralConfig_ImportedAptosKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ImportedAptosKeys' +type GeneralConfig_ImportedAptosKeys_Call struct { *mock.Call } -// ImportedDKGRecipientKey is a helper method to define mock.On call -func (_e *GeneralConfig_Expecter) ImportedDKGRecipientKey() *GeneralConfig_ImportedDKGRecipientKey_Call { - return &GeneralConfig_ImportedDKGRecipientKey_Call{Call: _e.mock.On("ImportedDKGRecipientKey")} +// ImportedAptosKeys is a helper method to define mock.On call +func (_e *GeneralConfig_Expecter) ImportedAptosKeys() *GeneralConfig_ImportedAptosKeys_Call { + return &GeneralConfig_ImportedAptosKeys_Call{Call: _e.mock.On("ImportedAptosKeys")} } -func (_c *GeneralConfig_ImportedDKGRecipientKey_Call) Run(run func()) *GeneralConfig_ImportedDKGRecipientKey_Call { +func (_c *GeneralConfig_ImportedAptosKeys_Call) Run(run func()) *GeneralConfig_ImportedAptosKeys_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *GeneralConfig_ImportedDKGRecipientKey_Call) Return(_a0 config.ImportableKey) *GeneralConfig_ImportedDKGRecipientKey_Call { +func (_c *GeneralConfig_ImportedAptosKeys_Call) Return(_a0 config.ImportableChainKeyLister) *GeneralConfig_ImportedAptosKeys_Call { _c.Call.Return(_a0) return _c } -func (_c *GeneralConfig_ImportedDKGRecipientKey_Call) RunAndReturn(run func() config.ImportableKey) *GeneralConfig_ImportedDKGRecipientKey_Call { +func (_c *GeneralConfig_ImportedAptosKeys_Call) RunAndReturn(run func() config.ImportableChainKeyLister) *GeneralConfig_ImportedAptosKeys_Call { _c.Call.Return(run) return _c } -// ImportedEthKeys provides a mock function with no fields -func (_m *GeneralConfig) ImportedEthKeys() config.ImportableChainKeyLister { +// ImportedDKGRecipientKey provides a mock function with no fields +func (_m *GeneralConfig) ImportedDKGRecipientKey() config.ImportableKey { ret := _m.Called() if len(ret) == 0 { - panic("no return value specified for ImportedEthKeys") + panic("no return value specified for ImportedDKGRecipientKey") } - var r0 config.ImportableChainKeyLister - if rf, ok := ret.Get(0).(func() config.ImportableChainKeyLister); ok { + var r0 config.ImportableKey + if rf, ok := ret.Get(0).(func() config.ImportableKey); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(config.ImportableChainKeyLister) + r0 = ret.Get(0).(config.ImportableKey) } } return r0 } -// GeneralConfig_ImportedEthKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ImportedEthKeys' -type GeneralConfig_ImportedEthKeys_Call struct { +// GeneralConfig_ImportedDKGRecipientKey_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ImportedDKGRecipientKey' +type GeneralConfig_ImportedDKGRecipientKey_Call struct { *mock.Call } -// ImportedEthKeys is a helper method to define mock.On call -func (_e *GeneralConfig_Expecter) ImportedEthKeys() *GeneralConfig_ImportedEthKeys_Call { - return &GeneralConfig_ImportedEthKeys_Call{Call: _e.mock.On("ImportedEthKeys")} +// ImportedDKGRecipientKey is a helper method to define mock.On call +func (_e *GeneralConfig_Expecter) ImportedDKGRecipientKey() *GeneralConfig_ImportedDKGRecipientKey_Call { + return &GeneralConfig_ImportedDKGRecipientKey_Call{Call: _e.mock.On("ImportedDKGRecipientKey")} } -func (_c *GeneralConfig_ImportedEthKeys_Call) Run(run func()) *GeneralConfig_ImportedEthKeys_Call { +func (_c *GeneralConfig_ImportedDKGRecipientKey_Call) Run(run func()) *GeneralConfig_ImportedDKGRecipientKey_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *GeneralConfig_ImportedEthKeys_Call) Return(_a0 config.ImportableChainKeyLister) *GeneralConfig_ImportedEthKeys_Call { +func (_c *GeneralConfig_ImportedDKGRecipientKey_Call) Return(_a0 config.ImportableKey) *GeneralConfig_ImportedDKGRecipientKey_Call { _c.Call.Return(_a0) return _c } -func (_c *GeneralConfig_ImportedEthKeys_Call) RunAndReturn(run func() config.ImportableChainKeyLister) *GeneralConfig_ImportedEthKeys_Call { +func (_c *GeneralConfig_ImportedDKGRecipientKey_Call) RunAndReturn(run func() config.ImportableKey) *GeneralConfig_ImportedDKGRecipientKey_Call { _c.Call.Return(run) return _c } -// ImportedAptosKeys provides a mock function with no fields -func (_m *GeneralConfig) ImportedAptosKeys() config.ImportableChainKeyLister { +// ImportedEthKeys provides a mock function with no fields +func (_m *GeneralConfig) ImportedEthKeys() config.ImportableChainKeyLister { ret := _m.Called() if len(ret) == 0 { - panic("no return value specified for ImportedAptosKeys") + panic("no return value specified for ImportedEthKeys") } var r0 config.ImportableChainKeyLister @@ -994,29 +994,29 @@ func (_m *GeneralConfig) ImportedAptosKeys() config.ImportableChainKeyLister { return r0 } -// GeneralConfig_ImportedAptosKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ImportedAptosKeys' -type GeneralConfig_ImportedAptosKeys_Call struct { +// GeneralConfig_ImportedEthKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ImportedEthKeys' +type GeneralConfig_ImportedEthKeys_Call struct { *mock.Call } -// ImportedAptosKeys is a helper method to define mock.On call -func (_e *GeneralConfig_Expecter) ImportedAptosKeys() *GeneralConfig_ImportedAptosKeys_Call { - return &GeneralConfig_ImportedAptosKeys_Call{Call: _e.mock.On("ImportedAptosKeys")} +// ImportedEthKeys is a helper method to define mock.On call +func (_e *GeneralConfig_Expecter) ImportedEthKeys() *GeneralConfig_ImportedEthKeys_Call { + return &GeneralConfig_ImportedEthKeys_Call{Call: _e.mock.On("ImportedEthKeys")} } -func (_c *GeneralConfig_ImportedAptosKeys_Call) Run(run func()) *GeneralConfig_ImportedAptosKeys_Call { +func (_c *GeneralConfig_ImportedEthKeys_Call) Run(run func()) *GeneralConfig_ImportedEthKeys_Call { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *GeneralConfig_ImportedAptosKeys_Call) Return(_a0 config.ImportableChainKeyLister) *GeneralConfig_ImportedAptosKeys_Call { +func (_c *GeneralConfig_ImportedEthKeys_Call) Return(_a0 config.ImportableChainKeyLister) *GeneralConfig_ImportedEthKeys_Call { _c.Call.Return(_a0) return _c } -func (_c *GeneralConfig_ImportedAptosKeys_Call) RunAndReturn(run func() config.ImportableChainKeyLister) *GeneralConfig_ImportedAptosKeys_Call { +func (_c *GeneralConfig_ImportedEthKeys_Call) RunAndReturn(run func() config.ImportableChainKeyLister) *GeneralConfig_ImportedEthKeys_Call { _c.Call.Return(run) return _c } From 61b58f18a968adb1775dd01acf96d8499d699d27 Mon Sep 17 00:00:00 2001 From: cawthorne Date: Tue, 31 Mar 2026 22:27:45 +0100 Subject: [PATCH 33/35] refactor: drop read contract aptos residue --- .../features/read_contract/read_contract.go | 53 ++----------------- 1 file changed, 5 insertions(+), 48 deletions(-) diff --git a/system-tests/lib/cre/features/read_contract/read_contract.go b/system-tests/lib/cre/features/read_contract/read_contract.go index 6f3f518a1d9..f5b198aae53 100644 --- a/system-tests/lib/cre/features/read_contract/read_contract.go +++ b/system-tests/lib/cre/features/read_contract/read_contract.go @@ -14,7 +14,6 @@ import ( capabilitiespb "github.com/smartcontractkit/chainlink-common/pkg/capabilities/pb" kcr "github.com/smartcontractkit/chainlink-evm/gethwrappers/keystone/generated/capabilities_registry_1_1_0" - "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" cre_jobs "github.com/smartcontractkit/chainlink/deployment/cre/jobs" cre_jobs_ops "github.com/smartcontractkit/chainlink/deployment/cre/jobs/operations" job_types "github.com/smartcontractkit/chainlink/deployment/cre/jobs/types" @@ -25,7 +24,6 @@ import ( credon "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs/standardcapability" - creblockchains "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/jobhelpers" ) @@ -51,22 +49,15 @@ func (o *ReadContract) PreEnvStartup( } for _, chainID := range enabledChainIDs { - labelledName, labelErr := capabilityLabelForChain(creEnv, chainID) - if labelErr != nil { - return nil, labelErr - } - - capConfig := &capabilitiespb.CapabilityConfig{ - LocalOnly: don.HasOnlyLocalCapabilities(), - } - capabilities = append(capabilities, keystone_changeset.DONCapabilityWithConfig{ Capability: kcr.CapabilitiesRegistryCapability{ - LabelledName: labelledName, + LabelledName: fmt.Sprintf("read-contract-evm-%d", chainID), Version: "1.0.0", - CapabilityType: 1, + CapabilityType: 1, // ACTION + }, + Config: &capabilitiespb.CapabilityConfig{ + LocalOnly: don.HasOnlyLocalCapabilities(), }, - Config: capConfig, }) } @@ -75,23 +66,6 @@ func (o *ReadContract) PreEnvStartup( }, nil } -func capabilityLabelForChain(creEnv *cre.Environment, chainID uint64) (string, error) { - for _, bc := range creEnv.Blockchains { - if bc.ChainID() != chainID { - continue - } - - switch { - case bc.IsFamily(blockchain.FamilyEVM), bc.IsFamily(blockchain.FamilyTron): - return fmt.Sprintf("read-contract-evm-%d", chainID), nil - default: - return "", fmt.Errorf("read-contract is not supported for chain family %s on chainID %d", bc.ChainFamily(), chainID) - } - } - - return "", fmt.Errorf("could not find blockchain for read-contract chainID %d", chainID) -} - const configTemplate = `{"chainId":{{printf "%d" .ChainID}},"network":"{{.NetworkFamily}}"}` func (o *ReadContract) PostEnvStartup( @@ -123,10 +97,6 @@ func (o *ReadContract) PostEnvStartup( for i, chainID := range enabledChainIDs { group.Go(func() error { - if _, findErr := findBlockchainByChainID(creEnv, chainID); findErr != nil { - return findErr - } - capabilityConfig, resolveErr := cre.ResolveCapabilityConfig(nodeSet, flag, cre.ChainCapabilityScope(chainID)) if resolveErr != nil { return fmt.Errorf("could not resolve capability config for '%s' on chain %d: %w", flag, chainID, resolveErr) @@ -212,9 +182,6 @@ func (o *ReadContract) PostEnvStartup( if mErr != nil { return mErr } - if len(specs) == 0 { - return nil - } approveErr := jobs.Approve(ctx, creEnv.CldfEnvironment.Offchain, dons, specs) if approveErr != nil { @@ -223,13 +190,3 @@ func (o *ReadContract) PostEnvStartup( return nil } - -func findBlockchainByChainID(creEnv *cre.Environment, chainID uint64) (creblockchains.Blockchain, error) { - for _, bc := range creEnv.Blockchains { - if bc.ChainID() == chainID { - return bc, nil - } - } - - return nil, fmt.Errorf("could not find blockchain for read-contract chainID %d", chainID) -} From b8c1d30a296c5098c96c90cb9a15b01fa8d6c84d Mon Sep 17 00:00:00 2001 From: cawthorne Date: Wed, 1 Apr 2026 10:08:53 +0100 Subject: [PATCH 34/35] test: speed up aptos cre suite --- .../lib/cre/features/aptos/aptos_workers.go | 124 ++++++++------ .../smoke/cre/v2_aptos_capability_test.go | 157 ++++++++++++------ 2 files changed, 183 insertions(+), 98 deletions(-) diff --git a/system-tests/lib/cre/features/aptos/aptos_workers.go b/system-tests/lib/cre/features/aptos/aptos_workers.go index 9c2674f667a..2c328b00c32 100644 --- a/system-tests/lib/cre/features/aptos/aptos_workers.go +++ b/system-tests/lib/cre/features/aptos/aptos_workers.go @@ -5,12 +5,14 @@ import ( "encoding/hex" "errors" "fmt" + "maps" "strconv" "strings" "dario.cat/mergo" pkgerrors "github.com/pkg/errors" chainselectors "github.com/smartcontractkit/chain-selectors" + "golang.org/x/sync/errgroup" "github.com/smartcontractkit/chainlink/deployment/cre/jobs" crejobops "github.com/smartcontractkit/chainlink/deployment/cre/jobs/operations" @@ -19,6 +21,7 @@ import ( keystone_changeset "github.com/smartcontractkit/chainlink/deployment/keystone/changeset" "github.com/smartcontractkit/chainlink/system-tests/lib/cre" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/don/jobs/standardcapability" + "github.com/smartcontractkit/chainlink/system-tests/lib/cre/features/jobhelpers" ) func proposeAptosWorkerSpecs( @@ -29,71 +32,90 @@ func proposeAptosWorkerSpecs( nodeSet cre.NodeSetWithCapabilityConfigs, enabledChainIDs []uint64, ) (map[string][]string, error) { - specs := make(map[string][]string) bootstrapPeers, err := bootstrapPeersForDons(dons) if err != nil { return nil, err } - for _, chainID := range enabledChainIDs { - aptosChain, err := findAptosChainByChainID(creEnv.Blockchains, chainID) - if err != nil { - return nil, err - } + workerMetadata, err := don.Metadata().Workers() + if err != nil { + return nil, fmt.Errorf("failed to collect Aptos worker metadata for DON %q: %w", don.Name, err) + } + p2pToTransmitterMap, err := p2pToTransmitterMapForWorkers(workerMetadata) + if err != nil { + return nil, fmt.Errorf("failed to collect Aptos worker transmitters for DON %q: %w", don.Name, err) + } - capabilityConfig, err := cre.ResolveCapabilityConfig(nodeSet, flag, cre.ChainCapabilityScope(chainID)) - if err != nil { - return nil, fmt.Errorf("could not resolve capability config for '%s' on chain %d: %w", flag, chainID, err) - } - command, err := standardcapability.GetCommand(capabilityConfig.BinaryName) - if err != nil { - return nil, pkgerrors.Wrap(err, "failed to get command for Aptos capability") - } + results := make([]map[string][]string, len(enabledChainIDs)) + group, _ := errgroup.WithContext(ctx) + group.SetLimit(jobhelpers.Parallelism(len(enabledChainIDs))) - forwarderAddress := mustForwarderAddress(creEnv.CldfEnvironment.DataStore, aptosChain.ChainSelector()) - workerMetadata, err := don.Metadata().Workers() - if err != nil { - return nil, fmt.Errorf("failed to collect Aptos worker metadata for DON %q: %w", don.Name, err) - } - p2pToTransmitterMap, err := p2pToTransmitterMapForWorkers(workerMetadata) - if err != nil { - return nil, fmt.Errorf("failed to collect Aptos worker transmitters for DON %q: %w", don.Name, err) - } - methodSettings, err := resolveMethodConfigSettings(capabilityConfig.Values) - if err != nil { - return nil, fmt.Errorf("failed to resolve Aptos method config settings for chain %d: %w", chainID, err) - } - configStr, err := buildWorkerConfigJSON(chainID, forwarderAddress, methodSettings, p2pToTransmitterMap, true) - if err != nil { - return nil, fmt.Errorf("failed to build Aptos worker config: %w", err) - } + for i, chainID := range enabledChainIDs { + i := i + chainID := chainID + group.Go(func() error { + aptosChain, err := findAptosChainByChainID(creEnv.Blockchains, chainID) + if err != nil { + return err + } - workerInput, err := newAptosWorkerJobInput(creEnv, don.Name, command, configStr, bootstrapPeers, aptosChain.ChainSelector(), chainID) - if err != nil { - return nil, err - } + capabilityConfig, err := cre.ResolveCapabilityConfig(nodeSet, flag, cre.ChainCapabilityScope(chainID)) + if err != nil { + return fmt.Errorf("could not resolve capability config for '%s' on chain %d: %w", flag, chainID, err) + } + command, err := standardcapability.GetCommand(capabilityConfig.BinaryName) + if err != nil { + return pkgerrors.Wrap(err, "failed to get command for Aptos capability") + } - proposer := jobs.ProposeJobSpec{} - err = proposer.VerifyPreconditions(*creEnv.CldfEnvironment, workerInput) - if err != nil { - return nil, fmt.Errorf("precondition verification failed for Aptos worker job: %w", err) - } - workerReport, err := proposer.Apply(*creEnv.CldfEnvironment, workerInput) - if err != nil { - return nil, fmt.Errorf("failed to propose Aptos worker job spec: %w", err) - } + forwarderAddress := mustForwarderAddress(creEnv.CldfEnvironment.DataStore, aptosChain.ChainSelector()) + methodSettings, err := resolveMethodConfigSettings(capabilityConfig.Values) + if err != nil { + return fmt.Errorf("failed to resolve Aptos method config settings for chain %d: %w", chainID, err) + } + configStr, err := buildWorkerConfigJSON(chainID, forwarderAddress, methodSettings, p2pToTransmitterMap, true) + if err != nil { + return fmt.Errorf("failed to build Aptos worker config: %w", err) + } - for _, report := range workerReport.Reports { - out, ok := report.Output.(crejobops.ProposeStandardCapabilityJobOutput) - if !ok { - return nil, fmt.Errorf("unable to cast to ProposeStandardCapabilityJobOutput, actual type: %T", report.Output) + workerInput, err := newAptosWorkerJobInput(creEnv, don.Name, command, configStr, bootstrapPeers, aptosChain.ChainSelector(), chainID) + if err != nil { + return err } - if err := mergo.Merge(&specs, out.Specs, mergo.WithAppendSlice); err != nil { - return nil, fmt.Errorf("failed to merge Aptos worker job specs: %w", err) + + proposer := jobs.ProposeJobSpec{} + if err := proposer.VerifyPreconditions(*creEnv.CldfEnvironment, workerInput); err != nil { + return fmt.Errorf("precondition verification failed for Aptos worker job: %w", err) } - } + workerReport, err := proposer.Apply(*creEnv.CldfEnvironment, workerInput) + if err != nil { + return fmt.Errorf("failed to propose Aptos worker job spec: %w", err) + } + + mergedSpecs := make(map[string][]string) + for _, report := range workerReport.Reports { + out, ok := report.Output.(crejobops.ProposeStandardCapabilityJobOutput) + if !ok { + return fmt.Errorf("unable to cast to ProposeStandardCapabilityJobOutput, actual type: %T", report.Output) + } + if err := mergo.Merge(&mergedSpecs, out.Specs, mergo.WithAppendSlice); err != nil { + return fmt.Errorf("failed to merge Aptos worker job specs: %w", err) + } + } + + results[i] = maps.Clone(mergedSpecs) + return nil + }) } + if err := group.Wait(); err != nil { + return nil, err + } + + specs, err := jobhelpers.MergeSpecsByIndex(results) + if err != nil { + return nil, err + } return specs, nil } diff --git a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go index eb20937444a..4002a19bfe8 100644 --- a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go +++ b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go @@ -69,8 +69,9 @@ func ExecuteAptosTest(t *testing.T, tenv *configuration.TestEnvironment) { } type aptosScenario struct { - name string - run func( + name string + requiresWrite bool + run func( t *testing.T, tenv *configuration.TestEnvironment, aptosChain blockchains.Blockchain, @@ -81,8 +82,8 @@ type aptosScenario struct { func aptosDefaultScenarios() []aptosScenario { return []aptosScenario{ - {name: "Aptos Write Read Roundtrip", run: ExecuteAptosWriteReadRoundtripTest}, - {name: "Aptos Write Expected Failure", run: ExecuteAptosWriteExpectedFailureTest}, + {name: "Aptos Write Read Roundtrip", requiresWrite: true, run: ExecuteAptosWriteReadRoundtripTest}, + {name: "Aptos Write Expected Failure", requiresWrite: true, run: ExecuteAptosWriteExpectedFailureTest}, } } @@ -100,20 +101,24 @@ func resolveAptosScenarios(t *testing.T) []aptosScenario { available := map[string]aptosScenario{ "read": { - name: "Aptos Read", - run: ExecuteAptosReadTest, + name: "Aptos Read", + requiresWrite: false, + run: ExecuteAptosReadTest, }, "write": { - name: "Aptos Write", - run: ExecuteAptosWriteTest, + name: "Aptos Write", + requiresWrite: true, + run: ExecuteAptosWriteTest, }, "roundtrip": { - name: "Aptos Write Read Roundtrip", - run: ExecuteAptosWriteReadRoundtripTest, + name: "Aptos Write Read Roundtrip", + requiresWrite: true, + run: ExecuteAptosWriteReadRoundtripTest, }, "write-expected-failure": { - name: "Aptos Write Expected Failure", - run: ExecuteAptosWriteExpectedFailureTest, + name: "Aptos Write Expected Failure", + requiresWrite: true, + run: ExecuteAptosWriteExpectedFailureTest, }, } @@ -141,39 +146,65 @@ func resolveAptosScenarios(t *testing.T) []aptosScenario { } func executeAptosScenarios(t *testing.T, tenv *configuration.TestEnvironment, scenarios []aptosScenario) { - creEnv := tenv.CreEnvironment - require.NotEmpty(t, creEnv.Blockchains, "Aptos suite expects at least one blockchain in the environment") - - var aptosChain blockchains.Blockchain - for _, bc := range creEnv.Blockchains { - if bc.IsFamily(blockchain.FamilyAptos) { - aptosChain = bc - break - } - } - require.NotNil(t, aptosChain, "Aptos suite expects an Aptos chain in the environment (use config workflow-gateway-don-aptos.toml)") - + aptosChain := mustAptosChainInEnv(t, tenv) lggr := framework.L - userLogsCh := make(chan *workflowevents.UserLogs, 1000) - baseMessageCh := make(chan *commonevents.BaseMessage, 1000) writeDon := findAptosDonForChain(t, tenv, aptosChain.ChainID()) assertAptosWorkerRuntimeKeysMatchMetadata(t, writeDon) - - server := t_helpers.StartChipTestSink(t, t_helpers.GetPublishFn(lggr, userLogsCh, baseMessageCh)) - t.Cleanup(func() { - server.Shutdown(t.Context()) - close(userLogsCh) - close(baseMessageCh) - }) + if aptosScenariosRequireWriteSetup(scenarios) { + ensureAptosWriteWorkersFunded(t, aptosChain, writeDon) + } for _, scenario := range scenarios { + scenario := scenario t.Run(scenario.name, func(t *testing.T) { - scenario.run(t, tenv, aptosChain, userLogsCh, baseMessageCh) + if parallelEnabled && fanoutEnabled { + t.Parallel() + } + + scenarioEnv := t_helpers.SetupTestEnvironmentWithPerTestKeys(t, tenv.TestConfig) + scenarioAptosChain := mustAptosChainInEnv(t, scenarioEnv) + + userLogsCh := make(chan *workflowevents.UserLogs, 1000) + baseMessageCh := make(chan *commonevents.BaseMessage, 1000) + server := t_helpers.StartChipTestSink(t, t_helpers.GetPublishFn(lggr, userLogsCh, baseMessageCh)) + t.Cleanup(func() { + server.Shutdown(t.Context()) + close(userLogsCh) + close(baseMessageCh) + }) + + scenario.run(t, scenarioEnv, scenarioAptosChain, userLogsCh, baseMessageCh) }) } } +func aptosScenariosRequireWriteSetup(scenarios []aptosScenario) bool { + for _, scenario := range scenarios { + if scenario.requiresWrite { + return true + } + } + return false +} + +func mustAptosChainInEnv(t *testing.T, tenv *configuration.TestEnvironment) blockchains.Blockchain { + t.Helper() + + require.NotNil(t, tenv, "Aptos suite requires a test environment") + require.NotNil(t, tenv.CreEnvironment, "Aptos suite requires a CRE environment") + require.NotEmpty(t, tenv.CreEnvironment.Blockchains, "Aptos suite expects at least one blockchain in the environment") + + for _, bc := range tenv.CreEnvironment.Blockchains { + if bc.IsFamily(blockchain.FamilyAptos) { + return bc + } + } + + require.FailNow(t, "Aptos suite expects an Aptos chain in the environment (use config workflow-gateway-don-aptos.toml)") + return nil +} + func assertAptosWorkerRuntimeKeysMatchMetadata(t *testing.T, writeDon *crelib.Don) { t.Helper() @@ -235,10 +266,10 @@ func ExecuteAptosReadTest( } const workflowFileLocation = "./aptos/aptosread/main.go" - t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &workflowConfig, workflowFileLocation) + workflowID := t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &workflowConfig, workflowFileLocation) expectedLog := "Aptos read consensus succeeded" - t_helpers.WatchWorkflowLogs(t, lggr, userLogsCh, baseMessageCh, t_helpers.WorkflowEngineInitErrorLog, expectedLog, aptosWorkflowTimeout) + t_helpers.WatchWorkflowLogs(t, lggr, userLogsCh, baseMessageCh, t_helpers.WorkflowEngineInitErrorLog, expectedLog, aptosWorkflowTimeout, t_helpers.WithUserLogWorkflowID(workflowID)) lggr.Info().Str("expected_log", expectedLog).Msg("Aptos read capability test passed") } @@ -298,10 +329,9 @@ func ExecuteAptosWriteTest( } const workflowFileLocation = "./aptos/aptoswrite/main.go" - ensureAptosWriteWorkersFunded(t, aptosChain, scenario.writeDon) - t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &workflowConfig, workflowFileLocation) + workflowID := t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &workflowConfig, workflowFileLocation) - txHash := waitForAptosWriteSuccessLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, aptosWorkflowTimeout) + txHash := waitForAptosWriteSuccessLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, workflowID, aptosWorkflowTimeout) assertAptosReceiverUpdatedOnChain(t, aptosChain, scenario.receiverHex, scenario.expectedBenchmarkValue) assertAptosWriteTxOnChain(t, aptosChain, txHash, scenario.receiverHex) lggr.Info(). @@ -333,8 +363,7 @@ func ExecuteAptosWriteReadRoundtripTest( ExpectedBenchmark: scenario.expectedBenchmarkValue, } - ensureAptosWriteWorkersFunded(t, aptosChain, scenario.writeDon) - t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &roundtripCfg, "./aptos/aptoswriteroundtrip/main.go") + workflowID := t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &roundtripCfg, "./aptos/aptoswriteroundtrip/main.go") t_helpers.WatchWorkflowLogs( t, lggr, @@ -343,6 +372,7 @@ func ExecuteAptosWriteReadRoundtripTest( t_helpers.WorkflowEngineInitErrorLog, "Aptos write/read consensus succeeded", aptosWorkflowTimeout, + t_helpers.WithUserLogWorkflowID(workflowID), ) lggr.Info(). Str("receiver", scenario.receiverHex). @@ -359,7 +389,7 @@ func ExecuteAptosWriteExpectedFailureTest( baseMessageCh <-chan *commonevents.BaseMessage, ) { lggr := framework.L - scenario := prepareAptosWriteScenario(t, tenv, aptosChain) + scenario := prepareAptosWriteFailureScenario(t, tenv, aptosChain) workflowName := uniqueAptosWorkflowName("aptos-write-expected-failure-workflow") workflowConfig := aptoswrite_config.Config{ @@ -374,10 +404,9 @@ func ExecuteAptosWriteExpectedFailureTest( } const workflowFileLocation = "./aptos/aptoswrite/main.go" - ensureAptosWriteWorkersFunded(t, aptosChain, scenario.writeDon) - t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &workflowConfig, workflowFileLocation) + workflowID := t_helpers.CompileAndDeployWorkflow(t, tenv, lggr, workflowName, &workflowConfig, workflowFileLocation) - txHash := waitForAptosWriteExpectedFailureLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, aptosWorkflowTimeout) + txHash := waitForAptosWriteExpectedFailureLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, workflowID, aptosWorkflowTimeout) assertAptosWriteFailureTxOnChain(t, aptosChain, txHash) lggr.Info(). @@ -415,6 +444,23 @@ func prepareAptosRoundtripScenario(t *testing.T, tenv *configuration.TestEnviron ) } +func prepareAptosWriteFailureScenario(t *testing.T, tenv *configuration.TestEnvironment, aptosChain blockchains.Blockchain) aptosWriteScenario { + t.Helper() + + writeDon := findAptosDonForChain(t, tenv, aptosChain.ChainID()) + workers, workerErr := writeDon.Workers() + require.NoError(t, workerErr, "failed to list Aptos write DON workers") + f := (len(workers) - 1) / 3 + require.GreaterOrEqual(t, f, 1, "Aptos write DON requires f>=1") + + return aptosWriteScenario{ + chainSelector: aptosChain.ChainSelector(), + reportPayloadHex: hex.EncodeToString(buildAptosDataFeedsBenchmarkPayloadFor(aptosTestFeedID(), aptosWriteBenchmarkValue)), + requiredSignatures: f + 1, + writeDon: writeDon, + } +} + func prepareAptosWriteScenarioWithBenchmark( t *testing.T, tenv *configuration.TestEnvironment, @@ -500,10 +546,11 @@ func waitForAptosWriteSuccessLogAndTxHash( lggr zerolog.Logger, userLogsCh <-chan *workflowevents.UserLogs, baseMessageCh <-chan *commonevents.BaseMessage, + workflowID string, timeout time.Duration, ) string { t.Helper() - return waitForAptosLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, "Aptos write capability succeeded", timeout) + return waitForAptosLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, workflowID, "Aptos write capability succeeded", timeout) } func waitForAptosWriteExpectedFailureLogAndTxHash( @@ -511,10 +558,11 @@ func waitForAptosWriteExpectedFailureLogAndTxHash( lggr zerolog.Logger, userLogsCh <-chan *workflowevents.UserLogs, baseMessageCh <-chan *commonevents.BaseMessage, + workflowID string, timeout time.Duration, ) string { t.Helper() - return waitForAptosLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, "Aptos write failure observed as expected", timeout) + return waitForAptosLogAndTxHash(t, lggr, userLogsCh, baseMessageCh, workflowID, "Aptos write failure observed as expected", timeout) } func waitForAptosLogAndTxHash( @@ -522,6 +570,7 @@ func waitForAptosLogAndTxHash( lggr zerolog.Logger, userLogsCh <-chan *workflowevents.UserLogs, baseMessageCh <-chan *commonevents.BaseMessage, + workflowID string, expectedLog string, timeout time.Duration, ) string { @@ -534,6 +583,10 @@ func waitForAptosLogAndTxHash( defer cancelCauseFn(nil) go func() { + if workflowID != "" { + t_helpers.FailOnBaseMessage(cancelCtx, cancelCauseFn, t, lggr, baseMessageCh, t_helpers.WorkflowEngineInitErrorLog, t_helpers.WithBaseMessageWorkflowID(workflowID)) + return + } t_helpers.FailOnBaseMessage(cancelCtx, cancelCauseFn, t, lggr, baseMessageCh, t_helpers.WorkflowEngineInitErrorLog) }() @@ -544,6 +597,9 @@ func waitForAptosLogAndTxHash( require.NoError(t, context.Cause(cancelCtx), "failed to observe Aptos log with non-empty tx hash: %s", expectedLog) return "" case logs := <-userLogsCh: + if workflowID != "" && !aptosUserLogsHaveWorkflowID(logs, workflowID) { + continue + } for _, line := range logs.LogLines { if !strings.Contains(line.Message, expectedLog) { mismatchCount++ @@ -574,6 +630,13 @@ func waitForAptosLogAndTxHash( } } +func aptosUserLogsHaveWorkflowID(logs *workflowevents.UserLogs, workflowID string) bool { + if logs == nil || logs.M == nil || workflowID == "" { + return false + } + return normalizeHexValue(logs.M.WorkflowID) == normalizeHexValue(workflowID) +} + func assertAptosWriteFailureTxOnChain(t *testing.T, aptosChain blockchains.Blockchain, txHash string) { t.Helper() From b8c456625719279aceb506f3866235fd0e201e9e Mon Sep 17 00:00:00 2001 From: cawthorne Date: Wed, 1 Apr 2026 10:23:58 +0100 Subject: [PATCH 35/35] fix: address aptos lint issues --- .../lib/cre/features/aptos/aptos_forwarder.go | 3 +- .../lib/cre/features/aptos/aptos_workers.go | 128 +++++++++++------- .../smoke/cre/v2_aptos_capability_test.go | 1 - 3 files changed, 78 insertions(+), 54 deletions(-) diff --git a/system-tests/lib/cre/features/aptos/aptos_forwarder.go b/system-tests/lib/cre/features/aptos/aptos_forwarder.go index 0b0556867b3..5751c9dbf7f 100644 --- a/system-tests/lib/cre/features/aptos/aptos_forwarder.go +++ b/system-tests/lib/cre/features/aptos/aptos_forwarder.go @@ -262,6 +262,7 @@ func configureForwarders( if f > 255 { return fmt.Errorf("aptos DON %q fault tolerance F=%d exceeds u8", don.Name, f) } + forwarderF := uint8(f) donIDUint32, err := aptosDonIDUint32(don.ID) if err != nil { @@ -313,7 +314,7 @@ func configureForwarders( var pendingTxHash string var lastSetConfigErr error if err := retry.Do(ctx, retry.WithMaxDuration(2*time.Minute, retry.NewFibonacci(500*time.Millisecond)), func(ctx context.Context) error { - pendingTx, err := forwarderContract.SetConfig(&bind.TransactOpts{Signer: deployerAccount}, donIDUint32, forwarderConfigVersion, byte(f), oracles) + pendingTx, err := forwarderContract.SetConfig(&bind.TransactOpts{Signer: deployerAccount}, donIDUint32, forwarderConfigVersion, forwarderF, oracles) if err != nil { lastSetConfigErr = err if fundErr := aptosChain.Fund(ctx, deployerAddress.StringLong(), 1_000_000_000_000); fundErr != nil { diff --git a/system-tests/lib/cre/features/aptos/aptos_workers.go b/system-tests/lib/cre/features/aptos/aptos_workers.go index 2c328b00c32..7c9f9eaa0fa 100644 --- a/system-tests/lib/cre/features/aptos/aptos_workers.go +++ b/system-tests/lib/cre/features/aptos/aptos_workers.go @@ -51,65 +51,27 @@ func proposeAptosWorkerSpecs( group.SetLimit(jobhelpers.Parallelism(len(enabledChainIDs))) for i, chainID := range enabledChainIDs { - i := i - chainID := chainID group.Go(func() error { - aptosChain, err := findAptosChainByChainID(creEnv.Blockchains, chainID) - if err != nil { - return err - } - - capabilityConfig, err := cre.ResolveCapabilityConfig(nodeSet, flag, cre.ChainCapabilityScope(chainID)) - if err != nil { - return fmt.Errorf("could not resolve capability config for '%s' on chain %d: %w", flag, chainID, err) + mergedSpecs, buildErr := proposeAptosWorkerSpecsForChain( + creEnv, + don.Name, + nodeSet, + flag, + chainID, + bootstrapPeers, + p2pToTransmitterMap, + ) + if buildErr != nil { + return buildErr } - command, err := standardcapability.GetCommand(capabilityConfig.BinaryName) - if err != nil { - return pkgerrors.Wrap(err, "failed to get command for Aptos capability") - } - - forwarderAddress := mustForwarderAddress(creEnv.CldfEnvironment.DataStore, aptosChain.ChainSelector()) - methodSettings, err := resolveMethodConfigSettings(capabilityConfig.Values) - if err != nil { - return fmt.Errorf("failed to resolve Aptos method config settings for chain %d: %w", chainID, err) - } - configStr, err := buildWorkerConfigJSON(chainID, forwarderAddress, methodSettings, p2pToTransmitterMap, true) - if err != nil { - return fmt.Errorf("failed to build Aptos worker config: %w", err) - } - - workerInput, err := newAptosWorkerJobInput(creEnv, don.Name, command, configStr, bootstrapPeers, aptosChain.ChainSelector(), chainID) - if err != nil { - return err - } - - proposer := jobs.ProposeJobSpec{} - if err := proposer.VerifyPreconditions(*creEnv.CldfEnvironment, workerInput); err != nil { - return fmt.Errorf("precondition verification failed for Aptos worker job: %w", err) - } - workerReport, err := proposer.Apply(*creEnv.CldfEnvironment, workerInput) - if err != nil { - return fmt.Errorf("failed to propose Aptos worker job spec: %w", err) - } - - mergedSpecs := make(map[string][]string) - for _, report := range workerReport.Reports { - out, ok := report.Output.(crejobops.ProposeStandardCapabilityJobOutput) - if !ok { - return fmt.Errorf("unable to cast to ProposeStandardCapabilityJobOutput, actual type: %T", report.Output) - } - if err := mergo.Merge(&mergedSpecs, out.Specs, mergo.WithAppendSlice); err != nil { - return fmt.Errorf("failed to merge Aptos worker job specs: %w", err) - } - } - results[i] = maps.Clone(mergedSpecs) return nil }) } - if err := group.Wait(); err != nil { - return nil, err + waitErr := group.Wait() + if waitErr != nil { + return nil, waitErr } specs, err := jobhelpers.MergeSpecsByIndex(results) @@ -119,6 +81,68 @@ func proposeAptosWorkerSpecs( return specs, nil } +func proposeAptosWorkerSpecsForChain( + creEnv *cre.Environment, + donName string, + nodeSet cre.NodeSetWithCapabilityConfigs, + flag string, + chainID uint64, + bootstrapPeers []string, + p2pToTransmitterMap map[string]string, +) (map[string][]string, error) { + aptosChain, err := findAptosChainByChainID(creEnv.Blockchains, chainID) + if err != nil { + return nil, err + } + + capabilityConfig, err := cre.ResolveCapabilityConfig(nodeSet, flag, cre.ChainCapabilityScope(chainID)) + if err != nil { + return nil, fmt.Errorf("could not resolve capability config for '%s' on chain %d: %w", flag, chainID, err) + } + command, err := standardcapability.GetCommand(capabilityConfig.BinaryName) + if err != nil { + return nil, pkgerrors.Wrap(err, "failed to get command for Aptos capability") + } + + forwarderAddress := mustForwarderAddress(creEnv.CldfEnvironment.DataStore, aptosChain.ChainSelector()) + methodSettings, err := resolveMethodConfigSettings(capabilityConfig.Values) + if err != nil { + return nil, fmt.Errorf("failed to resolve Aptos method config settings for chain %d: %w", chainID, err) + } + configStr, err := buildWorkerConfigJSON(chainID, forwarderAddress, methodSettings, p2pToTransmitterMap, true) + if err != nil { + return nil, fmt.Errorf("failed to build Aptos worker config: %w", err) + } + + workerInput, err := newAptosWorkerJobInput(creEnv, donName, command, configStr, bootstrapPeers, aptosChain.ChainSelector(), chainID) + if err != nil { + return nil, err + } + + proposer := jobs.ProposeJobSpec{} + verifyErr := proposer.VerifyPreconditions(*creEnv.CldfEnvironment, workerInput) + if verifyErr != nil { + return nil, fmt.Errorf("precondition verification failed for Aptos worker job: %w", verifyErr) + } + workerReport, err := proposer.Apply(*creEnv.CldfEnvironment, workerInput) + if err != nil { + return nil, fmt.Errorf("failed to propose Aptos worker job spec: %w", err) + } + + mergedSpecs := make(map[string][]string) + for _, report := range workerReport.Reports { + out, ok := report.Output.(crejobops.ProposeStandardCapabilityJobOutput) + if !ok { + return nil, fmt.Errorf("unable to cast to ProposeStandardCapabilityJobOutput, actual type: %T", report.Output) + } + if err := mergo.Merge(&mergedSpecs, out.Specs, mergo.WithAppendSlice); err != nil { + return nil, fmt.Errorf("failed to merge Aptos worker job specs: %w", err) + } + } + + return mergedSpecs, nil +} + func newAptosWorkerJobInput( creEnv *cre.Environment, donName string, diff --git a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go index 4002a19bfe8..5a182b0e1d2 100644 --- a/system-tests/tests/smoke/cre/v2_aptos_capability_test.go +++ b/system-tests/tests/smoke/cre/v2_aptos_capability_test.go @@ -156,7 +156,6 @@ func executeAptosScenarios(t *testing.T, tenv *configuration.TestEnvironment, sc } for _, scenario := range scenarios { - scenario := scenario t.Run(scenario.name, func(t *testing.T) { if parallelEnabled && fanoutEnabled { t.Parallel()