From 88d0fe02b4aea61cbbe68afca05f5f4bc37b8ad1 Mon Sep 17 00:00:00 2001 From: Krish-vemula Date: Thu, 10 Sep 2026 13:37:09 -0700 Subject: [PATCH] Validate Stellar signer account binding --- .../stellar/actions/forwarder_client.go | 20 ++++++++++++++++-- .../stellar/actions/forwarder_client_test.go | 21 ++++++++++++++++++- chain_capabilities/stellar/main.go | 6 +++++- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/chain_capabilities/stellar/actions/forwarder_client.go b/chain_capabilities/stellar/actions/forwarder_client.go index 3d0f02e8b..f880ae6ca 100644 --- a/chain_capabilities/stellar/actions/forwarder_client.go +++ b/chain_capabilities/stellar/actions/forwarder_client.go @@ -148,6 +148,22 @@ func (fc *forwarderClient) ResolveSigningAccount(ctx context.Context) (string, e return fc.resolveSigningAccount(ctx) } +// ValidateSigningAccountAddress verifies the relayer signer is a plain Stellar account +// address suitable for both the forwarder transmitter argument and SubmitTransaction.FromAddress. +func ValidateSigningAccountAddress(accountAddress string) error { + if accountAddress == "" { + return errors.New("relayer returned empty signing account") + } + accountBytes, err := strkey.Decode(strkey.VersionByteAccountID, accountAddress) + if err != nil { + return fmt.Errorf("relayer returned invalid signing account %q: %w", accountAddress, err) + } + if len(accountBytes) != 32 { + return fmt.Errorf("relayer signing account must decode to 32 bytes, got %d", len(accountBytes)) + } + return nil +} + func (fc *forwarderClient) InvokeOnReport( ctx context.Context, transmitter, receiver string, @@ -341,8 +357,8 @@ func (fc *forwarderClient) resolveSigningAccount(ctx context.Context) (string, e if err != nil { return "", err } - if resp.AccountAddress == "" { - return "", errors.New("relayer returned empty signing account") + if err := ValidateSigningAccountAddress(resp.AccountAddress); err != nil { + return "", err } return resp.AccountAddress, nil } diff --git a/chain_capabilities/stellar/actions/forwarder_client_test.go b/chain_capabilities/stellar/actions/forwarder_client_test.go index 0db8daa39..c5d6e7576 100644 --- a/chain_capabilities/stellar/actions/forwarder_client_test.go +++ b/chain_capabilities/stellar/actions/forwarder_client_test.go @@ -4,6 +4,7 @@ import ( "errors" "testing" + "github.com/stellar/go-stellar-sdk/strkey" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -61,6 +62,18 @@ func TestForwarderClient_ResolveSigningAccount(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "empty signing account") }) + + t.Run("invalid signing account type", func(t *testing.T) { + t.Parallel() + svc := mocks.NewStellarService(t) + svc.EXPECT().GetSigningAccount(mock.Anything). + Return(stellartypes.GetSigningAccountResponse{AccountAddress: testReceiverAddress}, nil).Once() + client := newForwarderClient(svc, lggr, testForwarderAddress, 100) + + _, err := client.ResolveSigningAccount(t.Context()) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid signing account") + }) } func TestForwarderClient_InvokeOnReport(t *testing.T) { @@ -73,7 +86,13 @@ func TestForwarderClient_InvokeOnReport(t *testing.T) { transmissionID := testTransmissionID() const maxResourceFee = uint64(100_000) svc.EXPECT().SubmitTransaction(mock.Anything, mock.MatchedBy(func(req stellartypes.SubmitTransactionRequest) bool { - return req.FromAddress == testNodeAddress && + if len(req.Args) == 0 || req.Args[0].Address == nil || req.Args[0].Address.AccountID == nil { + return false + } + transmitter, err := strkey.Encode(strkey.VersionByteAccountID, req.Args[0].Address.AccountID) + return err == nil && + transmitter == testNodeAddress && + req.FromAddress == transmitter && req.IdempotencyKey == transmissionID.idempotencyKey() && req.MaxResourceFee == maxResourceFee })). diff --git a/chain_capabilities/stellar/main.go b/chain_capabilities/stellar/main.go index be707b48a..6a796bcb8 100644 --- a/chain_capabilities/stellar/main.go +++ b/chain_capabilities/stellar/main.go @@ -146,9 +146,13 @@ func (c *capabilityGRPCService) Initialise(ctx context.Context, dependencies cor if err != nil { return fmt.Errorf("failed to get stellar service: %w", err) } - if _, err = stellarService.GetSigningAccount(ctx); err != nil { + signingAccount, err := stellarService.GetSigningAccount(ctx) + if err != nil { return fmt.Errorf("stellar relayer has no signing account: %w", err) } + if err = actions.ValidateSigningAccountAddress(signingAccount.AccountAddress); err != nil { + return fmt.Errorf("stellar relayer has invalid signing account: %w", err) + } if err = c.setSelector(cfg); err != nil { return err