Skip to content

RAM-LFE

RAM-LFE is Iroha's generic hidden-function layer for programs whose public policy is on-chain but whose evaluator logic, secret, or raw input should not be written to world state. It is used by SORA Nexus identifier flows, such as private phone or email lookup, and can also be exposed as a generic Torii program-execution helper when a node profile enables the app-facing routes.

The chain stores the policy commitment and receipt verification metadata. A resolver or Torii runtime evaluates the hidden program, returns only the allowed output, and attaches a receipt that clients, support tooling, or ledger instructions can verify against the registered policy.

Naming

The naming split matters:

TermMeaning
ram_lfeThe outer hidden-function abstraction: program policies, commitments, execution receipts, and receipt verification mode.
BFVThe Brakerski/Fan-Vercauteren homomorphic encryption scheme used by encrypted-input RAM-LFE backends.
ram_fhe_profileBFV-specific metadata for the programmed encrypted execution machine. It is not a second name for RAM-LFE.

In the data model, RamLfeProgramPolicy and RamLfeExecutionReceipt are RAM-LFE types. BFV parameters, ciphertext envelopes, and the hidden RAM-FHE program profile belong to the encrypted-execution backend used by a policy.

What It Records

A RAM-LFE program policy is globally registered by program_id. The policy contains:

  • the owner account that can activate, deactivate, or otherwise mutate the policy
  • the backend advertised to clients
  • the receipt verification mode, either signed or proof
  • a commitment to the hidden program metadata and evaluator secret
  • the resolver public key for signed receipts
  • optional public encrypted-input metadata, such as BFV parameters and ram_fhe_profile
  • an active flag that controls whether the policy can issue new receipts

The hidden secret, plaintext identifier value, and hidden program body are not stored in world state. Clients should treat commitments, opaque hashes, receipt hashes, ciphertexts, and program digests as opaque protocol values.

Backends

Current RAM-LFE support is centered on three backend identifiers:

BackendUse
hkdf-sha3-512-prf-v1Historical commitment-bound PRF evaluation.
bfv-affine-sha3-256-v1BFV-backed secret affine evaluation over encrypted identifier slots.
bfv-programmed-sha3-256-v1BFV-backed programmed execution over encrypted registers and memory lanes.

For identifier policies, the programmed BFV backend is the important modern path. It lets wallets encrypt normalized input locally, lets the resolver evaluate without seeing a public identifier in the transaction, and returns a receipt that binds the output hash to the registered program policy.

Math

This section describes the implementation-level algebra used by the current RAM-LFE code. It is not a security proof; it is the deterministic transcript and encrypted-evaluation model that policies, receipts, and clients must agree on.

Notation

Let:

  • (H(m)) be Iroha Hash::new(m): Blake2b-32 over m, with the least significant bit of the final byte forced to 1.
  • (N(x)) be the canonical Norito encoding of x.
  • (a \parallel b) mean byte-string concatenation.
  • (\operatorname{le64}(i)) be the 8-byte little-endian encoding of an unsigned integer.
  • (s) be the resolver secret held outside world state.
  • (P) be public policy parameters.
  • (A) be request associated data.
  • (x) be normalized input bytes or a Norito-encoded encrypted-input envelope, depending on the backend.

RAM-LFE uses domain-separated hashes. The formulas below name the domains by purpose; their current byte strings are:

SymbolDomain string
(D_{\mathrm{policy}})iroha.ram_lfe.policy.hkdf_sha3_512_prf.v1
(D_{\mathrm{secret}})iroha.ram_lfe.policy_secret.hkdf_sha3_512_prf.v1
(D_{\mathrm{salt}})iroha.ram_lfe.hkdf_salt.hkdf_sha3_512_prf.v1
(D_{\mathrm{hkdf_opaque}})iroha.ram_lfe.opaque_info.hkdf_sha3_512_prf.v1
(D_{\mathrm{hkdf_receipt}})iroha.ram_lfe.receipt_info.hkdf_sha3_512_prf.v1
(D_{\mathrm{opaque}})iroha.ram_lfe.opaque_hash.hkdf_sha3_512_prf.v1
(D_{\mathrm{receipt}})iroha.ram_lfe.receipt_hash.hkdf_sha3_512_prf.v1
(D_{\mathrm{affine_circuit}})iroha.ram_lfe.bfv_affine.circuit.v1
(D_{\mathrm{affine_opaque}})iroha.ram_lfe.bfv_affine.opaque_hash.v1
(D_{\mathrm{affine_receipt}})iroha.ram_lfe.bfv_affine.receipt_hash.v1
(D_{\mathrm{program_memory}})iroha.ram_lfe.bfv_program.memory.v1
(D_{\mathrm{program_opaque}})iroha.ram_lfe.bfv_program.opaque_hash.v1
(D_{\mathrm{program_receipt}})iroha.ram_lfe.bfv_program.receipt_hash.v1
(D_{\mathrm{program_digest}})iroha.ram_lfe.bfv_program.digest.v1
(D_{\mathrm{output}})iroha.ram_lfe.output_hash.v1
(D_{\mathrm{id_opaque}})iroha.ram_lfe.identifier.opaque_hash.v1
(D_{\mathrm{id_receipt}})iroha.ram_lfe.identifier.receipt_hash.v1
(D_{\mathrm{bfv_keygen}})iroha.crypto.fhe.bfv.keygen.v1
(D_{\mathrm{bfv_encrypt}})iroha.crypto.fhe.bfv.encrypt.v1
(D_{\mathrm{id_keygen}})iroha.crypto.fhe.bfv.identifier.keygen.v1
(D_{\mathrm{id_slot}})iroha.crypto.fhe.bfv.identifier.slot.v1

Policy Commitment

A policy commitment binds the public parameters and hidden resolver secret to a backend. First, the secret is committed separately:

Cs=H(Dsecrets) C_s = H(D_{\mathrm{secret}} \parallel s)

Then the full policy transcript is encoded:

Tpolicy=N(backend,P,Cs) T_{\mathrm{policy}} = N(\mathrm{backend}, P, C_s)

and the published policy hash is:

policy_hash=H(DpolicyTpolicy) \mathrm{policy\_hash} = H(D_{\mathrm{policy}} \parallel T_{\mathrm{policy}})

The on-chain PolicyCommitment is:

(backend,policy_hash,P) (\mathrm{backend}, \mathrm{policy\_hash}, P)

Evaluation recomputes the same value from the runtime secret. If the recomputed hash differs, evaluation fails with a commitment mismatch.

HKDF-SHA3-512 Backend

For hkdf-sha3-512-prf-v1, the output is the normalized input itself, but the opaque identifier and receipt hash are secret-bound PRF outputs.

The request transcript is:

Treq=N(policy_hash,P,A,x) T_{\mathrm{req}} = N(\mathrm{policy\_hash}, P, A, x)

The HKDF salt and pseudorandom key are:

salt=Dsaltpolicy_hash \mathrm{salt} = D_{\mathrm{salt}} \parallel \mathrm{policy\_hash}

PRK=HKDF-ExtractSHA3-512(salt,s) \mathrm{PRK} = \operatorname{HKDF\text{-}Extract}_{\mathrm{SHA3\text{-}512}} (\mathrm{salt}, s)

Opaque material is expanded and hashed:

mo=HKDF-ExpandSHA3-512(PRK,Dhkdf_opaqueTreq,32) m_o = \operatorname{HKDF\text{-}Expand}_{\mathrm{SHA3\text{-}512}} (\mathrm{PRK}, D_{\mathrm{hkdf\_opaque}} \parallel T_{\mathrm{req}}, 32)

opaque_id=H(Dopaquemo) \mathrm{opaque\_id} = H(D_{\mathrm{opaque}} \parallel m_o)

Receipt material additionally binds the opaque id:

mr=HKDF-ExpandSHA3-512(PRK,Dhkdf_receiptTreqopaque_id,32) m_r = \operatorname{HKDF\text{-}Expand}_{\mathrm{SHA3\text{-}512}} (\mathrm{PRK}, D_{\mathrm{hkdf\_receipt}} \parallel T_{\mathrm{req}} \parallel \mathrm{opaque\_id}, 32)

receipt_hash=H(Dreceiptmropaque_id) \mathrm{receipt\_hash} = H(D_{\mathrm{receipt}} \parallel m_r \parallel \mathrm{opaque\_id})

The backend returns:

(output,opaque_id,receipt_hash)=(x,opaque_id,receipt_hash) (\mathrm{output}, \mathrm{opaque\_id}, \mathrm{receipt\_hash}) = (x, \mathrm{opaque\_id}, \mathrm{receipt\_hash})

BFV Primer

BFV is a lattice-based homomorphic encryption scheme. "Homomorphic" means that a program can add and multiply encrypted values and, after decryption, get the same result as if it had performed the additions and multiplications on the plaintext values.

For RAM-LFE, BFV is used as an encrypted-input mechanism:

  1. A wallet normalizes a private value, such as a phone number or email address.
  2. The wallet turns the bytes into small integer slots.
  3. Each slot is encrypted with the resolver's BFV public key.
  4. The resolver runtime evaluates the hidden program over those ciphertexts.
  5. The runtime decrypts only the hidden program output and signs or proves a receipt.

BFV is exact integer arithmetic, not approximate arithmetic. This is why it is better suited to identifier bytes and small modular computations than to floating-point model inference. In Iroha's current BFV usage, each encrypted slot carries one scalar value modulo (t), usually a byte or a byte-length field. The ciphertext itself lives modulo a much larger integer (q). The gap between (q) and (t) gives decryption room for the noise that encryption and homomorphic operations introduce.

A BFV ciphertext has two polynomial components:

c=(c0,c1) c=(c_0,c_1)

The secret key is another polynomial (s_k). Decryption combines the components:

v=c0+c1sk v = c_0 + c_1s_k

If the ciphertext was formed correctly and the noise is still small enough, (v) is close to the scaled plaintext. Rounding recovers the plaintext coefficient modulo (t). The useful property is that ciphertext operations preserve this structure:

Plain operationCiphertext operation
(m+n)Add ciphertext components.
(m+\alpha)Add a scaled plaintext constant into (c_0).
(\alpha m)Scale both ciphertext components by (\alpha).
(mn)Multiply ciphertext polynomials, rescale, then relinearize.

Multiplication is the expensive operation. A product of two two-component ciphertexts naturally creates a three-component ciphertext that decrypts with (1), (s_k), and (s_k^2). Relinearization uses a published evaluation key to fold the (s_k^2) term back into a normal two-component ciphertext. That keeps later additions and multiplications using the same ciphertext shape.

BFV is also "leveled": every encrypted operation consumes some noise budget. This implementation does not bootstrap ciphertexts to refresh that budget. Instead, RAM-LFE publishes a small ram_fhe_profile and accepts only a bounded hidden program shape. That keeps evaluation within the parameter set's supported depth. The current programmed profile allows a fixed register count, fixed memory-lane count, and at most one ciphertext-ciphertext multiplication per programmed step.

In this RAM-LFE design, BFV hides client input from public ledger data and from observers who only see the transaction or route payload. It does not mean the chain executes arbitrary encrypted programs by itself. The Torii resolver runtime still owns the BFV secret material, evaluates the configured hidden program, decrypts the permitted output, and attests the result. The ledger then verifies the attestation against the on-chain policy commitment and resolver public key or proof metadata.

The identifier use case chooses a simple representation on purpose. A normalized string is encoded as:

text
[length, byte_0, byte_1, ..., byte_n, 0, 0, ...]

Each element is encrypted as its own BFV scalar ciphertext. That shape makes normalization and envelope validation explicit, lets wallets build encrypted requests from public parameters, and lets the resolver canonicalize equivalent encrypted inputs into a stable receipt transcript.

BFV Ring Model

The BFV backends use the negacyclic polynomial ring:

Rq=Zq[X]/(Xn+1) R_q = \mathbb{Z}_q[X] / (X^n + 1)

and plaintext ring:

Rt=Zt[X]/(Xn+1) R_t = \mathbb{Z}_t[X] / (X^n + 1)

where:

  • (n) is polynomial_degree, a power of two
  • (q) is ciphertext_modulus
  • (t) is plaintext_modulus
  • (q > t) and (t \mid q)
  • (\Delta = q/t)
  • (B = 2^{\mathrm{decomposition_base_log}})

Plaintext coefficient vectors are encoded by scaling each coefficient:

EncPlain(m)i=Δmimodq \operatorname{EncPlain}(m)_i = \Delta m_i \bmod q

Decryption center-lifts each coefficient of:

v=c0+c1skRq v = c_0 + c_1 s_k \in R_q

then rounds it back into (R_t):

Dec(c)i=tcenterq(vi)qmodt \operatorname{Dec}(c)_i = \left\lfloor \frac{t \cdot \operatorname{center}_q(v_i)}{q} \right\rceil \bmod t

Here (s_k) is the BFV secret-key polynomial, not the outer RAM-LFE resolver secret (s).

BFV Key Generation

For encrypted identifier input, BFV key material is deterministic per resolver secret and associated data:

σid=H(Did_keygenAs) \sigma_{\mathrm{id}} = H(D_{\mathrm{id\_keygen}} \parallel A \parallel s)

The BFV RNG is seeded as:

ChaCha20Rng(H(Dbfv_keygenσid)) \operatorname{ChaCha20Rng}(H(D_{\mathrm{bfv\_keygen}} \parallel \sigma_{\mathrm{id}}))

The key generator samples:

  • (s_k \in {-1,0,1}^n), represented modulo (q)
  • (a \leftarrow R_q) uniformly
  • (e \in {-1,0,1}^n)

The public key is:

pk=(b,a),b=aske(modq) \mathrm{pk}=(b,a),\qquad b = -a s_k - e \pmod q

For relinearization, let (s_k^2) be the ring product in (R_q). For each base-(B) digit (j), sample (a_j) uniformly and (e_j) from the small distribution, then publish:

rlkj=(bj,aj),bj=ajskej+Bjsk2(modq) \mathrm{rlk}_j=(b_j,a_j),\qquad b_j = -a_j s_k - e_j + B^j s_k^2 \pmod q

The public BFV policy metadata contains ((n,q,t,B)), the public key, and max_input_bytes. The BFV secret key and relinearization key stay in the resolver runtime.

BFV Encryption and Operations

To encrypt a plaintext polynomial (m), the implementation seeds another ChaCha20 RNG from:

H(Dbfv_encryptseed) H(D_{\mathrm{bfv\_encrypt}} \parallel \mathrm{seed})

It samples (u,e_1,e_2 \in {-1,0,1}^n) and computes:

c0=bu+e1+EncPlain(m)(modq) c_0 = b u + e_1 + \operatorname{EncPlain}(m) \pmod q

c1=au+e2(modq) c_1 = a u + e_2 \pmod q

The ciphertext is (c=(c_0,c_1)).

Homomorphic addition is component-wise:

c+d=(c0+d0, c1+d1)(modq) c+d=(c_0+d_0,\ c_1+d_1)\pmod q

Adding a plaintext scalar (\alpha) to coefficient zero changes only (c_0):

c+α=(c0+Δα, c1)(modq) c+\alpha = (c_0 + \Delta\alpha,\ c_1)\pmod q

Multiplying by a plaintext scalar (\alpha) scales both components:

αc=(αc0, αc1)(modq) \alpha c = (\alpha c_0,\ \alpha c_1)\pmod q

For two ciphertexts (c=(c_0,c_1)) and (d=(d_0,d_1)), ciphertext multiplication first computes a size-three ciphertext and scales each coefficient back by (t/q):

c~0=t(c0d0)qmodq \tilde c_0 = \left\lfloor \frac{t(c_0 d_0)}{q} \right\rceil \bmod q

c~1=t(c0d1+c1d0)qmodq \tilde c_1 = \left\lfloor \frac{t(c_0 d_1 + c_1 d_0)}{q} \right\rceil \bmod q

c~2=t(c1d1)qmodq \tilde c_2 = \left\lfloor \frac{t(c_1 d_1)}{q} \right\rceil \bmod q

All products above are negacyclic ring products in (R_q). Then (\tilde c_2) is decomposed into base-(B) polynomials:

c~2=jBjuj \tilde c_2 = \sum_j B^j u_j

and relinearized:

c0=c~0+jujbj(modq) c'_0 = \tilde c_0 + \sum_j u_j b_j \pmod q

c1=c~1+jujaj(modq) c'_1 = \tilde c_1 + \sum_j u_j a_j \pmod q

The result is again a two-component BFV ciphertext.

Identifier Ciphertext Envelope

An identifier input byte string:

x=(x0,,x1) x=(x_0,\ldots,x_{\ell-1})

is encoded into scalar slots:

m0= m_0 = \ell

mi+1=xi,0i< m_{i+1}=x_i,\qquad 0 \le i < \ell

and all remaining slots are zero up to max_input_bytes + 1. Each scalar slot is encrypted as the coefficient-zero plaintext polynomial ([m_i]). The per-slot encryption seed is:

σi=H(Did_slotseedle64(i)) \sigma_i = H(D_{\mathrm{id\_slot}} \parallel \mathrm{seed} \parallel \operatorname{le64}(i))

The encrypted identifier envelope is:

(BFV.Encpk([m0];σ0),,BFV.Encpk([mM];σM)) (\operatorname{BFV.Enc}_{\mathrm{pk}}([m_0];\sigma_0),\ldots, \operatorname{BFV.Enc}_{\mathrm{pk}}([m_M];\sigma_M))

where (M=\mathrm{max_input_bytes}).

BFV Affine Backend

For bfv-affine-sha3-256-v1, the runtime first derives BFV key material from (s) and (A). The derived public parameters must exactly match the public parameters committed on-chain.

The affine circuit seed is:

σaffine=H(Daffine_circuitspolicy_hashA) \sigma_{\mathrm{affine}} = H(D_{\mathrm{affine\_circuit}} \parallel s \parallel \mathrm{policy\_hash} \parallel A)

From this seed the runtime samples, modulo (t), a 32-row affine circuit:

yj=bj+iwj,imi(modt),0j<32 y_j = b_j + \sum_i w_{j,i} m_i \pmod t, \qquad 0 \le j < 32

where (m_i) are the decrypted identifier slots. Homomorphically, it computes the same value over ciphertexts:

Cj=bj+iwj,iCi C_j = b_j + \sum_i w_{j,i} C_i

The resolver decrypts each (C_j), requires all trailing plaintext coefficients to be zero, converts the coefficient-zero values to bytes, and forms:

O=(y0,,y31) O=(y_0,\ldots,y_{31})

Then:

opaque_id=H(Daffine_opaquepolicy_hashO) \mathrm{opaque\_id} = H(D_{\mathrm{affine\_opaque}} \parallel \mathrm{policy\_hash} \parallel O)

receipt_hash=H(Daffine_receiptpolicy_hashOopaque_id) \mathrm{receipt\_hash} = H(D_{\mathrm{affine\_receipt}} \parallel \mathrm{policy\_hash} \parallel O \parallel \mathrm{opaque\_id})

BFV Programmed Backend

For bfv-programmed-sha3-256-v1, public parameters wrap the BFV identifier encryption parameters plus a hidden-program digest:

program_digest=H(Dprogram_digestN(program)) \mathrm{program\_digest} = H(D_{\mathrm{program\_digest}} \parallel N(\mathrm{program}))

The current RAM-FHE profile is:

FieldValue
profile_version1
register_count4
memory_lane_count32
ciphertext_mul_per_step1
encrypted_input_moderesolver_canonicalized_envelope_v1
min_ciphertext_modulus(2^{52})

