Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions content/sdks/identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ Each name below is exported from `@citratelabs/sdk` (`identity` namespace) and m
| `discover()` | Fetch and cache the OIDC discovery document; the issuer is pinned to the artifact. |
| `authorizeUrl({state, nonce, pkce?})` | Build the PKCE S256 authorize URL; returns the URL and the PKCE pair. |
| `exchangeCode({code, codeVerifier, nonce?})` | Exchange a code for tokens; the ID token is verified before return. |
| `refresh(refreshToken)` | Rotate tokens with a refresh token. |
| `siweChallenge(address)` / `siweVerify({message, signature})` | EIP-4361 sign-in with a single-use nonce. |
| `refresh(refreshToken, expectedSub)` | Rotate tokens with a refresh token. The refreshed ID token must name the same `sub` (Python: `refresh(refresh_token, expected_sub)`). |
| `siweChallenge()` / `buildSiweMessage({address, nonce})` / `siweVerify({message, signature})` | EIP-4361 sign-in. `siweChallenge` GETs a single-use nonce, `buildSiweMessage` builds the message the authority accepts (chain 40204, expiry of at most 24 h), and `siweVerify` returns `{kind: 'redirect', redirectTo}` inside an OIDC login or `{kind: 'token', idToken, claims}` for the headless grant. Python: `siwe_challenge()`, `build_siwe_message(...)`, `siwe_verify(...)`. |
| `userInfo(accessToken)` | Fresh claims plus a normalized tier and capability set. |
| `logout(accessToken)` | End the session; fires the cross-instance revocation cascade. |

Expand All @@ -118,7 +118,8 @@ Each name below is exported from `@citratelabs/sdk` (`identity` namespace) and m
`verifyIdToken(token, { issuer, audience, jwks })` is the trust boundary and is called for you by
`exchangeCode` and `refresh`. It accepts only `RS256`, verifies the signature before reading any claim, and
rejects `alg:none`, algorithm confusion, a wrong audience or issuer, an expired or not-yet-valid token, a
tampered payload, and a token whose `kid` matches no key.
tampered payload, and a token whose `kid` matches no key. It also requires numeric `exp` and `iat` claims
(an `iat` more than the clock tolerance in the future is refused) and a `typ` of `JWT` or none.

### The embedded account

Expand Down
2 changes: 1 addition & 1 deletion content/sdks/js.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ The hooks are `useCitrateClient`, `useModelDeployment`, `useInference`, `useMode

### Constants and errors

- `src/utils/constants.ts`: `CHAIN_IDS` (`TESTNET: 40204`; releases up to 0.2.x also carry `MAINNET: 1`, which is Ethereum mainnet's chain id, not Citrate's, so do not use it: Citrate's network is 40204 and mainnet keeps that id), `DEFAULT_RPC_URLS`,
- `src/utils/constants.ts`: `CHAIN_IDS` (`TESTNET: 40204`; releases before 0.2.3 also carried `MAINNET: 1`, which is Ethereum mainnet's chain id, not Citrate's. It was removed in 0.2.3. Citrate's network is 40204, and mainnet keeps that id), `DEFAULT_RPC_URLS`,
`DEFAULT_WS_URLS`, `PRECOMPILE_ADDRESSES`, `GAS_LIMITS`, `TIMEOUTS`, `MODEL_LIMITS`, `ENCRYPTION`, `EVENTS`,
and `API_ENDPOINTS`.
- `src/errors/CitrateError.ts`: `CitrateError`, `ModelNotFoundError`, `InsufficientFundsError`, and
Expand Down
4 changes: 3 additions & 1 deletion content/sdks/marketplace.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,9 @@ at 2.0×). Error type: `MarketplaceError`.
| `parseJobEvents`, `parseCreditsEvents`, `parseTrainingEvents` | `(logs: Log[]) => Event[]` | Decode receipt logs into typed events. |
| `grainsToSalt`, `grainsToSaltDisplay` | `(grains: bigint) => string` | Display formatters; SALT has 18 decimals, and "grains" are its wei. |

`PostJobArgs` carries `modelHash`, `input` (bytes or a CID; keccak256-hashed if bytes), `maxPriceGrains`,
`PostJobArgs` carries `modelHash`, `input` (the raw input bytes, which are keccak256-hashed, or a `Hex` that is
already the 32-byte keccak256 of the input; providers only bid when `inputHash` is `keccak256(input)`, so a CID
or other pre-hash is refused), `maxPriceGrains`,
`tier`, `bidWindowBlocks`, `execWindowBlocks`, and an optional `paymentMethod`.

## Design rationale
Expand Down
58 changes: 37 additions & 21 deletions content/sdks/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,37 +95,53 @@ Source: `citrate_sdk/client.py` (class `CitrateClient`), exported from `citrate_

| Method | Signature | Notes |
|---|---|---|
| `__init__` | `(rpc_url="http://localhost:8545", private_key=None, allow_insecure_http=False)` | `client.py:31`. Read-only without a key. |
| `get_chain_id()` | `-> int` | `eth_chainId`, `client.py:103`. |
| `get_balance(address)` | `-> int` | wei, `eth_getBalance`, `client.py:107`. |
| `get_nonce(address)` | `-> int` | pending nonce, `eth_getTransactionCount`, `client.py:112`. |
| `deploy_model(model_path, config)` | `-> ModelDeployment` | needs a key; hashes, optionally encrypts, uploads to IPFS, deploys via precompile `0x...0100`, `client.py:117`. |
| `inference(model_id, input_data, encrypted=False, max_gas=1000000, recipient_public_key=None)` | `-> InferenceResult` | precompile `0x...0101`; the encrypted path fails closed without `recipient_public_key`, `client.py:192`. |
| `get_model_info(model_id)` | `-> Dict` | `citrate_getModel`, raises `ModelNotFoundError`, `client.py:267`. |
| `list_models(owner=None, limit=100)` | `-> List[Dict]` | `citrate_listModels`, `client.py:277`. |
| `purchase_model_access(model_id, payment_amount)` | `-> str` | needs a key; access-control precompile `0x...0104`, `client.py:282`. |

Signing binds `chainId` under EIP-155 (`_eip155_chain_id`, `client.py:312`) so a signature cannot be replayed
on another network. IPFS upload fails closed rather than fabricating a fallback CID (`client.py:298`). A
| `__init__` | `(rpc_url="http://localhost:8545", private_key=None, allow_insecure_http=False)` | `client.py`. Read-only without a key. |
| `get_chain_id()` | `-> int` | `eth_chainId`, `client.py`. |
| `get_balance(address)` | `-> int` | wei, `eth_getBalance`, `client.py`. |
| `get_nonce(address)` | `-> int` | pending nonce, `eth_getTransactionCount`, `client.py`. |
| `deploy_model(model_path, config)` | `-> ModelDeployment` | needs a key; hashes, optionally encrypts, uploads to IPFS, deploys via precompile `0x...0100`, `client.py`. |
| `inference(model_id, input_data, encrypted=False, max_gas=1000000, recipient_public_key=None)` | `-> InferenceResult` | precompile `0x...0101`; the encrypted path fails closed without `recipient_public_key`, `client.py`. |
| `get_model_info(model_id)` | `-> Dict` | `citrate_getModel`, raises `ModelNotFoundError`, `client.py`. |
| `list_models(owner=None, limit=100)` | `-> List[Dict]` | `citrate_listModels`, `client.py`. |
| `purchase_model_access(model_id, payment_amount)` | `-> str` | needs a key; access-control precompile `0x...0104`, `client.py`. |

Signing binds `chainId` under EIP-155 (`_eip155_chain_id` in `client.py`) so a signature cannot be replayed
on another network. IPFS upload fails closed rather than fabricating a fallback CID (`_upload_to_ipfs` in `client.py`). A
private key creates a `KeyManager` on `client.key_manager` (`citrate_sdk/crypto.py`), which exposes
`get_address()`, `get_private_key()`, and the ECDH helpers used by encrypted inference.

Model-key threshold sharing uses `KeyManager.encrypt_model_with_key_shares(data, config)`, where `config` is an
`EncryptionConfig` with `threshold_shares`, `total_shares` and one distinct `share_holder_public_keys` entry per
share. Each share is wrapped to its holder and returned for off-chain delivery; `deploy_model` returns them as
`deployment.key_share_envelopes`, and nothing share-related is written on-chain. Holders open their share with
`unwrap_key_share(share_record, owner_public_key)` (the first argument is the whole record from
`key_share_envelopes`, with its `x`, `threshold`, `holder_public_key` and `envelope` fields) and rebuild the key with
`reconstruct_key_from_shares(shares, threshold)`. `threshold_shares=1` requires
`allow_single_holder_recovery=True`. IPFS downloads require `expected_sha256` for content addresses that cannot
verify themselves, unless you pass `verify=False` explicitly.

### Economic and education managers

These are separate classes, not attributes of `CitrateClient`. Each takes the `_rpc_call` callable, an
optional `default_account` (required for writes), `gas_limit`, `gas_price`, and the addresses it acts on.
Most take a `contract_addresses` dict; `StakingManager` and `ClassroomManager` instead take a single
`staking_address` or `classroom_address`. Writes raise `ConfigurationError` when `default_account` is unset;
read methods are `eth_call`-only and need no account.
read methods are `eth_call`-only and need no account. Every write first checks `eth_chainId` against the pinned
chain (40204 by default; pass `chain_id=` to target another Citrate network) and refuses to send on a mismatch.
Unknown `access`, `tier` or `mode` strings raise `ValueError`. A classroom invite is a key pair:
`ClassroomManager.create` (and `rotate_invite_code`) registers the invite key's commitment and returns the invite
secret in `last_invite_code`. Share that secret with students out of band. A student calls
`enroll_with_invite(secret)`, which signs an enrolment proof bound to the student's account, and only the invite
key and that proof go on-chain. `enroll` is deprecated.

| Manager | Source | Selected methods |
|---|---|---|
| `LearningManager` | `learning.py:176` | `list_pools`, `join_pool`, `leave_pool`, `create_pool`, `get_cycle_status`, `register_for_cycle`, `claim_cycle_reward`, `get_contributions`, `claim_contribution_rewards` |
| `StakingManager` | `learning.py:461` | `deposit`, `withdraw`, `claim_withdrawal`, `get_info`, `preview_deposit`, `preview_withdraw`, `get_withdrawal` |
| `ClassroomManager` | `learning.py:629` | `create`, `enroll`, `unenroll`, `deploy_model`, `remove_model`, `rotate_invite_code`, `get_classroom`, `can_student_access_model`, `get_student_teacher` |
| `ComputeManager` | `compute.py:72` | `post_job`, `bid_on_job`, `get_job`, `list_jobs`, `submit_result`, `register_provider`, `get_provider_info`, `heartbeat`, `create_pool`, `join_pool`, `leave_pool`, `get_pools`, `dispute_result`, `get_dispute` |
| `TreasuryManager` | `treasury.py:60` | `deposit_stablecoin`, `purchase_compute_credits`, `get_credit_balance`, `estimate_calls_remaining`, `get_treasury_value`, `get_epoch_revenue`, `get_current_epoch`, `get_stablecoin_balance`, `get_total_distributed`, `get_credit_price_usd` |
| `FarmingManager` | `farming.py:56` | `get_my_score`, `get_my_share`, `get_leaderboard`, `claim`, `has_claimed`, `get_distribution_info`, `is_in_snapshot`, `get_claimed_amount` |
| `LearningManager` | `learning.py` | `list_pools`, `join_pool`, `leave_pool`, `create_pool`, `get_cycle_status`, `register_for_cycle`, `claim_cycle_reward`, `get_contributions`, `claim_contribution_rewards` |
| `StakingManager` | `learning.py` | `deposit`, `withdraw`, `claim_withdrawal`, `get_info`, `preview_deposit`, `preview_withdraw`, `get_withdrawal` |
| `ClassroomManager` | `learning.py` | `create`, `enroll_with_invite`, `unenroll`, `deploy_model`, `remove_model`, `rotate_invite_code`, `get_classroom`, `can_student_access_model`, `get_student_teacher` |
| `ComputeManager` | `compute.py` | `post_job`, `bid_on_job`, `get_job`, `list_jobs`, `submit_result`, `register_provider`, `get_provider_info`, `heartbeat`, `create_pool`, `join_pool`, `leave_pool`, `get_pools`, `dispute_result`, `get_dispute` |
| `TreasuryManager` | `treasury.py` | `deposit_stablecoin`, `purchase_compute_credits`, `get_credit_balance`, `estimate_calls_remaining`, `get_treasury_value`, `get_epoch_revenue`, `get_current_epoch`, `get_stablecoin_balance`, `get_total_distributed`, `get_credit_price_usd` |
| `FarmingManager` | `farming.py` | `get_my_score`, `get_my_share`, `get_leaderboard`, `claim`, `has_claimed`, `get_distribution_info`, `is_in_snapshot`, `get_claimed_amount` |

All six classes are re-exported from `citrate_sdk/__init__.py`. Shared data types (`LearningPool`,
`CycleStatus`, `ComputeJob`, `ProviderInfo`, `StakingInfo`, and the rest) live in `citrate_sdk/types.py`;
Expand Down Expand Up @@ -175,10 +191,10 @@ and we say so rather than paper over it.

## Failure modes

- Encrypted inference without `recipient_public_key` fails closed (`client.py:208`). The symmetric key is
- Encrypted inference without `recipient_public_key` fails closed (`inference` in `client.py`). The symmetric key is
ECDH-wrapped to the recipient and is never shipped in cleartext on public calldata.
- A signed transaction binds `chainId` via EIP-155, so it cannot be replayed on a different network.
- IPFS upload failures propagate; `deploy_model` never invents a fallback CID (`client.py:298`).
- IPFS upload failures propagate; `deploy_model` never invents a fallback CID (`_upload_to_ipfs` in `client.py`).
- A manager write without `default_account` raises `ConfigurationError`. Reads are `eth_call`-only and need
no account.
- A remote `http://` RPC endpoint raises a cleartext-transport warning. Use `https://`, or set
Expand Down
Loading