Skip to content

feat(zeph-llm): RequestSigner with k256 + bech32 + Python-derived fixtures #3609

@bug-ops

Description

@bug-ops

Part of epic #3602.

Scope

Implement RequestSigner that produces gonka-compatible signed-request headers, with fixture-driven tests derived from the upstream Python reference (gonka-ai/gonka-openai MIT).

Files to create

  • crates/zeph-llm/src/gonka/signer.rs:
    pub struct RequestSigner { signing_key: k256::ecdsa::SigningKey, address: String }
    impl RequestSigner {
        pub fn from_hex(priv_hex: &str, chain_prefix: &str) -> Result<Self, LlmError>;
        pub fn address(&self) -> &str;
        pub fn sign(&self, body_bytes: &[u8], timestamp_ns: u128, transfer_address: &str)
            -> Result<String, LlmError>;
    }
  • crates/zeph-llm/src/gonka/fixtures.json — at least three signing test vectors generated from running the Python reference against fixed inputs (private key, body, timestamp_ns, transfer_address → expected base64 signature).
  • crates/zeph-llm/src/gonka/tests.rs — fixture-driven tests covering:
    • Address derivation matches Python output.
    • Signature byte-for-byte match for each vector.
    • Hex parsing errors (odd length, invalid chars).
    • Unicode body bytes.

Files to modify

  • crates/zeph-llm/Cargo.toml — add k256 = { version = \"0.13\", default-features = false, features = [\"ecdsa\", \"std\", \"sha256\"] }, ripemd = \"0.1\", bech32 (workspace), hex (workspace), sha2 (workspace).
  • Root Cargo.toml — promote bech32 = \"0.9.1\" to [workspace.dependencies] (currently transitive via age).
  • crates/zeph-llm/src/lib.rspub mod gonka; (only signer + endpoints submodules wired in this PR; provider lands in Implement OllamaProvider using ollama-rs #9).

Algorithm (per upstream Python reference)

  1. payload_hash_hex = lowercase hex(sha256(body_bytes)) (64 chars).
  2. input = payload_hash_hex || timestamp_ns.to_string() || transfer_address UTF-8 bytes.
  3. digest = sha256(input).
  4. sig = signing_key.sign(digest) — k256 0.13 produces deterministic low-S RFC6979.
  5. Return base64::STANDARD.encode(sig.to_bytes()) (64 raw bytes → 88-char base64).

Address derivation: bech32(chain_prefix, ripemd160(sha256(compressed_secp256k1_pubkey))).

Acceptance

  • cargo nextest run -p zeph-llm -E 'test(gonka_signer)' green.
  • All three fixture vectors pass.
  • cargo clippy --workspace --features full -- -D warnings green.
  • New pub items have /// docs with # Examples (per project rule).
  • cargo doc --no-deps -p zeph-llm clean with RUSTDOCFLAGS=\"--deny rustdoc::broken_intra_doc_links\".
  • Block merge until live testnet probe confirms a real gonka node accepts a signature produced by this signer (per Risk M0: Project bootstrap — workspace and crate skeleton #1 in the plan).
  • CHANGELOG.md [Unreleased] updated.

Depends on

None (parallelisable with #5, #6, #8).

Size

M (~4h)

Metadata

Metadata

Assignees

Labels

P2High value, medium complexityenhancementNew feature or requestfeatureNew functionalityllmzeph-llm crate (Ollama, Claude)size/MMedium PR (51-200 lines)

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions