Soda Labs
Soda Labs Docs
User InterfaceHttp proxy

Read an encrypted value

Read a value encrypted to a user, and decrypt it locally with their AES key.

A copy-paste example that signs a handle, calls /encrypt-to-user, and decrypts the MPC response with your AES key. Available in Python and JavaScript.

Quick start

  • Requirements: a wallet private key, an AES user key from onboarding, the handle you want to decrypt, default chain 11155111 (Sepolia). Override the proxy with BUBBLE_PROXY_URL if needed.
  • Install dependencies:

Python 3.10+. Use a virtualenv — the SDK pins web3==6.11.2.

pip install soda-bubble-sdk

Example script

encrypt_to_user.py

import base64, json, os, urllib.request, urllib.error
from soda_python_sdk.crypto import decrypt, sign_eip191, verify_signatures
from eth_account import Account
from web3 import Web3

PRIVATE_KEY = os.environ["PRIVATE_KEY"]
USER_AES_KEY = os.environ["USER_AES_KEY"]   # hex, from onboarding
HANDLE = os.environ["HANDLE"]               # decimal or 0x-hex

CHAIN_ID = int(os.environ.get("CHAIN_ID", 11155111))
PROXY_URL = os.environ.get("BUBBLE_PROXY_URL", "https://proxy.bubble.sodalabs.net")
RPC_URL = os.environ.get("RPC_URL", "https://ethereum-sepolia-rpc.publicnode.com")
# GCDecryptionVerifier for your chain — see Contract addresses
VERIFIER = os.environ.get("VERIFIER", "0x336646CF32aD1EdB82a8e31eE94DB1Be91932aea")

account = Account.from_key(PRIVATE_KEY)
handle = int(HANDLE, 16) if HANDLE.startswith("0x") else int(HANDLE)
handle_bytes = handle.to_bytes(32, "big")

signature = sign_eip191(handle_bytes, bytes.fromhex(PRIVATE_KEY[2:]))
body = {
    "handle": base64.b64encode(handle_bytes).decode(),
    "chain_id": CHAIN_ID,
    "user_signature": base64.b64encode(signature).decode(),
}
req = urllib.request.Request(f"{PROXY_URL}/encrypt-to-user", data=json.dumps(body).encode(),
                             headers={"Content-Type": "application/json"}, method="POST")
# urllib raises on 4xx/5xx, so read the body to see why a call was refused
try:
    with urllib.request.urlopen(req, timeout=60) as res:
        data = json.load(res)
except urllib.error.HTTPError as e:
    raise SystemExit(f"HTTP {e.code}: {e.read().decode()}")

encrypted = base64.b64decode(data["output"])

# Verify the evaluators signed this output for this handle, before decrypting it.
# The signature binds handle to output, so a value from another handle is rejected.
w3 = Web3(Web3.HTTPProvider(RPC_URL))
abi = [{"inputs": [], "name": "getSigners", "outputs": [{"type": "address[]"}],
        "stateMutability": "view", "type": "function"}]
signers = w3.eth.contract(address=Web3.to_checksum_address(VERIFIER), abi=abi).functions.getSigners().call()
sigs = [base64.b64decode(s) for s in data.get("mpc_signatures", [])]
if len(sigs) != len(signers) or not verify_signatures(handle_bytes + encrypted, sigs, signers):
    raise SystemExit("encrypt-to-user response failed signature verification — refusing to trust it")

key = bytes.fromhex(USER_AES_KEY)
if len(encrypted) == 64:
    plain = decrypt(key, encrypted[16:32], encrypted[:16], encrypted[48:64], encrypted[32:48])
else:
    plain = decrypt(key, encrypted[16:32], encrypted[:16])
print("Decrypted value:", int.from_bytes(plain, "big"))

Run it

PRIVATE_KEY=0xabc... USER_AES_KEY=deadbeef... HANDLE=12345 python encrypt_to_user.py

The script signs the handle, posts to /encrypt-to-user, prints the encrypted payload, and decrypts it locally using your AES key. A 403 with user is not permitted to make this operation means the address does not hold ACL permission for that handle.

On this page