AtomicIP uses a Pedersen commitment scheme to allow inventors to prove they held an idea at a specific time without revealing the idea itself. This document explains how to construct valid commitment hashes and secrets.
The commitment scheme uses SHA-256 hashing with a blinding factor to create a cryptographic commitment:
commitment_hash = sha256(secret || blinding_factor)
Where:
secret- A 32-byte value representing your IP (e.g., a hash of your design document)blinding_factor- A 32-byte random value that hides the secret||- Concatenation operatorsha256- The SHA-256 cryptographic hash function
A valid secret must be:
- Exactly 32 bytes - The secret must be a
BytesN<32>type - Cryptographically random - Use a secure random number generator
- Kept secret - Only you should know the secret until you choose to reveal it
- Unique per commitment - Each IP should have a different secret
For maximum security, construct your secret from your actual IP:
// Example: Creating a secret from a design document
use soroban_sdk::{BytesN, Env};
fn create_secret(env: &Env, design_document: &[u8]) -> BytesN<32> {
// Hash the design document to create a 32-byte secret
let secret: BytesN<32> = env.crypto().sha256(design_document).into();
secret
}You can use any 32-byte value as a secret:
- Hash of a PDF document
- Hash of source code
- Hash of a design schematic
- Randomly generated value (if you can remember it)
The blinding factor is a random value that prevents attackers from guessing your secret through brute force.
use soroban_sdk::{BytesN, Env};
fn generate_blinding_factor(env: &Env) -> BytesN<32> {
// Generate 32 random bytes
let mut random_bytes = [0u8; 32];
env.crypto().random_bytes(&mut random_bytes);
BytesN::from_array(env, &random_bytes)
}- Must be random - Use cryptographically secure random generation
- Must be kept secret - Like the secret, the blinding factor must remain private
- Must be unique - Use a different blinding factor for each commitment
Here's a complete example showing how to create a commitment hash:
use soroban_sdk::{BytesN, Env};
/// Creates a Pedersen commitment hash from a secret and blinding factor.
///
/// # Arguments
///
/// * `env` - The Soroban environment
/// * `secret` - The 32-byte secret representing your IP
/// * `blinding_factor` - The 32-byte random blinding factor
///
/// # Returns
///
/// The 32-byte commitment hash to register on-chain
///
/// # Example
///
/// ```ignore
/// let env = Env::default();
/// let secret = create_secret(&env, b"My invention design");
/// let blinding_factor = generate_blinding_factor(&env);
/// let commitment_hash = create_commitment_hash(&env, &secret, &blinding_factor);
/// ```
fn create_commitment_hash(
env: &Env,
secret: &BytesN<32>,
blinding_factor: &BytesN<32>,
) -> BytesN<32> {
// Concatenate secret || blinding_factor
let mut preimage = soroban_sdk::Bytes::new(env);
preimage.append(&secret.clone().into());
preimage.append(&blinding_factor.clone().into());
// Hash the preimage
let commitment_hash: BytesN<32> = env.crypto().sha256(&preimage).into();
commitment_hash
}- Prepare your secret - Hash your IP document or generate a random 32-byte value
- Generate blinding factor - Create a random 32-byte value
- Concatenate - Combine secret and blinding factor:
secret || blinding_factor - Hash - Compute SHA-256 of the concatenated value
- Register - Submit the commitment hash to the IP registry contract
To verify a commitment, you need the original secret and blinding factor:
use soroban_sdk::BytesN;
/// Verifies that a secret and blinding factor match a commitment hash.
///
/// # Arguments
///
/// * `env` - The Soroban environment
/// * `commitment_hash` - The stored commitment hash to verify against
/// * `secret` - The secret to verify
/// * `blinding_factor` - The blinding factor to verify
///
/// # Returns
///
/// `true` if the secret and blinding factor produce the commitment hash
///
/// # Example
///
/// ```ignore
/// let is_valid = verify_commitment(
/// &env,
/// &stored_commitment_hash,
/// &secret,
/// &blinding_factor
/// );
/// ```
fn verify_commitment(
env: &Env,
commitment_hash: &BytesN<32>,
secret: &BytesN<32>,
blinding_factor: &BytesN<32>,
) -> bool {
let computed_hash = create_commitment_hash(env, secret, blinding_factor);
commitment_hash == &computed_hash
}Every IP commitment is assigned a strength score (0–100) that reflects the entropy and complexity of the commitment hash. Weak commitments (e.g. all-same-byte hashes or zero PoW) score low; strong, high-entropy commitments with meaningful PoW score near 100.
entropy_score = (unique_bytes_in_hash * 50) / 32 // 0–50 points
pow_score = min(50, (pow_difficulty * 50) / 32) // 0–50 points
strength = min(100, entropy_score + pow_score)
| Component | Max Points | Description |
|---|---|---|
| Byte entropy | 50 | Number of unique byte values in the 32-byte commitment hash, scaled to 0–50 |
| PoW difficulty | 50 | Leading-zero-bit difficulty used at commit time, scaled to 0–50 (32 bits = 50 pts) |
let strength: u32 = registry.get_ip_strength(&ip_id);
// Returns 0–100- A SHA-256 hash of real content will have ~30–32 unique bytes → ~47–50 entropy points.
- Using
pow_difficulty = 4(default) adds ~6 points. - A typical real-world commitment scores 53–56 / 100.
- To reach 100, use a high-entropy hash (32 unique bytes) with
pow_difficulty ≥ 32.
A commitment hash derived from a real design document (via SHA-256) will have high byte entropy — the 256 possible byte values are roughly uniformly distributed. A weak hash like [0x01; 32] (all same byte) signals the commitment may not represent genuine IP, and scores near zero.
Without a blinding factor, an attacker could:
- Guess common secrets (e.g., "patent application 2024")
- Hash the guess
- Compare against all commitment hashes
- Identify which commitments match their guess
The blinding factor makes this attack computationally infeasible.
CRITICAL: If you lose your secret and blinding factor, you cannot:
- Prove ownership of your IP
- Complete an atomic swap
- Reveal your IP to buyers
Store your secret and blinding factor securely:
- Use encrypted storage
- Create multiple backups
- Store in different physical locations
- Never share until you're ready to reveal
If someone discovers your secret before you reveal it:
- They can claim they own the IP (but cannot prove it on-chain without your signature)
- They cannot complete a swap (they need your authorization)
- You should still be able to prove ownership via your Stellar wallet signature
// WRONG - Don't do this!
let secret = BytesN::from_array(&env, &[1u8; 32]);
let hash1 = create_commitment_hash(&env, &secret, &blinding_factor1);
let hash2 = create_commitment_hash(&env, &secret, &blinding_factor2);
// If someone discovers the secret, they can claim both IPs// WRONG - Don't do this!
let blinding_factor = BytesN::from_array(&env, &[0u8; 32]); // All zeros
// Attackers can easily guess this// WRONG - Don't do this!
let secret = generate_random_secret();
let commitment_hash = create_commitment_hash(&env, &secret, &blinding_factor);
// If you don't store the secret, you can never prove ownership!// CORRECT - Do this!
let secret = create_secret(&env, my_design_document);
let blinding_factor = generate_blinding_factor(&env);
let commitment_hash = create_commitment_hash(&env, &secret, &blinding_factor);
// Store both securely!
store_secret_securely(&secret);
store_blinding_factor_securely(&blinding_factor);Here's a complete workflow for registering and verifying IP:
use soroban_sdk::{BytesN, Env, Address};
/// Complete workflow for registering IP with a Pedersen commitment
fn register_ip_workflow(env: &Env, owner: &Address, design_document: &[u8]) {
// 1. Create secret from design document
let secret = create_secret(env, design_document);
// 2. Generate random blinding factor
let blinding_factor = generate_blinding_factor(env);
// 3. Create commitment hash
let commitment_hash = create_commitment_hash(env, &secret, &blinding_factor);
// 4. Register on-chain (this is done via the contract)
// let ip_id = registry.commit_ip(owner, &commitment_hash);
// 5. Store secret and blinding factor securely OFF-CHAIN
// This is your responsibility - the blockchain doesn't store these!
store_offchain(&secret, &blinding_factor);
}
/// Later, to verify or complete a swap:
fn verify_ip_workflow(env: &Env, commitment_hash: &BytesN<32>) -> bool {
// 1. Retrieve your secret and blinding factor from secure storage
let (secret, blinding_factor) = retrieve_from_secure_storage();
// 2. Verify they match the commitment
verify_commitment(env, commitment_hash, &secret, &blinding_factor)
}AtomicIP uses SHA-256 because:
- It's cryptographically secure
- It's widely supported in Soroban
- It produces fixed-size 32-byte outputs
- It's resistant to collision attacks
True Pedersen commitments use elliptic curve cryptography and have special properties:
- Homomorphic:
C(m1) * C(m2) = C(m1 + m2) - Perfectly hiding: Commitment reveals nothing about the message
- Computationally binding: Cannot change the message after committing
AtomicIP uses a simpler SHA-256-based scheme because:
- It's easier to implement and verify
- It's sufficient for the use case (proving prior art)
- It has lower gas costs
- It's more accessible to developers
The trade-off is that SHA-256 commitments are not homomorphic, but this property isn't needed for IP registration.
- SHA-256 Wikipedia
- Pedersen Commitment Wikipedia
- Soroban Cryptography Documentation
- NIST SHA-2 Standard
Batch verification allows multiple IP commitments to be verified simultaneously in a single on-chain call. The implementation combines three advanced features:
- Hash aggregation — validated commitment hashes are folded into a single deterministic proof
- Constant-time comparison — all 32-byte comparisons execute in fixed time, preventing timing side-channel attacks
- On-chain event — a
b_vfyevent is emitted with the aggregate proof and summary counts
A single call to batch_verify_commitments processes N verification requests and produces:
- A
Vec<VerifyResult>— one result per request in input order - An aggregate proof hash — a single 32-byte value that cryptographically binds all validated commitments
The aggregate proof is built using incremental SHA-256 hashing:
proof_0 = 0x0000...0000 (32 zero bytes)
proof_1 = sha256(proof_0 || hash_1) # only if verification 1 is valid
proof_2 = sha256(proof_1 || hash_2) # only if verification 2 is valid
...
proof_N = sha256(proof_{N-1} || hash_N)
Where hash_i = sha256(secret_i || blinding_factor_i) is the on-chain commitment hash.
This produces a deterministic, order-dependent proof. The same set of requests in a different order yields a different aggregate proof, preventing replay across reordered batches.
pub fn batch_verify_commitments(
env: Env,
requests: Vec<VerifyRequest>,
) -> Vec<VerifyResult>pub struct VerifyRequest {
pub ip_id: u64,
pub secret: BytesN<32>,
pub blinding_factor: BytesN<32>,
}pub struct VerifyResult {
pub ip_id: u64,
pub valid: bool,
}Each call emits a single event with topic b_vfy and data (aggregate_proof, total_count, valid_count):
topic: (symbol_short!("b_vfy"),)
data: (BytesN<32>, u32, u32) // (aggregate_proof, total, valid)
Off-chain listeners can subscribe to this event to track batch verification completion.
The aggregate proof and summary are stored on-chain under DataKey::BatchVerifyResult(proof_hash):
pub struct BatchVerifyResultStorage {
pub aggregate_proof: BytesN<32>,
pub total_count: u32,
pub valid_count: u32,
}Batch verification processes all requests in a single contract call, amortising the fixed overhead
of storage reads and authentication across N verifications. For large batches this can reduce gas
costs by up to 50% compared to N individual verify_commitment calls.
All commitment hash comparisons use a dedicated constant-time comparator:
fn constant_time_bytes_32_eq(a: &BytesN<32>, b: &BytesN<32>) -> bool {
let a_arr = a.to_array();
let b_arr = b.to_array();
let mut diff: u8 = 0;
for i in 0..32 {
diff |= a_arr[i] ^ b_arr[i];
}
diff == 0
}Every code path performs exactly 32 XOR+OR operations, regardless of how many bytes match. This prevents timing attacks where an adversary could exploit short-circuit equality checks to iteratively guess secret bytes.
| Scenario | Behaviour |
|---|---|
| Empty batch | Returns an empty Vec<VerifyResult>, aggregate proof is sha256(0x00..00), event emitted with total=0, valid=0 |
| Single item | Returns one VerifyResult, aggregate proof equals the commitment hash if valid, or remains the zero seed if invalid |
| Non-existent IP | Panics with IpNotFound — all requests are treated as authoritative; a missing IP is a fatal error |
| All invalid | Aggregate proof remains 0x00..00 (the seed), event shows valid=0 |
| Mixed valid/invalid | Only valid hashes contribute to the aggregate proof; invalid entries are skipped |
use soroban_sdk::{BytesN, Vec};
let secret1: BytesN<32> = /* ... */;
let blind1: BytesN<32> = /* ... */;
let secret2: BytesN<32> = /* ... */;
let blind2: BytesN<32> = /* ... */;
let mut requests = Vec::new(&env);
requests.push_back(VerifyRequest { ip_id: 1, secret: secret1, blinding_factor: blind1 });
requests.push_back(VerifyRequest { ip_id: 2, secret: secret2, blinding_factor: blind2 });
let results: Vec<VerifyResult> = registry.batch_verify_commitments(&requests);
for r in results.iter() {
println!("IP {} valid: {}", r.ip_id, r.valid);
}If you have questions about the commitment scheme:
- Open a GitHub Issue
- Join our Discord community
- Email: support@atomicip.io
IP commitments have an on-chain TTL of approximately 1 year (~6,307,200 ledgers). Owners can renew an expiring commitment without re-committing or changing the commitment hash:
fn renew_ip(ip_id: u64)- Requires owner authorization
- Resets the storage TTL back to
LEDGER_BUMP(~1 year) - Increments an on-chain renewal counter (queryable via
get_renewal_count) - Emits a
renewedevent with(ip_id, renewal_count) - Panics if the IP is revoked or does not exist
The original commitment hash, timestamp, and owner are never modified by renewal — prior art proof is fully preserved.
fn get_renewal_count(ip_id: u64) -> u32Returns how many times the IP has been renewed (0 if never renewed).