Skip to content

Latest commit

 

History

History
600 lines (440 loc) · 33.9 KB

cap-0046-12.md

File metadata and controls

600 lines (440 loc) · 33.9 KB

Preamble

CAP: 0046-12
Title: Soroban State Archival Interface
Working Group:
    Owner: Garand Tyson <@SirTyson>
    Authors: Garand Tyson <@SirTyson>, Siddharth Suresh <@sisuresh>
    Consulted: Nicolas Barry <@MonsieurNicolas>
Status: Final
Created: 2023-09-14
Discussion:
Protocol version: 20

Simple Summary

This proposal defines a state archival interface for Soroban Ledger Entries. This is just the initial step in a full state archival solution, but defines a stable interface such that all smart contracts deployed with this interface will be compatible with future state archival developments.

Motivation

See the Soroban overview CAP for overall Soroban motivation.

In Stellar classic, the size of the ledger is growing exponentially and will significantly hurt network performance if not mitigated. Many of these entries are “spam” entries that are not actually used, such as claimable balance token airdrops that accounts never claim. To mitigate this growth, this proposal will establish the concept of entry TTL, where the users of a given Soroban entry must pay for the entry to remain active and immediately useable.

Goals Alignment

This CAP is aligned with the following Stellar Network Goals:

  • The Stellar Network should run at scale and at low cost to all participants of the network.

Abstract

This CAP introduces the Soroban State Archival interface. A complete state archival implementation includes:

  1. Host functions and Operations for smart contracts and users to interact with state lifetimes.
  2. A robust off-chain system for storing and providing cryptographic proofs of archived data.

This CAP implements part 1. Initially, most data will not actually be removed from validator databases. However, by introducing this interface at Soroban launch, all deployed smart contracts will be compatible with state archival when the full solution is introduced in a future protocol upgrade.

Specification

XDR

See the XDR diffs in the Soroban overview CAP, specifically those referring to LedgerCloseMetaV1, ContractDataDurability, and TTLEntry.

Semantics

State Archival Entry States

While not codified in XDR or explicitly defined in code, each Soroban entry must be in one of the following states. Note that the BucketList still only uses the INITENTRY, LIVEENTRY, and DEADENTRY states, which are unrelated to these archival states. These are only defined here to help discussion for this document.

In this context, “accessibility” is defined as a Soroban TX being able to read the given entry. See Lifetime Enforcement for more detail.

From the perspective of a TX, each LedgerEntry is in one of these three states:

  • LIVE - Entry exists on the BucketList and validator DB, is accessible.
  • ARCHIVED - PERSISTENT entry with TTL of 0, not accessible.
  • DEAD - TEMPORARY entry with TTL of 0, not accessible.

In a full state archival implementation, ARCHIVED and DEAD entries are eligible for removal from the BucketList and validator DB. From the perspective of TX invocation, it does not matter if an ARCHIVED or DEAD entry exists on the validator or not. While this state should not be exposed to contract developers and TX invokers, it is necessary for implementation:

  • EVICTED - Entry does not exist on the BucketList and validator DB. Only ARCHIVED and DEAD entries can be EVICTED.

Contract Data Durability

ContractDataDurability defines the durability of storage, either TEMPORARY or PERSISTENT. ContractDataDurability is included as a field in LedgerKey such that TEMPORARY and PERSISTENT entries are in separate key spaces.

  • TEMPORARY - Durability of storage that cannot be restored, "dies" and is permanently inaccessible after liveUntilLedgerSeq.
  • PERSISTENT - Durability of storage that is "archived" and can be restored after its liveUntilLedgerSeq.

TTL Entry

TTLEntry is a new type of LedgerEntry that contains lifetime information. Every Soroban LedgerEntry type (ContractData and ContractCode) must always have an associated TTLEntry. Similarly, a TTLEntry cannot exist without an associated ContractCode or ContractData entry. It has the following fields:

  • keyHash - Serves as the LedgerKey, SHA256 hash of the LedgerKey of the associated ContractCode or ContractData entry.
  • liveUntilLedgerSeq - The ledger sequence number after which the associated ContractCode or ContractData entry will no longer be accessible. A PERSISTENT entry is ARCHIVED immediately following its liveUntilLedgerSeq, while a TEMPORARY entry DIES.

An entry is considered to be in the LIVE state iff for its TTLEntry e:

isLive(e) == currentLedger <= e.liveUntilLedgerSeq

TTL

An entry’s Time To Live (TTL) is the number of ledgers that the entry will be LIVE based on the current ledger sequence number. For example, if currentLedger == 10 and entry.liveUntilLedgerSeq == 15, the entry has a TTL of 5 ledgers.

Restore

To restore an entry is to change the entry’s state from ARCHIVED to LIVE. An entry that is currently LIVE cannot be restored. Only entries with PERSISTENT durability can be restored.

Rent Fee

The cost of extending an entry’s TTL. This fee is variable depending on the current size of the BucketList. Fee is determined by entry durability, size, and number of ledgers added to the TTL:

rent_fee(size, durability, num_ledgers)
   = (wfee_rate_average(size) / rent_rate_denominator(durability)) * num_ledgers + write_fee(sizeof(TTLEntry))

See Fee and resource model in smart contracts for more details.

Network Config Settings

See Network Configuration Ledger Entries for more details on Network Config Settings.

  • maxEntryTTL - Maximum TTL that an entry can be extended to at any given point.
  • minTemporaryTTL - Minimum TTL a TEMPORARY entry must have on creation.
  • minPersistentTTL - Minimum lifetime a PERSISTENT entry must have on creation or restoration.
  • persistentRentRateDenominator - rent_rate_denominator for PERSISTENT entries used to calculate rent_fee. Must be strictly less than tempRentRateDenominator.
  • tempRentRateDenominator - rent_rate_denominator for TEMPORARY entries used to calculate rent_fee. Must be strictly greater than persistentRentRateDenominator.
  • maxEntriesToArchive - Maximum number of entries that can be evicted in a single ledger.
  • evictionScanSize - Maximum number of bytes of the BucketList that the eviction scan will consume per ledger.

EvictionIterator

EvictionIterator keeps track of the current eviction scan position within the BucketList. It is updated and written to the BucketList after every eviction scan on ledger close.

  • bucketListLevel - BucketList level of the scan.
  • isCurrBucket - Indicates if curr Bucket or snap bucket is being scanned.
  • bucketFileOffset - Indicates file offset to begin eviction scan at.

LedgerCloseMetaV1

  • totalByteSizeOfBucketList - Average BucketList size used in wfee_rate calculation.
  • evictedTemporaryLedgerKeys - Vector of LedgerKey containing TEMPORARY entries that have been evicted in the given ledger.
  • evictedPersistentLedgerEntries - Vector of PERSISTENT LedgerEntry that have been evicted in the given ledger. Note that this will always be empty until a protocol upgrade with the full state archival implementation.

Design Rationale

Durability of Storage

From a network health perspective, it would be ideal to not allow entry restoration and just permanently delete all entries after the liveUntilLedgerSeq. Unfortunately this would result in significant issues when it comes to user experience. Should a user forget to periodically issue TTL extensions to a high value entry, such as a token balance, the valuable entry could be permanently lost. To avoid this, there must be a durability of storage that is defined to be recoverable after its TTL goes to 0, PERSISTENT storage.

While some data types, such as token balances, need to be recoverable after archival, there are also many data types that do not need such guarantees. Many entries can be arbitrarily recreated after their TTL goes to 0 or are only relevant for a given period of time. Examples include token allowances, oracle information, and time restricted KYC. For these entries that do not need to be recoverable, there is the TEMPORARY storage durability.

In order to allow PERSISTENT storage restoration, some amount of data must be permanently stored, both on validators and in off-chain services. Because of this cost, TEMPORARY storage should always be preferred. To incentivize this at the protocol level, TEMPORARY storage fees are strictly cheaper than PERSISTENT storage fees.

Storage Guarantees

At all times, there must only be a single valid version of an entry per key. This requires certain invariants for each durability type.

PERSISTENT Storage Invariants

To maintain this single version invariant, a PERSISTENT entry cannot be recreated if an entry with the same key is ARCHIVED. If recreation was allowed, multiple different versions of an entry with the same key could exist in the ARCHIVED state. This creates many security vulnerabilities and is not allowed.

If a given PERSISTENT entry key is ARCHIVED, the only valid operation for that key is restore. Any call to create, load, or erase on the key will fail. In order to delete an ARCHIVED PERSISTENT entry, it is necessary to restore the entry then delete the restored entry.

TEMPORARY Storage Invariants

Because TEMPORARY entries cannot be restored, it is much simpler to maintain uniqueness guarantees. Once a TEMPORARY DIES (i.e. TTL == 0), it is as if the entry never existed. Even if a DEAD TEMPORARY entry has not yet been EVICTED (i.e., deleted from the BucketList and DB), the key of the entry can be recreated.

Soroban Entry Invariants

For each ContractCode and ContractWasm entry, there must also be an associated TTLEntry. Similarly, each TTLEntry must always be accompanied by a ContractCode or ContractData entry.

Contract Instance and Contract Code Durability

All ContractData entries are subject to state archival, including contract instances. ContractCode is also subject to state archival. This means that contract instance and code must also periodically receive TTL extensions and may also become ARCHIVED and be inaccessible.

To prevent difficult edge cases, all ContractCode and contract instance entries must be of PERSISTENT durability.

TTL Enforcement

TTLs are enforced at the transaction level. Enforcement differs slightly based on the durability of the entry:

PERSISTENT Entry TTL Enforcement

If a transaction has the key of an ARCHIVED PERSISTENT entry in the read-only or read-write section of the footprint, the transaction immediately fails. This is necessary to maintain the PERSISTENT entry invariant that the entry cannot be recreated after being ARCHIVED.

TEMPORARY Entry TTL Enforcement

Unlike PERSISTENT entries, if the key of a DEAD TEMPORARY entry is in the read-only or read-write section of a transaction's footprint, the transaction does not immediately fail. Instead, during transaction application, it is as if the entry has never existed. The transaction may fail during application if the contract function attempts to read the entry. If the contract function writes to the key, the entry is recreated with the new value as if the key never existed.

TTL Management

A LIVE entry’s TTL can be extended by a Soroban operation (ExtendFootprintTTLOp) or by a host function (extend_* family of functions). ARCHIVED entries can only be restored by a Soroban operation (RestoreFootprintOp).

See Operations and Host Functions.

Authorization

Any account may issue a TTL extension or restoration for any entry without authorization. TTL extension and restoration is defined to always be beneficial to the owner of the entry and can not be used maliciously. This means that an entry’s TTL must not be used for security guarantees. If an entry must be invalidated after some number of ledgers, a smart contract must define this behavior and can not assume that the entry will only live to a specific ledger.

Fees

Since the fee to write to the BucketList increases as the BucketList grows in size, rent_fee for TTL extensions should grow at the same rate. Intuitively, the larger the BucketList size, the faster entries should be evicted. By increasing rent fees as write fees increase, the protocol creates a negative back pressure where the more expensive it is to add the BucketList, the faster entries will be evicted thus causing the size to decrease. By using wfee_rate_average as the basis for both write and rent fees, the protocol can ensure that neither the rate of adding to the BucketList nor the rate of eviction can outpace one another (within some reasonable correlation).

The rent_fee is a fraction of the cost of writing an entry. However, updating an entry's TTL necessitates a write, as the liveUntilLedgerSeq field must be modified. Due to the structure of the BucketList, it is not possible to change a single field in a LedgerEntry, as the entire entry must be rewritten. If liveUntilLedgerSeq was stored directly in the ContractData or ContractCode entry, TTL extension would require rewriting the entire entry. This would make ContractCode TTL extensions particularly expensive, as the write fees for rewriting an entire WASM blob would be exponentially higher than the actual rent_fee cost.

To avoid expensive rewrites, liveUntilLedgerSeq are stored in a dedicated LedgerEntry (TTLEntry) separate to the ContractCode or ContractData entry. These entries are a small, fixed size. While users must still pay write fees when updating entry TTLs, TTLEntry writes are significantly smaller and cheaper than rewriting the associated ContractCode or ContractData entry.

TTL Entry Fees

TTLEntry counts toward readEntry and readBytes fees. This means for each ContractCode and ContractData entry in the footprint (both readOnly and readWrite), the TTLEntry is implicitly included in the readOnly set. TTLEntry does not count towards writeEntry and writeBytes. Every key in both the readOnly and readWrite is eligible to have its TTL extended, but the extension is conditional on runtime state. If write related fees were charged, every key in the footprint would have to pay for a TTLEntry write even if no extension occurs. In order to account for the write, rent_fee includes the TTLEntry write fees. This allows for only charging TTLEntry write fees if an entry's TTL is actually extended. The side effect of this approach is that TTLEntry writes do not count towards resource write limits. However, TTLEntry has a small, fixed size, so this should not be an issue.

Resize Fees

When the size of an entry is increased, an additional rent_fee must be paid to account for the new size of the entry. This fee is as follows:

rent_fee_for_size_increase(sizeDelta, entryCurrTTL) = rent_fee(sizeDelta, entryCurrTTL)

This additional fee is charged to prevent gamification, where an entry is originally created as the minimal size, pays for the maximum TTL extension, then is resized to a much larger entry. If an entry decreases in size, no additional rent_fee is charged and there is no fee refund (see No Refunds of Rent Fees).

TTL Limits

While the cost of a TTL extension is dynamic based on BucketList size, once an entry’s TTL is extended, it cannot be reduced. This can create potential issues with rent gamification. Consider a smart contract that reserves storage on behalf of other contracts. When the BucketList size is small, the storage provider smart contract can allocate large amounts of storage with very large TTLs. Later when storage is more expensive, the storage provider contract can auction this storage to other smart contracts at rates below the protocol defined rent_fee and make a profit. This is detrimental to network health as it incentivizes intermediate storage contracts to allocate significant amounts of data by writing dummy values that will never be used. If storage intermediaries become the norm, this also harms network performance, where smart contract data access must invoke another smart contract. This is a common issue on several smart contract platforms (see Ethereum’s Gas Token).

Initial TTL

Whenever an entry is initially created or restored, it has a protocol defined minimum TTL by default. This minimum provides a better UX by giving users a reasonable time window to submit a TTL extension operation or invoke a smart contract function that calls the bump host function before an entry expires. This also helps reduce BucketList churn, as entries are guaranteed to live for a certain amount of time before they are eligible for eviction.

TEMPORARY and PERSISTENT storage serve fundamentally different use cases so they have different minimum values. TEMPORARY entries are largely used for non-user facing contract specific data, such as oracle pricing information, nonces, etc. Because TEMPORARY entries are primarily managed by the smart contract itself and may only need to live for very short periods of time, TEMPORARY entries have a smaller minimum TTL. PERSISTENT entries are more likely to be used for user facing entries, such as token balances, and also put additional strain on the network if frequently ARCHIVED and restored. For these reasons, PERSISTENT entries have a longer minimum lifetime.

Interface Limitations

Under this proposal is no way for smart contracts to determine the current TTL of an entry. Additionally, all TTL extensions are conditional (i.e. there is only “extend TTL to at least 100 ledgers”, there is no way to “extend TTL by 100 ledgers no matter the current value). This is intentional. The current TTL extension interface is friendly to parallelism. Because TTL extensions are conditional, two transactions can issue a TTL extension to the same entry without creating a data dependency.

Suppose TX A invokes the host function extend_contract_data(“key”, TEMPORARY, 50) and TX B invokes extend_contract_data(“key”, TEMPORARY, 150). From a state and fee fairness perspective, the execution order of these TXs is irrelevant. Suppose TEMPORARY(“key”) has a current TTL of 10 ledgers.

If execution order is A, B:

  • A sees that the entry’s TTL is 10 ledgers. A extends the entry’s TTL by 40 ledgers such that the resulting TTL is 50. A is charged for 40 ledgers of rent_fees.

  • B sees that the entry’s TTL is 50 ledgers. B extends the entry’s TTL by 100 ledgers such that the resulting TTL is 150. B is charged for 100 ledgers of rent_fees.

  • The resulting state is that TEMPORARY(“key”) has a TTL of 150 ledgers.

If execution order is B, A:

  • B sees that the entry’s TTL is 10 ledgers. B extends the entry’s TTL by 140 ledgers such that the resulting TTL is 150. B is charged for 140 ledgers of rent_fees.
  • A sees that the entry’s TTL is 150 ledgers. A does nothing and is charged no fees.
  • The resulting state is that TEMPORARY(“key”) has a TTL of 150 ledgers.

No matter the execution order, the resulting state is the same. While the fees charged to each individual transaction are dependent on execution order, no single TX is charged more fees than it would have been charged should the other transaction not have occurred. A given TX may be charged less fees if another TX extends the TTL of the same entry, but is never more fees. This means that the TTL extension interface is thread safe from both a state and fairness perspective.

If the entry’s TTL was accessible to smart contract functions, contracts could define arbitrary execution paths based on the TTL value and create data dependencies. If unconditional rent bumps were allowed (i.e. extend the TTL by 100 ledgers no matter the current TTL), the end state of the entry’s TTL would be dependent on execution order.

Should more TTL management functionality be required, these host functions can be added in the future at the cost of parallelism. However, the current interface preserves parallelism and allows contracts to define a guaranteed lower bound TTL for all entries. From a contract execution standpoint, a TTL lower bound guarantee should be sufficient.

No Refunds of Rent Fees

When reducing the size of an entry or deleting an entry that still has a non-zero TTL, there is no rent_fee refund. Due to the variable nature of rent_fee, a rent_fee refund could be gamed for a profit where a lifetime increase is paid for when rent_fee is low, but a refund is issued when rent_fee is high (see Ethereum’s Gas Token).

There is also a fairness issue with offering refunds. While many accounts may pay for TTL extensions, especially for shared entries such as contract instances and contract code, only a single account receives the refund. This opens the door for subtle attacks, where a contract developer could periodically delete a shared entry whose TTL is extended on each contract invocation, effectively stealing fees from invokers of the contract.

EVICTED State

From the perspective of transaction, it does not matter if an ARCHIVED or DEAD entry is EVICTED. Once an entry is no longer LIVE (the current ledger is greater than an entry’s liveUntilLedgerSeq), the entry is inaccessible. This applies whether an ARCHIVED or DEAD entry still remains in the BucketList and validator DB or has been EVICTED.

This distinction is required for validators for operational reasons. Because TTL extensions may define arbitrary extension amounts, an arbitrary number of entries may have TTL of 0 on a given ledger. If entries were immediately EVICTED following their liveUntilLedgerSeq, the validator would have to emit an arbitrary amount of EVICTION meta and update an arbitrary number of database entries. This is especially detrimental because BucketList deletion requires a disk write. For this reason, ARCHIVED/DEAD entries are not immediately EVICTED.

EVICTION Process

In this proposal, only TEMPORARY entries are eligible for eviction. PERSISTENT entries will remain perpetually in the ARCHIVED state in the BucketList and validator DB until a restore operation makes them LIVE.

To EVICT an entry is to delete it from the BucketList and validator DB. Only DEAD TEMPORARY entries can be evicted. Ideally, TEMPORARY entries would be EVICTED as soon as their TTL goes to 0, reducing the size of the BucketList as fast as possible. This is not possible in practice because an arbitrary number of entries can DIE on a given ledger. Instead, we periodically scan a portion of the BucketList and EVICT any DEAD entries in the given scan region as follows.

On each ledger close, a small portion of the BucketList is scanned to check if any entries are eligible for EVICTION. The eviction scan is an expensive process. For each TEMPORARY entry in the region, it is necessary to load its TTLEntry to check if the entry's TTL is 0. Due to this disk read amplification, the amount of bytes scanned per ledger close is limited (maxEvictionScanSize). Additionally, to not overwhelm downstream systems, there is a maximum number of entries that can be evicted in a given ledger (maxEntriesToArchive).

To keep track of the current scan position and so validators joining the network can maintain a uniform scan region, an iterator of the current scan position is stored in the BucketList as a NetworkConfig entry. If a bucket being scanned changes (either via an incoming merge or snap event), the iterator is reset to the beginning of the new bucket in the same position. Due to this, iteration scans begin at level 6 of the BucketList so that there is ample time to finish scanning a given bucket before it is modified. Should the BucketList size increase over time, it may be necessary to change the scan size and maximum allowed evicted entries via network config setting vote.

On each ledger close, the validator scans a small section of the BucketList as follows:

bytes_read = 0
entries_evicted = 0
for bucket_entry in scan_region:
   bytes_read += size(bucket_entry)
   if (!isTemporaryEntry(bucket_entry))
      continue

   ttl_entry = loadAssociatedTtlEntry(bucket_entry)
   if !isLive(ttl_entry):
      meta.evictedTemporaryLedgerKeys.push(ttl_entry)
      meta.evictedTemporaryLedgerKeys.push(bucket_entry)
      delete(ttl_entry)
      delete(bucket_entry)
      ++entries_evicted

   if entries_evicted > EVICTED_MAX or bytes_read > BYTES_MAX:
      break

update_and_write_iterator()

Meta and Downstream System Expectations

TTL Extension and Restore

All TTL extension and restore operations are fundamentally just modifications of the liveUntilLedgerSeq of a given TTLEntry. This means no additional work or special casing is required to emit meta for these operations. The transaction meta for these operations will contain ledger_entry_updated meta events for the updated TTLEntry. This should require no ingestion logic changes to downstream systems (other than support for the new LedgerEntry type, TTLEntry).

EVICTION Meta

Whenever an entry is EVICTED, meta should be emitted to help downstream systems garbage collect. This is not strictly necessary since downstream systems can deduce if an entry is eligible for EVICTION and should be deleted based on the current ledger number and the entry’s liveUntilLedgerSeq. However, this meta is useful for downstream system garbage collection and maintains the current status quo where all BucketList changes are emitted as meta.

Because an arbitrary number of entries can become non-live on a given ledger, it is not possible to emit meta on the ledger in which an entry's TTL goes to 0. Due to this, there is a distinction between ARCHIVED/DEAD and EVICTION states. Meta is not emitted when an entry becomes non-live, but only when it is EVICTED.

Due to this, downstream systems that simulate Soroban transactions must track an entry’s liveUntilLedgerSeq and enforce TX access control themselves. EVICTION meta does not tell downstream systems that an entry has a TTL of 0, but tells downstream systems that the validator has deleted the given entry from its database and that it is safe for the downstream system to also delete the entry.

EVICTION is not caused by a transaction and is not included in transaction meta. Instead, evictedTemporaryLedgerKeys in LedgerCloseMetaV1 contains the keys of evicted TEMPORARY ContractData entries and their associated TTLEntry. Downstream systems should delete these keys from their database when they receive this meta.

This proposal does not evict PERSISTENT entries. evictedPersistentLedgerEntries will always be empty until a future protocol upgrade.

Operations

ExtendFootprintTTLOp

This is a Soroban operation that will bump the TTL of all entries specified in the read-only set of the footprint. If an entry is not LIVE, then that entry will not be bumped. Note that the transaction does not necessarily fail if a non LIVE entry is in the read-only set. The bump will ensure that each LIVE entry in the read-only set has a TTL of at least extendTo ledgers. The operation has the following requirements:

  • ExtendFootprintTTLOp is a Soroban operation, and therefore must be the only operation in a transaction.
  • The read-write set of the footprint must be empty.
  • Every key in the read-only set must be of type ContractData or ContractCode.
  • The transaction needs to populate the SorobanTransactionData transaction extension where readBytes includes the entry size of every entry in the readOnly set plus the size of the TTLEntry for each entry.
  • extendedMetaDataSizeBytes is at least double of readBytes.
  • If extendTo > maxEntryTTL - 1, TX fails. Since the current ledger is included in the extension charges, the max extension value is maxEntryTTL - 1.

This op charges readEntry and readByte fees for each entry in the footprint plus each entry's TTLEntry. There are no write fees. For any entry bumped, rent_fee is charged from the refundableFee (see Fees).

RestoreFootprintOp

This is a Soroban operation that will restore PERSISTENT entries specified in the read-write set of the footprint that are ARCHIVED and make them LIVE. Entries that have been restored will have a lifetime of minPersistentTTL as if the entry was created for the first time. The operation has the following requirements:

  • ExtendFootprintTTLOp is a Soroban operation, and therefore must be the only operation in a transaction.
  • The read-only set of the footprint must be empty.
  • Every key in the read-write set must be of type ContractCode or PERSISTENT ContractData.
  • The transaction needs to populate the SorobanTransactionData transaction extension where writeBytes includes the entry size of every entry in the readWrite.
  • readBytes must include the entry size of every entry in the readWrite set plus the size of the TTLEntry for each entry.
  • extendedMetaDataSizeBytes is at least double of writeBytes.

This op charges readEntry and readByte fees for each entry in the footprint plus each entry's TTLEntry. writeEntry and writeBytes fees are charged for each entry in the readWrite set. For any entry restored, rent_fee is charged from the refundableFee (see Fees).

Host Functions

extend_contract_data(key:Val, type:StorageType, threshold: U32Val, extend_to: U32Val)

For the given ContractData entry specified by key and val, if the current TTL is less than threshold, set the TTL to extend_to ledgers, i.e., extend live_until_ledger_seq such that TTL == extend_to. This host function does not charge any read or write fees, but charges rent_fee if the entry's TTL is extended (see Fees). If extendTo > maxEntryTTL - 1, TX fails. Since the current ledger is included in the extension charges, the max extension value is maxEntryTTL - 1.

extend_current_contract_instance_and_code(threshold: U32Val, extend_to: U32Val)

For the current contract instance and code (if applicable), if the current TTL is less than threshold, set the TTL to extend_to ledgers, i.e., extend live_until_ledger_seq such that TTL == extend_to. If a given entry’s TTL is >= threshold, that entry’s TTL is not changed. Note that if the contract instance TTL < threshold but the contract code TTL >= threshold, only the contract instance TTL will be extended. This host function does not charge any read or write fees, but charges rent_fee if the entry's TTL is extended (see Fees). If extendTo > maxEntryTTL - 1, TX fails. Since the current ledger is included in the extension charges, the max extension value is maxEntryTTL - 1.

extend_contract_instance_and_code(contract:Address, threshold: U32Val, extend_to: U32Val)

Same as extend_current_contract_instance_and_code, but will extend the contract instance and ContractCode entry for contract instead of the current running contract.

get_max_live_until_ledger()

Returns the maximum liveUntilLedgerSeq that an entry can currently have.

get_max_live_until_ledger() == currentLedgerSeq - networkConfig.maxEntryTTL

Resource Utilization

By introducing a self-deleting entry type (TEMPORARY ContractData), this CAP should encourage healthy data usage and reduce the growth of validator databases. The full implementation of state archival will further reduce disk usage.

The eviction scan requires blocking disk IO during ledger close. However, this is done on small batches and should close time increase too much, the batch size can be adjusted via Network Setting vote.

Security Concerns

Given that

  1. Transaction footprints that contain an ARCHIVED PERSISTENT key immediately fail
  2. There is at most one valid version of a given entry at all times

there are no security issues at the protocol level. The greatest protocol security concern is that an entry that is in the ARCHIVED state may be recreated with a different value instead of being restored to its previous value. Because the state of a given key is checked before transaction application, recreation is not possible.

While not a protocol security issue, TEMPORARY ContractData entries expose potential footguns for contract developers. There are three classes of vulnerability:

  1. A TEMPORARY entry's liveUntilLedgerSeq is relied upon for security purposes. For example, suppose a smart contract stores a signature in a TEMPORARY entry and bumps the TTL to 7 days with the expectation that the entry will become non-live and invalidate the signature in 7 days. Because TTL extensions do not require authorization, a byzantine actor could extend the TTL of the entry again, causing the signature to be valid for longer than intended. liveUntilLedgerSeq must never be relied on for security purposes.

  2. A TEMPORARY entry expires and is maliciously recreated. Because TEMPORARY entries can not be restored, the key can be recreated immediately after it's TTL goes to 0. Suppose a contract uses a TEMPORARY entry to store a nonce counter. On creation, the nonce is zero initialized. Throughout the lifetime of the entry, it is incremented several times. Eventually, the nonce entry's TTL goes to 0. At this point, a buggy contract may reinitialize the nonce value to zero, as the entry does not exist and is being recreated. This allows for potential nonce replay attacks. To mitigate this, smart contracts should not rely on the existence of a TEMPORARY entry for anything security related.

These potential footguns are exclusive to TEMPORARY entries. Because PERSISTENT entries can be restored and cannot be recreated after being ARCHIVED, they are not subject to these concerns.

These issues cannot be solved at the protocol level. While TEMPORARY entries do open the door to some security risks, they are also beneficial to contract developers and to network health as a whole. Documentation should warn developers about these potential issues and offer best practices regarding data durability and state archival.