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.