1. Overview
ChainSignal has two layers:
- On-chain (Solana program): registers nodes/models/jobs/results, manages stake/slashing/rewards, and provides deterministic state transitions (accounts).
- Off-chain (community node runtime): executes inference jobs, manages model shards, signs results, and produces attestations.
2. Repository Layout
chainsignal/
programs/
csap/ # Solana program (Rust/Anchor)
node/
runtime/ # Node daemon (Rust)
adapters/ # RPC/indexers
sdk/
ts/ # TypeScript SDK
rust/ # Rust SDK
api/
openapi.yaml # HTTP gateway (optional)
proto/ # gRPC definitions
docs/
spec.md # Documentation
3. Core Concepts
3.1 Node
A community node is a worker that posts stake, registers on-chain, executes jobs, and signs results.
3.2 Model
Models are registered as commitments:
model_hash (SHA-256 over architecture/weights/tokenizer/config),
shard_manifest_hash,
version_semver.
3.3 Job
A job defines input (e.g., wallet address, time window, featureset), target model, quorum, and fees/rewards.
3.4 Result
Results contain scores/labels, a node signature, and optionally a Proof-of-Inference (PoI) commitment.
4. System Flow
Client (SDK)
| create_job()
v
Solana Program (CSAP)
| job_account created
v
Nodes subscribe (WS/gRPC)
| claim_job()
v
Nodes compute inference
| submit_result()
v
Program aggregates results
| finalize_job()
v
Rewards / Slashing / Registry updates
5. On-Chain Program – Accounts
5.1 GlobalConfig (PDA)
pub struct GlobalConfig {
pub admin: Pubkey,
pub fee_mint: Pubkey,
pub min_node_stake: u64,
pub result_ttl_slots: u64,
pub quorum_default: u8,
pub slashing_bps: u16,
pub bump: u8,
}
5.2 NodeRegistry (PDA)
pub struct NodeRecord {
pub node_pubkey: Pubkey,
pub operator: Pubkey,
pub stake_lamports: u64,
pub endpoint: [u8; 96], // utf8: "grpc://node1..."
pub capabilities: u64, // bitflags: GPU, AVX2, RAM>=32, ...
pub reputation: i64, // signed score
pub last_heartbeat_slot: u64,
pub status: u8, // 0=inactive,1=active,2=jailed
}
5.3 ModelRegistry (PDA)
pub struct ModelRecord {
pub model_id: [u8; 32], // derived from model_hash
pub owner: Pubkey,
pub model_hash: [u8; 32],
pub shard_manifest_hash: [u8; 32],
pub version_semver: [u8; 16],
pub min_quorum: u8,
pub max_latency_ms: u32,
pub status: u8, // 0=deprecated,1=active
}
5.4 InferenceJob (PDA)
pub struct InferenceJob {
pub job_id: [u8; 32],
pub requester: Pubkey,
pub model_id: [u8; 32],
pub created_slot: u64,
pub expires_slot: u64,
pub quorum: u8,
pub max_results: u8,
pub input_hash: [u8; 32], // hash(InputPayload)
pub input_uri: [u8; 96], // optional blob ref
pub status: u8, // 0=open,1=finalizing,2=final,3=expired
pub fee_lamports: u64,
}
5.5 InferenceResult (PDA)
pub struct InferenceResult {
pub job_id: [u8; 32],
pub node_pubkey: Pubkey,
pub submitted_slot: u64,
pub result_hash: [u8; 32], // hash(ResultPayload)
pub result_uri: [u8; 96], // optional blob ref
pub signature: [u8; 64], // ed25519(job_id||result_hash)
pub poi_hash: [u8; 32], // optional PoI commitment
pub status: u8, // 0=submitted,1=accepted,2=rejected
}
6. On-Chain Instructions (ABI)
| Instruction | Purpose | Validation |
|---|---|---|
init_global_config(admin, fee_mint, params) |
Initialize GlobalConfig | Admin authority |
register_node(endpoint, capabilities) |
Register/update node | Stake ≥ min_node_stake |
heartbeat() |
Liveness signal | Node exists |
register_model(model_hash, shard_manifest_hash, version, constraints) |
Register model commitment | Owner signature |
create_job(model_id, input_hash, input_uri, quorum, ttl_slots) |
Create inference job | Model active, quorum valid |
submit_result(job_id, result_hash, result_uri, signature, poi_hash) |
Submit result | Job open, TTL ok, node active, signature ok |
finalize_job(job_id) |
Aggregate + rewards/slashing | Quorum reached, not expired |
|score - median| > threshold.
7. Payload Formats (Off-Chain)
7.1 InputPayload (CBOR / JSON view)
{
"schema": "csap.input.v1",
"kind": "wallet_risk_score",
"wallet": "9xQeWvG816bUx9EP... (base58)",
"chain": "solana",
"window": {"from": 1734300000, "to": 1734386400},
"featureset": "standard_v2",
"options": {"include_explanations": true}
}
7.2 ResultPayload (CBOR / JSON view)
{
"schema": "csap.result.v1",
"job_id": "0x…",
"model_id": "0x…",
"score": 72,
"labels": ["high_velocity", "new_counterparties"],
"explanations": [
{"feature":"tx_burst_15m", "impact": 0.31},
{"feature":"counterparty_entropy", "impact": 0.22}
],
"stats": {"latency_ms": 184, "confidence": 0.78}
}
7.3 Hashing & Signatures
input_hash = sha256(cbor(InputPayload))
result_hash = sha256(cbor(ResultPayload))
sig = ed25519_sign(node_key, job_id || result_hash)
8. Node Runtime
8.1 Components
- Watcher: Solana WS/RPC → new jobs
- Scheduler: claiming, rate-limits, backpressure
- ModelManager: shards, cache, versions
- InferenceEngine: CPU/GPU backend
- Attestor: PoI commitments + signatures
- Submitter: submits results on-chain
8.2 Node Config (node.toml)
[node]
operator_keypair_path = "./keys/operator.json"
node_keypair_path = "./keys/node.json"
endpoint = "grpc://127.0.0.1:9090"
capabilities = 0b0000000000000011 # bit0=CPU_AVX2, bit1=GPU
[solana]
cluster = "mainnet-beta"
rpc_url = "https://api.mainnet-beta.solana.com"
ws_url = "wss://api.mainnet-beta.solana.com"
program_id = "CSAP111111111111111111111111111111111111"
[models]
cache_dir = "./model_cache"
max_gb = 60
allowlist = ["model_id_hex_1", "model_id_hex_2"]
[scheduler]
max_concurrent_jobs = 4
job_poll_ms = 250
result_timeout_ms = 2500
8.3 Startup (CLI example)
csap-node init-keys
csap-node stake --lamports 5000000000
csap-node register --endpoint grpc://node.example:9090 --capabilities GPU,AVX2
csap-node run
9. gRPC API (Node ↔ Client/Gateway)
9.1 Proto sketch
service NodeService {
rpc Health(HealthRequest) returns (HealthReply);
rpc Describe(DescribeRequest) returns (DescribeReply);
rpc SubmitLocalJob(LocalJobRequest) returns (LocalJobReply);
rpc StreamJobs(StreamJobsRequest) returns (stream JobEvent);
}
9.2 Health
Returns version, uptime, loaded models, and queue depth.
10. TypeScript SDK
10.1 Install
pnpm add @chainsignal/csap-sdk
10.2 Create a job + read final result
import { CSAPClient } from "@chainsignal/csap-sdk";
const csap = new CSAPClient({
rpcUrl: "https://api.mainnet-beta.solana.com",
programId: "CSAP111111111111111111111111111111111111",
});
const input = {
schema: "csap.input.v1",
kind: "wallet_risk_score",
wallet: "9xQeWvG816bUx9EP...",
chain: "solana",
window: { from: 1734300000, to: 1734386400 },
featureset: "standard_v2",
options: { include_explanations: true },
};
const { jobId } = await csap.jobs.create({
modelId: "b3f1…",
input,
quorum: 7,
ttlSlots: 900,
});
const final = await csap.jobs.waitFinal(jobId, { timeoutMs: 15000 });
console.log(final.score, final.labels);
11. Error Codes
11.1 On-chain ProgramError
| Code | Name | Meaning |
|---|---|---|
1001 | NodeStakeTooLow | Stake below minimum |
1002 | NodeNotActive | Node inactive/jailed |
1101 | ModelNotActive | Model deprecated/inactive |
1201 | JobExpired | TTL exceeded |
1202 | QuorumInvalid | Quorum out of bounds |
1301 | DuplicateResult | Node already submitted |
1302 | InvalidSignature | Signature verification failed |
1401 | FinalizeNotReady | Quorum not reached |
11.2 Node runtime
| Code | Meaning |
|---|---|
N200 | ModelShardMissing |
N240 | InferenceTimeout |
N260 | RPCBackoff |
12. Security & Integrity
- Signed results: ed25519 over
job_id||result_hash. - Quorum: reduces single-node manipulation.
- Reputation + slashing: penalizes systematic outliers.
- PoI: commitment over
ModelCommitment + InputHash + Nonce + TraceHash. - Privacy: inputs can be referenced by hash + optional blob URI.
13. Governance & Upgrades
GlobalConfig.admincan update parameters viaset_params().- Model owners can deprecate models.
- An upgrade authority can deploy program upgrades (Upgradeable Loader).
- Optional: migrate to DAO governance via timelock + multisig.
14. Minimal Anchor IDL Excerpt
{
"name": "csap",
"instructions": [
{"name":"registerNode","accounts":[...],
"args":[{"name":"endpoint","type":"bytes"},{"name":"capabilities","type":"u64"}]},
{"name":"registerModel","accounts":[...],
"args":[{"name":"modelHash","type":"bytes"},{"name":"manifestHash","type":"bytes"},{"name":"version","type":"bytes"}]},
{"name":"createJob","accounts":[...],
"args":[{"name":"modelId","type":"bytes"},{"name":"inputHash","type":"bytes"},{"name":"inputUri","type":"bytes"},{"name":"quorum","type":"u8"},{"name":"ttlSlots","type":"u64"}]},
{"name":"submitResult","accounts":[...],
"args":[{"name":"jobId","type":"bytes"},{"name":"resultHash","type":"bytes"},{"name":"resultUri","type":"bytes"},
{"name":"signature","type":"bytes"},{"name":"poiHash","type":"bytes"}]},
{"name":"finalizeJob","accounts":[...],
"args":[{"name":"jobId","type":"bytes"}]}
]
}