Skip to content

Library bounds that exceed Stellar network limits, and values with no bound #909

Description

@brozorec

Summary

Several library bounds allow a structure to grow past a per-transaction Stellar network limit before the bound itself is reached. Past that point, the library still accepts writes, but the operation that reads or rewrites the structure fails on Mainnet. Separately, a number of caller-supplied values have no length or count bound at all.

Limit Value
Contract events plus the top-level return value, per transaction 16,384 B
Contract-data entry size 65,536 B
Ledger key size 250 B

The events-plus-return-value limit is enforced by stellar-core at apply time, after the top-level call returns: it sums the contract events, then adds the encoded return value, and fails the operation with INVOKE_HOST_FUNCTION_RESOURCE_LIMIT_EXCEEDED if the total is over. simulateTransaction computes the same total but only uses it to price the fee, so a call that exceeds the limit simulates successfully, then fails once submitted and is still charged. stellar/stellar-rpc#964 reports this for events; the open fix, stellar/stellar-rpc#992, checks only the events, so an oversized return value would still pass simulation. Cross-contract reads are unaffected because only the top-level return value counts.

Findings were taken on v0.9.0 at df602b61. Sizes were measured in the SDK test host unless marked "est.", which means a hand-computed XDR size.

Bounds that break before they are reached

Module Library maximum Breaks at What fails
accounts::policies::spending_limit MAX_HISTORY_ENTRIES = 1,000 history entries 816 entries (65,592 B) enforce rewrites the whole AccountContext entry, which exceeds the 65,536 B entry limit. Every spend authorized through the policy fails until old entries leave the period window.
rwa::compliance::modules::initial_lockup_period MAX_LOCKS = 512 locks per wallet (41,040 B) 204 active locks get_locked_details as a transaction exceeds the 16,384 B events-plus-return limit.
rwa::compliance::modules::initial_lockup_period MAX_LOCKS = 512 two wallets whose combined locks exceed about 800 migrate_locks appends to the destination without checking MAX_LOCKS (storage.rs:500-518), so the destination can exceed the bound and, at about 82 KB (est.), the entry limit. The migration write fails.
rwa::identity_verification::claim_topics_and_issuers MAX_CLAIM_TOPICS × MAX_ISSUERS = 15 × 50 (30,312 B) about 8 fully populated topics get_claim_topics_and_issuers as a transaction exceeds the events-plus-return limit.
rwa::identity_verification::identity_registry_storage MAX_COUNTRY_ENTRIES = 15 max-size entries (24,672 B) 10 entries get_country_data_entries as a transaction exceeds the events-plus-return limit.
rwa::identity_verification::identity_registry_storage MAX_COUNTRY_ENTRIES = 15 10 max-size entries in one call add_identity, add_country_data_entries, and remove_identity emit one event per entry carrying the whole CountryData, about 1,796 B each. The call exceeds the events limit. An identity built up to 10 or more entries across several calls can no longer be removed.
rwa::extensions::doc_manager BUCKET_SIZE = 50 documents with 200-byte URIs (19,012 B) 44 documents get_documents as a transaction exceeds the events-plus-return limit.

Values with no bound

Module Unbounded value Where it lands Consequence
fungible name, symbol in set_metadata instance entry; name() / symbol() return Oversized metadata inflates every instance read and the getters' return value.
rwa::identity_verification::claim_issuer SigningKey.public_key (only an is_empty check) inside the Pairs(SigningKey) ledger key The key exceeds 250 B from a public key of about 115 bytes (est.); allow_key then fails. A 65-byte secp256k1 key sits at about 204 B.
rwa::identity_verification::identity_claims Claim { signature, data, uri } Claim(id) entry; the whole claim is a #[topic] of ClaimAdded / ClaimRemoved / ClaimChanged; get_claim return A large claim breaks the events limit on add, change, and removal.
rwa::identity_verification::identity_claims ClaimsByTopic(topic), one id per issuer per topic entry; get_claim_ids_by_topic return The getter exceeds the events-plus-return limit at about 410 ids (est.).
accounts::policies::weighted_threshold signer_weights map, not tied to MAX_SIGNERS WeightedInstalled event; example getter Install exceeds the events limit at about 200 entries (est.).
governance::governor VoteCast.reason; propose targets, functions, args VoteCast and ProposalCreated events (the latter also carries up to 4,096 B of description) Large inputs break the events limit on voting and proposing.
governance::timelock Operation.args; the target's return value, passed through by execute events; top-level return value A target returning a large value makes execute fail as a transaction.
fee-abstraction target_args; the target's return value, passed through by forward ForwardExecuted event; top-level return value Same as timelock.
zk-email::dkim_registry input Vec of set_dkim_public_key_hashes 2 footprint entries, 1 write, and 1 event of about 190 B per item The call exceeds the events limit at about 80 items (est.).
tokens::confidential::verifier verification key Bytes on register and update instance entry; VerificationKeyRegistered embeds it, VerificationKeyUpdated embeds old and new About 3.6 KB of events per update at the real 1,760 B key; larger keys scale linearly.
tokens::confidential proof Bytes UltraHonk verifier CPU No test in stellar-tokens verifies a real proof, so the CPU cost at realistic sizes is unmeasured.
contract-utils::crypto::merkle proof length in sorted verify CPU verify_with_index caps at a bare 32 (merkle.rs:93); verify has no cap.
rwa root and compliance modules every batch_* input Vec per item: hook chain, writes, events The documented 35 / 99 item figures (rwa/mod.rs:95-104) come from an on-demand benchmark outside the workspace, not a test.

Proposed fixes

  • spending_limit: lower MAX_HISTORY_ENTRIES below 816, or split the history across several entries (MAX_HISTORY_ENTRIES is unreachable, so HistoryCapacityExceeded can never be returned #847).
  • initial_lockup_period: enforce MAX_LOCKS in migrate_locks; lower MAX_LOCKS to about 150 or paginate get_locked_details.
  • claim_topics_and_issuers: have verify_identity use the per-topic getter instead of the full map, and either lower the bounds or paginate get_claim_topics_and_issuers.
  • identity_registry_storage: emit one event per call instead of one per entry; lower MAX_COUNTRY_ENTRIES to 6 or paginate the entries getter.
  • doc_manager: BUCKET_SIZE 25, MAX_BUCKETS 200, which puts a full bucket at 9,512 B. Existing deployments need a migration because bucket indices derive from BUCKET_SIZE.
  • Add length or count bounds for each value in the second table, each with a max-fill test.

Every new or changed bound should ship with a test that fills it to the maximum with maximum-sized items and checks the result against the network limits, so the bound and the network limit cannot drift apart again.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions