Contractless/docs/DATA_STORAGE.md

11 KiB

Data Storage

Contractless data storage lets applications record user-owned public data on chain without relying on smart contracts.

Instead of an application contract owning and controlling user data, each wallet writes its own data under an application-style storage key. Other users and applications can read that data, but only the wallet that owns the address can update records under that address.

This is useful for applications that need public, user-controlled records, but it is not a private database and it is not a smart-contract state machine.

What Data Storage Is For

Data storage is designed for application data that should be public, portable, and controlled by the user.

Good examples include:

  • public user profiles
  • usernames and display names
  • social-network posts or profile fields
  • public application preferences
  • swap matchmaking listings
  • loan matchmaking listings
  • public marketplace listings
  • public reputation fields
  • app-specific public metadata

The key idea is user ownership. An application can define the meaning of fields, but the user signs the transaction that writes those fields. That means the user can update their own data at any time.

What Data Storage Is Not For

Do not store sensitive data in plaintext.

Bad examples include:

  • passwords
  • private keys
  • seed phrases
  • API keys
  • authentication tokens
  • private customer records
  • confidential business data
  • anything a user or company would not want permanently public

Data storage should also not be used for data where the user must not be able to change the value themselves.

Bad authoritative-use examples include:

  • account balances controlled by an app
  • proof that a user paid a fee
  • escrow balances
  • internal accounting state
  • access-control state that depends only on a user-written value

If an app treats the mere presence of user-written data as proof of payment, ownership, or entitlement, that app is probably using storage incorrectly. A user can update their own storage fields, so app designers must not treat those fields like trusted server-side state.

Storage Keys

A storage key is the application identification key for a group of user data.

The storage key is created by a StorageKey transaction. The hash of that signed transaction becomes the storage key hash used by later storage value transactions.

Important details:

  • Storage key transaction type: 100
  • Storage key creation fee: 500 CLTC
  • Storage key hash length: 32 bytes
  • Storage key hash display format: 64 hex characters
  • One address can create multiple storage keys.
  • A storage key identifies a data namespace, but it is not owned exclusively by an app creator.

That last point matters. A storage key is best thought of as an app namespace. It lets wallets and applications agree on where to look for data, but users still own their own records under their own wallet address.

For example, an app may publish a storage key and say:

Use this storage key for profile records.
Field username stores the public username.
Field bio stores the public biography.
Field active stores whether the profile is active.

Each user then writes their own values under that storage key.

Storage Fields

Storage values are stored under:

storage_key_hash + wallet_address + field_key

The storage_key_hash identifies the app namespace.

The wallet_address identifies the user whose data is being read or written.

The field_key identifies the app-defined field.

Field keys:

  • must be 1 to 50 bytes
  • are padded internally for fixed-size transaction storage
  • should be stable and predictable for application use

Examples:

username
bio
active
loan_offer
swap_offer
profile_url

Supported Value Types

The data_storage_tx tool supports the following storage value types:

Type Transaction Type Use
bool 101 true or false values
u8 / 8bit 102 small unsigned integer values
u16 / 16bit 103 unsigned 16-bit values
u32 / 32bit 104 unsigned 32-bit values
u64 / 64bit 105 unsigned 64-bit values
u128 / 128bit 106 large unsigned integer values
string 107 public text values up to 180 bytes per chunk

Storage value transaction fee:

1 CLTC

String Storage

String values are stored in chunks of up to 180 bytes.

For short text, use a single string transaction with previous hash 0.

For longer text, later string transactions can point to the previous string transaction hash. Contractless stores those chunks as incremented field keys internally:

bio_00
bio_01
bio_02

The lookup command joins string chunks back together when returning data.

String chains are limited to 20 chunks:

key_00 through key_19

That gives a practical maximum of 3,600 bytes for one logical string field. Larger application content should be split across multiple app-defined fields or handled by an application-specific linking scheme.

Creating Storage Transactions

The storage transaction tool is:

./data_storage_tx

It creates signed transaction JSON files in the same style as the other transaction creation tools. Those transactions can then be broadcast with broadcast_transaction.

Create a Storage Key

./data_storage_tx create_storage

Optional fee override:

./data_storage_tx create_storage 500.00000000

The tool will ask for:

  • wallet file path
  • wallet decryption key

The output includes the transaction hash. That hash is the storage key hash used when writing or reading data under this namespace.

Write a Boolean

./data_storage_tx bool <storage_key_hash> <field_key> <true|false>

Example:

./data_storage_tx bool fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed active true

Write an Unsigned Integer

./data_storage_tx u8 <storage_key_hash> <field_key> <value>
./data_storage_tx u16 <storage_key_hash> <field_key> <value>
./data_storage_tx u32 <storage_key_hash> <field_key> <value>
./data_storage_tx u64 <storage_key_hash> <field_key> <value>
./data_storage_tx u128 <storage_key_hash> <field_key> <value>

Aliases are also accepted:

8bit
16bit
32bit
64bit
128bit

Example:

./data_storage_tx u32 fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed reputation_score 120

Write a String

./data_storage_tx string <storage_key_hash> <field_key> <value> [previous_hash|0]

For the first string chunk, use 0 as the previous hash:

./data_storage_tx string fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed username viraladmin 0

For a follow-up chunk, use the previous string transaction hash:

./data_storage_tx string fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed bio "Second chunk of public profile text" <previous_string_tx_hash>

The previous hash must either be:

  • 0, meaning no previous string chunk
  • a 64-character hash of a previous storage string transaction for the same storage key, field key, and wallet address

Broadcasting Storage Transactions

data_storage_tx creates the transaction file. It does not automatically place the transaction on chain.

Use:

./broadcast_transaction

Then select the saved storage transaction file.

The node validates that:

  • the wallet address exists
  • the signer owns the address being updated
  • the storage key exists before storage values are written
  • the fee is sufficient
  • the field key is valid
  • the value matches the selected type
  • string previous-hash links are valid when used

Only the address owner can write or update records for that address.

Pricing Storage Lookups

Public RPC nodes may charge a fee for returning storage data. The node operator sets the storage lookup fee per byte in settings.ini.

To price a lookup before paying:

./lookup_storage_cost <storage_key_hash> <data_key_or_all> <wallet_address>

Examples:

./lookup_storage_cost fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed username 1439f758f93333734778a6459dc64b8d63a74c06.cltc
./lookup_storage_cost fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed all 1439f758f93333734778a6459dc64b8d63a74c06.cltc

Arguments:

Argument Meaning
storage_key_hash 64-character storage key transaction hash
data_key_or_all one field key, or all to price every stored field for that wallet under that storage key
wallet_address short, long, or vanity address whose stored data will be priced

Example output:

{
  "payment_address": "7ba97ce531937388ef8bca4d8a4ca54cefbefb87.cltc",
  "total_cost": 0.0025,
  "cost_per_byte": 0.0001,
  "total_bytes": 25
}

payment_address is the node wallet that receives the lookup payment.

total_bytes is the size of the compact returned data.

cost_per_byte is the node operator's configured fee per returned byte.

total_cost is the lookup fee before any transfer transaction fee.

Looking Up Storage Data

To retrieve storage data:

./lookup_storage <storage_key_hash> <data_key_or_all> <wallet_address>

Examples:

./lookup_storage fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed username 1439f758f93333734778a6459dc64b8d63a74c06.cltc
./lookup_storage fb674c21877bbb12b85f9572b5851d293ef7ece01c8fa217494feff070bdd0ed all 1439f758f93333734778a6459dc64b8d63a74c06.cltc

The tool first asks the node for a quote. If payment is required, it builds and signs the transfer transaction for the lookup fee, shows the total spend, asks for confirmation, submits the payment, and then prints the returned data.

Example quote shown by lookup_storage:

{
  "payment_address": "7ba97ce531937388ef8bca4d8a4ca54cefbefb87.cltc",
  "lookup_cost": 0.0025,
  "txfee": 0.000025,
  "total_cost": 0.002525,
  "cost_per_byte": 0.0001,
  "total_bytes": 25
}

Example returned data:

{
  "username": "viraladmin"
}

When all is used, the returned object contains every compact field the node found for that wallet under that storage key.

Example:

{
  "username": "viraladmin",
  "active": true,
  "uid": 12345678
}

Lookup Fees and Node Operators

Storage lookup fees are paid to the node serving the lookup. This allows public RPC nodes to earn fees for application data access, including non-mining nodes that serve public RPC requests.

If the requester wallet is the same as the node wallet serving the request, the lookup does not need to pay itself. This lets app creators and node operators query their own node without setting the public lookup fee to zero.

Application Design Notes

Apps should treat storage as public, user-owned, user-editable data.

Good app designs:

  • publish a storage key for app users
  • define stable field names
  • read user records by storage key and wallet address
  • validate user intent with wallet signatures
  • treat user-written fields as claims made by that user

Risky app designs:

  • storing private or sensitive data
  • treating a storage field as proof of payment
  • treating user-written values as trusted app balances
  • assuming an app creator owns all data under a storage key
  • assuming a user cannot later change their own data

Storage is powerful precisely because the user controls their own records. Application rules should be built around that fact.