Plaintext input submitted to Torii is encrypted into the same BFV envelope before execution. The deterministic seed for that server-side encryption is:

H("iroha.ram_lfe.execute.plaintext_bfv.v1"N(program_id)x) H( \texttt{"iroha.ram\_lfe.execute.plaintext\_bfv.v1"} \parallel N(\mathrm{program\_id}) \parallel x )

For externally supplied encrypted input, the resolver decrypts the identifier envelope and re-encrypts it onto this deterministic envelope before executing. That canonicalization keeps receipt hashes stable across semantically equal BFV ciphertexts.

Initial encrypted memory lanes are derived from:

σmem=H(Dprogram_memoryspolicy_hashAle64(0)) \sigma_{\mathrm{mem}} = H(D_{\mathrm{program\_memory}} \parallel s \parallel \mathrm{policy\_hash} \parallel A \parallel \operatorname{le64}(0))

For each of 32 lanes, the runtime samples (r_j \in [0,t)) and stores a BFV ciphertext encrypting (r_j). The hidden program then executes over encrypted registers and encrypted memory:

InstructionAlgebra
LoadInput(dst, i)(R_{\mathrm{dst}} \leftarrow C_i)
LoadState(dst, j)(R_{\mathrm{dst}} \leftarrow S_j)
StoreState(j, src)(S_j \leftarrow R_{\mathrm{src}})
LoadConst(dst, a)(R_{\mathrm{dst}} \leftarrow \operatorname{Enc}(a))
Add(dst, a, b)(R_{\mathrm{dst}} \leftarrow R_a + R_b)
AddPlain(dst, src, a)(R_{\mathrm{dst}} \leftarrow R_{\mathrm{src}} + a)
SubPlain(dst, src, a)(R_{\mathrm{dst}} \leftarrow R_{\mathrm{src}} - a)
MulPlain(dst, src, a)(R_{\mathrm{dst}} \leftarrow aR_{\mathrm{src}})
Mul(dst, a, b)(R_{\mathrm{dst}} \leftarrow R_aR_b), then relinearize
SelectEqZero(dst, cond, z, nz)Decrypt (R_{\mathrm{cond}}); choose (R_z) when it is zero, otherwise (R_{nz}).
Output(src)Append (R_{\mathrm{src}}) to the output register list.

After the instruction tape finishes, the resolver decrypts each output register, converts coefficient zero to a byte, and concatenates those bytes:

O=bytes(Dec(Ro0)0,,Dec(Rok)0) O = \operatorname{bytes}(\operatorname{Dec}(R_{o_0})_0,\ldots, \operatorname{Dec}(R_{o_k})_0)

The generic programmed backend hashes are:

opaque_hash=H(Dprogram_opaquepolicy_hashO) \mathrm{opaque\_hash} = H(D_{\mathrm{program\_opaque}} \parallel \mathrm{policy\_hash} \parallel O)

receipt_hashprogram=H(Dprogram_receiptpolicy_hashOopaque_hash) \mathrm{receipt\_hash}_{\mathrm{program}} = H(D_{\mathrm{program\_receipt}} \parallel \mathrm{policy\_hash} \parallel O \parallel \mathrm{opaque\_hash})

The default programmed identifier tape has 64 input slots. For each slot (i), it loads the input slot, loads memory lane (i \bmod 32), adds them, and outputs the result:

R0Ci,R1Simod32,R2R0+R1,Output(R2) R_0 \leftarrow C_i,\qquad R_1 \leftarrow S_{i\bmod 32},\qquad R_2 \leftarrow R_0 + R_1,\qquad \operatorname{Output}(R_2)

Output Hashes and Receipts

The generic RAM-LFE execution receipt does not sign the raw output. It signs the output hash:

output_hash=H(DoutputO) \mathrm{output\_hash} = H(D_{\mathrm{output}} \parallel O)

For Torii RAM-LFE execution receipts, associated data is the canonical program identifier bytes:

A=N(program_id) A = N(\mathrm{program\_id})

associated_data_hash=H(A) \mathrm{associated\_data\_hash}=H(A)

The signed receipt payload is:

R=(program_id,program_digest,backend,verification_mode,output_hash,associated_data_hash,executed_at_ms,expires_at_ms) R = (\mathrm{program\_id}, \mathrm{program\_digest}, \mathrm{backend}, \mathrm{verification\_mode}, \mathrm{output\_hash}, \mathrm{associated\_data\_hash}, \mathrm{executed\_at\_ms}, \mathrm{expires\_at\_ms})

For signed mode:

attestation=Signresolver(N(R)) \mathrm{attestation} = \operatorname{Sign}_{\mathrm{resolver}}(N(R))

Verification checks the signature with resolver_public_key and rejects the receipt unless all of these equalities hold:

R.program_id=policy.program_id R.\mathrm{program\_id} = \mathrm{policy.program\_id}

R.backend=policy.backend R.\mathrm{backend} = \mathrm{policy.backend}

R.verification_mode=policy.verification_mode R.\mathrm{verification\_mode} = \mathrm{policy.verification\_mode}

R.program_digest=policy.public_parameters.hidden_program_digest R.\mathrm{program\_digest} = \mathrm{policy.public\_parameters.hidden\_program\_digest}

R.associated_data_hash=H(N(policy.program_id)) R.\mathrm{associated\_data\_hash} = H(N(\mathrm{policy.program\_id}))

If the caller supplies output_hex, the verifier also checks:

H(Doutputbytes(output_hex))=R.output_hash H(D_{\mathrm{output}} \parallel \operatorname{bytes}(\mathrm{output\_hex})) = R.\mathrm{output\_hash}

For proof mode, the attestation carries a proof envelope instead of a signature. Verification checks that the proof backend, circuit id, public-input schema hash, verifying-key hash, and exposed public instances match the proof verifier metadata and the encoded receipt-payload hash. Let:

hR=H(N(R))=(h0,,h31) h_R = H(N(R)) = (h_0,\ldots,h_{31})

The expected public instances are four one-element columns. Column (j) contains bytes (h_{8j}\ldots h_{8j+7}) followed by 24 zero bytes:

instancej=h8jh8j+7024,0j<4 \mathrm{instance}_j = h_{8j}\parallel\cdots\parallel h_{8j+7}\parallel 0^{24}, \qquad 0 \le j < 4

Identifier Projection

Identifier resolution does not use the generic backend opaque_hash as the user-facing opaque account identifier. It projects the RAM-LFE output hash through identifier-specific domains:

opaque_idid=H(Did_opaqueN(program_id)output_hash) \mathrm{opaque\_id}_{\mathrm{id}} = H(D_{\mathrm{id\_opaque}} \parallel N(\mathrm{program\_id}) \parallel \mathrm{output\_hash})

receipt_hashid=H(Did_receiptN(program_id)output_hashopaque_idid) \mathrm{receipt\_hash}_{\mathrm{id}} = H(D_{\mathrm{id\_receipt}} \parallel N(\mathrm{program\_id}) \parallel \mathrm{output\_hash} \parallel \mathrm{opaque\_id}_{\mathrm{id}})

An IdentifierResolutionReceipt signs a higher-level payload:

I=(policy_id,R,opaque_idid,receipt_hashid,uaid,account_id) I = (\mathrm{policy\_id}, R, \mathrm{opaque\_id}_{\mathrm{id}}, \mathrm{receipt\_hash}_{\mathrm{id}}, \mathrm{uaid}, \mathrm{account\_id})

For signed identifier receipts:

attestation=Signresolver(N(I)) \mathrm{attestation} = \operatorname{Sign}_{\mathrm{resolver}}(N(I))

ClaimIdentifier accepts the receipt only when the signature or proof is valid, the embedded RAM-LFE execution payload matches the referenced program policy, and the uaid and account_id are the binding being claimed.

Execution Flow

A generic RAM-LFE execution follows this shape:

  1. Governance or an operator registers RamLfeProgramPolicy.
  2. The owner activates the policy.
  3. The client reads the public policy metadata from Torii.
  4. The client submits exactly one input form to the resolver: plaintext input_hex or an encrypted BFV input envelope.
  5. The runtime evaluates the hidden program and returns output_hex, output_hash, opaque_hash, receipt_hash, and a RamLfeExecutionReceipt.
  6. The client or backend verifies the receipt against the published policy, optionally checking that the returned output_hex hashes to the receipt's output_hash.
  7. A higher-level instruction, such as ClaimIdentifier, can embed the attested receipt instead of embedding the raw input.

Identifier Policies

Identifier policies are a concrete use of RAM-LFE. They add a business namespace and normalization rule on top of a generic program policy:

text
RegisterRamLfeProgramPolicy(
  program_id = "phone_team",
  owner = "<POLICY_OWNER>",
  backend = "bfv-programmed-sha3-256-v1",
  verification_mode = "signed",
  commitment = "<HIDDEN_PROGRAM_POLICY_COMMITMENT>",
  resolver_public_key = "<RESOLVER_PUBLIC_KEY>"
)
ActivateRamLfeProgramPolicy(program_id = "phone_team")

RegisterIdentifierPolicy(
  id = "phone#team",
  owner = "<POLICY_OWNER>",
  normalization = "PhoneE164",
  program_id = "phone_team",
  note = "Private phone registration for team dataspace"
)
ActivateIdentifierPolicy(policy_id = "phone#team")

The identifier layer uses the RAM-LFE receipt to bind:

  • policy_id
  • the opaque identifier derived by the hidden function
  • the deterministic receipt_hash
  • the account's UAID
  • the canonical account_id
  • the generic RAM-LFE execution payload

For user-facing onboarding, keep account aliases separate from private identifiers. Aliases are public names; phone numbers, email addresses, and similar values should flow through identifier policies and receipts.

Torii Routes

When the app-facing route family is enabled, Torii exposes RAM-LFE and identifier helpers:

RoutePurpose
GET /v1/ram-lfe/program-policiesList active and inactive RAM-LFE program policies and public execution metadata.
POST /v1/ram-lfe/programs/{program_id}/executeExecute one program from input_hex or encrypted_input and return output hashes plus a stateless receipt.
POST /v1/ram-lfe/receipts/verifyVerify a RamLfeExecutionReceipt against the published policy and optionally compare output_hex to output_hash.
GET /v1/identifier-policiesList identifier policies, normalization modes, resolver keys, and encrypted-input metadata.
POST /v1/accounts/{account_id}/identifiers/claim-receiptIssue the receipt that a user can embed in ClaimIdentifier.
POST /v1/identifiers/resolveResolve a normalized identifier input to the bound account when an active claim exists.
GET /v1/identifiers/receipts/{receipt_hash}Look up a persisted identifier claim by receipt hash for audit and support tooling.

Always check the target node's /openapi or /openapi.json document before building against these routes. Availability depends on the node build and network profile.

Node Runtime

Torii's in-process RAM-LFE runtime is configured under torii.ram_lfe.programs[*], keyed by program_id. Each configured program must match the on-chain policy commitment and must provide the runtime material needed to evaluate and attest receipts. Identifier routes reuse this same runtime; they do not require a separate identifier-resolver config surface.

Registering a policy on-chain is not enough by itself. A target node must also expose the route family and have matching runtime material for the programs it is expected to execute.

Operational Guardrails

  • Register policies inactive, verify the public metadata, then activate them.
  • Keep hidden evaluator secrets, resolver signing keys, and BFV secret material out of docs, logs, transactions, and client bundles.
  • Do not put raw identifiers in account aliases, transaction metadata, events, or world-state fields.
  • Verify receipts client-side before submitting higher-level instructions when the SDK exposes a verifier.
  • Use expiry fields where stale receipts should not remain valid forever.
  • Rotate by registering a new program or identifier policy, migrating clients, and deactivating the old policy once new receipts are flowing.