Stellar Atlas
Protocol
CAP-0090

Custom signature payload for account contracts

DraftGitHub

Specification

CAP: 0090
Title: Custom signature payload for account contracts
Working Group:
    Owner: Dmytro Kozhevin <@dmkozh>
    Authors: Dmytro Kozhevin <@dmkozh>
    Consulted: Boyan <@brozorec>, Leigh McCulloch <@leighmcculloch>
Status: Draft
Created: 2026-10-21
Discussion: https://github.com/orgs/stellar/discussions/2049
Protocol version: 31

Simple Summary

Add an option for users to pass an arbitrary value to the custom account contract's signature payload, as well as a way for an account contract to access this value during authorization.

Working Group

As specified in the Preamble.

Motivation

Soroban has built-in support for building the custom account contracts. In order to implement a custom account, developers have to define a special contract function __check_auth that takes the custom signature value, 32-byte hashed payload, and the authorization context (a list of the contract calls being authorized). The signature must be over the host-provided payload, and the payload itself is strictly defined by the protocol. It includes all the necessary data to authenticate the authorization context (specifically, the tree of the contract invocations being authorized), as well as the network identifier and replay protection information (signer address, nonce, and signature expiration ledger).

While the built-in payload is sufficient to cover context that is external to the account (the call tree), it misses the potential internal account context entirely. A simple example of useful internal context is an account-managed nonce: Soroban nonces are arbitrary and not ordered, and if an account wanted to enforce strict ordering of its signatures, it would need to include its own (sequential) nonce in the signature payload. However, that's currently not possible, as the account can't enforce its nonce to be signed. A more advanced example would be a custom account implementation that has different internal policies each with its own authorization rules, and the signer must be able to choose and sign for a policy to be used - again, that's not something that would be currently possible to do.

Internal context is not necessary for every custom account, but it increases the account developer toolset significantly. Custom, account-specific payload is a natural extension to the existing protocol-defined signature payload, and it arguably was an oversight in the protocol design to not include it from the inception.

Goals Alignment

This CAP is aligned with the following Stellar Network Goals:

  • The Stellar Network should make it easy for developers of Stellar projects to create highly usable products

Abstract

This CAP introduces two new kinds of Soroban credentials: SOROBAN_CREDENTIALS_ADDRESS_V3 and SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES_V2 that respectively extend SOROBAN_CREDENTIALS_ADDRESS_V2 and SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES with the new optional customPayload field. SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES_V2 also uses SorobanDelegateSignatureV2 that has a customPayload field as well for delegate-specific custom payload.

When custom payload is provided, a new signature preimage envelope ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_CUSTOM_PAYLOAD has to be used in order to include the custom payload into the signature payload.

In order to handle custom payload in the account implementation, __check_auth function can now take the fourth argument that represents the account's custom payload provided in the credentials. Custom accounts that don't accept the fourth argument will not be able to authorize credentials with custom payload.

Specification

XDR changes

This patch of XDR changes is based on the XDR files in commit 4f524bbac80c781c06e4fb5fc93a2d0d331d6705 of stellar-xdr.

diff --git a/Stellar-ledger-entries.x b/Stellar-ledger-entries.x
index 348311c..51a49ce 100644
--- a/Stellar-ledger-entries.x
+++ b/Stellar-ledger-entries.x
@@ -665,7 +665,8 @@ enum EnvelopeType
     ENVELOPE_TYPE_POOL_REVOKE_OP_ID = 7,
     ENVELOPE_TYPE_CONTRACT_ID = 8,
     ENVELOPE_TYPE_SOROBAN_AUTHORIZATION = 9,
-    ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS = 10
+    ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS = 10,
+    ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_CUSTOM_PAYLOAD = 11
 };
 
 enum BucketListType
diff --git a/Stellar-transaction.x b/Stellar-transaction.x
index 11cacd7..621bb1c 100644
--- a/Stellar-transaction.x
+++ b/Stellar-transaction.x
@@ -569,6 +569,16 @@ struct SorobanAddressCredentials
     SCVal signature;
 };
 
+struct SorobanAddressCredentialsV3
+{
+    SCAddress address;
+    int64 nonce;
+    uint32 signatureExpirationLedger;    
+    SCVal signature;
+    SCVal* customPayload;
+};
+
+
 struct SorobanDelegateSignature
 {
     SCAddress address;
@@ -576,18 +586,34 @@ struct SorobanDelegateSignature
     SorobanDelegateSignature nestedDelegates<>;
 };
 
+struct SorobanDelegateSignatureV2
+{
+    SCAddress address;
+    SCVal signature;
+    SCVal* customPayload;
+    SorobanDelegateSignatureV2 nestedDelegates<>;
+};
+
 struct SorobanAddressCredentialsWithDelegates
 {
     SorobanAddressCredentials addressCredentials;
     SorobanDelegateSignature delegates<>;
 };
 
+struct SorobanAddressCredentialsWithDelegatesV2
+{
+    SorobanAddressCredentialsV3 addressCredentials;
+    SorobanDelegateSignatureV2 delegates<>;
+};
+
 enum SorobanCredentialsType
 {
     SOROBAN_CREDENTIALS_SOURCE_ACCOUNT = 0,
     SOROBAN_CREDENTIALS_ADDRESS = 1,
     SOROBAN_CREDENTIALS_ADDRESS_V2 = 2,
-    SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES = 3
+    SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES = 3,
+    SOROBAN_CREDENTIALS_ADDRESS_V3 = 4,
+    SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES_V2 = 5
 };
 
 union SorobanCredentials switch (SorobanCredentialsType type)
@@ -600,6 +626,10 @@ case SOROBAN_CREDENTIALS_ADDRESS_V2:
     SorobanAddressCredentials addressV2;
 case SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES:
     SorobanAddressCredentialsWithDelegates addressWithDelegates;
+case SOROBAN_CREDENTIALS_ADDRESS_V3:
+    SorobanAddressCredentialsV3 addressV3;
+case SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES_V2:
+    SorobanAddressCredentialsWithDelegatesV2 addressWithDelegatesV2;
 };
 
 /* Unit of authorization data for Soroban.
@@ -759,6 +789,15 @@ case ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS:
         SCAddress address;
         SorobanAuthorizedInvocation invocation;
     } sorobanAuthorizationWithAddress;
+case ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_CUSTOM_PAYLOAD:
+    struct
+    {
+        // Hash of the Soroban authorization preimage, currently only either
+        // ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS or 
+        // ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_CUSTOM_PAYLOAD can be used.
+        Hash sorobanAuthPayloadHash;
+        SCVal customPayload;
+    } sorobanAuthorizationWithCustomPayload;
 };
 
 enum MemoType

Semantics

New credential types

The CAP introduces SOROBAN_CREDENTIALS_ADDRESS_V3 and credential types.

SOROBAN_CREDENTIALS_ADDRESS_V3 extends the existing SOROBAN_CREDENTIALS_ADDRESS_V2 with an additional customPayload field that is an optional arbitrary SCVal passed to the respective custom account.

SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES_V2 extends the existing SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES credential type. Credentials with delegates include the optional common customPayload through the addressCredentials of the top-level address, as well as per-delegate custom payload in SorobanDelegateSignatureV2 customPayload optional field. Custom payloads are inherited by all the nested delegates of the top-level address or a given delegate via signature payloads, i.e. customPayload of addressCredentials is signed by both the top-level address and all the nested delegates, while a delegate's customPayload is signed by that delegate and all its nestedDelegates. The preimage generation logic is described in the following section.

Signature preimage for custom payload

A new signature preimage type ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_CUSTOM_PAYLOAD is added to HashIDPreimage union. This preimage is used for computing signature payloads that cover the set customPayload field in the credentials. This preimage allows for recursive combination via sorobanAuthPayloadHash:

  • For SOROBAN_CREDENTIALS_ADDRESS_V3 or SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES_V2 addressCredentials that have customPayload set, the expected signature payload is SHA256(HashIDPreimage::ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_CUSTOM_PAYLOAD(sorobanAuthPayloadHash=SHA256(HashIDPreimage::ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS(...), customPayload))), where ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS is computed based on the credential inputs and network id as usual. If customPayload is not set, ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS preimage should be used directly.
    • Notice that legacy unsafe ENVELOPE_TYPE_SOROBAN_AUTHORIZATION preimage is never used for computing signature payloads for the new credential types.
  • For a delegate of SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES_V2 that has customPayload set, the expected signature payload is SHA256(HashIDPreimage::ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_CUSTOM_PAYLOAD(sorobanAuthPayloadHash=parentSignaturePayload, customPayload))), where parentSignaturePayload is SHA256 of signature payload of the delegate's parent (addressCredentials for the top-level delegate, or the enclosing delegate for nestedDelegates). If customPayload is not set for the delegate, the hashed parent's payload should be signed directly.

Custom payload dispatch rules

The reserved __check_auth contract function is now allowed to receive 3 or 4 arguments, instead of exactly 3 arguments in the current protocol.

The fourth argument is used for receiving the customPayload from the host. The value of customPayload argument is going to be a VecObject with the following contents:

  • Empty vector if customPayload is not set in the credentials or SorobanDelegateSignatureV2 of the delegate.
  • A single-element vector with the only element being the Val-converted customPayload SCVal from the credentials or SorobanDelegateSignatureV2 of the delegate.

The forwarded customPayload argument always corresponds to the customPayload field directly set in the credentials or the delegate signature, and in case of delegation it is not passed from the parent to the child delegates, even though it is implicitly hashed into the signature payload as per section above. This means that delegates sign all the parents' custom payloads as well as their own payloads, but they can only interpret their own custom payload.

This CAP does not change the host authentication flow: it will still follow CAP-0046-11 rules for when to authenticate the provided credentials. The only changes in this CAP concern the logic around handling the customPayload argument in address-based credentials during the authentication process. Specifically, the host will follow the following updated rules:

  • If the legacy credential type is used:
    • For G-account addresses, no changes
    • For __check_auth with 3 arguments, no changes
    • For __check_auth with 4 arguments, pass empty vector as the fourth argument.
  • If the new credential type (or SorobanDelegateSignatureV2 for delegate's __check_auth call) is used with unset customPayload:
    • For G-account addresses, no changes
    • For __check_auth with 3 arguments, no changes
    • For __check_auth with 4 arguments, pass empty vector as the fourth argument.
  • If the new credential type (or SorobanDelegateSignatureV2 for delegate's __check_auth call) is used with set customPayload:
    • For G-account addresses, return an error and cause guest to panic (since customPayload is not supported for G-account addresses).
    • For __check_auth with 3 arguments, return an error and cause guest to panic (since the account is not able to handle customPayload).
    • For __check_auth with 4 arguments, pass a single-element vector with the Val-converted customPayload SCVal as the fourth argument.

Design Rationale

Explicit customPayload vs signature extension

A potential design alternative to consider compared to this CAP is to extend SCVal with a special new variant that represents a signature and a custom payload, i.e. a variant that is a struct with an SCVal field for signature and another SCVal field for the custom payload.

This alternative would reduce the scope of the XDR and __check_auth changes compared to this CAP:

  • No need to add the new credential type
  • No need to add arguments to __check_auth function, as guest could use a host function to extract the signature and custom payload from the provided signature Val

While this may slightly reduce the downstream integration surface at the XDR API level, it instead introduces more complexity and non-intuitive behavior, specifically:

  • Signatures would need to change their types depending on the presence of the custom payload
  • The new preimage kind is still required, and now the signer would need to choose the preimage based on the expected structure of the signature (in addition to just the credentials contents)
  • The contracts would still need to be updated in order to support the new type of the signature, and the handling is less intuitive compared to just receiving a new argument
    • Alternatively, the host itself could 'unwrap' the signature and pass custom payload as the 4th argument like in this CAP, but that is rather hacky
  • More value validation rules would need to be introduced, as the new SCVal variant would only be valid in the specific context of custom account signatures

Custom payload and delegated signers

Delegated signers introduced in CAP-0071 are supposed to act 'as if' they were authorizing exactly the same call as their parent address. Custom payload is an exception to this rule, as by definition it represents the internal context of the parent account. The delegates generally do not know what that context means and thus can't make a meaningful decision based on it. They still do sign the parent's custom payload though, as it is hashed into the expected signature preimage.

Delegated signers are also allowed to have their own custom payload that they can interpret. While this increases the complexity of the CAP to a degree, it allows for more flexible delegation schemes, e.g. combining the top-level account with context rules in custom payload with a delegate that has a sequential nonce as its own custom payload.

Custom payload argument format

The CAP uses somewhat unusual format for the custom payload argument, specifically, a 0 or 1 argument vector object. This is done in order to disambiguate between the absence of a custom payload and the presence of SCVal::VOID payload, both of which would map to Val::Void (or None from the guest contract point of view) if we used a more intuitive Option<Val> type on the guest side.

An alternative approach would be to prohibit SCVal::VOID as a custom payload and use Option<Val> on the guest side, but this may be non-intuitive: usually any host-representable SCVal can be used as a Val argument in the contract, so it would be weird to have the only exception in this specific context.

Protocol Upgrade Transition

The proposed functionality will be available starting from protocol version 31.

Backwards Incompatibilities

There are no backwards incompatibilities introduced by this CAP.

Resource Utilization

The new host functions are going to be metered using the existing metering primitives. No other resource utilization changes are expected.

Security Concerns

Custom payloads can only extend the signature payloads and thus do not have a potential to decrease the authorization scope of the signatures or introduce new angles of attack on the authorization logic. Also, the account contract implementations that don't support custom payload are prohibited from authorizing credentials that include a custom payload, thus ensuring that it's not possible for the custom payload to pass authorization checks unnoticed.

Supporting custom payloads requires more work from the custom account and wallet developers, which potentially increases the risk of implementation errors and security vulnerabilities if not handled correctly.

Test Cases

TBD

Implementation

TBD

Preamble

Status
Draft
Protocol version
31
Authors
Dmytro Kozhevin
Created
2026-10-21

Discussion

2 linked threads